PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.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 / view / View_CapturedURLsTable.php

View_CapturedURLsTable.php in 404 Solution 4.3.0, at includes/view/View_CapturedURLsTable.php

446 lines 21.8 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 * Captured 404 URLs admin page renderer. Owns the page wrapper, the table
9 * shell, and the per-row action buttons for the "Captured 404 URLs" admin
10 * page (subpage abj404_captured).
11 *
12 * Outside callers (via the View facade __call dispatch):
13 * - includes/ajax/ViewUpdater.php (pagination AJAX -> getCapturedURLSPageTable)
14 * - PluginLogic admin page entry points (echoAdminCapturedURLsPage)
15 */
16 class ABJ_404_Solution_View_CapturedURLsTable extends ABJ_404_Solution_ViewComponent {
17
18 /** @var ABJ_404_Solution_CapturedTableHeaderRenderer|null */
19 private $capturedTableHeaderRenderer = null;
20
21 private function tpl(string $name): string {
22 $raw = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/' . $name, false);
23 return rtrim((string)$raw, "\n");
24 }
25
26 /** @param array<string,string> $vars */
27 private function fillTpl(string $name, array $vars): string {
28 return (string)$this->f->str_replace(array_keys($vars), array_values($vars), $this->tpl($name));
29 }
30
31 /**
32 * Build an action link with SVG icon.
33 *
34 * @param array<string,string> $vars
35 */
36 private function buildActionLink(string $tplName, array $vars): string {
37 $tpl = $this->tpl($tplName);
38 foreach ($vars as $k => $v) {
39 $tpl = $this->f->str_replace('{' . $k . '}', $v, $tpl);
40 }
41 return $tpl;
42 }
43
44 /** @return void */
45 public function echoAdminCapturedURLsPage() {
46 $sub = 'abj404_captured';
47
48 $tableOptions = $this->logic->settingsUpdate()->getTableOptions($sub);
49
50 $isSimpleMode = abj_service('settings_mode_preference')->getMode() === 'simple';
51
52 // Filter row (native WP subsubsub). Counts are placeholders, populated via AJAX.
53 if ($isSimpleMode) {
54 $items = array(
55 array(ABJ404_STATUS_CAPTURED, __('Needs Review', '404-solution')),
56 array(ABJ404_HANDLED_FILTER, __('Handled', '404-solution')),
57 );
58 } else {
59 $items = array(
60 array(0, __('All', '404-solution')),
61 array(ABJ404_STATUS_CAPTURED, __('Captured', '404-solution')),
62 array(ABJ404_STATUS_IGNORED, __('Ignored', '404-solution')),
63 array(ABJ404_STATUS_LATER, __('Later', '404-solution')),
64 array(ABJ404_TRASH_FILTER, __('Trash', '404-solution')),
65 );
66 }
67 $subsubsubHtml = $this->listTableChrome->buildSubsubsubFilters('abj404_captured', $items, $tableOptions);
68
69 // Filter bar with server-side search
70 $filterText = is_string($tableOptions['filterText'] ?? '') ? (string)($tableOptions['filterText'] ?? '') : '';
71 $perPage = isset($tableOptions['perpage']) ? (int)$tableOptions['perpage'] : 25;
72
73 $paginationNonce = wp_create_nonce('abj404_updatePaginationLink');
74 $inflightNonce = wp_create_nonce('abj404_fetchInflightStage');
75 $lazyBackfillNonce = wp_create_nonce('abj404_runLazyBackfill');
76 $autoRefresh = '1';
77 $rawFilter = $tableOptions['filter'] ?? 0;
78 $currentFilter = is_scalar($rawFilter) ? $rawFilter : 0;
79 $rawOrderBy = $tableOptions['orderby'] ?? '';
80 $currentOrderBy = is_string($rawOrderBy) ? $rawOrderBy : 'url';
81 $rawOrder = $tableOptions['order'] ?? '';
82 $currentOrder = is_string($rawOrder) ? $rawOrder : 'ASC';
83 $rawPaged = $tableOptions['paged'] ?? 1;
84 $currentPaged = is_scalar($rawPaged) ? intval($rawPaged) : 1;
85 if ($currentPaged < 1) {
86 $currentPaged = 1;
87 }
88 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
89 $currentScoreRange = is_string($rawScoreRange) ? $rawScoreRange : 'all';
90
91 // Subtitle
92 $subtitleHtml = '';
93 if ($isSimpleMode) {
94 $subtitleHtml = $this->f->str_replace(
95 '{text}',
96 esc_html__('Broken links visitors tried to reach. Create Redirect for important ones, Dismiss the rest.', '404-solution'),
97 $this->tpl('viewRedirectsTableSubtitle.html')
98 ) . "\n";
99 }
100
101 $emptyTrashHtml = '';
102 if ($currentFilter == ABJ404_TRASH_FILTER) {
103 $eturl = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_captured&filter=" . ABJ404_TRASH_FILTER, 'abj404_bulkProcess');
104 $emptyTrashHtml = $this->fillTpl('viewRedirectsTableEmptyTrashButton.html', array(
105 '{href}' => esc_url($eturl . '&abj404action=emptyCapturedTrash'),
106 '{confirm_js}' => esc_js(__('Are you sure you want to permanently delete all items in trash?', '404-solution')),
107 '{label}' => esc_html__('Empty Trash', '404-solution'),
108 )) . "\n";
109 }
110
111 $bulkButtons = '';
112 if ($isSimpleMode) {
113 $bulkButtons = $this->listTableChrome->buildBulkButton('bulkignore', __('Dismiss', '404-solution'))
114 . $this->listTableChrome->buildBulkButton('editRedirect', __('Create Redirect', '404-solution'));
115 } else {
116 if ($currentFilter != ABJ404_STATUS_CAPTURED) { $bulkButtons .= $this->listTableChrome->buildBulkButton('bulkcaptured', __('Mark Captured', '404-solution')); }
117 if ($currentFilter != ABJ404_STATUS_IGNORED) { $bulkButtons .= $this->listTableChrome->buildBulkButton('bulkignore', __('Mark Ignored', '404-solution')); }
118 if ($currentFilter != ABJ404_STATUS_LATER) { $bulkButtons .= $this->listTableChrome->buildBulkButton('bulklater', __('Organize Later', '404-solution')); }
119 if ($currentFilter != ABJ404_TRASH_FILTER) { $bulkButtons .= $this->listTableChrome->buildBulkButton('bulktrash', __('Move to Trash', '404-solution')); }
120 $bulkButtons .= $this->listTableChrome->buildBulkButton('editRedirect', __('Create Redirect', '404-solution'));
121 }
122
123 // Bulk form action URL + nonce field
124 $formAction = $this->listTableChrome->getBulkOperationsFormURL($sub, $tableOptions);
125 ob_start();
126 wp_nonce_field('abj404_bulkProcess');
127 $nonceField = (string)ob_get_clean();
128
129 $warmup = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/tableWarmupPlaceholder.html");
130
131 $refresh = $this->listTableChrome->paginationRefreshStrings();
132
133 echo $this->fillTpl('viewRedirectsTableCapturedPageWrapper.html', array(
134 '{captured_title}' => __('Captured 404 URLs', '404-solution'),
135 '{subtitle}' => $subtitleHtml,
136 '{subsubsub}' => $subsubsubHtml,
137 '{data-pagination-ajax-url}' => esc_attr(admin_url('admin-ajax.php')),
138 '{data-lazy-backfill-ajax-url}' => esc_attr(admin_url('admin-ajax.php')),
139 '{data-lazy-backfill-nonce}' => esc_attr($lazyBackfillNonce),
140 '{data-pagination-ajax-subpage}' => esc_attr($sub),
141 '{data-pagination-ajax-nonce}' => esc_attr($paginationNonce),
142 '{data-pagination-inflight-nonce}' => esc_attr($inflightNonce),
143 '{data-pagination-current-orderby}' => esc_attr($currentOrderBy),
144 '{data-pagination-current-order}' => esc_attr($currentOrder),
145 '{data-pagination-current-filter}' => esc_attr((string)$currentFilter),
146 '{data-pagination-current-paged}' => esc_attr((string)$currentPaged),
147 '{data-pagination-current-score-range}' => esc_attr($currentScoreRange),
148 '{data-pagination-auto-refresh}' => esc_attr($autoRefresh),
149 '{data-pagination-refresh-available-text}' => esc_attr($refresh['available']),
150 '{search_placeholder}' => esc_attr__('Type to filter URLs... (press Enter)', '404-solution'),
151 '{filter_text}' => esc_attr($filterText),
152 '{rows_per_page_label}' => esc_html__('Rows per page:', '404-solution'),
153 '{perpage_options}' => $this->listTableChrome->buildPerpageOptions($perPage),
154 '{empty_trash_button}' => $emptyTrashHtml,
155 '{selected_label}' => __('selected', '404-solution'),
156 '{bulk_buttons}' => $bulkButtons,
157 '{clear_label}' => __('Clear', '404-solution'),
158 '{form_action}' => esc_url($formAction),
159 '{nonce_field}' => $nonceField,
160 '{warmup_placeholder}' => $warmup,
161 '{loading_badge_text}' => esc_html__('Loading...', '404-solution'),
162 ));
163 }
164
165 /**
166 * Build the per-row action buttons (edit/logs/trash/delete/ignore/later/
167 * dismiss/create-redirect) for the Captured URLs table. Supports both
168 * Simple and Advanced settings modes and the Trash filter variant.
169 *
170 * @param array<string, mixed> $row
171 * @param array<string, mixed> $tableOptions
172 * @param array<string, string> $links Pre-built action URLs / titles.
173 * @return array{edit:string,logs:string,trash:string,delete:string,ignore:string,later:string}
174 */
175 private function buildCapturedRowActionButtons(array $row, array $tableOptions, array $links): array {
176 $svgEdit = $this->tpl('viewRedirectsTableSvgEdit.html');
177 $svgDismiss = $this->tpl('viewRedirectsTableSvgDismiss.html');
178 $svgLogs = $this->tpl('viewRedirectsTableSvgLogs.html');
179 $svgTrash = $this->tpl('viewRedirectsTableSvgTrash.html');
180 $svgRestore = $this->tpl('viewRedirectsTableSvgRestore.html');
181 $svgX = $this->tpl('viewRedirectsTableSvgX.html');
182 $svgClock = $this->tpl('viewRedirectsTableSvgClock.html');
183
184 $edit = $logs = $trash = $delete = $ignore = $later = '';
185
186 $currentFilter = $tableOptions['filter'] ?? 0;
187 $isSimpleModeRow = abj_service('settings_mode_preference')->getMode() === 'simple';
188
189 if ($isSimpleModeRow) {
190 $edit = $this->buildActionLink('viewRedirectsTableActionLink.html', array(
191 'href' => esc_url($links['editlink']), 'class' => 'abj404-action-link',
192 'title' => esc_attr__('Create Redirect', '404-solution'),
193 'svg_path' => $svgEdit, 'label' => esc_html__('Create Redirect', '404-solution'),
194 ));
195 if ($row['status'] != ABJ404_STATUS_IGNORED) {
196 $ignore = $this->buildActionLink('viewRedirectsTableActionLinkSeparated.html', array(
197 'href' => esc_url($links['ignorelink']), 'class' => 'abj404-action-link',
198 'title' => esc_attr__('Dismiss', '404-solution'),
199 'svg_path' => $svgDismiss, 'label' => esc_html__('Dismiss', '404-solution'),
200 ));
201 }
202 return ['edit'=>$edit,'logs'=>$logs,'trash'=>$trash,'delete'=>$delete,'ignore'=>$ignore,'later'=>$later];
203 }
204
205 if ($currentFilter != ABJ404_TRASH_FILTER) {
206 $edit = $this->buildActionLink('viewRedirectsTableActionLink.html', array(
207 'href' => esc_url($links['editlink']), 'class' => 'abj404-action-link',
208 'title' => esc_attr__('Edit', '404-solution'),
209 'svg_path' => $svgEdit, 'label' => esc_html__('Edit', '404-solution'),
210 ));
211 }
212 if (($row['logsid'] ?? 0) > 0) {
213 $logs = $this->buildActionLink('viewRedirectsTableActionLink.html', array(
214 'href' => esc_url($links['logslink']), 'class' => 'abj404-action-link',
215 'title' => esc_attr__('View Logs', '404-solution'),
216 'svg_path' => $svgLogs, 'label' => esc_html__('Logs', '404-solution'),
217 ));
218 }
219 if ($currentFilter != ABJ404_TRASH_FILTER) {
220 $trash = $this->buildActionLink('viewRedirectsTableActionLink.html', array(
221 'href' => esc_url($links['trashlink']), 'class' => 'abj404-action-link danger',
222 'title' => esc_attr($links['trashtitle']),
223 'svg_path' => $svgTrash, 'label' => esc_html__('Trash', '404-solution'),
224 ));
225 }
226
227 if ($currentFilter == ABJ404_TRASH_FILTER) {
228 $trash = $this->buildActionLink('viewRedirectsTableActionLink.html', array(
229 'href' => esc_url($links['trashlink']), 'class' => 'abj404-action-link',
230 'title' => esc_attr__('Restore', '404-solution'),
231 'svg_path' => $svgRestore, 'label' => esc_html__('Restore', '404-solution'),
232 ));
233 $delete = $this->buildActionLink('viewRedirectsTableActionLinkConfirm.html', array(
234 'href' => esc_url($links['deletelink']), 'class' => 'abj404-action-link danger',
235 'title' => esc_attr__('Delete Permanently', '404-solution'),
236 'confirm_js' => esc_js(__('Are you sure you want to permanently delete this item?', '404-solution')),
237 'svg_path' => $svgX, 'label' => esc_html__('Delete', '404-solution'),
238 ));
239 } else {
240 if ($row['status'] != ABJ404_STATUS_IGNORED) {
241 $ignore = $this->buildActionLink('viewRedirectsTableActionLinkSeparated.html', array(
242 'href' => esc_url($links['ignorelink']), 'class' => 'abj404-action-link',
243 'title' => esc_attr($links['ignoretitle']),
244 'svg_path' => $svgDismiss, 'label' => esc_html__('Ignore', '404-solution'),
245 ));
246 }
247 if ($row['status'] != ABJ404_STATUS_LATER) {
248 $later = $this->buildActionLink('viewRedirectsTableActionLinkSeparated.html', array(
249 'href' => esc_url($links['laterlink']), 'class' => 'abj404-action-link',
250 'title' => esc_attr($links['latertitle']),
251 'svg_path' => $svgClock, 'label' => esc_html__('Later', '404-solution'),
252 ));
253 }
254 }
255 return ['edit'=>$edit,'logs'=>$logs,'trash'=>$trash,'delete'=>$delete,'ignore'=>$ignore,'later'=>$later];
256 }
257
258 public function getCapturedURLSPageTable(string $sub): string {
259 $tableOptions = $this->logic->settingsUpdate()->getTableOptions($sub);
260 $rows = $this->viewReadService->getRedirectsForView($sub, $tableOptions);
261 /** @var array<int, array<string, mixed>> $typedRows */
262 $typedRows = array_values(array_filter($rows, 'is_array'));
263 $this->shared->rememberTableDataSignature($sub, $typedRows);
264
265 return $this->f->str_replace(
266 array('{select_all_label}', '{header_cells}', '{body_rows}'),
267 array(
268 esc_attr__('Select all', '404-solution'),
269 $this->buildCapturedHeaderCells($tableOptions),
270 $this->buildCapturedBodyRows($sub, $tableOptions, $typedRows),
271 ),
272 $this->tpl('viewRedirectsTableCapturedTableShell.html')
273 );
274 }
275
276 /** @param array<string, mixed> $tableOptions */
277 private function buildCapturedHeaderCells(array $tableOptions): string {
278 return $this->capturedTableHeaderRenderer()->render($tableOptions);
279 }
280
281 private function capturedTableHeaderRenderer(): ABJ_404_Solution_CapturedTableHeaderRenderer {
282 if ($this->capturedTableHeaderRenderer === null) {
283 $this->capturedTableHeaderRenderer = new ABJ_404_Solution_CapturedTableHeaderRenderer(
284 $this->f,
285 $this->shared,
286 $this->viewReadService
287 );
288 }
289 return $this->capturedTableHeaderRenderer;
290 }
291
292 /**
293 * @param array<string, mixed> $tableOptions
294 * @param array<int, array<string, mixed>> $rows
295 */
296 private function buildCapturedBodyRows(string $sub, array $tableOptions, array $rows): string {
297 if (count($rows) === 0) {
298 // Same i455 bug class as the redirects table: a pending/errored/
299 // stale-empty staged read must not be shown as "No records" while
300 // the live source count says captured rows exist.
301 $emptyMessage = $this->viewReadService->lastRedirectsViewReadWasIncomplete()
302 ? __('Preparing the captured 404s table. The list is still loading and will appear in a moment.', '404-solution')
303 : __('No Captured 404 Records To Display', '404-solution');
304 return $this->f->str_replace(
305 '{message}',
306 $emptyMessage,
307 $this->tpl('viewRedirectsTableCapturedEmptyRow.html')
308 ) . "\n";
309 }
310
311 $bodyRows = '';
312 foreach ($rows as $row) {
313 $bodyRows .= $this->capturedBodyRow($sub, $tableOptions, $row);
314 }
315 return $bodyRows;
316 }
317
318 /**
319 * @param array<string, mixed> $tableOptions
320 * @param array<string, mixed> $row
321 */
322 private function capturedBodyRow(string $sub, array $tableOptions, array $row): string {
323 $hits = is_scalar($row['logshits'] ?? 0) ? (int)($row['logshits'] ?? 0) : 0;
324 $lastUsed = $this->capturedLastUsedPresentation($row);
325 $status = $this->capturedStatusPresentation($row);
326 $btns = $this->capturedActionButtons($sub, $tableOptions, $row);
327 $vars = $this->capturedRowTemplateVars($row, $hits, $lastUsed, $status, $btns);
328
329 $tempHtml = $this->f->str_replace(
330 array_keys($vars),
331 array_values($vars),
332 ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/tableRowCapturedURLs.html")
333 );
334 return $this->f->doNormalReplacements($tempHtml);
335 }
336
337 /**
338 * @param array<string, mixed> $row
339 * @return array{date: string, class: string}
340 */
341 private function capturedLastUsedPresentation(array $row): array {
342 $last_used = is_scalar($row['last_used'] ?? 0) ? (int)($row['last_used'] ?? 0) : 0;
343 if ($last_used != 0) {
344 return array(
345 'date' => (string)wp_date("Y/m/d h:i:s A", abs($last_used)),
346 'class' => '',
347 );
348 }
349
350 return array(
351 'date' => __('Never', '404-solution'),
352 'class' => 'abj404-never-used',
353 );
354 }
355
356 /**
357 * @param array<string, mixed> $row
358 * @return array{class: string, text: string, title: string}
359 */
360 private function capturedStatusPresentation(array $row): array {
361 if ($row['status'] == ABJ404_STATUS_IGNORED) {
362 return array(
363 'class' => 'abj404-badge-ignored',
364 'text' => __('Ignored', '404-solution'),
365 'title' => __('Ignored URL - will not be suggested', '404-solution'),
366 );
367 }
368 if ($row['status'] == ABJ404_STATUS_LATER) {
369 return array(
370 'class' => 'abj404-badge-later',
371 'text' => __('Later', '404-solution'),
372 'title' => __('Organize Later', '404-solution'),
373 );
374 }
375
376 return array(
377 'class' => 'abj404-badge-captured',
378 'text' => __('Captured', '404-solution'),
379 'title' => __('Captured 404 URL', '404-solution'),
380 );
381 }
382
383 /**
384 * @param array<string, mixed> $tableOptions
385 * @param array<string, mixed> $row
386 * @return array{edit: string, logs: string, trash: string, delete: string, ignore: string, later: string}
387 */
388 private function capturedActionButtons(string $sub, array $tableOptions, array $row): array {
389 $links = $this->shared->buildTableActionLinks($row, $sub, $tableOptions, true);
390
391 return $this->buildCapturedRowActionButtons($row, $tableOptions, array(
392 'editlink' => $this->linkValue($links, 'editlink'),
393 'logslink' => $this->linkValue($links, 'logslink'),
394 'trashlink' => $this->linkValue($links, 'trashlink'),
395 'trashtitle' => $this->linkValue($links, 'trashtitle'),
396 'deletelink' => $this->linkValue($links, 'deletelink'),
397 'ignorelink' => $this->linkValue($links, 'ignorelink'),
398 'ignoretitle' => $this->linkValue($links, 'ignoretitle'),
399 'laterlink' => $this->linkValue($links, 'laterlink'),
400 'latertitle' => $this->linkValue($links, 'latertitle'),
401 ));
402 }
403
404 /** @param array<string, mixed> $links */
405 private function linkValue(array $links, string $key): string {
406 return is_scalar($links[$key] ?? '') ? (string)($links[$key] ?? '') : '';
407 }
408
409 /**
410 * @param array<string, mixed> $row
411 * @param array{date: string, class: string} $lastUsed
412 * @param array{class: string, text: string, title: string} $status
413 * @param array{edit: string, logs: string, trash: string, delete: string, ignore: string, later: string} $btns
414 * @return array<string, string>
415 */
416 private function capturedRowTemplateVars(array $row, int $hits, array $lastUsed, array $status, array $btns): array {
417 $capturedRowUrl = is_string($row['url'] ?? '') ? (string)($row['url'] ?? '') : '';
418 $capturedRowId = is_scalar($row['id'] ?? '') ? (string)($row['id'] ?? '') : '';
419 $capturedEngine = is_string($row['engine'] ?? '') ? trim((string)($row['engine'] ?? '')) : '';
420 $capturedEngineHTML = ($capturedEngine !== '') ? '<br><span class="abj404-engine-label">' . esc_html($capturedEngine) . '</span>' : '';
421 $createdTimestamp = is_scalar($row['timestamp'] ?? 0) ? intval($row['timestamp'] ?? 0) : 0;
422
423 $vars = array(
424 '{rowid}' => $capturedRowId,
425 '{rowClass}' => '',
426 '{visitorURL}' => esc_url(home_url($capturedRowUrl)),
427 '{url}' => esc_html($capturedRowUrl),
428 '{statusBadgeClass}' => $status['class'],
429 '{statusTitle}' => esc_attr($status['title']),
430 '{status}' => $status['text'],
431 '{engineHTML}' => $capturedEngineHTML,
432 '{hits}' => esc_html((string)$hits),
433 '{created_date}' => esc_html((string)wp_date("Y/m/d h:i:s A", abs($createdTimestamp))),
434 '{last_used_date}' => esc_html($lastUsed['date']),
435 '{lastUsedClass}' => $lastUsed['class'],
436 '{editBtnHTML}' => $btns['edit'],
437 '{logsBtnHTML}' => $btns['logs'],
438 '{trashBtnHTML}' => $btns['trash'],
439 '{deleteBtnHTML}' => $btns['delete'],
440 '{ignoreBtnHTML}' => $btns['ignore'],
441 '{laterBtnHTML}' => $btns['later'],
442 );
443 return $vars;
444 }
445 }
446