| @@ -16,9 +16,31 @@ | ||
| 16 | 16 | # uses the simple html dom library |
| 17 | 17 | use simplehtmldom\HtmlDocument; |
| 18 | 18 | |
| 19 | 19 | # include the otto class file |
| 20 | -require_once plugin_dir_path( __FILE__ ) . '/vendor/autoload.php'; | |
| 20 | +# | |
| 21 | +# The vendor tree is generated content, unlike the plugin files beside it: an | |
| 22 | +# update that copies the plugin directory piece by piece can briefly leave this | |
| 23 | +# file beside an otto/vendor that is missing, half-written or holding a stale | |
| 24 | +# classmap. A bare require on a missing autoloader is a fatal during plugin | |
| 25 | +# bootstrap — the whole site, not just OTTO. Load it only when it is actually | |
| 26 | +# readable, and catch Throwable while doing so: the autoloader is a trampoline | |
| 27 | +# of bare requires into otto/vendor/composer/, so a tree that is present but | |
| 28 | +# still mid-copy (or a truncated autoload.php, which parses only to a | |
| 29 | +# ParseError) would otherwise fatal here too. Either way the parser class | |
| 30 | +# stays unloaded, and the DOM-parser guard in metasync_start_otto() degrades | |
| 31 | +# the request to OTTO-less rendering instead of fataling. | |
| 32 | +$metasync_otto_vendor_autoload = plugin_dir_path( __FILE__ ) . '/vendor/autoload.php'; | |
| 33 | +if ( is_readable( $metasync_otto_vendor_autoload ) ) { | |
| 34 | + try { | |
| 35 | + require_once $metasync_otto_vendor_autoload; | |
| 36 | + } catch ( \Throwable $e ) { | |
| 37 | + # Deliberately swallowed: any exception raised while loading generated | |
| 38 | + # autoloader code means this request runs without the DOM parser. The | |
| 39 | + # guard below turns that into one OTTO-less request rather than a | |
| 40 | + # bootstrap fatal on the whole site. | |
| 41 | + } | |
| 42 | +} | |
| 21 | 43 | require_once plugin_dir_path( __FILE__ ) . '/Otto_html_class.php'; |
| 22 | 44 | require_once plugin_dir_path( __FILE__ ) . '/Otto_pixel_class.php'; |
| 23 | 45 | require_once plugin_dir_path( __FILE__ ) . '/metasync-otto-seo-functions.php'; |
| 24 | 46 | require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-transient-cache.php'; |
| @@ -77,9 +99,9 @@ | ||
| 77 | 99 | # Use endpoint manager to get the correct domain |
| 78 | 100 | $otto_domain = 'sa.searchatlas.com'; # default |
| 79 | 101 | if (class_exists('Metasync_Endpoint_Manager')) { |
| 80 | 102 | $otto_api_domain = Metasync_Endpoint_Manager::get_endpoint('OTTO_API_DOMAIN'); |
| 81 | - $parsed = parse_url($otto_api_domain); | |
| 103 | + $parsed = wp_parse_url($otto_api_domain); | |
| 82 | 104 | if (!empty($parsed['host'])) { |
| 83 | 105 | $otto_domain = $parsed['host']; |
| 84 | 106 | } |
| 85 | 107 | } |
| @@ -301,9 +323,9 @@ | ||
| 301 | 323 | // event may therefore invoke this callback without its required route. |
| 302 | 324 | // Docblock type isn't enforced at runtime; a stale cron record can pass a non-string. |
| 303 | 325 | // @phpstan-ignore-next-line function.alreadyNarrowedType |
| 304 | 326 | if (!is_string($route) || $route === '') { |
| 305 | - error_log('MetaSync OTTO: skipping crawl-url job without a valid route.'); | |
| 327 | + error_log('MetaSync OTTO: skipping crawl-url job without a valid route.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 306 | 328 | return; |
| 307 | 329 | } |
| 308 | 330 | |
| 309 | 331 | $max_retries = defined('METASYNC_OTTO_JOB_MAX_RETRIES') ? METASYNC_OTTO_JOB_MAX_RETRIES : 3; |
| @@ -319,13 +341,13 @@ | ||
| 319 | 341 | Metasync_Otto_Job_Status::record($route, Metasync_Otto_Job_Status::STATE_ACCEPTED, 'queue', $retry_count + 1, $reason . '/cron_refused'); |
| 320 | 342 | } else { |
| 321 | 343 | Metasync_Otto_Job_Status::record($route, Metasync_Otto_Job_Status::STATE_RETRYING, 'job', $retry_count + 1, $reason); |
| 322 | 344 | } |
| 323 | - error_log('MetaSync OTTO: retrying crawl-url job for ' . $route . ' (attempt ' . ($retry_count + 1) . '/' . $max_retries . ') reason=' . $reason . ' in ' . $delay . 's'); | |
| 345 | + error_log('MetaSync OTTO: retrying crawl-url job for ' . $route . ' (attempt ' . ($retry_count + 1) . '/' . $max_retries . ') reason=' . $reason . ' in ' . $delay . 's'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 324 | 346 | } else { |
| 325 | 347 | metasync_record_failed_action('metasync_process_otto_crawl_url_job'); |
| 326 | 348 | Metasync_Otto_Job_Status::record($route, Metasync_Otto_Job_Status::STATE_FAILED, 'job', $retry_count, 'exhausted:' . $reason); |
| 327 | - error_log('MetaSync OTTO: background crawl-url job permanently failed for ' . $route . ' after ' . $max_retries . ' retries, last reason=' . $reason); | |
| 349 | + error_log('MetaSync OTTO: background crawl-url job permanently failed for ' . $route . ' after ' . $max_retries . ' retries, last reason=' . $reason); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 328 | 350 | } |
| 329 | 351 | }; |
| 330 | 352 | |
| 331 | 353 | try { |
| @@ -350,9 +372,9 @@ | ||
| 350 | 372 | # attempt budget on warm calls that will keep being rejected. |
| 351 | 373 | if ($transient_cache->last_failure_is_permanent()) { |
| 352 | 374 | metasync_record_failed_action('metasync_process_otto_crawl_url_job'); |
| 353 | 375 | Metasync_Otto_Job_Status::record($route, Metasync_Otto_Job_Status::STATE_FAILED, 'job', $retry_count, 'permanent:warm_rejected'); |
| 354 | - error_log('MetaSync OTTO: crawl-url job failed permanently for ' . $route . ' (warm request rejected by the API)'); | |
| 376 | + error_log('MetaSync OTTO: crawl-url job failed permanently for ' . $route . ' (warm request rejected by the API)'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 355 | 377 | return; |
| 356 | 378 | } |
| 357 | 379 | # Transient API failure (timeout / 5xx / breaker open). No meta |
| 358 | 380 | # write, no purge — straight to the retry path. |
| @@ -429,9 +451,9 @@ | ||
| 429 | 451 | // event may therefore invoke this callback without its required route list. |
| 430 | 452 | // Docblock type isn't enforced at runtime; a stale cron record can pass a non-array. |
| 431 | 453 | // @phpstan-ignore-next-line function.alreadyNarrowedType |
| 432 | 454 | if (!is_array($routes) || empty($routes)) { |
| 433 | - error_log('MetaSync OTTO: skipping batch cache job without valid routes.'); | |
| 455 | + error_log('MetaSync OTTO: skipping batch cache job without valid routes.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 434 | 456 | return; |
| 435 | 457 | } |
| 436 | 458 | |
| 437 | 459 | $max_waits = defined('METASYNC_OTTO_BATCH_MAX_WAITS') ? METASYNC_OTTO_BATCH_MAX_WAITS : 3; |
| @@ -473,9 +495,9 @@ | ||
| 473 | 495 | # Tag-based providers purge only the affected posts; full-flush providers fire once per batch. |
| 474 | 496 | Metasync_Edge_Cache_Purge::purge($ready); |
| 475 | 497 | } catch (Exception $e) { |
| 476 | 498 | metasync_record_failed_action('metasync_process_otto_batch_cache_job'); |
| 477 | - error_log('MetaSync OTTO: batch cache job failed for ' . count($ready) . ' URLs: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); | |
| 499 | + error_log('MetaSync OTTO: batch cache job failed for ' . count($ready) . ' URLs: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 478 | 500 | } |
| 479 | 501 | } |
| 480 | 502 | |
| 481 | 503 | /** |
| @@ -522,42 +544,8 @@ | ||
| 522 | 544 | metasync_otto_schedule_single_event($now + 60, 'metasync_otto_pending_drainer', array()); |
| 523 | 545 | } |
| 524 | 546 | } |
| 525 | 547 | |
| 526 | -# NOTE: Cache system removed - these functions are no longer needed | |
| 527 | -# Kept for backward compatibility in case old cache directories need cleanup | |
| 528 | -function metasync_deleteDir($dir) { | |
| 529 | - if (!is_dir($dir)) { | |
| 530 | - return false; | |
| 531 | - } | |
| 532 | - $files = array_diff(scandir($dir), array('.', '..')); | |
| 533 | - foreach ($files as $file) { | |
| 534 | - $filePath = $dir . DIRECTORY_SEPARATOR . $file; | |
| 535 | - if (is_dir($filePath)) { | |
| 536 | - metasync_deleteDir($filePath); | |
| 537 | - } else { | |
| 538 | - unlink($filePath); | |
| 539 | - } | |
| 540 | - } | |
| 541 | - return rmdir($dir); | |
| 542 | -} | |
| 543 | - | |
| 544 | -# Cleanup function for removing old cache directories (if they exist) | |
| 545 | -function metasync_invalidate_all_caches($folder = ''){ | |
| 546 | - # Cache system removed - this function only exists to clean up old cache directories | |
| 547 | - if(!defined('WP_CONTENT_DIR')){ | |
| 548 | - return false; | |
| 549 | - } | |
| 550 | - $wp_content_dir = WP_CONTENT_DIR; | |
| 551 | - $cache_dir = $wp_content_dir . '/metasync_caches'; | |
| 552 | - if(in_array($folder, ['posts', 'pages'])){ | |
| 553 | - $cache_dir = $cache_dir . '/' . $folder; | |
| 554 | - } | |
| 555 | - if(is_dir($cache_dir)){ | |
| 556 | - metasync_deleteDir($cache_dir); | |
| 557 | - } | |
| 558 | -} | |
| 559 | - | |
| 560 | 548 | // metasync_is_custom_or_lps_page() now lives in includes/metasync-helpers.php |
| 561 | 549 | // (loaded unconditionally before this file) so all SEO surfaces share one rule. |
| 562 | 550 | |
| 563 | 551 | /** |
| @@ -580,15 +568,15 @@ | ||
| 580 | 568 | */ |
| 581 | 569 | function metasync_is_elementor_editor_request() { |
| 582 | 570 | # The preview iframe loads the front end with ?elementor-preview=POST_ID. |
| 583 | 571 | # This is the request OTTO would otherwise buffer and corrupt. |
| 584 | - if (isset($_GET['elementor-preview'])) { | |
| 572 | + if (isset($_GET['elementor-preview'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only editor request detection | |
| 585 | 573 | return true; |
| 586 | 574 | } |
| 587 | 575 | |
| 588 | 576 | # Elementor editor / app entry points carried as an action on the front end. |
| 589 | - if (isset($_REQUEST['action']) | |
| 590 | - && in_array($_REQUEST['action'], array('elementor', 'elementor_ajax'), true) | |
| 577 | + if (isset($_REQUEST['action']) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only editor request detection | |
| 578 | + && in_array($_REQUEST['action'], array('elementor', 'elementor_ajax'), true) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only editor request detection | |
| 591 | 579 | ) { |
| 592 | 580 | return true; |
| 593 | 581 | } |
| 594 | 582 | |
| @@ -639,10 +627,10 @@ | ||
| 639 | 627 | # and OTTO's route identity ignores the query string, so a processed preview |
| 640 | 628 | # is answered with the route's public page (the homepage for /?p=…) instead |
| 641 | 629 | # of the draft the editor asked for. Skip early and let WP render it. |
| 642 | 630 | if ( |
| 643 | - (isset($_GET['preview']) && $_GET['preview'] === 'true') || | |
| 644 | - !empty($_GET['preview_id']) | |
| 631 | + (isset($_GET['preview']) && $_GET['preview'] === 'true') || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 632 | + !empty($_GET['preview_id']) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 645 | 633 | ) { |
| 646 | 634 | if (!defined('DONOTCACHEPAGE')) { |
| 647 | 635 | define('DONOTCACHEPAGE', true); |
| 648 | 636 | } |
| @@ -682,9 +670,9 @@ | ||
| 682 | 670 | # then lower-case and strip surrounding slashes. |
| 683 | 671 | $otto_req_path = explode('?', (string) wp_unslash($_SERVER['REQUEST_URI'] ?? ''), 2)[0]; |
| 684 | 672 | $otto_req_path = strtolower(trim($otto_req_path, '/')); |
| 685 | 673 | # Strip the site's base path so first-segment matching works on subdir installs. |
| 686 | - $otto_home_path = trim((string) parse_url(home_url(), PHP_URL_PATH), '/'); | |
| 674 | + $otto_home_path = trim((string) wp_parse_url(home_url(), PHP_URL_PATH), '/'); | |
| 687 | 675 | if ($otto_home_path !== '' && strpos($otto_req_path, $otto_home_path . '/') === 0) { |
| 688 | 676 | $otto_req_path = substr($otto_req_path, strlen($otto_home_path) + 1); |
| 689 | 677 | } |
| 690 | 678 | $otto_first_segment = explode('/', $otto_req_path, 2)[0]; |
| @@ -702,11 +690,11 @@ | ||
| 702 | 690 | # ────────────────────────────────────────────────────────────────────── |
| 703 | 691 | |
| 704 | 692 | if ( |
| 705 | 693 | # disable ajax calls |
| 706 | - isset($_GET['ucfrontajaxaction']) || | |
| 694 | + isset($_GET['ucfrontajaxaction']) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 707 | 695 | # OTTO Preview mode - skip OTTO when previewing original content |
| 708 | - (isset($_GET['otto_preview']) && $_GET['otto_preview'] === '1') || | |
| 696 | + (isset($_GET['otto_preview']) && $_GET['otto_preview'] === '1') || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 709 | 697 | # WooCommerce shop archive page only (products and categories now use SSR) |
| 710 | 698 | //(function_exists('is_shop') && is_shop()) || |
| 711 | 699 | # Cart page |
| 712 | 700 | (function_exists('is_cart') && is_cart()) || |
| @@ -718,32 +706,32 @@ | ||
| 718 | 706 | (function_exists('wp_doing_ajax') && wp_doing_ajax()) || |
| 719 | 707 | # check by constant |
| 720 | 708 | (defined('DOING_AJAX') && DOING_AJAX) || |
| 721 | 709 | # WooCommerce AJAX endpoint (e.g., ?wc-ajax=update_cart) |
| 722 | - (isset($_REQUEST['wc-ajax']) && !empty($_REQUEST['wc-ajax'])) || | |
| 710 | + (isset($_REQUEST['wc-ajax']) && !empty($_REQUEST['wc-ajax'])) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 723 | 711 | # AJAX requests via X-Requested-With header |
| 724 | 712 | (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || |
| 725 | 713 | # Gravity Forms submission detection - skip OTTO to allow form processing |
| 726 | - (isset($_POST['gform_submit']) && ( | |
| 727 | - is_array($_POST['gform_submit']) || | |
| 728 | - (is_string($_POST['gform_submit']) && isset($_POST['is_submit_' . $_POST['gform_submit']]) && !empty($_POST['gform_submit'])) | |
| 714 | + (isset($_POST['gform_submit']) && ( // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 715 | + is_array($_POST['gform_submit']) || // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 716 | + (is_string($_POST['gform_submit']) && isset($_POST['is_submit_' . $_POST['gform_submit']]) && !empty($_POST['gform_submit'])) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 729 | 717 | )) || |
| 730 | 718 | # Gravity Forms AJAX submission |
| 731 | - (isset($_POST['gform_ajax']) && isset($_POST['gform_submit']) && ( | |
| 732 | - is_array($_POST['gform_submit']) || !empty($_POST['gform_submit']) | |
| 719 | + (isset($_POST['gform_ajax']) && isset($_POST['gform_submit']) && ( // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 720 | + is_array($_POST['gform_submit']) || !empty($_POST['gform_submit']) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 733 | 721 | )) || |
| 734 | 722 | # Gravity Forms file upload |
| 735 | - (isset($_POST['gform_uploaded_files'])) || | |
| 723 | + (isset($_POST['gform_uploaded_files'])) || // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 736 | 724 | # Any Gravity Forms POST parameter |
| 737 | - (isset($_POST['gform_submit']) || isset($_POST['gform_unique_id']) || isset($_POST['gform_field_values'])) || | |
| 725 | + (isset($_POST['gform_submit']) || isset($_POST['gform_unique_id']) || isset($_POST['gform_field_values'])) || // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 738 | 726 | # Formidable Forms AJAX submission detection - skip OTTO to allow form processing |
| 739 | - (isset($_POST['action']) && $_POST['action'] === 'frm_entries_create') || | |
| 727 | + (isset($_POST['action']) && $_POST['action'] === 'frm_entries_create') || // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 740 | 728 | # Formidable Forms POST parameters |
| 741 | - (isset($_POST['form_id']) && !empty($_POST['form_id'])) || | |
| 729 | + (isset($_POST['form_id']) && !empty($_POST['form_id'])) || // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 742 | 730 | # Formidable Forms action parameter |
| 743 | - (isset($_POST['frm_action']) && !empty($_POST['frm_action'])) || | |
| 731 | + (isset($_POST['frm_action']) && !empty($_POST['frm_action'])) || // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 744 | 732 | # Formidable Forms item_key (used in form submissions) |
| 745 | - (isset($_POST['item_key']) && !empty($_POST['item_key'])) | |
| 733 | + (isset($_POST['item_key']) && !empty($_POST['item_key'])) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only front-end check that skips OTTO | |
| 746 | 734 | ) { |
| 747 | 735 | return; |
| 748 | 736 | } |
| 749 | 737 | |
| @@ -814,9 +802,22 @@ | ||
| 814 | 802 | } |
| 815 | 803 | |
| 816 | 804 | # A headless frontend renders the public HTML, so stop after the cheap cache |
| 817 | 805 | # protections and crawl telemetry, before any OTTO rendering work begins. |
| 818 | - if (Metasync_Headless_Config::is_active()) { | |
| 806 | + # | |
| 807 | + # Routed through metasync_headless_is_active() rather than calling the class | |
| 808 | + # directly: Metasync_Headless_Config resolves only through the committed | |
| 809 | + # Composer classmap, so a partially updated install can leave this newer | |
| 810 | + # caller beside a missing or older copy with no fallback lookup path. This | |
| 811 | + # function is hooked to `wp`, so an unguarded static call is a white screen | |
| 812 | + # on every front-end page view. The helper treats an unresolvable class as | |
| 813 | + # "not headless", which keeps OTTO behaving as it did before headless mode. | |
| 814 | + # @phpstan-ignore-next-line function.alreadyNarrowedType | |
| 815 | + $metasync_is_headless = function_exists('metasync_headless_is_active') | |
| 816 | + ? metasync_headless_is_active() | |
| 817 | + : (class_exists('Metasync_Headless_Config') && Metasync_Headless_Config::is_active()); | |
| 818 | + | |
| 819 | + if ($metasync_is_headless) { | |
| 819 | 820 | return; |
| 820 | 821 | } |
| 821 | 822 | |
| 822 | 823 | # Throttle OTTO rendering for bots that are not exempt: at most one render |
| @@ -901,9 +902,9 @@ | ||
| 901 | 902 | # ?et_blog = Divi AJAX pagination callback |
| 902 | 903 | # /page/N/ = paginated blog/archive pages — OTTO's buffer/HTTP render causes |
| 903 | 904 | # module numbering mismatch between page 1 (with TB template) and page N |
| 904 | 905 | # (without TB template), breaking Divi's JS pagination selector matching. |
| 905 | - if (isset($_GET['et_blog']) || (is_paged() && !is_singular())) { | |
| 906 | + if (isset($_GET['et_blog']) || (is_paged() && !is_singular())) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 906 | 907 | return; |
| 907 | 908 | } |
| 908 | 909 | |
| 909 | 910 | # Skip OTTO on search-results pages. A search request carries its |
| @@ -918,9 +919,9 @@ | ||
| 918 | 919 | # correct pre-rendered search page to everyone else. Search-results pages |
| 919 | 920 | # have no per-canonical-URL OTTO suggestions of their own, so OTTO must never |
| 920 | 921 | # run here. The raw $_GET['s'] check is a defensive fallback for setups where |
| 921 | 922 | # the main query is altered before this point. |
| 922 | - if (is_search() || !empty($_GET['s'])) { | |
| 923 | + if (is_search() || !empty($_GET['s'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 923 | 924 | return; |
| 924 | 925 | } |
| 925 | 926 | |
| 926 | 927 | # Handle cache plugin compatibility early - before any caching happens |
| @@ -972,9 +973,9 @@ | ||
| 972 | 973 | $otto_has_fetch_detector = class_exists('Metasync_Otto_Render_Strategy') && method_exists('Metasync_Otto_Render_Strategy', 'is_internal_fetch'); |
| 973 | 974 | |
| 974 | 975 | $otto_is_internal_fetch = $otto_has_fetch_detector |
| 975 | 976 | ? Metasync_Otto_Render_Strategy::is_internal_fetch() |
| 976 | - : !empty($_GET['is_otto_page_fetch']); | |
| 977 | + : !empty($_GET['is_otto_page_fetch']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only loopback marker, request-scoped filters only | |
| 977 | 978 | |
| 978 | 979 | if($otto_is_internal_fetch){ |
| 979 | 980 | |
| 980 | 981 | # Block SEO plugins NOW for this internal fetch request |
| @@ -979,10 +980,10 @@ | ||
| 979 | 980 | |
| 980 | 981 | # Block SEO plugins NOW for this internal fetch request |
| 981 | 982 | # metasync_otto_block_seo_plugins(); |
| 982 | 983 | # $_SERVER['REQUEST_URI'] = remove_query_arg('is_otto_page_fetch', $_SERVER['REQUEST_URI']); |
| 983 | - $block_title = !empty($_GET['otto_block_title']) && $_GET['otto_block_title'] === '1'; | |
| 984 | - $block_description = !empty($_GET['otto_block_desc']) && $_GET['otto_block_desc'] === '1'; | |
| 984 | + $block_title = !empty($_GET['otto_block_title']) && $_GET['otto_block_title'] === '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only loopback marker, request-scoped filters only | |
| 985 | + $block_description = !empty($_GET['otto_block_desc']) && $_GET['otto_block_desc'] === '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only loopback marker, request-scoped filters only | |
| 985 | 986 | |
| 986 | 987 | # Block SEO plugins conditionally based on what Otto has |
| 987 | 988 | if ($block_title || $block_description) { |
| 988 | 989 | metasync_otto_block_seo_plugins($block_title, $block_description); |
| @@ -1018,8 +1019,19 @@ | ||
| 1018 | 1019 | |
| 1019 | 1020 | # get the otto uuid |
| 1020 | 1021 | $otto_uuid = Metasync_Otto_Config::get_otto_uuid(); |
| 1021 | 1022 | |
| 1023 | + # Upgrade-window guard: the bundled DOM parser resolves only through the | |
| 1024 | + # otto/vendor autoloader required at the top of this file. While an update | |
| 1025 | + # is mid-copy (or a stale opcache classmap is still resident), that | |
| 1026 | + # dependency can briefly be undefined here, and constructing the render | |
| 1027 | + # stack below would throw a class-not-found Error and take the whole page | |
| 1028 | + # down with it. Degrade to an OTTO-less page for this request instead — | |
| 1029 | + # the next request, on the settled tree, renders normally. | |
| 1030 | + if ( ! class_exists( 'simplehtmldom\HtmlDocument' ) ) { | |
| 1031 | + return; | |
| 1032 | + } | |
| 1033 | + | |
| 1022 | 1034 | # start the class |
| 1023 | 1035 | $otto = new Metasync_otto_pixel($otto_uuid); |
| 1024 | 1036 | |
| 1025 | 1037 | # call render |
| @@ -1051,9 +1063,9 @@ | ||
| 1051 | 1063 | if ($cached !== false) { |
| 1052 | 1064 | $has_brizy_posts = ($cached === 'yes'); |
| 1053 | 1065 | } else { |
| 1054 | 1066 | # Query database only if cache missed |
| 1055 | - $has_brizy_posts = $wpdb->get_var( | |
| 1067 | + $has_brizy_posts = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- feature-detection probe for Brizy posts — postmeta flag scan has no WordPress API | |
| 1056 | 1068 | "SELECT COUNT(*) FROM {$wpdb->postmeta} |
| 1057 | 1069 | WHERE meta_key = 'brizy_post_uid' |
| 1058 | 1070 | AND meta_value != '' |
| 1059 | 1071 | LIMIT 1" |
| @@ -1653,8 +1665,17 @@ | ||
| 1653 | 1665 | |
| 1654 | 1666 | # Handle AJAX Clear Cache request |
| 1655 | 1667 | # NOTE: Cache system removed - this is now a no-op |
| 1656 | 1668 | function metasync_clear_otto_cache_handler() { |
| 1669 | + # State-changing admin action: verify the nonce sent by admin/js | |
| 1670 | + # (clear_otto_caches) and confirm plugin access before clearing the | |
| 1671 | + # detection transient or queueing the loopback re-check. | |
| 1672 | + check_ajax_referer('metasync_clear_otto_cache', 'nonce'); | |
| 1673 | + | |
| 1674 | + if (!Metasync::current_user_has_plugin_access()) { | |
| 1675 | + wp_send_json_error(['message' => 'Not authorized']); | |
| 1676 | + } | |
| 1677 | + | |
| 1657 | 1678 | if (!empty($_GET['clear_otto_cache'])) { |
| 1658 | 1679 | delete_transient('metasync_otto_js_detected'); |
| 1659 | 1680 | # Re-run the JS detection in the background so the notice refreshes. |
| 1660 | 1681 | metasync_schedule_otto_js_check(); |
| @@ -1791,9 +1812,8 @@ | ||
| 1791 | 1812 | } |
| 1792 | 1813 | |
| 1793 | 1814 | # Pre-flight 404 check: exclude URLs that would return 404 before making API call |
| 1794 | 1815 | if (!metasync_otto_is_url_available($route)) { |
| 1795 | - error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404: {$route}"); | |
| 1796 | 1816 | metasync_otto_auto_exclude_404_url($route); |
| 1797 | 1817 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 1798 | 1818 | } |
| 1799 | 1819 | |
| @@ -1832,9 +1852,9 @@ | ||
| 1832 | 1852 | } |
| 1833 | 1853 | |
| 1834 | 1854 | # Mark this URL as crawled by OTTO for SSR |
| 1835 | 1855 | # Extract domain and path from route |
| 1836 | - $parsed_url = parse_url($route); | |
| 1856 | + $parsed_url = wp_parse_url($route); | |
| 1837 | 1857 | $domain_with_scheme = ($parsed_url['scheme'] ?? 'https') . '://' . ($parsed_url['host'] ?? ''); |
| 1838 | 1858 | $url_path = ($parsed_url['path'] ?? '/'); |
| 1839 | 1859 | |
| 1840 | 1860 | # Create crawl data structure |
| @@ -1867,9 +1887,9 @@ | ||
| 1867 | 1887 | |
| 1868 | 1888 | # Try to find WooCommerce product by URL if url_to_postid failed |
| 1869 | 1889 | if ((!$post_id || $post_id <= 0) && strpos($route, '/product/') !== false && function_exists('wc_get_products')) { |
| 1870 | 1890 | # Extract product slug from URL |
| 1871 | - $product_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 1891 | + $product_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 1872 | 1892 | |
| 1873 | 1893 | # Try to get product by slug |
| 1874 | 1894 | $products = wc_get_products(array( |
| 1875 | 1895 | 'name' => $product_slug, |
| @@ -1900,15 +1920,14 @@ | ||
| 1900 | 1920 | if (!$post_id || $post_id <= 0) { |
| 1901 | 1921 | # Check if this is a category page |
| 1902 | 1922 | if (strpos($route, '/category/') !== false) { |
| 1903 | 1923 | # Extract category slug from URL |
| 1904 | - $category_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 1924 | + $category_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 1905 | 1925 | $category = get_category_by_slug($category_slug); |
| 1906 | 1926 | |
| 1907 | 1927 | if ($category) { |
| 1908 | 1928 | # Check if category would return 404 before applying OTTO changes |
| 1909 | 1929 | if (metasync_would_term_return_404($category->term_id, 'category', $route)) { |
| 1910 | - error_log("MetaSync OTTO: Skipping SEO processing for category that would return 404: {$route} (Category ID: {$category->term_id})"); | |
| 1911 | 1930 | metasync_otto_auto_exclude_404_url($route); |
| 1912 | 1931 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 1913 | 1932 | } |
| 1914 | 1933 | |
| @@ -2004,15 +2023,14 @@ | ||
| 2004 | 2023 | |
| 2005 | 2024 | # Check if this is a WooCommerce product category |
| 2006 | 2025 | if (strpos($route, '/product-category/') !== false) { |
| 2007 | 2026 | # Extract product category slug from URL |
| 2008 | - $category_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 2027 | + $category_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 2009 | 2028 | $term = get_term_by('slug', $category_slug, 'product_cat'); |
| 2010 | 2029 | |
| 2011 | 2030 | if ($term && !is_wp_error($term)) { |
| 2012 | 2031 | # Check if product category would return 404 before applying OTTO changes |
| 2013 | 2032 | if (metasync_would_term_return_404($term->term_id, 'product_cat', $route)) { |
| 2014 | - error_log("MetaSync OTTO: Skipping SEO processing for product category that would return 404: {$route} (Term ID: {$term->term_id})"); | |
| 2015 | 2033 | metasync_otto_auto_exclude_404_url($route); |
| 2016 | 2034 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2017 | 2035 | } |
| 2018 | 2036 | |
| @@ -2125,9 +2143,8 @@ | ||
| 2125 | 2143 | |
| 2126 | 2144 | if ($home_page) { |
| 2127 | 2145 | # Check if home page would return 404 before applying OTTO changes |
| 2128 | 2146 | if (metasync_would_page_return_404($home_page->ID, $route)) { |
| 2129 | - error_log("MetaSync OTTO: Skipping SEO processing for home page that would return 404: {$route} (Post ID: {$home_page->ID}, Status: {$home_page->post_status})"); | |
| 2130 | 2147 | metasync_otto_auto_exclude_404_url($route); |
| 2131 | 2148 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2132 | 2149 | } |
| 2133 | 2150 | |
| @@ -2231,9 +2248,8 @@ | ||
| 2231 | 2248 | |
| 2232 | 2249 | if ($posts_page && $route_clean === $posts_page_url) { |
| 2233 | 2250 | # Check if blog page would return 404 |
| 2234 | 2251 | if (metasync_would_page_return_404($posts_page->ID, $route)) { |
| 2235 | - error_log("MetaSync OTTO: Skipping SEO processing for blog page that would return 404: {$route} (Post ID: {$posts_page->ID}, Status: {$posts_page->post_status})"); | |
| 2236 | 2252 | metasync_otto_auto_exclude_404_url($route); |
| 2237 | 2253 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2238 | 2254 | } |
| 2239 | 2255 | |
| @@ -2331,9 +2347,8 @@ | ||
| 2331 | 2347 | |
| 2332 | 2348 | # URL didn't resolve to any supported entity (post, category, home page, blog page) |
| 2333 | 2349 | # Treat as 404 and auto-exclude (e.g. deleted post, non-existent page) |
| 2334 | 2350 | if (!metasync_otto_is_url_available($route)) { |
| 2335 | - error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404 (no matching entity): {$route}"); | |
| 2336 | 2351 | metasync_otto_auto_exclude_404_url($route); |
| 2337 | 2352 | } |
| 2338 | 2353 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2339 | 2354 | } |
| @@ -2350,9 +2365,8 @@ | ||
| 2350 | 2365 | } |
| 2351 | 2366 | |
| 2352 | 2367 | # Check if page would return 404 before applying OTTO changes |
| 2353 | 2368 | if (metasync_would_page_return_404($post_id, $route)) { |
| 2354 | - error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404: {$route} (Post ID: {$post_id}, Status: {$post->post_status})"); | |
| 2355 | 2369 | metasync_otto_auto_exclude_404_url($route); |
| 2356 | 2370 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2357 | 2371 | } |
| 2358 | 2372 | |
| @@ -2470,11 +2484,10 @@ | ||
| 2470 | 2484 | |
| 2471 | 2485 | // Minimal duplicate prevention within short time window |
| 2472 | 2486 | if (!empty($data['title']) && !empty($data['source'])) { |
| 2473 | 2487 | global $wpdb; |
| 2474 | - $table = $wpdb->prefix . Metasync_Sync_History_Database::$table_name; | |
| 2475 | - $recent = $wpdb->get_var($wpdb->prepare( | |
| 2476 | - "SELECT COUNT(*) FROM `$table` WHERE title = %s AND source = %s AND created_at >= %s", | |
| 2488 | + $recent = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- sync-history duplicate guard over the plugin custom table — write-path freshness required | |
| 2489 | + "SELECT COUNT(*) FROM `{$wpdb->prefix}metasync_sync_history` WHERE title = %s AND source = %s AND created_at >= %s", | |
| 2477 | 2490 | $data['title'], |
| 2478 | 2491 | $data['source'], |
| 2479 | 2492 | gmdate('Y-m-d H:i:s', time() - 60) |
| 2480 | 2493 | )); |
| @@ -2485,9 +2498,9 @@ | ||
| 2485 | 2498 | |
| 2486 | 2499 | $sync_db->add($data); |
| 2487 | 2500 | |
| 2488 | 2501 | } catch (Exception $e) { |
| 2489 | - error_log("MetaSync: Failed to log sync history: " . $e->getMessage()); | |
| 2502 | + error_log("MetaSync: Failed to log sync history: " . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 2490 | 2503 | } |
| 2491 | 2504 | } |
| 2492 | 2505 | |
| 2493 | 2506 | /** |
| @@ -2563,9 +2576,9 @@ | ||
| 2563 | 2576 | $db = new Metasync_Redirection_Database(); |
| 2564 | 2577 | $redirect = new Metasync_Redirection($db); |
| 2565 | 2578 | return $redirect->resolve_url_to_final_destination($url, 10); |
| 2566 | 2579 | } catch (Exception $e) { |
| 2567 | - error_log('MetaSync OTTO: Redirect resolution failed for ' . $url . ' - ' . $e->getMessage()); | |
| 2580 | + error_log('MetaSync OTTO: Redirect resolution failed for ' . $url . ' - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 2568 | 2581 | return $url; |
| 2569 | 2582 | } |
| 2570 | 2583 | } |
| 2571 | 2584 | |
| @@ -2596,9 +2609,9 @@ | ||
| 2596 | 2609 | 'status' => 'active', |
| 2597 | 2610 | 'auto_excluded' => 1, |
| 2598 | 2611 | ]); |
| 2599 | 2612 | } catch (Exception $e) { |
| 2600 | - error_log('MetaSync OTTO: Failed to auto-exclude 404 URL: ' . $url . ' - ' . $e->getMessage()); | |
| 2613 | + error_log('MetaSync OTTO: Failed to auto-exclude 404 URL: ' . $url . ' - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 2601 | 2614 | return false; |
| 2602 | 2615 | } |
| 2603 | 2616 | } |
| 2604 | 2617 | |
| @@ -2618,14 +2631,13 @@ | ||
| 2618 | 2631 | try { |
| 2619 | 2632 | require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php'; |
| 2620 | 2633 | $db = new Metasync_Otto_Excluded_URLs_Database(); |
| 2621 | 2634 | global $wpdb; |
| 2622 | - $table = $wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name; | |
| 2623 | 2635 | # Normalize the same way is_url_excluded() does |
| 2624 | 2636 | $url_normalized = rtrim(trim($url), '/'); |
| 2625 | - $records = $wpdb->get_results( | |
| 2637 | + $records = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- webhook-path cleanup of auto-exclusions over the plugin custom table | |
| 2626 | 2638 | $wpdb->prepare( |
| 2627 | - "SELECT id FROM `{$table}` WHERE url_pattern = %s AND auto_excluded = 1 AND status = 'active'", | |
| 2639 | + "SELECT id FROM `{$wpdb->prefix}metasync_otto_excluded_urls` WHERE url_pattern = %s AND auto_excluded = 1 AND status = 'active'", | |
| 2628 | 2640 | $url_normalized |
| 2629 | 2641 | ) |
| 2630 | 2642 | ); |
| 2631 | 2643 | if (!empty($records)) { |
| @@ -2655,24 +2667,26 @@ | ||
| 2655 | 2667 | } |
| 2656 | 2668 | try { |
| 2657 | 2669 | require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php'; |
| 2658 | 2670 | global $wpdb; |
| 2659 | - $table = $wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name; | |
| 2660 | 2671 | $url_normalized = rtrim(trim($url), '/'); |
| 2661 | 2672 | |
| 2662 | 2673 | $records = get_transient(METASYNC_OTTO_EXCLUDED_TRANSIENT_KEY); |
| 2663 | 2674 | |
| 2664 | 2675 | if ($records === false) { |
| 2665 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- no user input, table name from $wpdb->prefix | |
| 2666 | - $records = $wpdb->get_results( | |
| 2667 | - "SELECT url_pattern, pattern_type FROM `{$table}` WHERE status = 'active' AND (auto_excluded = 0 OR auto_excluded IS NULL) ORDER BY created_at DESC" | |
| 2676 | + $records = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- render-path read IS cached via get/set_transient with invalidation on every write — wp_cache_* is not used because the payload must survive object-cache-only setups | |
| 2677 | + "SELECT url_pattern, pattern_type FROM `{$wpdb->prefix}metasync_otto_excluded_urls` WHERE status = 'active' AND (auto_excluded = 0 OR auto_excluded IS NULL) ORDER BY created_at DESC" | |
| 2668 | 2678 | ); |
| 2669 | 2679 | |
| 2670 | 2680 | // Graceful recovery: auto_excluded column missing on pre-v2.7.4 installs. |
| 2671 | - // Run ALTER TABLE to add it and treat URL as not excluded so OTTO continues rendering. | |
| 2681 | + // Schema changes belong to the upgrade routines, never the render path: | |
| 2682 | + // degrade to "not excluded" so OTTO keeps rendering, and schedule a | |
| 2683 | + // one-shot background repair that runs the same column migration off | |
| 2684 | + // the visitor request (handled by metasync_repair_otto_excluded_schema). | |
| 2672 | 2685 | if ($wpdb->last_error && strpos($wpdb->last_error, 'auto_excluded') !== false) { |
| 2673 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared | |
| 2674 | - $wpdb->query("ALTER TABLE `{$table}` ADD COLUMN `auto_excluded` TINYINT(1) NOT NULL DEFAULT 0"); | |
| 2686 | + if (false === wp_next_scheduled('metasync_repair_otto_excluded_schema')) { | |
| 2687 | + wp_schedule_single_event(time() + MINUTE_IN_SECONDS, 'metasync_repair_otto_excluded_schema'); | |
| 2688 | + } | |
| 2675 | 2689 | return false; |
| 2676 | 2690 | } |
| 2677 | 2691 | |
| 2678 | 2692 | set_transient(METASYNC_OTTO_EXCLUDED_TRANSIENT_KEY, $records ?: [], METASYNC_OTTO_EXCLUDED_TRANSIENT_TTL); |
| @@ -2748,9 +2762,9 @@ | ||
| 2748 | 2762 | } |
| 2749 | 2763 | |
| 2750 | 2764 | # WooCommerce product by slug |
| 2751 | 2765 | if ((!$post_id || $post_id <= 0) && strpos($route, '/product/') !== false && function_exists('wc_get_products')) { |
| 2752 | - $product_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 2766 | + $product_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 2753 | 2767 | $products = wc_get_products(array('name' => $product_slug, 'limit' => 1, 'status' => 'publish')); |
| 2754 | 2768 | if (!empty($products)) { |
| 2755 | 2769 | $post_id = $products[0]->get_id(); |
| 2756 | 2770 | } else { |
| @@ -2777,9 +2791,9 @@ | ||
| 2777 | 2791 | } |
| 2778 | 2792 | |
| 2779 | 2793 | # Category |
| 2780 | 2794 | if (strpos($route, '/category/') !== false) { |
| 2781 | - $category_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 2795 | + $category_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 2782 | 2796 | $category = get_category_by_slug($category_slug); |
| 2783 | 2797 | if ($category) { |
| 2784 | 2798 | return !metasync_would_term_return_404($category->term_id, 'category', $route); |
| 2785 | 2799 | } |
| @@ -2786,9 +2800,9 @@ | ||
| 2786 | 2800 | } |
| 2787 | 2801 | |
| 2788 | 2802 | # WooCommerce product category |
| 2789 | 2803 | if (strpos($route, '/product-category/') !== false) { |
| 2790 | - $category_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 2804 | + $category_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 2791 | 2805 | $term = get_term_by('slug', $category_slug, 'product_cat'); |
| 2792 | 2806 | if ($term && !is_wp_error($term)) { |
| 2793 | 2807 | return !metasync_would_term_return_404($term->term_id, 'product_cat', $route); |
| 2794 | 2808 | } |
| @@ -2830,9 +2844,9 @@ | ||
| 2830 | 2844 | |
| 2831 | 2845 | $removed = 0; |
| 2832 | 2846 | $marked_permanent = 0; |
| 2833 | 2847 | $thirty_days_ago = strtotime('-30 days'); |
| 2834 | - $next_recheck = date('Y-m-d H:i:s', current_time('timestamp') + 7 * DAY_IN_SECONDS); | |
| 2848 | + $next_recheck = gmdate('Y-m-d H:i:s', current_time('timestamp') + 7 * DAY_IN_SECONDS); | |
| 2835 | 2849 | |
| 2836 | 2850 | foreach ($records as $record) { |
| 2837 | 2851 | $url = trim($record->url_pattern); |
| 2838 | 2852 | if (empty($url)) { |
| @@ -2853,16 +2867,10 @@ | ||
| 2853 | 2867 | } |
| 2854 | 2868 | } |
| 2855 | 2869 | } |
| 2856 | 2870 | |
| 2857 | - if ($removed > 0) { | |
| 2858 | - error_log("MetaSync OTTO: Recheck 404 exclusions - removed {$removed} URL(s) that are now available"); | |
| 2859 | - } | |
| 2860 | - if ($marked_permanent > 0) { | |
| 2861 | - error_log("MetaSync OTTO: Recheck 404 exclusions - marked {$marked_permanent} URL(s) as permanent (still 404 after 30 days)"); | |
| 2862 | - } | |
| 2863 | 2871 | } catch (Exception $e) { |
| 2864 | - error_log('MetaSync OTTO: Recheck 404 exclusions failed - ' . $e->getMessage()); | |
| 2872 | + error_log('MetaSync OTTO: Recheck 404 exclusions failed - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 2865 | 2873 | } |
| 2866 | 2874 | } |
| 2867 | 2875 | |
| 2868 | 2876 | add_action('metasync_otto_recheck_404_exclusions', 'metasync_otto_recheck_404_exclusions'); |
| @@ -2867,8 +2875,24 @@ | ||
| 2867 | 2875 | |
| 2868 | 2876 | add_action('metasync_otto_recheck_404_exclusions', 'metasync_otto_recheck_404_exclusions'); |
| 2869 | 2877 | |
| 2870 | 2878 | /** |
| 2879 | + * Background repair of the metasync_otto_excluded_urls schema (cron). | |
| 2880 | + * | |
| 2881 | + * Scheduled by metasync_is_otto_url_manually_excluded() when a render-path | |
| 2882 | + * read fails because the auto_excluded column is missing (pre-2.7.4 install | |
| 2883 | + * whose version migration did not run). Runs the DB layer's self-healing | |
| 2884 | + * column migration off the visitor request instead of ALTERing inline. | |
| 2885 | + */ | |
| 2886 | +function metasync_repair_otto_excluded_schema() | |
| 2887 | +{ | |
| 2888 | + require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php'; | |
| 2889 | + Metasync_Otto_Excluded_URLs_Database::ensure_schema(); | |
| 2890 | +} | |
| 2891 | + | |
| 2892 | +add_action('metasync_repair_otto_excluded_schema', 'metasync_repair_otto_excluded_schema'); | |
| 2893 | + | |
| 2894 | +/** | |
| 2871 | 2895 | * Check if a post/page would return 404 without making HTTP request |
| 2872 | 2896 | * Uses WordPress database checks for fast validation |
| 2873 | 2897 | * |
| 2874 | 2898 | * @param int $post_id WordPress post ID |
| @@ -3044,9 +3068,9 @@ | ||
| 3044 | 3068 | } |
| 3045 | 3069 | } |
| 3046 | 3070 | |
| 3047 | 3071 | // WP Cloud's warmer also tags its requests with a query marker. |
| 3048 | - if ( ! $is_infra && isset( $_GET['x-cache-engine'] ) ) { | |
| 3072 | + if ( ! $is_infra && isset( $_GET['x-cache-engine'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only flag for render throttling | |
| 3049 | 3073 | $is_infra = true; |
| 3050 | 3074 | } |
| 3051 | 3075 | |
| 3052 | 3076 | /** |