pattern across the plugin so every * user-facing error notice on a plugin screen ships a one-click way to * report the failure. * * The button mount div renders after the message paragraph so the JS * component bootstraps it on DOMContentLoaded. The trigger source is * allowlisted server-side by Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES; * passing a slug not in that list will render a button that 400s on * click. The integration test pins the matching set. * * Only call this from screens that live under the plugin's own pages * (CLAUDE.md Self-Healing §4 bans support buttons on generic wp-admin * notices). * * @param string $messageHtml Pre-escaped/-allowed inner HTML * for the

inside the notice. * Callers escape per their own * needs (esc_html / wp_kses_post). * @param string $triggeredFrom One of * ABJ_404_Solution_Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES. * @param string|null $contextSummary Optional one-line summary shown * in the support-request modal. * @return string Notice HTML, ready to echo. */ public static function renderErrorNoticeWithSupportButton(string $messageHtml, string $triggeredFrom, ?string $contextSummary = null): string { $buttonHtml = class_exists('ABJ_404_Solution_SupportRequestButton') ? ABJ_404_Solution_SupportRequestButton::render($triggeredFrom, $contextSummary) : ''; return '

' . $messageHtml . '

' . $buttonHtml . '
'; } /** Get the text to notify the user when some URLs have been captured and need attention. * @param int $captured the number of captured URLs * @return string html */ function getDashboardNotificationCaptured($captured) { /* Translators: %s is the number of captured 404 URLs. */ $capturedMessage = sprintf( _n( 'There is %s captured 404 URL that needs to be processed.', 'There are %s captured 404 URLs to be processed.', $captured, '404-solution'), $captured); $capturedMessage = $this->f->str_replace("", "", $capturedMessage); $capturedMessage = $this->f->str_replace("", "", $capturedMessage); return '

' . PLUGIN_NAME . ": " . $capturedMessage . "

"; } /** Do an action like trash/delete/ignore/edit and display a page like stats/logs/redirects/options. * @return void */ static function handleMainAdminPageActionAndDisplay() { global $abj404view; $instance = self::getInstance(); try { $action = $instance->viewGetPostOrGetSanitize('action'); if (!is_admin() || !$instance->logic->userIsPluginAdmin()) { $instance->logger->logUserCapabilities("handleMainAdminPageActionAndDisplay (" . esc_html($action == '' ? '(none)' : $action) . ")"); echo '
'; echo '

' . esc_html(PLUGIN_NAME) . '

'; $permMessage = '' . esc_html__('Permission denied.', '404-solution') . ' ' . esc_html__('Your user account does not have permission to access this page.', '404-solution') . '

' . esc_html__('Please verify that your WordPress role has the', '404-solution') . ' ' . 'manage_options ' . esc_html__('capability.', '404-solution') . ' ' . esc_html__('If you have a security plugin installed, it may be restricting access to this page.', '404-solution'); $subpageForContext = (string)$instance->viewGetPostOrGetSanitize('subpage'); $triggerForPerm = ($subpageForContext === 'abj404_captured') ? 'captured_404s_page' : 'redirects_page'; echo self::renderErrorNoticeWithSupportButton( $permMessage, $triggerForPerm, 'Permission denied on plugin admin page (action=' . ($action == '' ? '(none)' : $action) . ')' ); echo '

'; return; } $sub = ""; // -------------------------------------------------------------------- // Handle Post Actions $instance->logger->debugMessage("Processing request for action: " . esc_html($action == '' ? '(none)' : $action)); // this should really not pass things by reference so it can be more object oriented (encapsulation etc). $message = ""; $message .= $instance->logic->handlePluginAction($action, $sub); $message .= $instance->logic->hanldeTrashAction(); $message .= $instance->logic->handleDeleteAction(); $message .= $instance->logic->handleIgnoreAction(); $message .= $instance->logic->handleLaterAction(); $message .= $instance->logic->handleActionEdit($sub, $action); $message .= $instance->logic->handleActionImportRedirects(); $instance->logic->handleActionChangeItemsPerRow(); $message .= $instance->logic->handleActionImportFile(); if ($action !== '' && $message !== '') { $instance->logger->debugMessage("Admin action completed: " . esc_html($action) . " => " . esc_html(substr($message, 0, 200))); } // -------------------------------------------------------------------- // Output the correct page. $abj404view->echoChosenAdminTab($action, $sub, $message); } catch (\Throwable $e) { $encodedEx = json_encode($e); $encodedContext = is_string($encodedEx) ? stripcslashes(wp_kses_post($encodedEx)) : ''; $instance->logger->errorMessage( "Caught exception (" . get_class($e) . "): " . $e->getMessage() . ($encodedContext !== '' ? " | context=" . $encodedContext : '') ); $subpageForContext = (string)$instance->viewGetPostOrGetSanitize('subpage'); $triggerForRenderError = ($subpageForContext === 'abj404_captured') ? 'captured_404s_page' : 'redirects_page'; $renderErrorMessage = '404 Solution: An error occurred while rendering this page.

' . '
Show error details' . '
'
                . esc_html($e->getMessage() . "\n" . $e->getTraceAsString())
                . '
' . '
' . '

'; echo '

'; echo self::renderErrorNoticeWithSupportButton( $renderErrorMessage, $triggerForRenderError, 'Render error: ' . substr($e->getMessage(), 0, 200) ); echo '
'; } } /** Display the chosen admin page. * @param string $action * @param string $sub * @param string $message * @return void */ function echoChosenAdminTab($action, $sub, $message) { global $abj404view; // If globals are not set, use sensible defaults if ($abj404view === null) { $abj404view = $this; } // Deal With Page Tabs if ($sub == "") { $sub = $this->f->strtolower($this->viewGetPostOrGetSanitize('subpage')); } if ($sub == "") { $sub = 'abj404_redirects'; $this->logger->debugMessage('No tab selected. Displaying the "redirects" tab.'); } // Check if we're returning from a successful redirect update $updated = $this->viewGetPostOrGetSanitize('updated'); if ($updated == '1') { $message .= __('Redirect Information Updated Successfully!', '404-solution'); } $this->logger->debugMessage("Displaying sub page: " . esc_html($sub == '' ? '(none)' : $sub)); $abj404view->outputAdminHeaderTabs($sub, $message); $abj404action = $this->viewGetPostOrGetSanitize('abj404action'); if (($action == 'editRedirect') || ($abj404action == 'editRedirect') || ($sub == 'abj404_edit')) { $abj404view->echoAdminEditRedirectPage(); } else if ($sub == 'abj404_redirects') { $abj404view->echoAdminRedirectsPage(); } else if ($sub == 'abj404_captured') { $abj404view->echoAdminCapturedURLsPage(); } else if ($sub == "abj404_options") { $abj404view->echoAdminOptionsPage(); } else if ($sub == 'abj404_logs') { $abj404view->echoAdminLogsPage(); } else if ($sub == 'abj404_stats') { $abj404view->outputAdminStatsPage(); } else if ($sub == 'abj404_tools') { $abj404view->echoAdminToolsPage(); } else if ($sub == 'abj404_debugfile') { $abj404view->echoAdminDebugFile(); } else { $this->logger->debugMessage('No tab selected. Displaying the "redirects" tab.'); $abj404view->echoAdminRedirectsPage(); } $abj404view->echoAdminFooter(); } /** * Echo the text that appears at the bottom of each admin page. * @return void */ function echoAdminFooter() { // read the html content. $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/adminFooter.html"); $html = $this->f->str_replace('{JAPANESE_FLASHCARDS_URL}', ABJ404_FC_URL, $html); $html = $this->f->str_replace( '{ABJ404_VIEW_FRESHNESS}', $this->renderViewFreshnessLabel(), $html ); // constants and translations. $html = $this->f->doNormalReplacements($html); echo $html; } /** * Render a short, human-readable "how long ago was the view_done cache * last successfully built" label for the admin footer. Returns one of: * - "n/a" when never built or freshness option cleared (post-invalidate); * - "Xs" when 0 to 59 seconds old; * - "Xm" when 1 to 59 minutes old; * - "Xh" when 1 to 23 hours old; * - "Xd" when a day or more. * * The view_done snapshot is what the Redirects / Captured / Logs tables * read from; if this label drifts upward into hours when the freshness * TTL is 120s, the auto-rebuild path is failing or being blocked. * * @return string */ private function renderViewFreshnessLabel(): string { if (!is_object($this->viewBuildOrchestrator) || !method_exists($this->viewBuildOrchestrator, 'getViewDoneBuiltAtTimestamp')) { return 'n/a'; } // Defensive: a unit-test mock may throw BadMethodCallException // when called without an expectation; any future implementation // could also throw on a transient read failure. The freshness // label is a footer cosmetic. Treat any failure as n/a so it // never blocks the page render. try { $builtAt = (int)$this->viewBuildOrchestrator->getViewDoneBuiltAtTimestamp(); } catch (\Throwable $e) { // allow-silent-catch: freshness label is a footer cosmetic; DAO stub failures must never block page render return 'n/a'; } if ($builtAt <= 0) { return 'n/a'; } $age = time() - $builtAt; if ($age < 0) { return 'n/a'; // clock skew; do not surface a negative age } if ($age < 60) { return $age . 's'; } if ($age < 3600) { return intval($age / 60) . 'm'; } if ($age < 86400) { return intval($age / 3600) . 'h'; } return intval($age / 86400) . 'd'; } /** Output the tabs at the top of the plugin page. * @param string $sub * @param string $message * @return void */ function outputAdminHeaderTabs($sub = 'list', $message = '') { ABJ_404_Solution_WPNotices::echoAdminNotices(); echo "
"; // Post-setup welcome banner if (isset($_GET['setup_complete']) && $_GET['setup_complete'] === '1') { $options = $this->logic->getOptions(); $auto = !empty($options['auto_redirects']) && $options['auto_redirects'] !== '0'; $notify = !empty($options['admin_notification']) && $options['admin_notification'] !== '0'; echo '

'; echo '' . esc_html__("You're all set!", '404-solution') . ' '; if ($auto && $notify) { echo esc_html__('404 Solution is now monitoring your site. When visitors hit broken links, they will be automatically redirected. We will email you if something needs attention.', '404-solution'); } elseif ($auto) { echo esc_html__('404 Solution is now monitoring your site. When visitors hit broken links, they will be automatically redirected.', '404-solution'); } elseif ($notify) { echo esc_html__('404 Solution is now monitoring your site. We will email you if captured 404 URLs need attention.', '404-solution'); } else { echo esc_html__('404 Solution is now monitoring your site. You can create manual redirects anytime from the Page Redirects tab.', '404-solution'); } echo '

' . "\n"; } if ($message != "") { $allowed_tags = array( 'br' => array(), 'em' => array(), 'strong' => array(), ); if (($this->f->strlen($message) >= 6) && ($this->f->substr($this->f->strtolower($message), 0, 6) == 'error:')) { $cssClasses = 'notice notice-error'; } else { $cssClasses = 'notice notice-success'; } echo '

' . wp_kses($message, $allowed_tags) . "

\n"; } // Regex auto-promote notice. Rendered as a separate info-level // banner because it carries an action (Undo) the user can choose // not to take. Kept distinct from the success message so the // visual hierarchy reads "save succeeded; by the way, we // promoted to regex and you can undo it" rather than mixing the // two. if (class_exists('ABJ_404_Solution_RegexAutoPromote')) { $regexNotice = ABJ_404_Solution_RegexAutoPromote::readNotice(); if (is_array($regexNotice) && $regexNotice['redirect_id'] > 0) { $this->renderRegexAutoPromoteNotice($regexNotice); } } $isSimpleMode = $this->logic->getSettingsMode() === 'simple'; echo ''; } /** * Render the "Detected as a regex pattern" notice with [Edit] and * [Undo] links. The notice is shown once per save that triggered * server-side regex auto-promotion (admin posted a URL with * unambiguous regex metachars but did not check the "Treat as regex" * box). The [Undo] link restores the row to status=MANUAL with the * original from_url; [Edit] jumps straight to the redirect edit * page for further adjustments. * * @param array{redirect_id: int, original_url: string, new_url: string, url_rewritten: bool, created_at: int} $notice * @return void */ private function renderRegexAutoPromoteNotice(array $notice) { $redirectId = (int)$notice['redirect_id']; $originalUrl = (string)$notice['original_url']; $newUrl = (string)$notice['new_url']; $urlRewritten = !empty($notice['url_rewritten']); $editUrl = admin_url('admin.php?page=' . ABJ404_PP . '&subpage=abj404_edit&id=' . $redirectId); $undoBase = admin_url('admin.php?page=' . ABJ404_PP . '&subpage=abj404_redirects&action=undoRegexAutoPromote'); $undoUrl = wp_nonce_url($undoBase, 'abj404undoRegexAutoPromote'); if ($urlRewritten) { $message = sprintf( /* translators: %1$s = original URL as typed by the admin; %2$s = the URL after glob-to-regex fixup (e.g. /sales/* becomes /sales/.*) */ __('Detected as a regex pattern. Stored "%1$s" as "%2$s".', '404-solution'), $originalUrl, $newUrl ); } else { $message = sprintf( /* translators: %s = the URL stored unchanged with Regex status */ __('Detected as a regex pattern. Stored "%s" with Regex status.', '404-solution'), $originalUrl ); } echo '

'; echo '' . esc_html__('404 Solution:', '404-solution') . ' '; echo esc_html($message); echo ' ' . esc_html__('Edit', '404-solution') . ''; echo ' | '; echo ''; echo esc_html__('Undo', '404-solution') . ''; echo '

' . "\n"; } /** This outputs a box with a title and some content in it. * It's used on the Stats, Options and Tools page (for example). * @param int|string $id * @param string $title * @param string $content * @return void */ function echoPostBox($id, $title, $content) { echo "
"; echo "

" . esc_html($title) . "

"; echo "
" . $content /* Can't escape here, as contains forms */ . "
"; echo "
"; } /** * Echo an accordion section using card-based layout * @param string $sectionId The section identifier * @param string $postboxId The ID for the postbox * @param string $title The title of the section * @param string $content The content to display * @param bool $initiallyVisible Whether the card starts expanded (default: false) * @param string $icon The SVG icon for the card header (optional) * @param string $badge Optional info badge text * @return void */ function echoOptionsSection($sectionId, $postboxId, $title, $content, $initiallyVisible = false, $icon = '', $badge = '') { $expandedClass = $initiallyVisible ? ' expanded' : ''; echo "
"; echo "
"; echo "

"; if ($icon) { echo $icon; // Icon is pre-sanitized SVG } echo esc_html($title); if ($badge) { echo "" . esc_html($badge) . ""; } echo "

"; echo ""; echo ""; echo ""; echo "
"; echo "
"; echo $content; /* Can't escape here, as contains forms */ echo "
"; echo "
"; } /** * Get SVG icon for a section * @param string $iconName The icon name * @return string The SVG icon markup */ function getCardIcon($iconName) { $icons = array( 'lightning' => '', 'gear' => '', 'filter' => '', 'document' => '', 'sliders' => '', 'lightbulb' => '', // Stats page icons 'chart' => '', 'warning' => '', 'clock' => '', // Tools page icons 'download' => '', 'upload' => '', 'trash' => '', 'database' => '', 'cog' => '' ); return isset($icons[$iconName]) ? $icons[$iconName] : ''; } /** * Echo the sticky save bar * @return void */ function echoStickySaveBar() { $version = ABJ404_VERSION; $restoreNonce = wp_create_nonce('abj404_restore_defaults'); echo '
'; echo '
'; /* translators: %s: plugin version number */ echo esc_html(sprintf(__('Plugin v%s', '404-solution'), $version)); echo '
'; echo '
'; echo ''; echo ''; echo '
'; echo '
'; $this->echoRestoreDefaultsModal(); } /** * Echo the confirmation modal shown before restoring settings to defaults. * Toggled open by adding `.active`; matches the markup of other plugin modals. * @return void */ function echoRestoreDefaultsModal() { ?> '; echo ' '; echo '' . esc_html__('Settings saved successfully!', '404-solution') . ''; echo '
'; } /** * Echo the expand/collapse all button and save button * @param bool $showSuggestions Not used (kept for compatibility) * @return void */ function echoExpandCollapseButton($showSuggestions = true) { ?>
f->str_replace('{nonce}', wp_create_nonce('abj404_mode_toggle'), $html); $html = $this->f->str_replace('{simpleActive}', $simpleActive, $html); $html = $this->f->str_replace('{advancedActive}', $advancedActive, $html); $html = $this->f->str_replace('{simplePressedState}', $simplePressedState, $html); $html = $this->f->str_replace('{advancedPressedState}', $advancedPressedState, $html); $html = $this->f->str_replace('{Simple Mode}', __('Simple Mode', '404-solution'), $html); $html = $this->f->str_replace('{Advanced Mode}', __('Advanced Mode', '404-solution'), $html); $html = $this->f->str_replace('{mode_description}', $modeDescription, $html); echo $html; } /** * Echo an inline (compact) mode toggle for the header row. * This is a smaller version without the description text. * @return void */ function echoInlineModeToggle() { $currentMode = $this->logic->getSettingsMode(); $simpleActive = ($currentMode === 'simple') ? 'active' : ''; $advancedActive = ($currentMode === 'advanced') ? 'active' : ''; $simplePressedState = ($currentMode === 'simple') ? 'true' : 'false'; $advancedPressedState = ($currentMode === 'advanced') ? 'true' : 'false'; echo '
'; echo '
'; echo ''; echo ''; echo '
'; echo '
'; } /** * Echo the Simple Mode options page. * @param array $options The plugin options * @return void */ function echoSimpleModeOptions($options) { // Build behavior tiles HTML (replaces old dropdown) $behaviorTilesHtml = $this->getBehaviorTilesHTML($options); // Build selected states for checkboxes and dropdowns $selectedAutoRedirects = $this->getCheckedAttr($options, 'auto_redirects'); $selectedCapture404 = $this->getCheckedAttr($options, 'capture_404'); $selectedDefaultRedirect301 = ($options['default_redirect'] == '301') ? 'selected' : ''; $selectedDefaultRedirect302 = ($options['default_redirect'] == '302') ? 'selected' : ''; $selectedDefaultRedirect307 = ($options['default_redirect'] == '307') ? 'selected' : ''; $selectedDefaultRedirect308 = ($options['default_redirect'] == '308') ? 'selected' : ''; // Theme selections $selectedThemeDefault = ($options['admin_theme'] == 'default') ? 'selected' : ''; $selectedThemeCalm = ($options['admin_theme'] == 'calm') ? 'selected' : ''; $selectedThemeMono = ($options['admin_theme'] == 'mono') ? 'selected' : ''; $selectedThemeNeon = ($options['admin_theme'] == 'neon') ? 'selected' : ''; $selectedThemeObsidian = ($options['admin_theme'] == 'obsidian') ? 'selected' : ''; // Notification frequency selections $notifyFrequency = isset($options['admin_notification_frequency']) ? (string)(is_scalar($options['admin_notification_frequency']) ? $options['admin_notification_frequency'] : 'instant') : 'instant'; $selectedNotifyInstant = ($notifyFrequency === 'instant') ? 'selected' : ''; $selectedNotifyDaily = ($notifyFrequency === 'daily') ? 'selected' : ''; $selectedNotifyWeekly = ($notifyFrequency === 'weekly') ? 'selected' : ''; // Read and build the simple options template $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/adminOptionsSimple.html"); // Replace dest404page tiles $html = $this->f->str_replace('{behaviorTiles}', $behaviorTilesHtml, $html); // Replace checkbox states $html = $this->f->str_replace('{selectedAutoRedirects}', $selectedAutoRedirects, $html); $html = $this->f->str_replace('{selectedCapture404}', $selectedCapture404, $html); $html = $this->f->str_replace('{selectedDefaultRedirect301}', $selectedDefaultRedirect301, $html); $html = $this->f->str_replace('{selectedDefaultRedirect302}', $selectedDefaultRedirect302, $html); $html = $this->f->str_replace('{selectedDefaultRedirect307}', $selectedDefaultRedirect307, $html); $html = $this->f->str_replace('{selectedDefaultRedirect308}', $selectedDefaultRedirect308, $html); // Replace values $html = $this->f->str_replace('{capture_deletion}', esc_attr($this->optStr($options, 'capture_deletion')), $html); $html = $this->f->str_replace('{admin_notification}', esc_attr($this->optStr($options, 'admin_notification')), $html); $html = $this->f->str_replace('{maximum_log_disk_usage}', esc_attr($this->optStr($options, 'maximum_log_disk_usage')), $html); // Theme selections $html = $this->f->str_replace('{selectedThemeDefault}', $selectedThemeDefault, $html); $html = $this->f->str_replace('{selectedThemeCalm}', $selectedThemeCalm, $html); $html = $this->f->str_replace('{selectedThemeMono}', $selectedThemeMono, $html); $html = $this->f->str_replace('{selectedThemeNeon}', $selectedThemeNeon, $html); $html = $this->f->str_replace('{selectedThemeObsidian}', $selectedThemeObsidian, $html); $html = $this->f->str_replace('{theme_default}', __('Default (Follows WordPress)', '404-solution'), $html); // Translate labels $html = $this->f->str_replace('{Core Settings}', __('Core Settings', '404-solution'), $html); $html = $this->f->str_replace('{404 Capture}', __('404 Capture', '404-solution'), $html); $html = $this->f->str_replace('{Maintenance}', __('Maintenance', '404-solution'), $html); $html = $this->f->str_replace('{Default 404 destination}', __('Default 404 destination', '404-solution'), $html); $html = $this->f->str_replace('{Where to send visitors when a 404 error occurs}', __('Where to send visitors when a 404 error occurs', '404-solution'), $html); $html = $this->f->str_replace('{Create automatic redirects}', __('Create automatic redirects', '404-solution'), $html); $html = $this->f->str_replace('{Automatically redirect 404s to similar pages when a good match is found}', __('Automatically redirect 404s to similar pages when a good match is found', '404-solution'), $html); $html = $this->f->str_replace('{Redirect type}', __('Redirect type', '404-solution'), $html); $html = $this->f->str_replace('{Permanent 301}', __('Permanent 301', '404-solution'), $html); $html = $this->f->str_replace('{Temporary 302}', __('Temporary 302', '404-solution'), $html); $html = $this->f->str_replace('{301 for SEO, 302 for temporary changes}', __('301 for SEO, 302 for temporary changes', '404-solution'), $html); $html = $this->f->str_replace('{Collect incoming 404 URLs}', __('Collect incoming 404 URLs', '404-solution'), $html); $html = $this->f->str_replace('{Log 404 errors so you can review and fix them}', __('Log 404 errors so you can review and fix them', '404-solution'), $html); $html = $this->f->str_replace('{Delete captured URLs after}', __('Delete captured URLs after', '404-solution'), $html); $html = $this->f->str_replace('{days}', __('days', '404-solution'), $html); $html = $this->f->str_replace('{Auto-remove old 404 records (0 to keep forever)}', __('Auto-remove old 404 records (0 to keep forever)', '404-solution'), $html); $html = $this->f->str_replace('{Notify me when captured URLs exceed}', __('Notify me when captured URLs exceed', '404-solution'), $html); $html = $this->f->str_replace('{URLs}', __('URLs', '404-solution'), $html); $html = $this->f->str_replace('{Show admin notice when 404 count gets high (0 to disable)}', __('Show admin notice when 404 count gets high (0 to disable)', '404-solution'), $html); $html = $this->f->str_replace('{Maximum log storage}', __('Maximum log storage', '404-solution'), $html); $html = $this->f->str_replace('{Oldest logs are deleted when this limit is reached}', __('Oldest logs are deleted when this limit is reached', '404-solution'), $html); $html = $this->f->str_replace('{Admin theme}', __('Admin theme', '404-solution'), $html); $html = $this->f->str_replace('{Calm Ops (Light)}', __('Calm Ops (Light)', '404-solution'), $html); $html = $this->f->str_replace('{Monochrome Minimal (Light)}', __('Monochrome Minimal (Light)', '404-solution'), $html); $html = $this->f->str_replace('{Neon Slate (Dark)}', __('Neon Slate (Dark)', '404-solution'), $html); $html = $this->f->str_replace('{Obsidian Blue (Dark)}', __('Obsidian Blue (Dark)', '404-solution'), $html); $html = $this->f->str_replace('{Save Settings}', __('Save Settings', '404-solution'), $html); $html = $this->f->str_replace('{Need more control?}', __('Need more control?', '404-solution'), $html); $html = $this->f->str_replace('{Switch to Advanced Mode}', __('Switch to Advanced Mode', '404-solution'), $html); $html = $this->f->str_replace('{for 30+ additional options.}', __('for 30+ additional options.', '404-solution'), $html); // Frequency dropdown selections and labels $html = $this->f->str_replace('{selectedNotifyInstant}', $selectedNotifyInstant, $html); $html = $this->f->str_replace('{selectedNotifyDaily}', $selectedNotifyDaily, $html); $html = $this->f->str_replace('{selectedNotifyWeekly}', $selectedNotifyWeekly, $html); $html = $this->f->str_replace('{Email notification frequency}', __('Email notification frequency', '404-solution'), $html); $html = $this->f->str_replace('{Instant (when threshold exceeded)}', __('Instant (when threshold exceeded)', '404-solution'), $html); $html = $this->f->str_replace('{Daily digest}', __('Daily digest', '404-solution'), $html); $html = $this->f->str_replace('{Weekly digest}', __('Weekly digest', '404-solution'), $html); $html = $this->f->str_replace('{Choose how often to receive email notifications about captured 404s.}', __('Choose how often to receive email notifications about captured 404s.', '404-solution'), $html); // Also handle the Temporary 307/308 options (present in simple mode too) $html = $this->f->str_replace('{Temporary 307 (preserve method)}', __('Temporary 307 (preserve method)', '404-solution'), $html); $html = $this->f->str_replace('{Permanent 308 (preserve method)}', __('Permanent 308 (preserve method)', '404-solution'), $html); echo $html; } }