Latest table data signatures by subpage. */ private $tableDataSignatures = array(); /** * Constructor with dependency injection. * Dependencies are now explicit and visible. * * @param ABJ_404_Solution_Functions|null $functions String manipulation utilities * @param ABJ_404_Solution_PluginLogic|null $pluginLogic Business logic service * @param ABJ_404_Solution_DataAccess|null $dataAccess Data access layer * @param ABJ_404_Solution_Logging|null $logging Logging service */ public function __construct($functions = null, $pluginLogic = null, $dataAccess = null, $logging = null) { // Use injected dependencies or fall back to getInstance() for backward compatibility $this->f = $functions !== null ? $functions : ABJ_404_Solution_Functions::getInstance(); $this->logic = $pluginLogic !== null ? $pluginLogic : ABJ_404_Solution_PluginLogic::getInstance(); $this->dao = $dataAccess !== null ? $dataAccess : ABJ_404_Solution_DataAccess::getInstance(); $this->logger = $logging !== null ? $logging : ABJ_404_Solution_Logging::getInstance(); } public static function getInstance() { if (self::$instance !== null) { return self::$instance; } // If the DI container is initialized, prefer it. if (function_exists('abj_service') && class_exists('ABJ_404_Solution_ServiceContainer')) { try { $c = ABJ_404_Solution_ServiceContainer::getInstance(); if (is_object($c) && method_exists($c, 'has') && $c->has('view')) { self::$instance = $c->get('view'); return self::$instance; } } catch (Throwable $e) { // fall back } } if (self::$instance == null) { self::$instance = new ABJ_404_Solution_View(); } return self::$instance; } /** Get the 'checked' attribute for a checkbox based on option value. * @param array $options The options array * @param string $key The option key to check * @return string Returns ' checked' if option is '1', empty string otherwise */ private function getCheckedAttr($options, $key) { return (array_key_exists($key, $options) && $options[$key] == '1') ? " checked" : ""; } private function getFallbackOptionDefaults() { return array( 'default_redirect' => '301', 'DB_VERSION' => defined('ABJ404_VERSION') ? ABJ404_VERSION : '', 'menuLocation' => 'optionsLevel', 'admin_theme' => 'default', 'capture_deletion' => '0', 'admin_notification' => '0', 'maximum_log_disk_usage' => '0', 'admin_notification_email' => '', 'suggest_cats' => '0', 'suggest_tags' => '0', 'update_suggest_url' => '0', 'suggest_max' => '5', 'suggest_title' => '', 'suggest_before' => '', 'suggest_after' => '', 'suggest_entrybefore' => '', 'suggest_entryafter' => '', 'suggest_noresults' => '', 'ignore_doprocess' => '', 'ignore_dontprocess' => '', 'recognized_post_types' => 'page', 'recognized_categories' => '', 'folders_files_ignore' => '', 'suggest_regex_exclusions' => '', 'plugin_admin_users' => '', 'auto_score' => '0', 'template_redirect_priority' => '9', 'days_wait_before_major_update' => '0', 'excludePages[]' => '', // These are used by other option cards; harmless defaults. 'auto_deletion' => '0', 'manual_deletion' => '0', ); } private function normalizeOptionsForView($options) { $options = is_array($options) ? $options : array(); return array_merge($this->getFallbackOptionDefaults(), $options); } /** * Normalize a scalar value for table signature comparisons. * * @param mixed $value * @return string */ private function normalizeSignatureValue($value) { if ($value === null) { return ''; } if (is_bool($value)) { return $value ? '1' : '0'; } if (is_int($value) || is_float($value)) { return (string)$value; } if (is_array($value)) { $value = implode(',', array_map(array($this, 'normalizeSignatureValue'), $value)); } $text = (string)$value; $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = preg_replace('/\s+/', ' ', $text); return trim((string)$text); } /** * Build a deterministic row signature payload for a specific admin list subpage. * * @param string $sub * @param array $row * @return array */ private function getSignatureFieldsForSubpage($sub, $row) { $sub = (string)$sub; $row = is_array($row) ? $row : array(); if ($sub === 'abj404_redirects') { return array( 'id' => $this->normalizeSignatureValue($row['id'] ?? ''), 'url' => $this->normalizeSignatureValue($row['url'] ?? ''), 'status' => $this->normalizeSignatureValue($row['status'] ?? ''), 'type' => $this->normalizeSignatureValue($row['type'] ?? ''), 'final_dest' => $this->normalizeSignatureValue($row['final_dest'] ?? ''), 'dest_for_view' => $this->normalizeSignatureValue($row['dest_for_view'] ?? ''), 'code' => $this->normalizeSignatureValue($row['code'] ?? ''), 'logshits' => $this->normalizeSignatureValue($row['logshits'] ?? 0), 'timestamp' => $this->normalizeSignatureValue($row['timestamp'] ?? 0), 'last_used' => $this->normalizeSignatureValue($row['last_used'] ?? 0), ); } if ($sub === 'abj404_captured') { $hits = array_key_exists('logshits', $row) ? $row['logshits'] : ($row['hit_count'] ?? 0); $timestamp = array_key_exists('timestamp', $row) ? $row['timestamp'] : ($row['created'] ?? 0); return array( 'id' => $this->normalizeSignatureValue($row['id'] ?? ''), 'url' => $this->normalizeSignatureValue($row['url'] ?? ''), 'status' => $this->normalizeSignatureValue($row['status'] ?? ''), 'logshits' => $this->normalizeSignatureValue($hits), 'timestamp' => $this->normalizeSignatureValue($timestamp), 'last_used' => $this->normalizeSignatureValue($row['last_used'] ?? 0), ); } if ($sub === 'abj404_logs') { return array( 'id' => $this->normalizeSignatureValue($row['id'] ?? ''), 'url' => $this->normalizeSignatureValue($row['url'] ?? ''), 'url_detail' => $this->normalizeSignatureValue($row['url_detail'] ?? ''), 'remote_host' => $this->normalizeSignatureValue($row['remote_host'] ?? ''), 'referrer' => $this->normalizeSignatureValue($row['referrer'] ?? ''), 'action' => $this->normalizeSignatureValue($row['action'] ?? ''), 'timestamp' => $this->normalizeSignatureValue($row['timestamp'] ?? 0), 'username' => $this->normalizeSignatureValue($row['username'] ?? ''), ); } $normalized = array(); foreach ($row as $k => $v) { if (is_scalar($v) || is_array($v) || $v === null) { $normalized[(string)$k] = $this->normalizeSignatureValue($v); } } ksort($normalized); return $normalized; } /** * Compute and remember a deterministic table signature for detect-only refresh checks. * * @param string $sub * @param array $rows * @return void */ private function rememberTableDataSignature($sub, $rows) { $sub = (string)$sub; if (!is_array($rows)) { $this->tableDataSignatures[$sub] = sha1($sub . '|0'); return; } $rowSignatures = array(); foreach ($rows as $row) { $fields = $this->getSignatureFieldsForSubpage($sub, is_array($row) ? $row : array()); $parts = array(); foreach ($fields as $k => $v) { $parts[] = $k . '=' . $v; } $rowSignatures[] = implode("\x1f", $parts); } sort($rowSignatures, SORT_STRING); $payload = $sub . '|' . count($rowSignatures) . '|' . implode("\n", $rowSignatures); $this->tableDataSignatures[$sub] = sha1($payload); } /** * Get the most recently computed table data signature for a subpage. * * @param string $sub * @return string */ public function getCurrentTableDataSignature($sub) { $sub = (string)$sub; return (string)($this->tableDataSignatures[$sub] ?? ''); } /** * Get plugin options merged with defaults. * * Some tests use partial/mocked PluginLogic instances; this method must not * assume getDefaultOptions() is available or safe to call. * * @return array */ private function getOptionsWithDefaults() { $options = $this->logic->getOptions(); if (!is_array($options)) { $options = array(); } $defaults = array(); if (is_object($this->logic) && method_exists($this->logic, 'getDefaultOptions')) { try { $defaults = $this->logic->getDefaultOptions(); } catch (Throwable $e) { $defaults = array(); } } $defaults = is_array($defaults) && !empty($defaults) ? array_merge($this->getFallbackOptionDefaults(), $defaults) : $this->getFallbackOptionDefaults(); $options = array_merge($defaults, $options); return $options; } /** * Get the tooltip HTML for Hits/Last Used columns when those values may lag. * * We only show this for views sorted by hits/last_used because those modes may * rely on the aggregated logs-hits table. Other sorts use live per-row lookup, * so showing an aggregation timestamp there is misleading. * * @param array $tableOptions Current table options. * @return string Tooltip HTML (not escaped - contains data attributes) */ private function getHitsColumnTooltip($tableOptions = array()) { $orderby = strtolower((string)($tableOptions['orderby'] ?? '')); $isAggregatedMode = ($orderby === 'logshits' || $orderby === 'last_used'); if (!$isAggregatedMode) { return ''; } // Ensure the "last checked"/"refresh scheduled" tooltip state is computed for this request. // This runs cheap checks and (when needed) schedules the expensive rebuild for shutdown. if (is_object($this->dao) && method_exists($this->dao, 'maybeUpdateRedirectsForViewHitsTable')) { $this->dao->maybeUpdateRedirectsForViewHitsTable(); } $timestamp = $this->dao->getLogsHitsTableLastUpdated(); $lines = array(); if ($timestamp !== null) { $lastUpdated = $this->dao->getLogsHitsTableLastUpdatedHuman(); $timeHtml = '' . esc_html($lastUpdated) . ''; $lines[] = sprintf(__('Last updated: %s', '404-solution'), $timeHtml); } $checkedAt = $this->dao->getLogsHitsTableLastCheckedAt(); if ($checkedAt !== null) { $checkedHtml = '' . esc_html($this->formatTimeAgo($checkedAt)) . ''; $lines[] = sprintf(__('Last checked: %s', '404-solution'), $checkedHtml); } $decision = $this->dao->getLogsHitsTableLastDecision(); // Treat "cooldown" as "scheduled recently" from a user perspective. if ($decision === 'scheduled' || $decision === 'cooldown') { $lines[] = __('Refresh scheduled', '404-solution'); } else if ($decision === 'running') { $lines[] = __('Refresh running', '404-solution'); } else if ($decision === 'paused') { $lines[] = __('Refresh paused', '404-solution'); } return implode('
', array_filter($lines)); } /** * Small, dependency-free time-ago formatter for tooltip use. * (We don't want to rely on WP human_time_diff() in unit tests.) * * @param int $timestamp * @return string */ private function formatTimeAgo($timestamp) { $diff = time() - absint($timestamp); if ($diff < 60) { return __('Just now', '404-solution'); } if ($diff < 3600) { $minutes = (int)floor($diff / 60); return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, '404-solution'), $minutes); } if ($diff < 86400) { $hours = (int)floor($diff / 3600); return sprintf(_n('%d hour ago', '%d hours ago', $hours, '404-solution'), $hours); } $days = (int)floor($diff / 86400); return sprintf(_n('%d day ago', '%d days ago', $days, '404-solution'), $days); } /** * Build shared sort state for table headers. * * @param array $tableOptions * @param string $orderby * @param bool $preferDescOnFirstClick * @return array{isSortable:bool,thClass:string,nextOrder:string,indicator:string} */ private function getHeaderSortState($tableOptions, $orderby, $preferDescOnFirstClick = false) { $result = array( 'isSortable' => false, 'thClass' => '', 'nextOrder' => 'ASC', 'indicator' => '', ); $orderby = (string)$orderby; if ($orderby === '') { return $result; } $result['isSortable'] = true; $currentOrderby = (string)($tableOptions['orderby'] ?? ''); $currentOrder = strtoupper((string)($tableOptions['order'] ?? 'ASC')); if ($currentOrder !== 'DESC') { $currentOrder = 'ASC'; } if ($currentOrderby === $orderby) { $result['thClass'] = 'sorted ' . strtolower($currentOrder); $result['nextOrder'] = ($currentOrder === 'ASC') ? 'DESC' : 'ASC'; $result['indicator'] = ($currentOrder === 'ASC') ? ' ↑' : ' ↓'; return $result; } $result['thClass'] = 'sortable ' . ($preferDescOnFirstClick ? 'asc' : 'desc'); $result['nextOrder'] = $preferDescOnFirstClick ? 'DESC' : 'ASC'; return $result; } /** * Build action links for table rows (edit, logs, trash, delete, etc.) * * @param array $row The data row from the database * @param string $sub The subpage parameter value * @param array $tableOptions Table options including filter, orderby, order * @param bool $isCapturedPage True for captured URLs page, false for redirects page * @return array Array of links and titles */ protected function buildTableActionLinks($row, $sub, $tableOptions, $isCapturedPage = false) { $result = []; // Sanitize $sub for safe use in URLs (prevents XSS via quote injection) $sub = rawurlencode($sub); // ID handling differs between pages if ($isCapturedPage) { // Captured page uses raw ID for most links $id = $row['id'] ?? 0; $logsId = $row['logsid'] ?? 0; } else { // Redirects page uses absint for all IDs $id = absint($row['id'] ?? 0); $logsId = absint($row['logsid'] ?? 0); } // Build base links $result['editlink'] = "?page=" . ABJ404_PP . "&subpage=abj404_edit&id=" . $id . "&source_page=" . $sub; $result['logslink'] = "?page=" . ABJ404_PP . "&subpage=abj404_logs&id=" . $logsId; if ($isCapturedPage) { // Captured page - use the dynamic $sub parameter only once $result['trashlink'] = "?page=" . ABJ404_PP . "&id=" . $id . "&subpage=" . $sub; $result['ajaxTrashLink'] = "admin-ajax.php?action=trashLink" . "&id=" . absint($row['id'] ?? 0) . "&subpage=" . $sub; $result['deletelink'] = "?page=" . ABJ404_PP . "&remove=1&id=" . $id . "&subpage=" . $sub; } else { // Redirects page does not have hardcoded subpage $result['trashlink'] = "?page=" . ABJ404_PP . "&id=" . $id . "&subpage=" . $sub; $result['ajaxTrashLink'] = "admin-ajax.php?action=trashLink" . "&id=" . $id . "&subpage=" . $sub; $result['deletelink'] = "?page=" . ABJ404_PP . "&remove=1&id=" . $id . "&subpage=" . $sub; } // Trash/Restore title and action if (is_array($tableOptions) && array_key_exists('filter', $tableOptions) && $tableOptions['filter'] == ABJ404_TRASH_FILTER) { $result['trashlink'] .= "&trash=0"; $result['ajaxTrashLink'] .= "&trash=0"; $result['trashtitle'] = __('Restore', '404-solution'); } else { $result['trashlink'] .= "&trash=1"; $result['ajaxTrashLink'] .= "&trash=1"; $result['trashtitle'] = __('Trash', '404-solution'); } // Captured page has ignore and later links if ($isCapturedPage) { $result['ignorelink'] = "?page=" . ABJ404_PP . "&id=" . $id . "&subpage=" . $sub; $result['laterlink'] = "?page=" . ABJ404_PP . "&id=" . $id . "&subpage=" . $sub; // Ignore title and action $result['ignoretitle'] = ""; if (is_array($tableOptions) && array_key_exists('filter', $tableOptions) && $tableOptions['filter'] == ABJ404_STATUS_IGNORED) { $result['ignorelink'] .= "&ignore=0"; $result['ignoretitle'] = __('Remove Ignore Status', '404-solution'); } else { $result['ignorelink'] .= "&ignore=1"; $result['ignoretitle'] = __('Ignore 404 Error', '404-solution'); } // Later title and action $result['latertitle'] = '?Organize Later?'; if (is_array($tableOptions) && array_key_exists('filter', $tableOptions) && $tableOptions['filter'] == ABJ404_STATUS_LATER) { $result['laterlink'] .= "&later=0"; $result['latertitle'] = __('Remove Later Status', '404-solution'); } else { $result['laterlink'] .= "&later=1"; $result['latertitle'] = __('Organize Later', '404-solution'); } } // Add orderby/order parameters if not default if (is_array($tableOptions) && array_key_exists('orderby', $tableOptions) && array_key_exists('order', $tableOptions)) { if (!($tableOptions['orderby'] == "url" && $tableOptions['order'] == "ASC")) { $result['trashlink'] .= "&orderby=" . sanitize_text_field($tableOptions['orderby']) . "&order=" . sanitize_text_field($tableOptions['order']); $result['deletelink'] .= "&orderby=" . sanitize_text_field($tableOptions['orderby']) . "&order=" . sanitize_text_field($tableOptions['order']); if ($isCapturedPage) { $result['ignorelink'] .= "&orderby=" . sanitize_text_field($tableOptions['orderby']) . "&order=" . sanitize_text_field($tableOptions['order']); $result['laterlink'] .= "&orderby=" . sanitize_text_field($tableOptions['orderby']) . "&order=" . sanitize_text_field($tableOptions['order']); } } } // Add filter parameter if not zero if (is_array($tableOptions) && array_key_exists('filter', $tableOptions) && $tableOptions['filter'] != 0) { $result['trashlink'] .= "&filter=" . $tableOptions['filter']; $result['deletelink'] .= "&filter=" . $tableOptions['filter']; $result['editlink'] .= "&filter=" . $tableOptions['filter']; if ($isCapturedPage) { $result['ignorelink'] .= "&filter=" . $tableOptions['filter']; $result['laterlink'] .= "&filter=" . $tableOptions['filter']; } } // Add orderby/order parameters to edit link if (is_array($tableOptions) && array_key_exists('orderby', $tableOptions) && array_key_exists('order', $tableOptions)) { if (!($tableOptions['orderby'] == "url" && $tableOptions['order'] == "ASC")) { $result['editlink'] .= "&orderby=" . sanitize_text_field($tableOptions['orderby']) . "&order=" . sanitize_text_field($tableOptions['order']); } } // Add paged parameter to edit link if present if (is_array($tableOptions) && array_key_exists('paged', $tableOptions) && $tableOptions['paged'] > 1) { $result['editlink'] .= "&paged=" . $tableOptions['paged']; } // Apply nonces $result['trashlink'] = wp_nonce_url($result['trashlink'], "abj404_trashRedirect"); $result['ajaxTrashLink'] = wp_nonce_url($result['ajaxTrashLink'], "abj404_ajaxTrash"); if (is_array($tableOptions) && array_key_exists('filter', $tableOptions) && $tableOptions['filter'] == ABJ404_TRASH_FILTER) { $result['deletelink'] = wp_nonce_url($result['deletelink'], "abj404_removeRedirect"); } if ($isCapturedPage) { $result['ignorelink'] = wp_nonce_url($result['ignorelink'], "abj404_ignore404"); $result['laterlink'] = wp_nonce_url($result['laterlink'], "abj404_organizeLater"); } return $result; } /** 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. * @global type $abj404view * @global type $abj404logic */ static function handleMainAdminPageActionAndDisplay() { global $abj404view; $instance = self::getInstance(); try { $action = $instance->dao->getPostOrGetSanitize('action'); if (!is_admin() || !$instance->logic->userIsPluginAdmin()) { $instance->logger->logUserCapabilities("handleMainAdminPageActionAndDisplay (" . esc_html($action == '' ? '(none)' : $action) . ")"); 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(); $message .= $instance->logic->handleActionChangeItemsPerRow(); $message .= $instance->logic->handleActionImportFile(); // -------------------------------------------------------------------- // Output the correct page. $abj404view->echoChosenAdminTab($action, $sub, $message); } catch (Exception $e) { $instance->logger->errorMessage("Caught exception: " . stripcslashes(wp_kses_post(json_encode($e)))); throw $e; } } /** Display the chosen admin page. * @global type $abj404view * @param string $sub * @param string $message */ 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->dao->getPostOrGetSanitize('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->dao->getPostOrGetSanitize('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->dao->getPostOrGetSanitize('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. */ 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); // constants and translations. $html = $this->f->doNormalReplacements($html); echo $html; } /** Output the tabs at the top of the plugin page. * @param string $sub * @param string $message */ function outputAdminHeaderTabs($sub = 'list', $message = '') { ABJ_404_Solution_WPNotices::echoAdminNotices(); echo "
"; 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"; } echo ''; } /** 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 $id * @param string $title * @param string $content */ 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 */ 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 quick links navigation bar */ function echoQuickLinks() { $statsUrl = admin_url('admin.php?page=' . ABJ404_PP . '&subpage=abj404_stats'); $capturedUrl = admin_url('admin.php?page=' . ABJ404_PP . '&subpage=abj404_captured'); $redirectsUrl = admin_url('admin.php?page=' . ABJ404_PP . '&subpage=abj404_redirects'); $docsUrl = 'https://ajexoop.com/404-solution/documentation/'; echo ''; } /** * Echo the sticky save bar */ function echoStickySaveBar() { $version = ABJ404_VERSION; echo '
'; echo '
'; /* translators: %s: plugin version number */ echo esc_html(sprintf(__('Plugin v%s', '404-solution'), $version)); echo '
'; echo '
'; echo ''; echo '
'; echo '
'; } /** * Echo the success toast notification container */ function echoToastNotification() { echo '
'; 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) */ 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. */ 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 */ function echoSimpleModeOptions($options) { // Get dest404page dropdown HTML (reuse existing logic) $userSelectedDefault404Page = (array_key_exists('dest404page', $options) && isset($options['dest404page']) ? $options['dest404page'] : null); $urlDestination = (array_key_exists('dest404pageURL', $options) && isset($options['dest404pageURL']) ? $options['dest404pageURL'] : null); $pageTitle = $this->logic->getPageTitleFromIDAndType($userSelectedDefault404Page, $urlDestination); $pageMissingWarning = ""; if ($userSelectedDefault404Page != null) { $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($userSelectedDefault404Page, 0); if (!in_array($permalink['status'], array('publish', 'published'))) { $pageMissingWarning = __("(The specified page doesn't exist. Please update this setting.)", '404-solution'); } } // Build dest404page dropdown $dest404Dropdown = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectPageSearchDropdown.html"); $dest404Dropdown = $this->f->str_replace('{redirect_to_label}', '', $dest404Dropdown); $dest404Dropdown = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', __('(Type a page name or an external URL)', '404-solution'), $dest404Dropdown); $dest404Dropdown = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', __('(A page has been selected.)', '404-solution'), $dest404Dropdown); $dest404Dropdown = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', __('(A custom string has been entered.)', '404-solution'), $dest404Dropdown); $dest404Dropdown = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', __('(An external URL will be used.)', '404-solution'), $dest404Dropdown); $dest404Dropdown = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', $pageMissingWarning, $dest404Dropdown); $dest404Dropdown = $this->f->str_replace('{redirectPageTitle}', $pageTitle, $dest404Dropdown); $dest404Dropdown = $this->f->str_replace('{pageIDAndType}', $userSelectedDefault404Page, $dest404Dropdown); $dest404Dropdown = $this->f->str_replace('{data-url}', "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $dest404Dropdown); $dest404Dropdown = $this->f->doNormalReplacements($dest404Dropdown); // 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' : ''; // 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' : ''; // Read and build the simple options template $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/adminOptionsSimple.html"); // Replace dest404page dropdown $html = $this->f->str_replace('{dest404pageOptions}', $dest404Dropdown, $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); // Replace values $html = $this->f->str_replace('{capture_deletion}', esc_attr($options['capture_deletion']), $html); $html = $this->f->str_replace('{admin_notification}', esc_attr($options['admin_notification']), $html); $html = $this->f->str_replace('{maximum_log_disk_usage}', esc_attr($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); echo $html; } /** Output the stats page. * @global type $wpdb * @global type $abj404dao */ function outputAdminStatsPage() { global $abj404view; $statsSnapshot = $this->dao->getStatsDashboardSnapshot(true); $statsData = (is_array($statsSnapshot) && isset($statsSnapshot['data']) && is_array($statsSnapshot['data'])) ? $statsSnapshot['data'] : array(); $statsHash = (is_array($statsSnapshot) && is_string($statsSnapshot['hash'] ?? null)) ? $statsSnapshot['hash'] : ''; // Main container echo "
"; echo "
"; // Header row with Expand All button echo "
"; echo "

" . esc_html__('Statistics', '404-solution') . "

"; echo "
"; echo ''; echo "
"; echo "
"; // Config for stale-while-refresh stats snapshot updates (no visible table overwrite). echo ''; // Flow layout for stats cards echo "
"; // Redirects Statistics Card $redirectStats = (is_array($statsData) && isset($statsData['redirects']) && is_array($statsData['redirects'])) ? $statsData['redirects'] : array(); $auto301 = intval($redirectStats['auto301'] ?? 0); $auto302 = intval($redirectStats['auto302'] ?? 0); $manual301 = intval($redirectStats['manual301'] ?? 0); $manual302 = intval($redirectStats['manual302'] ?? 0); $trashed = intval($redirectStats['trashed'] ?? 0); $total = $auto301 + $auto302 + $manual301 + $manual302 + $trashed; $content = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/statsRedirectsBox.html"); $content = $this->f->str_replace('{auto301}', esc_html($auto301), $content); $content = $this->f->str_replace('{auto302}', esc_html($auto302), $content); $content = $this->f->str_replace('{manual301}', esc_html($manual301), $content); $content = $this->f->str_replace('{manual302}', esc_html($manual302), $content); $content = $this->f->str_replace('{trashed}', esc_html($trashed), $content); $content = $this->f->str_replace('{total}', esc_html($total), $content); $content = $this->f->doNormalReplacements($content); $abj404view->echoOptionsSection('stats-redirects', 'abj404-redirectStats', __('Redirects', '404-solution'), $content, true, $abj404view->getCardIcon('chart')); // Captured URLs Statistics Card $capturedStats = (is_array($statsData) && isset($statsData['captured']) && is_array($statsData['captured'])) ? $statsData['captured'] : array(); $captured = intval($capturedStats['captured'] ?? 0); $ignored = intval($capturedStats['ignored'] ?? 0); $trashed = intval($capturedStats['trashed'] ?? 0); $total = $captured + $ignored + $trashed; $content = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/statsCapturedURLsBox.html"); $content = $this->f->str_replace('{captured}', esc_html($captured), $content); $content = $this->f->str_replace('{ignored}', esc_html($ignored), $content); $content = $this->f->str_replace('{trashed}', esc_html($trashed), $content); $content = $this->f->str_replace('{total}', esc_html($total), $content); $content = $this->f->doNormalReplacements($content); $abj404view->echoOptionsSection('stats-captured', 'abj404-capturedStats', __('Captured URLs', '404-solution'), $content, true, $abj404view->getCardIcon('warning')); // Periodic Stats Cards $periodicStats = (is_array($statsData) && isset($statsData['periods']) && is_array($statsData['periods'])) ? $statsData['periods'] : array(); $periodMeta = array( array('title' => __("Today's Stats", '404-solution'), 'key' => 'today'), array('title' => __("This Month", '404-solution'), 'key' => 'month'), array('title' => __("This Year", '404-solution'), 'key' => 'year'), array('title' => __("All Stats", '404-solution'), 'key' => 'all'), ); for ($x = 0; $x <= 3; $x++) { $title = $periodMeta[$x]['title']; $periodKey = $periodMeta[$x]['key']; $periodStats = (is_array($periodicStats) && isset($periodicStats[$periodKey]) && is_array($periodicStats[$periodKey])) ? $periodicStats[$periodKey] : array(); $disp404 = intval($periodStats['disp404'] ?? 0); $distinct404 = intval($periodStats['distinct404'] ?? 0); $visitors404 = intval($periodStats['visitors404'] ?? 0); $refer404 = intval($periodStats['refer404'] ?? 0); $redirected = intval($periodStats['redirected'] ?? 0); $distinctredirected = intval($periodStats['distinctredirected'] ?? 0); $distinctvisitors = intval($periodStats['distinctvisitors'] ?? 0); $distinctrefer = intval($periodStats['distinctrefer'] ?? 0); $content = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/statsPeriodicBox.html"); $content = $this->f->str_replace('{disp404}', esc_html($disp404), $content); $content = $this->f->str_replace('{distinct404}', esc_html($distinct404), $content); $content = $this->f->str_replace('{visitors404}', esc_html($visitors404), $content); $content = $this->f->str_replace('{refer404}', esc_html($refer404), $content); $content = $this->f->str_replace('{redirected}', esc_html($redirected), $content); $content = $this->f->str_replace('{distinctredirected}', esc_html($distinctredirected), $content); $content = $this->f->str_replace('{distinctvisitors}', esc_html($distinctvisitors), $content); $content = $this->f->str_replace('{distinctrefer}', esc_html($distinctrefer), $content); $content = $this->f->doNormalReplacements($content); $abj404view->echoOptionsSection('stats-periodic-' . $x, 'abj404-stats' . $x, $title, $content, ($x == 0), $abj404view->getCardIcon('clock')); } echo "
"; // Close flow layout echo "
"; // Close settings content echo "
"; // Close container } function echoAdminDebugFile() { if ($this->logic->userIsPluginAdmin()) { $filesToEcho = array($this->logger->getDebugFilePath(), $this->logger->getDebugFilePathOld()); for ($i = 0; $i < count($filesToEcho); $i++) { $currentFile = $filesToEcho[$i]; echo "
"; echo "
Contents of: " . $currentFile . ":

"; // read the file and replace new lines with
. $this->echoFileContents($currentFile); echo "
"; } } else { echo "Non-admin request to view debug file."; $current_user = wp_get_current_user(); $userInfo = "Login: " . ($current_user->user_login ?? '') . ", display name: " . ($current_user->display_name ?? '') . ", Email: " . ($current_user->user_email ?? '') . ", UserID: " . ($current_user->ID ?? ''); $this->logger->infoMessage("Non-admin request to view debug file. User info: " . $userInfo); } } function echoFileContents($fileName) { if (!is_string($fileName)) { $fileName = ''; } if (file_exists($fileName)) { $linesRead = 0; $handle = null; try { if ($handle = fopen($fileName, "r")) { // read the file one line at a time. while (($line = fgets($handle)) !== false) { $linesRead++; echo nl2br(esc_html($line)); if ($linesRead > 1000000) { echo "

Read " . $linesRead . " lines. Download debug file to see more."; break; } } } else { $this->logger->errorMessage("Error opening debug file."); } } catch (Exception $e) { $this->logger->errorMessage("Error while reading debug file.", $e); } if ($handle != null) { fclose($handle); } } else { echo nl2br(__('(The log file does not exist.)', '404-solution')); } } /** Display the tools page. * @global type $abj404view */ function echoAdminToolsPage() { global $abj404view; // Main container echo "
"; echo "
"; // Header row with Expand All button echo "
"; echo "

" . esc_html__('Tools', '404-solution') . "

"; echo "
"; echo ''; echo "
"; echo "
"; // Export Card $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_exportRedirects"); $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsExportForm.html"); $html = $this->f->str_replace('{toolsExportRedirectsLink}', $link, $html); $html = $this->f->doNormalReplacements($html); $abj404view->echoOptionsSection('tools-export', 'abj404-exportRedirects', __('Export', '404-solution'), $html, true, $abj404view->getCardIcon('download')); // Import Card $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_importRedirectsFile"); $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsImportForm.html"); $html = $this->f->str_replace('{toolsImportRedirectsLink}', $link, $html); $html = $this->f->doNormalReplacements($html); $abj404view->echoOptionsSection('tools-import', 'abj404-importRedirects', __('Import', '404-solution'), $html, false, $abj404view->getCardIcon('upload')); // Purge Card $url = "?page=" . ABJ404_PP . "&subpage=abj404_tools"; $link = wp_nonce_url($url, "abj404_purgeRedirects"); $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsPurgeForm.html"); $html = $this->f->str_replace('{toolsPurgeFormActionLink}', $link, $html); $html = $this->f->doNormalReplacements($html); $abj404view->echoOptionsSection('tools-purge', 'abj404-purgeRedirects', __('Purge Options', '404-solution'), $html, false, $abj404view->getCardIcon('trash')); // Cache Management Card $ngramLink = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_rebuildNgramCache"); $spellingLink = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_clearSpellingCache"); $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsCacheForm.html"); $html = $this->f->str_replace('{toolsNgramCacheFormActionLink}', $ngramLink, $html); $html = $this->f->str_replace('{toolsSpellingCacheFormActionLink}', $spellingLink, $html); $html = $this->f->doNormalReplacements($html); $abj404view->echoOptionsSection('tools-cache', 'abj404-cacheTools', __('Cache Management', '404-solution'), $html, false, $abj404view->getCardIcon('database')); // Diagnostics Card $html = $this->getToolsDiagnosticsMarkup(); $abj404view->echoOptionsSection('tools-diagnostics', 'abj404-diagnosticsTools', __('Diagnostics', '404-solution'), $html, false, $abj404view->getCardIcon('warning')); // Etcetera Card $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_runMaintenance"); $link .= '&manually_fired=true'; $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsEtcForm.html"); $html = $this->f->str_replace('{toolsMaintenanceFormActionLink}', $link, $html); $html = $this->f->doNormalReplacements($html); $abj404view->echoOptionsSection('tools-etc', 'abj404-etcTools', __('Etcetera', '404-solution'), $html, false, $abj404view->getCardIcon('cog')); echo "
"; echo "
"; } /** * Build compact diagnostics markup for the Tools page. * This is intentionally read-only and lightweight. * * @return string */ private function getToolsDiagnosticsMarkup() { $rows = $this->getToolsDiagnosticsRows(); $html = '
'; $html .= '

' . esc_html__('Quick environment checks for troubleshooting and support.', '404-solution') . '

'; $html .= ''; foreach ($rows as $row) { $label = array_key_exists('label', $row) ? $row['label'] : ''; $value = array_key_exists('value', $row) ? $row['value'] : ''; $valueHtml = array_key_exists('value_html', $row) ? $row['value_html'] : ''; $status = array_key_exists('status', $row) ? $row['status'] : 'info'; $statusLabel = ($status === 'ok') ? __('OK', '404-solution') : (($status === 'warn') ? __('Warning', '404-solution') : __('Info', '404-solution')); $statusClass = ($status === 'ok') ? 'abj404-pill-success' : (($status === 'warn') ? 'abj404-pill-warning' : 'abj404-pill-info'); $html .= ''; $html .= ''; if ($valueHtml !== '') { $html .= ''; } else { $html .= ''; } $html .= ''; $html .= ''; } $html .= '
' . esc_html($label) . '' . wp_kses_post($valueHtml) . '' . esc_html($value) . '' . esc_html($statusLabel) . '
'; $html .= '
'; return $html; } /** * Collect diagnostics fields displayed on the Tools page. * * @return array> */ private function getToolsDiagnosticsRows() { $wpVersion = get_bloginfo('version'); if (!is_string($wpVersion) || trim($wpVersion) === '') { $wpVersion = __('Unknown', '404-solution'); } $uploadDir = ''; if (function_exists('abj404_getUploadsDir')) { $uploadDir = (string)abj404_getUploadsDir(); } $uploadDirReadable = ($uploadDir !== '' && is_dir($uploadDir)); $uploadDirWritable = ($uploadDir !== '' && is_writable($uploadDir)); $rows = array(); $pluginVersion = defined('ABJ404_VERSION') ? ABJ404_VERSION : __('Unknown', '404-solution'); $rows[] = array('label' => __('Plugin Version', '404-solution'), 'value' => $pluginVersion, 'status' => 'info'); $rows[] = array('label' => __('WordPress Version', '404-solution'), 'value' => $wpVersion, 'status' => 'info'); $rows[] = array('label' => __('PHP Version', '404-solution'), 'value' => PHP_VERSION, 'status' => 'info'); $rows[] = array( 'label' => __('Uploads Directory', '404-solution'), 'value' => ($uploadDir !== '') ? $uploadDir : __('Not available', '404-solution'), 'status' => $uploadDirReadable ? 'ok' : 'warn', ); $rows[] = array( 'label' => __('Uploads Writable', '404-solution'), 'value' => $uploadDirWritable ? __('Yes', '404-solution') : __('No', '404-solution'), 'status' => $uploadDirWritable ? 'ok' : 'warn', ); $rows[] = array( 'label' => __('mbstring Extension', '404-solution'), 'value' => extension_loaded('mbstring') ? __('Loaded', '404-solution') : __('Missing', '404-solution'), 'status' => extension_loaded('mbstring') ? 'ok' : 'warn', ); $rows[] = array( 'label' => __('ZipArchive Support', '404-solution'), 'value' => class_exists('ZipArchive') ? __('Available', '404-solution') : __('Missing', '404-solution'), 'status' => class_exists('ZipArchive') ? 'ok' : 'warn', ); $rows[] = array( 'label' => __('WP_DEBUG', '404-solution'), 'value' => (defined('WP_DEBUG') && WP_DEBUG) ? __('Enabled', '404-solution') : __('Disabled', '404-solution'), 'status' => 'info', ); if (function_exists('abj404_is_local_debug_host') && function_exists('abj404_get_simulated_db_latency_ms') && abj404_is_local_debug_host()) { $latencyMs = absint(abj404_get_simulated_db_latency_ms()); $latencyUrls = array( 250 => wp_nonce_url(admin_url('options-general.php?page=' . ABJ404_PP . '&subpage=abj404_tools&abj404_set_sim_db_ms=250'), 'abj404_set_sim_db_ms'), 500 => wp_nonce_url(admin_url('options-general.php?page=' . ABJ404_PP . '&subpage=abj404_tools&abj404_set_sim_db_ms=500'), 'abj404_set_sim_db_ms'), 900 => wp_nonce_url(admin_url('options-general.php?page=' . ABJ404_PP . '&subpage=abj404_tools&abj404_set_sim_db_ms=900'), 'abj404_set_sim_db_ms'), 0 => wp_nonce_url(admin_url('options-general.php?page=' . ABJ404_PP . '&subpage=abj404_tools&abj404_set_sim_db_ms=0'), 'abj404_set_sim_db_ms'), ); $controls = '' . esc_html(__('250ms', '404-solution')) . '' . ' | ' . esc_html(__('500ms', '404-solution')) . '' . ' | ' . esc_html(__('900ms', '404-solution')) . '' . ' | ' . esc_html(__('Disable', '404-solution')) . ''; $rows[] = array( 'label' => __('Simulated DB Latency', '404-solution'), 'value' => ($latencyMs > 0) ? sprintf(__('ON (%d ms per plugin query)', '404-solution'), $latencyMs) : __('OFF', '404-solution'), 'value_html' => '
' . esc_html(($latencyMs > 0) ? sprintf(__('ON (%d ms per plugin query)', '404-solution'), $latencyMs) : __('OFF', '404-solution')) . '
' . $controls . '
', 'status' => ($latencyMs > 0) ? 'warn' : 'info', ); } return $rows; } function echoAdminOptionsPage() { global $abj404view; global $abj404viewSuggestions; // If globals are not set, use sensible defaults if ($abj404view === null) { $abj404view = $this; } $options = $this->getOptionsWithDefaults(); // Get the current user's settings mode preference $settingsMode = $this->logic->getSettingsMode(); // if the current URL does not match the chosen menuLocation then redirect to the correct URL $helperFunctions = ABJ_404_Solution_Functions::getInstance(); $urlParts = parse_url($helperFunctions->normalizeUrlString($_SERVER['REQUEST_URI'] ?? '')); $currentURL = (is_array($urlParts) && isset($urlParts['path'])) ? $urlParts['path'] : ''; if (is_array($options) && isset($options['menuLocation']) && $options['menuLocation'] == 'settingsLevel') { if ($this->f->strpos($currentURL, 'options-general.php') !== false) { // the option changed and we're at the wrong URL now, so we redirect to the correct one. $this->logic->forceRedirect(admin_url() . "admin.php?page=" . ABJ404_PP . '&subpage=abj404_options'); } } else if ($this->f->strpos($currentURL, 'admin.php') !== false) { // if the current URL has admin.php then the URLs don't match and we need to reload. $this->logic->forceRedirect(admin_url() . "options-general.php?page=" . ABJ404_PP . '&subpage=abj404_options'); } // Toast notification container $abj404view->echoToastNotification(); // Options page header with mode toggle and expand button echo "\n
"; echo "

" . esc_html__('Options', '404-solution') . "

"; echo "
"; $this->echoInlineModeToggle(); // Expand/Collapse All button for both Simple and Advanced modes echo ''; echo "
"; echo "
"; // Main container echo "
"; echo "
"; $formBeginning = '
' . "\n"; $formBeginning .= '' . "\n"; $formBeginning .= '' . "\n"; $formBeginning = $this->f->str_replace('{data-url}', "admin-ajax.php?action=updateOptions", $formBeginning); echo $formBeginning; // Add loading overlay for save operations echo ''; if ($settingsMode === 'simple') { // Simple Mode: Show streamlined options with card layout $abj404view->echoSimpleModeOptions($options); } else { // Advanced Mode: Show all card sections with icons // Render each section with card structure and icons $contentAutomaticRedirects = $abj404view->getAdminOptionsPageAutoRedirects($options); $abj404view->echoOptionsSection( "abj404-autooptions", "abj404-autooptions", __('Automatic Redirects', '404-solution'), $contentAutomaticRedirects, true, $abj404view->getCardIcon('lightning') ); $contentGeneralSettings = $abj404view->getAdminOptionsPageGeneralSettings($options); $abj404view->echoOptionsSection( "abj404-generaloptions", "abj404-generaloptions", __('General Settings', '404-solution'), $contentGeneralSettings, true, $abj404view->getCardIcon('gear') ); $contentAdvancedContent = $abj404view->getAdminOptionsPageAdvancedContent($options); $abj404view->echoOptionsSection( "abj404-advanced-content", "abj404-advanced-content", __('Content & URL Filtering', '404-solution'), $contentAdvancedContent, true, $abj404view->getCardIcon('filter') ); $contentAdvancedLogging = $abj404view->getAdminOptionsPageAdvancedLogging($options); $abj404view->echoOptionsSection( "abj404-advanced-logging", "abj404-advanced-logging", __('Logging & Privacy', '404-solution'), $contentAdvancedLogging, true, $abj404view->getCardIcon('document') ); $contentAdvancedSystem = $abj404view->getAdminOptionsPageAdvancedSystem($options); $abj404view->echoOptionsSection( "abj404-advanced-system", "abj404-advanced-system", __('Advanced Configuration', '404-solution'), $contentAdvancedSystem, true, $abj404view->getCardIcon('sliders') ); // Only render suggestions section if the suggestions view is available if ($abj404viewSuggestions !== null && method_exists($abj404viewSuggestions, 'getAdminOptionsPage404Suggestions')) { $content404PageSuggestions = $abj404viewSuggestions->getAdminOptionsPage404Suggestions($options); $abj404view->echoOptionsSection( "abj404-suggestoptions", "abj404-suggestoptions", __('404 Page Suggestions', '404-solution'), $content404PageSuggestions, true, $abj404view->getCardIcon('lightbulb') ); } } // Sticky save bar $abj404view->echoStickySaveBar(); echo "
"; echo "
"; // end abj404-settings-content echo "
"; // end abj404-container } /** * @global type $abj404dao * @global type $abj404logic */ function echoAdminEditRedirectPage() { $options = $this->getOptionsWithDefaults(); // Modern page container echo '
'; echo '
'; echo '

' . esc_html__('Edit Redirect', '404-solution') . '

'; $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_edit", "abj404editRedirect"); echo '
'; echo ""; // Capture source page and table options to return user to the same place after saving $source_page = $this->dao->getPostOrGetSanitize('source_page'); if ($source_page === null) { $source_page = $this->dao->getPostOrGetSanitize('subpage'); } // Default to redirects page if no source specified if ($source_page === null || $source_page == 'abj404_edit') { $source_page = 'abj404_redirects'; } echo ""; // Preserve table options so we can return to the exact same view $filter = $this->dao->getPostOrGetSanitize('filter'); if ($filter !== null) { echo ""; } $orderby = $this->dao->getPostOrGetSanitize('orderby'); if ($orderby !== null) { echo ""; } $order = $this->dao->getPostOrGetSanitize('order'); if ($order !== null) { echo ""; } $paged = $this->dao->getPostOrGetSanitize('paged'); if ($paged !== null) { echo ""; } $recnum = null; $recnums_multiple = null; if (isset($_GET['id']) && $this->f->regexMatch('[0-9]+', $_GET['id'])) { $this->logger->debugMessage("Edit redirect page. GET ID: " . wp_kses_post(json_encode($_GET['id']))); $recnum = absint($_GET['id']); } else if (isset($_POST['id']) && $this->f->regexMatch('[0-9]+', $_POST['id'])) { $this->logger->debugMessage("Edit redirect page. POST ID: " . wp_kses_post(json_encode($_POST['id']))); $recnum = absint($_POST['id']); } else if ($this->dao->getPostOrGetSanitize('idnum') !== null) { $recnums_multiple = array_map('absint', (array)$this->dao->getPostOrGetSanitize('idnum')); $this->logger->debugMessage("Edit redirect page. ids_multiple: " . wp_kses_post(json_encode($recnums_multiple))); } else { echo __('Error: No ID(s) found for edit request.', '404-solution'); $this->logger->debugMessage("No ID(s) found in GET or POST data for edit request."); return; } // Decide whether we're editing one or multiple redirects. // If we're editing only one then set the ID to that one value. if ($recnum != null) { $recnumAsArray = array(); $recnumAsArray[] = $recnum; $redirects_multiple = $this->dao->getRedirectsByIDs($recnumAsArray); if (empty($redirects_multiple)) { echo "Error: Invalid ID Number! (id: " . esc_html($recnum) . ")"; $this->logger->errorMessage("Error: Invalid ID Number! (id: " . esc_html($recnum) . ")"); return; } $redirect = reset($redirects_multiple); $isRegexChecked = ''; if ($redirect['status'] == ABJ404_STATUS_REGEX) { $isRegexChecked = ' checked '; } echo ''; // URL field echo '
'; echo ''; echo ''; echo '
'; // Regex checkbox echo '
'; echo '
'; echo ''; echo ''; echo ' ' . esc_html__('(Explain)', '404-solution') . ''; echo '
'; echo ''; echo '
'; } else if ($recnums_multiple != null) { $redirects_multiple = $this->dao->getRedirectsByIDs($recnums_multiple); if ($redirects_multiple == null) { echo "Error: Invalid ID Numbers! (ids: " . esc_html(implode(',', $recnums_multiple)) . ")"; $this->logger->debugMessage("Error: Invalid ID Numbers! (ids: " . esc_html(implode(',', $recnums_multiple)) . ")"); return; } echo ''; // Bulk URL list echo '
'; echo ''; echo '
'; echo '
    '; foreach ($redirects_multiple as $redirect) { echo '
  • ' . esc_html($redirect['url']) . '
  • '; } echo '
'; echo '
'; echo '
'; // here we set the variable to the first value returned because it's used to set default values // in the form data. $redirect = reset($redirects_multiple); } else { $idsText = is_array($recnums_multiple) ? implode(',', $recnums_multiple) : (string)$recnums_multiple; echo "Error: Invalid ID Number(s) specified! (id: " . esc_html((string)$recnum) . ", ids: " . esc_html($idsText) . ")"; $this->logger->debugMessage("Error: Invalid ID Number(s) specified! (id: " . esc_html((string)$recnum) . ", ids: " . esc_html($idsText) . ")"); return; } $final = ""; $pageIDAndType = ""; $redirectType = $redirect['type'] ?? null; $redirectFinalDest = $redirect['final_dest'] ?? 0; if ($redirectType == ABJ404_TYPE_EXTERNAL) { $final = $redirectFinalDest; $pageIDAndType = ABJ404_TYPE_EXTERNAL . "|" . ABJ404_TYPE_EXTERNAL; } else if ($redirectFinalDest != 0) { // if a destination has been specified then let's fill it in. $pageIDAndType = $redirectFinalDest . "|" . $redirectType; } else if ($redirectType == ABJ404_TYPE_404_DISPLAYED) { $pageIDAndType = ABJ404_TYPE_404_DISPLAYED . "|" . ABJ404_TYPE_404_DISPLAYED; } if (($redirect['code'] ?? '') == "") { $codeSelected = $options['default_redirect'] ?? '301'; } else { $codeSelected = $redirect['code'] ?? ''; } $pageTitle = $this->logic->getPageTitleFromIDAndType($pageIDAndType, $redirectFinalDest); $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectPageSearchDropdown.html"); $html = $this->f->str_replace('{redirect_to_label}', __('Redirect to', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', __('(Type a page name or an external URL)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', __('(A page has been selected.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', __('(A custom string has been entered.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', __('(An external URL will be used.)', '404-solution'), $html); $html = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', '', $html); $html = $this->f->str_replace('{redirectPageTitle}', $pageTitle, $html); $html = $this->f->str_replace('{pageIDAndType}', $pageIDAndType, $html); $html = $this->f->str_replace('{data-url}', "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $html); $html = $this->f->doNormalReplacements($html); echo $html; $this->echoEditRedirect($final, $codeSelected, __('Update Redirect', '404-solution'), $source_page, $filter, $orderby, $order); echo '
'; echo '
'; // end abj404-edit-container echo '
'; // end abj404-edit-page } function echoRedirectDestinationOptionsOthers($dest, $rows) { $content = array(); $rowCounter = 0; $currentPostType = ''; foreach ($rows as $row) { $rowCounter++; $id = $row->id; $theTitle = get_the_title($id); $thisval = $id . "|" . ABJ404_TYPE_POST; $selected = ""; if ($thisval == $dest) { $selected = " selected"; } $_REQUEST[ABJ404_PP]['debug_info'] = 'Before row: ' . $rowCounter . ', Title: ' . $theTitle . ', Post type: ' . $row->post_type; if ($row->post_type != $currentPostType) { if ($currentPostType != '') { $content[] = "\n" . '' . "\n"; } $content[] = "\n" . '' . "\n"; $currentPostType = $row->post_type; } // this is split in this ridiculous way to help me figure out how to resolve a memory issue. // (https://wordpress.org/support/topic/options-tab-is-not-loading/) $content[] = "\n "; $_REQUEST[ABJ404_PP]['debug_info'] = 'After row: ' . $rowCounter . ', Title: ' . $theTitle . ', Post type: ' . $row->post_type; } $content[] = "\n" . '' . "\n"; $_REQUEST[ABJ404_PP]['debug_info'] = 'Cleared after building redirect destination page list.'; return implode('', $content); } function echoRedirectDestinationOptionsCatsTags($dest) { $content = ""; $content .= "\n" . '' . "\n"; $customTagsEtc = array(); // categories --------------------------------------------- $cats = $this->dao->getPublishedCategories(); foreach ($cats as $cat) { $taxonomy = $cat->taxonomy; if ($taxonomy != 'category') { continue; } $id = $cat->term_id; $theTitle = $cat->name; $thisval = $id . "|" . ABJ404_TYPE_CAT; $selected = ""; if ($thisval == $dest) { $selected = " selected"; } $content .= "\n"; } $content .= "\n" . '' . "\n"; $customTagsEtc = $this->logic->getMapOfCustomCategories($cats); // tags --------------------------------------------- $content .= "\n" . '' . "\n"; $tags = $this->dao->getPublishedTags(); foreach ($tags as $tag) { $id = $tag->term_id; $theTitle = $tag->name; $thisval = $id . "|" . ABJ404_TYPE_TAG; $selected = ""; if ($thisval == $dest) { $selected = " selected"; } $content .= "\n"; } $content .= "\n" . '' . "\n"; // custom --------------------------------------------- foreach ($customTagsEtc as $taxonomy => $catRow) { $content .= "\n" . '' . "\n"; foreach ($catRow as $cat) { $id = $cat->term_id; $theTitle = $cat->name; $thisval = $id . "|" . ABJ404_TYPE_CAT; $selected = ""; if ($thisval == $dest) { $selected = " selected"; } $content .= "\n"; } $content .= "\n" . '' . "\n"; } return $content; } /** * @global type $abj404dao */ function echoAdminCapturedURLsPage() { $sub = 'abj404_captured'; $tableOptions = $this->logic->getTableOptions($sub); $timezone = get_option('timezone_string'); if ('' == $timezone) { $timezone = 'UTC'; } date_default_timezone_set($timezone); // Get counts for tabs $counts = $this->dao->getCapturedStatusCounts(); // Modern page wrapper echo '
'; // Header with page title echo '
'; echo '

' . __('Captured 404 URLs', '404-solution') . '

'; echo '
'; // Content tabs (Captured, Ignored, Later, Trash) echo '
'; $baseUrl = "?page=" . ABJ404_PP . "&subpage=abj404_captured"; $baseUrl .= "&orderby=" . sanitize_text_field($tableOptions['orderby'] ?? 'url'); $baseUrl .= "&order=" . sanitize_text_field($tableOptions['order'] ?? 'ASC'); // All tab $this->echoContentTab('abj404_captured', 0, __('All', '404-solution'), $counts['all'] ?? 0, $tableOptions); // Captured tab $this->echoContentTab('abj404_captured', ABJ404_STATUS_CAPTURED, __('Captured', '404-solution'), $counts['captured'] ?? 0, $tableOptions); // Ignored tab $this->echoContentTab('abj404_captured', ABJ404_STATUS_IGNORED, __('Ignored', '404-solution'), $counts['ignored'] ?? 0, $tableOptions); // Organize Later tab $this->echoContentTab('abj404_captured', ABJ404_STATUS_LATER, __('Later', '404-solution'), $counts['later'] ?? 0, $tableOptions); // Trash tab $this->echoContentTab('abj404_captured', ABJ404_TRASH_FILTER, __('Trash', '404-solution'), $counts['trash'] ?? 0, $tableOptions); echo '
'; // Filter bar with server-side search $filterText = isset($tableOptions['filterText']) ? $tableOptions['filterText'] : ''; $perPage = isset($tableOptions['perpage']) ? $tableOptions['perpage'] : 25; $paginationNonce = wp_create_nonce('abj404_updatePaginationLink'); $autoRefresh = (($sub === 'abj404_redirects' || $sub === 'abj404_captured' || $sub === 'abj404_logs') ? '1' : '0'); echo '
'; echo ''; echo '
'; echo '' . esc_html__('Rows per page:', '404-solution') . ''; echo ''; echo '
'; // Empty trash button $currentFilter = $tableOptions['filter'] ?? 0; if ($currentFilter == ABJ404_TRASH_FILTER) { $eturl = "?page=" . ABJ404_PP . "&subpage=abj404_captured&filter=" . ABJ404_TRASH_FILTER; $eturl = wp_nonce_url($eturl, 'abj404_bulkProcess'); echo ''; echo esc_html__('Empty Trash', '404-solution'); echo ''; } echo ''; echo '
'; // Bulk actions bar $url = $this->getBulkOperationsFormURL($sub, $tableOptions); echo '
'; echo '
0 ' . __('selected', '404-solution') . '
'; echo '
'; // Bulk action buttons based on current filter if ($currentFilter != ABJ404_STATUS_CAPTURED) { echo ''; } if ($currentFilter != ABJ404_STATUS_IGNORED) { echo ''; } if ($currentFilter != ABJ404_STATUS_LATER) { echo ''; } if ($currentFilter != ABJ404_TRASH_FILTER) { echo ''; } echo ''; echo '
'; echo ''; echo '
'; // Hidden form for bulk actions echo '
'; wp_nonce_field('abj404_bulkProcess'); // Table echo $this->getCapturedURLSPageTable($sub); // Pagination (using original AJAX-integrated pagination) echo $this->getPaginationLinks($sub, false); echo '
'; echo '
'; } function getCapturedURLSPageTable($sub) { $tableOptions = $this->logic->getTableOptions($sub); // Build column headers with sorting $hitsTooltip = $this->getHitsColumnTooltip($tableOptions); $columns = array( 'url' => array('title' => __('URL', '404-solution'), 'orderby' => 'url'), 'status' => array('title' => __('Status', '404-solution'), 'orderby' => 'status'), 'hits' => array('title' => __('Hits', '404-solution'), 'orderby' => 'logshits', 'title_attr_html' => $hitsTooltip), 'timestamp' => array('title' => __('Created', '404-solution'), 'orderby' => 'timestamp', 'class' => 'hide-on-tablet'), 'last_used' => array('title' => __('Last Used', '404-solution'), 'orderby' => 'last_used', 'title_attr_html' => $hitsTooltip), ); $html = ''; $html .= ''; $html .= ''; // Generate sortable column headers foreach ($columns as $key => $col) { $sortUrl = "?page=" . ABJ404_PP . "&subpage=abj404_captured&filter=" . ($tableOptions['filter'] ?? 0); $sortUrl .= "&orderby=" . $col['orderby']; $sortState = $this->getHeaderSortState($tableOptions, (string)$col['orderby'], false); $newOrder = $sortState['nextOrder']; $sortUrl .= "&order=" . $newOrder; $extraClass = isset($col['class']) ? ' ' . esc_attr($col['class']) : ''; $sortClass = trim($sortState['thClass'] . $extraClass); $sortIndicator = $sortState['indicator']; $classAttr = $sortClass ? ' class="' . trim($sortClass) . '"' : ''; // Build tooltip HTML if present $tooltipHtml = ''; if (isset($col['title_attr_html']) && !empty($col['title_attr_html'])) { // Raw HTML (already escaped where needed) $tooltipHtml = '' . '' . '' . $col['title_attr_html'] . '' . ''; } elseif (isset($col['title_attr']) && !empty($col['title_attr'])) { // Plain text - escape it $tooltipHtml = '' . '' . '' . esc_html($col['title_attr']) . '' . ''; } $html .= ''; } $html .= ''; $html .= ''; $rows = $this->dao->getRedirectsForView($sub, $tableOptions); $this->rememberTableDataSignature($sub, $rows); $displayed = 0; foreach ($rows as $row) { $displayed++; $hits = $row['logshits'] ?? 0; $last_used = $row['last_used'] ?? 0; $lastUsedClass = ''; if ($last_used != 0) { $last = date("Y/m/d h:i:s A", abs(intval($last_used))); } else { $last = __('Never', '404-solution'); $lastUsedClass = 'abj404-never-used'; } // Build action links using helper method $links = $this->buildTableActionLinks($row, $sub, $tableOptions, true); extract($links); // Determine status badge $statusBadgeClass = 'abj404-badge-captured'; $statusText = __('Captured', '404-solution'); $statusTitle = __('Captured 404 URL', '404-solution'); if ($row['status'] == ABJ404_STATUS_IGNORED) { $statusBadgeClass = 'abj404-badge-ignored'; $statusText = __('Ignored', '404-solution'); $statusTitle = __('Ignored URL - will not be suggested', '404-solution'); } else if ($row['status'] == ABJ404_STATUS_LATER) { $statusBadgeClass = 'abj404-badge-later'; $statusText = __('Later', '404-solution'); $statusTitle = __('Organize Later', '404-solution'); } // Build row action buttons $editBtnHTML = ''; $logsBtnHTML = ''; $trashBtnHTML = ''; $deleteBtnHTML = ''; $ignoreBtnHTML = ''; $laterBtnHTML = ''; $currentFilter = $tableOptions['filter'] ?? 0; if ($currentFilter != ABJ404_TRASH_FILTER) { $editBtnHTML = '' . ' ' . esc_html__('Edit', '404-solution') . ''; } if (($row['logsid'] ?? 0) > 0) { $logsBtnHTML = '' . ' ' . esc_html__('Logs', '404-solution') . ''; } if ($currentFilter != ABJ404_TRASH_FILTER) { $trashBtnHTML = '' . ' ' . esc_html__('Trash', '404-solution') . ''; } if ($currentFilter == ABJ404_TRASH_FILTER) { // Show Restore button $trashBtnHTML = '' . ' ' . esc_html__('Restore', '404-solution') . ''; $deleteBtnHTML = ' | ' . ' ' . esc_html__('Delete', '404-solution') . ''; } else { // Show Ignore and Later buttons (with separators) if ($row['status'] != ABJ404_STATUS_IGNORED) { $ignoreBtnHTML = ' | ' . ' ' . esc_html__('Ignore', '404-solution') . ''; } if ($row['status'] != ABJ404_STATUS_LATER) { $laterBtnHTML = ' | ' . ' ' . esc_html__('Later', '404-solution') . ''; } } // Build full URL for visiting $fullVisitorURL = esc_url(home_url($row['url'])); $tempHtml = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/tableRowCapturedURLs.html"); $tempHtml = $this->f->str_replace('{rowid}', $row['id'], $tempHtml); $tempHtml = $this->f->str_replace('{rowClass}', '', $tempHtml); $tempHtml = $this->f->str_replace('{visitorURL}', $fullVisitorURL, $tempHtml); $tempHtml = $this->f->str_replace('{url}', esc_html($row['url']), $tempHtml); $tempHtml = $this->f->str_replace('{statusBadgeClass}', $statusBadgeClass, $tempHtml); $tempHtml = $this->f->str_replace('{statusTitle}', esc_attr($statusTitle), $tempHtml); $tempHtml = $this->f->str_replace('{status}', $statusText, $tempHtml); $tempHtml = $this->f->str_replace('{hits}', esc_html($hits), $tempHtml); $tempHtml = $this->f->str_replace('{created_date}', esc_html(date("Y/m/d h:i:s A", abs(intval($row['timestamp'] ?? 0)))), $tempHtml); $tempHtml = $this->f->str_replace('{last_used_date}', esc_html($last), $tempHtml); $tempHtml = $this->f->str_replace('{lastUsedClass}', $lastUsedClass, $tempHtml); $tempHtml = $this->f->str_replace('{editBtnHTML}', $editBtnHTML, $tempHtml); $tempHtml = $this->f->str_replace('{logsBtnHTML}', $logsBtnHTML, $tempHtml); $tempHtml = $this->f->str_replace('{trashBtnHTML}', $trashBtnHTML, $tempHtml); $tempHtml = $this->f->str_replace('{deleteBtnHTML}', $deleteBtnHTML, $tempHtml); $tempHtml = $this->f->str_replace('{ignoreBtnHTML}', $ignoreBtnHTML, $tempHtml); $tempHtml = $this->f->str_replace('{laterBtnHTML}', $laterBtnHTML, $tempHtml); $tempHtml = $this->f->doNormalReplacements($tempHtml); $html .= $tempHtml; } if ($displayed == 0) { $html .= ''; } $html .= '
' . esc_html($col['title']) . $sortIndicator . '' . $tooltipHtml . '
' . __('No Captured 404 Records To Display', '404-solution') . '
'; return $html; } /** * @global type $abj404dao * @global type $abj404logic */ function echoAdminRedirectsPage() { $sub = 'abj404_redirects'; $tableOptions = $this->logic->getTableOptions($sub); // Sanitizing unchecked table options $tableOptions = $this->logic->sanitizePostData($tableOptions); $currentFilter = $tableOptions['filter'] ?? 0; $timezone = get_option('timezone_string'); if ('' == $timezone) { $timezone = 'UTC'; } date_default_timezone_set($timezone); // Get counts for tabs $counts = $this->dao->getRedirectStatusCounts(); // Modern table page wrapper echo '
'; // Page header with Add Redirect button echo '
'; echo '

' . esc_html__('Page Redirects', '404-solution') . '

'; if ($currentFilter != ABJ404_TRASH_FILTER) { echo ''; } echo '
'; // Content tabs echo '
'; $this->echoContentTab($sub, 0, __('All', '404-solution'), $counts['all'] ?? 0, $tableOptions); $this->echoContentTab($sub, ABJ404_STATUS_MANUAL, __('Manual', '404-solution'), $counts['manual'] ?? 0, $tableOptions); $this->echoContentTab($sub, ABJ404_STATUS_AUTO, __('Automatic', '404-solution'), $counts['auto'] ?? 0, $tableOptions); $this->echoContentTab($sub, ABJ404_TRASH_FILTER, __('Trash', '404-solution'), $counts['trash'] ?? 0, $tableOptions); echo '
'; // Filter bar with server-side search $filterText = isset($tableOptions['filterText']) ? $tableOptions['filterText'] : ''; $perPage = isset($tableOptions['perpage']) ? $tableOptions['perpage'] : 25; $paginationNonce = wp_create_nonce('abj404_updatePaginationLink'); $autoRefresh = (($sub === 'abj404_redirects' || $sub === 'abj404_captured' || $sub === 'abj404_logs') ? '1' : '0'); echo '
'; echo ''; echo '
'; echo '' . esc_html__('Rows per page:', '404-solution') . ''; echo ''; echo '
'; echo ''; echo '
'; // Bulk actions bar (hidden by default, shown when items selected) $url = $this->getBulkOperationsFormURL($sub, $tableOptions); // Form must wrap both bulk actions and table so dropdown values are submitted echo '
'; echo '
'; echo '0 ' . esc_html__('redirects selected', '404-solution') . ''; echo ''; echo ''; echo ''; echo '
'; // Table container echo '
'; echo $this->getAdminRedirectsPageTable($sub); echo '
'; // Pagination (using original AJAX-integrated pagination) echo $this->getPaginationLinks($sub, false); echo '
'; // Empty trash button (within page container but outside form) if ($currentFilter == ABJ404_TRASH_FILTER) { $eturl = "?page=" . ABJ404_PP . "&filter=" . ABJ404_TRASH_FILTER . "&subpage=" . $sub; $eturl = wp_nonce_url($eturl, "abj404_bulkProcess"); echo '
'; echo '
'; echo ''; echo ''; echo '
'; echo '
'; } echo '
'; // end abj404-table-page // Add redirect modal (outside the main container) if (($tableOptions['filter'] ?? 0) != ABJ404_TRASH_FILTER) { $this->echoAddRedirectModal($tableOptions); } } /** * Echo a content tab for the table pages */ function echoContentTab($sub, $filter, $label, $count, $tableOptions) { $currentFilter = $tableOptions['filter'] ?? 0; $isActive = ($currentFilter == $filter) ? 'active' : ''; $url = "?page=" . ABJ404_PP . "&subpage=" . $sub; if ($filter != 0) { $url .= "&filter=" . $filter; } echo ''; echo esc_html($label); echo '' . esc_html($count) . ''; echo ''; } /** * Echo the modern Add Redirect modal */ function echoAddRedirectModal($tableOptions) { $options = $this->getOptionsWithDefaults(); $url = "?page=" . ABJ404_PP . "&subpage=abj404_redirects"; $orderby = $tableOptions['orderby'] ?? 'url'; $order = $tableOptions['order'] ?? 'ASC'; if (!($orderby == "url" && $order == "ASC")) { $url .= "&orderby=" . sanitize_text_field($orderby) . "&order=" . sanitize_text_field($order); } $filter = $tableOptions['filter'] ?? 0; if ($filter != 0) { $url .= "&filter=" . $filter; } $link = wp_nonce_url($url, "abj404addRedirect"); $urlPlaceholder = parse_url(get_home_url(), PHP_URL_PATH) . "/example"; echo '
'; echo '
'; echo '
'; echo '

' . esc_html__('Add Manual Redirect', '404-solution') . '

'; echo ''; echo '
'; echo '
'; echo ''; echo '
'; // URL field echo '
'; echo ''; echo ''; echo '

' . esc_html__('The URL path that should be redirected (without domain)', '404-solution') . '

'; echo '
'; echo ''; echo ''; echo ' ' . esc_html__('(Explain)', '404-solution') . ''; echo '
'; echo ''; echo '
'; // Redirect to field - using the existing AJAX autocomplete template echo '
'; echo ''; // Load the autocomplete HTML template (includes wrapper and spinner) $redirectHtml = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectPageSearchDropdown.html"); $redirectHtml = $this->f->str_replace('{redirect_to_label}', '', $redirectHtml); $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', __('(Type a page name or an external URL)', '404-solution'), $redirectHtml); $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMTPY}', __('(Type a page name or an external URL)', '404-solution'), $redirectHtml); $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', __('(A page has been selected.)', '404-solution'), $redirectHtml); $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', __('(A custom string has been entered.)', '404-solution'), $redirectHtml); $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', __('(An external URL will be used.)', '404-solution'), $redirectHtml); $redirectHtml = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', '', $redirectHtml); $redirectHtml = $this->f->str_replace('{redirectPageTitle}', '', $redirectHtml); $redirectHtml = $this->f->str_replace('{pageIDAndType}', '', $redirectHtml); $redirectHtml = $this->f->str_replace('{data-url}', "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $redirectHtml); $redirectHtml = $this->f->doNormalReplacements($redirectHtml); echo $redirectHtml; echo '
'; // Redirect type echo '
'; echo ''; echo ''; echo '
'; echo '
'; echo ''; echo '
'; echo '
'; echo '
'; } /** * Get modern pagination HTML */ function getModernPagination($sub, $tableOptions) { $tableOptions = is_array($tableOptions) ? $tableOptions : array(); $logsid = isset($tableOptions['logsid']) ? intval($tableOptions['logsid']) : 0; $filter = isset($tableOptions['filter']) ? intval($tableOptions['filter']) : 0; $orderby = isset($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url'; $order = isset($tableOptions['order']) ? $tableOptions['order'] : 'ASC'; // Use appropriate count method based on sub type if ($sub == 'abj404_logs') { $totalRows = $this->dao->getLogsCount($logsid); } else { $totalRows = $this->dao->getRedirectsForViewCount($sub, $tableOptions); } $perPage = isset($tableOptions['perpage']) ? intval($tableOptions['perpage']) : 25; $currentPage = isset($tableOptions['paged']) ? intval($tableOptions['paged']) : 1; $totalPages = ceil($totalRows / $perPage); if ($totalPages <= 1) { return ''; } $startItem = (($currentPage - 1) * $perPage) + 1; $endItem = min($currentPage * $perPage, $totalRows); $baseUrl = "?page=" . ABJ404_PP . "&subpage=" . $sub; // Include logsid for logs pagination if ($sub == 'abj404_logs' && isset($tableOptions['logsid'])) { $baseUrl .= "&id=" . $tableOptions['logsid']; } if ($filter != 0) { $baseUrl .= "&filter=" . $filter; } if (!( $orderby == "url" && $order == "ASC" )) { $baseUrl .= "&orderby=" . sanitize_text_field($orderby) . "&order=" . sanitize_text_field($order); } // Different label for logs vs redirects $itemLabel = ($sub == 'abj404_logs') ? __('logs', '404-solution') : __('redirects', '404-solution'); $html = '
'; $html .= '
'; $html .= sprintf( /* translators: %1$d is start item, %2$d is end item, %3$d is total count, %4$s is item type (logs/redirects) */ esc_html__('Showing %1$d-%2$d of %3$d %4$s', '404-solution'), $startItem, $endItem, $totalRows, $itemLabel ); $html .= '
'; $html .= '
'; // Previous button if ($currentPage > 1) { $html .= '‹'; } else { $html .= '‹'; } // Page numbers $range = 2; for ($i = 1; $i <= $totalPages; $i++) { if ($i == 1 || $i == $totalPages || ($i >= $currentPage - $range && $i <= $currentPage + $range)) { $activeClass = ($i == $currentPage) ? ' active' : ''; $html .= '' . $i . ''; } elseif ($i == $currentPage - $range - 1 || $i == $currentPage + $range + 1) { $html .= '…'; } } // Next button if ($currentPage < $totalPages) { $html .= '›'; } else { $html .= '›'; } $html .= '
'; $html .= '
'; return $html; } function getBulkOperationsFormURL($sub, $tableOptions) { $url = "?page=" . ABJ404_PP . "&subpage=" . $sub; $filter = $tableOptions['filter'] ?? 0; if ($filter != 0) { $url .= "&filter=" . $filter; } $orderby = $tableOptions['orderby'] ?? 'url'; $order = $tableOptions['order'] ?? 'ASC'; if (!($orderby == "url" && $order == "ASC")) { $url .= "&orderby=" . sanitize_text_field($orderby) . "&order=" . sanitize_text_field($order); } $url = wp_nonce_url($url, 'abj404_bulkProcess'); return $url; } function getAdminRedirectsPageTable($sub) { $tableOptions = $this->logic->getTableOptions($sub); // these are used for a GET request so they're not translated. $columns = array(); $columns['url']['title'] = __('URL', '404-solution'); $columns['url']['orderby'] = "url"; $columns['url']['width'] = "25%"; $columns['status']['title'] = __('Status', '404-solution'); $columns['status']['orderby'] = "status"; $columns['status']['width'] = "5%"; $columns['type']['title'] = __('Type', '404-solution'); $columns['type']['orderby'] = "type"; $columns['type']['width'] = "10%"; $columns['dest']['title'] = __('Destination', '404-solution');; $columns['dest']['orderby'] = "final_dest"; $columns['dest']['width'] = "22%"; $columns['code']['title'] = __('Redirect', '404-solution'); $columns['code']['orderby'] = "code"; $columns['code']['width'] = "5%"; $columns['hits']['title'] = __('Hits', '404-solution'); $columns['hits']['orderby'] = "logshits"; $columns['hits']['width'] = "7%"; $hitsTooltip = $this->getHitsColumnTooltip($tableOptions); $columns['hits']['title_attr_html'] = $hitsTooltip; $columns['timestamp']['title'] = __('Created', '404-solution'); $columns['timestamp']['orderby'] = "timestamp"; $columns['timestamp']['width'] = "10%"; $columns['timestamp']['class'] = "hide-on-tablet"; $columns['last_used']['title'] = __('Last Used', '404-solution'); $columns['last_used']['orderby'] = "last_used"; $columns['last_used']['width'] = "10%"; $columns['last_used']['title_attr_html'] = $hitsTooltip; $html = ""; $html .= $this->getTableColumns($sub, $columns); $html .= ""; $rows = $this->dao->getRedirectsForView($sub, $tableOptions); $this->rememberTableDataSignature($sub, $rows); $displayed = 0; $y = 1; foreach ($rows as $row) { $displayed++; $rowType = $row['type'] ?? 0; $rowStatus = $row['status'] ?? 0; $rowFinalDest = $row['final_dest'] ?? ''; $destForView = trim((string)($row['dest_for_view'] ?? '')); $statusTitle = ''; if ($rowStatus == ABJ404_STATUS_MANUAL) { $statusTitle = __('Manually created', '404-solution'); } else if ($rowStatus == ABJ404_STATUS_AUTO) { $statusTitle = __('Automatically created', '404-solution'); } else if ($rowStatus == ABJ404_STATUS_REGEX) { $statusTitle = __('Regular Expression (Manually Created)', '404-solution'); } else { $statusTitle = __('Unknown', '404-solution'); } $link = ""; $title = __('Visit', '404-solution') . " "; if ($rowType == ABJ404_TYPE_EXTERNAL) { if ($rowFinalDest !== '') { $link = $rowFinalDest; $title .= $rowFinalDest; } } else if ($rowType == ABJ404_TYPE_CAT) { if ($rowFinalDest !== '') { $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . "|" . ABJ404_TYPE_CAT, 0); $link = $permalink['link']; $title .= __('Category:', '404-solution') . " " . $permalink['title']; } } else if ($rowType == ABJ404_TYPE_TAG) { if ($rowFinalDest !== '') { $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . "|" . ABJ404_TYPE_TAG, 0); $link = $permalink['link']; $title .= __('Tag:', '404-solution') . " " . $permalink['title']; } } else if ($rowType == ABJ404_TYPE_HOME) { $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . "|" . ABJ404_TYPE_HOME, 0); $link = $permalink['link']; $title .= __('Home Page:', '404-solution') . " " . $permalink['title']; } else if ($rowType == ABJ404_TYPE_POST) { if ($rowFinalDest !== '') { $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . "|" . ABJ404_TYPE_POST, 0); $link = $permalink['link']; $title .= $permalink['title']; } } else if ($rowType == ABJ404_TYPE_404_DISPLAYED) { $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . "|" . ABJ404_TYPE_404_DISPLAYED, 0); // for custom 404 page use the link $link = $permalink['link']; $title .= $permalink['title']; // for the normal 404 page just use # if ($rowFinalDest == '0') { $link = ''; } } else { $this->logger->errorMessage("Unexpected row type while displaying table: " . $rowType); } if ($link != '') { $link = "href='$link'"; } $hits = $row['logshits'] ?? 0; $last_used = $row['last_used'] ?? 0; if ($last_used != 0) { $last = date("Y/m/d h:i:s A", abs(intval($last_used))); } else { $last = __('Never Used', '404-solution'); } // Build action links using helper method $links = $this->buildTableActionLinks($row, $sub, $tableOptions, false); extract($links); $class = ""; if ($y == 0) { $class = "alternate"; $y++; } else { $y = 0; $class = "normal-non-alternate"; } // make the entire row red if the destination is missing, doesn't exist, or is unpublished. $destinationDoesNotExistClass = ''; $destinationIsMissing = false; if ($rowType != ABJ404_TYPE_404_DISPLAYED && trim((string)$rowFinalDest) === '') { $destinationIsMissing = true; $destinationDoesNotExistClass = ' destination-does-not-exist'; } if (array_key_exists('published_status', $row)) { if ($row['published_status'] == '0') { $destinationDoesNotExistClass = ' destination-does-not-exist'; } } // Check if URL looks like a regex pattern but is not marked as a regex redirect $urlLooksLikeRegexClass = ''; $urlLooksLikeRegex = ABJ_404_Solution_Functions::urlLooksLikeRegex($row['url']); $isRegexStatus = ($row['status'] == ABJ404_STATUS_REGEX); if ($urlLooksLikeRegex && !$isRegexStatus) { $urlLooksLikeRegexClass = ' url-looks-like-regex'; } $class = $class . $destinationDoesNotExistClass . $urlLooksLikeRegexClass; // ------------------------------------------- // Build modern row action buttons with icons $editBtnHTML = ''; $logsBtnHTML = ''; $trashBtnHTML = ''; $deleteBtnHTML = ''; $currentFilter = $tableOptions['filter'] ?? 0; if ($currentFilter != ABJ404_TRASH_FILTER) { $editBtnHTML = '' . ' ' . '{Edit}'; $trashBtnHTML = '' . ' ' . '{Trash}'; } if (($row['logsid'] ?? 0) > 0) { $logsBtnHTML = '' . ' ' . '{Logs}'; } if ($currentFilter == ABJ404_TRASH_FILTER) { $trashBtnHTML = '' . ' ' . '{Restore}'; $deleteBtnHTML = ' | ' . ' ' . '{Delete}'; } // Determine badge classes $statusBadgeClass = 'abj404-badge-manual'; if ($row['status'] == ABJ404_STATUS_AUTO) { $statusBadgeClass = 'abj404-badge-auto'; } else if ($row['status'] == ABJ404_STATUS_REGEX) { $statusBadgeClass = 'abj404-badge-regex'; } $codeBadgeClass = ($row['code'] == '301') ? 'abj404-badge-301' : 'abj404-badge-302'; $lastUsedClass = ''; if ($last_used == 0) { $lastUsedClass = 'abj404-never-used'; } // Legacy variables for backwards compatibility $editlinkHTML = ''; $logslinkHTML = ''; $deletePermanentlyHTML = ''; $destinationExists = ''; $destinationDoesNotExist = 'display: none;'; $destinationWarningText = __("This page doesn't exist or is not published so the redirect won't work.", '404-solution'); if ($destinationIsMissing) { $destinationExists = 'display: none;'; $destinationDoesNotExist = ''; $destinationWarningText = __('Destination missing. Edit this redirect and choose a destination.', '404-solution'); if (trim((string)$destForView) === '') { $destForView = __('(Destination missing)', '404-solution'); } } if (array_key_exists('published_status', $row)) { if ($row['published_status'] == '0') { $destinationExists = 'display: none;'; $destinationDoesNotExist = ''; if (trim((string)$destForView) === '') { $destForView = __('(Destination unavailable)', '404-solution'); } } } // URL regex warning visibility $urlIsNormal = ''; $urlLooksLikeRegexWarning = 'display: none;'; if ($urlLooksLikeRegex && !$isRegexStatus) { $urlIsNormal = 'display: none;'; $urlLooksLikeRegexWarning = ''; } // Build full URL with WordPress base path for subdirectory installations $fullVisitorURL = esc_url(home_url($row['url'])); $htmlTemp = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/tableRowPageRedirects.html"); $htmlTemp = $this->f->str_replace('{rowid}', $row['id'], $htmlTemp); $htmlTemp = $this->f->str_replace('{rowClass}', $class, $htmlTemp); $htmlTemp = $this->f->str_replace('{visitorURL}', $fullVisitorURL, $htmlTemp); $htmlTemp = $this->f->str_replace('{rowURL}', esc_html($row['url']), $htmlTemp); // URL regex warning $htmlTemp = $this->f->str_replace('{url-is-normal}', $urlIsNormal, $htmlTemp); $htmlTemp = $this->f->str_replace('{url-looks-like-regex}', $urlLooksLikeRegexWarning, $htmlTemp); // Modern row action buttons $htmlTemp = $this->f->str_replace('{editBtnHTML}', $editBtnHTML, $htmlTemp); $htmlTemp = $this->f->str_replace('{logsBtnHTML}', $logsBtnHTML, $htmlTemp); $htmlTemp = $this->f->str_replace('{trashBtnHTML}', $trashBtnHTML, $htmlTemp); $htmlTemp = $this->f->str_replace('{deleteBtnHTML}', $deleteBtnHTML, $htmlTemp); // Badge classes $htmlTemp = $this->f->str_replace('{statusBadgeClass}', $statusBadgeClass, $htmlTemp); $htmlTemp = $this->f->str_replace('{codeBadgeClass}', $codeBadgeClass, $htmlTemp); $htmlTemp = $this->f->str_replace('{lastUsedClass}', $lastUsedClass, $htmlTemp); $htmlTemp = $this->f->str_replace('{link}', $link, $htmlTemp); $htmlTemp = $this->f->str_replace('{title}', $title, $htmlTemp); $htmlTemp = $this->f->str_replace('{dest}', $destForView, $htmlTemp); $htmlTemp = $this->f->str_replace('{destination-exists}', $destinationExists, $htmlTemp); $htmlTemp = $this->f->str_replace('{destination-does-not-exist}', $destinationDoesNotExist, $htmlTemp); $htmlTemp = $this->f->str_replace('{destination-warning-text}', $destinationWarningText, $htmlTemp); $htmlTemp = $this->f->str_replace('{status}', $row['status_for_view'] ?? '', $htmlTemp); $htmlTemp = $this->f->str_replace('{statusTitle}', $statusTitle, $htmlTemp); $htmlTemp = $this->f->str_replace('{type}', $row['type_for_view'] ?? '', $htmlTemp); $htmlTemp = $this->f->str_replace('{rowCode}', $row['code'] ?? '', $htmlTemp); $htmlTemp = $this->f->str_replace('{hits}', esc_html($hits), $htmlTemp); $htmlTemp = $this->f->str_replace('{logsLink}', $logslink, $htmlTemp); $htmlTemp = $this->f->str_replace('{trashLink}', $trashlink, $htmlTemp); $htmlTemp = $this->f->str_replace('{ajaxTrashLink}', $ajaxTrashLink, $htmlTemp); $htmlTemp = $this->f->str_replace('{trashtitle}', $trashtitle, $htmlTemp); $htmlTemp = $this->f->str_replace('{deletelink}', $deletelink, $htmlTemp); $htmlTemp = $this->f->str_replace('{created_date}', esc_html(date("Y/m/d h:i:s A", abs(intval($row['timestamp'] ?? 0)))), $htmlTemp); $htmlTemp = $this->f->str_replace('{last_used_date}', esc_html($last), $htmlTemp); $htmlTemp = $this->f->doNormalReplacements($htmlTemp); $html .= $htmlTemp; } if ($displayed == 0) { $html .= "\n" . ""; } $html .= "
" . "
πŸ“‹
" . "

" . __('No Redirect Records To Display', '404-solution') . "

" . "

" . __('Redirects will appear here once created.', '404-solution') . "

" . "
"; return $html; } function echoAddManualRedirect($tableOptions) { $options = $this->getOptionsWithDefaults(); $url = "?page=" . ABJ404_PP . "&subpage=abj404_redirects"; $orderby = $tableOptions['orderby'] ?? 'url'; $order = $tableOptions['order'] ?? 'ASC'; if (!($orderby == "url" && $order == "ASC")) { $url .= "&orderby=" . sanitize_text_field($orderby) . "&order=" . sanitize_text_field($order); } $filter = $tableOptions['filter'] ?? 0; if ($filter != 0) { $url .= "&filter=" . $filter; } $link = wp_nonce_url($url, "abj404addRedirect"); $urlPlaceholder = parse_url(get_home_url(), PHP_URL_PATH) . "/example"; if (isset($_POST['url']) && $_POST['url'] != '') { $postedURL = esc_url($_POST['url']); } else { $postedURL = $urlPlaceholder; } $selected301 = ""; $selected302 = ""; if ($options['default_redirect'] == '301') { $selected301 = " selected "; } else { $selected302 = " selected "; } // read the html content. $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectTop.html"); $html .= ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectPageSearchDropdown.html"); $html = $this->f->str_replace('{redirect_to_label}', __('Redirect to', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', __('(Type a page name or an external URL)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', __('(A page has been selected.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', __('(A custom string has been entered.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', __('(An external URL will be used.)', '404-solution'), $html); $html = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', '', $html); $html = $this->f->str_replace('{redirectPageTitle}', '', $html); $html = $this->f->str_replace('{pageIDAndType}', '', $html); $html = $this->f->str_replace('{redirectPageTitle}', '', $html); $html = $this->f->str_replace('{data-url}', "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $html); $html .= ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectBottom.html"); $html = $this->f->str_replace('{addManualRedirectAction}', $link, $html); $html = $this->f->str_replace('{urlPlaceholder}', esc_attr($urlPlaceholder), $html); $html = $this->f->str_replace('{postedURL}', esc_attr($postedURL), $html); $html = $this->f->str_replace('{301selected}', $selected301, $html); $html = $this->f->str_replace('{302selected}', $selected302, $html); // constants and translations. $html = $this->f->doNormalReplacements($html); echo $html; } /** This is used both to add and to edit a redirect. * @param string $destination * @param string $codeselected * @param string $label */ function echoEditRedirect($destination, $codeselected, $label, $source_page = null, $filter = null, $orderby = null, $order = null) { // Redirect type dropdown echo '
'; echo ''; echo ''; echo '
'; // Button group echo '
'; // Cancel button $cancelUrl = '?page=' . ABJ404_PP; if ($source_page) { $cancelUrl .= '&subpage=' . esc_attr($source_page); } if ($filter !== null) { $cancelUrl .= '&filter=' . esc_attr($filter); } if ($orderby !== null) { $cancelUrl .= '&orderby=' . esc_attr($orderby); } if ($order !== null) { $cancelUrl .= '&order=' . esc_attr($order); } echo '' . esc_html__('Cancel', '404-solution') . ''; // Submit button echo ''; echo '
'; } function echoRedirectDestinationOptionsDefaults($currentlySelected) { $content = ""; $content .= "\n" . '' . "\n"; $selected = ""; if ($currentlySelected == ABJ404_TYPE_EXTERNAL) { $selected = " selected"; } $content .= "\n"; if ($currentlySelected == ABJ404_TYPE_HOME) { $selected = " selected"; } else { $selected = ""; } $content .= "\n"; $content .= "\n" . '' . "\n"; return $content; } /** * @global type $abj404dao * @global type $wpdb * @param array $options * @return string */ function getAdminOptionsPageAutoRedirects($options) { $options = is_array($options) ? $options : array(); $options = $this->normalizeOptionsForView($options); $spaces = esc_html("   "); $content = ""; $userSelectedDefault404Page = (array_key_exists('dest404page', $options) && isset($options['dest404page']) ? $options['dest404page'] : null); $urlDestination = (array_key_exists('dest404pageURL', $options) && isset($options['dest404pageURL']) ? $options['dest404pageURL'] : null); $pageMissingWarning = ""; if ($userSelectedDefault404Page != null) { $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($userSelectedDefault404Page, 0); if (!in_array($permalink['status'], array('publish', 'published'))) { $pageMissingWarning = __("(The specified page doesn't exist. " . "Please update this setting.)", '404-solution'); } } $pageTitle = $this->logic->getPageTitleFromIDAndType($userSelectedDefault404Page, $urlDestination); $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectPageSearchDropdown.html"); $html = $this->f->str_replace('{redirect_to_label}', __('Redirect all unhandled 404s to', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', __('(Type a page name or an external URL)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', __('(A page has been selected.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', __('(A custom string has been entered.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', __('(An external URL will be used.)', '404-solution'), $html); $html = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', $pageMissingWarning, $html); $html = $this->f->str_replace('{redirectPageTitle}', $pageTitle, $html); $html = $this->f->str_replace('{pageIDAndType}', $userSelectedDefault404Page, $html); $html = $this->f->str_replace('{redirectPageTitle}', $pageTitle, $html); $html = $this->f->str_replace('{data-url}', "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $html); $html = $this->f->doNormalReplacements($html); $content .= $html; // ----------------------------------------------- // Load auto redirects options template $selectedAutoRedirects = $this->getCheckedAttr($options, 'auto_redirects'); $selectedAutoSlugs = $this->getCheckedAttr($options, 'auto_slugs'); $selectedAutoCats = $this->getCheckedAttr($options, 'auto_cats'); $selectedAutoTags = $this->getCheckedAttr($options, 'auto_tags'); $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/adminOptionsAutoRedirects.html"); $html = $this->f->str_replace('{selectedAutoRedirects}', $selectedAutoRedirects, $html); $html = $this->f->str_replace('{selectedAutoSlugs}', $selectedAutoSlugs, $html); $html = $this->f->str_replace('{selectedAutoCats}', $selectedAutoCats, $html); $html = $this->f->str_replace('{selectedAutoTags}', $selectedAutoTags, $html); $html = $this->f->str_replace('{auto_deletion}', esc_attr($options['auto_deletion']), $html); $html = $this->f->str_replace('{spaces}', $spaces, $html); $html = $this->f->doNormalReplacements($html); $content .= $html; return $content; } function getAdminOptionsPageAdvancedSettings($options) { $options = $this->normalizeOptionsForView($options); // Only allow redirecting all requests on trusted sites because someone will break // their website and complain to me about it and I don't want to hear that because I have // other things to do besides deal with people that don't listen to warnings about things // that will break their website. $hideRedirectAllRequests = 'true'; $serverName = array_key_exists('SERVER_NAME', $_SERVER) ? $_SERVER['SERVER_NAME'] : (array_key_exists('HTTP_HOST', $_SERVER) ? $_SERVER['HTTP_HOST'] : '(not found)'); if (in_array($serverName, $GLOBALS['abj404_whitelist'])) { $hideRedirectAllRequests = 'false'; } $selectedDebugLogging = $this->getCheckedAttr($options, 'debug_mode'); $selectedRedirectAllRequests = $this->getCheckedAttr($options, 'redirect_all_requests'); $selectedLogRawIPs = $this->getCheckedAttr($options, 'log_raw_ips'); $debugExplanation = __('View the debug file.', '404-solution'); $debugLogLink = $this->logic->getDebugLogFileLink(); $debugExplanation = $this->f->str_replace('', '', $debugExplanation); $kbFileSize = $this->logger->getDebugFileSize() / 1024; $kbFileSizePretty = number_format($kbFileSize, 2, ".", ","); $mbFileSize = $this->logger->getDebugFileSize() / 1024 / 1000; $mbFileSizePretty = number_format($mbFileSize, 2, ".", ","); /* Translators: 1: The file size in KB. 2: The file size in MB. */ $debugFileSize = sprintf(__('Debug file size: %1$s KB (%2$s MB).', '404-solution'), $kbFileSizePretty, $mbFileSizePretty); $allPostTypesTemp = $this->dao->getAllPostTypes(); // Ensure we have an array before imploding $allPostTypes = is_array($allPostTypesTemp) ? esc_html(implode(', ', $allPostTypesTemp)) : ''; // ---- // read the html content. $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/settingsAdvanced.html"); $html = $this->f->str_replace('{DATABASE_VERSION}', esc_html($options['DB_VERSION']), $html); $html = $this->f->str_replace('checked="debug_mode"', $selectedDebugLogging, $html); $html = $this->f->str_replace('checked="redirect_all_requests"', $selectedRedirectAllRequests, $html); $html = $this->f->str_replace('checked="log_raw_ips"', $selectedLogRawIPs, $html); $html = $this->f->str_replace('{View the debug file.}', $debugExplanation, $html); $html = $this->f->str_replace('{Debug file size: %s KB.}', $debugFileSize, $html); $html = $this->f->str_replace('{ignore_dontprocess}', str_replace('\\n', "\n", wp_kses_post($options['ignore_dontprocess'])), $html); $html = $this->f->str_replace('{ignore_doprocess}', str_replace('\\n', "\n", wp_kses_post($options['ignore_doprocess'])), $html); $html = $this->f->str_replace('{recognized_post_types}', str_replace('\\n', "\n", wp_kses_post($options['recognized_post_types'])), $html); $html = $this->f->str_replace('{all_post_types}', $allPostTypes, $html); $html = $this->f->str_replace('{days_wait_before_major_update}', $options['days_wait_before_major_update'], $html); $html = $this->f->str_replace('{recognized_categories}', str_replace('\\n', "\n", wp_kses_post($options['recognized_categories'])), $html); $html = $this->f->str_replace('{folders_files_ignore}', str_replace('\\n', "\n", wp_kses_post($options['folders_files_ignore'])), $html); $html = $this->f->str_replace('{suggest_regex_exclusions}', str_replace('\\n', "\n", esc_textarea($options['suggest_regex_exclusions'])), $html); // Use esc_textarea for textareas // Handle plugin_admin_users - convert array to string first before sanitization $pluginAdminUsers = $options['plugin_admin_users']; if (is_array($pluginAdminUsers)) { $pluginAdminUsers = implode("\n", $pluginAdminUsers); } $pluginAdminUsers = str_replace('\\n', "\n", wp_kses_post($pluginAdminUsers)); $html = $this->f->str_replace('{plugin_admin_users}', wp_kses_post($pluginAdminUsers), $html); $html = $this->f->str_replace('{OPTION_MIN_AUTO_SCORE}', esc_attr($options['auto_score']), $html); $html = $this->f->str_replace('{OPTION_TEMPLATE_REDIRECT_PRIORITY}', esc_attr($options['template_redirect_priority']), $html); $html = $this->f->str_replace('{disallow-redirect-all-requests}', $hideRedirectAllRequests, $html); $html = $this->f->str_replace('{add-exclude-page-data-url}', "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=false&includeSpecial=false&nonce=" . wp_create_nonce('abj404_ajax'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', __('(Type a page name)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', __('(A page has been selected.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', __('(A custom string has been entered.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', __('(An external URL will be used.)', '404-solution'), $html); $html = $this->f->str_replace('{loaded-excluded-pages}', urlencode($options['excludePages[]']), $html); // constants and translations. $html = $this->f->doNormalReplacements($html); // ------------------ return $html; } /** * Get the HTML content for the Content & URL Filtering section * @param array $options * @return string */ function getAdminOptionsPageAdvancedContent($options) { $options = is_array($options) ? $options : array(); $options = $this->normalizeOptionsForView($options); $allPostTypesTemp = $this->dao->getAllPostTypes(); // Ensure we have an array before imploding $allPostTypes = is_array($allPostTypesTemp) ? esc_html(implode(', ', $allPostTypesTemp)) : ''; // Read the html content $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/settingsAdvancedContent.html"); $html = $this->f->str_replace('{recognized_post_types}', str_replace('\\n', "\n", wp_kses_post($options['recognized_post_types'])), $html); $html = $this->f->str_replace('{all_post_types}', $allPostTypes, $html); $html = $this->f->str_replace('{recognized_categories}', str_replace('\\n', "\n", wp_kses_post($options['recognized_categories'])), $html); $html = $this->f->str_replace('{folders_files_ignore}', str_replace('\\n', "\n", wp_kses_post($options['folders_files_ignore'])), $html); $html = $this->f->str_replace('{suggest_regex_exclusions}', str_replace('\\n', "\n", esc_textarea($options['suggest_regex_exclusions'])), $html); $html = $this->f->str_replace('{add-exclude-page-data-url}', "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=false&includeSpecial=false&nonce=" . wp_create_nonce('abj404_ajax'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', __('(Type a page name)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', __('(A page has been selected.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', __('(A custom string has been entered.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', __('(An external URL will be used.)', '404-solution'), $html); $html = $this->f->str_replace('{loaded-excluded-pages}', urlencode($options['excludePages[]']), $html); // Constants and translations $html = $this->f->doNormalReplacements($html); return $html; } /** * Get the HTML content for the Logging & Privacy section * @param array $options * @return string */ function getAdminOptionsPageAdvancedLogging($options) { $options = $this->normalizeOptionsForView($options); $selectedLogRawIPs = $this->getCheckedAttr($options, 'log_raw_ips'); $selectedDebugLogging = $this->getCheckedAttr($options, 'debug_mode'); $debugExplanation = __('View the debug file.', '404-solution'); $debugLogLink = $this->logic->getDebugLogFileLink(); $debugExplanation = $this->f->str_replace('', '', $debugExplanation); $kbFileSize = $this->logger->getDebugFileSize() / 1024; $kbFileSizePretty = number_format($kbFileSize, 2, ".", ","); $mbFileSize = $this->logger->getDebugFileSize() / 1024 / 1000; $mbFileSizePretty = number_format($mbFileSize, 2, ".", ","); /* Translators: 1: The file size in KB. 2: The file size in MB. */ $debugFileSize = sprintf(__('Debug file size: %1$s KB (%2$s MB).', '404-solution'), $kbFileSizePretty, $mbFileSizePretty); // Read the html content $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/settingsAdvancedLogging.html"); $html = $this->f->str_replace('checked="log_raw_ips"', $selectedLogRawIPs, $html); $html = $this->f->str_replace('checked="debug_mode"', $selectedDebugLogging, $html); $html = $this->f->str_replace('{View the debug file.}', $debugExplanation, $html); $html = $this->f->str_replace('{Debug file size: %s KB.}', $debugFileSize, $html); $html = $this->f->str_replace('{ignore_dontprocess}', str_replace('\\n', "\n", wp_kses_post($options['ignore_dontprocess'])), $html); $html = $this->f->str_replace('{ignore_doprocess}', str_replace('\\n', "\n", wp_kses_post($options['ignore_doprocess'])), $html); // Constants and translations $html = $this->f->doNormalReplacements($html); return $html; } /** * Get the HTML content for the Advanced Configuration section * @param array $options * @return string */ function getAdminOptionsPageAdvancedSystem($options) { $options = $this->normalizeOptionsForView($options); $selectedRedirectAllRequests = $this->getCheckedAttr($options, 'redirect_all_requests'); $hideRedirectAllRequests = "false"; if (array_key_exists('disallow-redirect-all-requests', $options) && $options['disallow-redirect-all-requests'] == '1') { $hideRedirectAllRequests = "true"; } // Read the html content $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/settingsAdvancedSystem.html"); $html = $this->f->str_replace('{DATABASE_VERSION}', esc_html($options['DB_VERSION']), $html); $html = $this->f->str_replace('checked="redirect_all_requests"', $selectedRedirectAllRequests, $html); $html = $this->f->str_replace('{disallow-redirect-all-requests}', $hideRedirectAllRequests, $html); $html = $this->f->str_replace('{OPTION_MIN_AUTO_SCORE}', esc_attr($options['auto_score']), $html); $html = $this->f->str_replace('{OPTION_TEMPLATE_REDIRECT_PRIORITY}', esc_attr($options['template_redirect_priority']), $html); $html = $this->f->str_replace('{days_wait_before_major_update}', $options['days_wait_before_major_update'], $html); // Handle plugin_admin_users - convert array to string first before sanitization $pluginAdminUsers = $options['plugin_admin_users']; if (is_array($pluginAdminUsers)) { $pluginAdminUsers = implode("\n", $pluginAdminUsers); } $pluginAdminUsers = str_replace('\\n', "\n", wp_kses_post($pluginAdminUsers)); $html = $this->f->str_replace('{plugin_admin_users}', wp_kses_post($pluginAdminUsers), $html); // Constants and translations $html = $this->f->doNormalReplacements($html); return $html; } /** * @param array $options * @return string */ function getAdminOptionsPageGeneralSettings($options) { $options = $this->normalizeOptionsForView($options); $selectedDefaultRedirect301 = ""; if ($options['default_redirect'] == '301') { $selectedDefaultRedirect301 = " selected"; } $selectedDefaultRedirect302 = ""; if ($options['default_redirect'] == '302') { $selectedDefaultRedirect302 = " selected"; } $selectedCapture404 = $this->getCheckedAttr($options, 'capture_404'); $selectedSendErrorLogs = $this->getCheckedAttr($options, 'send_error_logs'); $selectedUnderSettings = ""; $selecteSsettingsLevel = ""; if ($options['menuLocation'] == 'settingsLevel') { $selecteSsettingsLevel = " selected"; } else { $selectedUnderSettings = " selected"; } // Theme selection $adminTheme = isset($options['admin_theme']) ? $options['admin_theme'] : 'default'; $selectedThemeDefault = ($adminTheme == 'default') ? " selected" : ""; $selectedThemeCalm = ($adminTheme == 'calm') ? " selected" : ""; $selectedThemeMono = ($adminTheme == 'mono') ? " selected" : ""; $selectedThemeNeon = ($adminTheme == 'neon') ? " selected" : ""; $selectedThemeObsidian = ($adminTheme == 'obsidian') ? " selected" : ""; // Theme name translations $themeDefault = __('Default', '404-solution'); // Language override selection $pluginLanguage = isset($options['plugin_language_override']) ? $options['plugin_language_override'] : ''; $selectedLanguageDefault = ($pluginLanguage == '') ? " selected" : ""; $selectedLanguageEnUS = ($pluginLanguage == 'en_US') ? " selected" : ""; $selectedLanguageDeDE = ($pluginLanguage == 'de_DE') ? " selected" : ""; $selectedLanguageEsES = ($pluginLanguage == 'es_ES') ? " selected" : ""; $selectedLanguageFrFR = ($pluginLanguage == 'fr_FR') ? " selected" : ""; $selectedLanguageItIT = ($pluginLanguage == 'it_IT') ? " selected" : ""; $selectedLanguagePtBR = ($pluginLanguage == 'pt_BR') ? " selected" : ""; $selectedLanguageNlNL = ($pluginLanguage == 'nl_NL') ? " selected" : ""; $selectedLanguageRuRU = ($pluginLanguage == 'ru_RU') ? " selected" : ""; $selectedLanguageJa = ($pluginLanguage == 'ja') ? " selected" : ""; $selectedLanguageZhCN = ($pluginLanguage == 'zh_CN') ? " selected" : ""; $selectedLanguageIdID = ($pluginLanguage == 'id_ID') ? " selected" : ""; $selectedLanguageSvSE = ($pluginLanguage == 'sv_SE') ? " selected" : ""; // Auto dark mode detection checkbox $disableAutoDarkMode = isset($options['disable_auto_dark_mode']) && $options['disable_auto_dark_mode'] == '1'; $disableAutoDarkModeChecked = $disableAutoDarkMode ? " checked" : ""; $logSizeBytes = $this->dao->getLogDiskUsage(); $logSizeMB = round($logSizeBytes / (1024 * 1000), 2); $totalLogLines = $this->dao->getLogsCount(0); $timeToDisplay = $this->dao->getEarliestLogTimestamp(); $earliestLogDate = 'N/A'; if ($timeToDisplay >= 0) { $earliestLogDate = date('Y/m/d', $timeToDisplay) . ' ' . date('h:i:s', $timeToDisplay) . ' ' . date('A', $timeToDisplay); } $selectedRemoveMatches = $this->getCheckedAttr($options, 'remove_matches'); $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/adminOptionsGeneral.html"); $html = $this->f->str_replace('{selectedSendErrorLogs}', $selectedSendErrorLogs, $html); $html = $this->f->str_replace('{selectedDefaultRedirect301}', $selectedDefaultRedirect301, $html); $html = $this->f->str_replace('{selectedDefaultRedirect302}', $selectedDefaultRedirect302, $html); $html = $this->f->str_replace('{selectedCapture404}', $selectedCapture404, $html); $html = $this->f->str_replace('{admin_notification}', $options['admin_notification'], $html); $html = $this->f->str_replace('{capture_deletion}', $options['capture_deletion'], $html); $html = $this->f->str_replace('{manual_deletion}', $options['manual_deletion'], $html); $html = $this->f->str_replace('{maximum_log_disk_usage}', $options['maximum_log_disk_usage'], $html); $html = $this->f->str_replace('{logCurrentSizeDiskUsage}', $logSizeMB, $html); $html = $this->f->str_replace('{logCurrentRowCount}', $totalLogLines, $html); $html = $this->f->str_replace('{earliestLogDate}', $earliestLogDate, $html); $html = $this->f->str_replace('{selectedRemoveMatches}', $selectedRemoveMatches, $html); $html = $this->f->str_replace('{selectedUnderSettings}', $selectedUnderSettings, $html); $html = $this->f->str_replace('{selecteSsettingsLevel}', $selecteSsettingsLevel, $html); $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}', $themeDefault, $html); $html = $this->f->str_replace('{selectedLanguageDefault}', $selectedLanguageDefault, $html); $html = $this->f->str_replace('{selectedLanguageEnUS}', $selectedLanguageEnUS, $html); $html = $this->f->str_replace('{selectedLanguageDeDE}', $selectedLanguageDeDE, $html); $html = $this->f->str_replace('{selectedLanguageEsES}', $selectedLanguageEsES, $html); $html = $this->f->str_replace('{selectedLanguageFrFR}', $selectedLanguageFrFR, $html); $html = $this->f->str_replace('{selectedLanguageItIT}', $selectedLanguageItIT, $html); $html = $this->f->str_replace('{selectedLanguagePtBR}', $selectedLanguagePtBR, $html); $html = $this->f->str_replace('{selectedLanguageNlNL}', $selectedLanguageNlNL, $html); $html = $this->f->str_replace('{selectedLanguageRuRU}', $selectedLanguageRuRU, $html); $html = $this->f->str_replace('{selectedLanguageJa}', $selectedLanguageJa, $html); $html = $this->f->str_replace('{selectedLanguageZhCN}', $selectedLanguageZhCN, $html); $html = $this->f->str_replace('{selectedLanguageIdID}', $selectedLanguageIdID, $html); $html = $this->f->str_replace('{selectedLanguageSvSE}', $selectedLanguageSvSE, $html); $html = $this->f->str_replace('{disableAutoDarkModeChecked}', $disableAutoDarkModeChecked, $html); $html = $this->f->str_replace('{admin_notification_email}', $options['admin_notification_email'], $html); $html = $this->f->str_replace('{default_wordpress_admin_email}', get_option('admin_email'), $html); $html = $this->f->str_replace('{PHP_VERSION}', PHP_VERSION, $html); // constants and translations. $html = $this->f->doNormalReplacements($html); return $html; } /** * @global type $abj404dao */ function echoAdminLogsPage() { $sub = 'abj404_logs'; $tableOptions = $this->logic->getTableOptions($sub); // Sanitizing unchecked table options $tableOptions = $this->logic->sanitizePostData($tableOptions); $timezone = get_option('timezone_string'); if ('' == $timezone) { $timezone = 'UTC'; } date_default_timezone_set($timezone); // Modern page wrapper echo '
'; // Header with page title echo '
'; echo '

' . __('Redirect Logs', '404-solution') . '

'; echo '
'; // Filter bar with search dropdown echo '
'; // Log search form echo '
'; echo ''; echo ''; // ----------------- dropdown search box. begin. $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/viewLogsForSearchBox.html"); $redirectPageTitle = $this->dao->getPostOrGetSanitize('redirect_to_data_field_title'); $pageIDAndType = $this->dao->getPostOrGetSanitize('redirect_to_data_field_id'); $html = $this->f->str_replace('{redirect_to_label}', __('View logs for', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', __('(Begin typing a URL)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', __('(A page has been selected.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', __('(A custom string has been entered.)', '404-solution'), $html); $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', __('(Please choose from the dropdown list instead of typing your own URL.)', '404-solution'), $html); $html = $this->f->str_replace('{pageIDAndType}', $pageIDAndType, $html); $html = $this->f->str_replace('{redirectPageTitle}', $redirectPageTitle, $html); $html = $this->f->str_replace('{data-url}', "admin-ajax.php?action=echoViewLogsFor&nonce=" . wp_create_nonce('abj404_ajax'), $html); $html = $this->f->doNormalReplacements($html); echo $html; // ----------------- dropdown search box. end. echo '
'; // Rows per page echo '
'; echo '' . __('Rows per page:', '404-solution') . ''; echo ''; echo '
'; echo '
'; // Table echo $this->getAdminLogsPageTable($sub); // Pagination (AJAX-capable, includes background refresh config) echo $this->getPaginationLinks($sub, false); echo '
'; } function getAdminLogsPageTable($sub) { $tableOptions = $this->logic->getTableOptions($sub); // Build column headers with sorting $columns = array( 'url' => array('title' => __('URL', '404-solution'), 'orderby' => 'url'), 'host' => array('title' => __('IP Address', '404-solution'), 'orderby' => 'remote_host'), 'refer' => array('title' => __('Referrer', '404-solution'), 'orderby' => 'referrer'), 'dest' => array('title' => __('Action', '404-solution'), 'orderby' => 'action'), 'timestamp' => array('title' => __('Date', '404-solution'), 'orderby' => 'timestamp'), 'username' => array('title' => __('User', '404-solution'), 'orderby' => 'username'), ); $html = ''; $html .= ''; // Generate sortable column headers foreach ($columns as $key => $col) { $sortUrl = "?page=" . ABJ404_PP . "&subpage=abj404_logs"; $sortUrl .= "&orderby=" . $col['orderby']; $sortState = $this->getHeaderSortState($tableOptions, (string)$col['orderby'], false); $newOrder = $sortState['nextOrder']; $sortUrl .= "&order=" . $newOrder; $sortClass = trim($sortState['thClass']); $sortClassAttr = ($sortClass !== '') ? ' class="' . $sortClass . '"' : ''; $sortIndicator = $sortState['indicator']; $html .= ''; } $html .= ''; $html .= ''; $rows = $this->dao->getLogRecords($tableOptions); $this->rememberTableDataSignature($sub, $rows); $logRecordsDisplayed = 0; foreach ($rows as $row) { $html .= ''; // URL column $url = (string)($row['url'] ?? ''); $urlDetail = (string)($row['url_detail'] ?? ''); $urlDisplay = esc_html($url); if ($urlDetail !== '' && trim($urlDetail) !== '') { $urlDisplay .= ' (' . esc_html(trim($urlDetail)) . ')'; } $html .= ''; // IP Address $html .= ''; // Referrer $referrer = (string)($row['referrer'] ?? ''); $html .= ''; // Action Taken $action = trim((string)($row['action'] ?? '')); $html .= ''; // Date $timeToDisplay = abs(intval($row['timestamp'] ?? 0)); $html .= ''; // User $html .= ''; $html .= ''; $logRecordsDisplayed++; } $this->logger->debugMessage($logRecordsDisplayed . " log records displayed on the page."); if ($logRecordsDisplayed == 0) { $html .= ''; } $html .= '
' . esc_html($col['title']) . $sortIndicator . '
' . $urlDisplay . '' . esc_html((string)($row['remote_host'] ?? '')) . ''; if ($referrer != "") { $html .= '' . esc_html($referrer) . ''; } else { $html .= '-'; } $html .= ''; if ($action === '' || $action == "404" || $action == "http://404") { $html .= '' . __('404', '404-solution') . ''; } else { $html .= '' . __('Redirect', '404-solution') . '
'; $html .= '' . esc_html($action) . ''; } $html .= '
' . date('Y/m/d', $timeToDisplay) . '
' . date('h:i:s A', $timeToDisplay) . '
'; if (!empty($row['username'])) { $html .= esc_html($row['username']); } else { $html .= '-'; } $html .= '
' . __('No Results To Display', '404-solution') . '
'; return $html; } /** * @param string $sub * @param array $tableOptions * @param array $columns */ function getTableColumns($sub, $columns) { $tableOptions = $this->logic->getTableOptions($sub); $html = ""; $cbinfo = 'class="manage-column column-cb check-column" style="{cb-info-style}"'; $cbinfoStyle = 'vertical-align: middle; padding-bottom: 6px;'; if ($sub == 'abj404_logs') { $cbinfoStyle .= ' width: 0px;'; } $cbinfo = $this->f->str_replace('{cb-info-style}', $cbinfoStyle, $cbinfo); $html .= ""; if ($sub != 'abj404_logs') { $html .= ""; } $html .= ""; foreach ($columns as $column) { // Skip invalid column definitions (e.g., string instead of array) if (!is_array($column)) { continue; } $style = ""; if (isset($column['width']) && $column['width'] != "") { $style = " style=\"width: " . esc_attr($column['width']) . ";\" "; } $nolink = 0; $sortorder = ""; $sortIndicator = ''; $orderby = isset($column['orderby']) ? $column['orderby'] : ''; $preferDescOnFirstClick = ($orderby == "timestamp" || $orderby == "last_used" || $orderby == "logshits"); $sortState = $this->getHeaderSortState($tableOptions, (string)$orderby, $preferDescOnFirstClick); if (!$sortState['isSortable']) { $thClass = ""; $nolink = 1; } else { $thClass = " " . $sortState['thClass']; $sortorder = $sortState['nextOrder']; $sortIndicator = $sortState['indicator']; } $url = "?page=" . ABJ404_PP; if ($sub !== '') { $url .= "&subpage=" . rawurlencode((string)$sub); } if ($sub == 'abj404_logs') { $url .= "&id=" . ($tableOptions['logsid'] ?? 0); } if (($tableOptions['filter'] ?? 0) != 0) { $url .= "&filter=" . ($tableOptions['filter'] ?? 0); } $url .= "&orderby=" . $orderby . "&order=" . $sortorder; $tooltipHtml = ''; if (array_key_exists('title_attr_html', $column) && !empty($column['title_attr_html'])) { // Raw HTML (already escaped where needed) $tooltipHtml = '' . '' . '' . $column['title_attr_html'] . '' . '' . "\n"; } elseif (array_key_exists('title_attr', $column) && !empty($column['title_attr'])) { // Plain text - escape it $tooltipHtml = '' . '' . '' . esc_html($column['title_attr']) . '' . '' . "\n"; } // Support custom column classes (e.g., hide-on-tablet, hide-on-mobile) if (isset($column['class']) && $column['class'] != '') { $thClass .= ' ' . esc_attr($column['class']); } $html .= " \n"; $title = isset($column['title']) ? $column['title'] : ''; if ($nolink == 1) { $html .= $title; $html .= $tooltipHtml; } else { $html .= ""; $html .= '' . esc_html($title) . $sortIndicator . ""; $html .= ""; $html .= $tooltipHtml; } $html .= ""; } $html .= ""; return $html; } /** * @global type $abj404dao * @param string $sub * @param array $tableOptions */ function getPaginationLinks($sub, $showSearchFilter = true) { $tableOptions = $this->logic->getTableOptions($sub); $logsid = $tableOptions['logsid'] ?? 0; $orderby = $tableOptions['orderby'] ?? 'url'; $order = $tableOptions['order'] ?? 'ASC'; $filter = $tableOptions['filter'] ?? 0; $url = "?page=" . ABJ404_PP; if ($sub !== '') { $url .= "&subpage=" . rawurlencode((string)$sub); } if ($sub == 'abj404_logs') { $url .= "&id=" . $logsid; } $url .= "&orderby=" . sanitize_text_field($orderby); $url .= "&order=" . sanitize_text_field($order); $url .= "&filter=" . absint($filter); if ($sub == 'abj404_logs') { $num_records = $this->dao->getLogsCount($logsid); } else { $num_records = $this->dao->getRedirectsForViewCount($sub, $tableOptions); } // Ensure perpage is never 0 to prevent division by zero $perpage = isset($tableOptions['perpage']) ? absint($tableOptions['perpage']) : ABJ404_OPTION_MIN_PERPAGE; if ($perpage == 0) { $perpage = ABJ404_OPTION_MIN_PERPAGE; } // Ensure paged is a valid integer $paged = isset($tableOptions['paged']) ? absint($tableOptions['paged']) : 1; if ($paged == 0) { $paged = 1; } $total_pages = ceil($num_records / $perpage); if ($total_pages == 0) { $total_pages = 1; } $firsturl = $url; if ($paged == 1) { $prevurl = $url; } else { $prev = $paged - 1; $prevurl = $url . "&paged=" . $prev; } if ($paged + 1 > $total_pages) { if ($paged == 1) { $nexturl = $url; } else { $nexturl = $url . "&paged=" . $paged; } } else { $next = $paged + 1; $nexturl = $url . "&paged=" . $next; } if ($paged + 1 > $total_pages) { if ($paged == 1) { $lasturl = $url; } else { $lasturl = $url . "&paged=" . $paged; } } else { $lasturl = $url . "&paged=" . $total_pages; } // ------------ $start = (($paged - 1) * $perpage) + 1; $end = min($start + $perpage - 1, $num_records); /* Translators: 1: Starting number, 2: Ending number, 3: Total count. */ $currentlyShowingText = sprintf(__('%1$s - %2$s of %3$s', '404-solution'), $start, $end, $num_records); $currentPageText = __('Page', '404-solution') . " " . $paged . " " . __('of', '404-solution') . " " . esc_html($total_pages); $showRowsText = __('Rows per page:', '404-solution'); $showRowsLink = wp_nonce_url($url . '&action=changeItemsPerRow', "abj404_changeItemsPerRow"); $ajaxAction = 'ajaxUpdatePaginationLinks'; $ajaxNonce = wp_create_nonce('abj404_updatePaginationLink'); $searchFilterControl = '\n\n"; $oneBigFormActionURL = $this->getBulkOperationsFormURL($sub, $tableOptions); $html .= '
'; return $html; } }