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_RedirectsTable.php

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

1,405 lines 77.0 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 * Redirects and captured URLs table/list pages.
9 */
10 trait ViewTrait_RedirectsTable {
11
12 /** @return void */
13 function echoAdminCapturedURLsPage() {
14 $sub = 'abj404_captured';
15
16 $tableOptions = $this->logic->getTableOptions($sub);
17
18 // Modern page wrapper
19 echo '<div class="abj404-table-page">';
20
21 // Header with page title
22 $isSimpleMode = $this->logic->getSettingsMode() === 'simple';
23 echo '<div class="abj404-table-header">';
24 echo '<div class="abj404-table-header-text">';
25 echo '<h2>' . __('Captured 404 URLs', '404-solution') . '</h2>';
26 if ($isSimpleMode) {
27 echo '<p class="abj404-guidance-subtitle">'
28 . esc_html__('Broken links visitors tried to reach. Create Redirect for important ones, Dismiss the rest.', '404-solution')
29 . '</p>';
30 }
31 echo '</div>';
32 echo '</div>';
33
34 // Content tabs — counts are placeholders, populated via AJAX
35 echo '<div class="abj404-content-tabs" data-tab-counts-placeholder="1">';
36 if ($isSimpleMode) {
37 // Simple mode: two tabs — "Needs Review" and "Handled"
38 $this->echoContentTab('abj404_captured', ABJ404_STATUS_CAPTURED, __('Needs Review', '404-solution'), '…', $tableOptions);
39 $this->echoContentTab('abj404_captured', ABJ404_HANDLED_FILTER, __('Handled', '404-solution'), '…', $tableOptions);
40 } else {
41 // Advanced mode: full 5-tab layout
42 $this->echoContentTab('abj404_captured', 0, __('All', '404-solution'), '…', $tableOptions);
43 $this->echoContentTab('abj404_captured', ABJ404_STATUS_CAPTURED, __('Captured', '404-solution'), '…', $tableOptions);
44 $this->echoContentTab('abj404_captured', ABJ404_STATUS_IGNORED, __('Ignored', '404-solution'), '…', $tableOptions);
45 $this->echoContentTab('abj404_captured', ABJ404_STATUS_LATER, __('Later', '404-solution'), '…', $tableOptions);
46 $this->echoContentTab('abj404_captured', ABJ404_TRASH_FILTER, __('Trash', '404-solution'), '…', $tableOptions);
47 }
48 echo '</div>';
49
50 // Filter bar with server-side search
51 $filterText = is_string($tableOptions['filterText'] ?? '') ? (string)($tableOptions['filterText'] ?? '') : '';
52 $perPage = isset($tableOptions['perpage']) ? $tableOptions['perpage'] : 25;
53
54 $paginationNonce = wp_create_nonce('abj404_updatePaginationLink');
55 $inflightNonce = wp_create_nonce('abj404_fetchInflightStage');
56 $autoRefresh = '1'; // $sub is always 'abj404_captured' here
57 $rawFilter = $tableOptions['filter'] ?? 0;
58 $currentFilter = is_scalar($rawFilter) ? $rawFilter : 0;
59 $rawOrderBy = $tableOptions['orderby'] ?? '';
60 $currentOrderBy = is_string($rawOrderBy) ? $rawOrderBy : 'url';
61 $rawOrder = $tableOptions['order'] ?? '';
62 $currentOrder = is_string($rawOrder) ? $rawOrder : 'ASC';
63 $rawPaged = $tableOptions['paged'] ?? 1;
64 $currentPaged = is_scalar($rawPaged) ? intval($rawPaged) : 1;
65 if ($currentPaged < 1) {
66 $currentPaged = 1;
67 }
68 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
69 $currentScoreRange = is_string($rawScoreRange) ? $rawScoreRange : 'all';
70
71 echo '<div class="abj404-filter-bar tablenav"'
72 . ' data-pagination-ajax-url="' . esc_attr(admin_url('admin-ajax.php')) . '"'
73 . ' data-pagination-ajax-action="ajaxUpdatePaginationLinks"'
74 . ' data-pagination-ajax-subpage="' . esc_attr($sub) . '"'
75 . ' data-pagination-ajax-nonce="' . esc_attr($paginationNonce) . '"'
76 . ' data-pagination-inflight-nonce="' . esc_attr($inflightNonce) . '"'
77 . ' data-pagination-current-signature=""'
78 . ' data-pagination-current-orderby="' . esc_attr($currentOrderBy) . '"'
79 . ' data-pagination-current-order="' . esc_attr($currentOrder) . '"'
80 . ' data-pagination-current-filter="' . esc_attr((string)$currentFilter) . '"'
81 . ' data-pagination-current-paged="' . esc_attr((string)$currentPaged) . '"'
82 . ' data-pagination-current-score-range="' . esc_attr($currentScoreRange) . '"'
83 . ' data-pagination-current-logsid=""'
84 . ' data-pagination-initial-load="1"'
85 . ' data-pagination-auto-refresh="' . esc_attr($autoRefresh) . '"'
86 . ' data-pagination-refresh-started-text="' . esc_attr(__('Refreshing data in background…', '404-solution')) . '"'
87 . ' data-pagination-refresh-finished-text="' . esc_attr(__('Data refreshed', '404-solution')) . '"'
88 . ' data-pagination-refresh-available-text="' . esc_attr(__('Refresh available', '404-solution')) . '">';
89 echo '<div class="abj404-search-box">';
90 echo '<input type="search" name="searchFilter" placeholder="' . esc_attr__('Type to filter URLs... (press Enter)', '404-solution') . '" value="' . esc_attr($filterText) . '" data-lpignore="true">';
91 echo '</div>';
92 echo '<div class="abj404-rows-per-page">';
93 echo '<span>' . esc_html__('Rows per page:', '404-solution') . '</span>';
94 echo '<select class="abj404-filter-select perpage" name="perpage" onchange="paginationLinksChange(this);">';
95 foreach ([10, 25, 50, 100, 200] as $opt) {
96 $selected = ($perPage == $opt) ? ' selected' : '';
97 echo '<option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
98 }
99 echo '</select>';
100 echo '</div>';
101
102 // Empty trash button
103 if ($currentFilter == ABJ404_TRASH_FILTER) {
104 $eturl = "?page=" . ABJ404_PP . "&subpage=abj404_captured&filter=" . ABJ404_TRASH_FILTER;
105 $eturl = wp_nonce_url($eturl, 'abj404_bulkProcess');
106 echo '<a href="' . esc_url($eturl) . '&abj404action=emptyCapturedTrash" class="button abj404-empty-trash-btn" onclick="return confirm(\'' . esc_js(__('Are you sure you want to permanently delete all items in trash?', '404-solution')) . '\');">';
107 echo esc_html__('Empty Trash', '404-solution');
108 echo '</a>';
109 }
110 echo '<span class="abj404-refresh-status" aria-live="polite"></span>';
111 echo '</div>';
112
113 // Bulk actions bar
114 $url = $this->getBulkOperationsFormURL($sub, $tableOptions);
115 echo '<div class="abj404-bulk-actions">';
116 echo '<div class="abj404-selection-info"><strong>0</strong> ' . __('selected', '404-solution') . '</div>';
117 echo '<div class="abj404-bulk-buttons">';
118
119 // Bulk action buttons based on current filter and mode
120 if ($isSimpleMode) {
121 // Simple mode: just "Dismiss" and "Create Redirect"
122 echo '<button type="submit" name="abj404action" value="bulkignore" form="bulk-action-form" class="button">' . __('Dismiss', '404-solution') . '</button>';
123 echo '<button type="submit" name="abj404action" value="editRedirect" form="bulk-action-form" class="button">' . __('Create Redirect', '404-solution') . '</button>';
124 } else {
125 // Advanced mode: full set of bulk actions
126 if ($currentFilter != ABJ404_STATUS_CAPTURED) {
127 echo '<button type="submit" name="abj404action" value="bulkcaptured" form="bulk-action-form" class="button">' . __('Mark Captured', '404-solution') . '</button>';
128 }
129 if ($currentFilter != ABJ404_STATUS_IGNORED) {
130 echo '<button type="submit" name="abj404action" value="bulkignore" form="bulk-action-form" class="button">' . __('Mark Ignored', '404-solution') . '</button>';
131 }
132 if ($currentFilter != ABJ404_STATUS_LATER) {
133 echo '<button type="submit" name="abj404action" value="bulklater" form="bulk-action-form" class="button">' . __('Organize Later', '404-solution') . '</button>';
134 }
135 if ($currentFilter != ABJ404_TRASH_FILTER) {
136 echo '<button type="submit" name="abj404action" value="bulktrash" form="bulk-action-form" class="button">' . __('Move to Trash', '404-solution') . '</button>';
137 }
138 echo '<button type="submit" name="abj404action" value="editRedirect" form="bulk-action-form" class="button">' . __('Create Redirect', '404-solution') . '</button>';
139 }
140
141 echo '</div>';
142 echo '<button type="button" class="abj404-clear-selection" onclick="abj404ClearSelection()">' . __('Clear', '404-solution') . '</button>';
143 echo '</div>';
144
145 // Hidden form for bulk actions
146 echo '<form id="bulk-action-form" method="POST" action="' . esc_url($url) . '">';
147 wp_nonce_field('abj404_bulkProcess');
148
149 // Table + pagination placeholders. Full data is loaded via AJAX
150 // so initial page render is not blocked on heavy queries.
151 echo '<table class="abj404-table" data-table-awaiting-load="1">';
152 echo '<thead><tr><th>' . esc_html__('Loading captured URLs…', '404-solution') . '</th></tr></thead>';
153 echo '<tbody><tr><td class="abj404-empty-message">' . esc_html__('Loading captured URLs…', '404-solution') . '</td></tr></tbody>';
154 echo '</table>';
155
156 // Pagination placeholder (bottom only, matching original layout).
157 echo '<div class="abj404-pagination tablenav abj404-pagination-right">';
158 echo '<span class="abj404-refresh-status" aria-live="polite">' . esc_html__('Loading…', '404-solution') . '</span>';
159 echo '</div>';
160
161 echo '</form>';
162 echo '</div><!-- .abj404-table-page -->';
163 }
164
165 /**
166 * @param string $sub
167 * @return string
168 */
169 function getCapturedURLSPageTable($sub) {
170
171 $tableOptions = $this->logic->getTableOptions($sub);
172
173 // Build column headers with sorting
174 $hitsTooltip = $this->getHitsColumnTooltip($tableOptions);
175 $columns = array(
176 'url' => array('title' => __('URL', '404-solution'), 'orderby' => 'url'),
177 'status' => array('title' => __('Status', '404-solution'), 'orderby' => 'status'),
178 'hits' => array('title' => __('Hits', '404-solution'), 'orderby' => 'logshits', 'title_attr_html' => $hitsTooltip),
179 'timestamp' => array('title' => __('Created', '404-solution'), 'orderby' => 'timestamp', 'class' => 'hide-on-tablet'),
180 'last_used' => array('title' => __('Last Used', '404-solution'), 'orderby' => 'last_used', 'title_attr_html' => $hitsTooltip),
181 );
182
183 $html = '<table class="abj404-table">';
184 $html .= '<thead><tr>';
185 $html .= '<th scope="col"><input type="checkbox" id="select-all-captured" aria-label="' . esc_attr__('Select all', '404-solution') . '"></th>';
186
187 // Generate sortable column headers
188 foreach ($columns as $key => $col) {
189 $sortUrl = "?page=" . ABJ404_PP . "&subpage=abj404_captured&filter=" . ($tableOptions['filter'] ?? 0);
190 $sortUrl .= "&orderby=" . $col['orderby'];
191 $sortState = $this->getHeaderSortState($tableOptions, (string)$col['orderby'], false);
192 $newOrder = $sortState['nextOrder'];
193 $sortUrl .= "&order=" . $newOrder;
194
195 $extraClass = isset($col['class']) ? ' ' . esc_attr($col['class']) : '';
196 $sortClass = trim($sortState['thClass'] . $extraClass);
197 $sortIndicator = $sortState['indicator'];
198
199 $classAttr = $sortClass ? ' class="' . trim($sortClass) . '"' : '';
200
201 // Build tooltip HTML if present
202 $tooltipHtml = '';
203 if (isset($col['title_attr_html']) && !empty($col['title_attr_html'])) {
204 // Raw HTML (already escaped where needed)
205 $tooltipHtml = '<span class="abj404-header-tooltip lefty-tooltip" aria-label="' . esc_attr__('More info', '404-solution') . '">' .
206 '<span class="abj404-header-tooltip-icon" aria-hidden="true">?</span>' .
207 '<span class="lefty-tooltiptext">' . $col['title_attr_html'] . '</span>' .
208 '</span>';
209 }
210
211 $html .= '<th scope="col"' . $classAttr . '><a href="' . esc_url($sortUrl) . '">' . esc_html($col['title']) . $sortIndicator . '</a>' . $tooltipHtml . '</th>';
212 }
213
214 $html .= '</tr></thead>';
215 $html .= '<tbody id="the-list">';
216
217 $rows = $this->viewReadService->getRedirectsForView($sub, $tableOptions);
218 /** @var array<int, array<string, mixed>> $typedRows */
219 $typedRows = array_values(array_filter($rows, 'is_array'));
220 $this->rememberTableDataSignature($sub, $typedRows);
221 $displayed = 0;
222
223 foreach ($typedRows as $row) {
224 $displayed++;
225
226 $hits = is_scalar($row['logshits'] ?? 0) ? (int)($row['logshits'] ?? 0) : 0;
227
228 $last_used = is_scalar($row['last_used'] ?? 0) ? (int)($row['last_used'] ?? 0) : 0;
229 $lastUsedClass = '';
230 if ($last_used != 0) {
231 $last = (string)wp_date("Y/m/d h:i:s A", abs($last_used));
232 } else {
233 $last = __('Never', '404-solution');
234 $lastUsedClass = 'abj404-never-used';
235 }
236
237 // Build action links using helper method
238 /** @var array<string, mixed> $row */
239 $links = $this->buildTableActionLinks($row, $sub, $tableOptions, true);
240 $editlink = '';
241 $logslink = '';
242 $trashlink = '';
243 $trashtitle = '';
244 $deletelink = '';
245 $ignorelink = '';
246 $ignoretitle = '';
247 $laterlink = '';
248 $latertitle = '';
249 $ajaxTrashLink = '';
250 extract($links);
251
252 // Determine status badge
253 $statusBadgeClass = 'abj404-badge-captured';
254 $statusText = __('Captured', '404-solution');
255 $statusTitle = __('Captured 404 URL', '404-solution');
256
257 if ($row['status'] == ABJ404_STATUS_IGNORED) {
258 $statusBadgeClass = 'abj404-badge-ignored';
259 $statusText = __('Ignored', '404-solution');
260 $statusTitle = __('Ignored URL - will not be suggested', '404-solution');
261 } else if ($row['status'] == ABJ404_STATUS_LATER) {
262 $statusBadgeClass = 'abj404-badge-later';
263 $statusText = __('Later', '404-solution');
264 $statusTitle = __('Organize Later', '404-solution');
265 }
266
267 // Build row action buttons
268 $editBtnHTML = '';
269 $logsBtnHTML = '';
270 $trashBtnHTML = '';
271 $deleteBtnHTML = '';
272 $ignoreBtnHTML = '';
273 $laterBtnHTML = '';
274
275 $currentFilter = $tableOptions['filter'] ?? 0;
276 $isSimpleModeRow = $this->logic->getSettingsMode() === 'simple';
277
278 if ($isSimpleModeRow) {
279 // Simple mode: just "Create Redirect" and "Dismiss"
280 $editBtnHTML = '<a href="' . esc_url($editlink) . '" class="abj404-action-link" title="' . esc_attr__('Create Redirect', '404-solution') . '">'
281 . '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg> '
282 . esc_html__('Create Redirect', '404-solution') . '</a>';
283 if ($row['status'] != ABJ404_STATUS_IGNORED) {
284 $ignoreBtnHTML = ' | <a href="' . esc_url($ignorelink) . '" class="abj404-action-link" title="' . esc_attr__('Dismiss', '404-solution') . '">'
285 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"/></svg> '
286 . esc_html__('Dismiss', '404-solution') . '</a>';
287 }
288 } else {
289 // Advanced mode: full action links
290 if ($currentFilter != ABJ404_TRASH_FILTER) {
291 $editBtnHTML = '<a href="' . esc_url($editlink) . '" class="abj404-action-link" title="' . esc_attr__('Edit', '404-solution') . '">'
292 . '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg> '
293 . esc_html__('Edit', '404-solution') . '</a>';
294 }
295
296 if (($row['logsid'] ?? 0) > 0) {
297 $logsBtnHTML = '<a href="' . esc_url($logslink) . '" class="abj404-action-link" title="' . esc_attr__('View Logs', '404-solution') . '">'
298 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/></svg> '
299 . esc_html__('Logs', '404-solution') . '</a>';
300 }
301
302 if ($currentFilter != ABJ404_TRASH_FILTER) {
303 $trashBtnHTML = '<a href="' . esc_url($trashlink) . '" class="abj404-action-link danger" title="' . esc_attr($trashtitle) . '">'
304 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg> '
305 . esc_html__('Trash', '404-solution') . '</a>';
306 }
307
308 if ($currentFilter == ABJ404_TRASH_FILTER) {
309 // Show Restore button
310 $trashBtnHTML = '<a href="' . esc_url($trashlink) . '" class="abj404-action-link" title="' . esc_attr__('Restore', '404-solution') . '">'
311 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/></svg> '
312 . esc_html__('Restore', '404-solution') . '</a>';
313 $deleteBtnHTML = ' | <a href="' . esc_url($deletelink) . '" class="abj404-action-link danger" title="' . esc_attr__('Delete Permanently', '404-solution') . '" onclick="return confirm(\'' . esc_js(__('Are you sure you want to permanently delete this item?', '404-solution')) . '\');">'
314 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg> '
315 . esc_html__('Delete', '404-solution') . '</a>';
316 } else {
317 // Show Ignore and Later buttons (with separators)
318 if ($row['status'] != ABJ404_STATUS_IGNORED) {
319 $ignoreBtnHTML = ' | <a href="' . esc_url($ignorelink) . '" class="abj404-action-link" title="' . esc_attr($ignoretitle) . '">'
320 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"/></svg> '
321 . esc_html__('Ignore', '404-solution') . '</a>';
322 }
323 if ($row['status'] != ABJ404_STATUS_LATER) {
324 $laterBtnHTML = ' | <a href="' . esc_url($laterlink) . '" class="abj404-action-link" title="' . esc_attr($latertitle) . '">'
325 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg> '
326 . esc_html__('Later', '404-solution') . '</a>';
327 }
328 }
329 }
330
331 // Build full URL for visiting
332 $capturedRowUrl = is_string($row['url'] ?? '') ? (string)($row['url'] ?? '') : '';
333 $capturedRowId = is_scalar($row['id'] ?? '') ? (string)($row['id'] ?? '') : '';
334 $fullVisitorURL = esc_url(home_url($capturedRowUrl));
335
336 $tempHtml = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/tableRowCapturedURLs.html");
337 $tempHtml = $this->f->str_replace('{rowid}', $capturedRowId, $tempHtml);
338 $tempHtml = $this->f->str_replace('{rowClass}', '', $tempHtml);
339 $tempHtml = $this->f->str_replace('{visitorURL}', $fullVisitorURL, $tempHtml);
340 $tempHtml = $this->f->str_replace('{url}', esc_html($capturedRowUrl), $tempHtml);
341 $tempHtml = $this->f->str_replace('{statusBadgeClass}', $statusBadgeClass, $tempHtml);
342 $tempHtml = $this->f->str_replace('{statusTitle}', esc_attr($statusTitle), $tempHtml);
343 $tempHtml = $this->f->str_replace('{status}', $statusText, $tempHtml);
344 $capturedEngine = is_string($row['engine'] ?? '') ? trim((string)($row['engine'] ?? '')) : '';
345 $capturedEngineHTML = ($capturedEngine !== '') ? '<br><span class="abj404-engine-label">' . esc_html($capturedEngine) . '</span>' : '';
346 $tempHtml = $this->f->str_replace('{engineHTML}', $capturedEngineHTML, $tempHtml);
347 $tempHtml = $this->f->str_replace('{hits}', esc_html((string)$hits), $tempHtml);
348 $tempHtml = $this->f->str_replace('{created_date}',
349 esc_html((string)wp_date("Y/m/d h:i:s A", abs(is_scalar($row['timestamp'] ?? 0) ? intval($row['timestamp'] ?? 0) : 0))), $tempHtml);
350 $tempHtml = $this->f->str_replace('{last_used_date}', esc_html($last), $tempHtml);
351 $tempHtml = $this->f->str_replace('{lastUsedClass}', $lastUsedClass, $tempHtml);
352 $tempHtml = $this->f->str_replace('{editBtnHTML}', $editBtnHTML, $tempHtml);
353 $tempHtml = $this->f->str_replace('{logsBtnHTML}', $logsBtnHTML, $tempHtml);
354 $tempHtml = $this->f->str_replace('{trashBtnHTML}', $trashBtnHTML, $tempHtml);
355 $tempHtml = $this->f->str_replace('{deleteBtnHTML}', $deleteBtnHTML, $tempHtml);
356 $tempHtml = $this->f->str_replace('{ignoreBtnHTML}', $ignoreBtnHTML, $tempHtml);
357 $tempHtml = $this->f->str_replace('{laterBtnHTML}', $laterBtnHTML, $tempHtml);
358
359 $tempHtml = $this->f->doNormalReplacements($tempHtml);
360 $html .= $tempHtml;
361 }
362
363 if ($displayed == 0) {
364 $html .= '<tr><td colspan="6" class="abj404-empty-message">' . __('No Captured 404 Records To Display', '404-solution') . '</td></tr>';
365 }
366
367 $html .= '</tbody></table>';
368
369 return $html;
370 }
371
372 /**
373 * @return void
374 */
375 function echoAdminRedirectsPage() {
376
377 $sub = 'abj404_redirects';
378
379 $tableOptions = $this->logic->getTableOptions($sub);
380
381 // Sanitizing unchecked table options
382 $tableOptions = $this->logic->sanitizePostData($tableOptions);
383 $rawFilter = $tableOptions['filter'] ?? 0;
384 $currentFilter = is_scalar($rawFilter) ? $rawFilter : 0;
385
386 // Modern table page wrapper
387 echo '<div class="abj404-table-page">';
388
389 // Health status summary — placeholder, populated via a dedicated AJAX call
390 // (ajaxRefreshHealthBar) so the slow getHighImpactCapturedCount() query
391 // never blocks the table's first paint. JS reads the attrs below to fire
392 // an independent request as soon as the placeholder is in the DOM.
393 $healthBarNonce = wp_create_nonce('abj404_refreshHealthBar');
394 echo '<div class="abj404-health-bar" data-health-bar-placeholder="1"'
395 . ' data-health-bar-ajax-url="' . esc_attr(admin_url('admin-ajax.php')) . '"'
396 . ' data-health-bar-ajax-action="ajaxRefreshHealthBar"'
397 . ' data-health-bar-nonce="' . esc_attr($healthBarNonce) . '">';
398 echo '<span class="abj404-health-dot"></span>';
399 echo esc_html__('Loading status…', '404-solution');
400 echo '</div>';
401
402 // Page header with Add Redirect button
403 echo '<div class="abj404-table-header">';
404 echo '<div class="abj404-table-header-text">';
405 echo '<h1>' . esc_html__('Page Redirects', '404-solution') . '</h1>';
406 if ($this->logic->getSettingsMode() === 'simple') {
407 echo '<p class="abj404-guidance-subtitle">'
408 . esc_html__('The plugin creates these automatically. You only need to act when the status bar above says so.', '404-solution')
409 . '</p>';
410 }
411 echo '</div>';
412 if ($currentFilter != ABJ404_TRASH_FILTER) {
413 echo '<button type="button" class="abj404-btn abj404-btn-primary" data-modal-open="abj404-add-redirect-modal">';
414 echo '+ ' . esc_html__('Add Redirect', '404-solution');
415 echo '</button>';
416 }
417 echo '</div>';
418
419 // Content tabs — counts are placeholders, populated via AJAX
420 echo '<div class="abj404-content-tabs" data-tab-counts-placeholder="1">';
421 $this->echoContentTab($sub, 0, __('All', '404-solution'), '…', $tableOptions);
422 $this->echoContentTab($sub, ABJ404_STATUS_MANUAL, __('Manual', '404-solution'), '…', $tableOptions);
423 $this->echoContentTab($sub, ABJ404_STATUS_AUTO, __('Automatic', '404-solution'), '…', $tableOptions);
424 $this->echoContentTab($sub, ABJ404_TRASH_FILTER, __('Trash', '404-solution'), '…', $tableOptions);
425 echo '</div>';
426
427 // Filter bar with server-side search
428 $filterText = is_string($tableOptions['filterText'] ?? '') ? (string)($tableOptions['filterText'] ?? '') : '';
429 $perPage = isset($tableOptions['perpage']) ? $tableOptions['perpage'] : 25;
430
431 $paginationNonce = wp_create_nonce('abj404_updatePaginationLink');
432 $inflightNonce = wp_create_nonce('abj404_fetchInflightStage');
433 $autoRefresh = '1'; // $sub is always 'abj404_redirects' here
434 $rawOrderBy = $tableOptions['orderby'] ?? '';
435 $currentOrderBy = is_string($rawOrderBy) ? $rawOrderBy : 'url';
436 $rawOrder = $tableOptions['order'] ?? '';
437 $currentOrder = is_string($rawOrder) ? $rawOrder : 'ASC';
438 $rawPaged = $tableOptions['paged'] ?? 1;
439 $currentPaged = is_scalar($rawPaged) ? intval($rawPaged) : 1;
440 if ($currentPaged < 1) {
441 $currentPaged = 1;
442 }
443 $rawScoreRangeForAttr = $tableOptions['score_range'] ?? 'all';
444 $currentScoreRangeForAttr = is_string($rawScoreRangeForAttr) ? $rawScoreRangeForAttr : 'all';
445 echo '<div class="abj404-filter-bar tablenav"'
446 . ' data-pagination-ajax-url="' . esc_attr(admin_url('admin-ajax.php')) . '"'
447 . ' data-pagination-ajax-action="ajaxUpdatePaginationLinks"'
448 . ' data-pagination-ajax-subpage="' . esc_attr($sub) . '"'
449 . ' data-pagination-ajax-nonce="' . esc_attr($paginationNonce) . '"'
450 . ' data-pagination-inflight-nonce="' . esc_attr($inflightNonce) . '"'
451 . ' data-pagination-current-signature=""'
452 . ' data-pagination-current-orderby="' . esc_attr($currentOrderBy) . '"'
453 . ' data-pagination-current-order="' . esc_attr($currentOrder) . '"'
454 . ' data-pagination-current-filter="' . esc_attr((string)$currentFilter) . '"'
455 . ' data-pagination-current-paged="' . esc_attr((string)$currentPaged) . '"'
456 . ' data-pagination-current-score-range="' . esc_attr($currentScoreRangeForAttr) . '"'
457 . ' data-pagination-current-logsid=""'
458 . ' data-pagination-initial-load="1"'
459 . ' data-pagination-auto-refresh="' . esc_attr($autoRefresh) . '"'
460 . ' data-pagination-refresh-started-text="' . esc_attr(__('Refreshing data in background…', '404-solution')) . '"'
461 . ' data-pagination-refresh-finished-text="' . esc_attr(__('Data refreshed', '404-solution')) . '"'
462 . ' data-pagination-refresh-available-text="' . esc_attr(__('Refresh available', '404-solution')) . '">';
463 echo '<div class="abj404-search-box">';
464 echo '<input type="search" name="searchFilter" placeholder="' . esc_attr__('Type to filter redirects... (press Enter)', '404-solution') . '" value="' . esc_attr($filterText) . '" data-lpignore="true">';
465 echo '</div>';
466 echo '<div class="abj404-rows-per-page">';
467 echo '<span>' . esc_html__('Rows per page:', '404-solution') . '</span>';
468 echo '<select class="abj404-filter-select perpage" name="perpage" onchange="paginationLinksChange(this);">';
469 foreach ([10, 25, 50, 100, 200] as $opt) {
470 $selected = ($perPage == $opt) ? ' selected' : '';
471 echo '<option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
472 }
473 echo '</select>';
474 echo '</div>';
475
476 // Confidence filter dropdown
477 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
478 $currentScoreRange = is_string($rawScoreRange) ? $rawScoreRange : 'all';
479 $rawFilter = $tableOptions['filter'] ?? 0;
480 $scoreRangeBaseUrl = '?page=' . ABJ404_PP . '&subpage=' . esc_attr($sub) . '&filter=' . (int)(is_scalar($rawFilter) ? $rawFilter : 0);
481 echo '<div class="abj404-rows-per-page">';
482 echo '<span>' . esc_html__('Confidence:', '404-solution') . '</span>';
483 $scoreRangeBaseUrlJs = esc_js(esc_url($scoreRangeBaseUrl));
484 echo '<select class="abj404-filter-select" name="score_range_filter" onchange="window.location=\'' . $scoreRangeBaseUrlJs . '&score_range=\'+encodeURIComponent(this.value);">';
485 $scoreRangeOptions = array(
486 'all' => __('All', '404-solution'),
487 'high' => __('High (≥80%)', '404-solution'),
488 'medium' => __('Medium (50–79%)', '404-solution'),
489 'low' => __('Low (<50%)', '404-solution'),
490 'manual' => __('Manual (no score)', '404-solution'),
491 );
492 foreach ($scoreRangeOptions as $val => $label) {
493 $sel = ($currentScoreRange === $val) ? ' selected' : '';
494 echo '<option value="' . esc_attr($val) . '"' . $sel . '>' . esc_html($label) . '</option>';
495 }
496 echo '</select>';
497 echo '</div>';
498
499 echo '<span class="abj404-refresh-status" aria-live="polite"></span>';
500 echo '</div>';
501
502 // Bulk actions bar (hidden by default, shown when items selected)
503 $url = $this->getBulkOperationsFormURL($sub, $tableOptions);
504
505 // Form must wrap both bulk actions and table so dropdown values are submitted
506 echo '<form method="POST" action="' . esc_url($url) . '">';
507
508 echo '<div class="abj404-bulk-actions" id="abj404-bulk-actions">';
509 echo '<span class="abj404-selection-info"><strong>0</strong> ' . esc_html__('redirects selected', '404-solution') . '</span>';
510 echo '<select class="abj404-filter-select" name="abj404action">';
511 echo '<option value="">' . esc_html__('Bulk Actions', '404-solution') . '</option>';
512 if ($currentFilter != ABJ404_STATUS_AUTO) {
513 echo '<option value="editRedirect">' . esc_html__('Edit Redirects', '404-solution') . '</option>';
514 }
515 if ($currentFilter != ABJ404_TRASH_FILTER) {
516 echo '<option value="bulktrash">' . esc_html__('Move to Trash', '404-solution') . '</option>';
517 }
518 if ($currentFilter == ABJ404_TRASH_FILTER) {
519 echo '<option value="bulk_trash_restore">' . esc_html__('Restore Redirects', '404-solution') . '</option>';
520 echo '<option value="bulk_trash_delete_permanently">' . esc_html__('Delete Permanently', '404-solution') . '</option>';
521 }
522 echo '</select>';
523 echo '<button type="submit" class="abj404-btn abj404-btn-primary">' . esc_html__('Apply', '404-solution') . '</button>';
524 echo '<button type="button" class="abj404-btn abj404-btn-secondary abj404-clear-selection" onclick="abj404ClearSelection()">' . esc_html__('Clear Selection', '404-solution') . '</button>';
525 echo '</div>';
526
527 // Table container
528 echo '<div class="abj404-table-container">';
529 echo '<table class="abj404-table" data-table-awaiting-load="1">';
530 echo '<thead><tr><th>' . esc_html__('Loading redirects…', '404-solution') . '</th></tr></thead>';
531 echo '<tbody><tr><td class="abj404-empty-message">' . esc_html__('Loading redirects…', '404-solution') . '</td></tr></tbody>';
532 echo '</table>';
533 echo '</div>';
534
535 // Pagination placeholder (bottom only, matching original layout).
536 echo '<div class="abj404-pagination tablenav abj404-pagination-right">';
537 echo '<span class="abj404-refresh-status" aria-live="polite">' . esc_html__('Loading…', '404-solution') . '</span>';
538 echo '</div>';
539
540 echo '</form>';
541
542 // Empty trash button (within page container but outside form)
543 if ($currentFilter == ABJ404_TRASH_FILTER) {
544 $eturl = "?page=" . ABJ404_PP . "&filter=" . ABJ404_TRASH_FILTER . "&subpage=" . $sub;
545 $eturl = wp_nonce_url($eturl, "abj404_bulkProcess");
546 echo '<div style="padding: 0 32px 20px;">';
547 echo '<form method="POST" action="' . esc_url($eturl) . '">';
548 echo '<input type="hidden" name="action" value="emptyRedirectTrash">';
549 echo '<button type="submit" class="abj404-btn abj404-btn-secondary" onclick="return confirm(\'' . esc_js(__('Are you sure you want to permanently delete all items in the trash?', '404-solution')) . '\')">';
550 echo esc_html__('Empty Trash', '404-solution');
551 echo '</button>';
552 echo '</form>';
553 echo '</div>';
554 }
555
556 echo '</div>'; // end abj404-table-page
557
558 // Add redirect modal (outside the main container)
559 if (($tableOptions['filter'] ?? 0) != ABJ404_TRASH_FILTER) {
560 $this->echoAddRedirectModal($tableOptions);
561 }
562 }
563
564 /**
565 * Echo a content tab for the table pages
566 */
567 /**
568 * @param string $sub
569 * @param int|string $filter
570 * @param string $label
571 * @param int|string $count
572 * @param array<string, mixed> $tableOptions
573 * @return void
574 */
575 function echoContentTab($sub, $filter, $label, $count, $tableOptions) {
576 $currentFilter = $tableOptions['filter'] ?? 0;
577 $isActive = ($currentFilter == $filter) ? 'active' : '';
578 $url = "?page=" . ABJ404_PP . "&subpage=" . $sub;
579 if ($filter != 0) {
580 $url .= "&filter=" . $filter;
581 }
582 echo '<a href="' . esc_url($url) . '" class="abj404-content-tab ' . $isActive . '" data-tab-filter="' . esc_attr((string)$filter) . '">';
583 echo esc_html($label);
584 echo '<span class="abj404-tab-count">' . esc_html((string)$count) . '</span>';
585 echo '</a>';
586 }
587
588 /**
589 * Echo the modern Add Redirect modal
590 */
591 /**
592 * @param array<string, mixed> $tableOptions
593 * @return void
594 */
595 function echoAddRedirectModal($tableOptions) {
596 $options = $this->getOptionsWithDefaults();
597 $url = "?page=" . ABJ404_PP . "&subpage=abj404_redirects";
598 $orderby = array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url';
599 $order = array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC';
600 if (!($orderby == "url" && $order == "ASC")) {
601 $url .= "&orderby=" . sanitize_text_field($orderby) . "&order=" . sanitize_text_field($order);
602 }
603 $filter = array_key_exists('filter', $tableOptions) && is_scalar($tableOptions['filter']) ? $tableOptions['filter'] : 0;
604 if ($filter != 0) {
605 $url .= "&filter=" . $filter;
606 }
607 $link = wp_nonce_url($url, "abj404addRedirect");
608 $urlPlaceholder = parse_url(get_home_url(), PHP_URL_PATH) . "/example";
609
610 echo '<div class="abj404-modal" id="abj404-add-redirect-modal">';
611 echo '<div class="abj404-modal-content">';
612 echo '<div class="abj404-modal-header">';
613 echo '<h2>' . esc_html__('Add Manual Redirect', '404-solution') . '</h2>';
614 echo '<button type="button" class="abj404-modal-close" onclick="abj404CloseAddRedirectModal()">&times;</button>';
615 echo '</div>';
616 echo '<form method="POST" action="' . esc_url($link) . '" onsubmit="return validateAddManualRedirectForm(event);">';
617 echo '<input type="hidden" name="action" value="addRedirect">';
618 echo '<div class="abj404-modal-body">';
619
620 // URL field
621 echo '<div class="abj404-form-group">';
622 echo '<label class="abj404-form-label">' . esc_html__('URL', '404-solution') . ' *</label>';
623 echo '<input type="text" name="manual_redirect_url" class="abj404-form-input" placeholder="' . esc_attr($urlPlaceholder) . '" required>';
624 echo '<p class="abj404-form-help">' . esc_html__('The URL path that should be redirected (without domain)', '404-solution') . '</p>';
625 echo '<div class="abj404-checkbox-group" style="margin-top: 12px;">';
626 echo '<input type="checkbox" name="is_regex_url" id="modal_is_regex" class="abj404-checkbox-input" value="1">';
627 echo '<label for="modal_is_regex" class="abj404-checkbox-label">' . esc_html__('Treat this URL as a regular expression', '404-solution') . '</label>';
628 echo ' <a href="#" class="abj404-regex-toggle" onclick="abj404ToggleRegexInfo(event)">' . esc_html__('(Explain)', '404-solution') . '</a>';
629 echo '</div>';
630 echo '<div class="abj404-regex-info" style="display: none;">';
631 echo '<p>' . esc_html__('When checked, the text is treated as a regular expression. Note that including a bad regular expression or one that takes too long will break your website. So please use caution and test them elsewhere before trying them here. If you don\'t know what you\'re doing please don\'t use this option (as it\'s not necessary for the functioning of the plugin).', '404-solution') . '</p>';
632 echo '<p><strong>' . esc_html__('Example:', '404-solution') . '</strong> <code>/events/(.+)</code></p>';
633 echo '<p>' . esc_html__('/events/(.+) will match any URL that begins with /events/ and redirect to the specified page. Since a capture group is used, you can use a $1 replacement in the destination string of an external URL.', '404-solution') . '</p>';
634 echo '<p>' . esc_html__('First, all of the normal "exact match" URLs are checked, then all of the regular expression URLs are checked.', '404-solution') . '</p>';
635 echo '</div>';
636 echo '</div>';
637
638 // Redirect to field - using the existing AJAX autocomplete template.
639 // The template provides the label via {redirect_to_label}; no separate PHP label needed.
640 echo '<div class="abj404-form-group abj404-autocomplete-wrapper">';
641
642 $redirectHtml = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectPageSearchDropdown.html");
643 $redirectHtml = $this->f->str_replace('{redirect_to_label}', esc_html__('Redirect to', '404-solution') . ' *', $redirectHtml);
644 $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}',
645 __('(Type a page name or an external URL)', '404-solution'), $redirectHtml);
646 $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}',
647 __('(A page has been selected.)', '404-solution'), $redirectHtml);
648 $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}',
649 __('(A custom string has been entered.)', '404-solution'), $redirectHtml);
650 $redirectHtml = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}',
651 __('(An external URL will be used.)', '404-solution'), $redirectHtml);
652 $redirectHtml = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', '', $redirectHtml);
653 $redirectHtml = $this->f->str_replace('{redirectPageTitle}', '', $redirectHtml);
654 $redirectHtml = $this->f->str_replace('{pageIDAndType}', '', $redirectHtml);
655 $redirectHtml = $this->f->str_replace('{data-url}',
656 "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $redirectHtml);
657 $redirectHtml = $this->f->doNormalReplacements($redirectHtml);
658 echo $redirectHtml;
659
660 echo '</div>';
661
662 // Redirect type — button grid
663 $rawDefault = $options['default_redirect'] ?? '301';
664 $defaultCode = is_string($rawDefault) ? $rawDefault : '301';
665 $this->echoRedirectTypeButtonGrid($defaultCode);
666
667 // Advanced Options: schedule + conditions
668 echo '<details class="abj404-advanced-options">';
669 echo '<summary class="abj404-advanced-options__summary">' . esc_html__('Advanced Options', '404-solution') . '</summary>';
670 echo '<div class="abj404-advanced-options__body">';
671
672 // Active From
673 echo '<div class="abj404-form-group">';
674 echo '<label class="abj404-form-label" for="redirect_start_date">' . esc_html__('Active From (optional)', '404-solution') . '</label>';
675 echo '<input type="date" name="redirect_start_date" id="redirect_start_date" class="abj404-form-input" value="">';
676 echo '<p class="abj404-form-help">' . esc_html__('Leave blank to activate immediately', '404-solution') . '</p>';
677 echo '</div>';
678
679 // Active Until
680 echo '<div class="abj404-form-group">';
681 echo '<label class="abj404-form-label" for="redirect_end_date">' . esc_html__('Active Until (optional)', '404-solution') . '</label>';
682 echo '<input type="date" name="redirect_end_date" id="redirect_end_date" class="abj404-form-input" value="">';
683 echo '<p class="abj404-form-help">' . esc_html__('Leave blank to never expire', '404-solution') . '</p>';
684 echo '</div>';
685
686 // Conditions
687 $this->echoRedirectConditionsSection();
688
689 echo '</div>'; // end .abj404-advanced-options__body
690 echo '</details>';
691
692 echo '</div>'; // end .abj404-modal-body
693 echo '<div class="abj404-modal-footer">';
694 echo '<button type="button" class="abj404-btn abj404-btn-secondary" onclick="abj404CloseAddRedirectModal()">' . esc_html__('Cancel', '404-solution') . '</button>';
695 echo '<button type="submit" class="abj404-btn abj404-btn-primary">' . esc_html__('Add Redirect', '404-solution') . '</button>';
696 echo '</div>';
697 echo '</form>';
698 echo '</div>';
699 echo '</div>';
700 }
701
702 /**
703 * Render the redirect type button grid, hidden input, and the JS handler.
704 * Used by both the Add Redirect modal and the Edit Redirect page.
705 *
706 * @param string $selectedCode The currently selected code value (e.g. '301').
707 * @return void
708 */
709 /**
710 * Get modern pagination HTML
711 */
712 /**
713 * @param string $sub
714 * @param array<string, mixed> $tableOptions
715 * @return string
716 */
717 function getModernPagination($sub, $tableOptions) {
718 $logsid = isset($tableOptions['logsid']) ? intval(is_scalar($tableOptions['logsid']) ? $tableOptions['logsid'] : 0) : 0;
719 $filter = isset($tableOptions['filter']) ? intval(is_scalar($tableOptions['filter']) ? $tableOptions['filter'] : 0) : 0;
720 $orderby = isset($tableOptions['orderby']) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url';
721 $order = isset($tableOptions['order']) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC';
722
723 // Use appropriate count method based on sub type
724 $logsidInt = (int)$logsid;
725 if ($sub == 'abj404_logs') {
726 $totalRows = $this->viewReadService->getLogsCount($logsidInt);
727 } else {
728 $totalRows = $this->viewReadService->getRedirectsForViewCount($sub, $tableOptions);
729 }
730 $rawPerpage = array_key_exists('perpage', $tableOptions) && is_scalar($tableOptions['perpage']) ? $tableOptions['perpage'] : 25;
731 $perPage = intval($rawPerpage);
732 if ($perPage <= 0) {
733 $perPage = 25;
734 }
735 $rawPaged = array_key_exists('paged', $tableOptions) && is_scalar($tableOptions['paged']) ? $tableOptions['paged'] : 1;
736 $currentPage = intval($rawPaged);
737 $totalPages = ceil($totalRows / $perPage);
738
739 if ($totalPages <= 1) {
740 return '';
741 }
742
743 $startItem = (($currentPage - 1) * $perPage) + 1;
744 $endItem = min($currentPage * $perPage, $totalRows);
745
746 $baseUrl = "?page=" . ABJ404_PP . "&subpage=" . $sub;
747 // Include logsid for logs pagination
748 if ($sub == 'abj404_logs' && isset($tableOptions['logsid'])) {
749 $baseUrl .= "&id=" . $tableOptions['logsid'];
750 }
751 if ($filter != 0) {
752 $baseUrl .= "&filter=" . $filter;
753 }
754 if (!( $orderby == "url" && $order == "ASC" )) {
755 $baseUrl .= "&orderby=" . sanitize_text_field($orderby) . "&order=" . sanitize_text_field($order);
756 }
757
758 // Different label for logs vs redirects
759 $itemLabel = ($sub == 'abj404_logs') ? __('logs', '404-solution') : __('redirects', '404-solution');
760
761 $html = '<div class="abj404-pagination">';
762 $html .= '<div class="abj404-pagination-info">';
763 $html .= sprintf(
764 /* translators: %1$d is start item, %2$d is end item, %3$d is total count, %4$s is item type (logs/redirects) */
765 esc_html__('Showing %1$d-%2$d of %3$d %4$s', '404-solution'),
766 $startItem,
767 $endItem,
768 $totalRows,
769 $itemLabel
770 );
771 $html .= '</div>';
772 $html .= '<div class="abj404-pagination-controls">';
773
774 // Previous button
775 if ($currentPage > 1) {
776 $html .= '<a href="' . esc_url($baseUrl . '&paged=' . ($currentPage - 1)) . '" class="abj404-page-btn">&lsaquo;</a>';
777 } else {
778 $html .= '<span class="abj404-page-btn disabled">&lsaquo;</span>';
779 }
780
781 // Page numbers
782 $range = 2;
783 for ($i = 1; $i <= $totalPages; $i++) {
784 if ($i == 1 || $i == $totalPages || ($i >= $currentPage - $range && $i <= $currentPage + $range)) {
785 $activeClass = ($i == $currentPage) ? ' active' : '';
786 $html .= '<a href="' . esc_url($baseUrl . '&paged=' . $i) . '" class="abj404-page-btn' . $activeClass . '">' . $i . '</a>';
787 } elseif ($i == $currentPage - $range - 1 || $i == $currentPage + $range + 1) {
788 $html .= '<span class="abj404-page-ellipsis">&hellip;</span>';
789 }
790 }
791
792 // Next button
793 if ($currentPage < $totalPages) {
794 $html .= '<a href="' . esc_url($baseUrl . '&paged=' . ($currentPage + 1)) . '" class="abj404-page-btn">&rsaquo;</a>';
795 } else {
796 $html .= '<span class="abj404-page-btn disabled">&rsaquo;</span>';
797 }
798
799 $html .= '</div>';
800 $html .= '</div>';
801
802 return $html;
803 }
804
805 /**
806 * @param string $sub
807 * @param array<string, mixed> $tableOptions
808 * @return string
809 */
810 function getBulkOperationsFormURL($sub, $tableOptions) {
811 $url = "?page=" . ABJ404_PP . "&subpage=" . $sub;
812 $filter = array_key_exists('filter', $tableOptions) && is_scalar($tableOptions['filter']) ? $tableOptions['filter'] : 0;
813 if ($filter != 0) {
814 $url .= "&filter=" . $filter;
815 }
816 $orderby = array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url';
817 $order = array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC';
818 if (!($orderby == "url" && $order == "ASC")) {
819 $url .= "&orderby=" . sanitize_text_field($orderby) . "&order=" . sanitize_text_field($order);
820 }
821 $url = wp_nonce_url($url, 'abj404_bulkProcess');
822 return $url;
823 }
824
825 /**
826 * @param string $sub
827 * @return string
828 */
829 function getAdminRedirectsPageTable($sub) {
830 $tableOptions = $this->logic->getTableOptions($sub);
831 $columns = $this->buildRedirectsColumnDefs($tableOptions);
832
833 $html = "<table class=\"abj404-table\"><thead>";
834 $html .= $this->getTableColumns($sub, $columns);
835 $html .= "</thead><tbody id=\"the-list\">";
836
837 $deadDestIds = function_exists('get_transient') ? get_transient('abj404_dead_dest_ids') : false;
838 if (!is_array($deadDestIds)) {
839 $deadDestIds = array();
840 }
841
842 $rows = $this->viewReadService->getRedirectsForView($sub, $tableOptions);
843 /** @var array<int, array<string, mixed>> $typedRedirectRows */
844 $typedRedirectRows = array_values(array_filter($rows, 'is_array'));
845 $this->rememberTableDataSignature($sub, $typedRedirectRows);
846 $displayed = 0;
847 $y = 1;
848 foreach ($typedRedirectRows as $row) {
849 $html .= $this->buildRedirectRowHTML($row, $sub, $tableOptions, $deadDestIds, $y);
850 $y = ($y === 0) ? 1 : 0;
851 $displayed++;
852 }
853 if ($displayed == 0) {
854 $html .= "<tr>\n" .
855 "<td colspan=\"10\" class=\"abj404-empty-state\">" .
856 "<div class=\"abj404-empty-state-icon\">📋</div>" .
857 "<h3>" . __('No Redirect Records To Display', '404-solution') . "</h3>" .
858 "<p>" . __('Redirects will appear here once created.', '404-solution') . "</p>" .
859 "</td></tr>";
860 }
861 $html .= "</tbody></table>";
862
863 return $html;
864 }
865
866 /**
867 * @param array<string, mixed> $tableOptions
868 * @return array<string, array<string, string>>
869 */
870 private function buildRedirectsColumnDefs(array $tableOptions): array {
871 $columns = array();
872 $columns['url']['title'] = __('URL', '404-solution');
873 $columns['url']['orderby'] = "url";
874 $columns['url']['width'] = "25%";
875 $columns['status']['title'] = __('Status', '404-solution');
876 $columns['status']['orderby'] = "status";
877 $columns['status']['width'] = "5%";
878 $columns['type']['title'] = __('Type', '404-solution');
879 $columns['type']['orderby'] = "type";
880 $columns['type']['width'] = "10%";
881 $columns['dest']['title'] = __('Destination', '404-solution');;
882 $columns['dest']['orderby'] = "final_dest";
883 $columns['dest']['width'] = "22%";
884 $columns['code']['title'] = __('Redirect', '404-solution');
885 $columns['code']['orderby'] = "code";
886 $columns['code']['width'] = "5%";
887 $columns['confidence']['title'] = __('Confidence', '404-solution');
888 $columns['confidence']['orderby'] = "score";
889 $columns['confidence']['width'] = "7%";
890 $columns['confidence']['class'] = "hide-on-tablet";
891 $columns['hits']['title'] = __('Hits', '404-solution');
892 $columns['hits']['orderby'] = "logshits";
893 $columns['hits']['width'] = "7%";
894 $hitsTooltip = $this->getHitsColumnTooltip($tableOptions);
895 $columns['hits']['title_attr_html'] = $hitsTooltip;
896 $columns['timestamp']['title'] = __('Created', '404-solution');
897 $columns['timestamp']['orderby'] = "timestamp";
898 $columns['timestamp']['width'] = "10%";
899 $columns['timestamp']['class'] = "hide-on-tablet";
900 $columns['last_used']['title'] = __('Last Used', '404-solution');
901 $columns['last_used']['orderby'] = "last_used";
902 $columns['last_used']['width'] = "10%";
903 $columns['last_used']['title_attr_html'] = $hitsTooltip;
904 return $columns;
905 }
906
907 /**
908 * @param array<string, mixed> $row
909 * @param string $sub
910 * @param array<string, mixed> $tableOptions
911 * @param array<mixed> $deadDestIds
912 * @param int $y
913 * @return string
914 */
915 private function buildRedirectRowHTML(array $row, string $sub, array $tableOptions, array $deadDestIds, int $y): string {
916 $rowType = $row['type'] ?? 0;
917 $rowStatus = $row['status'] ?? 0;
918 $rowFinalDest = is_string($row['final_dest'] ?? '') ? (string)($row['final_dest'] ?? '') : '';
919 $destForView = trim(is_scalar($row['dest_for_view'] ?? '') ? (string)($row['dest_for_view'] ?? '') : '');
920 $statusTitle = '';
921 if ($rowStatus == ABJ404_STATUS_MANUAL) {
922 $statusTitle = __('Manually created', '404-solution');
923 } else if ($rowStatus == ABJ404_STATUS_AUTO) {
924 $statusTitle = __('Automatically created', '404-solution');
925 } else if ($rowStatus == ABJ404_STATUS_REGEX) {
926 $statusTitle = __('Regular Expression (Manually Created)', '404-solution');
927 } else {
928 $statusTitle = __('Unknown', '404-solution');
929 }
930
931 $destLink = $this->resolveRedirectDestLink($rowType, $rowFinalDest);
932 $link = $destLink['link'];
933 $title = $destLink['title'];
934 if ($link != '') {
935 $link = "href='" . esc_url($link) . "'";
936 }
937
938 $hits = is_scalar($row['logshits'] ?? 0) ? (int)($row['logshits'] ?? 0) : 0;
939
940 $last_used = is_scalar($row['last_used'] ?? 0) ? (int)($row['last_used'] ?? 0) : 0;
941 if ($last_used != 0) {
942 $last = (string)wp_date("Y/m/d h:i:s A", abs($last_used));
943 } else {
944 $last = __('Never Used', '404-solution');
945 }
946
947 // Build action links using helper method
948 /** @var array<string, mixed> $row */
949 $links = $this->buildTableActionLinks($row, $sub, $tableOptions, false);
950 $editlink = '';
951 $logslink = '';
952 $trashlink = '';
953 $trashtitle = '';
954 $deletelink = '';
955 $ajaxTrashLink = '';
956 extract($links);
957
958 $class = "";
959 if ($y == 0) {
960 $class = "alternate";
961 $y++;
962 } else {
963 $y = 0;
964 $class = "normal-non-alternate";
965 }
966 // make the entire row red if the destination is missing, doesn't exist, or is unpublished.
967 $destinationDoesNotExistClass = '';
968 $destinationIsMissing = false;
969 if ($rowType != ABJ404_TYPE_404_DISPLAYED && trim((string)$rowFinalDest) === '') {
970 $destinationIsMissing = true;
971 $destinationDoesNotExistClass = ' destination-does-not-exist';
972 }
973 if (array_key_exists('published_status', $row)) {
974 if ($row['published_status'] == '0') {
975 $destinationDoesNotExistClass = ' destination-does-not-exist';
976 }
977 }
978
979 // Check if URL looks like a regex pattern but is not marked as a regex redirect
980 $urlLooksLikeRegexClass = '';
981 $rowUrl = is_string($row['url'] ?? '') ? (string)($row['url'] ?? '') : '';
982 $urlLooksLikeRegex = ABJ_404_Solution_Functions::urlLooksLikeRegex($rowUrl);
983 $isRegexStatus = ($rowStatus == ABJ404_STATUS_REGEX);
984 if ($urlLooksLikeRegex && !$isRegexStatus) {
985 $urlLooksLikeRegexClass = ' url-looks-like-regex';
986 }
987
988 $class = $class . $destinationDoesNotExistClass . $urlLooksLikeRegexClass;
989
990 // -------------------------------------------
991 // Build modern row action buttons with icons
992 $editBtnHTML = '';
993 $logsBtnHTML = '';
994 $trashBtnHTML = '';
995 $deleteBtnHTML = '';
996
997 $currentFilter = $tableOptions['filter'] ?? 0;
998 if ($currentFilter != ABJ404_TRASH_FILTER) {
999 $editBtnHTML = '<a href="' . esc_url($editlink) . '" class="abj404-action-link" title="{Edit Redirect Details}">'
1000 . '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg> '
1001 . '{Edit}</a>';
1002 $trashBtnHTML = '<a href="#" class="abj404-action-link danger ajax-trash-link" data-url="{ajaxTrashLink}" title="{Trash Redirected URL}">'
1003 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg> '
1004 . '{Trash}</a>';
1005 }
1006 if (($row['logsid'] ?? 0) > 0) {
1007 $logsBtnHTML = '<a href="{logsLink}" class="abj404-action-link" title="{View Redirect Logs}">'
1008 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/></svg> '
1009 . '{Logs}</a>';
1010 }
1011 if ($currentFilter == ABJ404_TRASH_FILTER) {
1012 $trashBtnHTML = '<a href="{trashLink}" class="abj404-action-link" title="{Restore}">'
1013 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/></svg> '
1014 . '{Restore}</a>';
1015 $deleteBtnHTML = ' | <a href="{deletelink}" class="abj404-action-link danger" title="{Delete Redirect Permanently}">'
1016 . '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg> '
1017 . '{Delete}</a>';
1018 }
1019
1020 // Determine badge classes
1021 $statusBadgeClass = 'abj404-badge-manual';
1022 if ($rowStatus == ABJ404_STATUS_AUTO) {
1023 $statusBadgeClass = 'abj404-badge-auto';
1024 } else if ($rowStatus == ABJ404_STATUS_REGEX) {
1025 $statusBadgeClass = 'abj404-badge-regex';
1026 }
1027
1028 $rowCode = is_scalar($row['code'] ?? '') ? (string)($row['code'] ?? '') : '';
1029 $codeBadgeMap = array(
1030 '301' => 'abj404-badge-301',
1031 '302' => 'abj404-badge-302',
1032 '307' => 'abj404-badge-307',
1033 '308' => 'abj404-badge-308',
1034 '410' => 'abj404-badge-410',
1035 '451' => 'abj404-badge-451',
1036 '0' => 'abj404-badge-meta',
1037 );
1038 $codeBadgeClass = isset($codeBadgeMap[$rowCode]) ? $codeBadgeMap[$rowCode] : 'abj404-badge-302';
1039
1040 // In Simple mode, show plain language labels instead of numeric codes
1041 $codeDisplay = $rowCode;
1042 if ($this->logic->getSettingsMode() === 'simple') {
1043 $codeDisplay = self::getPlainLanguageCodeLabel($rowCode);
1044 }
1045
1046 $lastUsedClass = '';
1047 if ($last_used == 0) {
1048 $lastUsedClass = 'abj404-never-used';
1049 }
1050
1051 $destWarning = $this->resolveDestinationWarnings($row, $rowType, $rowFinalDest, $destForView, $destinationIsMissing, $deadDestIds);
1052 $destinationExists = $destWarning['exists'];
1053 $destinationDoesNotExist = $destWarning['notExists'];
1054 $destinationWarningText = $destWarning['text'];
1055 $destForView = $destWarning['destForView'];
1056
1057 // URL regex warning visibility
1058 $urlIsNormal = '';
1059 $urlLooksLikeRegexWarning = 'display: none;';
1060 if ($urlLooksLikeRegex && !$isRegexStatus) {
1061 $urlIsNormal = 'display: none;';
1062 $urlLooksLikeRegexWarning = '';
1063 }
1064
1065 // Build full URL with WordPress base path for subdirectory installations
1066 $rowId = is_scalar($row['id'] ?? '') ? (string)($row['id'] ?? '') : '';
1067 $fullVisitorURL = esc_url(home_url($rowUrl));
1068
1069 $rowEngine = is_string($row['engine'] ?? '') ? trim((string)($row['engine'] ?? '')) : '';
1070 $engineHTML = ($rowEngine !== '') ? '<br><span class="abj404-engine-label">' . esc_html($rowEngine) . '</span>' : '';
1071 $scoreCell = $this->buildScoreCell($row['score'] ?? null, $rowEngine);
1072 $statusForView = is_string($row['status_for_view'] ?? '') ? (string)($row['status_for_view'] ?? '') : '';
1073 $typeForView = is_string($row['type_for_view'] ?? '') ? (string)($row['type_for_view'] ?? '') : '';
1074
1075 return $this->fillRedirectRowTemplate([
1076 '{rowid}' => $rowId, '{rowClass}' => $class,
1077 '{visitorURL}' => $fullVisitorURL, '{rowURL}' => esc_html($rowUrl),
1078 '{url-is-normal}' => $urlIsNormal, '{url-looks-like-regex}' => $urlLooksLikeRegexWarning,
1079 '{editBtnHTML}' => $editBtnHTML, '{logsBtnHTML}' => $logsBtnHTML,
1080 '{trashBtnHTML}' => $trashBtnHTML, '{deleteBtnHTML}' => $deleteBtnHTML,
1081 '{statusBadgeClass}' => $statusBadgeClass, '{codeBadgeClass}' => $codeBadgeClass,
1082 '{lastUsedClass}' => $lastUsedClass,
1083 '{link}' => $link, '{title}' => esc_attr($title),
1084 '{dest}' => esc_attr($destForView),
1085 '{destination-exists}' => $destinationExists,
1086 '{destination-does-not-exist}' => $destinationDoesNotExist,
1087 '{destination-warning-text}' => $destinationWarningText,
1088 '{status}' => $statusForView, '{statusTitle}' => $statusTitle,
1089 '{engineHTML}' => $engineHTML, '{rowScore}' => '', '{scoreCell}' => $scoreCell,
1090 '{type}' => $typeForView, '{rowCode}' => esc_html($codeDisplay),
1091 '{hits}' => esc_html((string)$hits),
1092 '{logsLink}' => $logslink, '{trashLink}' => $trashlink,
1093 '{ajaxTrashLink}' => $ajaxTrashLink, '{trashtitle}' => $trashtitle,
1094 '{deletelink}' => $deletelink,
1095 '{created_date}' => esc_html((string)wp_date("Y/m/d h:i:s A", abs(is_scalar($row['timestamp'] ?? 0) ? intval($row['timestamp'] ?? 0) : 0))),
1096 '{last_used_date}' => esc_html($last),
1097 ]);
1098 }
1099
1100 /**
1101 * @param array<string, mixed> $row
1102 * @param mixed $rowType
1103 * @param string $rowFinalDest
1104 * @param string $destForView
1105 * @param bool $destinationIsMissing
1106 * @param array<mixed> $deadDestIds
1107 * @return array{exists: string, notExists: string, text: string, destForView: string}
1108 */
1109 private function resolveDestinationWarnings(array $row, $rowType, string $rowFinalDest, string $destForView, bool $destinationIsMissing, array $deadDestIds): array {
1110 $exists = '';
1111 $notExists = 'display: none;';
1112 $text = __("This page doesn't exist or is not published so the redirect won't work.", '404-solution');
1113 if ($destinationIsMissing) {
1114 $exists = 'display: none;';
1115 $notExists = '';
1116 $text = __('Destination missing. Edit this redirect and choose a destination.', '404-solution');
1117 if (trim((string)$destForView) === '') {
1118 $destForView = __('(Destination missing)', '404-solution');
1119 }
1120 }
1121 if (array_key_exists('published_status', $row) && $row['published_status'] == '0') {
1122 $exists = 'display: none;';
1123 $notExists = '';
1124 if (trim((string)$destForView) === '') {
1125 $destForView = __('(Destination unavailable)', '404-solution');
1126 }
1127 }
1128 $rowIdStr = is_scalar($row['id'] ?? '') ? (string) ($row['id'] ?? '') : '';
1129 if (in_array($rowIdStr, $deadDestIds, true)) {
1130 $exists = 'display: none;';
1131 $notExists = '';
1132 $text = __('Destination returned 404 recently, redirect suspended until destination is restored.', '404-solution');
1133 }
1134 return ['exists' => $exists, 'notExists' => $notExists, 'text' => $text, 'destForView' => $destForView];
1135 }
1136
1137 /**
1138 * @param array<string, string> $replacements
1139 * @return string
1140 */
1141 private function fillRedirectRowTemplate(array $replacements): string {
1142 $htmlTemp = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/tableRowPageRedirects.html");
1143 foreach ($replacements as $placeholder => $value) {
1144 $htmlTemp = $this->f->str_replace($placeholder, $value, $htmlTemp);
1145 }
1146 return $this->f->doNormalReplacements($htmlTemp);
1147 }
1148
1149 /**
1150 * @param mixed $rawScore
1151 * @param string $rowEngine
1152 * @return string
1153 */
1154 private function buildScoreCell($rawScore, string $rowEngine): string {
1155 if ($rawScore !== null && $rawScore !== '') {
1156 $scoreNum = (float)(is_numeric($rawScore) ? $rawScore : 0);
1157 $scorePct = number_format($scoreNum, 0);
1158 if ($scoreNum >= 80) {
1159 $scoreBadgeClass = 'abj404-score-high';
1160 } elseif ($scoreNum >= 50) {
1161 $scoreBadgeClass = 'abj404-score-medium';
1162 } else {
1163 $scoreBadgeClass = 'abj404-score-low';
1164 }
1165 return '<span class="abj404-score-badge ' . $scoreBadgeClass . '">' . esc_html($scorePct) . '%</span>';
1166 }
1167 $noScoreTitle = ($rowEngine !== '')
1168 ? __('No confidence score for this engine', '404-solution')
1169 : __('Manual redirect, no confidence score', '404-solution');
1170 return '<span class="abj404-score-manual" title="' . esc_attr($noScoreTitle) . '">&#x2014;</span>'; // allow-em-dash: em-dash used as visual placeholder in HTML table cell
1171 }
1172
1173 /**
1174 * @param mixed $rowType
1175 * @param string $rowFinalDest
1176 * @return array{link: string, title: string}
1177 */
1178 private function resolveRedirectDestLink($rowType, string $rowFinalDest): array {
1179 $link = '';
1180 $title = __('Visit', '404-solution') . ' ';
1181 if ($rowType == ABJ404_TYPE_EXTERNAL) {
1182 if ($rowFinalDest !== '') {
1183 $link = $rowFinalDest;
1184 $title .= $rowFinalDest;
1185 }
1186 } else if ($rowType == ABJ404_TYPE_CAT) {
1187 if ($rowFinalDest !== '') {
1188 $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . '|' . ABJ404_TYPE_CAT, 0);
1189 $link = is_string($permalink['link']) ? $permalink['link'] : '';
1190 $title .= __('Category:', '404-solution') . ' ' . (is_string($permalink['title']) ? $permalink['title'] : '');
1191 }
1192 } else if ($rowType == ABJ404_TYPE_TAG) {
1193 if ($rowFinalDest !== '') {
1194 $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . '|' . ABJ404_TYPE_TAG, 0);
1195 $link = is_string($permalink['link']) ? $permalink['link'] : '';
1196 $title .= __('Tag:', '404-solution') . ' ' . (is_string($permalink['title']) ? $permalink['title'] : '');
1197 }
1198 } else if ($rowType == ABJ404_TYPE_HOME) {
1199 $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . '|' . ABJ404_TYPE_HOME, 0);
1200 $link = is_string($permalink['link']) ? $permalink['link'] : '';
1201 $title .= __('Home Page:', '404-solution') . ' ' . (is_string($permalink['title']) ? $permalink['title'] : '');
1202 } else if ($rowType == ABJ404_TYPE_POST) {
1203 if ($rowFinalDest !== '') {
1204 $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . '|' . ABJ404_TYPE_POST, 0);
1205 $link = is_string($permalink['link']) ? $permalink['link'] : '';
1206 $title .= is_string($permalink['title']) ? $permalink['title'] : '';
1207 }
1208 } else if ($rowType == ABJ404_TYPE_404_DISPLAYED) {
1209 $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($rowFinalDest . '|' . ABJ404_TYPE_404_DISPLAYED, 0);
1210 $link = is_string($permalink['link']) ? $permalink['link'] : '';
1211 $title .= is_string($permalink['title']) ? $permalink['title'] : '';
1212 if ($rowFinalDest == '0') {
1213 $link = '';
1214 }
1215 } else {
1216 $this->logger->errorMessage('Unexpected row type while displaying table: ' . $rowType);
1217 }
1218 return ['link' => $link, 'title' => $title];
1219 }
1220
1221 /**
1222 * @param array<string, mixed> $tableOptions
1223 * @return void
1224 */
1225 function echoAddManualRedirect($tableOptions) {
1226
1227 $options = $this->getOptionsWithDefaults();
1228
1229 $url = "?page=" . ABJ404_PP . "&subpage=abj404_redirects";
1230 $orderby = array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : 'url';
1231 $order = array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : 'ASC';
1232 if (!($orderby == "url" && $order == "ASC")) {
1233 $url .= "&orderby=" . sanitize_text_field($orderby) . "&order=" . sanitize_text_field($order);
1234 }
1235 $filter = array_key_exists('filter', $tableOptions) && is_scalar($tableOptions['filter']) ? $tableOptions['filter'] : 0;
1236 if ($filter != 0) {
1237 $url .= "&filter=" . $filter;
1238 }
1239 $link = wp_nonce_url($url, "abj404addRedirect");
1240
1241 $urlPlaceholder = parse_url(get_home_url(), PHP_URL_PATH) . "/example";
1242 if (isset($_POST['url']) && $_POST['url'] != '') {
1243 $postedURL = esc_url($_POST['url']);
1244 } else {
1245 $postedURL = $urlPlaceholder;
1246 }
1247
1248 $selected301 = ($options['default_redirect'] == '301') ? ' selected ' : '';
1249 $selected302 = ($options['default_redirect'] == '302') ? ' selected ' : '';
1250 $selected307 = ($options['default_redirect'] == '307') ? ' selected ' : '';
1251 $selected308 = ($options['default_redirect'] == '308') ? ' selected ' : '';
1252 $selected410 = '';
1253 $selected451 = '';
1254 $selected0 = '';
1255
1256 // read the html content.
1257 $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectTop.html");
1258 $html .= ABJ_404_Solution_Functions::readFileContents(__DIR__ .
1259 "/html/addManualRedirectPageSearchDropdown.html");
1260
1261 $html = $this->f->str_replace('{redirect_to_label}', __('Redirect to', '404-solution'), $html);
1262 $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}',
1263 __('(Type a page name or an external URL)', '404-solution'), $html);
1264 $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}',
1265 __('(A page has been selected.)', '404-solution'), $html);
1266 $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}',
1267 __('(A custom string has been entered.)', '404-solution'), $html);
1268 $html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}',
1269 __('(An external URL will be used.)', '404-solution'), $html);
1270 $html = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', '', $html);
1271 $html = $this->f->str_replace('{redirectPageTitle}', '', $html);
1272 $html = $this->f->str_replace('{pageIDAndType}', '', $html);
1273 $html = $this->f->str_replace('{redirectPageTitle}', '', $html);
1274 $html = $this->f->str_replace('{data-url}',
1275 "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $html);
1276
1277 $html .= ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/addManualRedirectBottom.html");
1278 $html = $this->f->str_replace('{addManualRedirectAction}', $link, $html);
1279 $html = $this->f->str_replace('{urlPlaceholder}', esc_attr($urlPlaceholder), $html);
1280 $html = $this->f->str_replace('{postedURL}', esc_attr($postedURL), $html);
1281 $html = $this->f->str_replace('{301selected}', $selected301, $html);
1282 $html = $this->f->str_replace('{302selected}', $selected302, $html);
1283 $html = $this->f->str_replace('{307selected}', $selected307, $html);
1284 $html = $this->f->str_replace('{308selected}', $selected308, $html);
1285 $html = $this->f->str_replace('{410selected}', $selected410, $html);
1286 $html = $this->f->str_replace('{451selected}', $selected451, $html);
1287 $html = $this->f->str_replace('{0selected}', $selected0, $html);
1288
1289 // constants and translations.
1290 $html = $this->f->doNormalReplacements($html);
1291
1292 echo $html;
1293 }
1294
1295 /** This is used both to add and to edit a redirect.
1296 * @param string $destination
1297 * @param string $codeselected
1298 * @param string $label
1299 */
1300 /**
1301 * @param string $destination
1302 * @param string $codeselected
1303 * @param string $label
1304 * @param string|null $source_page
1305 * @param string|null $filter
1306 * @param string|null $orderby
1307 * @param string|null $order
1308 * @param string $startDate
1309 * @param string $endDate
1310 * @return void
1311 */
1312 function echoEditRedirect($destination, $codeselected, $label, $source_page = null, $filter = null, $orderby = null, $order = null, $startDate = '', $endDate = '') {
1313 // Redirect type — button grid with hidden input
1314 $this->echoRedirectTypeButtonGrid((string)$codeselected);
1315
1316 // Advanced Options: Active From/Until + Conditions (collapsed by default)
1317 {
1318 $redirectId = 0;
1319 if (isset($_GET['id']) && $this->f->regexMatch('[0-9]+', (string)$_GET['id'])) {
1320 $redirectId = absint($_GET['id']);
1321 } elseif (isset($_POST['id']) && $this->f->regexMatch('[0-9]+', (string)$_POST['id'])) {
1322 $redirectId = absint($_POST['id']);
1323 }
1324 $hasExistingConditions = ($redirectId > 0) && !empty($this->redirectsRepository->getRedirectConditions($redirectId));
1325 $hasAdvancedValues = ($startDate !== '' || $endDate !== '' || $hasExistingConditions);
1326 $openAttr = $hasAdvancedValues ? ' open' : '';
1327 echo '<details class="abj404-advanced-options"' . $openAttr . '>';
1328 echo '<summary class="abj404-advanced-options__summary">' . esc_html__('Advanced Options', '404-solution') . '</summary>';
1329 echo '<div class="abj404-advanced-options__body">';
1330
1331 // Active From
1332 echo '<div class="abj404-form-group">';
1333 echo '<label class="abj404-form-label" for="redirect_start_date">' . esc_html__('Active From (optional)', '404-solution') . '</label>';
1334 echo '<input type="date" name="redirect_start_date" id="redirect_start_date" class="abj404-form-input" value="' . esc_attr($startDate) . '">';
1335 echo '<p class="abj404-form-help">' . esc_html__('Leave blank to activate immediately', '404-solution') . '</p>';
1336 echo '</div>';
1337
1338 // Active Until
1339 echo '<div class="abj404-form-group">';
1340 echo '<label class="abj404-form-label" for="redirect_end_date">' . esc_html__('Active Until (optional)', '404-solution') . '</label>';
1341 echo '<input type="date" name="redirect_end_date" id="redirect_end_date" class="abj404-form-input" value="' . esc_attr($endDate) . '">';
1342 echo '<p class="abj404-form-help">' . esc_html__('Leave blank to never expire', '404-solution') . '</p>';
1343 echo '</div>';
1344
1345 // Conditions
1346 $this->echoRedirectConditionsSection();
1347
1348 echo '</div>'; // end abj404-advanced-options__body
1349 echo '</details>';
1350 }
1351
1352 // Button group
1353 echo '<div class="abj404-button-group">';
1354
1355 // Cancel button
1356 $cancelUrl = '?page=' . ABJ404_PP;
1357 if ($source_page) {
1358 $cancelUrl .= '&subpage=' . esc_attr($source_page);
1359 }
1360 if ($filter !== null) {
1361 $cancelUrl .= '&filter=' . esc_attr($filter);
1362 }
1363 if ($orderby !== null) {
1364 $cancelUrl .= '&orderby=' . esc_attr($orderby);
1365 }
1366 if ($order !== null) {
1367 $cancelUrl .= '&order=' . esc_attr($order);
1368 }
1369 echo '<a href="' . esc_url($cancelUrl) . '" class="abj404-btn abj404-btn-secondary">' . esc_html__('Cancel', '404-solution') . '</a>';
1370
1371 // Submit button
1372 echo '<button type="submit" class="abj404-btn abj404-btn-primary">' . esc_html($label) . '</button>';
1373 echo '</div>';
1374 }
1375
1376 /**
1377 * @param string $currentlySelected
1378 * @return string
1379 */
1380 function echoRedirectDestinationOptionsDefaults($currentlySelected) {
1381 $content = "";
1382 $content .= "\n" . '<optgroup label="' . __('Special', '404-solution') . '">' . "\n";
1383
1384 $selected = "";
1385 if ($currentlySelected == ABJ404_TYPE_EXTERNAL) {
1386 $selected = " selected";
1387 }
1388 $content .= "\n<option value=\"" . ABJ404_TYPE_EXTERNAL . "|" . ABJ404_TYPE_EXTERNAL . "\"" . $selected . ">" .
1389 __('External Page', '404-solution') . "</option>";
1390
1391 if ($currentlySelected == ABJ404_TYPE_HOME) {
1392 $selected = " selected";
1393 } else {
1394 $selected = "";
1395 }
1396 $content .= "\n<option value=\"" . ABJ404_TYPE_HOME . "|" . ABJ404_TYPE_HOME . "\"" . $selected . ">" .
1397 __('Home Page', '404-solution') . "</option>";
1398
1399 $content .= "\n" . '</optgroup>' . "\n";
1400
1401 return $content;
1402 }
1403
1404 }
1405