PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
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
← All changes | redirections/class-metasync-redirection-list-table.php +116 -36 2.6.22 → 2.7.1 View file →
@@ -14,8 +14,16 @@
14 14 * @author Engineering Team <[email protected]>
15 15 */
16 16 class Metasync_Redirection_List_Table extends WP_List_Table
17 17 {
18 + /**
19 + * Name of the submit-button marker that identifies an Apply submit.
20 + *
21 + * Matches the name WordPress core itself gives the button from 7.1 on, so
22 + * the marker is core's own on new installs and ours on older ones.
23 + */
24 + private const BULK_APPLY_MARKER = 'bulk_action';
25 +
18 26 private $records;
19 27 private $db_redirection;
20 28 private $_displayed = false;
21 29 private $_nav_displayed = [];
@@ -41,9 +49,9 @@
41 49 * Get the current page number for redirections (use separate parameter)
42 50 */
43 51 public function get_pagenum()
44 52 {
45 - $pagenum = isset($_REQUEST['paged_redir']) ? absint($_REQUEST['paged_redir']) : 0;
53 + $pagenum = isset($_REQUEST['paged_redir']) ? absint($_REQUEST['paged_redir']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page number for the list query
46 54
47 55 if (isset($this->_pagination_args['total_pages']) && $pagenum > $this->_pagination_args['total_pages']) {
48 56 $pagenum = $this->_pagination_args['total_pages'];
49 57 }
@@ -95,14 +103,14 @@
95 103
96 104 $current_url = remove_query_arg($removable_query_args, $current_url);
97 105
98 106 // Preserve the tab parameter
99 - if (isset($_GET['tab'])) {
100 - $current_url = add_query_arg('tab', sanitize_text_field($_GET['tab']), $current_url);
107 + if (isset($_GET['tab'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only values used in pagination links
108 + $current_url = add_query_arg('tab', sanitize_text_field(wp_unslash($_GET['tab'])), $current_url); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only values used in pagination links
101 109 }
102 110
103 111 // If filter action was submitted, remove paged_redir to reset to page 1
104 - if (isset($_POST['filter_action']) || isset($_POST['post-query-submit'])) {
112 + if (isset($_POST['filter_action']) || isset($_POST['post-query-submit'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only values used in pagination links
105 113 $current_url = remove_query_arg('paged_redir', $current_url);
106 114 }
107 115
108 116 // Rebuild visible list state from the allow-listed request values. Removing
@@ -142,9 +150,9 @@
142 150 } else {
143 151 $page_links[] = sprintf(
144 152 "<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
145 153 esc_url(remove_query_arg('paged_redir', $current_url)),
146 - esc_html__('First page'),
154 + esc_html__('First page', 'metasync'),
147 155 '&laquo;'
148 156 );
149 157 }
150 158
@@ -153,9 +161,9 @@
153 161 } else {
154 162 $page_links[] = sprintf(
155 163 "<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
156 164 esc_url(add_query_arg('paged_redir', max(1, $current - 1), $current_url)),
157 - esc_html__('Previous page'),
165 + esc_html__('Previous page', 'metasync'),
158 166 '&lsaquo;'
159 167 );
160 168 }
161 169
@@ -183,9 +191,9 @@
183 191 } else {
184 192 $page_links[] = sprintf(
185 193 "<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
186 194 esc_url(add_query_arg('paged_redir', min($total_pages, $current + 1), $current_url)),
187 - esc_html__('Next page'),
195 + esc_html__('Next page', 'metasync'),
188 196 '&rsaquo;'
189 197 );
190 198 }
191 199
@@ -194,9 +202,9 @@
194 202 } else {
195 203 $page_links[] = sprintf(
196 204 "<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
197 205 esc_url(add_query_arg('paged_redir', $total_pages, $current_url)),
198 - esc_html__('Last page'),
206 + esc_html__('Last page', 'metasync'),
199 207 '&raquo;'
200 208 );
201 209 }
202 210
@@ -293,27 +301,27 @@
293 301 if (!empty($request_state['s_redir'])) {
294 302 $filters['search'] = $request_state['s_redir'];
295 303 }
296 304
297 - if (!empty($_REQUEST['status_filter'])) {
298 - $filters['status'] = sanitize_text_field($_REQUEST['status_filter']);
305 + if (!empty($_REQUEST['status_filter'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
306 + $filters['status'] = sanitize_text_field(wp_unslash($_REQUEST['status_filter'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
299 307 }
300 308
301 - if (!empty($_REQUEST['pattern_filter'])) {
302 - $filters['pattern_type'] = sanitize_text_field($_REQUEST['pattern_filter']);
309 + if (!empty($_REQUEST['pattern_filter'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
310 + $filters['pattern_type'] = sanitize_text_field(wp_unslash($_REQUEST['pattern_filter'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
303 311 }
304 312
305 - if (!empty($_REQUEST['http_code_filter'])) {
306 - $filters['http_code'] = intval($_REQUEST['http_code_filter']);
313 + if (!empty($_REQUEST['http_code_filter'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
314 + $filters['http_code'] = intval($_REQUEST['http_code_filter']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
307 315 }
308 316
309 317 // Use separate orderby/order parameters for redirections
310 - if (!empty($_REQUEST['orderby_redir'])) {
311 - $filters['order_by'] = sanitize_sql_orderby($_REQUEST['orderby_redir']);
318 + if (!empty($_REQUEST['orderby_redir'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
319 + $filters['order_by'] = sanitize_sql_orderby($_REQUEST['orderby_redir']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
312 320 }
313 321
314 - if (!empty($_REQUEST['order_redir'])) {
315 - $filters['order'] = sanitize_text_field($_REQUEST['order_redir']);
322 + if (!empty($_REQUEST['order_redir'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
323 + $filters['order'] = sanitize_text_field(wp_unslash($_REQUEST['order_redir'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and sort values for the list query
316 324 }
317 325
318 326 return $filters;
319 327 }
@@ -353,9 +361,9 @@
353 361 * Override to use custom orderby/order parameter names
354 362 */
355 363 protected function get_orderby()
356 364 {
357 - return isset($_REQUEST['orderby_redir']) ? sanitize_key($_REQUEST['orderby_redir']) : '';
365 + return isset($_REQUEST['orderby_redir']) ? sanitize_key(wp_unslash($_REQUEST['orderby_redir'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only sort state for the column headers
358 366 }
359 367
360 368 /**
361 369 * Override to use custom orderby/order parameter names
@@ -361,9 +369,9 @@
361 369 * Override to use custom orderby/order parameter names
362 370 */
363 371 protected function get_order()
364 372 {
365 - $raw = isset($_REQUEST['order_redir']) ? strtolower(sanitize_key($_REQUEST['order_redir'])) : '';
373 + $raw = isset($_REQUEST['order_redir']) ? strtolower(sanitize_key(wp_unslash($_REQUEST['order_redir']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only sort state for the column headers
366 374 if ($raw === 'desc') {
367 375 return 'desc';
368 376 }
369 377 return 'asc';
@@ -375,9 +383,9 @@
375 383 public function print_column_headers($with_id = true)
376 384 {
377 385 list($columns, $hidden, $sortable, $primary) = $this->get_column_info();
378 386
379 - $current_page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : Metasync_Admin::$page_slug;
387 + $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : Metasync_Admin::$page_slug; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page slug used in escaped sort links
380 388 $current_url = set_url_scheme('http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
381 389 $current_url = remove_query_arg(['_wpnonce', '_wp_http_referer', 'action', 'action2', 'id'], $current_url);
382 390 $current_url = add_query_arg('page', $current_page, $current_url);
383 391 $current_url = add_query_arg('tab', 'redirections', $current_url);
@@ -453,8 +461,9 @@
453 461 case 'pattern_type':
454 462 return $this->column_pattern_type($item);
455 463 default:
456 464 # return print_r($item, true); // Show the whole array for troubleshooting purposes.
465 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- unreachable in production
457 466 return esc_html(print_r($item, true)); # Fixed: Added esc_html() to prevent XSS in debug output
458 467 }
459 468 }
460 469
@@ -468,8 +477,9 @@
468 477 }
469 478
470 479 protected function column_sources_from($item)
471 480 {
481 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only, row-action links carry their own nonce
472 482 $request_data = metasync_sanitize_input_array($_REQUEST); // WPCS: Input var ok.
473 483 if (!isset($request_data['page'])) return;
474 484
475 485 // Build delete row action.
@@ -614,21 +624,23 @@
614 624 * @return string|false
615 625 */
616 626 public function current_action()
617 627 {
618 - // A "Filter"/query submit is not a bulk action. WP core's own
628 + // A "Filter"/"Search" submit is not a bulk action. WP core's own
619 629 // current_action() short-circuits on filter_action for exactly this
620 630 // reason, and dropping that check makes ticking rows, choosing a bulk
621 - // action and then clicking Filter silently run the action.
622 - if (!empty($_REQUEST['filter_action'])) {
631 + // action and then clicking Filter silently run the action. The Search
632 + // submit posts no filter_action, so it needs its own named marker to
633 + // stop the same silent delete when it shares a form with the dropdown.
634 + if (!empty($_REQUEST['filter_action']) || !empty($_REQUEST['search_submit'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only action name, callers verify a nonce before any write
623 635 return false;
624 636 }
625 637
626 - if (isset($_REQUEST['action']) && '-1' !== $_REQUEST['action'] && '' !== $_REQUEST['action']) {
627 - return sanitize_text_field(wp_unslash($_REQUEST['action']));
638 + if (isset($_REQUEST['action']) && '-1' !== $_REQUEST['action'] && '' !== $_REQUEST['action']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only action name, callers verify a nonce before any write
639 + return sanitize_text_field(wp_unslash($_REQUEST['action'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only action name, callers verify a nonce before any write
628 640 }
629 - if (isset($_REQUEST['action2']) && '-1' !== $_REQUEST['action2'] && '' !== $_REQUEST['action2']) {
630 - return sanitize_text_field(wp_unslash($_REQUEST['action2']));
641 + if (isset($_REQUEST['action2']) && '-1' !== $_REQUEST['action2'] && '' !== $_REQUEST['action2']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only action name, callers verify a nonce before any write
642 + return sanitize_text_field(wp_unslash($_REQUEST['action2'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only action name, callers verify a nonce before any write
631 643 }
632 644 return parent::current_action();
633 645 }
634 646
@@ -660,15 +672,90 @@
660 672
661 673 return $actions;
662 674 }
663 675
676 + /**
677 + * Guarantee the Apply button posts a marker identifying it as the submitter.
678 + *
679 + * The bulk dropdown, the row checkboxes and the bulk-items nonce all live
680 + * inside the same form as the Search field and the filter controls, so every
681 + * submit of that form carries a complete-looking bulk request. The only thing
682 + * that distinguishes a real Apply from Search, Enter-in-search or a filter
683 + * dropdown's auto-submit is which submit button (if any) was used, and a
684 + * button only reports itself when it has a name attribute.
685 + *
686 + * WordPress core names the Apply button `bulk_action` from 7.1 onwards; on
687 + * the older releases this plugin still supports get_submit_button() is called
688 + * with an empty name and omits the attribute entirely. Add it ourselves in
689 + * that case so process_bulk_action() can rely on the marker on every
690 + * supported version, with no JavaScript involved.
691 + *
692 + * @param string $which Which tablenav is being rendered ('top' or 'bottom').
693 + * @return void
694 + */
695 + protected function bulk_actions($which = '')
696 + {
697 + ob_start();
698 + parent::bulk_actions($which);
699 + $markup = ob_get_clean();
700 +
701 + // get_bulk_actions() returns an empty array on screens where this table
702 + // must not emit bulk controls, and core then renders nothing at all.
703 + if (!is_string($markup) || '' === $markup) {
704 + return;
705 + }
706 +
707 + if (false === strpos($markup, 'name="' . self::BULK_APPLY_MARKER . '"')) {
708 + $named = preg_replace(
709 + '/<input\b(?=[^>]*\bid="doaction2?")/',
710 + '<input name="' . self::BULK_APPLY_MARKER . '"',
711 + $markup,
712 + 1
713 + );
714 +
715 + // Keep core's markup untouched if the shape ever changes and the
716 + // pattern stops matching, rather than emitting nothing.
717 + if (is_string($named)) {
718 + $markup = $named;
719 + }
720 + }
721 +
722 + echo $markup; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- core markup, already escaped by WP_List_Table.
723 + }
724 +
725 + /**
726 + * Whether the current request was submitted with the Apply button.
727 + *
728 + * Only Apply carries the marker added in bulk_actions(); Search and the
729 + * unnamed implicit-submission button send no submit-button value, and a
730 + * filter dropdown's programmatic form.submit() sends none either. Testing
731 + * for the marker's presence rather than for the absence of `filter_action`
732 + * keeps every current and future non-Apply submit path suppressed by
733 + * default.
734 + *
735 + * @param array $post_data Sanitised copy of $_POST.
736 + * @return bool
737 + */
738 + private function is_bulk_apply_submit($post_data)
739 + {
740 + return !empty($post_data[self::BULK_APPLY_MARKER]);
741 + }
742 +
664 743 protected function process_bulk_action()
665 744 {
666 - $post_data = metasync_sanitize_input_array($_POST);
745 + $post_data = metasync_sanitize_input_array($_POST); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check_admin_referer() runs before any write
667 746 $items = isset($post_data['items']) && is_array($post_data['items']) ? array_map('sanitize_title', $post_data['items']) : [];
668 747
669 748 if (empty($post_data['items'])) return;
670 749
750 + // Suppress every submit that did not come from Apply. Search, Enter in the
751 + // search box and a filter dropdown's auto-submit all post the bulk
752 + // dropdown, the ticked rows and a valid nonce, so without this the
753 + // selected action ran on any of them - silently, with no confirmation.
754 + if (!$this->is_bulk_apply_submit($post_data)) {
755 + return;
756 + }
757 +
671 758 if (!$this->current_action()) {
672 759 return;
673 760 }
674 761
@@ -878,15 +965,8 @@
878 965 # esc_html(_x($source_name, 'List table row action', 'metasync')) # Fixed: Added esc_html() to prevent XSS
879 966 esc_html($clean_source)
880 967 );
881 968 }
882 - echo sprintf(
883 - # '<span>%1$s</span>',
884 - # ' [' . $source_type . ']'
885 - # ' [' . esc_html($source_type) . ']' # Fixed: Added esc_html() to prevent XSS
886 - '<span style="color: #666; font-size: 0.9em;">%1$s</span>',
887 - ' [' . esc_html($source_type) . ']'
888 - );
889 969 echo "<br>";
890 970 }
891 971 }
892 972