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

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

624 lines 24.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * ViewTrait_Shared methods.
9 */
10 trait ViewTrait_Shared {
11
12 /**
13 * Sanitize a GET or POST parameter.
14 * Delegates to Functions::getPostOrGetSanitize() when available,
15 * falls back to direct $_GET/$_POST read for test environments
16 * where the Functions mock may not have this method stubbed.
17 *
18 * @param string $name The parameter name.
19 * @param string|null $defaultValue Default value when not found.
20 * @return string
21 */
22 private function viewGetPostOrGetSanitize($name, $defaultValue = null) {
23 if (is_object($this->f)) {
24 try {
25 // DI resolver call: delegate to the injected Functions service
26 $result = $this->f->getPostOrGetSanitize($name, $defaultValue);
27 return is_string($result) ? $result : (is_scalar($result) ? (string)$result : '');
28 } catch (\Throwable $e) {
29 // allow-silent-catch: DI-injected service may not implement getPostOrGetSanitize.
30 // DI-injected service may not implement getPostOrGetSanitize
31 // (legacy mock). Fall through to inline GET/POST reader.
32 $val = null;
33 }
34 }
35 // Inline fallback for test contexts without the Functions mock expectation
36 $val = isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : null);
37 if ($val !== null && is_scalar($val)) {
38 return function_exists('sanitize_text_field') ? sanitize_text_field((string)$val) : (string)$val;
39 }
40 return is_string($defaultValue) ? $defaultValue : '';
41 }
42
43 /** Get the 'checked' attribute for a checkbox based on option value.
44 * @param array<string, mixed> $options The options array
45 * @param string $key The option key to check
46 * @return string Returns ' checked' if option is '1', empty string otherwise
47 */
48 private function getCheckedAttr($options, $key) {
49 return (array_key_exists($key, $options) && $options[$key] == '1') ? " checked" : "";
50 }
51
52 /** @return array<string, string> */
53 private function getFallbackOptionDefaults() {
54 return array(
55 'default_redirect' => '301',
56 'DB_VERSION' => defined('ABJ404_VERSION') ? ABJ404_VERSION : '',
57 'menuLocation' => 'optionsLevel',
58 'admin_theme' => 'default',
59 'capture_deletion' => '0',
60 'admin_notification' => '0',
61 'maximum_log_disk_usage' => '0',
62 'admin_notification_email' => '',
63 'suggest_cats' => '0',
64 'suggest_tags' => '0',
65 'update_suggest_url' => '0',
66 'suggest_max' => '5',
67 'suggest_title' => '',
68 'suggest_before' => '',
69 'suggest_after' => '',
70 'suggest_entrybefore' => '',
71 'suggest_entryafter' => '',
72 'suggest_noresults' => '',
73 'ignore_doprocess' => '',
74 'ignore_dontprocess' => '',
75 'recognized_post_types' => 'page',
76 'recognized_categories' => '',
77 'folders_files_ignore' => '',
78 'suggest_regex_exclusions' => '',
79 'plugin_admin_users' => '',
80 'auto_score' => '0',
81 'template_redirect_priority' => '9',
82 'days_wait_before_major_update' => '0',
83 'excludePages[]' => '',
84 // These are used by other option cards; harmless defaults.
85 'auto_deletion' => '0',
86 'manual_deletion' => '0',
87 );
88 }
89
90 /**
91 * @param array<string, mixed> $options
92 * @return array<string, mixed>
93 */
94 private function normalizeOptionsForView($options) {
95 return array_merge($this->getFallbackOptionDefaults(), $options);
96 }
97
98 /**
99 * Build the behavior tiles HTML for the 404 destination setting.
100 * Used by both simple and advanced mode.
101 *
102 * @param array<string, mixed> $options
103 * @return string
104 */
105 private function getBehaviorTilesHTML($options) {
106 $behavior = isset($options['dest404_behavior']) && is_string($options['dest404_behavior'])
107 ? $options['dest404_behavior'] : 'theme_default';
108
109 $userSelectedDefault404PageRaw = (array_key_exists('dest404page', $options) &&
110 isset($options['dest404page']) ? $options['dest404page'] : null);
111 $userSelectedDefault404Page = is_string($userSelectedDefault404PageRaw) ? $userSelectedDefault404PageRaw : '';
112 $urlDestinationRaw = (array_key_exists('dest404pageURL', $options) &&
113 isset($options['dest404pageURL']) ? $options['dest404pageURL'] : null);
114 $urlDestination = is_string($urlDestinationRaw) ? $urlDestinationRaw : '';
115
116 // Build the custom page dropdown for when 'custom' is selected
117 $pageTitle = $this->logic->getPageTitleFromIDAndType($userSelectedDefault404Page, $urlDestination);
118 $pageMissingWarning = "";
119 if ($behavior === 'custom' && $userSelectedDefault404Page !== '') {
120 $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($userSelectedDefault404Page, 0);
121 if (!in_array($permalink['status'], array('publish', 'published'))) {
122 $pageMissingWarning = __("(The specified page doesn't exist. Please update this setting.)", '404-solution');
123 }
124 }
125
126 $customDropdown = ABJ_404_Solution_Functions::readFileContents(__DIR__ .
127 "/html/addManualRedirectPageSearchDropdown.html");
128 $customDropdown = $this->f->str_replace('{redirect_to_label}', '', $customDropdown);
129 $customDropdown = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}',
130 __('(Type a page name or an external URL)', '404-solution'), $customDropdown);
131 $customDropdown = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}',
132 __('(A page has been selected.)', '404-solution'), $customDropdown);
133 $customDropdown = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}',
134 __('(A custom string has been entered.)', '404-solution'), $customDropdown);
135 $customDropdown = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}',
136 __('(An external URL will be used.)', '404-solution'), $customDropdown);
137 $customDropdown = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', $pageMissingWarning, $customDropdown);
138 $customDropdown = $this->f->str_replace('{redirectPageTitle}', esc_attr($pageTitle), $customDropdown);
139 $customDropdown = $this->f->str_replace('{pageIDAndType}', esc_attr($userSelectedDefault404Page), $customDropdown);
140 $customDropdown = $this->f->str_replace('{data-url}',
141 "admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $customDropdown);
142 $customDropdown = $this->f->doNormalReplacements($customDropdown);
143
144 // Build tiles template
145 $html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/behaviorTiles.html");
146
147 $behaviors = array('suggest', 'homepage', 'custom', 'theme_default');
148 foreach ($behaviors as $b) {
149 $key = str_replace('_', '_', $b); // just to be explicit
150 $isSelected = ($behavior === $b);
151 $html = $this->f->str_replace('{tile_' . $key . '_selected}', $isSelected ? ' selected' : '', $html);
152 $html = $this->f->str_replace('{' . $key . '_aria_checked}', $isSelected ? 'true' : 'false', $html);
153 }
154
155 $html = $this->f->str_replace('{selected_behavior}', esc_attr($behavior), $html);
156 $html = $this->f->str_replace('{pageIDAndType}', esc_attr($userSelectedDefault404Page), $html);
157 $html = $this->f->str_replace('{custom_picker_display}', $behavior === 'custom' ? '' : 'none', $html);
158 $html = $this->f->str_replace('{customPageDropdown}', $customDropdown, $html);
159
160 // Translations
161 $html = $this->f->str_replace('{Recommended}', __('Recommended', '404-solution'), $html);
162 $html = $this->f->str_replace('{Suggest similar pages}', __('Suggest similar pages', '404-solution'), $html);
163 $html = $this->f->str_replace('{Shows visitors a list of pages matching the URL they were looking for}',
164 __('Shows visitors a list of pages matching the URL they were looking for', '404-solution'), $html);
165 $html = $this->f->str_replace('{Redirect to homepage}', __('Redirect to homepage', '404-solution'), $html);
166 $html = $this->f->str_replace('{Sends all 404 visitors to the site front page}',
167 __('Sends all 404 visitors to the site front page', '404-solution'), $html);
168 $html = $this->f->str_replace('{Custom page}', __('Custom page', '404-solution'), $html);
169 $html = $this->f->str_replace('{Choose a specific page to show for all 404 errors}',
170 __('Choose a specific page to show for all 404 errors', '404-solution'), $html);
171 $html = $this->f->str_replace('{Theme default}', __('Theme default', '404-solution'), $html);
172 $html = $this->f->str_replace('{Uses the theme built-in 404 page, no redirect}',
173 __('Uses the theme built-in 404 page, no redirect', '404-solution'), $html);
174 $html = $this->f->str_replace('{Select a page}', __('Select a page', '404-solution'), $html);
175
176 return $html;
177 }
178
179 /**
180 * Safely extract a string value from an options array.
181 *
182 * @param array<string, mixed> $options
183 * @param string $key
184 * @param string $default
185 * @return string
186 */
187 private function optStr($options, $key, $default = '') {
188 if (!array_key_exists($key, $options)) {
189 return $default;
190 }
191 $val = $options[$key];
192 if (is_string($val)) {
193 return $val;
194 }
195 if (is_scalar($val)) {
196 return (string)$val;
197 }
198 return $default;
199 }
200
201 /**
202 * Normalize a scalar value for table signature comparisons.
203 *
204 * @param mixed $value
205 * @return string
206 */
207 private function normalizeSignatureValue($value) {
208 if ($value === null) {
209 return '';
210 }
211 if (is_bool($value)) {
212 return $value ? '1' : '0';
213 }
214 if (is_int($value) || is_float($value)) {
215 return (string)$value;
216 }
217 if (is_array($value)) {
218 $value = implode(',', array_map(array($this, 'normalizeSignatureValue'), $value));
219 }
220 $text = is_scalar($value) ? (string)$value : '';
221 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
222 $text = preg_replace('/\s+/', ' ', $text);
223 return trim((string)$text);
224 }
225
226 /**
227 * Build a deterministic row signature payload for a specific admin list subpage.
228 *
229 * @param string $sub
230 * @param array<string, mixed> $row
231 * @return array<string,string>
232 */
233 private function getSignatureFieldsForSubpage($sub, $row) {
234 $sub = (string)$sub;
235
236 if ($sub === 'abj404_redirects') {
237 return array(
238 'id' => $this->normalizeSignatureValue($row['id'] ?? ''),
239 'url' => $this->normalizeSignatureValue($row['url'] ?? ''),
240 'status' => $this->normalizeSignatureValue($row['status'] ?? ''),
241 'type' => $this->normalizeSignatureValue($row['type'] ?? ''),
242 'final_dest' => $this->normalizeSignatureValue($row['final_dest'] ?? ''),
243 'dest_for_view' => $this->normalizeSignatureValue($row['dest_for_view'] ?? ''),
244 'code' => $this->normalizeSignatureValue($row['code'] ?? ''),
245 'logshits' => $this->normalizeSignatureValue($row['logshits'] ?? 0),
246 'timestamp' => $this->normalizeSignatureValue($row['timestamp'] ?? 0),
247 'last_used' => $this->normalizeSignatureValue($row['last_used'] ?? 0),
248 );
249 }
250
251 if ($sub === 'abj404_captured') {
252 $hits = array_key_exists('logshits', $row) ? $row['logshits'] : ($row['hit_count'] ?? 0);
253 $timestamp = array_key_exists('timestamp', $row) ? $row['timestamp'] : ($row['created'] ?? 0);
254 return array(
255 'id' => $this->normalizeSignatureValue($row['id'] ?? ''),
256 'url' => $this->normalizeSignatureValue($row['url'] ?? ''),
257 'status' => $this->normalizeSignatureValue($row['status'] ?? ''),
258 'logshits' => $this->normalizeSignatureValue($hits),
259 'timestamp' => $this->normalizeSignatureValue($timestamp),
260 'last_used' => $this->normalizeSignatureValue($row['last_used'] ?? 0),
261 );
262 }
263
264 if ($sub === 'abj404_logs') {
265 return array(
266 'id' => $this->normalizeSignatureValue($row['id'] ?? ''),
267 'url' => $this->normalizeSignatureValue($row['url'] ?? ''),
268 'url_detail' => $this->normalizeSignatureValue($row['url_detail'] ?? ''),
269 'remote_host' => $this->normalizeSignatureValue($row['remote_host'] ?? ''),
270 'referrer' => $this->normalizeSignatureValue($row['referrer'] ?? ''),
271 'action' => $this->normalizeSignatureValue($row['action'] ?? ''),
272 'timestamp' => $this->normalizeSignatureValue($row['timestamp'] ?? 0),
273 'username' => $this->normalizeSignatureValue($row['username'] ?? ''),
274 );
275 }
276
277 $normalized = array();
278 foreach ($row as $k => $v) {
279 if (is_scalar($v) || is_array($v) || $v === null) {
280 $normalized[(string)$k] = $this->normalizeSignatureValue($v);
281 }
282 }
283 ksort($normalized);
284 return $normalized;
285 }
286
287 /**
288 * Compute and remember a deterministic table signature for detect-only refresh checks.
289 *
290 * @param string $sub
291 * @param array<int, array<string, mixed>> $rows
292 * @return void
293 */
294 private function rememberTableDataSignature($sub, $rows) {
295 $sub = (string)$sub;
296 if (!is_array($rows)) {
297 $this->tableDataSignatures[$sub] = sha1($sub . '|0');
298 return;
299 }
300
301 $rowSignatures = array();
302 foreach ($rows as $row) {
303 $fields = $this->getSignatureFieldsForSubpage($sub, $row);
304 $parts = array();
305 foreach ($fields as $k => $v) {
306 $parts[] = $k . '=' . $v;
307 }
308 $rowSignatures[] = implode("\x1f", $parts);
309 }
310 sort($rowSignatures, SORT_STRING);
311 $payload = $sub . '|' . count($rowSignatures) . '|' . implode("\n", $rowSignatures);
312 $this->tableDataSignatures[$sub] = sha1($payload);
313 }
314
315 /**
316 * Get the most recently computed table data signature for a subpage.
317 *
318 * @param string $sub
319 * @return string
320 */
321 public function getCurrentTableDataSignature($sub) {
322 $sub = (string)$sub;
323 return (string)($this->tableDataSignatures[$sub] ?? '');
324 }
325
326 /**
327 * Get plugin options merged with defaults.
328 *
329 * Some tests use partial/mocked PluginLogic instances; this method must not
330 * assume getDefaultOptions() is available or safe to call.
331 *
332 * @return array<string, mixed>
333 */
334 private function getOptionsWithDefaults() {
335 $options = $this->logic->getOptions();
336 if (!is_array($options)) {
337 $options = array();
338 }
339
340 $defaults = array();
341 if (is_object($this->logic) && method_exists($this->logic, 'getDefaultOptions')) {
342 try {
343 $defaults = $this->logic->getDefaultOptions();
344 } catch (Throwable $e) { // allow-silent-catch: getDefaultOptions() is best-effort; empty array merges with getFallbackOptionDefaults() below
345 $defaults = array();
346 }
347 }
348
349 $defaults = is_array($defaults) && !empty($defaults)
350 ? array_merge($this->getFallbackOptionDefaults(), $defaults)
351 : $this->getFallbackOptionDefaults();
352
353 $options = array_merge($defaults, $options);
354
355 return $options;
356 }
357
358 /**
359 * Get the tooltip HTML for Hits/Last Used columns when those values may lag.
360 *
361 * We only show this for views sorted by hits/last_used because those modes may
362 * rely on the aggregated logs-hits table. Other sorts use live per-row lookup,
363 * so showing an aggregation timestamp there is misleading.
364 *
365 * @param array<string, mixed> $tableOptions Current table options.
366 * @return string Tooltip HTML (not escaped - contains data attributes)
367 */
368 private function getHitsColumnTooltip($tableOptions = array()) {
369 $rawOrderby = $tableOptions['orderby'] ?? '';
370 $orderby = strtolower(is_string($rawOrderby) ? $rawOrderby : '');
371 $isAggregatedMode = ($orderby === 'logshits' || $orderby === 'last_used');
372 if (!$isAggregatedMode) {
373 return '';
374 }
375
376 // Ensure the "last checked"/"refresh scheduled" tooltip state is computed for this request.
377 // This runs cheap checks and (when needed) schedules the expensive rebuild for shutdown.
378 if (is_object($this->viewReadService) && method_exists($this->viewReadService, 'maybeUpdateRedirectsForViewHitsTable')) {
379 $this->viewReadService->maybeUpdateRedirectsForViewHitsTable();
380 }
381
382 $timestamp = $this->logsRepository->getLogsHitsTableLastUpdated();
383 $lines = array();
384 if ($timestamp !== null) {
385 $lastUpdated = $this->logsRepository->getLogsHitsTableLastUpdatedHuman();
386 $timeHtml = '<span class="abj404-time-ago" data-timestamp="' . esc_attr((string)$timestamp) . '">' . esc_html($lastUpdated) . '</span>';
387 $lines[] = sprintf(__('Last updated: %s', '404-solution'), $timeHtml);
388 }
389
390 $checkedAt = $this->logsRepository->getLogsHitsTableLastCheckedAt();
391 if ($checkedAt !== null) {
392 $checkedHtml = '<span class="abj404-time-ago" data-timestamp="' . esc_attr((string)$checkedAt) . '">' . esc_html($this->formatTimeAgo($checkedAt)) . '</span>';
393 $lines[] = sprintf(__('Last checked: %s', '404-solution'), $checkedHtml);
394 }
395
396 $decision = $this->logsRepository->getLogsHitsTableLastDecision();
397 // Treat "cooldown" as "scheduled recently" from a user perspective.
398 if ($decision === 'scheduled' || $decision === 'cooldown') {
399 $lines[] = __('Refresh scheduled', '404-solution');
400 } else if ($decision === 'running') {
401 $lines[] = __('Refresh running', '404-solution');
402 } else if ($decision === 'paused') {
403 $lines[] = __('Refresh paused', '404-solution');
404 }
405
406 return implode('<br>', array_filter($lines));
407 }
408
409 /**
410 * Small, dependency-free time-ago formatter for tooltip use.
411 * (We don't want to rely on WP human_time_diff() in unit tests.)
412 *
413 * @param int $timestamp
414 * @return string
415 */
416 private function formatTimeAgo($timestamp) {
417 $diff = time() - absint($timestamp);
418 if ($diff < 60) {
419 return __('Just now', '404-solution');
420 }
421 if ($diff < 3600) {
422 $minutes = (int)floor($diff / 60);
423 return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, '404-solution'), $minutes);
424 }
425 if ($diff < 86400) {
426 $hours = (int)floor($diff / 3600);
427 return sprintf(_n('%d hour ago', '%d hours ago', $hours, '404-solution'), $hours);
428 }
429 $days = (int)floor($diff / 86400);
430 return sprintf(_n('%d day ago', '%d days ago', $days, '404-solution'), $days);
431 }
432
433 /**
434 * Build shared sort state for table headers.
435 *
436 * @param array<string, mixed> $tableOptions
437 * @param string $orderby
438 * @param bool $preferDescOnFirstClick
439 * @return array{isSortable:bool,thClass:string,nextOrder:string,indicator:string}
440 */
441 private function getHeaderSortState($tableOptions, $orderby, $preferDescOnFirstClick = false) {
442 $result = array(
443 'isSortable' => false,
444 'thClass' => '',
445 'nextOrder' => 'ASC',
446 'indicator' => '',
447 );
448
449 $orderby = (string)$orderby;
450 if ($orderby === '') {
451 return $result;
452 }
453
454 $result['isSortable'] = true;
455 $rawCurrentOrderby = $tableOptions['orderby'] ?? '';
456 $currentOrderby = is_string($rawCurrentOrderby) ? $rawCurrentOrderby : '';
457 $rawCurrentOrder = $tableOptions['order'] ?? 'ASC';
458 $currentOrder = strtoupper(is_string($rawCurrentOrder) ? $rawCurrentOrder : 'ASC');
459 if ($currentOrder !== 'DESC') {
460 $currentOrder = 'ASC';
461 }
462
463 if ($currentOrderby === $orderby) {
464 $result['thClass'] = 'sorted ' . strtolower($currentOrder);
465 $result['nextOrder'] = ($currentOrder === 'ASC') ? 'DESC' : 'ASC';
466 $result['indicator'] = ($currentOrder === 'ASC') ? ' ↑' : ' ↓';
467 return $result;
468 }
469
470 $result['thClass'] = 'sortable ' . ($preferDescOnFirstClick ? 'asc' : 'desc');
471 $result['nextOrder'] = $preferDescOnFirstClick ? 'DESC' : 'ASC';
472 return $result;
473 }
474
475 /**
476 * Build action links for table rows (edit, logs, trash, delete, etc.)
477 *
478 * @param array<string, mixed> $row The data row from the database
479 * @param string $sub The subpage parameter value
480 * @param array<string, mixed> $tableOptions Table options including filter, orderby, order
481 * @param bool $isCapturedPage True for captured URLs page, false for redirects page
482 * @return array<string, string> Array of links and titles
483 */
484 protected function buildTableActionLinks($row, $sub, $tableOptions, $isCapturedPage = false) {
485 $result = [];
486
487 // Sanitize $sub for safe use in URLs (prevents XSS via quote injection)
488 $sub = rawurlencode($sub);
489
490 // ID handling differs between pages
491 $rawId = $row['id'] ?? 0;
492 $rawLogsId = $row['logsid'] ?? 0;
493 if ($isCapturedPage) {
494 // Captured page uses raw ID for most links
495 $id = $rawId;
496 $logsId = $rawLogsId;
497 } else {
498 // Redirects page uses absint for all IDs
499 $id = absint(is_scalar($rawId) ? $rawId : 0);
500 $logsId = absint(is_scalar($rawLogsId) ? $rawLogsId : 0);
501 }
502
503 // Build base links
504 $result['editlink'] = "?page=" . ABJ404_PP . "&subpage=abj404_edit&id=" . $id . "&source_page=" . $sub;
505 $result['logslink'] = "?page=" . ABJ404_PP . "&subpage=abj404_logs&id=" . $logsId;
506
507 if ($isCapturedPage) {
508 // Captured page - use the dynamic $sub parameter only once
509 $result['trashlink'] = "?page=" . ABJ404_PP . "&id=" . $id .
510 "&subpage=" . $sub;
511 $result['ajaxTrashLink'] = "admin-ajax.php?action=trashLink" . "&id=" . absint(is_scalar($rawId) ? $rawId : 0) .
512 "&subpage=" . $sub;
513 $result['deletelink'] = "?page=" . ABJ404_PP . "&remove=1&id=" . $id .
514 "&subpage=" . $sub;
515 } else {
516 // Redirects page does not have hardcoded subpage
517 $result['trashlink'] = "?page=" . ABJ404_PP . "&id=" . $id .
518 "&subpage=" . $sub;
519 $result['ajaxTrashLink'] = "admin-ajax.php?action=trashLink" . "&id=" . $id .
520 "&subpage=" . $sub;
521 $result['deletelink'] = "?page=" . ABJ404_PP . "&remove=1&id=" . $id .
522 "&subpage=" . $sub;
523 }
524
525 // Extract type-safe table option values
526 $toOrderby = is_array($tableOptions) && array_key_exists('orderby', $tableOptions) && is_string($tableOptions['orderby']) ? $tableOptions['orderby'] : '';
527 $toOrder = is_array($tableOptions) && array_key_exists('order', $tableOptions) && is_string($tableOptions['order']) ? $tableOptions['order'] : '';
528 $toFilter = is_array($tableOptions) && array_key_exists('filter', $tableOptions) ? $tableOptions['filter'] : 0;
529
530 // Trash/Restore title and action
531 if ($toFilter == ABJ404_TRASH_FILTER) {
532 $result['trashlink'] .= "&trash=0";
533 $result['ajaxTrashLink'] .= "&trash=0";
534 $result['trashtitle'] = __('Restore', '404-solution');
535 } else {
536 $result['trashlink'] .= "&trash=1";
537 $result['ajaxTrashLink'] .= "&trash=1";
538 $result['trashtitle'] = __('Trash', '404-solution');
539 }
540
541 // Captured page has ignore and later links
542 if ($isCapturedPage) {
543 $result['ignorelink'] = "?page=" . ABJ404_PP . "&id=" . $id .
544 "&subpage=" . $sub;
545 $result['laterlink'] = "?page=" . ABJ404_PP . "&id=" . $id .
546 "&subpage=" . $sub;
547
548 // Ignore title and action
549 $result['ignoretitle'] = "";
550 if ($toFilter == ABJ404_STATUS_IGNORED) {
551 $result['ignorelink'] .= "&ignore=0";
552 $result['ignoretitle'] = __('Remove Ignore Status', '404-solution');
553 } else {
554 $result['ignorelink'] .= "&ignore=1";
555 $result['ignoretitle'] = __('Ignore 404 Error', '404-solution');
556 }
557
558 // Later title and action
559 $result['latertitle'] = '?Organize Later?';
560 if ($toFilter == ABJ404_STATUS_LATER) {
561 $result['laterlink'] .= "&later=0";
562 $result['latertitle'] = __('Remove Later Status', '404-solution');
563 } else {
564 $result['laterlink'] .= "&later=1";
565 $result['latertitle'] = __('Organize Later', '404-solution');
566 }
567 }
568
569 // Add orderby/order parameters if not default
570 if ($toOrderby !== '' && $toOrder !== '') {
571 if (!($toOrderby == "url" && $toOrder == "ASC")) {
572 $result['trashlink'] .= "&orderby=" . sanitize_text_field($toOrderby) . "&order=" . sanitize_text_field($toOrder);
573 $result['deletelink'] .= "&orderby=" . sanitize_text_field($toOrderby) . "&order=" . sanitize_text_field($toOrder);
574
575 if ($isCapturedPage && array_key_exists('ignorelink', $result) && array_key_exists('laterlink', $result)) {
576 $result['ignorelink'] .= "&orderby=" . sanitize_text_field($toOrderby) . "&order=" . sanitize_text_field($toOrder);
577 $result['laterlink'] .= "&orderby=" . sanitize_text_field($toOrderby) . "&order=" . sanitize_text_field($toOrder);
578 }
579 }
580 }
581
582 // Add filter parameter if not zero
583 if ($toFilter != 0) {
584 $result['trashlink'] .= "&filter=" . $toFilter;
585 $result['deletelink'] .= "&filter=" . $toFilter;
586 $result['editlink'] .= "&filter=" . $toFilter;
587
588 if ($isCapturedPage && array_key_exists('ignorelink', $result) && array_key_exists('laterlink', $result)) {
589 $result['ignorelink'] .= "&filter=" . $toFilter;
590 $result['laterlink'] .= "&filter=" . $toFilter;
591 }
592 }
593
594 // Add orderby/order parameters to edit link
595 if ($toOrderby !== '' && $toOrder !== '') {
596 if (!($toOrderby == "url" && $toOrder == "ASC")) {
597 $result['editlink'] .= "&orderby=" . sanitize_text_field($toOrderby) . "&order=" . sanitize_text_field($toOrder);
598 }
599 }
600
601 // Add paged parameter to edit link if present
602 if (is_array($tableOptions) && array_key_exists('paged', $tableOptions) && $tableOptions['paged'] > 1) {
603 $result['editlink'] .= "&paged=" . $tableOptions['paged'];
604 }
605
606 // Apply nonces
607 $result['trashlink'] = wp_nonce_url($result['trashlink'], "abj404_trashRedirect");
608 $result['ajaxTrashLink'] = wp_nonce_url($result['ajaxTrashLink'], "abj404_ajaxTrash");
609
610 if ($toFilter == ABJ404_TRASH_FILTER) {
611 $result['deletelink'] = wp_nonce_url($result['deletelink'], "abj404_removeRedirect");
612 }
613
614 if ($isCapturedPage && array_key_exists('ignorelink', $result) && array_key_exists('laterlink', $result)) {
615 $result['ignorelink'] = wp_nonce_url($result['ignorelink'], "abj404_ignore404");
616 $result['laterlink'] = wp_nonce_url($result['laterlink'], "abj404_organizeLater");
617 }
618
619 return $result;
620 }
621
622
623 }
624