'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).'

';} } private static function preset_control($s) { echo '


'.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__('Site-wide presentation','ai-webmaster-studio').'

'; wp_nonce_field('aiwest_workspace'); echo '';self::preset_control($s); echo '

'.esc_html__('Video plays muted and loops. Visitors can pause motion; reduced-motion preferences disable autoplay. An existing header image remains a fallback.','ai-webmaster-studio').'

'; echo '

'; submit_button(__('Save presentation','ai-webmaster-studio'));echo '
';self::preview($s);echo '
'; } public static function panel($tab) { $s=self::settings();self::notice(); echo '

'.esc_html($tab==='motion'?__('3D & motion','ai-webmaster-studio'):($tab==='blog'?__('Blog','ai-webmaster-studio'):__('HTTP redirects','ai-webmaster-studio'))).'

';wp_nonce_field('aiwest_workspace'); echo ''; if ($tab==='motion') { echo '
';self::preset_control($s); echo '



'.esc_html__('3D perspective follows the pointer on content cards. Backgrounds move without interaction when motion is enabled. On touch devices cards keep static depth. Browser reduced-motion preferences and the visitor pause button stop motion.','ai-webmaster-studio').'

'; submit_button(__('Save style and motion','ai-webmaster-studio'));echo '
';self::preview($s);echo '
'; } elseif ($tab==='redirects') { echo '

'.esc_html__('Send visitors from an old address on this website to the correct destination. Enter both complete URLs, including https:// and every slash. Example: /product-category/glasses/ stays a nested path; it is not a WordPress slug.','ai-webmaster-studio').'

'.esc_html__('301 means a permanent move (normally used when an indexed address changes). Choose 302 for a temporary move. Matching includes the exact path and query string. This changes the HTTP response; it does not rename a page.','ai-webmaster-studio').'

'; self::redirect_fields('new', '', '', 301, false); if ($s['redirects']) echo '

'.esc_html__('Saved redirects — edit addresses or select Remove, then save','ai-webmaster-studio').'

'; $base=wp_parse_url(home_url('/'));$origin=$base['scheme'].'://'.$base['host'].(isset($base['port'])?':'.$base['port']:''); $i=0;foreach ($s['redirects'] as $path=>$rule) self::redirect_fields($i++, $origin.$path, $rule['url'], $rule['status'], true); } else { echo '

'.esc_html__('Choose existing published pages. WordPress uses the active theme archive template for the posts page; page Canvas content does not replace the blog archive.','ai-webmaster-studio').'

'; foreach (['front_page'=>['page_on_front',__('Homepage','ai-webmaster-studio')],'blog_page'=>['page_for_posts',__('Blog posts page','ai-webmaster-studio')]] as $name=>$cfg) { echo '

'; } echo '

'.esc_html__('Write a blog post','ai-webmaster-studio').' · '.esc_html__('Manage categories','ai-webmaster-studio').' · '.esc_html__('Manage posts and publication dates','ai-webmaster-studio').'

'.esc_html__('Create a page','ai-webmaster-studio').' · '.esc_html__('WordPress reading settings','ai-webmaster-studio').'

'; } if ($tab!=='motion') submit_button(__('Save settings','ai-webmaster-studio'));echo '
'; } private static function redirect_fields($key, $source, $target, $status, $existing) { $name='rules['.$key.']'; echo '
'.esc_html($existing?__('Redirect','ai-webmaster-studio'):__('Add a redirect','ai-webmaster-studio')).'

'; if ($existing) echo ' '; echo '

'; } public static function validate_redirect_rows($rows) { if (!is_array($rows) || count($rows)>201) return new WP_Error('rows',__('Invalid redirect form or too many rules.','ai-webmaster-studio')); $lines=[]; foreach ($rows as $row) { if (!is_array($row)) return new WP_Error('row',__('Invalid redirect form or too many rules.','ai-webmaster-studio')); if (!empty($row['delete'])) continue; foreach (['source','destination','status'] as $field) if (isset($row[$field]) && !is_scalar($row[$field])) return new WP_Error('field',__('Invalid redirect form or too many rules.','ai-webmaster-studio')); $source=trim(sanitize_text_field($row['source']??''));$target=trim(sanitize_text_field($row['destination']??'')); if ($source==='' && $target==='') continue; if (!preg_match('~^https?://~i',$source) || strpos($source,'|')!==false || strpos($target,'|')!==false) return new WP_Error('url',__('Enter complete HTTP(S) URLs in both address fields.','ai-webmaster-studio')); $lines[]=$source.' | '.$target.' | '.absint($row['status']??301); } return self::validate_redirects(implode("\n",$lines)); } private static function apply_preset($preset) { if ($preset==='custom') return; $shapes=['rounded'=>['radius'=>'24px','button_radius'=>'999px','layout_template'=>'standard_landing'],'square'=>['radius'=>'0px','button_radius'=>'0px','layout_template'=>'comparison'],'editorial'=>['radius'=>'4px','button_radius'=>'4px','layout_template'=>'minimal']]; if (!isset($shapes[$preset])) return; update_option('aiwest_design_system', array_merge(AIWEST_Design_System::get(),$shapes[$preset]), false); } public static function consume_public_draft() { if (get_option('aiwest_public_ai_drafting','no')!=='yes') return false; $today=gmdate('Ymd');$previous=get_option('aiwest_public_draft_day',''); if ($previous!==$today) { if (preg_match('/^[0-9]{8}$/',(string)$previous)) for($i=0;$i<20;$i++) delete_option('aiwest_public_draft_'.$previous.'_'.$i); update_option('aiwest_public_draft_day',$today,false); } for($i=0;$i<20;$i++) if(add_option('aiwest_public_draft_'.$today.'_'.$i,'used','',false)) return true; return false; } public static function model_cost_panel() { $rates = [ 'gpt-6.1-sol'=>[2,10], 'gpt-6-astra'=>[10,50], 'gpt-5.6-terra'=>[2,12], 'gpt-5.6-luna'=>[0.2,1.2], 'gpt-5.6-sol'=>[4,20], 'gpt-4.1'=>[2,8], 'gpt-4.1-mini'=>[0.4,1.6], 'gpt-4'=>[30,60], ]; echo '

'.esc_html__('Indicative OpenAI API costs','ai-webmaster-studio').'

'.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').'

'; foreach($rates as $model=>$rate)echo ''; echo '
'.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']);