$options * @return void */ public static function initializeSuggestionDestination(array &$options): void { // wp_insert_post() runs cache listeners before returning, including // during WP-CLI's plugin sandbox load before the activation hook. // Prepare their tables for every suggestion-initialization caller; // full activation/upgrade still owns schema convergence. abj_service('database_upgrades')->components() ->bootstrapUpgrade()->repairMissingTables(); $policy = new self(); $message = $policy->apply($options, array('dest404_behavior' => 'suggest')); if ($message !== '') { $policy->apply($options, array('dest404_behavior' => 'theme_default')); // Options initialization must use the inert sink, not the runtime // logger, which reads options itself and would re-enter initialization. abj404_logPhpFallback('early-boot', 'Warning: Could not create the initial suggestion page; using theme_default.'); } } /** * Derive the 404 behavior a stored destination implies: the read-side * inverse of applyBehaviorToDest404Page(). Used to backfill `dest404_behavior` * for settings written before that key existed, before defaults are merged * (a default would otherwise overwrite the owner's choice). * * Lives here, not on the version upgrader, because the boot-time settings * read needs it on every request and must not load the upgrade service graph. * * @param string $dest A stored `dest404page` value ("|"), or "". * @return string One of 'theme_default', 'homepage', 'suggest', 'custom'. */ public static function behaviorFromDestination(string $dest): string { if ($dest === '0|' . ABJ404_TYPE_404_DISPLAYED || $dest === (string)ABJ404_TYPE_404_DISPLAYED || $dest === '') { return 'theme_default'; } if ($dest === '0|' . ABJ404_TYPE_HOME) { return 'homepage'; } $parts = explode('|', $dest); $pageId = isset($parts[0]) ? (int)$parts[0] : 0; if ($pageId > 0 && ABJ_404_Solution_SystemPage::isSystemPage($pageId)) { return 'suggest'; } return 'custom'; } /** * @param array $options * @param array $postData * @return string Any translated validation messages. */ public function apply(array &$options, array $postData): string { $message = ""; if (isset($postData['default_redirect'])) { $message .= $this->applyDefaultRedirect($options, $postData['default_redirect']); } if (isset($postData['dest404_behavior'])) { $message .= $this->applyBehavior($options, $postData); } else { $message .= $this->applyLegacyDestination($options, $postData); } if (isset($postData['template_redirect_priority'])) { $message .= $this->applyTemplateRedirectPriority($options, $postData['template_redirect_priority']); } return $message; } /** * @param array $options * @param mixed $rawDefaultRedirect */ private function applyDefaultRedirect(array &$options, $rawDefaultRedirect): string { $validDefaultCodes = array('301', '302', '307', '308'); $redirectCode = (string)(is_scalar($rawDefaultRedirect) ? $rawDefaultRedirect : ''); if (in_array($redirectCode, $validDefaultCodes, true)) { $options['default_redirect'] = intval($redirectCode); return ""; } return __('Error: Invalid value specified for default redirect type', '404-solution') . ".
"; } /** * @param array $options * @param array $postData */ private function applyBehavior(array &$options, array $postData): string { $validBehaviors = array('suggest', 'homepage', 'custom', 'theme_default'); $behavior = sanitize_text_field(is_string($postData['dest404_behavior']) ? $postData['dest404_behavior'] : ''); if (!in_array($behavior, $validBehaviors, true)) { return __('Error: Invalid 404 behavior selected', '404-solution') . ".
"; } $options['dest404_behavior'] = $behavior; return $this->applyBehaviorToDest404Page($options, $behavior, $postData); } /** * @param array $options * @param array $postData */ private function applyLegacyDestination(array &$options, array $postData): string { $candidateUrlLegacy = null; if (isset($postData['redirect_to_data_field_title'])) { $destination = $this->sanitizeDestinationTitle($postData); if ($destination['message'] !== '') { return $destination['message']; } $candidateUrlLegacy = $destination['dest']; if (strlen($candidateUrlLegacy) > ABJ404_MAX_URL_LENGTH) { return sprintf( __('Error: 404 destination URL exceeds the maximum length of %d characters', '404-solution'), ABJ404_MAX_URL_LENGTH ) . ".
"; } } if ($candidateUrlLegacy !== null) { if (isset($postData['redirect_to_data_field_id'])) { $options['dest404page'] = sanitize_text_field( is_string($postData['redirect_to_data_field_id']) ? $postData['redirect_to_data_field_id'] : '' ); } $options['dest404pageURL'] = $candidateUrlLegacy; $this->rewriteExternalDestinationOption($options); } else if (isset($postData['redirect_to_data_field_id'])) { $options['dest404page'] = sanitize_text_field( is_string($postData['redirect_to_data_field_id']) ? $postData['redirect_to_data_field_id'] : '' ); } return ""; } /** * The destination title field holds a page title for an internal page and * the typed URL for an external one. sanitize_text_field() removes every * "%XX" octet, which destroys a URL (a nested URL, %20 and %2B are * escapes), so an external URL is validated as a URL instead, the same way * the Add/Edit Redirect forms validate one. A URL that cannot be stored * comes back with a message and is not saved. * * @param array $postData * @return array{dest: string, message: string} message is '' when dest may be stored. */ private function sanitizeDestinationTitle(array $postData): array { $title = is_string($postData['redirect_to_data_field_title']) ? $postData['redirect_to_data_field_title'] : ''; $selectedId = isset($postData['redirect_to_data_field_id']) && is_string($postData['redirect_to_data_field_id']) ? sanitize_text_field($postData['redirect_to_data_field_id']) : ''; if (!ABJ_404_Solution_ExternalDestinationPolicy::isExternalChoice($selectedId)) { return array('dest' => sanitize_text_field($title), 'message' => ''); } return (new ABJ_404_Solution_ExternalDestinationPolicy(abj_service('functions'))) ->validate($title); } /** * @param array $options * @param mixed $rawPriority */ private function applyTemplateRedirectPriority(array &$options, $rawPriority): string { if (is_numeric($rawPriority) && $rawPriority >= 0 && $rawPriority <= 999) { $options['template_redirect_priority'] = absint($rawPriority); return ""; } return __('Error: Template redirect priority value must be a number between 0 and 999', '404-solution') . ".
"; } /** * @param array $options * @param string $behavior * @param array $postData * @return string Any translated validation messages. */ private function applyBehaviorToDest404Page(array &$options, string $behavior, array $postData): string { switch ($behavior) { case 'suggest': $systemPage = ABJ_404_Solution_SystemPage::getInstance(); $pageId = $systemPage->ensureSystemPage(); if ($pageId > 0) { $options['dest404page'] = $pageId . '|' . ABJ404_TYPE_POST; return ""; } return __('Error: Could not create the suggestion page', '404-solution') . ".
"; case 'homepage': $options['dest404page'] = '0|' . ABJ404_TYPE_HOME; return ""; case 'custom': return $this->applyLegacyDestination($options, $postData); case 'theme_default': default: $options['dest404page'] = '0|' . ABJ404_TYPE_404_DISPLAYED; return ""; } } /** @param array $options */ private function rewriteExternalDestinationOption(array &$options): void { if (ABJ_404_Solution_ExternalDestinationPolicy::isExternalChoice($options['dest404page'])) { $destinationUrl = isset($options['dest404pageURL']) && is_scalar($options['dest404pageURL']) ? (string)$options['dest404pageURL'] : ''; $options['dest404page'] = $destinationUrl . '|' . ABJ404_TYPE_EXTERNAL; } } }