PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
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 | includes/class-metasync-otto-cache-manager.php +114 -29 2.6.20 → 2.7.2 View file →
@@ -80,9 +80,9 @@
80 80 } else {
81 81 echo '<p style="color: var(--dashboard-text-secondary);">ℹ️ No cache plugins detected.</p>';
82 82 }
83 83 } catch (Exception $e) {
84 - error_log('MetaSync Cache Status Error: ' . $e->getMessage());
84 + error_log('MetaSync Cache Status Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- unreachable in production
85 85 echo '<p style="color: var(--dashboard-error);">⚠️ An error occurred while retrieving cache plugin status.</p>';
86 86 }
87 87 } else {
88 88 echo '<p style="color: var(--dashboard-error);">⚠️ Cache Purge class not loaded.</p>';
@@ -98,12 +98,12 @@
98 98 <p class="description" style="margin-top: 10px; color: var(--dashboard-text-secondary);">This will clear cache from WP Rocket, LiteSpeed, W3 Total Cache, and all other detected cache plugins.</p>
99 99 </form>
100 100
101 101 <?php
102 - if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] == '1') {
103 - $cleared = isset($_GET['cleared']) ? intval($_GET['cleared']) : 0;
104 - $failed = isset($_GET['failed']) ? intval($_GET['failed']) : 0;
105 - $plugins = isset($_GET['plugins']) ? sanitize_text_field($_GET['plugins']) : '';
102 + if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
103 + $cleared = isset($_GET['cleared']) ? intval($_GET['cleared']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
104 + $failed = isset($_GET['failed']) ? intval($_GET['failed']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
105 + $plugins = isset($_GET['plugins']) ? sanitize_text_field(wp_unslash($_GET['plugins'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
106 106
107 107 if ($cleared > 0) {
108 108 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
109 109 echo '✅ <strong>Success!</strong> Cleared cache for ' . intval( $cleared ) . ' plugin(s)';
@@ -123,10 +123,10 @@
123 123 echo '</p></div>';
124 124 }
125 125 }
126 126
127 - if (isset($_GET['cache_error']) && $_GET['cache_error'] == '1') {
128 - $message = isset($_GET['message']) ? urldecode(sanitize_text_field($_GET['message'])) : '';
127 + if (isset($_GET['cache_error']) && $_GET['cache_error'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
128 + $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
129 129 if (empty($message)) {
130 130 $message = 'An unknown error occurred while clearing cache. Please check error logs for details.';
131 131 }
132 132 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
@@ -201,9 +201,9 @@
201 201 </button>
202 202 <span id="metasync-hc-save-msg" style="display: none; font-size: 13px;"></span>
203 203 </div>
204 204
205 - <input type="hidden" id="metasync-hc-nonce" value="<?php echo wp_create_nonce('metasync_hosting_cache_nonce'); ?>" />
205 + <input type="hidden" id="metasync-hc-nonce" value="<?php echo esc_attr(wp_create_nonce('metasync_hosting_cache_nonce')); ?>" />
206 206 </div>
207 207
208 208 <!-- Purge button -->
209 209 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
@@ -222,12 +222,12 @@
222 222 </form>
223 223
224 224 <?php
225 225 // Hosting cache result messages
226 - if (isset($_GET['hosting_cache_cleared']) && $_GET['hosting_cache_cleared'] == '1') {
227 - $hc_cleared = isset($_GET['hc_cleared']) ? sanitize_text_field(urldecode($_GET['hc_cleared'])) : '';
228 - $hc_failed = isset($_GET['hc_failed']) ? sanitize_text_field(urldecode($_GET['hc_failed'])) : '';
229 - $hc_not_detected = isset($_GET['hc_not_detected']) ? sanitize_text_field(urldecode($_GET['hc_not_detected'])) : '';
226 + if (isset($_GET['hosting_cache_cleared']) && $_GET['hosting_cache_cleared'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
227 + $hc_cleared = isset($_GET['hc_cleared']) ? sanitize_text_field(urldecode($_GET['hc_cleared'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
228 + $hc_failed = isset($_GET['hc_failed']) ? sanitize_text_field(urldecode($_GET['hc_failed'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
229 + $hc_not_detected = isset($_GET['hc_not_detected']) ? sanitize_text_field(urldecode($_GET['hc_not_detected'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
230 230
231 231 if ($hc_cleared) {
232 232 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
233 233 echo '✅ <strong>Success!</strong> Purged hosting cache on: ' . esc_html($hc_cleared);
@@ -295,11 +295,11 @@
295 295 <span style="color: var(--dashboard-accent);"><?php echo esc_html($cache_count); ?> cached entries</span>
296 296 </div>
297 297
298 298 <?php
299 - if (isset($_GET['otto_cache_cleared']) && $_GET['otto_cache_cleared'] == '1') {
300 - $cleared_count = isset($_GET['count']) ? intval($_GET['count']) : 0;
301 - $url = isset($_GET['url']) ? urldecode(sanitize_text_field($_GET['url'])) : '';
299 + if (isset($_GET['otto_cache_cleared']) && $_GET['otto_cache_cleared'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
300 + $cleared_count = isset($_GET['count']) ? intval($_GET['count']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
301 + $url = isset($_GET['url']) ? urldecode(sanitize_text_field(wp_unslash($_GET['url']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
302 302
303 303 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
304 304 if (!empty($url)) {
305 305 echo '✅ <strong>Success!</strong> Cleared cache for URL: <code>' . esc_html($url) . '</code> (' . intval( $cleared_count ) . ' entries)';
@@ -308,10 +308,10 @@
308 308 }
309 309 echo '</p></div>';
310 310 }
311 311
312 - if (isset($_GET['otto_cache_error']) && $_GET['otto_cache_error'] == '1') {
313 - $message = isset($_GET['message']) ? urldecode(sanitize_text_field($_GET['message'])) : 'An unknown error occurred.';
312 + if (isset($_GET['otto_cache_error']) && $_GET['otto_cache_error'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
313 + $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : 'An unknown error occurred.'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for an escaped notice
314 314 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
315 315 echo '❌ <strong>Error:</strong> ' . esc_html($message);
316 316 echo '</p></div>';
317 317 }
@@ -350,9 +350,9 @@
350 350 <td>
351 351 <input type="url"
352 352 id="otto_cache_url"
353 353 name="otto_cache_url"
354 - value="<?php echo isset($_GET['url']) ? esc_attr(urldecode(sanitize_text_field($_GET['url']))) : ''; ?>"
354 + value="<?php echo isset($_GET['url']) ? esc_attr(urldecode(sanitize_text_field(wp_unslash($_GET['url'])))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only URL prefill, the form submit carries a nonce ?>"
355 355 class="regular-text"
356 356 placeholder="https://example.com/page/"
357 357 required />
358 358 <p class="description" style="color: var(--dashboard-text-secondary);">Enter the full URL of the page whose cache you want to clear.</p>
@@ -398,9 +398,9 @@
398 398 if (!empty($results['cleared'])) {
399 399 $redirect_url .= '&plugins=' . urlencode(implode(',', $results['cleared']));
400 400 }
401 401 } catch (Exception $e) {
402 - error_log('MetaSync Cache Clear Error: ' . $e->getMessage());
402 + error_log('MetaSync Cache Clear Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
403 403 $redirect_url .= '&cache_error=1&message=' . urlencode('An error occurred while clearing the cache. Please try again.');
404 404 }
405 405 } else {
406 406 $redirect_url .= '&cache_error=1&message=' . urlencode('Cache Purge class not available');
@@ -437,9 +437,9 @@
437 437 # the suggestions cache is now empty. Purge the page caches too so the
438 438 # next request regenerates with the current (clean) OTTO state.
439 439 self::purge_page_caches_all();
440 440 } catch (Exception $e) {
441 - error_log('MetaSync OTTO Cache Clear Error: ' . $e->getMessage());
441 + error_log('MetaSync OTTO Cache Clear Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
442 442 $redirect_url .= '&otto_cache_error=1&message=' . urlencode('An error occurred while clearing the OTTO cache. Please try again.');
443 443 }
444 444 } else {
445 445 $redirect_url .= '&otto_cache_error=1&message=' . urlencode('Transient Cache class not found');
@@ -492,9 +492,9 @@
492 492 } else {
493 493 $redirect_url .= '&otto_cache_error=1&message=' . urlencode($result['message']);
494 494 }
495 495 } catch (Exception $e) {
496 - error_log('MetaSync OTTO Cache Clear Error: ' . $e->getMessage());
496 + error_log('MetaSync OTTO Cache Clear Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
497 497 $redirect_url .= '&otto_cache_error=1&message=' . urlencode('An error occurred while clearing the OTTO cache for this URL. Please try again.');
498 498 }
499 499 } else {
500 500 $redirect_url .= '&otto_cache_error=1&message=' . urlencode('Transient Cache class not found');
@@ -584,11 +584,11 @@
584 584 wp_send_json_error(['message' => 'Invalid security token']);
585 585 return;
586 586 }
587 587
588 - $url_pattern = isset($_POST['url_pattern']) ? sanitize_text_field($_POST['url_pattern']) : '';
589 - $pattern_type = isset($_POST['pattern_type']) ? sanitize_text_field($_POST['pattern_type']) : 'exact';
590 - $description = isset($_POST['description']) ? sanitize_textarea_field($_POST['description']) : '';
588 + $url_pattern = isset($_POST['url_pattern']) ? sanitize_text_field(wp_unslash($_POST['url_pattern'])) : '';
589 + $pattern_type = isset($_POST['pattern_type']) ? sanitize_text_field(wp_unslash($_POST['pattern_type'])) : 'exact';
590 + $description = isset($_POST['description']) ? sanitize_textarea_field(wp_unslash($_POST['description'])) : '';
591 591
592 592 if (empty($url_pattern)) {
593 593 wp_send_json_error(['message' => 'URL pattern is required']);
594 594 return;
@@ -655,8 +655,17 @@
655 655 ]);
656 656 return;
657 657 }
658 658
659 + if ($result === 'reactivated' || $result === true) {
660 + // Consent-gated analytics: excluding URLs from OTTO is an opt-out signal.
661 + Metasync_PostHog::feature('otto_opt_out', [
662 + 'scope' => 'pattern',
663 + 'action' => 'add',
664 + 'pattern_type' => sanitize_key((string) $pattern_type),
665 + ]);
666 + }
667 +
659 668 if ($result === 'reactivated') {
660 669 $db->clear_cache();
661 670
662 671 if ($pattern_type === 'exact') {
@@ -666,9 +675,9 @@
666 675 $cache_purge->clear_url_cache($url_pattern);
667 676 }
668 677 wp_cache_flush();
669 678 } catch (Exception $e) {
670 - error_log('MetaSync: Failed to clear cache for reactivated URL: ' . $e->getMessage());
679 + error_log('MetaSync: Failed to clear cache for reactivated URL: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
671 680 }
672 681 }
673 682
674 683 wp_send_json_success([
@@ -688,9 +697,9 @@
688 697 }
689 698
690 699 wp_cache_flush();
691 700 } catch (Exception $e) {
692 - error_log('MetaSync: Failed to clear cache for excluded URL: ' . $e->getMessage());
701 + error_log('MetaSync: Failed to clear cache for excluded URL: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
693 702 }
694 703 }
695 704
696 705 wp_send_json_success([
@@ -728,8 +737,12 @@
728 737
729 738 $result = $db->delete([$id]);
730 739
731 740 if ($result) {
741 + Metasync_PostHog::feature('otto_opt_out', [
742 + 'scope' => 'pattern',
743 + 'action' => 'remove',
744 + ]);
732 745 wp_send_json_success([
733 746 'message' => 'Excluded URL deleted successfully',
734 747 ]);
735 748 } else {
@@ -758,9 +771,8 @@
758 771 return;
759 772 }
760 773
761 774 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-excluded-urls-database.php';
762 - require_once plugin_dir_path(dirname(__FILE__)) . 'otto/otto_pixel.php';
763 775
764 776 $db = new Metasync_Otto_Excluded_URLs_Database();
765 777 $row = $db->get_record_by_id($id);
766 778
@@ -768,13 +780,86 @@
768 780 wp_send_json_error(['message' => 'Excluded URL not found']);
769 781 return;
770 782 }
771 783
784 + // Only an exact pattern resolves to a single URL that can be probed;
785 + // contain/start/end/regex rows match many URLs, so there is nothing to check.
786 + if ('exact' !== $row->pattern_type) {
787 + wp_send_json_error(['message' => 'Only exact URL exclusions can be rechecked']);
788 + }
789 +
772 790 $url = trim($row->url_pattern);
773 - $available = metasync_otto_is_url_available($url);
774 791
792 + // The probe needs a full URL. Older rows may store a site-relative
793 + // path, and a protocol-relative pattern only lacks the scheme; any
794 + // other scheme-less pattern cannot be turned into one URL reliably,
795 + // and guessing (host as path, other host as this site) would probe a
796 + // URL nobody excluded.
797 + if (null === wp_parse_url($url, PHP_URL_SCHEME)) {
798 + if (0 === strpos($url, '//')) {
799 + $url = wp_parse_url(home_url(), PHP_URL_SCHEME) . ':' . $url;
800 + } elseif (0 === strpos($url, '/')) {
801 + $url = home_url($url);
802 + } else {
803 + wp_send_json_success([
804 + 'available' => false,
805 + 'checked' => false,
806 + 'message' => 'Stored pattern is not a full URL; edit the entry to include the scheme.',
807 + 'url' => $url,
808 + ]);
809 + }
810 + }
811 +
812 + // Exclusions exist for this site's URLs, and letting the probe leave
813 + // for arbitrary hosts would turn Recheck into a server-side request
814 + // oracle, so the probe never targets another host.
815 + $url_host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
816 + $home_host = strtolower((string) wp_parse_url(home_url(), PHP_URL_HOST));
817 + if ($url_host !== $home_host || '' === $url_host) {
818 + wp_send_json_success([
819 + 'available' => false,
820 + 'checked' => false,
821 + 'message' => 'Only URLs on this site can be rechecked.',
822 + 'url' => $url,
823 + ]);
824 + }
825 +
826 + // Probe the real URL rather than guessing from local data — a deleted
827 + // post is invisible to local lookups but still 404s over HTTP.
828 + $args = [
829 + 'timeout' => 10,
830 + 'redirection' => 5,
831 + // Same opt-out core uses for loopback requests to this site.
832 + 'sslverify' => apply_filters('https_local_ssl_verify', true),
833 + ];
834 +
835 + $response = wp_remote_head($url, $args);
836 +
837 + // Some servers and edge layers refuse HEAD outright while the page
838 + // still serves to GET requests, so retry once before reporting the
839 + // refusal as a verdict.
840 + if (
841 + !is_wp_error($response)
842 + && 405 === (int) wp_remote_retrieve_response_code($response)
843 + ) {
844 + $response = wp_remote_get($url, $args);
845 + }
846 +
847 + if (is_wp_error($response)) {
848 + wp_send_json_success([
849 + 'available' => false,
850 + 'checked' => false,
851 + 'message' => $response->get_error_message(),
852 + 'url' => $url,
853 + ]);
854 + }
855 +
856 + $code = (int) wp_remote_retrieve_response_code($response);
857 +
775 858 wp_send_json_success([
776 - 'available' => $available,
859 + 'available' => ($code >= 200 && $code < 400),
860 + 'checked' => true,
861 + 'status' => $code,
777 862 'url' => $url,
778 863 ]);
779 864 }
780 865