'custom','motion'=>'off','shared_colors'=>'no','header_video'=>'','redirects'=>[],'motion_background'=>'orbs','motion_strength'=>7,'menu_motion'=>'yes']); } public static function admin_assets() { if (!current_user_can('manage_options')) return; $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only asset routing. if (strpos($page, 'aiwest-') !== 0) return; wp_enqueue_media(); wp_enqueue_style('wp-color-picker'); wp_enqueue_style('aiwest-presentation', AIWEST_URL.'assets/site-workspace.css', [], AIWEST_VERSION); wp_enqueue_style('aiwest-presentation-admin', AIWEST_URL.'assets/site-workspace-admin.css', [], AIWEST_VERSION); wp_enqueue_script('aiwest-presentation-engine', AIWEST_URL.'assets/presentation-engine.js', [], AIWEST_VERSION, true); wp_enqueue_script('aiwest-workspace', AIWEST_URL.'assets/site-workspace-admin.js', ['jquery','wp-color-picker','aiwest-presentation-engine'], AIWEST_VERSION, true); wp_localize_script('aiwest-workspace', 'AIWESTWorkspace', ['ajax'=>admin_url('admin-ajax.php'),'nonce'=>wp_create_nonce('aiwest_workspace'),'working'=>__('Working…','ai-webmaster-studio'),'design'=>self::presentation_config(),'error'=>__('The change could not be saved.','ai-webmaster-studio')]); } public static function canvas() { check_ajax_referer('aiwest_workspace', 'nonce'); $id = absint($_POST['post_id'] ?? 0); if (!current_user_can('manage_options') || !current_user_can('edit_post',$id) || get_post_type($id)!=='page') wp_send_json_error(['message'=>__('Missing permission or invalid page.','ai-webmaster-studio')],403); $enabled = !empty($_POST['enabled']); $result = $enabled ? AIWEST_Canvas::enable_for_page($id) : AIWEST_Canvas::disable_for_page($id); if (!$result) wp_send_json_error(['message'=>__('Canvas could not be changed.','ai-webmaster-studio')]); clean_post_cache($id); wp_send_json_success(['enabled'=>$enabled,'label'=>$enabled?__('Disable Canvas','ai-webmaster-studio'):__('Enable Canvas','ai-webmaster-studio')]); } public static function canvas_button($id) { if (get_post_type($id)!=='page') return; $enabled = AIWEST_Canvas::is_canvas_page($id); echo ''; } /** Exact same-origin source URLs. Queries are matched exactly; fragments never identify HTTP resources. */ public static function source_path($value) { $base = wp_parse_url(home_url('/')); $p = wp_parse_url(trim((string)$value)); if (!$p || !empty($p['fragment']) || !empty($p['user']) || !empty($p['pass'])) return ''; if (!empty($p['host']) && (strtolower($p['host'])!==strtolower($base['host']) || ($p['port']??null)!==($base['port']??null))) return ''; if (!empty($p['scheme']) && !in_array(strtolower($p['scheme']),['http','https'],true)) return ''; $path = $p['path']??'/'; if (strpos($path,'/')!==0 || strpos($path,'//')===0 || preg_match('/[\x00-\x20\\\\]/',$value)) return ''; return $path.(isset($p['query'])?'?'.$p['query']:''); } public static function validate_redirects($text) { $rules=[]; foreach (array_filter(preg_split('/\r?\n/',trim((string)$text))) as $line) { if (count($rules)>=200) return new WP_Error('limit',__('Use no more than 200 redirect rules.','ai-webmaster-studio')); $parts=array_map('trim',explode('|',$line)); $source=self::source_path($parts[0]??''); $destination=esc_url_raw($parts[1]??'', ['http','https']); $target=wp_parse_url($destination); $code=absint($parts[2]??301); if (!$source || !$target || empty($target['host']) || !empty($target['user']) || !empty($target['pass']) || !in_array($code,[301,302,307,308],true)) return new WP_Error('invalid',__('Each rule requires a local source URL, a full HTTP(S) destination URL and status 301, 302, 307 or 308.','ai-webmaster-studio')); if (self::source_path($destination)===$source || isset($rules[$source])) return new WP_Error('loop',__('Duplicate sources and redirects to themselves are not allowed.','ai-webmaster-studio')); $rules[$source]=['url'=>$destination,'status'=>$code]; } foreach ($rules as $start=>$rule) { $seen=[$start=>true];$next=self::source_path($rule['url']); while ($next && isset($rules[$next])) { if (isset($seen[$next])) return new WP_Error('cycle',__('A redirect cycle was detected.','ai-webmaster-studio')); $seen[$next]=true;$next=self::source_path($rules[$next]['url']); } } return $rules; } public static function redirect() { if (is_admin() || wp_doing_ajax() || is_preview() || is_feed() || (defined('REST_REQUEST') && REST_REQUEST)) return; $method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : ''; if (!in_array($method,['GET','HEAD'],true)) return; $uri=isset($_SERVER['REQUEST_URI'])?wp_unslash($_SERVER['REQUEST_URI']):''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated as an exact local path below; never rendered. $source=self::source_path($uri);$rules=self::settings()['redirects']; if (!$source || !isset($rules[$source])) return; $rule=$rules[$source]; // Destination is explicitly configured by a manage_options user, never taken from the visitor request. if (wp_redirect($rule['url'], $rule['status'], 'AI Webmaster Studio')) exit; // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Explicit administrator-owned external destination. } public static function save() { if (!isset($_POST['aiwest_workspace_action'])) return; if (!current_user_can('manage_options')) wp_die(esc_html__('Missing permission.','ai-webmaster-studio')); check_admin_referer('aiwest_workspace'); $action=sanitize_key(wp_unslash($_POST['aiwest_workspace_action'])); $s=self::settings();$error=null; if ($action==='appearance') { $preset=sanitize_key(wp_unslash($_POST['preset']??'custom')); if (!in_array($preset,['custom','rounded','square','editorial'],true)) $preset='custom'; $s['preset']=$preset;update_option('aiwest_public_ai_drafting',empty($_POST['public_ai_drafting'])?'no':'yes',false);$s['shared_colors']=empty($_POST['shared_colors'])?'no':'yes'; $s['header_video']=esc_url_raw(wp_unslash($_POST['header_video']??'')); $ext=strtolower(pathinfo(wp_parse_url($s['header_video'],PHP_URL_PATH)?:'',PATHINFO_EXTENSION)); if ($s['header_video'] && !in_array($ext,['mp4','webm','ogv'],true)) $error=new WP_Error('video',__('Use an MP4, WebM or OGV media URL.','ai-webmaster-studio')); } elseif ($action==='motion') { $mode=sanitize_key(wp_unslash($_POST['motion']??'off')); $s['motion']=in_array($mode,['off','subtle','depth'],true)?$mode:'off'; $preset=sanitize_key(wp_unslash($_POST['preset']??$s['preset'])); $s['preset']=in_array($preset,['custom','rounded','square','editorial'],true)?$preset:'custom'; $bg=sanitize_key(wp_unslash($_POST['motion_background']??'orbs')); $s['motion_background']=in_array($bg,['none','orbs','gradient'],true)?$bg:'orbs'; $s['motion_strength']=max(2,min(12,absint($_POST['motion_strength']??7))); $s['menu_motion']=empty($_POST['menu_motion'])?'no':'yes'; } elseif ($action==='redirects') { $rules=isset($_POST['rules_present']) ? self::validate_redirect_rows(wp_unslash($_POST['rules']??[])) : self::validate_redirects(sanitize_textarea_field(wp_unslash($_POST['redirects']??''))); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is sanitized and validated in validate_redirect_rows. if (is_wp_error($rules)) $error=$rules; else $s['redirects']=$rules; } elseif ($action==='blog') { $blog=absint($_POST['blog_page']??0);$front=absint($_POST['front_page']??0); if (!$front || !$blog || $front===$blog || get_post_type($front)!=='page' || get_post_type($blog)!=='page' || get_post_status($front)!=='publish' || get_post_status($blog)!=='publish') $error=new WP_Error('blog',__('Choose two different published pages for the homepage and blog.','ai-webmaster-studio')); else { update_option('page_on_front',$front);update_option('page_for_posts',$blog);update_option('show_on_front','page'); update_option('posts_per_page',max(1,min(50,absint($_POST['posts_per_page']??10)))); update_option('rss_use_excerpt',empty($_POST['rss_excerpt'])?0:1); update_option('default_comment_status',empty($_POST['comments'])?'closed':'open'); } } if (!$error && in_array($action,['appearance','motion'],true)) self::apply_preset($s['preset']); if (!$error) {update_option(self::OPTION,$s,false);} if (!$error) self::invalidate_presentation_cache(); set_transient('aiwest_workspace_notice_'.get_current_user_id(),$error?$error->get_error_message():__('Settings saved.','ai-webmaster-studio'),60); $tab=['appearance'=>'style','motion'=>'motion','redirects'=>'redirects','blog'=>'blog'][$action]??'home'; wp_safe_redirect(add_query_arg(['page'=>'aiwest-site-builder','tab'=>$tab],admin_url('admin.php')));exit; } public static function notice() { $key='aiwest_workspace_notice_'.get_current_user_id();$message=get_transient($key); if ($message) {delete_transient($key);echo '
'.esc_html($message).'
'.esc_html__('Shapes and spacing apply to existing AI content and standard WordPress buttons. The layout template guides newly generated content; existing sections are not reordered.','ai-webmaster-studio').'
'; } private static function preview($s) { echo ''; } public static function appearance_panel() { $s=self::settings();self::notice(); echo ''.esc_html__('USD per 1 million input / output tokens, Standard short-context rates reviewed 2026-10-15. Example: 2,000 input + 1,000 output tokens. Estimates exclude caching, tools, long-context, image and regional charges. Reasoning tokens can increase output usage. Access depends on your API project.','ai-webmaster-studio').'
| '.esc_html__('Model','ai-webmaster-studio').' | '.esc_html__('Input USD / 1M','ai-webmaster-studio').' | '.esc_html__('Output USD / 1M','ai-webmaster-studio').' | '.esc_html__('Example USD / request','ai-webmaster-studio').' |
|---|---|---|---|
| '.esc_html($model).' | '.esc_html(number_format($rate[0],2)).' | '.esc_html(number_format($rate[1],2)).' | '.esc_html(number_format(($rate[0]*2000+$rate[1]*1000)/1000000,4)).' |
'.esc_html__('Verify current OpenAI pricing','ai-webmaster-studio').'
'; } /** Only public presentation values leave this boundary. */ public static function presentation_config() { $s=self::settings();$d=AIWEST_Design_System::get();$vars=[]; foreach (['primary','primary_dark','accent','ink','surface','border','soft','radius','button_radius'] as $key) { $value=AIWEST_Design_System::sanitize_value($key,$d[$key]??''); if ($value!=='') $vars['--aiwsp-'.str_replace('_','-',$key)]=$value; } return ['preset'=>$s['preset'],'motion'=>$s['motion'],'background'=>$s['motion_background'],'strength'=>absint($s['motion_strength']),'menu'=>$s['menu_motion'],'vars'=>$vars]; } public static function public_presentation() { nocache_headers();wp_send_json_success(self::presentation_config()); } public static function body_classes($classes) { $s=self::settings();$classes[]='aiwest-motion-'.$s['motion'];$classes[]='aiwsp-site';return $classes; } public static function front_assets() { $s=self::settings(); wp_enqueue_style('aiwest-workspace',AIWEST_URL.'assets/site-workspace.css',[],AIWEST_VERSION); if ($s['shared_colors']==='yes') { $css=''; foreach (['header','footer'] as $area) $css.=':is(.aiw-shortcode-'.$area.',.aiwest-shortcode-'.$area.') :is(section,nav,.aiw-card,.aiwest-card){background:var(--aiwsp-surface)!important;color:var(--aiwsp-ink)!important;border-color:var(--aiwsp-border)!important}'; $css.=':is(.aiw-shortcode,.aiwest-shortcode) :is(a,button){color:var(--aiwsp-primary)}:is(.aiw-shortcode,.aiwest-shortcode) :is(.aiw-btn,.aiwest-btn){background:var(--aiwsp-primary)!important;color:var(--aiwsp-surface)!important}'; wp_add_inline_style('aiwest-workspace',$css); } // Always bootstrap so cached HTML also receives current public presentation. wp_enqueue_script('aiwest-presentation-engine',AIWEST_URL.'assets/presentation-engine.js',[],AIWEST_VERSION,true); wp_enqueue_script('aiwest-motion',AIWEST_URL.'assets/site-workspace-front.js',['aiwest-presentation-engine'],AIWEST_VERSION,true); wp_localize_script('aiwest-motion','AIWESTPresentation',['endpoint'=>add_query_arg('action','aiwest_presentation',admin_url('admin-ajax.php')),'initial'=>self::presentation_config(),'pause'=>__('Pause site animation','ai-webmaster-studio'),'play'=>__('Resume site animation','ai-webmaster-studio')]); } public static function video_markup() { $url=self::settings()['header_video']; if (!$url) { $legacy=get_option('aiwest_header_bg_url','');$ext=strtolower(pathinfo(wp_parse_url($legacy,PHP_URL_PATH)?:'',PATHINFO_EXTENSION));if(in_array($ext,['mp4','webm','ogv'],true)) $url=$legacy; } if (!$url) return ''; $poster=get_option('aiwest_header_bg_url',''); if (preg_match('/\.(mp4|webm|ogv)(?:\?|$)/i',$poster)) $poster=''; return ''; } } AIWEST_Site_Workspace::init(); add_filter('body_class', ['AIWEST_Site_Workspace','body_classes']);