PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / ViewTrait_Logs.php

ViewTrait_Logs.php in 404 Solution 4.2.0, at includes/ViewTrait_Logs.php

830 lines 41.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * ViewTrait_Logs methods.
9 */
10 trait ViewTrait_Logs {
11
12
13 /**
14 * @return void
15 */
16 function echoAdminLogsPage() {
17
18 $sub = 'abj404_logs';
19 $tableOptions = $this->logic->getTableOptions($sub);
20
21 // Sanitizing unchecked table options
22 $tableOptions = $this->logic->sanitizePostData($tableOptions);
23
24
25 // Modern page wrapper
26 echo '<div class="abj404-table-page">';
27
28 // Header with page title
29 echo '<div class="abj404-table-header">';
30 echo '<h2>' . __('Redirect Logs', '404-solution') . '</h2>';
31 echo '</div>';
32
33 // Extract current table options for the filter bar data attributes
34 $perpage = array_key_exists('perpage', $tableOptions) && is_scalar($tableOptions['perpage']) ? $tableOptions['perpage'] : 25;
35 $orderby = array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'timestamp';
36 $order = array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'DESC';
37 $paginationNonce = wp_create_nonce('abj404_updatePaginationLink');
38 $inflightNonce = wp_create_nonce('abj404_fetchInflightStage');
39
40 // Filter bar with search dropdown and AJAX data attributes for deferred table load
41 echo '<div class="abj404-filter-bar tablenav"'
42 . ' data-pagination-ajax-url="' . esc_attr(admin_url('admin-ajax.php')) . '"'
43 . ' data-pagination-ajax-action="ajaxUpdatePaginationLinks"'
44 . ' data-pagination-ajax-subpage="' . esc_attr($sub) . '"'
45 . ' data-pagination-ajax-nonce="' . esc_attr($paginationNonce) . '"'
46 . ' data-pagination-inflight-nonce="' . esc_attr($inflightNonce) . '"'
47 . ' data-pagination-current-signature=""'
48 . ' data-pagination-current-orderby="' . esc_attr($orderby) . '"'
49 . ' data-pagination-current-order="' . esc_attr($order) . '"'
50 . ' data-pagination-current-filter="0"'
51 . ' data-pagination-current-paged="1"'
52 . ' data-pagination-current-score-range="all"'
53 . ' data-pagination-current-logsid="' . esc_attr((string)$this->viewGetPostOrGetSanitize('redirect_to_data_field_id')) . '"'
54 . ' data-pagination-initial-load="1"'
55 . ' data-pagination-auto-refresh="1"'
56 . ' data-pagination-refresh-started-text="' . esc_attr(__('Refreshing data in background…', '404-solution')) . '"'
57 . ' data-pagination-refresh-finished-text="' . esc_attr(__('Data refreshed', '404-solution')) . '"'
58 . ' data-pagination-refresh-available-text="' . esc_attr(__('Refresh available', '404-solution')) . '">';
59
60 // Log search form
61 echo '<form id="logs_search_form" name="admin-logs-page" method="GET" action="" class="abj404-logs-search-form">';
62 echo '<input type="hidden" name="page" value="' . ABJ404_PP . '">';
63 echo '<input type="hidden" name="subpage" value="abj404_logs">';
64
65 // ----------------- dropdown search box. begin.
66 $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ .
67 "/html/viewLogsForSearchBox.html");
68
69 $redirectPageTitle = $this->viewGetPostOrGetSanitize('redirect_to_data_field_title');
70 $pageIDAndType = $this->viewGetPostOrGetSanitize('redirect_to_data_field_id');
71
72 $html = $this->f->str_replace('{redirect_to_label}', __('View logs for', '404-solution'), $html);
73 $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}',
74 __('(Begin typing a URL)', '404-solution'), $html);
75 $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}',
76 __('(A page has been selected.)', '404-solution'), $html);
77 $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}',
78 __('(A custom string has been entered.)', '404-solution'), $html);
79 $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}',
80 __('(Please choose from the dropdown list instead of typing your own URL.)', '404-solution'), $html);
81 $html = $this->f->str_replace('{pageIDAndType}', esc_attr($pageIDAndType), $html);
82 $html = $this->f->str_replace('{redirectPageTitle}', esc_attr($redirectPageTitle), $html);
83 $html = $this->f->str_replace('{data-url}',
84 "admin-ajax.php?action=echoViewLogsFor&nonce=" . wp_create_nonce('abj404_ajax'), $html);
85 $html = $this->f->doNormalReplacements($html);
86 echo $html;
87 // ----------------- dropdown search box. end.
88
89 echo '</form>';
90
91 // Rows per page
92 echo '<div class="abj404-rows-per-page">';
93 echo '<span>' . __('Rows per page:', '404-solution') . '</span>';
94 echo '<select class="abj404-filter-select perpage" name="perpage" onchange="paginationLinksChange(this);">';
95 $perPageOptions = array(10, 25, 50, 100, 250);
96 foreach ($perPageOptions as $opt) {
97 $selected = ($perpage == $opt) ? ' selected' : '';
98 echo '<option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
99 }
100 echo '</select>';
101 echo '</div>';
102
103 echo '<span class="abj404-refresh-status" aria-live="polite"></span>';
104 echo '</div><!-- .abj404-filter-bar -->';
105
106 // Table placeholder — data loaded via AJAX
107 echo '<table class="abj404-table" data-table-awaiting-load="1">';
108 echo '<thead><tr><th>' . esc_html__('Loading logs…', '404-solution') . '</th></tr></thead>';
109 echo '<tbody><tr><td class="abj404-empty-message">' . esc_html__('Loading logs…', '404-solution') . '</td></tr></tbody>';
110 echo '</table>';
111
112 // Bottom pagination placeholder
113 echo '<div class="abj404-pagination tablenav abj404-pagination-right abj404-pagination-bottom">';
114 echo '<span class="abj404-refresh-status" aria-live="polite">' . esc_html__('Loading…', '404-solution') . '</span>';
115 echo '</div>';
116
117 echo '</div><!-- .abj404-table-page -->';
118 }
119
120 /**
121 * @param string $sub
122 * @return string
123 */
124 function getAdminLogsPageTable($sub) {
125
126 $tableOptions = $this->logic->getTableOptions($sub);
127
128 // Build column headers with sorting.
129 // Engine is displayed inside the Action cell; User is displayed inside the Date cell.
130 // Perf audit F5: only `url` (alias for requested_url) and `timestamp`
131 // have indexes on logsv2. IP / Referrer / Action map to unindexed
132 // columns, so we render them as plain non-clickable headers to avoid
133 // offering a sort that would trigger filesort on large logs tables.
134 $columns = array(
135 'url' => array('title' => __('URL', '404-solution'), 'orderby' => 'url'),
136 'host' => array('title' => __('IP Address', '404-solution'), 'orderby' => ''),
137 'refer' => array('title' => __('Referrer', '404-solution'), 'orderby' => ''),
138 'dest' => array('title' => __('Action', '404-solution'), 'orderby' => ''),
139 'timestamp' => array('title' => __('Date', '404-solution'), 'orderby' => 'timestamp'),
140 );
141
142 $html = '<table class="abj404-table abj404-logs-table">';
143 $html .= '<thead><tr>';
144
145 // Detail toggle column (not sortable)
146 $html .= '<th scope="col" class="abj404-trace-th"></th>';
147
148 // Generate column headers (sortable if the underlying column is indexed).
149 foreach ($columns as $key => $col) {
150 $orderbyCol = $col['orderby'];
151 if ($orderbyCol === '') {
152 $html .= '<th scope="col">' . esc_html($col['title']) . '</th>';
153 continue;
154 }
155 $sortUrl = "?page=" . ABJ404_PP . "&subpage=abj404_logs";
156 $sortUrl .= "&orderby=" . $orderbyCol;
157 $sortState = $this->getHeaderSortState($tableOptions, $orderbyCol, false);
158 $newOrder = $sortState['nextOrder'];
159 $sortUrl .= "&order=" . $newOrder;
160
161 $sortClass = trim($sortState['thClass']);
162 $sortClassAttr = ($sortClass !== '') ? ' class="' . $sortClass . '"' : '';
163 $sortIndicator = $sortState['indicator'];
164
165 $html .= '<th scope="col"' . $sortClassAttr . '><a href="' . esc_url($sortUrl) . '">' . esc_html($col['title']) . $sortIndicator . '</a></th>';
166 }
167
168 $html .= '</tr></thead>';
169 $html .= '<tbody id="the-list">';
170
171 $rows = $this->logsRepository->getLogRecords($tableOptions);
172 /** @var array<int, array<string, mixed>> $typedLogRows */
173 $typedLogRows = array_values(array_filter($rows, 'is_array'));
174 $this->rememberTableDataSignature($sub, $typedLogRows);
175 $logRecordsDisplayed = 0;
176
177 foreach ($typedLogRows as $row) {
178 $logId = is_scalar($row['log_id'] ?? '') ? (string)($row['log_id'] ?? '') : '';
179 $rawTrace = isset($row['pipeline_trace']) && is_string($row['pipeline_trace']) ? $row['pipeline_trace'] : null;
180 $traceSteps = ABJ_404_Solution_LogsRepository::decompressPipelineTrace($rawTrace);
181 $hasTrace = is_array($traceSteps) && !empty($traceSteps);
182
183 $html .= '<tr>';
184
185 // Detail toggle button
186 $html .= '<td class="abj404-trace-toggle-cell">';
187 if ($hasTrace) {
188 $html .= '<button class="abj404-trace-toggle" data-row-id="' . esc_attr($logId) . '" title="' . esc_attr__('Show pipeline trace', '404-solution') . '">&#x25B6;</button>';
189 } else {
190 $html .= '<button class="abj404-trace-toggle" disabled title="' . esc_attr__('No trace available', '404-solution') . '">&#x25B6;</button>';
191 }
192 $html .= '</td>';
193
194 // URL column
195 $url = is_string($row['url'] ?? '') ? (string)($row['url'] ?? '') : '';
196 $urlDetail = is_string($row['url_detail'] ?? '') ? (string)($row['url_detail'] ?? '') : '';
197 $fullVisitorURL = esc_url(home_url($url));
198 $urlDisplay = '<a href="' . $fullVisitorURL . '" target="_blank" title="' . esc_attr($url) . '">' . esc_html($url) . '</a>';
199 if ($urlDetail !== '' && trim($urlDetail) !== '') {
200 $urlDisplay .= ' <span class="abj404-url-detail">(' . esc_html(trim($urlDetail)) . ')</span>';
201 }
202 $html .= '<td class="abj404-url-cell">' . $urlDisplay . '</td>';
203
204 // IP Address
205 $remoteHost = is_string($row['remote_host'] ?? '') ? (string)($row['remote_host'] ?? '') : '';
206 $html .= '<td class="abj404-ip-cell">' . esc_html($remoteHost) . '</td>';
207
208 // Referrer
209 $referrer = is_string($row['referrer'] ?? '') ? (string)($row['referrer'] ?? '') : '';
210 $html .= '<td class="abj404-url-cell">';
211 if ($referrer != "") {
212 $html .= '<a href="' . esc_url($referrer) . '" title="' . esc_attr($referrer) . '" target="_blank">' . esc_html($referrer) . '</a>';
213 } else {
214 $html .= '<span class="abj404-text-muted">-</span>';
215 }
216 $html .= '</td>';
217
218 // Action Taken (with engine on second line)
219 $action = trim(is_string($row['action'] ?? '') ? (string)($row['action'] ?? '') : '');
220 $engineVal = is_string($row['engine'] ?? '') ? (string)($row['engine'] ?? '') : '';
221 $html .= '<td>';
222 if ($action === '' || $action == "404" || $action == "http://404") {
223 $html .= '<span class="abj404-badge abj404-badge-404">' . __('404', '404-solution') . '</span>';
224 } else {
225 $html .= '<span class="abj404-badge abj404-badge-redirect">' . __('Redirect', '404-solution') . '</span>';
226 $html .= '<br><a href="' . esc_url($action) . '" title="' . esc_attr($action) . '" target="_blank" class="abj404-action-url">' . esc_html($action) . '</a>';
227 }
228 if ($engineVal !== '') {
229 $html .= '<br><span class="abj404-engine-label" title="' . esc_attr__('Engine', '404-solution') . '">' . esc_html($engineVal) . '</span>';
230 }
231 $html .= '</td>';
232
233 // Date (with user on second line)
234 $timeToDisplay = abs(is_scalar($row['timestamp'] ?? 0) ? intval($row['timestamp'] ?? 0) : 0);
235 $rowUsername = is_string($row['username'] ?? '') ? (string)($row['username'] ?? '') : '';
236 $html .= '<td class="abj404-date-cell">' . (string)wp_date('Y/m/d', $timeToDisplay) . '<br>' . (string)wp_date('h:i:s A', $timeToDisplay);
237 if ($rowUsername !== '') {
238 $html .= '<br><span class="abj404-username-label">' . esc_html($rowUsername) . '</span>';
239 }
240 $html .= '</td>';
241
242 $html .= '</tr>';
243
244 // Hidden detail row with pipeline trace
245 if ($hasTrace && $logId !== '') {
246 $html .= '<tr id="abj404-trace-' . esc_attr($logId) . '" class="abj404-trace-detail" style="display:none">';
247 $html .= '<td colspan="6" class="abj404-trace-detail-cell">';
248 $html .= '<ol class="abj404-trace-steps">';
249 foreach ($traceSteps as $step) {
250 $stepName = esc_html($this->translateTraceLabel($step['step']));
251 $outcome = esc_html($this->translateTraceLabel($step['outcome']));
252 $detail = ($step['detail'] !== '')
253 ? ' <span class="abj404-trace-detail-text">' . esc_html($step['detail']) . '</span>' : '';
254 $outcomeClass = $this->traceOutcomeClass($step['outcome']);
255 $html .= '<li><span class="abj404-trace-step-name">' . $stepName . '</span>'
256 . '<span class="abj404-trace-outcome ' . $outcomeClass . '">' . $outcome . '</span>'
257 . $detail . '</li>';
258 }
259 $html .= '</ol>';
260 $html .= '</td>';
261 $html .= '</tr>';
262 }
263
264 $logRecordsDisplayed++;
265 }
266
267 $this->logger->debugMessage($logRecordsDisplayed . " log records displayed on the page.");
268
269 if ($logRecordsDisplayed == 0) {
270 $html .= '<tr><td colspan="6" class="abj404-empty-message">' . __('No Results To Display', '404-solution') . '</td></tr>';
271 }
272
273 $html .= '</tbody></table>';
274
275 // Inline JS for trace row expand/collapse (output once per page)
276 $html .= '<script>
277 (function() {
278 if (window._abj404TraceToggleInit) { return; }
279 window._abj404TraceToggleInit = true;
280 document.addEventListener("click", function(e) {
281 var btn = e.target;
282 if (!btn || !btn.classList || !btn.classList.contains("abj404-trace-toggle")) { return; }
283 var rowId = btn.getAttribute("data-row-id");
284 if (!rowId) { return; }
285 var detail = document.getElementById("abj404-trace-" + rowId);
286 if (!detail) { return; }
287 var visible = detail.style.display !== "none";
288 detail.style.display = visible ? "none" : "";
289 btn.textContent = visible ? "\u25B6" : "\u25BC";
290 });
291 }());
292 </script>';
293
294 return $html;
295 }
296
297 /**
298 * Translate a known pipeline-trace string for admin display.
299 *
300 * Step names and outcomes are stored in English in the database so they
301 * remain stable across language switches. This method translates them
302 * at render time into the current admin locale.
303 *
304 * @param string $text The English string stored in the trace.
305 * @return string The translated string (falls through unchanged
306 * for dynamic/unknown values like engine names or URLs).
307 */
308 private function translateTraceLabel(string $text): string {
309 // Static map — evaluated once per request.
310 static $map = null;
311 if ($map === null) {
312 $map = [
313 // Step names
314 'Ignore list' => __('Ignore list', '404-solution'),
315 'Redirect lookup' => __('Redirect lookup', '404-solution'),
316 'Redirect lookup (without comments)' => __('Redirect lookup (without comments)', '404-solution'),
317 'Conditions' => __('Conditions', '404-solution'),
318 'Conditions (without comments)' => __('Conditions (without comments)', '404-solution'),
319 'Health check' => __('Health check', '404-solution'),
320 'Health check (without comments)' => __('Health check (without comments)', '404-solution'),
321 'Regex rules' => __('Regex rules', '404-solution'),
322 'Suggestion engines' => __('Suggestion engines', '404-solution'),
323 'Result' => __('Result', '404-solution'),
324 // Outcomes
325 'Not ignored' => __('Not ignored', '404-solution'),
326 'Matched — request ignored' => __('Matched — request ignored', '404-solution'),
327 'Found existing redirect' => __('Found existing redirect', '404-solution'),
328 'No matching redirect' => __('No matching redirect', '404-solution'),
329 'All conditions met' => __('All conditions met', '404-solution'),
330 'Blocked by conditions' => __('Blocked by conditions', '404-solution'),
331 'Destination unreachable — skipped' => __('Destination unreachable — skipped', '404-solution'),
332 'Matched' => __('Matched', '404-solution'),
333 'No match' => __('No match', '404-solution'),
334 'Skipped' => __('Skipped', '404-solution'),
335 'Excluded' => __('Excluded', '404-solution'),
336 'Error' => __('Error', '404-solution'),
337 'No match found' => __('No match found', '404-solution'),
338 'Showed 404 page' => __('Showed 404 page', '404-solution'),
339 'Redirected to external URL' => __('Redirected to external URL', '404-solution'),
340 'No redirect — showed 404 page' => __('No redirect — showed 404 page', '404-solution'),
341 ];
342 }
343
344 // Exact match
345 if (isset($map[$text])) {
346 return $map[$text];
347 }
348
349 // Dynamic patterns: "Engine: Spelling", "Redirected (301)", etc.
350 if (strpos($text, 'Engine: ') === 0) {
351 $engineName = substr($text, 8);
352 return sprintf(__('Engine: %s', '404-solution'), $engineName);
353 }
354 if (preg_match('/^Redirected \((\d+)\)$/', $text, $m)) {
355 return sprintf(__('Redirected (%s)', '404-solution'), $m[1]);
356 }
357 if (preg_match('/^Responded with (.+)$/', $text, $m)) {
358 return sprintf(__('Responded with %s', '404-solution'), $m[1]);
359 }
360 if (preg_match('/^Showed 404 page — (.+)$/', $text, $m)) {
361 return sprintf(__('Showed 404 page — %s', '404-solution'), $m[1]);
362 }
363
364 return $text;
365 }
366
367 /**
368 * Return a CSS class for the trace outcome badge based on the outcome text.
369 *
370 * @param string $outcome
371 * @return string
372 */
373 private function traceOutcomeClass(string $outcome): string {
374 $lower = strtolower($outcome);
375 // Negative / blocking outcomes
376 if (strpos($lower, 'blocked') !== false || strpos($lower, 'unreachable') !== false
377 || strpos($lower, 'error') !== false || strpos($lower, 'ignored') !== false
378 || strpos($lower, 'missing') !== false || strpos($lower, 'invalid') !== false) {
379 return 'abj404-trace-outcome-fail';
380 }
381 // Final result outcomes
382 if (strpos($lower, 'redirected') !== false || strpos($lower, 'responded') !== false
383 || strpos($lower, 'showed 404') !== false || strpos($lower, 'no redirect') !== false) {
384 return 'abj404-trace-outcome-result';
385 }
386 // Positive / pass-through outcomes
387 if (strpos($lower, 'found') !== false || strpos($lower, 'matched') !== false
388 || strpos($lower, 'passed') !== false || strpos($lower, 'not ignored') !== false
389 || strpos($lower, 'all conditions met') !== false) {
390 return 'abj404-trace-outcome-pass';
391 }
392 // Neutral (skipped, no match, etc.)
393 return 'abj404-trace-outcome-neutral';
394 }
395
396 /**
397 * @param string $sub
398 * @param array<string, array<string, string>> $columns
399 * @return string
400 */
401 function getTableColumns($sub, $columns) {
402 $tableOptions = $this->logic->getTableOptions($sub);
403
404 $html = "<tr>";
405
406 $cbinfo = 'class="manage-column column-cb check-column" style="{cb-info-style}"';
407 $cbinfoStyle = 'vertical-align: middle; padding-bottom: 6px;';
408 if ($sub == 'abj404_logs') {
409 $cbinfoStyle .= ' width: 0px;';
410 }
411 $cbinfo = $this->f->str_replace('{cb-info-style}', $cbinfoStyle, $cbinfo);
412
413 $html .= "<th scope=\"col\" " . $cbinfo . ">";
414 if ($sub != 'abj404_logs') {
415 $html .= "<input type=\"checkbox\" name=\"bulkSelectorCheckbox\" onchange=\"enableDisableApplyButton();\" aria-label=\"" . esc_attr__('Select all', '404-solution') . "\">";
416 }
417 $html .= "</th>";
418
419 foreach ($columns as $column) {
420 $style = "";
421 if (isset($column['width']) && $column['width'] != "") {
422 $style = " style=\"width: " . esc_attr($column['width']) . ";\" ";
423 }
424 $nolink = 0;
425 $sortorder = "";
426 $sortIndicator = '';
427 $orderby = isset($column['orderby']) ? $column['orderby'] : '';
428 $preferDescOnFirstClick = ($orderby == "timestamp" ||
429 $orderby == "last_used" ||
430 $orderby == "logshits");
431 $sortState = $this->getHeaderSortState($tableOptions, (string)$orderby, $preferDescOnFirstClick);
432 if (!$sortState['isSortable']) {
433 $thClass = "";
434 $nolink = 1;
435 } else {
436 $thClass = " " . $sortState['thClass'];
437 $sortorder = $sortState['nextOrder'];
438 $sortIndicator = $sortState['indicator'];
439 }
440
441 $url = "?page=" . ABJ404_PP;
442 if ($sub !== '') {
443 $url .= "&subpage=" . rawurlencode((string)$sub);
444 }
445 if ($sub == 'abj404_logs') {
446 $url .= "&id=" . ($tableOptions['logsid'] ?? 0);
447 }
448 if (($tableOptions['filter'] ?? 0) != 0) {
449 $url .= "&filter=" . $tableOptions['filter'];
450 }
451 $url .= "&orderby=" . $orderby . "&order=" . $sortorder;
452
453 $tooltipHtml = '';
454 if (array_key_exists('title_attr_html', $column) && !empty($column['title_attr_html'])) {
455 // Raw HTML (already escaped where needed)
456 $tooltipHtml = '<span class="abj404-header-tooltip lefty-tooltip" aria-label="' . esc_attr__('More info', '404-solution') . '">' .
457 '<span class="abj404-header-tooltip-icon" aria-hidden="true">?</span>' .
458 '<span class="lefty-tooltiptext">' . $column['title_attr_html'] . '</span>' .
459 '</span>' . "\n";
460 } elseif (array_key_exists('title_attr', $column) && !empty($column['title_attr'])) {
461 // Plain text - escape it
462 $tooltipHtml = '<span class="abj404-header-tooltip lefty-tooltip" aria-label="' . esc_attr__('More info', '404-solution') . '">' .
463 '<span class="abj404-header-tooltip-icon" aria-hidden="true">?</span>' .
464 '<span class="lefty-tooltiptext">' . esc_html($column['title_attr']) . '</span>' .
465 '</span>' . "\n";
466 }
467
468 // Support custom column classes (e.g., hide-on-tablet, hide-on-mobile)
469 if (isset($column['class']) && $column['class'] != '') {
470 $thClass .= ' ' . esc_attr($column['class']);
471 }
472
473 $html .= "<th scope=\"col\" " . $style . " class=\"manage-column column-title" . $thClass . "\"> \n";
474
475 $title = isset($column['title']) ? $column['title'] : '';
476 if ($nolink == 1) {
477 $html .= $title;
478 $html .= $tooltipHtml;
479 } else {
480 $html .= "<a href=\"" . esc_url($url) . "\">";
481 $html .= '<span class="table_header_' . $orderby . '">' .
482 esc_html($title) . $sortIndicator . "</span>";
483 $html .= "</a>";
484 $html .= $tooltipHtml;
485 }
486 $html .= "</th>";
487 }
488 $html .= "</tr>";
489
490 return $html;
491 }
492
493 /**
494 * @param string $sub
495 * @param bool $showSearchFilter
496 */
497 /**
498 * @param string $sub
499 * @param bool $showSearchFilter
500 * @return string
501 */
502 function getPaginationLinks($sub, $showSearchFilter = true) {
503
504 $tableOptions = $this->logic->getTableOptions($sub);
505 $logsid = array_key_exists('logsid', $tableOptions) && is_scalar($tableOptions['logsid']) ? $tableOptions['logsid'] : 0;
506 $orderby = array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url';
507 $order = array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC';
508 $filter = array_key_exists('filter', $tableOptions) && is_scalar($tableOptions['filter']) ? $tableOptions['filter'] : 0;
509
510 $url = "?page=" . ABJ404_PP;
511 if ($sub !== '') {
512 $url .= "&subpage=" . rawurlencode((string)$sub);
513 }
514 if ($sub == 'abj404_logs') {
515 $url .= "&id=" . $logsid;
516 }
517
518 $url .= "&orderby=" . sanitize_text_field($orderby);
519 $url .= "&order=" . sanitize_text_field($order);
520 $url .= "&filter=" . absint((int)$filter);
521
522 if ($sub == 'abj404_logs') {
523 $num_records = $this->viewReadService->getLogsCount((int)$logsid);
524 } else {
525 $num_records = $this->viewReadService->getRedirectsForViewCount($sub, $tableOptions);
526 }
527
528 // Ensure perpage is never 0 to prevent division by zero
529 $perpage = absint(array_key_exists('perpage', $tableOptions) && is_scalar($tableOptions['perpage']) ? $tableOptions['perpage'] : ABJ404_OPTION_MIN_PERPAGE);
530 if ($perpage == 0) {
531 $perpage = ABJ404_OPTION_MIN_PERPAGE;
532 }
533
534 // Ensure paged is a valid integer
535 $paged = absint(array_key_exists('paged', $tableOptions) && is_scalar($tableOptions['paged']) ? $tableOptions['paged'] : 1);
536 if ($paged == 0) {
537 $paged = 1;
538 }
539
540 $total_pages = ceil($num_records / $perpage);
541 if ($total_pages == 0) {
542 $total_pages = 1;
543 }
544
545 $firsturl = $url;
546
547 if ($paged == 1) {
548 $prevurl = $url;
549 } else {
550 $prev = $paged - 1;
551 $prevurl = $url . "&paged=" . $prev;
552 }
553
554 if ($paged + 1 > $total_pages) {
555 if ($paged == 1) {
556 $nexturl = $url;
557 } else {
558 $nexturl = $url . "&paged=" . $paged;
559 }
560 } else {
561 $next = $paged + 1;
562 $nexturl = $url . "&paged=" . $next;
563 }
564
565 if ($paged + 1 > $total_pages) {
566 if ($paged == 1) {
567 $lasturl = $url;
568 } else {
569 $lasturl = $url . "&paged=" . $paged;
570 }
571 } else {
572 $lasturl = $url . "&paged=" . $total_pages;
573 }
574
575 // ------------
576 $start = (($paged - 1) * $perpage) + 1;
577 $end = min($start + $perpage - 1, $num_records);
578 /* Translators: 1: Starting number, 2: Ending number, 3: Total count. */
579 $currentlyShowingText = sprintf(__('%1$s - %2$s of %3$s', '404-solution'), $start, $end, $num_records);
580 $currentPageText = __('Page', '404-solution') . " " . $paged . " " . __('of', '404-solution') . " " . esc_html((string)$total_pages);
581 $showRowsText = __('Rows per page:', '404-solution');
582 $showRowsLink = wp_nonce_url($url . '&action=changeItemsPerRow', "abj404_changeItemsPerRow");
583
584 $ajaxAction = 'ajaxUpdatePaginationLinks';
585 $ajaxNonce = wp_create_nonce('abj404_updatePaginationLink');
586 $inflightNonce = wp_create_nonce('abj404_fetchInflightStage');
587
588 $searchFilterControl = '<!--';
589 if ($sub == 'abj404_redirects' || $sub == 'abj404_captured') {
590 $searchFilterControl = '';
591 }
592 if (!$showSearchFilter) {
593 $searchFilterControl = '<!--';
594 }
595
596 $filterText = array_key_exists('filterText', $tableOptions) && is_string($tableOptions['filterText']) ? $tableOptions['filterText'] : '';
597 if ($filterText != '') {
598 $encodedFilterText = rawurlencode((string)$filterText);
599 $nexturl .= '&filterText=' . $encodedFilterText;
600 $prevurl .= '&filterText=' . $encodedFilterText;
601 $firsturl .= '&filterText=' . $encodedFilterText;
602 $lasturl .= '&filterText=' . $encodedFilterText;
603 }
604
605 // read the html content.
606 $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/paginationLinks.html");
607 // do special replacements
608 $perpage = array_key_exists('perpage', $tableOptions) && is_scalar($tableOptions['perpage']) ? $tableOptions['perpage'] : ABJ404_OPTION_DEFAULT_PERPAGE;
609 $html = $this->f->str_replace(' value="' . $perpage . '"',
610 ' value="' . $perpage . '" selected',
611 $html);
612 $html = $this->f->str_replace('{changeItemsPerPage}', $showRowsLink, $html);
613 $html = $this->f->str_replace('{showSearchFilter}', $searchFilterControl, $html);
614 $html = $this->f->str_replace('{TEXT_BEFORE_LINKS}', $currentlyShowingText, $html);
615 $html = $this->f->str_replace('{TEXT_SHOW_ROWS}', $showRowsText, $html);
616 // Build navigation buttons: disabled (span) when at the boundary page, link (a) otherwise.
617 $onFirstPage = ($paged <= 1);
618 $onLastPage = ($paged >= $total_pages);
619
620 if ($onFirstPage) {
621 $btnFirst = '<span class="abj404-page-btn disabled" aria-label="' . esc_attr__('Go to first page', '404-solution') . '" aria-disabled="true">&laquo;</span>';
622 $btnPrev = '<span class="abj404-page-btn disabled" aria-label="' . esc_attr__('Go to previous page', '404-solution') . '" aria-disabled="true">&lsaquo;</span>';
623 } else {
624 $btnFirst = '<a href="' . esc_url($firsturl) . '" class="abj404-page-btn" title="' . esc_attr__('Go to first page', '404-solution') . '" aria-label="' . esc_attr__('Go to first page', '404-solution') . '">&laquo;</a>';
625 $btnPrev = '<a href="' . esc_url($prevurl) . '" class="abj404-page-btn" title="' . esc_attr__('Go to previous page', '404-solution') . '" aria-label="' . esc_attr__('Go to previous page', '404-solution') . '">&lsaquo;</a>';
626 }
627
628 if ($onLastPage) {
629 $btnNext = '<span class="abj404-page-btn disabled" aria-label="' . esc_attr__('Go to next page', '404-solution') . '" aria-disabled="true">&rsaquo;</span>';
630 $btnLast = '<span class="abj404-page-btn disabled" aria-label="' . esc_attr__('Go to last page', '404-solution') . '" aria-disabled="true">&raquo;</span>';
631 } else {
632 $btnNext = '<a href="' . esc_url($nexturl) . '" class="abj404-page-btn" title="' . esc_attr__('Go to next page', '404-solution') . '" aria-label="' . esc_attr__('Go to next page', '404-solution') . '">&rsaquo;</a>';
633 $btnLast = '<a href="' . esc_url($lasturl) . '" class="abj404-page-btn" title="' . esc_attr__('Go to last page', '404-solution') . '" aria-label="' . esc_attr__('Go to last page', '404-solution') . '">&raquo;</a>';
634 }
635
636 $html = $this->f->str_replace('{BTN_FIRST_PAGE}', $btnFirst, $html);
637 $html = $this->f->str_replace('{BTN_PREV_PAGE}', $btnPrev, $html);
638 $html = $this->f->str_replace('{TEXT_CURRENT_PAGE}', $currentPageText, $html);
639 $html = $this->f->str_replace('{BTN_NEXT_PAGE}', $btnNext, $html);
640 $html = $this->f->str_replace('{BTN_LAST_PAGE}', $btnLast, $html);
641 $html = $this->f->str_replace('{filterText}', esc_attr($filterText), $html);
642 $html = $this->f->str_replace('{data-pagination-ajax-url}', esc_attr(admin_url('admin-ajax.php')), $html);
643 $html = $this->f->str_replace('{data-pagination-ajax-action}', esc_attr($ajaxAction), $html);
644 $html = $this->f->str_replace('{data-pagination-ajax-subpage}', esc_attr($sub), $html);
645 $html = $this->f->str_replace('{data-pagination-ajax-nonce}', esc_attr($ajaxNonce), $html);
646 $html = $this->f->str_replace('{data-pagination-inflight-nonce}', esc_attr($inflightNonce), $html);
647 $html = $this->f->str_replace('{data-pagination-current-signature}', esc_attr($this->getCurrentTableDataSignature($sub)), $html);
648 $html = $this->f->str_replace('{data-pagination-current-orderby}', esc_attr((string)$orderby), $html);
649 $html = $this->f->str_replace('{data-pagination-current-order}', esc_attr((string)$order), $html);
650 $html = $this->f->str_replace('{data-pagination-current-filter}', esc_attr((string)$filter), $html);
651 $html = $this->f->str_replace('{data-pagination-current-paged}', esc_attr((string)$paged), $html);
652 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
653 $scoreRangeForAttr = is_string($rawScoreRange) ? $rawScoreRange : 'all';
654 $html = $this->f->str_replace('{data-pagination-current-score-range}', esc_attr($scoreRangeForAttr), $html);
655 $html = $this->f->str_replace('{data-pagination-current-logsid}', esc_attr((string)$logsid), $html);
656 $autoRefresh = (($sub === 'abj404_redirects' || $sub === 'abj404_captured' || $sub === 'abj404_logs') ? '1' : '0');
657 $html = $this->f->str_replace('{data-pagination-auto-refresh}', esc_attr($autoRefresh), $html);
658 $html = $this->f->str_replace('{data-pagination-refresh-started-text}', esc_attr(__('Refreshing data in background…', '404-solution')), $html);
659 $html = $this->f->str_replace('{data-pagination-refresh-finished-text}', esc_attr(__('Data refreshed', '404-solution')), $html);
660 $html = $this->f->str_replace('{data-pagination-refresh-available-text}', esc_attr(__('Refresh available', '404-solution')), $html);
661 // constants and translations.
662 $html = $this->f->doNormalReplacements($html);
663
664 return $html;
665 }
666
667 /** Output the filters for a tab.
668 * @param string $sub
669 * @param array<string, mixed> $tableOptions
670 * @return string
671 */
672 function getTabFilters($sub, $tableOptions) {
673
674 if (empty($tableOptions)) {
675 $tableOptions = $this->logic->getTableOptions($sub);
676 }
677
678 $html = '';
679 $html .= "<span class=\"clearbothdisplayblock\" style=\"clear: both; display: block;\" ></span>";
680
681 $html .= $this->getSubSubSub($sub);
682
683 $html .= "</span>";
684
685 return $html;
686 }
687
688 /**
689 * @param string $sub
690 * @return string
691 */
692 function getSubSubSub($sub) {
693 global $abj404_redirect_types;
694 global $abj404_captured_types;
695
696 $tableOptions = $this->logic->getTableOptions($sub);
697 $filter = isset($tableOptions['filter']) ? intval(is_scalar($tableOptions['filter']) ? $tableOptions['filter'] : 0) : 0;
698 $orderby = isset($tableOptions['orderby']) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url';
699 $order = isset($tableOptions['order']) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC';
700
701 $url = "?page=" . ABJ404_PP;
702 if ($sub == 'abj404_captured') {
703 $url .= "&subpage=abj404_captured";
704 } else if ($sub == 'abj404_redirects') {
705 $url .= "&subpage=abj404_redirects";
706 } else {
707 $this->logger->errorMessage("Unexpected sub page: " . $sub);
708 }
709
710 $url .= "&orderby=" . sanitize_text_field($orderby);
711 $url .= "&order=" . sanitize_text_field($order);
712
713 if ($sub == 'abj404_redirects') {
714 $types = array(ABJ404_STATUS_MANUAL, ABJ404_STATUS_AUTO, ABJ404_STATUS_REGEX);
715 if (isset($abj404_redirect_types) && is_array($abj404_redirect_types)) {
716 // Some tests/plugins may set this global to a label map; only accept a numeric status list.
717 $candidate = array_values($abj404_redirect_types);
718 $isNumericList = true;
719 foreach ($candidate as $v) {
720 if (!is_int($v) && !(is_string($v) && ctype_digit($v))) {
721 $isNumericList = false;
722 break;
723 }
724 }
725 if ($isNumericList && !empty($candidate)) {
726 $types = array_map('intval', $candidate);
727 }
728 }
729 $counts = $this->viewReadService->getRedirectStatusCounts();
730 } else if ($sub == 'abj404_captured') {
731 $types = array(ABJ404_STATUS_CAPTURED, ABJ404_STATUS_IGNORED, ABJ404_STATUS_LATER);
732 if (isset($abj404_captured_types) && is_array($abj404_captured_types)) {
733 $candidate = array_values($abj404_captured_types);
734 $isNumericList = true;
735 foreach ($candidate as $v) {
736 if (!is_int($v) && !(is_string($v) && ctype_digit($v))) {
737 $isNumericList = false;
738 break;
739 }
740 }
741 if ($isNumericList && !empty($candidate)) {
742 $types = array_map('intval', $candidate);
743 }
744 }
745 $counts = $this->viewReadService->getCapturedStatusCounts();
746 } else {
747 $this->logger->debugMessage("Unexpected sub type for tab filter: " . $sub);
748 $types = array(ABJ404_STATUS_CAPTURED, ABJ404_STATUS_IGNORED, ABJ404_STATUS_LATER);
749 $counts = array();
750 }
751
752 $class = "";
753 if ($filter == 0) {
754 $class = " class=\"current\"";
755 }
756
757 $html = '<ul class="subsubsub" >';
758 if ($sub != 'abj404_captured') {
759 $html .= "<li>";
760 $html .= "<a href=\"" . esc_url($url) . "\"" . $class . ">" . __('All', '404-solution');
761 $html .= " <span class=\"count\">(" . esc_html((string)($counts['all'] ?? 0)) . ")</span>";
762 $html .= "</a>";
763 $html .= "</li>";
764 }
765 foreach ($types as $type) {
766 $thisurl = $url . "&filter=" . $type;
767
768 $class = "";
769 if ($filter == $type) {
770 $class = " class=\"current\"";
771 }
772
773 $recordCount = 0;
774 $title = __('Unknown', '404-solution');
775 if ($type == ABJ404_STATUS_MANUAL) {
776 $title = __('Manual Redirects', '404-solution');
777 $recordCount = intval($counts['manual'] ?? 0) + intval($counts['regex'] ?? 0);
778 } else if ($type == ABJ404_STATUS_AUTO) {
779 $title = __('Automatic Redirects', '404-solution');
780 $recordCount = intval($counts['auto'] ?? 0);
781 } else if ($type == ABJ404_STATUS_CAPTURED) {
782 $title = "Captured URLs";
783 $recordCount = intval($counts['captured'] ?? 0);
784 } else if ($type == ABJ404_STATUS_IGNORED) {
785 $title = "Ignored 404s";
786 $recordCount = intval($counts['ignored'] ?? 0);
787 } else if ($type == ABJ404_STATUS_LATER) {
788 $title = "Organize Later";
789 $recordCount = intval($counts['later'] ?? 0);
790 } else if ($type == ABJ404_STATUS_REGEX) {
791 // don't include a tab here because these are included in the manual redirects.
792 continue;
793 } else {
794 $this->logger->errorMessage("Unrecognized redirect type in View: " . esc_html((string)$type));
795 }
796
797 $html .= "<li>";
798 if ($sub != 'abj404_captured' || $type != ABJ404_STATUS_CAPTURED) {
799 $html .= " | ";
800 }
801 $html .= "<a href=\"" . esc_url($thisurl) . "\"" . $class . ">" . ( $title );
802 $html .= " <span class=\"count\">(" . esc_html((string)$recordCount) . ")</span>";
803 $html .= "</a>";
804 $html .= "</li>";
805 }
806
807
808 $trashurl = $url . "&filter=" . ABJ404_TRASH_FILTER;
809 $class = "";
810 if (($tableOptions['filter'] ?? 0) == ABJ404_TRASH_FILTER) {
811 $class = " class=\"current\"";
812 }
813 $html .= "<li> | ";
814 $html .= "<a href=\"" . esc_url($trashurl) . "\"" . $class . ">" . __('Trash', '404-solution');
815 $html .= " <span class=\"count\">(" . esc_html((string)($counts['trash'] ?? 0)) . ")</span>";
816 $html .= "</a>";
817 $html .= "</li>";
818 $html .= "</ul>";
819 $html .= "\n\n<!-- page-form big outer form could go here -->\n\n";
820
821 $oneBigFormActionURL = $this->getBulkOperationsFormURL($sub, $tableOptions);
822 $html .= '<form method="POST" name="bulk-operations-form" action="' . $oneBigFormActionURL . '">';
823
824
825 return $html;
826 }
827
828
829 }
830