*/ public static function state(): array { $settings = TheDotDevAgenticBridge_Storage::get(); $stored = is_array($settings['onboarding'] ?? null) ? $settings['onboarding'] : []; return array_merge([ 'status' => 'not_started', 'step' => 1, 'business' => [], 'questions' => [], 'questionsReviewed' => false, 'contentReviewedAt' => '', 'completedAt' => '', ], $stored); } /** @param array $state @param array $scan */ public static function resolved_step(array $state, array $scan): int { $step = max(1, min(6, (int) ($state['step'] ?? 1))); if ($step === 5 && isset($scan['score'])) { return 6; } return $step; } /** @param array $raw @return array */ public static function sanitize_business(array $raw): array { $type = sanitize_key((string) ($raw['site_type'] ?? '')); $representation = sanitize_key((string) ($raw['representation'] ?? '')); return [ 'site_type' => in_array($type, ['services', 'store', 'publisher', 'nonprofit', 'portfolio', 'other'], true) ? $type : 'other', 'representation' => in_array($representation, ['organisation', 'person'], true) ? $representation : 'organisation', 'brand_name' => self::bounded_text((string) ($raw['brand_name'] ?? ''), self::MAX_SHORT_CHARS), 'core_offerings' => self::bounded_text((string) ($raw['core_offerings'] ?? ''), self::MAX_LONG_CHARS), 'service_area' => self::bounded_text((string) ($raw['service_area'] ?? ''), self::MAX_LONG_CHARS), 'availability' => self::bounded_text((string) ($raw['availability'] ?? ''), self::MAX_LONG_CHARS), 'primary_action' => self::bounded_text((string) ($raw['primary_action'] ?? ''), self::MAX_SHORT_CHARS), ]; } /** @param array $business @return list */ public static function suggested_questions(array $business): array { $brand = self::bounded_text((string) ($business['brand_name'] ?? ''), self::MAX_SHORT_CHARS); if ($brand === '') { $brand = self::bounded_text((string) get_bloginfo('name'), self::MAX_SHORT_CHARS); } $subject = $brand !== '' ? $brand : __('this business', 'agentic-bridge'); $questions = [ /* translators: %s: business or site name. */ sprintf(__('What does %s offer?', 'agentic-bridge'), $subject), /* translators: %s: business or site name. */ sprintf(__('Where does %s provide services or deliver?', 'agentic-bridge'), $subject), /* translators: %s: business or site name. */ sprintf(__('When is %s available?', 'agentic-bridge'), $subject), /* translators: %s: business or site name. */ sprintf(__('How can a customer contact or book with %s?', 'agentic-bridge'), $subject), /* translators: %s: business or site name. */ sprintf(__('Which policies should customers know before choosing %s?', 'agentic-bridge'), $subject), ]; return array_values(array_unique(array_map(static fn (string $question): string => self::bounded_text($question, 240), $questions))); } /** @return list */ public static function sanitize_questions(string $raw): array { $questions = []; foreach (preg_split('/\R/u', $raw) ?: [] as $line) { $question = self::bounded_text((string) $line, 240); if ($question === '' || in_array($question, $questions, true)) { continue; } $questions[] = $question; if (count($questions) >= self::MAX_QUESTIONS) { break; } } return $questions; } private static function process_submission(bool $strict_admin_post): void { if (!current_user_can('manage_options')) { self::redirect_setup( __('You do not have permission to confirm setup.', 'agentic-bridge'), 'error' ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- verified via wp_verify_nonce below; sanitized immediately. $raw_nonce = isset($_POST['_wpnonce']) ? wp_unslash($_POST['_wpnonce']) : ''; $nonce = is_string($raw_nonce) ? sanitize_text_field($raw_nonce) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, self::NONCE_ACTION)) { self::redirect_setup( __('Setup could not be confirmed. Refresh the page and try again.', 'agentic-bridge'), 'error' ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified immediately above. $operation = sanitize_key((string) wp_unslash($_POST['onboarding_operation'] ?? 'dismiss')); $state = self::state(); if ($operation === 'start') { self::save_state(array_merge($state, ['status' => 'in_progress', 'step' => 2])); self::redirect_setup(__('Quick Start started. Nothing has left this site.', 'agentic-bridge'), 'success'); } if ($operation === 'save_business') { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified above; the bounded business array is sanitized by sanitize_business() below. $raw_business = isset($_POST['business']) && is_array($_POST['business']) ? wp_unslash($_POST['business']) : []; self::save_state(array_merge($state, [ 'status' => 'in_progress', 'step' => 3, 'business' => self::sanitize_business($raw_business), ])); self::redirect_setup(__('Business basics saved locally. Review the suggested customer questions next.', 'agentic-bridge'), 'success'); } if ($operation === 'save_questions') { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified above; the text is sanitized by sanitize_questions() below. $raw_questions = (string) wp_unslash($_POST['questions'] ?? ''); self::save_state(array_merge($state, [ 'status' => 'in_progress', 'step' => 4, 'questions' => self::sanitize_questions($raw_questions), 'questionsReviewed' => false, ])); self::redirect_setup(__('Question drafts saved locally. They are not marked reviewed or published.', 'agentic-bridge'), 'success'); } if ($operation === 'confirm_content') { self::save_state(array_merge($state, [ 'status' => 'in_progress', 'step' => 5, 'contentReviewedAt' => gmdate('c'), ])); self::redirect_setup(__('Public content scope reviewed. Run the first local check when ready.', 'agentic-bridge'), 'success'); } if ($operation === 'restart') { TheDotDevAgenticBridge_Storage::update(['onboarding_complete' => false]); self::save_state(array_merge($state, ['status' => 'in_progress', 'step' => 1])); self::redirect_setup(__('Quick Start reopened. Your saved local answers are still available.', 'agentic-bridge'), 'success'); } $status = $operation === 'finish' ? 'complete' : 'skipped'; self::save_state(array_merge($state, [ 'status' => $status, 'step' => $operation === 'finish' ? 6 : max(1, (int) ($state['step'] ?? 1)), 'completedAt' => $operation === 'finish' ? gmdate('c') : '', ])); if (!TheDotDevAgenticBridge_Storage::mark_onboarding_complete()) { self::redirect_setup(__('Setup could not be saved. Try again or contact your host if this keeps happening.', 'agentic-bridge'), 'error'); } set_transient(self::TRANSIENT_KEY, true, 7 * 24 * HOUR_IN_SECONDS); self::redirect_setup( $operation === 'finish' ? __('Quick Start complete. Your next improvement is ready to review.', 'agentic-bridge') : __('Quick Start skipped. You can reopen it any time; local features remain available.', 'agentic-bridge'), 'success' ); } /** @param array $state */ private static function save_state(array $state): void { TheDotDevAgenticBridge_Storage::update(['onboarding' => $state]); } private static function bounded_text(string $value, int $limit): string { $value = trim((string) preg_replace('/\s+/u', ' ', wp_strip_all_tags($value, true))); if ($value === '') { return ''; } if (function_exists('mb_substr')) { return mb_substr($value, 0, $limit); } return substr($value, 0, $limit); } private static function redirect_setup(string $message, string $type): void { wp_safe_redirect(add_query_arg( [ 'thedotdev_agentic_bridge_notice' => $message, 'thedotdev_agentic_bridge_notice_type' => $type === 'error' ? 'error' : 'success', ], TheDotDevAgenticBridge_Admin::setup_url() )); exit; } }