PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.6
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.6
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / redirections / class-metasync-redirection-list-table.php

class-metasync-redirection-list-table.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.6, at redirections/class-metasync-redirection-list-table.php

838 lines 25.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7
8 /**
9 * The Record list for the Redirections.
10 *
11 * @since 1.0.0
12 * @package Metasync
13 * @subpackage Metasync/redirections
14 * @author Engineering Team <[email protected]>
15 */
16 class Metasync_Redirection_List_Table extends WP_List_Table
17 {
18 private $records;
19 private $db_redirection;
20 private $_displayed = false;
21 private $_nav_displayed = false;
22 public function __construct()
23 {
24 // Set parent defaults.
25 parent::__construct(array(
26 'singular' => 'item', // Singular name of the listed records.
27 'plural' => 'items', // Plural name of the listed records.
28 'ajax' => false, // Does this table support ajax?
29 ));
30 }
31
32 /**
33 * Get the current page number for redirections (use separate parameter)
34 */
35 public function get_pagenum()
36 {
37 $pagenum = isset($_REQUEST['paged_redir']) ? absint($_REQUEST['paged_redir']) : 0;
38
39 if (isset($this->_pagination_args['total_pages']) && $pagenum > $this->_pagination_args['total_pages']) {
40 $pagenum = $this->_pagination_args['total_pages'];
41 }
42
43 return max(1, $pagenum);
44 }
45
46 /**
47 * Override pagination args to preserve tab parameter
48 */
49 protected function get_views()
50 {
51 return array();
52 }
53
54 /**
55 * Ensure tab parameter is preserved in pagination links
56 */
57 protected function pagination($which)
58 {
59 if (empty($this->_pagination_args)) {
60 return;
61 }
62
63 $total_items = $this->_pagination_args['total_items'];
64 $total_pages = $this->_pagination_args['total_pages'];
65 $infinite_scroll = false;
66 if (isset($this->_pagination_args['infinite_scroll'])) {
67 $infinite_scroll = $this->_pagination_args['infinite_scroll'];
68 }
69
70 if ('top' === $which && $total_pages > 1) {
71 $this->screen->render_screen_reader_content('heading_pagination');
72 }
73
74 // Add tab parameter to pagination links
75 add_filter('removable_query_args', array($this, 'preserve_tab_in_pagination'));
76
77 $output = '<span class="displaying-num">' . sprintf(
78 /* translators: %s: Number of items */
79 _n('%s item', '%s items', $total_items, 'metasync'),
80 number_format_i18n($total_items)
81 ) . '</span>';
82
83 $current = $this->get_pagenum();
84 $removable_query_args = wp_removable_query_args();
85
86 $current_url = set_url_scheme('http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
87
88 $current_url = remove_query_arg($removable_query_args, $current_url);
89
90 // Preserve the tab parameter
91 if (isset($_GET['tab'])) {
92 $current_url = add_query_arg('tab', sanitize_text_field($_GET['tab']), $current_url);
93 }
94
95 // If filter action was submitted, remove paged_redir to reset to page 1
96 if (isset($_POST['filter_action']) || isset($_POST['post-query-submit'])) {
97 $current_url = remove_query_arg('paged_redir', $current_url);
98 }
99
100 // Preserve filter parameters from REQUEST (works for both GET and POST)
101 // First, remove all filter parameters from URL, then add them back if they exist
102 $filter_params = ['status_filter', 'pattern_filter', 'http_code_filter', 's'];
103 foreach ($filter_params as $param) {
104 // Always remove first to clear old values
105 $current_url = remove_query_arg($param, $current_url);
106 // Then add back only if it has a value in REQUEST
107 if (!empty($_REQUEST[$param])) {
108 $current_url = add_query_arg($param, sanitize_text_field($_REQUEST[$param]), $current_url);
109 }
110 }
111
112 // Preserve sorting parameters (same approach - remove first, then add if exists)
113 $sort_params = ['orderby_redir', 'order_redir'];
114 foreach ($sort_params as $param) {
115 // Always remove first to clear old values
116 $current_url = remove_query_arg($param, $current_url);
117 // Then add back only if it has a value in REQUEST
118 if (!empty($_REQUEST[$param])) {
119 if ($param === 'orderby_redir') {
120 $current_url = add_query_arg($param, sanitize_sql_orderby($_REQUEST[$param]), $current_url);
121 } else {
122 $current_url = add_query_arg($param, sanitize_text_field($_REQUEST[$param]), $current_url);
123 }
124 }
125 }
126
127 $page_links = array();
128
129 $total_pages_before = '<span class="paging-input">';
130 $total_pages_after = '</span></span>';
131
132 $disable_first = $disable_last = $disable_prev = $disable_next = false;
133
134 if ($current == 1) {
135 $disable_first = true;
136 $disable_prev = true;
137 }
138 if ($current == 2) {
139 $disable_first = true;
140 }
141 if ($current == $total_pages) {
142 $disable_last = true;
143 $disable_next = true;
144 }
145 if ($current == $total_pages - 1) {
146 $disable_last = true;
147 }
148
149 if ($disable_first) {
150 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
151 } else {
152 $page_links[] = sprintf(
153 "<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
154 esc_url(remove_query_arg('paged_redir', $current_url)),
155 esc_html__('First page'),
156 '&laquo;'
157 );
158 }
159
160 if ($disable_prev) {
161 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
162 } else {
163 $page_links[] = sprintf(
164 "<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
165 esc_url(add_query_arg('paged_redir', max(1, $current - 1), $current_url)),
166 esc_html__('Previous page'),
167 '&lsaquo;'
168 );
169 }
170
171 if ('bottom' === $which) {
172 $html_current_page = $current;
173 $total_pages_before = '<span class="screen-reader-text">' . esc_html__('Current Page', 'metasync') . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">';
174 } else {
175 $html_current_page = sprintf(
176 "%s<input class='current-page' id='current-page-selector-redir' type='text' name='paged_redir' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
177 '<label for="current-page-selector-redir" class="screen-reader-text">' . esc_html__('Current Page', 'metasync') . '</label>',
178 $current,
179 strlen($total_pages)
180 );
181 }
182 $html_total_pages = sprintf("<span class='total-pages'>%s</span>", number_format_i18n($total_pages));
183 $page_links[] = $total_pages_before . sprintf(
184 /* translators: 1: Current page, 2: Total pages */
185 _x('%1$s of %2$s', 'paging', 'metasync'),
186 $html_current_page,
187 $html_total_pages
188 ) . $total_pages_after;
189
190 if ($disable_next) {
191 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
192 } else {
193 $page_links[] = sprintf(
194 "<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
195 esc_url(add_query_arg('paged_redir', min($total_pages, $current + 1), $current_url)),
196 esc_html__('Next page'),
197 '&rsaquo;'
198 );
199 }
200
201 if ($disable_last) {
202 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
203 } else {
204 $page_links[] = sprintf(
205 "<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
206 esc_url(add_query_arg('paged_redir', $total_pages, $current_url)),
207 esc_html__('Last page'),
208 '&raquo;'
209 );
210 }
211
212 $pagination_links_class = 'pagination-links';
213 if (!empty($infinite_scroll)) {
214 $pagination_links_class .= ' hide-if-js';
215 }
216 $output .= "\n<span class='$pagination_links_class'>" . join("\n", $page_links) . '</span>';
217
218 if ($total_pages) {
219 $page_class = $total_pages < 2 ? ' one-page' : '';
220 } else {
221 $page_class = ' no-pages';
222 }
223 $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";
224
225 echo $this->_pagination;
226
227 remove_filter('removable_query_args', array($this, 'preserve_tab_in_pagination'));
228 }
229
230 /**
231 * Preserve tab parameter in pagination
232 */
233 public function preserve_tab_in_pagination($args)
234 {
235 // Remove 'tab' from removable query args so it's preserved
236 $key = array_search('tab', $args, true);
237 if ($key !== false) {
238 unset($args[$key]);
239 }
240
241 // Also preserve our pagination parameter
242 $key = array_search('paged_redir', $args, true);
243 if ($key !== false) {
244 unset($args[$key]);
245 }
246
247 // Remove the other pagination parameters to avoid conflicts
248 $args[] = 'paged';
249 $args[] = 'paged_404';
250
251 return array_unique($args);
252 }
253
254 public function setDatabaseResource(&$db_redirection)
255 {
256 $this->db_redirection = $db_redirection;
257 }
258
259 private function setRecords($records)
260 {
261 return $this->records = json_decode(json_encode($records), true);
262 }
263
264 private function loadRecords()
265 {
266 $filters = $this->get_search_filters();
267 return $this->setRecords($this->db_redirection->search_redirections($filters));
268 }
269
270 private function loadRecordsWithPagination($per_page, $offset)
271 {
272 $filters = $this->get_search_filters();
273 $filters['per_page'] = $per_page;
274 $filters['offset'] = $offset;
275 return $this->setRecords($this->db_redirection->search_redirections($filters));
276 }
277
278 private function getTotalRecords()
279 {
280 $filters = $this->get_search_filters();
281 return $this->db_redirection->count_redirections($filters);
282 }
283
284 /**
285 * Get search filters from request
286 */
287 private function get_search_filters()
288 {
289 $filters = [];
290
291 if (!empty($_REQUEST['s'])) {
292 $filters['search'] = sanitize_text_field($_REQUEST['s']);
293 }
294
295 if (!empty($_REQUEST['status_filter'])) {
296 $filters['status'] = sanitize_text_field($_REQUEST['status_filter']);
297 }
298
299 if (!empty($_REQUEST['pattern_filter'])) {
300 $filters['pattern_type'] = sanitize_text_field($_REQUEST['pattern_filter']);
301 }
302
303 if (!empty($_REQUEST['http_code_filter'])) {
304 $filters['http_code'] = intval($_REQUEST['http_code_filter']);
305 }
306
307 // Use separate orderby/order parameters for redirections
308 if (!empty($_REQUEST['orderby_redir'])) {
309 $filters['order_by'] = sanitize_sql_orderby($_REQUEST['orderby_redir']);
310 }
311
312 if (!empty($_REQUEST['order_redir'])) {
313 $filters['order'] = sanitize_text_field($_REQUEST['order_redir']);
314 }
315
316 return $filters;
317 }
318
319 public function get_columns()
320 {
321 $columns = array(
322 'cb' => '<input type="checkbox" />', // Render a checkbox instead of text.
323 'sources_from' => _x('From', 'Column label', 'metasync'),
324 'url_redirect_to' => _x('To', 'Column label', 'metasync'),
325 'http_code' => _x('Type', 'Column label', 'metasync'),
326 'pattern_type' => _x('Pattern', 'Column label', 'metasync'),
327 'hits_count' => _x('Hits', 'Column label', 'metasync'),
328 'status' => _x('Status', 'Column label', 'metasync'),
329 'last_accessed_at' => _x('Last Accessed', 'Column label', 'metasync'),
330 );
331
332 return $columns;
333 }
334
335 protected function get_sortable_columns()
336 {
337 $sortable_columns = array(
338 'sources_from' => array('sources_from', false),
339 'url_redirect_to' => array('url_redirect_to', false),
340 'http_code' => array('http_code', false),
341 'pattern_type' => array('pattern_type', false),
342 'hits_count' => array('hits_count', false),
343 'status' => array('status', false),
344 'last_accessed_at' => array('last_accessed_at', false),
345 );
346
347 return $sortable_columns;
348 }
349
350 /**
351 * Override to use custom orderby/order parameter names
352 */
353 protected function get_orderby()
354 {
355 return isset($_REQUEST['orderby_redir']) ? $_REQUEST['orderby_redir'] : '';
356 }
357
358 /**
359 * Override to use custom orderby/order parameter names
360 */
361 protected function get_order()
362 {
363 return isset($_REQUEST['order_redir']) ? $_REQUEST['order_redir'] : '';
364 }
365
366 /**
367 * Override column headers to use custom parameter names for sorting
368 */
369 public function print_column_headers($with_id = true)
370 {
371 list($columns, $hidden, $sortable, $primary) = $this->get_column_info();
372
373 $current_url = set_url_scheme('http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
374 $current_url = remove_query_arg(array('paged_redir', 'orderby_redir', 'order_redir'), $current_url);
375
376 // Preserve tab parameter - force redirections tab
377 $current_url = add_query_arg('tab', 'redirections', $current_url);
378
379 $current_orderby = $this->get_orderby();
380 $current_order = $this->get_order();
381
382 foreach ($columns as $column_key => $column_display_name) {
383 $class = array('manage-column', "column-$column_key");
384
385 if (in_array($column_key, $hidden, true)) {
386 $class[] = 'hidden';
387 }
388
389 if ($column_key === $primary) {
390 $class[] = 'column-primary';
391 }
392
393 // Add check-column class for checkbox column
394 if ('cb' === $column_key) {
395 $class[] = 'check-column';
396 }
397
398 if (isset($sortable[$column_key])) {
399 list($orderby, $desc_first) = $sortable[$column_key];
400
401 if ($current_orderby === $orderby) {
402 $order = 'asc' === $current_order ? 'desc' : 'asc';
403 $class[] = 'sorted';
404 $class[] = $current_order;
405 } else {
406 $order = strtolower($desc_first);
407 if (!in_array($order, array('desc', 'asc'), true)) {
408 $order = $desc_first ? 'desc' : 'asc';
409 }
410 $class[] = 'sortable';
411 $class[] = 'desc' === $order ? 'asc' : 'desc';
412 }
413
414 $column_display_name = sprintf(
415 '<a href="%s"><span>%s</span><span class="sorting-indicators"></span></a>',
416 esc_url(add_query_arg(array('orderby_redir' => $orderby, 'order_redir' => $order), $current_url)),
417 $column_display_name
418 );
419 }
420
421 $tag = ('cb' === $column_key) ? 'td' : 'th';
422 $scope = ('th' === $tag) ? 'scope="col"' : '';
423 $id = $with_id ? "id='$column_key'" : '';
424
425 if (!empty($class)) {
426 $class = "class='" . implode(' ', $class) . "'";
427 }
428
429 echo "<$tag $scope $id $class>$column_display_name</$tag>";
430 }
431 }
432
433 protected function column_default($item, $column_name)
434 {
435 switch ($column_name) {
436 case 'sources_from':
437 case 'url_redirect_to':
438 case 'http_code':
439 case 'hits_count':
440 case 'status':
441 case 'last_accessed_at':
442 return esc_html($item[$column_name]); # Fixed: Added esc_html() to prevent XSS
443 case 'pattern_type':
444 return $this->column_pattern_type($item);
445 default:
446 # return print_r($item, true); // Show the whole array for troubleshooting purposes.
447 return esc_html(print_r($item, true)); # Fixed: Added esc_html() to prevent XSS in debug output
448 }
449 }
450
451 protected function column_cb($item)
452 {
453 return sprintf(
454 '<input type="checkbox" name="%1$s[]" value="%2$s" />',
455 $this->_args['plural'], // Let's simply repurpose the table's singular label ("error").
456 $item['id'] // The value of the checkbox should be the record's ID.
457 );
458 }
459
460 protected function column_sources_from($item)
461 {
462 $request_data = metasync_sanitize_input_array($_REQUEST); // WPCS: Input var ok.
463 if (!isset($request_data['page'])) return;
464
465 // Build delete row action.
466 $delete_query_args = array(
467 'page' => sanitize_text_field($request_data['page']),
468 'action' => 'delete',
469 'id' => $item['id'],
470 );
471
472 // Build edit row action.
473 $edit_query_args = array(
474 'page' => sanitize_text_field($request_data['page']),
475 'action' => 'edit',
476 'id' => $item['id'],
477 );
478
479 // Build activate row action.
480 $activate_query_args = array(
481 'page' => sanitize_text_field($request_data['page']),
482 'action' => 'activate',
483 'id' => $item['id'],
484 );
485
486 // Build deactivate row action.
487 $deactivate_query_args = array(
488 'page' => sanitize_text_field($request_data['page']),
489 'action' => 'deactivate',
490 'id' => $item['id'],
491 );
492
493 $actions['edit'] = sprintf(
494 '<a href="%1$s">%2$s</a>',
495 esc_url(add_query_arg($edit_query_args, 'admin.php')),
496 _x('Edit', 'List table row action', 'metasync')
497 );
498
499 if ($item['status'] === 'inactive') {
500
501 $actions['activate'] = sprintf(
502 '<a href="%1$s">%2$s</a>',
503 esc_url(add_query_arg($activate_query_args, 'admin.php')),
504 _x('Activate', 'List table row action', 'metasync')
505 );
506 } else {
507
508 $actions['deactivate'] = sprintf(
509 '<a href="%1$s">%2$s</a>',
510 esc_url(add_query_arg($deactivate_query_args, 'admin.php')),
511 _x('Deactivate', 'List table row action', 'metasync')
512 );
513 }
514
515 $actions['delete'] = sprintf(
516 '<a href="%1$s">%2$s</a>',
517 esc_url(add_query_arg($delete_query_args, 'admin.php')),
518 _x('Delete', 'List table row action', 'metasync')
519 );
520
521 // Return the source from contents.
522 return sprintf(
523 '%1$s %2$s',
524 $this->show_source_from_urls($item),
525 $this->row_actions($actions)
526 );
527 }
528
529 /**
530 * Display pattern type column with user-friendly labels
531 */
532 protected function column_pattern_type($item)
533 {
534 $pattern_type = isset($item['pattern_type']) ? $item['pattern_type'] : 'exact';
535 $regex_pattern = isset($item['regex_pattern']) ? $item['regex_pattern'] : '';
536
537 // Pattern type labels
538 $labels = [
539 'exact' => 'Exact Match',
540 'contain' => 'Contains',
541 'start' => 'Starts With',
542 'end' => 'Ends With',
543 'regex' => 'Regex Pattern'
544 ];
545
546 $label = isset($labels[$pattern_type]) ? $labels[$pattern_type] : ucfirst($pattern_type);
547
548 // Add regex pattern as tooltip if available
549 if ($pattern_type === 'regex' && !empty($regex_pattern)) {
550 // Validate regex pattern
551 // Check if pattern has proper delimiters (same character at start and end)
552 $test_pattern = $regex_pattern;
553 $first_char = substr($test_pattern, 0, 1);
554 $has_valid_delimiter = false;
555
556 // Check if it looks like a properly delimited regex
557 if (in_array($first_char, ['/', '#', '~', '%', '@', '!'], true)) {
558 // Find the last occurrence of the delimiter (accounting for flags after it)
559 if (preg_match('/^' . preg_quote($first_char, '/') . '.*' . preg_quote($first_char, '/') . '[imsxuADU]*$/', $test_pattern)) {
560 $has_valid_delimiter = true;
561 }
562 }
563
564 // Add delimiters if missing - use # to avoid conflicts with / in URL patterns
565 if (!$has_valid_delimiter) {
566 $test_pattern = '#' . str_replace('#', '\\#', $regex_pattern) . '#';
567 }
568 $is_valid = @preg_match($test_pattern, '');
569
570 if ($is_valid === false) {
571 // Invalid regex - show red warning icon
572 return sprintf(
573 '<span style="display: flex; align-items: center; gap: 5px;">
574 <span class="dashicons dashicons-warning" style="color: #dc3232; font-size: 18px;" title="Invalid Regex Pattern: %s"></span>
575 <span title="Invalid regex pattern">%s</span>
576 </span>',
577 esc_attr($regex_pattern),
578 esc_html($label)
579 );
580 }
581
582 // Valid regex - show with tooltip
583 return sprintf(
584 '<span title="%s">%s</span>',
585 esc_attr($regex_pattern),
586 esc_html($label)
587 );
588 }
589
590 return esc_html($label);
591 }
592
593 protected function get_bulk_actions()
594 {
595 $actions = array(
596 'delete_bulk' => _x('Delete', 'List table bulk action', 'metasync'),
597 'activate_bulk' => _x('Activate', 'List table bulk action', 'metasync'),
598 'deactivate_bulk' => _x('Deactivate', 'List table bulk action', 'metasync'),
599 );
600
601 return $actions;
602 }
603
604 protected function process_bulk_action()
605 {
606 $post_data = metasync_sanitize_input_array($_POST);
607 $items = isset($post_data['items']) && is_array($post_data['items']) ? array_map('sanitize_title', $post_data['items']) : [];
608
609 if (empty($post_data['items'])) return;
610
611
612 // Detect when bulk delete action is being triggered.
613 if ('delete_bulk' === $this->current_action()) {
614 $this->db_redirection->delete($items);
615 }
616
617 // Detect when bulk activate action is being triggered.
618 if ('activate_bulk' === $this->current_action()) {
619 $this->db_redirection->update_status($items, 'active');
620 }
621
622 // Detect when bulk deactivate action is being triggered.
623 if ('deactivate_bulk' === $this->current_action()) {
624 $this->db_redirection->update_status($items, 'inactive');
625 }
626 }
627
628 protected function process_row_action()
629 {
630 $get_data = metasync_sanitize_input_array($_GET);
631 $item = isset($get_data['id']) ? sanitize_text_field($get_data['id']) : '';
632
633 // Detect when a delete action is being triggered.
634 if ('delete' === $this->current_action()) {
635 $this->db_redirection->delete([$item]);
636 }
637 // Detect when a activate action is being triggered.
638 if ('activate' === $this->current_action()) {
639 $this->db_redirection->update_status([$item], 'active');
640 }
641 // Detect when a deactivate action is being triggered.
642 if ('deactivate' === $this->current_action()) {
643 $this->db_redirection->update_status([$item], 'inactive');
644 }
645 }
646
647 function prepare_items()
648 {
649 $per_page = 10;
650
651 $columns = $this->get_columns();
652 $hidden = array();
653 $sortable = $this->get_sortable_columns();
654 $this->_column_headers = array($columns, $hidden, $sortable);
655
656 $this->process_bulk_action();
657 $this->process_row_action();
658
659 $current_page = $this->get_pagenum();
660 $offset = ($current_page - 1) * $per_page;
661
662 // Get total count for pagination
663 $total_items = $this->getTotalRecords();
664
665 // Load only the records for the current page (already sorted by database)
666 $data = $this->loadRecordsWithPagination($per_page, $offset);
667
668 $this->items = $data;
669
670 $this->set_pagination_args(array(
671 'total_items' => $total_items, // Total number of items from database
672 'per_page' => $per_page, // Items per page
673 'total_pages' => ceil($total_items / $per_page), // Total number of pages
674 ));
675 }
676
677 /**
678 * Override display method to prevent duplicate table rendering
679 */
680 public function display()
681 {
682 // Only display the table once
683 if (isset($this->_displayed) && $this->_displayed) {
684 return;
685 }
686
687 $this->_displayed = true;
688
689 // Call parent display method
690 parent::display();
691 }
692
693 /**
694 * Override display_tablenav to prevent duplicate navigation
695 */
696 protected function display_tablenav($which)
697 {
698 // Only display navigation once
699 if (isset($this->_nav_displayed) && $this->_nav_displayed) {
700 return;
701 }
702
703 if ($which === 'top') {
704 $this->_nav_displayed = true;
705 }
706
707 parent::display_tablenav($which);
708 }
709
710 private function show_source_from_urls($item)
711 {
712 # $source_urls = unserialize($item['sources_from']);
713 # Unserialize the sources_from field
714 $source_urls = unserialize($item['sources_from'], ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
715
716 # If unserialize failed or returned non-array, try to handle it
717 if (!is_array($source_urls)) {
718 # Fallback: display raw value (shouldn't happen normally)
719 echo esc_html($item['sources_from']);
720 return;
721 }
722
723 $pattern_type = isset($item['pattern_type']) ? $item['pattern_type'] : 'exact';
724 $regex_pattern = isset($item['regex_pattern']) ? $item['regex_pattern'] : '';
725
726 foreach ($source_urls as $source_name => $source_type) {
727
728 # Clean the source name in case it contains serialized data
729 $clean_source = $source_name;
730
731 # Check if source_name itself is serialized (edge case)
732 if (is_string($source_name) && strpos($source_name, 'a:') === 0) {
733 $unserialized = unserialize($source_name, ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
734 if (is_array($unserialized) && isset($unserialized[0]['pattern'])) {
735 $clean_source = $unserialized[0]['pattern'];
736 }
737 }
738
739 // For regex patterns, show the regex pattern instead of the label
740 if ($pattern_type === 'regex' && !empty($regex_pattern)) {
741 $display_text = $regex_pattern;
742 // Build the full URL for the link (use the actual pattern for regex)
743 $full_url = '#'; // No direct link for regex patterns
744
745 // Validate regex pattern
746 // Add delimiters if missing
747 $test_pattern = $regex_pattern;
748 if (!preg_match('/^[\/#~%@]/', $test_pattern)) {
749 $test_pattern = '/' . $test_pattern . '/';
750 }
751 $is_valid = @preg_match($test_pattern, '');
752 $warning_icon = '';
753
754 if ($is_valid === false) {
755 // Invalid regex - add red warning icon
756 $warning_icon = '<span class="dashicons dashicons-warning" style="color: #dc3232; font-size: 16px; vertical-align: middle; margin-right: 3px;" title="Invalid Regex Pattern"></span>';
757 }
758
759 echo sprintf(
760 '<span title="Label: %3$s">%1$s%2$s</span>',
761 $warning_icon,
762 esc_html($display_text),
763 esc_attr($clean_source)
764 );
765 } else {
766 // Build the full URL for the link
767 # $full_url = $this->build_full_url($source_name);
768 $full_url = $this->build_full_url($clean_source);
769
770 echo sprintf(
771 '<a href="%1$s" target="_blank">%2$s</a>',
772 esc_url($full_url),
773 # esc_html(_x($source_name, 'List table row action', 'metasync')) # Fixed: Added esc_html() to prevent XSS
774 esc_html($clean_source)
775 );
776 }
777 echo sprintf(
778 # '<span>%1$s</span>',
779 # ' [' . $source_type . ']'
780 # ' [' . esc_html($source_type) . ']' # Fixed: Added esc_html() to prevent XSS
781 '<span style="color: #666; font-size: 0.9em;">%1$s</span>',
782 ' [' . esc_html($source_type) . ']'
783 );
784 echo "<br>";
785 }
786 }
787
788 /**
789 * Build full URL from source name, handling different formats
790 */
791 private function build_full_url($source_name)
792 {
793 // If it's already a full URL, return as-is
794 if (strpos($source_name, 'http') === 0) {
795 return $source_name;
796 }
797
798 // If it starts with /, it's an absolute path
799 if (strpos($source_name, '/') === 0) {
800 return site_url() . $source_name;
801 }
802
803 // Otherwise, it's a relative path
804 return site_url() . '/' . $source_name;
805 }
806
807
808 /**
809 * Display HTTP code column with better formatting
810 */
811 protected function column_http_code($item)
812 {
813 $http_code = $item['http_code'];
814 $code_labels = [
815 '301' => '301 Permanent',
816 '302' => '302 Temporary',
817 '307' => '307 Temporary',
818 '410' => '410 Gone',
819 '451' => '451 Unavailable'
820 ];
821
822 $label = isset($code_labels[$http_code]) ? $code_labels[$http_code] : $http_code;
823
824 $class = '';
825 if ($http_code >= 300 && $http_code < 400) {
826 $class = 'color: #0073aa;';
827 } elseif ($http_code >= 400) {
828 $class = 'color: #d63638;';
829 }
830
831 return sprintf(
832 '<span style="%s">%s</span>',
833 esc_attr($class),
834 esc_html($label)
835 );
836 }
837 }
838