| @@ -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 | |