| @@ -297,8 +297,17 @@ | ||
| 297 | 297 | // `metasync_enable_seo_suite` filter. |
| 298 | 298 | require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-suite.php'; |
| 299 | 299 | new Metasync_Seo_Suite(); |
| 300 | 300 | |
| 301 | + // Classic-editor save path for the three fields that exist only as | |
| 302 | + // Gutenberg sidebar (REST) meta otherwise: breadcrumb title, language | |
| 303 | + // alternates, primary category. The Suite renders the first two and | |
| 304 | + // emits the nonce; this handler persists them from the classic form. | |
| 305 | + // It guards itself against block-editor and managed-page context, so | |
| 306 | + // it is safe to register unconditionally. | |
| 307 | + require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-classic-seo-fields.php'; | |
| 308 | + new Metasync_Classic_Seo_Fields(); | |
| 309 | + | |
| 301 | 310 | // SEO Health CSV export: must run on admin_init (before output). |
| 302 | 311 | // Cheap $_GET check avoids loading the class on every admin page. |
| 303 | 312 | if ( |
| 304 | 313 | isset($_GET['page'], $_GET['export'], $_GET['_wpnonce']) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing check, the handler verifies nonce and capability |
| @@ -343,13 +352,8 @@ | ||
| 343 | 352 | if (class_exists('Metasync_Breadcrumbs_Schema')) { |
| 344 | 353 | new Metasync_Breadcrumbs_Schema($this->get_plugin_name(), $this->get_version()); |
| 345 | 354 | } |
| 346 | 355 | |
| 347 | - // Initialize Developer Panel (for endpoint switching) | |
| 348 | - if (class_exists('Metasync_Dev_Panel')) { | |
| 349 | - $dev_panel = new Metasync_Dev_Panel($this->get_plugin_name(), $this->get_version()); | |
| 350 | - } | |
| 351 | - | |
| 352 | 356 | // Initialize Site Health integration |
| 353 | 357 | if (class_exists('Metasync_Site_Health')) { |
| 354 | 358 | $site_health = new Metasync_Site_Health(); |
| 355 | 359 | $site_health->register_tests(); |
| @@ -568,8 +572,20 @@ | ||
| 568 | 572 | // One-time upgrade: regenerate sitemap to remove any Beaver Builder template entries |
| 569 | 573 | if ( ! get_option( 'metasync_sitemap_bb_exclusion_applied' ) ) { |
| 570 | 574 | $this->loader->add_action('init', $this, 'maybe_regenerate_sitemap_after_upgrade'); |
| 571 | 575 | } |
| 576 | + | |
| 577 | + // One-time upgrade: delete the legacy Otto crawl-data option. Nothing | |
| 578 | + // reads or writes it anymore (the webhook and SEO sync job stopped | |
| 579 | + // persisting crawl data), and on sites where OTTO crawled many URLs | |
| 580 | + // the row could grow to megabytes while being autoloaded on every | |
| 581 | + // request before WordPress 6.6. The guard below runs on every request, | |
| 582 | + // so the flag must be autoloaded (like the sitemap flag above): a | |
| 583 | + // boolean true costs ~2 bytes inside the alloptions query WordPress | |
| 584 | + // already always runs, keeping the steady-state cost a cache hit. | |
| 585 | + if ( ! get_option( 'metasync_otto_crawldata_removed' ) ) { | |
| 586 | + $this->loader->add_action('init', $this, 'maybe_delete_legacy_otto_crawl_data'); | |
| 587 | + } | |
| 572 | 588 | } |
| 573 | 589 | |
| 574 | 590 | /** |
| 575 | 591 | * One-time upgrade routine: regenerate the XML sitemap so that Beaver Builder |
| @@ -599,8 +615,40 @@ | ||
| 599 | 615 | // so that enabling sitemaps later will still clean up BB templates. |
| 600 | 616 | } |
| 601 | 617 | |
| 602 | 618 | /** |
| 619 | + * One-time upgrade routine: delete the legacy Otto crawl-data option. | |
| 620 | + * | |
| 621 | + * The option stored every URL Otto ever sent and grew without bound; | |
| 622 | + * nothing writes it anymore and nothing in production reads it (per-URL | |
| 623 | + * state lives in Metasync_Otto_Job_Status, delivery receipts in | |
| 624 | + * Metasync_Otto_Webhook_Audit, both bounded). This drops the leftover | |
| 625 | + * row — which on long-running sites can be megabytes of autoloaded data. | |
| 626 | + * | |
| 627 | + * Runs once on 'init' and sets a flag so it never runs again. The flag | |
| 628 | + * is autoloaded deliberately: the registration guard reads it on every | |
| 629 | + * request, and an autoloaded boolean is a free cache hit inside the | |
| 630 | + * alloptions query WordPress always performs — a non-autoloaded flag | |
| 631 | + * would cost a dedicated SELECT per request forever, the exact waste | |
| 632 | + * this routine exists to remove. delete_option() is called directly | |
| 633 | + * without reading the option first: loading a multi-megabyte value into | |
| 634 | + * memory just to delete it would be the one way this cleanup could hurt | |
| 635 | + * a site, and the SQL DELETE never needs the value. | |
| 636 | + */ | |
| 637 | + public function maybe_delete_legacy_otto_crawl_data() { | |
| 638 | + $done_key = 'metasync_otto_crawldata_removed'; | |
| 639 | + if ( get_option( $done_key ) ) { | |
| 640 | + return; | |
| 641 | + } | |
| 642 | + | |
| 643 | + // No "feature in use" conditional here, unlike the sitemap routine: | |
| 644 | + // the row is dead weight whether or not Otto is configured, and | |
| 645 | + // delete_option() on an absent row is a cheap no-op. | |
| 646 | + delete_option( 'metasync_otto_crawldata' ); | |
| 647 | + update_option( $done_key, true ); | |
| 648 | + } | |
| 649 | + | |
| 650 | + /** | |
| 603 | 651 | * Initialize endpoint URL filtering for staging mode |
| 604 | 652 | * Intercepts HTTP requests and replaces production URLs with staging URLs |
| 605 | 653 | */ |
| 606 | 654 | private function init_endpoint_filtering() { |
| @@ -903,8 +951,52 @@ | ||
| 903 | 951 | return is_string($value) && strncmp($value, self::API_KEY_ENC_PREFIX, strlen(self::API_KEY_ENC_PREFIX)) === 0; |
| 904 | 952 | } |
| 905 | 953 | |
| 906 | 954 | /** |
| 955 | + * Whether a value is a strictly valid UUID (versions 1-8). | |
| 956 | + * | |
| 957 | + * The single shared validator for every write path that persists | |
| 958 | + * `general.otto_pixel_uuid` (SSO push callback, pull-based connect, | |
| 959 | + * heartbeat writes, settings sanitizer). The OTTO Pixel UUID must never | |
| 960 | + * hold arbitrary text — usernames such as `raza.imran` landing in it | |
| 961 | + * break OTTO SSR authentication — so every write path funnels through | |
| 962 | + * this check instead of a scattered one-off regex. | |
| 963 | + * | |
| 964 | + * Deliberately stricter than wp_is_uuid(), which only checks the | |
| 965 | + * 8-4-4-4-12 hex shape and would accept placeholder values such as | |
| 966 | + * `11111111-2222-3333-4444-555555555555`. The version nibble (first hex | |
| 967 | + * of the third group) must be 1-8 — RFC 4122 defined 1-5 and RFC 9562 | |
| 968 | + * added 6/7/8, which modern generators increasingly emit — and the | |
| 969 | + * variant nibble (first hex of the fourth group) must be 8/9/a/b. That | |
| 970 | + * keeps behaviour identical with and without the WP helper loaded (unit | |
| 971 | + * tests run without it). | |
| 972 | + * | |
| 973 | + * An empty string is NOT valid: clearing the field is a separate concern | |
| 974 | + * owned by the disconnect flow (unset + write_options_without_stored_merge), | |
| 975 | + * so '' must not masquerade as a usable UUID here. | |
| 976 | + * | |
| 977 | + * @param mixed $value | |
| 978 | + * @return bool True only for a canonical 8-4-4-4-12 hex UUID of version 1-8 | |
| 979 | + * with an RFC-4122 variant (8/9/a/b) in the fourth group. | |
| 980 | + */ | |
| 981 | + public static function is_valid_uuid($value) | |
| 982 | + { | |
| 983 | + if (!is_string($value)) { | |
| 984 | + return false; | |
| 985 | + } | |
| 986 | + | |
| 987 | + $value = trim($value); | |
| 988 | + if ($value === '') { | |
| 989 | + return false; | |
| 990 | + } | |
| 991 | + | |
| 992 | + return (bool) preg_match( | |
| 993 | + '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', | |
| 994 | + $value | |
| 995 | + ); | |
| 996 | + } | |
| 997 | + | |
| 998 | + /** | |
| 907 | 999 | * Encrypt a plaintext Search Atlas API key for storage at rest. |
| 908 | 1000 | * |
| 909 | 1001 | * Uses AES-256-GCM (authenticated) with a random 12-byte IV. The IV, the |
| 910 | 1002 | * 16-byte GCM tag and the ciphertext are concatenated and base64-encoded |
| @@ -1494,8 +1586,9 @@ | ||
| 1494 | 1586 | * @param string $text Plain-English help text (escaped internally). |
| 1495 | 1587 | */ |
| 1496 | 1588 | public static function render_tooltip_icon($tooltip_id, $text) |
| 1497 | 1589 | { |
| 1590 | + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_icon_html() escapes every interpolated value internally (esc_attr / esc_html) | |
| 1498 | 1591 | echo self::get_tooltip_icon_html($tooltip_id, $text); |
| 1499 | 1592 | } |
| 1500 | 1593 | |
| 1501 | 1594 | /** |