logic->getTableOptions($sub); // Sanitizing unchecked table options $tableOptions = $this->logic->sanitizePostData($tableOptions); // Modern page wrapper echo '
'; // Header with page title echo '
'; echo '

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

'; echo '
'; // Extract current table options for the filter bar data attributes $perpage = array_key_exists('perpage', $tableOptions) && is_scalar($tableOptions['perpage']) ? $tableOptions['perpage'] : 25; $orderby = array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'timestamp'; $order = array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'DESC'; $paginationNonce = wp_create_nonce('abj404_updatePaginationLink'); $inflightNonce = wp_create_nonce('abj404_fetchInflightStage'); // Filter bar with search dropdown and AJAX data attributes for deferred table load 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}', esc_attr($pageIDAndType), $html); $html = $this->f->str_replace('{redirectPageTitle}', esc_attr($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 ''; echo '
'; // Table placeholder — data loaded via AJAX echo ''; echo ''; echo ''; echo '
' . esc_html__('Loading logs…', '404-solution') . '
' . esc_html__('Loading logs…', '404-solution') . '
'; // Bottom pagination placeholder echo '
'; echo '' . esc_html__('Loading…', '404-solution') . ''; echo '
'; echo '
'; } /** * @param string $sub * @return string */ function getAdminLogsPageTable($sub) { $tableOptions = $this->logic->getTableOptions($sub); // Build column headers with sorting. // Engine is displayed inside the Action cell; User is displayed inside the Date cell. // Perf audit F5: only `url` (alias for requested_url) and `timestamp` // have indexes on logsv2. IP / Referrer / Action map to unindexed // columns, so we render them as plain non-clickable headers to avoid // offering a sort that would trigger filesort on large logs tables. $columns = array( 'url' => array('title' => __('URL', '404-solution'), 'orderby' => 'url'), 'host' => array('title' => __('IP Address', '404-solution'), 'orderby' => ''), 'refer' => array('title' => __('Referrer', '404-solution'), 'orderby' => ''), 'dest' => array('title' => __('Action', '404-solution'), 'orderby' => ''), 'timestamp' => array('title' => __('Date', '404-solution'), 'orderby' => 'timestamp'), ); $html = ''; $html .= ''; // Detail toggle column (not sortable) $html .= ''; // Generate column headers (sortable if the underlying column is indexed). foreach ($columns as $key => $col) { $orderbyCol = $col['orderby']; if ($orderbyCol === '') { $html .= ''; continue; } $sortUrl = "?page=" . ABJ404_PP . "&subpage=abj404_logs"; $sortUrl .= "&orderby=" . $orderbyCol; $sortState = $this->getHeaderSortState($tableOptions, $orderbyCol, 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); /** @var array> $typedLogRows */ $typedLogRows = array_values(array_filter($rows, 'is_array')); $this->rememberTableDataSignature($sub, $typedLogRows); $logRecordsDisplayed = 0; foreach ($typedLogRows as $row) { $logId = is_scalar($row['log_id'] ?? '') ? (string)($row['log_id'] ?? '') : ''; $rawTrace = isset($row['pipeline_trace']) && is_string($row['pipeline_trace']) ? $row['pipeline_trace'] : null; $traceSteps = ABJ_404_Solution_DataAccess::decompressPipelineTrace($rawTrace); $hasTrace = is_array($traceSteps) && !empty($traceSteps); $html .= ''; // Detail toggle button $html .= ''; // URL column $url = is_string($row['url'] ?? '') ? (string)($row['url'] ?? '') : ''; $urlDetail = is_string($row['url_detail'] ?? '') ? (string)($row['url_detail'] ?? '') : ''; $fullVisitorURL = esc_url(home_url($url)); $urlDisplay = '' . esc_html($url) . ''; if ($urlDetail !== '' && trim($urlDetail) !== '') { $urlDisplay .= ' (' . esc_html(trim($urlDetail)) . ')'; } $html .= ''; // IP Address $remoteHost = is_string($row['remote_host'] ?? '') ? (string)($row['remote_host'] ?? '') : ''; $html .= ''; // Referrer $referrer = is_string($row['referrer'] ?? '') ? (string)($row['referrer'] ?? '') : ''; $html .= ''; // Action Taken (with engine on second line) $action = trim(is_string($row['action'] ?? '') ? (string)($row['action'] ?? '') : ''); $engineVal = is_string($row['engine'] ?? '') ? (string)($row['engine'] ?? '') : ''; $html .= ''; // Date (with user on second line) $timeToDisplay = abs(is_scalar($row['timestamp'] ?? 0) ? intval($row['timestamp'] ?? 0) : 0); $rowUsername = is_string($row['username'] ?? '') ? (string)($row['username'] ?? '') : ''; $html .= ''; $html .= ''; // Hidden detail row with pipeline trace if ($hasTrace && $logId !== '') { $html .= ''; $html .= ''; $html .= ''; } $logRecordsDisplayed++; } $this->logger->debugMessage($logRecordsDisplayed . " log records displayed on the page."); if ($logRecordsDisplayed == 0) { $html .= ''; } $html .= '
' . esc_html($col['title']) . '' . esc_html($col['title']) . $sortIndicator . '
'; if ($hasTrace) { $html .= ''; } else { $html .= ''; } $html .= '' . $urlDisplay . '' . esc_html($remoteHost) . ''; 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) . ''; } if ($engineVal !== '') { $html .= '
' . esc_html($engineVal) . ''; } $html .= '
' . (string)wp_date('Y/m/d', $timeToDisplay) . '
' . (string)wp_date('h:i:s A', $timeToDisplay); if ($rowUsername !== '') { $html .= '
' . esc_html($rowUsername) . ''; } $html .= '
' . __('No Results To Display', '404-solution') . '
'; // Inline JS for trace row expand/collapse (output once per page) $html .= ''; return $html; } /** * Translate a known pipeline-trace string for admin display. * * Step names and outcomes are stored in English in the database so they * remain stable across language switches. This method translates them * at render time into the current admin locale. * * @param string $text The English string stored in the trace. * @return string The translated string (falls through unchanged * for dynamic/unknown values like engine names or URLs). */ private function translateTraceLabel(string $text): string { // Static map — evaluated once per request. static $map = null; if ($map === null) { $map = [ // Step names 'Ignore list' => __('Ignore list', '404-solution'), 'Redirect lookup' => __('Redirect lookup', '404-solution'), 'Redirect lookup (without comments)' => __('Redirect lookup (without comments)', '404-solution'), 'Conditions' => __('Conditions', '404-solution'), 'Conditions (without comments)' => __('Conditions (without comments)', '404-solution'), 'Health check' => __('Health check', '404-solution'), 'Health check (without comments)' => __('Health check (without comments)', '404-solution'), 'Regex rules' => __('Regex rules', '404-solution'), 'Suggestion engines' => __('Suggestion engines', '404-solution'), 'Result' => __('Result', '404-solution'), // Outcomes 'Not ignored' => __('Not ignored', '404-solution'), 'Matched — request ignored' => __('Matched — request ignored', '404-solution'), 'Found existing redirect' => __('Found existing redirect', '404-solution'), 'No matching redirect' => __('No matching redirect', '404-solution'), 'All conditions met' => __('All conditions met', '404-solution'), 'Blocked by conditions' => __('Blocked by conditions', '404-solution'), 'Destination unreachable — skipped' => __('Destination unreachable — skipped', '404-solution'), 'Matched' => __('Matched', '404-solution'), 'No match' => __('No match', '404-solution'), 'Skipped' => __('Skipped', '404-solution'), 'Excluded' => __('Excluded', '404-solution'), 'Error' => __('Error', '404-solution'), 'No match found' => __('No match found', '404-solution'), 'Showed 404 page' => __('Showed 404 page', '404-solution'), 'Redirected to external URL' => __('Redirected to external URL', '404-solution'), 'No redirect — showed 404 page' => __('No redirect — showed 404 page', '404-solution'), ]; } // Exact match if (isset($map[$text])) { return $map[$text]; } // Dynamic patterns: "Engine: Spelling", "Redirected (301)", etc. if (strpos($text, 'Engine: ') === 0) { $engineName = substr($text, 8); return sprintf(__('Engine: %s', '404-solution'), $engineName); } if (preg_match('/^Redirected \((\d+)\)$/', $text, $m)) { return sprintf(__('Redirected (%s)', '404-solution'), $m[1]); } if (preg_match('/^Responded with (.+)$/', $text, $m)) { return sprintf(__('Responded with %s', '404-solution'), $m[1]); } if (preg_match('/^Showed 404 page — (.+)$/', $text, $m)) { return sprintf(__('Showed 404 page — %s', '404-solution'), $m[1]); } return $text; } /** * Return a CSS class for the trace outcome badge based on the outcome text. * * @param string $outcome * @return string */ private function traceOutcomeClass(string $outcome): string { $lower = strtolower($outcome); // Negative / blocking outcomes if (strpos($lower, 'blocked') !== false || strpos($lower, 'unreachable') !== false || strpos($lower, 'error') !== false || strpos($lower, 'ignored') !== false || strpos($lower, 'missing') !== false || strpos($lower, 'invalid') !== false) { return 'abj404-trace-outcome-fail'; } // Final result outcomes if (strpos($lower, 'redirected') !== false || strpos($lower, 'responded') !== false || strpos($lower, 'showed 404') !== false || strpos($lower, 'no redirect') !== false) { return 'abj404-trace-outcome-result'; } // Positive / pass-through outcomes if (strpos($lower, 'found') !== false || strpos($lower, 'matched') !== false || strpos($lower, 'passed') !== false || strpos($lower, 'not ignored') !== false || strpos($lower, 'all conditions met') !== false) { return 'abj404-trace-outcome-pass'; } // Neutral (skipped, no match, etc.) return 'abj404-trace-outcome-neutral'; } /** * @param string $sub * @param array> $columns * @return string */ 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) { $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']; } $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; } /** * @param string $sub * @param bool $showSearchFilter */ /** * @param string $sub * @param bool $showSearchFilter * @return string */ function getPaginationLinks($sub, $showSearchFilter = true) { $tableOptions = $this->logic->getTableOptions($sub); $logsid = array_key_exists('logsid', $tableOptions) && is_scalar($tableOptions['logsid']) ? $tableOptions['logsid'] : 0; $orderby = array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url'; $order = array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC'; $filter = array_key_exists('filter', $tableOptions) && is_scalar($tableOptions['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((int)$filter); if ($sub == 'abj404_logs') { $num_records = $this->dao->getLogsCount((int)$logsid); } else { $num_records = $this->dao->getRedirectsForViewCount($sub, $tableOptions); } // Ensure perpage is never 0 to prevent division by zero $perpage = absint(array_key_exists('perpage', $tableOptions) && is_scalar($tableOptions['perpage']) ? $tableOptions['perpage'] : ABJ404_OPTION_MIN_PERPAGE); if ($perpage == 0) { $perpage = ABJ404_OPTION_MIN_PERPAGE; } // Ensure paged is a valid integer $paged = absint(array_key_exists('paged', $tableOptions) && is_scalar($tableOptions['paged']) ? $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((string)$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'); $inflightNonce = wp_create_nonce('abj404_fetchInflightStage'); $searchFilterControl = '\n\n"; $oneBigFormActionURL = $this->getBulkOperationsFormURL($sub, $tableOptions); $html .= '
'; return $html; } }