PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.7
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.7
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.7, at redirections/class-metasync-redirection-list-table.php

843 lines 25.5 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']) ? sanitize_key($_REQUEST['orderby_redir']) : '';
356 }
357
358 /**
359 * Override to use custom orderby/order parameter names
360 */
361 protected function get_order()
362 {
363 $raw = isset($_REQUEST['order_redir']) ? strtolower(sanitize_key($_REQUEST['order_redir'])) : '';
364 if ($raw === 'desc') {
365 return 'desc';
366 }
367 return 'asc';
368 }
369
370 /**
371 * Override column headers to use custom parameter names for sorting
372 */
373 public function print_column_headers($with_id = true)
374 {
375 list($columns, $hidden, $sortable, $primary) = $this->get_column_info();
376
377 $current_page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : Metasync_Admin::$page_slug;
378 $current_url = admin_url('admin.php');
379 $current_url = add_query_arg('page', $current_page, $current_url);
380 $current_url = add_query_arg('tab', 'redirections', $current_url);
381
382 $current_orderby = $this->get_orderby();
383 $current_order = $this->get_order();
384
385 foreach ($columns as $column_key => $column_display_name) {
386 $class = array('manage-column', "column-$column_key");
387
388 if (in_array($column_key, $hidden, true)) {
389 $class[] = 'hidden';
390 }
391
392 if ($column_key === $primary) {
393 $class[] = 'column-primary';
394 }
395
396 // Add check-column class for checkbox column
397 if ('cb' === $column_key) {
398 $class[] = 'check-column';
399 }
400
401 if (isset($sortable[$column_key])) {
402 list($orderby, $desc_first) = $sortable[$column_key];
403
404 if ($current_orderby === $orderby) {
405 $order = 'asc' === $current_order ? 'desc' : 'asc';
406 $class[] = 'sorted';
407 $class[] = $current_order;
408 } else {
409 $order = strtolower($desc_first);
410 if (!in_array($order, array('desc', 'asc'), true)) {
411 $order = $desc_first ? 'desc' : 'asc';
412 }
413 $class[] = 'sortable';
414 $class[] = 'desc' === $order ? 'asc' : 'desc';
415 }
416
417 $column_display_name = sprintf(
418 '<a href="%s"><span>%s</span><span class="sorting-indicators"></span></a>',
419 esc_url(add_query_arg(array('orderby_redir' => $orderby, 'order_redir' => $order), $current_url)),
420 esc_html($column_display_name)
421 );
422 } elseif ('cb' !== $column_key) {
423 $column_display_name = esc_html($column_display_name);
424 }
425
426 $tag = ('cb' === $column_key) ? 'td' : 'th';
427 $scope = ('th' === $tag) ? 'scope="col"' : '';
428 $id = $with_id ? "id='" . esc_attr($column_key) . "'" : '';
429
430 if (!empty($class)) {
431 $class = "class='" . esc_attr(implode(' ', $class)) . "'";
432 }
433
434 echo '<' . esc_attr($tag) . ' ' . $scope . ' ' . $id . ' ' . $class . '>' . $column_display_name . '</' . esc_attr($tag) . '>';
435 }
436 }
437
438 protected function column_default($item, $column_name)
439 {
440 switch ($column_name) {
441 case 'sources_from':
442 case 'url_redirect_to':
443 case 'http_code':
444 case 'hits_count':
445 case 'status':
446 case 'last_accessed_at':
447 return esc_html($item[$column_name]); # Fixed: Added esc_html() to prevent XSS
448 case 'pattern_type':
449 return $this->column_pattern_type($item);
450 default:
451 # return print_r($item, true); // Show the whole array for troubleshooting purposes.
452 return esc_html(print_r($item, true)); # Fixed: Added esc_html() to prevent XSS in debug output
453 }
454 }
455
456 protected function column_cb($item)
457 {
458 return sprintf(
459 '<input type="checkbox" name="%1$s[]" value="%2$s" />',
460 $this->_args['plural'], // Let's simply repurpose the table's singular label ("error").
461 $item['id'] // The value of the checkbox should be the record's ID.
462 );
463 }
464
465 protected function column_sources_from($item)
466 {
467 $request_data = metasync_sanitize_input_array($_REQUEST); // WPCS: Input var ok.
468 if (!isset($request_data['page'])) return;
469
470 // Build delete row action.
471 $delete_query_args = array(
472 'page' => sanitize_text_field($request_data['page']),
473 'action' => 'delete',
474 'id' => $item['id'],
475 );
476
477 // Build edit row action.
478 $edit_query_args = array(
479 'page' => sanitize_text_field($request_data['page']),
480 'action' => 'edit',
481 'id' => $item['id'],
482 );
483
484 // Build activate row action.
485 $activate_query_args = array(
486 'page' => sanitize_text_field($request_data['page']),
487 'action' => 'activate',
488 'id' => $item['id'],
489 );
490
491 // Build deactivate row action.
492 $deactivate_query_args = array(
493 'page' => sanitize_text_field($request_data['page']),
494 'action' => 'deactivate',
495 'id' => $item['id'],
496 );
497
498 $actions['edit'] = sprintf(
499 '<a href="%1$s">%2$s</a>',
500 esc_url(add_query_arg($edit_query_args, 'admin.php')),
501 _x('Edit', 'List table row action', 'metasync')
502 );
503
504 if ($item['status'] === 'inactive') {
505
506 $actions['activate'] = sprintf(
507 '<a href="%1$s">%2$s</a>',
508 esc_url(add_query_arg($activate_query_args, 'admin.php')),
509 _x('Activate', 'List table row action', 'metasync')
510 );
511 } else {
512
513 $actions['deactivate'] = sprintf(
514 '<a href="%1$s">%2$s</a>',
515 esc_url(add_query_arg($deactivate_query_args, 'admin.php')),
516 _x('Deactivate', 'List table row action', 'metasync')
517 );
518 }
519
520 $actions['delete'] = sprintf(
521 '<a href="%1$s">%2$s</a>',
522 esc_url(add_query_arg($delete_query_args, 'admin.php')),
523 _x('Delete', 'List table row action', 'metasync')
524 );
525
526 // Return the source from contents.
527 return sprintf(
528 '%1$s %2$s',
529 $this->show_source_from_urls($item),
530 $this->row_actions($actions)
531 );
532 }
533
534 /**
535 * Display pattern type column with user-friendly labels
536 */
537 protected function column_pattern_type($item)
538 {
539 $pattern_type = isset($item['pattern_type']) ? $item['pattern_type'] : 'exact';
540 $regex_pattern = isset($item['regex_pattern']) ? $item['regex_pattern'] : '';
541
542 // Pattern type labels
543 $labels = [
544 'exact' => 'Exact Match',
545 'contain' => 'Contains',
546 'start' => 'Starts With',
547 'end' => 'Ends With',
548 'regex' => 'Regex Pattern'
549 ];
550
551 $label = isset($labels[$pattern_type]) ? $labels[$pattern_type] : ucfirst($pattern_type);
552
553 // Add regex pattern as tooltip if available
554 if ($pattern_type === 'regex' && !empty($regex_pattern)) {
555 // Validate regex pattern
556 // Check if pattern has proper delimiters (same character at start and end)
557 $test_pattern = $regex_pattern;
558 $first_char = substr($test_pattern, 0, 1);
559 $has_valid_delimiter = false;
560
561 // Check if it looks like a properly delimited regex
562 if (in_array($first_char, ['/', '#', '~', '%', '@', '!'], true)) {
563 // Find the last occurrence of the delimiter (accounting for flags after it)
564 if (preg_match('/^' . preg_quote($first_char, '/') . '.*' . preg_quote($first_char, '/') . '[imsxuADU]*$/', $test_pattern)) {
565 $has_valid_delimiter = true;
566 }
567 }
568
569 // Add delimiters if missing - use # to avoid conflicts with / in URL patterns
570 if (!$has_valid_delimiter) {
571 $test_pattern = '#' . str_replace('#', '\\#', $regex_pattern) . '#';
572 }
573 $is_valid = @preg_match($test_pattern, '');
574
575 if ($is_valid === false) {
576 // Invalid regex - show red warning icon
577 return sprintf(
578 '<span style="display: flex; align-items: center; gap: 5px;">
579 <span class="dashicons dashicons-warning" style="color: #dc3232; font-size: 18px;" title="Invalid Regex Pattern: %s"></span>
580 <span title="Invalid regex pattern">%s</span>
581 </span>',
582 esc_attr($regex_pattern),
583 esc_html($label)
584 );
585 }
586
587 // Valid regex - show with tooltip
588 return sprintf(
589 '<span title="%s">%s</span>',
590 esc_attr($regex_pattern),
591 esc_html($label)
592 );
593 }
594
595 return esc_html($label);
596 }
597
598 protected function get_bulk_actions()
599 {
600 $actions = array(
601 'delete_bulk' => _x('Delete', 'List table bulk action', 'metasync'),
602 'activate_bulk' => _x('Activate', 'List table bulk action', 'metasync'),
603 'deactivate_bulk' => _x('Deactivate', 'List table bulk action', 'metasync'),
604 );
605
606 return $actions;
607 }
608
609 protected function process_bulk_action()
610 {
611 $post_data = metasync_sanitize_input_array($_POST);
612 $items = isset($post_data['items']) && is_array($post_data['items']) ? array_map('sanitize_title', $post_data['items']) : [];
613
614 if (empty($post_data['items'])) return;
615
616
617 // Detect when bulk delete action is being triggered.
618 if ('delete_bulk' === $this->current_action()) {
619 $this->db_redirection->delete($items);
620 }
621
622 // Detect when bulk activate action is being triggered.
623 if ('activate_bulk' === $this->current_action()) {
624 $this->db_redirection->update_status($items, 'active');
625 }
626
627 // Detect when bulk deactivate action is being triggered.
628 if ('deactivate_bulk' === $this->current_action()) {
629 $this->db_redirection->update_status($items, 'inactive');
630 }
631 }
632
633 protected function process_row_action()
634 {
635 $get_data = metasync_sanitize_input_array($_GET);
636 $item = isset($get_data['id']) ? sanitize_text_field($get_data['id']) : '';
637
638 // Detect when a delete action is being triggered.
639 if ('delete' === $this->current_action()) {
640 $this->db_redirection->delete([$item]);
641 }
642 // Detect when a activate action is being triggered.
643 if ('activate' === $this->current_action()) {
644 $this->db_redirection->update_status([$item], 'active');
645 }
646 // Detect when a deactivate action is being triggered.
647 if ('deactivate' === $this->current_action()) {
648 $this->db_redirection->update_status([$item], 'inactive');
649 }
650 }
651
652 function prepare_items()
653 {
654 $per_page = 10;
655
656 $columns = $this->get_columns();
657 $hidden = array();
658 $sortable = $this->get_sortable_columns();
659 $this->_column_headers = array($columns, $hidden, $sortable);
660
661 $this->process_bulk_action();
662 $this->process_row_action();
663
664 $current_page = $this->get_pagenum();
665 $offset = ($current_page - 1) * $per_page;
666
667 // Get total count for pagination
668 $total_items = $this->getTotalRecords();
669
670 // Load only the records for the current page (already sorted by database)
671 $data = $this->loadRecordsWithPagination($per_page, $offset);
672
673 $this->items = $data;
674
675 $this->set_pagination_args(array(
676 'total_items' => $total_items, // Total number of items from database
677 'per_page' => $per_page, // Items per page
678 'total_pages' => ceil($total_items / $per_page), // Total number of pages
679 ));
680 }
681
682 /**
683 * Override display method to prevent duplicate table rendering
684 */
685 public function display()
686 {
687 // Only display the table once
688 if (isset($this->_displayed) && $this->_displayed) {
689 return;
690 }
691
692 $this->_displayed = true;
693
694 // Call parent display method
695 parent::display();
696 }
697
698 /**
699 * Override display_tablenav to prevent duplicate navigation
700 */
701 protected function display_tablenav($which)
702 {
703 // Only display navigation once
704 if (isset($this->_nav_displayed) && $this->_nav_displayed) {
705 return;
706 }
707
708 if ($which === 'top') {
709 $this->_nav_displayed = true;
710 }
711
712 parent::display_tablenav($which);
713 }
714
715 private function show_source_from_urls($item)
716 {
717 # $source_urls = unserialize($item['sources_from']);
718 # Unserialize the sources_from field
719 $source_urls = unserialize($item['sources_from'], ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
720
721 # If unserialize failed or returned non-array, try to handle it
722 if (!is_array($source_urls)) {
723 # Fallback: display raw value (shouldn't happen normally)
724 echo esc_html($item['sources_from']);
725 return;
726 }
727
728 $pattern_type = isset($item['pattern_type']) ? $item['pattern_type'] : 'exact';
729 $regex_pattern = isset($item['regex_pattern']) ? $item['regex_pattern'] : '';
730
731 foreach ($source_urls as $source_name => $source_type) {
732
733 # Clean the source name in case it contains serialized data
734 $clean_source = $source_name;
735
736 # Check if source_name itself is serialized (edge case)
737 if (is_string($source_name) && strpos($source_name, 'a:') === 0) {
738 $unserialized = unserialize($source_name, ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
739 if (is_array($unserialized) && isset($unserialized[0]['pattern'])) {
740 $clean_source = $unserialized[0]['pattern'];
741 }
742 }
743
744 // For regex patterns, show the regex pattern instead of the label
745 if ($pattern_type === 'regex' && !empty($regex_pattern)) {
746 $display_text = $regex_pattern;
747 // Build the full URL for the link (use the actual pattern for regex)
748 $full_url = '#'; // No direct link for regex patterns
749
750 // Validate regex pattern
751 // Add delimiters if missing
752 $test_pattern = $regex_pattern;
753 if (!preg_match('/^[\/#~%@]/', $test_pattern)) {
754 $test_pattern = '/' . $test_pattern . '/';
755 }
756 $is_valid = @preg_match($test_pattern, '');
757 $warning_icon = '';
758
759 if ($is_valid === false) {
760 // Invalid regex - add red warning icon
761 $warning_icon = '<span class="dashicons dashicons-warning" style="color: #dc3232; font-size: 16px; vertical-align: middle; margin-right: 3px;" title="Invalid Regex Pattern"></span>';
762 }
763
764 echo sprintf(
765 '<span title="Label: %3$s">%1$s%2$s</span>',
766 $warning_icon,
767 esc_html($display_text),
768 esc_attr($clean_source)
769 );
770 } else {
771 // Build the full URL for the link
772 # $full_url = $this->build_full_url($source_name);
773 $full_url = $this->build_full_url($clean_source);
774
775 echo sprintf(
776 '<a href="%1$s" target="_blank">%2$s</a>',
777 esc_url($full_url),
778 # esc_html(_x($source_name, 'List table row action', 'metasync')) # Fixed: Added esc_html() to prevent XSS
779 esc_html($clean_source)
780 );
781 }
782 echo sprintf(
783 # '<span>%1$s</span>',
784 # ' [' . $source_type . ']'
785 # ' [' . esc_html($source_type) . ']' # Fixed: Added esc_html() to prevent XSS
786 '<span style="color: #666; font-size: 0.9em;">%1$s</span>',
787 ' [' . esc_html($source_type) . ']'
788 );
789 echo "<br>";
790 }
791 }
792
793 /**
794 * Build full URL from source name, handling different formats
795 */
796 private function build_full_url($source_name)
797 {
798 // If it's already a full URL, return as-is
799 if (strpos($source_name, 'http') === 0) {
800 return $source_name;
801 }
802
803 // If it starts with /, it's an absolute path
804 if (strpos($source_name, '/') === 0) {
805 return site_url() . $source_name;
806 }
807
808 // Otherwise, it's a relative path
809 return site_url() . '/' . $source_name;
810 }
811
812
813 /**
814 * Display HTTP code column with better formatting
815 */
816 protected function column_http_code($item)
817 {
818 $http_code = $item['http_code'];
819 $code_labels = [
820 '301' => '301 Permanent',
821 '302' => '302 Temporary',
822 '307' => '307 Temporary',
823 '410' => '410 Gone',
824 '451' => '451 Unavailable'
825 ];
826
827 $label = isset($code_labels[$http_code]) ? $code_labels[$http_code] : $http_code;
828
829 $class = '';
830 if ($http_code >= 300 && $http_code < 400) {
831 $class = 'color: #0073aa;';
832 } elseif ($http_code >= 400) {
833 $class = 'color: #d63638;';
834 }
835
836 return sprintf(
837 '<span style="%s">%s</span>',
838 esc_attr($class),
839 esc_html($label)
840 );
841 }
842 }
843