[ 'group' => 'content', 'label' => __('Create new pages', 'ai-webmaster-studio'), 'description' => __('Creating new WordPress pages, for example landing pages, menu pages or new site sections.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'content_post_create' => [ 'group' => 'content', 'label' => __('Create new posts', 'ai-webmaster-studio'), 'description' => __('Creating new articles, guides and blog posts.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'content_page_expand' => [ 'group' => 'content', 'label' => __('Expand existing pages', 'ai-webmaster-studio'), 'description' => __('Expanding existing pages without creating new URLs.', 'ai-webmaster-studio'), 'default' => self::DECISION_ALLOW, ], 'content_post_expand' => [ 'group' => 'content', 'label' => __('Expand existing posts', 'ai-webmaster-studio'), 'description' => __('Expanding existing posts and guides.', 'ai-webmaster-studio'), 'default' => self::DECISION_ALLOW, ], 'content_page_update' => [ 'group' => 'content', 'label' => __('Update existing pages', 'ai-webmaster-studio'), 'description' => __('Updating facts, layout and content elements on existing pages.', 'ai-webmaster-studio'), 'default' => self::DECISION_ALLOW, ], 'content_post_update' => [ 'group' => 'content', 'label' => __('Update existing posts', 'ai-webmaster-studio'), 'description' => __('Updating older posts, guides and articles.', 'ai-webmaster-studio'), 'default' => self::DECISION_ALLOW, ], 'content_archive' => [ 'group' => 'content', 'label' => __('Archive content', 'ai-webmaster-studio'), 'description' => __('Moving outdated content to archive or marking it for review.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'content_delete' => [ 'group' => 'content', 'label' => __('Delete content', 'ai-webmaster-studio'), 'description' => __('Deleting pages, posts or content blocks. Blocked by default.', 'ai-webmaster-studio'), 'default' => self::DECISION_DENY, ], 'internal_links' => [ 'group' => 'site', 'label' => __('Internal links', 'ai-webmaster-studio'), 'description' => __('Adding and fixing links between content items.', 'ai-webmaster-studio'), 'default' => self::DECISION_ALLOW, ], 'external_links' => [ 'group' => 'site', 'label' => __('External links', 'ai-webmaster-studio'), 'description' => __('Adding or updating links that point outside the website.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'seo' => [ 'group' => 'site', 'label' => __('SEO and metadata', 'ai-webmaster-studio'), 'description' => __('SEO analysis, titles, descriptions, headings and metadata.', 'ai-webmaster-studio'), 'default' => self::DECISION_ALLOW, ], 'schema' => [ 'group' => 'site', 'label' => __('Schema / structured data', 'ai-webmaster-studio'), 'description' => __('Adding structured data for pages, posts, products and FAQ.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'faq' => [ 'group' => 'content', 'label' => __('FAQ', 'ai-webmaster-studio'), 'description' => __('Creating and updating FAQ sections.', 'ai-webmaster-studio'), 'default' => self::DECISION_ALLOW, ], 'cta' => [ 'group' => 'content', 'label' => __('CTA', 'ai-webmaster-studio'), 'description' => __('Adding calls to action and buttons.', 'ai-webmaster-studio'), 'default' => self::DECISION_ALLOW, ], 'images' => [ 'group' => 'media', 'label' => __('Images', 'ai-webmaster-studio'), 'description' => __('Generating featured images and section graphics according to the image policy.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'gallery' => [ 'group' => 'media', 'label' => __('Galleries', 'ai-webmaster-studio'), 'description' => __('Creating or updating image galleries.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'directory' => [ 'group' => 'business', 'label' => __('Directory / map', 'ai-webmaster-studio'), 'description' => __('Directory records, map, partners and requests.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'forms' => [ 'group' => 'business', 'label' => __('Forms', 'ai-webmaster-studio'), 'description' => __('Creating and modifying contact, lead and request forms.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_product_create' => [ 'group' => 'commerce', 'label' => __('Create products', 'ai-webmaster-studio'), 'description' => __('Future Commerce Engine: creating products or sales offers.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_product_update' => [ 'group' => 'commerce', 'label' => __('Update products', 'ai-webmaster-studio'), 'description' => __('Updating existing products, offers and sales data.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_descriptions' => [ 'group' => 'commerce', 'label' => __('Product descriptions', 'ai-webmaster-studio'), 'description' => __('Generating and expanding product descriptions.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_seo' => [ 'group' => 'commerce', 'label' => __('Product SEO', 'ai-webmaster-studio'), 'description' => __('Optimizing product titles, descriptions and SEO data.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_faq' => [ 'group' => 'commerce', 'label' => __('Product FAQ', 'ai-webmaster-studio'), 'description' => __('Creating questions and answers for products.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_gallery' => [ 'group' => 'commerce', 'label' => __('Product galleries', 'ai-webmaster-studio'), 'description' => __('Creating and organizing product galleries.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_taxonomy' => [ 'group' => 'commerce', 'label' => __('Product categories and tags', 'ai-webmaster-studio'), 'description' => __('Creating and organizing product categories, tags and attributes.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_links' => [ 'group' => 'commerce', 'label' => __('Product linking', 'ai-webmaster-studio'), 'description' => __('Linking products with guides, categories and other products.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'commerce_cta' => [ 'group' => 'commerce', 'label' => __('Product CTA', 'ai-webmaster-studio'), 'description' => __('Adding buttons and calls to action around products.', 'ai-webmaster-studio'), 'default' => self::DECISION_APPROVE, ], 'header' => [ 'group' => 'structure', 'label' => __('Header', 'ai-webmaster-studio'), 'description' => __('Structural site header. Protected by default.', 'ai-webmaster-studio'), 'default' => self::DECISION_DENY, ], 'footer' => [ 'group' => 'structure', 'label' => __('Footer', 'ai-webmaster-studio'), 'description' => __('Structural site footer. Protected by default.', 'ai-webmaster-studio'), 'default' => self::DECISION_DENY, ], 'menu' => [ 'group' => 'structure', 'label' => __('Menu', 'ai-webmaster-studio'), 'description' => __('Main site navigation. Protected by default.', 'ai-webmaster-studio'), 'default' => self::DECISION_DENY, ], 'system_pages' => [ 'group' => 'structure', 'label' => __('System pages', 'ai-webmaster-studio'), 'description' => __('Technical, legal, test and structural pages.', 'ai-webmaster-studio'), 'default' => self::DECISION_DENY, ], ]; } public static function groups() { return [ 'content' => ['label' => __('Content', 'ai-webmaster-studio')], 'site' => ['label' => __('SEO and linking', 'ai-webmaster-studio')], 'media' => ['label' => __('Media and images', 'ai-webmaster-studio')], 'business' => ['label' => __('Directory, forms and leads', 'ai-webmaster-studio')], 'commerce' => ['label' => __('Commerce / products', 'ai-webmaster-studio')], 'structure' => ['label' => __('Site structure', 'ai-webmaster-studio')], ]; } public static function image_styles() { return [ 'photo' => ['label' => __('Photo', 'ai-webmaster-studio')], 'render' => ['label' => __('Render', 'ai-webmaster-studio')], 'illustration' => ['label' => __('Illustration', 'ai-webmaster-studio')], 'diagram' => ['label' => __('Diagram', 'ai-webmaster-studio')], 'infographic' => ['label' => __('Infographic', 'ai-webmaster-studio')], 'icon' => ['label' => __('Icon / symbol', 'ai-webmaster-studio')], ]; } public static function image_positions() { return [ 'above' => ['label' => __('Above text', 'ai-webmaster-studio')], 'below' => ['label' => __('Below text', 'ai-webmaster-studio')], 'left' => ['label' => __('Left column', 'ai-webmaster-studio')], 'right' => ['label' => __('Right column', 'ai-webmaster-studio')], 'background' => ['label' => __('Section background', 'ai-webmaster-studio')], ]; } public static function image_sizes() { return [ 'thumbnail' => ['label' => __('Thumbnail', 'ai-webmaster-studio')], 'small' => ['label' => __('Small', 'ai-webmaster-studio')], 'medium' => ['label' => __('Medium', 'ai-webmaster-studio')], 'large' => ['label' => __('Large', 'ai-webmaster-studio')], 'full' => ['label' => __('Full width', 'ai-webmaster-studio')], ]; } public static function profiles() { return [ 'safe' => ['label' => __('Safe', 'ai-webmaster-studio'), 'description' => __('AI mainly analyzes. Changes require a separate administrator decision.', 'ai-webmaster-studio')], 'standard' => ['label' => __('Standard', 'ai-webmaster-studio'), 'description' => __('Within a started task, AI can perform safe content and link changes while protecting site structure.', 'ai-webmaster-studio')], 'advanced' => ['label' => __('Advanced', 'ai-webmaster-studio'), 'description' => __('A broader scope for administrator-started operations; structural areas still require approval.', 'ai-webmaster-studio')], 'expert' => ['label' => __('Expert', 'ai-webmaster-studio'), 'description' => __('The broadest permission scope for administrator-started tasks.', 'ai-webmaster-studio')], 'custom' => ['label' => __('Custom', 'ai-webmaster-studio'), 'description' => __('Administrator manually defines every area policy.', 'ai-webmaster-studio')], ]; } public static function decisions() { return [ self::DECISION_ALLOW => ['label' => __('Execute within a started task', 'ai-webmaster-studio')], self::DECISION_APPROVE => ['label' => __('Require approval', 'ai-webmaster-studio')], self::DECISION_REPORT => ['label' => __('Analyze only', 'ai-webmaster-studio')], self::DECISION_DENY => ['label' => __('Never change', 'ai-webmaster-studio')], ]; } public static function change_scales() { return [ 'small' => ['label' => __('Small', 'ai-webmaster-studio'), 'max_actions' => 5], 'medium' => ['label' => __('Medium', 'ai-webmaster-studio'), 'max_actions' => 10], 'large' => ['label' => __('Large', 'ai-webmaster-studio'), 'max_actions' => 20], 'unlimited' => ['label' => __('Unlimited', 'ai-webmaster-studio'), 'max_actions' => 999], ]; } public static function defaults() { $modules = []; foreach (self::modules() as $key => $module) $modules[$key] = $module['default']; return [ 'profile' => 'standard', 'change_scale' => 'small', 'modules' => $modules, 'image_policy' => [ 'style' => 'photo', 'position' => 'right', 'size' => 'medium', 'max_count' => 1, 'featured' => 'yes', 'section' => 'yes', 'gallery' => 'no', ], 'xai_enabled' => 'yes', 'approval_queue_ready' => 'yes', ]; } public static function get_settings() { $settings = get_option(self::OPTION, []); if (!is_array($settings)) $settings = []; return self::normalize_settings(array_replace_recursive(self::defaults(), $settings)); } public static function normalize_settings($settings) { $defaults = self::defaults(); if (!is_array($settings)) $settings = []; $settings = array_replace_recursive($defaults, $settings); if (!in_array($settings['profile'], array_keys(self::profiles()), true)) $settings['profile'] = 'standard'; if (!in_array($settings['change_scale'], array_keys(self::change_scales()), true)) $settings['change_scale'] = 'small'; $valid_decisions = array_keys(self::decisions()); foreach (self::modules() as $key => $module) { if (!isset($settings['modules'][$key]) || !in_array($settings['modules'][$key], $valid_decisions, true)) $settings['modules'][$key] = $module['default']; } // Backward compatibility: migrate older broad policy keys into the new granular model where useful. if (isset($settings['modules']['content']) && in_array($settings['modules']['content'], $valid_decisions, true)) { foreach (['content_page_expand','content_post_expand','content_page_update','content_post_update'] as $k) { if (empty(get_option(self::OPTION, [])) || !isset(get_option(self::OPTION, [])['modules'][$k])) $settings['modules'][$k] = $settings['modules']['content']; } } $image = isset($settings['image_policy']) && is_array($settings['image_policy']) ? $settings['image_policy'] : []; $settings['image_policy']['style'] = in_array($image['style'] ?? 'photo', array_keys(self::image_styles()), true) ? $image['style'] : 'photo'; $settings['image_policy']['position'] = in_array($image['position'] ?? 'right', array_keys(self::image_positions()), true) ? $image['position'] : 'right'; $settings['image_policy']['size'] = in_array($image['size'] ?? 'medium', array_keys(self::image_sizes()), true) ? $image['size'] : 'medium'; $settings['image_policy']['max_count'] = max(1, min(5, intval($image['max_count'] ?? 1))); foreach (['featured','section','gallery'] as $flag) $settings['image_policy'][$flag] = (($image[$flag] ?? 'no') === 'yes') ? 'yes' : 'no'; $settings['xai_enabled'] = ($settings['xai_enabled'] ?? 'yes') === 'yes' ? 'yes' : 'no'; $settings['approval_queue_ready'] = 'yes'; return $settings; } public static function save_settings($input) { $settings = self::get_settings(); $settings['profile'] = sanitize_key($input['profile'] ?? $settings['profile']); $settings['change_scale'] = sanitize_key($input['change_scale'] ?? $settings['change_scale']); $settings['xai_enabled'] = !empty($input['xai_enabled']) ? 'yes' : 'no'; if ($settings['profile'] !== 'custom') $settings = self::apply_profile($settings, $settings['profile']); $valid_decisions = array_keys(self::decisions()); $posted_modules = isset($input['modules']) && is_array($input['modules']) ? $input['modules'] : []; foreach (self::modules() as $key => $module) { $value = sanitize_key($posted_modules[$key] ?? ($settings['modules'][$key] ?? $module['default'])); $settings['modules'][$key] = in_array($value, $valid_decisions, true) ? $value : $module['default']; } $image = isset($input['image_policy']) && is_array($input['image_policy']) ? $input['image_policy'] : []; $settings['image_policy'] = [ 'style' => sanitize_key($image['style'] ?? $settings['image_policy']['style']), 'position' => sanitize_key($image['position'] ?? $settings['image_policy']['position']), 'size' => sanitize_key($image['size'] ?? $settings['image_policy']['size']), 'max_count' => intval($image['max_count'] ?? $settings['image_policy']['max_count']), 'featured' => !empty($image['featured']) ? 'yes' : 'no', 'section' => !empty($image['section']) ? 'yes' : 'no', 'gallery' => !empty($image['gallery']) ? 'yes' : 'no', ]; $settings = self::normalize_settings($settings); update_option(self::OPTION, $settings, false); self::sync_legacy_options($settings); return $settings; } public static function apply_profile($settings, $profile) { $settings = self::normalize_settings($settings); $all = array_keys(self::modules()); switch ($profile) { case 'safe': foreach ($all as $key) $settings['modules'][$key] = self::DECISION_REPORT; foreach (['header','footer','menu','system_pages','content_delete'] as $key) $settings['modules'][$key] = self::DECISION_DENY; $settings['change_scale'] = 'small'; break; case 'standard': foreach ($all as $key) $settings['modules'][$key] = self::modules()[$key]['default']; foreach (['content_page_expand','content_post_expand','content_page_update','content_post_update','internal_links','seo','faq','cta'] as $key) $settings['modules'][$key] = self::DECISION_ALLOW; foreach (['content_page_create','content_post_create','images','directory','forms'] as $key) $settings['modules'][$key] = self::DECISION_APPROVE; foreach (['header','footer','menu','system_pages','content_delete'] as $key) $settings['modules'][$key] = self::DECISION_DENY; $settings['change_scale'] = 'small'; break; case 'advanced': foreach ($all as $key) $settings['modules'][$key] = self::DECISION_ALLOW; foreach (['header','footer','menu','images','directory','forms','commerce_product_create','commerce_product_update'] as $key) $settings['modules'][$key] = self::DECISION_APPROVE; foreach (['system_pages','content_delete'] as $key) $settings['modules'][$key] = self::DECISION_DENY; $settings['change_scale'] = 'medium'; break; case 'expert': foreach ($all as $key) $settings['modules'][$key] = self::DECISION_ALLOW; $settings['modules']['system_pages'] = self::DECISION_APPROVE; $settings['modules']['content_delete'] = self::DECISION_APPROVE; $settings['change_scale'] = 'large'; break; } return self::normalize_settings($settings); } public static function sync_legacy_options($settings = null) { $settings = $settings ? self::normalize_settings($settings) : self::get_settings(); update_option('aiwest_policy_image_style', $settings['image_policy']['style'], false); update_option('aiwest_policy_image_position', $settings['image_policy']['position'], false); update_option('aiwest_policy_image_size', $settings['image_policy']['size'], false); update_option('aiwest_policy_image_max_count', intval($settings['image_policy']['max_count']), false); } public static function is_protected($module, $settings = null) { $settings = $settings ? self::normalize_settings($settings) : self::get_settings(); $decision = $settings['modules'][$module] ?? self::DECISION_DENY; return in_array($decision, [self::DECISION_DENY, self::DECISION_REPORT, self::DECISION_APPROVE], true); } public static function module_decision($module) { $settings = self::get_settings(); return $settings['modules'][$module] ?? self::DECISION_DENY; } public static function evaluate($operation, $module = null, $context = []) { $module = $module ?: self::module_from_operation($operation, $context); $decision = self::module_decision($module); $allowed = $decision === self::DECISION_ALLOW; $needs_approval = $decision === self::DECISION_APPROVE; $report_only = $decision === self::DECISION_REPORT; $denied = $decision === self::DECISION_DENY; return [ 'operation' => $operation, 'module' => $module, 'decision' => $decision, 'allowed' => $allowed, 'needs_approval' => $needs_approval, 'report_only' => $report_only, 'denied' => $denied, 'reason' => self::decision_reason($operation, $module, $decision, $context), ]; } public static function can_execute($type, $module = null, $context = []) { $result = self::evaluate($type, $module, $context); return !empty($result['allowed']); } public static function should_queue_approval($type, $module = null, $context = []) { $result = self::evaluate($type, $module, $context); return !empty($result['needs_approval']); } public static function module_from_operation($operation, $context = []) { $operation = strtolower((string)$operation); $post_type = strtolower((string)($context['post_type'] ?? '')); if (strpos($operation, 'header') !== false) return 'header'; if (strpos($operation, 'footer') !== false) return 'footer'; if (strpos($operation, 'menu') !== false) return 'menu'; if (strpos($operation, 'delete') !== false || strpos($operation, 'remove_content') !== false) return 'content_delete'; if (strpos($operation, 'archive') !== false) return 'content_archive'; if (strpos($operation, 'create_page') !== false || (strpos($operation, 'create') !== false && $post_type === 'page')) return 'content_page_create'; if (strpos($operation, 'create_post') !== false || (strpos($operation, 'create') !== false && $post_type === 'post')) return 'content_post_create'; if (strpos($operation, 'expand_page') !== false || (strpos($operation, 'expand') !== false && $post_type === 'page')) return 'content_page_expand'; if (strpos($operation, 'expand_post') !== false || (strpos($operation, 'expand') !== false && $post_type === 'post')) return 'content_post_expand'; if (strpos($operation, 'update_page') !== false || (strpos($operation, 'update') !== false && $post_type === 'page')) return 'content_page_update'; if (strpos($operation, 'update_post') !== false || (strpos($operation, 'update') !== false && $post_type === 'post')) return 'content_post_update'; if (strpos($operation, 'product') !== false || strpos($operation, 'commerce') !== false) { if (strpos($operation, 'create') !== false) return 'commerce_product_create'; if (strpos($operation, 'seo') !== false) return 'commerce_seo'; if (strpos($operation, 'faq') !== false) return 'commerce_faq'; if (strpos($operation, 'gallery') !== false || strpos($operation, 'image') !== false) return 'commerce_gallery'; if (strpos($operation, 'link') !== false) return 'commerce_links'; if (strpos($operation, 'cta') !== false) return 'commerce_cta'; if (strpos($operation, 'description') !== false || strpos($operation, 'desc') !== false) return 'commerce_descriptions'; return 'commerce_product_update'; } if (strpos($operation, 'category') !== false || strpos($operation, 'tag') !== false || strpos($operation, 'attribute') !== false) return 'commerce_taxonomy'; if (strpos($operation, 'directory') !== false || strpos($operation, 'map') !== false || strpos($operation, 'lead') !== false || strpos($operation, 'partner') !== false) return 'directory'; if (strpos($operation, 'form') !== false) return 'forms'; if (strpos($operation, 'external_link') !== false) return 'external_links'; if (strpos($operation, 'link') !== false) return 'internal_links'; if (strpos($operation, 'schema') !== false || strpos($operation, 'structured') !== false) return 'schema'; if (strpos($operation, 'seo') !== false || strpos($operation, 'meta') !== false) return 'seo'; if (strpos($operation, 'faq') !== false) return 'faq'; if (strpos($operation, 'cta') !== false) return 'cta'; if (strpos($operation, 'gallery') !== false) return 'gallery'; if (strpos($operation, 'image') !== false || strpos($operation, 'graphic') !== false) return 'images'; if (strpos($operation, 'system') !== false) return 'system_pages'; return $post_type === 'post' ? 'content_post_update' : 'content_page_update'; } public static function decision_reason($operation, $module, $decision, $context = []) { $modules = self::modules(); $decisions = self::decisions(); $module_label = self::label($modules[$module] ?? ['label' => $module]); $decision_label = self::label($decisions[$decision] ?? ['label' => $decision]); $settings = self::get_settings(); $profiles = self::profiles(); $profile_label = self::label($profiles[$settings['profile']] ?? ['label' => $settings['profile']]); return sprintf( /* translators: 1: Operation name. 2: Policy area. 3: Policy profile. 4: Policy decision. */ __('Operation "%1$s" belongs to the "%2$s" area. Policy profile "%3$s": %4$s.', 'ai-webmaster-studio'), $operation, $module_label, $profile_label, $decision_label ); } public static function label($item) { return (string) ($item['label'] ?? ''); } public static function description($item) { return (string) ($item['description'] ?? ''); } public static function classify($type) { $result = self::evaluate($type); if (!empty($result['allowed'])) return 'safe'; if (!empty($result['needs_approval']) || !empty($result['report_only'])) return 'review'; return 'critical'; } public static function max_actions_for_scale($settings = null) { $settings = $settings ? self::normalize_settings($settings) : self::get_settings(); $scales = self::change_scales(); return intval($scales[$settings['change_scale']]['max_actions'] ?? 5); } }