self::SLUG, 'step' => $step]), admin_url('admin.php')); } /** * Whether Home should offer the wizard to a site set up before 2.0, once. */ public static function offerReview(): bool { return !get_option(self::DONE_OPTION) && Setup::isConfigured(); } /** * @return list */ private static function goals(): array { $state = get_option(self::STATE_OPTION, []); if (is_array($state) && isset($state['goals']) && is_array($state['goals'])) { return array_values(array_map('strval', $state['goals'])); } // First visit: what is on already, so a review starts from the site as it is. $s = get_plugin_settings(); $out = []; foreach (['seo' => 'backfill_enabled', 'related' => 'related_enabled', 'search' => 'search_enabled', 'assistant' => 'chat_enabled', 'links' => 'links_enabled'] as $goal => $key) { if (!empty($s[$key])) { $out[] = $goal; } } return $out; } private static function current(): string { $step = isset($_GET['step']) ? sanitize_key(wp_unslash((string) $_GET['step'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (in_array($step, self::STEPS, true)) { return $step; } return Setup::isConfigured() ? 'goals' : 'welcome'; } // ================================================================ page public static function render(): void { if (!current_user_can('manage_options')) { return; } $step = self::current(); $labels = [ 'welcome' => __('Welcome', 'aipcf-ai-provider-for-cloudflare'), 'connect' => __('Connect', 'aipcf-ai-provider-for-cloudflare'), 'goals' => __('What you want', 'aipcf-ai-provider-for-cloudflare'), 'index' => __('Content index', 'aipcf-ai-provider-for-cloudflare'), 'settings' => __('Settings', 'aipcf-ai-provider-for-cloudflare'), 'done' => __('Done', 'aipcf-ai-provider-for-cloudflare'), ]; // Only the steps the chosen goals go through (and the one open now). $steps = array_values(array_filter(self::STEPS, static fn(string $s): bool => $s === $step || in_array($s, self::stepsFor(self::goals()), true))); echo '
'; echo Ui::header(__('Setup', 'aipcf-ai-provider-for-cloudflare'), __('A few steps from nothing to a working site. You can leave and come back.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Ui escapes echo '
'; echo '
'; echo Ui::stepper(array_map(static fn(string $s): string => $labels[$s], $steps), (int) array_search($step, $steps, true)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Ui escapes $message = get_transient('aipcf_wizard_msg_' . get_current_user_id()); if (is_array($message) && !empty($message['text'])) { delete_transient('aipcf_wizard_msg_' . get_current_user_id()); echo Ui::notice((string) $message['text'], (string) ($message['type'] ?? 'info'), true); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Ui escapes } $html = match ($step) { 'connect' => self::stepConnect(), 'goals' => self::stepGoals(), 'index' => self::stepIndex(), 'settings' => self::stepSettings(), 'done' => self::stepDone(), default => self::stepWelcome(), }; echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Ui escapes echo '
'; } /** * A step's form: posted to admin-post.php, with Back and Skip setup. */ private static function form(string $step, string $body, string $next, string $back = ''): string { $out = '
'; $out .= ''; $out .= wp_nonce_field(self::NONCE, '_wpnonce', true, false); $out .= $body; $out .= '
'; if ($back !== '') { $out .= Ui::link(__('Back', 'aipcf-ai-provider-for-cloudflare'), self::url($back)); } $out .= '' . esc_html__('Skip setup', 'aipcf-ai-provider-for-cloudflare') . ''; return $out . '
'; } private static function stepWelcome(): string { $body = Ui::facts([ [__('In the editor', 'aipcf-ai-provider-for-cloudflare'), __('Excerpts, titles, meta descriptions, alt text and images', 'aipcf-ai-provider-for-cloudflare'), __('With the WordPress AI plugin.', 'aipcf-ai-provider-for-cloudflare')], [__('On your site', 'aipcf-ai-provider-for-cloudflare'), __('A site assistant, related posts and search by meaning', 'aipcf-ai-provider-for-cloudflare'), __('Answered from your own posts.', 'aipcf-ai-provider-for-cloudflare')], [__('What it costs', 'aipcf-ai-provider-for-cloudflare'), __('Nothing for most sites', 'aipcf-ai-provider-for-cloudflare'), __('Cloudflare gives 10,000 neurons a day for free, and a daily budget keeps you inside it.', 'aipcf-ai-provider-for-cloudflare')], [__('Your data', 'aipcf-ai-provider-for-cloudflare'), __('Goes to Cloudflare, with your own account', 'aipcf-ai-provider-for-cloudflare'), __('Only what a feature you switch on needs. Nothing is sent before you connect.', 'aipcf-ai-provider-for-cloudflare')], ]); return Ui::card(__('Welcome to AIPCF', 'aipcf-ai-provider-for-cloudflare'), self::form('welcome', $body, __('Start', 'aipcf-ai-provider-for-cloudflare')), [ 'text' => __('AIPCF brings AI to WordPress on Cloudflare Workers AI. It takes about five minutes to set up.', 'aipcf-ai-provider-for-cloudflare'), ]); } private static function stepConnect(): string { $s = get_plugin_settings(); $body = ''; if (Setup::isConfigured() && class_exists('\\WordPress\\AI\\Connector_Approval\\Approvals_Store') && !is_connector_approved()) { $body .= Ui::notice(__('The WordPress AI plugin asks you to allow AIPCF to use your Cloudflare token.', 'aipcf-ai-provider-for-cloudflare'), 'warn'); $body .= Ui::actions([[ 'label' => __('Allow AIPCF', 'aipcf-ai-provider-for-cloudflare'), 'url' => wp_nonce_url(admin_url('admin-post.php?action=aipcf_wizard_approve'), 'aipcf_wizard_approve'), 'kind' => 'primary', ]]); } $body .= Ui::row(__('Account ID', 'aipcf-ai-provider-for-cloudflare'), Ui::text('account_id', (string) $s['account_id'], 'aipcf-wizard-account', ['placeholder' => 'fc6034fbe3a9c364856c3d380c47dc3e']), [ 'for' => 'aipcf-wizard-account', 'help' => __('The 32-character code on the Workers & Pages page.', 'aipcf-ai-provider-for-cloudflare'), 'link' => [__('Open it', 'aipcf-ai-provider-for-cloudflare'), 'https://dash.cloudflare.com/?to=/:account/workers-and-pages'], ]); if (core_connector_is_available()) { $body .= Ui::row(__('API token', 'aipcf-ai-provider-for-cloudflare'), Ui::status(get_api_key() !== '' ? __('Saved on Settings > Connectors.', 'aipcf-ai-provider-for-cloudflare') : __('Add it on Settings > Connectors, then come back here.', 'aipcf-ai-provider-for-cloudflare')), [ 'link' => [__('Open Connectors', 'aipcf-ai-provider-for-cloudflare'), admin_url('options-connectors.php')], 'more' => self::tokenHelp(), ]); } else { $body .= Ui::row(__('API token', 'aipcf-ai-provider-for-cloudflare'), Ui::secret('api_key', (string) $s['api_key'] !== '', 'aipcf-wizard-token'), [ 'for' => 'aipcf-wizard-token', 'help' => __('From the "Workers AI" template.', 'aipcf-ai-provider-for-cloudflare'), 'link' => [__('Create a token', 'aipcf-ai-provider-for-cloudflare'), 'https://dash.cloudflare.com/profile/api-tokens'], 'more' => self::tokenHelp(), ]); } $body .= Ui::moreSettings(Ui::row(__('AI Gateway', 'aipcf-ai-provider-for-cloudflare'), Ui::text('gateway_id', (string) ($s['gateway_id'] ?? ''), 'aipcf-wizard-gateway'), [ 'for' => 'aipcf-wizard-gateway', 'help' => __('Optional: a gateway name, for caching, logs and analytics in Cloudflare.', 'aipcf-ai-provider-for-cloudflare'), ])); return Ui::card(__('Connect to Cloudflare', 'aipcf-ai-provider-for-cloudflare'), self::form('connect', $body, __('Connect', 'aipcf-ai-provider-for-cloudflare'), 'welcome'), [ 'text' => __('Your own Cloudflare account. No credit card is needed for the free daily allowance.', 'aipcf-ai-provider-for-cloudflare'), ]); } private static function tokenHelp(): string { return '
    ' . '
  1. ' . esc_html__('In Cloudflare, open My Profile > API Tokens and click Create Token.', 'aipcf-ai-provider-for-cloudflare') . '
  2. ' . '
  3. ' . esc_html__('Use the "Workers AI" template.', 'aipcf-ai-provider-for-cloudflare') . '
  4. ' . '
  5. ' . esc_html__('For the content index on Cloudflare, also add Account > Vectorize > Edit.', 'aipcf-ai-provider-for-cloudflare') . '
  6. ' . '
  7. ' . esc_html__('Create it, copy it (it is shown once) and paste it here.', 'aipcf-ai-provider-for-cloudflare') . '
  8. ' . '
'; } private static function stepGoals(): string { $goals = self::goals(); $seo = Backfill::metaDescriptionKey() !== ''; $body = Ui::checks('wizard_goals', [ 'seo' => ['label' => __('Fill in missing meta descriptions and alt text', 'aipcf-ai-provider-for-cloudflare'), 'note' => $seo ? __('about 2 neurons each, 17 per image', 'aipcf-ai-provider-for-cloudflare') : __('alt text only: no SEO plugin for meta descriptions', 'aipcf-ai-provider-for-cloudflare')], 'related' => ['label' => __('Show related posts under each post', 'aipcf-ai-provider-for-cloudflare'), 'note' => __('free', 'aipcf-ai-provider-for-cloudflare')], 'search' => ['label' => __('Search by meaning for members', 'aipcf-ai-provider-for-cloudflare'), 'note' => __('about 0.03 neurons a search', 'aipcf-ai-provider-for-cloudflare')], 'assistant' => ['label' => __('A site assistant that answers from your posts', 'aipcf-ai-provider-for-cloudflare'), 'note' => __('about 10 to 30 neurons an answer', 'aipcf-ai-provider-for-cloudflare')], 'links' => ['label' => __('Internal link suggestions for editors', 'aipcf-ai-provider-for-cloudflare'), 'note' => __('free; its chat costs a little', 'aipcf-ai-provider-for-cloudflare')], ], $goals, __('What you want', 'aipcf-ai-provider-for-cloudflare')); // Ui::checks names fields as settings; the wizard reads its own field. $body = str_replace(esc_attr(Ui::field('wizard_goals')) . '[]', 'goals[]', $body); return Ui::card(__('What do you want AIPCF to do?', 'aipcf-ai-provider-for-cloudflare'), self::form('goals', $body, __('Continue', 'aipcf-ai-provider-for-cloudflare'), 'connect'), [ 'text' => __('Tick what you want now. Everything can be changed later on Features.', 'aipcf-ai-provider-for-cloudflare'), ]); } private static function stepIndex(): string { $s = get_plugin_settings(); $store = (string) ($s['embed_store'] ?? StoreFactory::DEFAULT); $body = Ui::radios('store', [ 'vectorize' => ['label' => __('Cloudflare Vectorize', 'aipcf-ai-provider-for-cloudflare'), 'note' => __('On your Cloudflare account; free for most sites. The token needs Vectorize > Edit.', 'aipcf-ai-provider-for-cloudflare')], 'other' => ['label' => __('Qdrant or PostgreSQL', 'aipcf-ai-provider-for-cloudflare'), 'note' => __('A server you run or rent. You enter its details on Settings after setup.', 'aipcf-ai-provider-for-cloudflare')], ], $store === 'vectorize' ? 'vectorize' : 'other', __('Where the index is kept', 'aipcf-ai-provider-for-cloudflare')); $types = (array) ($s['embed_post_types'] ?? ['post', 'page']); $body .= Ui::row(__('What goes in', 'aipcf-ai-provider-for-cloudflare'), str_replace(esc_attr(Ui::field('wizard_types')) . '[]', 'types[]', Ui::checks('wizard_types', [ 'post' => ['label' => __('Posts', 'aipcf-ai-provider-for-cloudflare')], 'page' => ['label' => __('Pages', 'aipcf-ai-provider-for-cloudflare')], ], array_map('strval', $types), __('What goes in', 'aipcf-ai-provider-for-cloudflare'))), [ 'help' => __('Published posts only. More choices are on Settings > Content index.', 'aipcf-ai-provider-for-cloudflare'), ]); $e = Estimate::get(); $body .= Ui::stats([ ['value' => Ui::num($e['objects']), 'label' => _n('item', 'items', (int) $e['objects'], 'aipcf-ai-provider-for-cloudflare')], /* translators: %s: days */ ['value' => Ui::num($e['neurons']), 'label' => sprintf(_n('neurons, about %s day', 'neurons, about %s days', (int) $e['days'], 'aipcf-ai-provider-for-cloudflare'), Ui::num($e['days']))], ]); $body .= Ui::status(__('Estimated from a sample of your posts. Changed posts are added later, a few at a time.', 'aipcf-ai-provider-for-cloudflare')); return Ui::card(__('Build the content index', 'aipcf-ai-provider-for-cloudflare'), self::form('index', $body, __('Build it', 'aipcf-ai-provider-for-cloudflare'), 'goals'), [ 'text' => __('The features you chose find posts by meaning. They need your posts in an index first.', 'aipcf-ai-provider-for-cloudflare'), ]); } private static function stepSettings(): string { $s = get_plugin_settings(); $applied = Setup::defaultsApplied(); $body = Ui::radios('preset', [ 'recommended' => [ 'label' => __('Use the recommended settings', 'aipcf-ai-provider-for-cloudflare'), /* translators: 1: writing model, 2: short-task model */ 'note' => sprintf(__('%1$s for writing, %2$s for short tasks, no paid fallbacks, caching on. Tested on live sites.', 'aipcf-ai-provider-for-cloudflare'), pretty_model_name((string) Setup::DEFAULTS['preferred_model']), pretty_model_name((string) Setup::DEFAULTS['light_model'])), ], 'keep' => ['label' => __('Keep my models and caching as they are', 'aipcf-ai-provider-for-cloudflare'), 'note' => __('Change them any time on Settings.', 'aipcf-ai-provider-for-cloudflare')], ], $applied || !Setup::isConfigured() || (string) $s['preferred_model'] === '' ? 'recommended' : 'keep', __('Models and caching', 'aipcf-ai-provider-for-cloudflare')); $body .= Ui::row(__('Daily budget', 'aipcf-ai-provider-for-cloudflare'), Ui::number('budget_neurons', (int) ($s['budget_neurons'] ?? UsageTracker::FREE_TIER), 'aipcf-wizard-budget', ['min' => 100, 'max' => 10000000, 'unit' => __('neurons a day', 'aipcf-ai-provider-for-cloudflare')]), [ 'for' => 'aipcf-wizard-budget', 'help' => __('10,000 is the free allowance. AIPCF stops here, so there is never a bill by surprise.', 'aipcf-ai-provider-for-cloudflare'), ]); $body .= Ui::row(__('When it is reached', 'aipcf-ai-provider-for-cloudflare'), Ui::select('budget_action', [ 'cheapest' => __('Switch text to the cheapest model', 'aipcf-ai-provider-for-cloudflare'), 'stop' => __('Pause AI requests until midnight UTC', 'aipcf-ai-provider-for-cloudflare'), ], (string) ($s['budget_action'] ?? 'cheapest'), 'aipcf-wizard-action'), ['for' => 'aipcf-wizard-action']); $body = str_replace([esc_attr(Ui::field('budget_neurons')), esc_attr(Ui::field('budget_action'))], ['budget_neurons', 'budget_action'], $body); return Ui::card(__('Settings', 'aipcf-ai-provider-for-cloudflare'), self::form('settings', $body, __('Finish', 'aipcf-ai-provider-for-cloudflare'), in_array('index', self::stepsFor(self::goals()), true) ? 'index' : 'goals'), [ 'text' => __('The models AIPCF uses, and what it may spend a day.', 'aipcf-ai-provider-for-cloudflare'), ]); } private static function stepDone(): string { $goals = self::goals(); $items = []; $names = [ 'seo' => [__('Backfill', 'aipcf-ai-provider-for-cloudflare'), FeaturesPage::url('backfill')], 'related' => [__('Related posts', 'aipcf-ai-provider-for-cloudflare'), FeaturesPage::url('related')], 'search' => [__('Search', 'aipcf-ai-provider-for-cloudflare'), FeaturesPage::url('search')], 'assistant' => [__('Site assistant', 'aipcf-ai-provider-for-cloudflare'), AssistantPage::pageUrl()], 'links' => [__('Link assistant', 'aipcf-ai-provider-for-cloudflare'), FeaturesPage::url('links')], ]; foreach ($goals as $g) { if (isset($names[$g])) { $items[] = ['done' => true, 'text' => $names[$g][0], 'action' => Ui::link(__('Its settings', 'aipcf-ai-provider-for-cloudflare'), $names[$g][1])]; } } $body = $items !== [] ? Ui::checklist($items) : Ui::status(__('Nothing is switched on yet. Pick features on Features whenever you like.', 'aipcf-ai-provider-for-cloudflare')); $s = get_plugin_settings(); if (array_intersect($goals, self::INDEX_GOALS) !== []) { $body .= (string) ($s['embed_store'] ?? '') === 'vectorize' && EmbeddingSettings::enabled() ? Ui::notice(__('The content index is being built in the background. Features that need it start as posts are added.', 'aipcf-ai-provider-for-cloudflare'), 'info') : Ui::notice(__('Enter your Qdrant or PostgreSQL details, then switch the content index on.', 'aipcf-ai-provider-for-cloudflare'), 'warn'); } $ai = ai_plugin_status(); $actions = [['label' => __('Go to Home', 'aipcf-ai-provider-for-cloudflare'), 'url' => SettingsPage::tabUrl('overview'), 'kind' => 'primary']]; if (array_intersect($goals, self::INDEX_GOALS) !== []) { $actions[] = ['label' => __('Content index', 'aipcf-ai-provider-for-cloudflare'), 'url' => SettingsPage::settingsUrl('content-index')]; } if ($ai['status'] !== 'active') { $actions[] = ['label' => $ai['action_label'], 'url' => $ai['action_url'], 'kind' => 'link']; } return Ui::card(__('You are set up', 'aipcf-ai-provider-for-cloudflare'), $body . Ui::actions($actions), [ 'text' => __('Here is what is on. Home shows how it is going.', 'aipcf-ai-provider-for-cloudflare'), ]); } /** * The steps for the chosen goals: the content index only when one of them needs it. * * @param list $goals * @return list */ private static function stepsFor(array $goals): array { return array_values(array_filter(self::STEPS, static fn(string $s): bool => $s !== 'index' || array_intersect($goals, self::INDEX_GOALS) !== [])); } // ================================================================ saving /** * Saves some settings, keeping all the others (the whole option goes through the usual * cleaning, so nothing the screen did not show can be lost). * * @param array $changes */ private static function save(array $changes): void { $s = get_option(AIPCF_SETTINGS_OPTION, []); update_option(AIPCF_SETTINGS_OPTION, array_merge(is_array($s) ? $s : [], $changes)); } private static function go(string $step, string $text = '', string $type = 'info'): never { if ($text !== '') { set_transient('aipcf_wizard_msg_' . get_current_user_id(), ['text' => $text, 'type' => $type], 120); } wp_safe_redirect(self::url($step)); exit; } public static function handle(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to do this.', 'aipcf-ai-provider-for-cloudflare'), 403); } check_admin_referer(self::NONCE); // Read once, after the nonce check; each value is sanitised where it is used. $in = wp_unslash($_POST); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $in = is_array($in) ? $in : []; $step = sanitize_key((string) ($in['step'] ?? '')); if ($step === 'welcome') { self::go('connect'); } if ($step === 'connect') { self::saveConnect($in); } if ($step === 'goals') { $goals = is_array($in['goals'] ?? null) ? array_values(array_intersect(['seo', 'related', 'search', 'assistant', 'links'], array_map('sanitize_key', $in['goals']))) : []; update_option(self::STATE_OPTION, ['goals' => $goals], false); self::go(in_array('index', self::stepsFor($goals), true) ? 'index' : 'settings'); } if ($step === 'index') { self::saveIndex($in); } if ($step === 'settings') { self::finish($in); } self::go('welcome'); } /** * @param array $post */ private static function saveConnect(array $post): never { $changes = []; if (is_array($post[AIPCF_SETTINGS_OPTION] ?? null)) { $in = $post[AIPCF_SETTINGS_OPTION]; $changes['account_id'] = sanitize_text_field(trim((string) ($in['account_id'] ?? ''))); $changes['gateway_id'] = sanitize_text_field(trim((string) ($in['gateway_id'] ?? ''))); $token = sanitize_text_field(trim((string) ($in['api_key'] ?? ''))); if (!empty($in['api_key_clear'])) { // The settings keep a blank token as "unchanged"; the clear flag removes it. $changes['api_key'] = ''; $changes['api_key_clear'] = 1; } elseif ($token !== '') { $changes['api_key'] = $token; } } self::save($changes); if (!Setup::isConfigured()) { self::go('connect', core_connector_is_available() && get_api_key() === '' ? __('Account ID saved. Now add your API token on Settings > Connectors.', 'aipcf-ai-provider-for-cloudflare') : __('Enter both your Account ID and your API token.', 'aipcf-ai-provider-for-cloudflare'), 'warn'); } $test = SettingsPage::testCredentials(get_account_id(), get_api_key()); if ($test === 'pending_approval') { self::go('connect', __('Connected. One more thing: allow AIPCF below.', 'aipcf-ai-provider-for-cloudflare'), 'warn'); } if ($test !== true) { /* translators: %s: the error from Cloudflare */ self::go('connect', sprintf(__('Cloudflare did not accept these: %s', 'aipcf-ai-provider-for-cloudflare'), (string) $test), 'bad'); } self::go('goals', __('Connected to Cloudflare Workers AI.', 'aipcf-ai-provider-for-cloudflare'), 'info'); } /** * @param array $in */ private static function saveIndex(array $in): never { $store = sanitize_key((string) ($in['store'] ?? '')) === 'vectorize' ? 'vectorize' : 'other'; $types = is_array($in['types'] ?? null) ? array_values(array_intersect(['post', 'page'], array_map('sanitize_key', $in['types']))) : []; $s = get_plugin_settings(); // Keep any other post types already chosen on Settings > Content index. $other = array_values(array_diff((array) ($s['embed_post_types'] ?? []), ['post', 'page'])); $types = array_merge($types !== [] ? $types : ['post'], $other); if ($store === 'vectorize') { self::save(['embed_store' => 'vectorize', 'embed_post_types' => $types, 'embeddings_enabled' => 1]); } else { // The server's details are entered on Settings; nothing is switched on unseen. self::save(['embed_post_types' => $types]); } self::go('settings'); } /** * The last step: models and budget, then the chosen features switched on. * * @param array $in */ private static function finish(array $in): never { $preset = sanitize_key((string) ($in['preset'] ?? 'keep')); if ($preset === 'recommended' && Setup::isConfigured()) { Setup::applyDefaults(); } $budget = isset($in['budget_neurons']) ? max(100, min(10000000, (int) $in['budget_neurons'])) : UsageTracker::FREE_TIER; $action = sanitize_key((string) ($in['budget_action'] ?? '')) === 'stop' ? 'stop' : 'cheapest'; $goals = self::goals(); $changes = ['budget_enabled' => 1, 'budget_neurons' => $budget, 'budget_action' => $action]; if (in_array('seo', $goals, true)) { $changes['backfill_enabled'] = 1; $changes['backfill_tasks'] = Backfill::metaDescriptionKey() !== '' ? ['meta_description', 'alt_text'] : ['alt_text']; } if (in_array('related', $goals, true)) { $changes['related_enabled'] = 1; $changes['related_auto'] = 1; } foreach (['search' => 'search_enabled', 'assistant' => 'chat_enabled', 'links' => 'links_enabled'] as $goal => $key) { if (in_array($goal, $goals, true)) { $changes[$key] = 1; } } self::save($changes); update_option(Setup::DONE_OPTION, time(), false); update_option(self::DONE_OPTION, time(), false); self::go('done'); } /** * "Allow AIPCF": the WordPress AI plugin's connector approval, as an explicit click. */ public static function handleApprove(): never { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to do this.', 'aipcf-ai-provider-for-cloudflare'), 403); } check_admin_referer('aipcf_wizard_approve'); if (class_exists('\\WordPress\\AI\\Connector_Approval\\Approvals_Store')) { $approvals = get_option('wpai_connector_approvals', []); $approvals = is_array($approvals) ? $approvals : []; $approvals[plugin_basename(\AIPCF_FILE)][CloudflareWorkersAiProvider::ID] = true; update_option('wpai_connector_approvals', $approvals); } self::go('goals', __('AIPCF is allowed. You are connected.', 'aipcf-ai-provider-for-cloudflare')); } /** * Skip setup (or dismiss Home's offer to review it): not offered again. */ public static function handleDismiss(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to do this.', 'aipcf-ai-provider-for-cloudflare'), 403); } check_admin_referer('aipcf_wizard_dismiss'); update_option(self::DONE_OPTION, time(), false); update_option(Setup::DONE_OPTION, time(), false); wp_safe_redirect(SettingsPage::tabUrl('overview')); exit; } }