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

870 lines 26.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(wp_nonce_url(add_query_arg($activate_query_args, 'admin.php'), 'metasync_redirection_row_action')),
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(wp_nonce_url(add_query_arg($deactivate_query_args, 'admin.php'), 'metasync_redirection_row_action')),
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(wp_nonce_url(add_query_arg($delete_query_args, 'admin.php'), 'metasync_redirection_row_action')),
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 if (!$this->current_action()) {
617 return;
618 }
619
620 // Verify the bulk-action nonce emitted by WP_List_Table::display_tablenav()
621 // (plural === 'items' → 'bulk-items') and confirm plugin access before any
622 // state change. Guards against CSRF on the bulk delete/activate/deactivate path.
623 check_admin_referer('bulk-items');
624
625 if (!Metasync::current_user_has_plugin_access()) {
626 return;
627 }
628
629 // Detect when bulk delete action is being triggered.
630 if ('delete_bulk' === $this->current_action()) {
631 $this->db_redirection->delete($items);
632 }
633
634 // Detect when bulk activate action is being triggered.
635 if ('activate_bulk' === $this->current_action()) {
636 $this->db_redirection->update_status($items, 'active');
637 }
638
639 // Detect when bulk deactivate action is being triggered.
640 if ('deactivate_bulk' === $this->current_action()) {
641 $this->db_redirection->update_status($items, 'inactive');
642 }
643 }
644
645 protected function process_row_action()
646 {
647 $state_actions = ['delete', 'activate', 'deactivate'];
648
649 if (!in_array($this->current_action(), $state_actions, true)) {
650 return;
651 }
652
653 // Verify the row-action nonce (added to the row-action links in column_sources_from())
654 // and confirm plugin access before any state change. The row actions run via GET
655 // links, so without this they are open to CSRF.
656 check_admin_referer('metasync_redirection_row_action');
657
658 if (!Metasync::current_user_has_plugin_access()) {
659 return;
660 }
661
662 $get_data = metasync_sanitize_input_array($_GET);
663 $item = isset($get_data['id']) ? sanitize_text_field($get_data['id']) : '';
664
665 // Detect when a delete action is being triggered.
666 if ('delete' === $this->current_action()) {
667 $this->db_redirection->delete([$item]);
668 }
669 // Detect when a activate action is being triggered.
670 if ('activate' === $this->current_action()) {
671 $this->db_redirection->update_status([$item], 'active');
672 }
673 // Detect when a deactivate action is being triggered.
674 if ('deactivate' === $this->current_action()) {
675 $this->db_redirection->update_status([$item], 'inactive');
676 }
677 }
678
679 function prepare_items()
680 {
681 $per_page = 10;
682
683 $columns = $this->get_columns();
684 $hidden = array();
685 $sortable = $this->get_sortable_columns();
686 $this->_column_headers = array($columns, $hidden, $sortable);
687
688 $this->process_bulk_action();
689 $this->process_row_action();
690
691 $current_page = $this->get_pagenum();
692 $offset = ($current_page - 1) * $per_page;
693
694 // Get total count for pagination
695 $total_items = $this->getTotalRecords();
696
697 // Load only the records for the current page (already sorted by database)
698 $data = $this->loadRecordsWithPagination($per_page, $offset);
699
700 $this->items = $data;
701
702 $this->set_pagination_args(array(
703 'total_items' => $total_items, // Total number of items from database
704 'per_page' => $per_page, // Items per page
705 'total_pages' => ceil($total_items / $per_page), // Total number of pages
706 ));
707 }
708
709 /**
710 * Override display method to prevent duplicate table rendering
711 */
712 public function display()
713 {
714 // Only display the table once
715 if (isset($this->_displayed) && $this->_displayed) {
716 return;
717 }
718
719 $this->_displayed = true;
720
721 // Call parent display method
722 parent::display();
723 }
724
725 /**
726 * Override display_tablenav to prevent duplicate navigation
727 */
728 protected function display_tablenav($which)
729 {
730 // Only display navigation once
731 if (isset($this->_nav_displayed) && $this->_nav_displayed) {
732 return;
733 }
734
735 if ($which === 'top') {
736 $this->_nav_displayed = true;
737 }
738
739 parent::display_tablenav($which);
740 }
741
742 private function show_source_from_urls($item)
743 {
744 # $source_urls = unserialize($item['sources_from']);
745 # Unserialize the sources_from field
746 $source_urls = unserialize($item['sources_from'], ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
747
748 # If unserialize failed or returned non-array, try to handle it
749 if (!is_array($source_urls)) {
750 # Fallback: display raw value (shouldn't happen normally)
751 echo esc_html($item['sources_from']);
752 return;
753 }
754
755 $pattern_type = isset($item['pattern_type']) ? $item['pattern_type'] : 'exact';
756 $regex_pattern = isset($item['regex_pattern']) ? $item['regex_pattern'] : '';
757
758 foreach ($source_urls as $source_name => $source_type) {
759
760 # Clean the source name in case it contains serialized data
761 $clean_source = $source_name;
762
763 # Check if source_name itself is serialized (edge case)
764 if (is_string($source_name) && strpos($source_name, 'a:') === 0) {
765 $unserialized = unserialize($source_name, ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
766 if (is_array($unserialized) && isset($unserialized[0]['pattern'])) {
767 $clean_source = $unserialized[0]['pattern'];
768 }
769 }
770
771 // For regex patterns, show the regex pattern instead of the label
772 if ($pattern_type === 'regex' && !empty($regex_pattern)) {
773 $display_text = $regex_pattern;
774 // Build the full URL for the link (use the actual pattern for regex)
775 $full_url = '#'; // No direct link for regex patterns
776
777 // Validate regex pattern
778 // Add delimiters if missing
779 $test_pattern = $regex_pattern;
780 if (!preg_match('/^[\/#~%@]/', $test_pattern)) {
781 $test_pattern = '/' . $test_pattern . '/';
782 }
783 $is_valid = @preg_match($test_pattern, '');
784 $warning_icon = '';
785
786 if ($is_valid === false) {
787 // Invalid regex - add red warning icon
788 $warning_icon = '<span class="dashicons dashicons-warning" style="color: #dc3232; font-size: 16px; vertical-align: middle; margin-right: 3px;" title="Invalid Regex Pattern"></span>';
789 }
790
791 echo sprintf(
792 '<span title="Label: %3$s">%1$s%2$s</span>',
793 $warning_icon,
794 esc_html($display_text),
795 esc_attr($clean_source)
796 );
797 } else {
798 // Build the full URL for the link
799 # $full_url = $this->build_full_url($source_name);
800 $full_url = $this->build_full_url($clean_source);
801
802 echo sprintf(
803 '<a href="%1$s" target="_blank">%2$s</a>',
804 esc_url($full_url),
805 # esc_html(_x($source_name, 'List table row action', 'metasync')) # Fixed: Added esc_html() to prevent XSS
806 esc_html($clean_source)
807 );
808 }
809 echo sprintf(
810 # '<span>%1$s</span>',
811 # ' [' . $source_type . ']'
812 # ' [' . esc_html($source_type) . ']' # Fixed: Added esc_html() to prevent XSS
813 '<span style="color: #666; font-size: 0.9em;">%1$s</span>',
814 ' [' . esc_html($source_type) . ']'
815 );
816 echo "<br>";
817 }
818 }
819
820 /**
821 * Build full URL from source name, handling different formats
822 */
823 private function build_full_url($source_name)
824 {
825 // If it's already a full URL, return as-is
826 if (strpos($source_name, 'http') === 0) {
827 return $source_name;
828 }
829
830 // If it starts with /, it's an absolute path
831 if (strpos($source_name, '/') === 0) {
832 return site_url() . $source_name;
833 }
834
835 // Otherwise, it's a relative path
836 return site_url() . '/' . $source_name;
837 }
838
839
840 /**
841 * Display HTTP code column with better formatting
842 */
843 protected function column_http_code($item)
844 {
845 $http_code = $item['http_code'];
846 $code_labels = [
847 '301' => '301 Permanent',
848 '302' => '302 Temporary',
849 '307' => '307 Temporary',
850 '410' => '410 Gone',
851 '451' => '451 Unavailable'
852 ];
853
854 $label = isset($code_labels[$http_code]) ? $code_labels[$http_code] : $http_code;
855
856 $class = '';
857 if ($http_code >= 300 && $http_code < 400) {
858 $class = 'color: #0073aa;';
859 } elseif ($http_code >= 400) {
860 $class = 'color: #d63638;';
861 }
862
863 return sprintf(
864 '<span style="%s">%s</span>',
865 esc_attr($class),
866 esc_html($label)
867 );
868 }
869 }
870