requireAdminWithNonce('abj404_setup_wizard'); ABJ_404_Solution_SetupWizardOptionStore::markCompleteToday(); // Response is intentionally minimal; the UI uses a fire-and-forget request. wp_send_json_success(array('message' => '')); } /** * Check if setup wizard should be shown * * @return bool True if wizard should display */ private static function shouldShowWizard() { return !ABJ_404_Solution_SetupWizardOptionStore::isComplete(); } /** * Check if current page is a 404 Solution admin page * * @return bool True if on 404 Solution page */ private static function isPluginPage() { if (!is_admin()) { return false; } // Check for the plugin's page parameter $page = ABJ_404_Solution_RequestInputNormalizer::readText($_GET, array('name' => 'page')); return $page === 'abj404_solution'; } /** * Handle form submission for setup wizard * @return void */ public static function handleFormSubmission(): void { // Check if this is our form submission if (!isset($_POST['abj404_setup_wizard_action'])) { return; } // Verify nonce with error feedback (Bug #10 fix) if (!isset($_POST['abj404_setup_wizard_nonce']) || !wp_verify_nonce($_POST['abj404_setup_wizard_nonce'], 'abj404_setup_wizard')) { wp_die( esc_html__('Security check failed. Please try again.', '404-solution'), esc_html__('Error', '404-solution'), array('response' => 403, 'back_link' => true) ); } // Verify plugin-admin access with error feedback (Bug #10 fix) if (!ABJ_404_Solution_PluginAdminAccessPolicy::currentUserCanAccessPluginAdmin()) { wp_die( esc_html__('You do not have permission to access this page.', '404-solution'), esc_html__('Error', '404-solution'), array('response' => 403, 'back_link' => true) ); } $action = self::submittedAction($_POST); $answers = ABJ_404_Solution_SetupWizardAnswerPolicy::answersFromRequest($_POST); // If user clicked "Save & Get Started", apply their settings if ($action === 'save') { $repository = abj_service('options_repository'); $checkedWriter = is_callable(array($repository, 'getRawSettingsSnapshot')) && is_callable(array($repository, 'updateOptionsChecked')); if (!$checkedWriter) { wp_die( esc_html__('Settings could not be saved because the checked storage writer is unavailable. Reload this page and try again.', '404-solution') . ' [ABJ404_SETTINGS_WRITER_UNAVAILABLE]', esc_html__('Error', '404-solution'), array('response' => 503, 'back_link' => true) ); } $lock = new ABJ_404_Solution_SiteResourceLock('initialization'); if (!$lock->acquire()) { wp_die( esc_html__('Settings could not be saved because another update is in progress. Reload this page and try again.', '404-solution') . ' [ABJ404_SETTINGS_WRITE_BUSY]', esc_html__('Error', '404-solution'), array('response' => 409, 'back_link' => true) ); } try { $saved = self::saveAnswersChecked($repository, $answers, $lock); if ($saved) { // Completion is durable only after the checked settings write. ABJ_404_Solution_SetupWizardOptionStore::markCompleteToday(); } } finally { $lock->release(); } // wp_die terminates PHP without running finally. Respond only // after the connection-owned lock has been released. if (!$saved) { wp_die( esc_html__('Settings could not be saved. Reload this page and try again.', '404-solution') . ' [ABJ404_SETTINGS_SAVE_REFUSED]', esc_html__('Error', '404-solution'), array('response' => 409, 'back_link' => true) ); } } else { ABJ_404_Solution_SetupWizardOptionStore::markCompleteToday(); } $redirect_url = ABJ_404_Solution_SetupWizardAnswerPolicy::redirectPath($answers); $destination = admin_url($redirect_url); if (!ABJ_404_Solution_RedirectLoopGuard::permitsDestination($destination)) { ABJ_404_Solution_WPNotices::respondToRedirectRefusal($destination); // Keep rendering this admin page. The guard queues the existing // plugin notice so the refusal is visible without a new surface. return; } wp_safe_redirect($destination); exit; } /** * Apply answers only against the exact settings snapshot which produced them. * This operation returns refusal so its caller can release the lock before responding. * @param ABJ_404_Solution_PluginLogicOptionsResolver $repository * @param array{q1:string,q2:string,q3:string} $answers */ private static function saveAnswersChecked($repository, array $answers, ABJ_404_Solution_SiteResourceLock $lock): bool { ABJ_404_Solution_OptionCacheInvalidator::site('abj404_settings'); if (is_callable(array($repository, 'clearCache'))) { call_user_func(array($repository, 'clearCache')); } // Keep the answer base and raw compare authority paired. The // options read may repair/default-persist the row while the // lock is held, so reload once when its exact bytes change. $before = $repository->getRawSettingsSnapshot(); $options = ABJ_404_Solution_SetupWizardOptionStore::loadPluginOptions(); $expected = $repository->getRawSettingsSnapshot(); if (!self::sameRawSettingsSnapshot($before, $expected)) { if (is_callable(array($repository, 'clearCache'))) { call_user_func(array($repository, 'clearCache')); } ABJ_404_Solution_OptionCacheInvalidator::site('abj404_settings'); $before = $expected; $options = ABJ_404_Solution_SetupWizardOptionStore::loadPluginOptions(); $expected = $repository->getRawSettingsSnapshot(); if (!self::sameRawSettingsSnapshot($before, $expected)) { return false; } } $options = ABJ_404_Solution_SetupWizardAnswerPolicy::applyToOptions( $options, $answers, ABJ_404_Solution_SetupWizardOptionStore::adminEmail() ); try { $saved = $repository->updateOptionsChecked($options, $expected, $lock); } catch (Throwable $e) { abj_service('logging')->warn('Setup settings write refused: ' . $e->getMessage()); $saved = false; } if ($saved !== true) { return false; } return true; } /** * @param array{status:string,value:string} $left * @param array{status:string,value:string} $right */ private static function sameRawSettingsSnapshot(array $left, array $right): bool { return isset($left['status'], $left['value'], $right['status'], $right['value']) && $left['status'] === $right['status'] && $left['value'] === $right['value']; } /** * Return a sanitized setup action or an empty non-save action for malformed input. * * @param array $post Raw POST payload. * @return string */ private static function submittedAction(array $post): string { $rawAction = $post['abj404_setup_wizard_action'] ?? ''; if (!is_scalar($rawAction)) { return ''; } return sanitize_text_field((string)$rawAction); } /** * Enqueue assets on 404 Solution admin pages * * @param string $hook Current admin page hook * @return void */ public static function enqueueAssets(string $hook): void { // Only load on 404 Solution pages if (!self::isPluginPage()) { return; } // Only load if wizard should be shown if (!self::shouldShowWizard()) { return; } // Only for users authorized for this plugin admin surface. if (!ABJ_404_Solution_PluginAdminAccessPolicy::currentUserCanAccessPluginAdmin()) { return; } // Add inline styles for the modal add_action('admin_head', array(__CLASS__, 'outputStyles')); // Output modal HTML in footer add_action('admin_footer', array(__CLASS__, 'outputModalHTML')); // Output JavaScript for dismiss functionality add_action('admin_footer', array(__CLASS__, 'outputScript'), 20); } /** * Output modal CSS styles * @return void */ public static function outputStyles(): void { ABJ_404_Solution_SetupWizardPresenter::outputStyles(); } /** * Output the modal HTML structure * @return void */ public static function outputModalHTML(): void { ABJ_404_Solution_SetupWizardPresenter::outputModalHTML(); } /** * Output JavaScript for dismiss and save functionality * @return void */ public static function outputScript(): void { ABJ_404_Solution_SetupWizardPresenter::outputScript(); } }