| @@ -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 | |
| @@ -812,8 +800,27 @@ | ||
| 812 | 800 | if ( $detection['is_bot'] ) { |
| 813 | 801 | $bot_detector->push_crawl_log_to_sa( $detection, $current_url ); |
| 814 | 802 | } |
| 815 | 803 | |
| 804 | + # A headless frontend renders the public HTML, so stop after the cheap cache | |
| 805 | + # protections and crawl telemetry, before any OTTO rendering work begins. | |
| 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) { | |
| 820 | + return; | |
| 821 | + } | |
| 822 | + | |
| 816 | 823 | # Throttle OTTO rendering for bots that are not exempt: at most one render |
| 817 | 824 | # per URL+bot every 5 minutes. Humans, search engines (Googlebot, Bingbot, |
| 818 | 825 | # Yahoo's Slurp, Sogou, Exabot, Applebot, ...), host cache warmers and |
| 819 | 826 | # synthetic performance auditors are never throttled — their hits always get |
| @@ -895,9 +902,9 @@ | ||
| 895 | 902 | # ?et_blog = Divi AJAX pagination callback |
| 896 | 903 | # /page/N/ = paginated blog/archive pages — OTTO's buffer/HTTP render causes |
| 897 | 904 | # module numbering mismatch between page 1 (with TB template) and page N |
| 898 | 905 | # (without TB template), breaking Divi's JS pagination selector matching. |
| 899 | - 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 | |
| 900 | 907 | return; |
| 901 | 908 | } |
| 902 | 909 | |
| 903 | 910 | # Skip OTTO on search-results pages. A search request carries its |
| @@ -912,9 +919,9 @@ | ||
| 912 | 919 | # correct pre-rendered search page to everyone else. Search-results pages |
| 913 | 920 | # have no per-canonical-URL OTTO suggestions of their own, so OTTO must never |
| 914 | 921 | # run here. The raw $_GET['s'] check is a defensive fallback for setups where |
| 915 | 922 | # the main query is altered before this point. |
| 916 | - 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 | |
| 917 | 924 | return; |
| 918 | 925 | } |
| 919 | 926 | |
| 920 | 927 | # Handle cache plugin compatibility early - before any caching happens |
| @@ -966,9 +973,9 @@ | ||
| 966 | 973 | $otto_has_fetch_detector = class_exists('Metasync_Otto_Render_Strategy') && method_exists('Metasync_Otto_Render_Strategy', 'is_internal_fetch'); |
| 967 | 974 | |
| 968 | 975 | $otto_is_internal_fetch = $otto_has_fetch_detector |
| 969 | 976 | ? Metasync_Otto_Render_Strategy::is_internal_fetch() |
| 970 | - : !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 | |
| 971 | 978 | |
| 972 | 979 | if($otto_is_internal_fetch){ |
| 973 | 980 | |
| 974 | 981 | # Block SEO plugins NOW for this internal fetch request |
| @@ -973,10 +980,10 @@ | ||
| 973 | 980 | |
| 974 | 981 | # Block SEO plugins NOW for this internal fetch request |
| 975 | 982 | # metasync_otto_block_seo_plugins(); |
| 976 | 983 | # $_SERVER['REQUEST_URI'] = remove_query_arg('is_otto_page_fetch', $_SERVER['REQUEST_URI']); |
| 977 | - $block_title = !empty($_GET['otto_block_title']) && $_GET['otto_block_title'] === '1'; | |
| 978 | - $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 | |
| 979 | 986 | |
| 980 | 987 | # Block SEO plugins conditionally based on what Otto has |
| 981 | 988 | if ($block_title || $block_description) { |
| 982 | 989 | metasync_otto_block_seo_plugins($block_title, $block_description); |
| @@ -1012,8 +1019,19 @@ | ||
| 1012 | 1019 | |
| 1013 | 1020 | # get the otto uuid |
| 1014 | 1021 | $otto_uuid = Metasync_Otto_Config::get_otto_uuid(); |
| 1015 | 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 | + | |
| 1016 | 1034 | # start the class |
| 1017 | 1035 | $otto = new Metasync_otto_pixel($otto_uuid); |
| 1018 | 1036 | |
| 1019 | 1037 | # call render |
| @@ -1045,9 +1063,9 @@ | ||
| 1045 | 1063 | if ($cached !== false) { |
| 1046 | 1064 | $has_brizy_posts = ($cached === 'yes'); |
| 1047 | 1065 | } else { |
| 1048 | 1066 | # Query database only if cache missed |
| 1049 | - $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 | |
| 1050 | 1068 | "SELECT COUNT(*) FROM {$wpdb->postmeta} |
| 1051 | 1069 | WHERE meta_key = 'brizy_post_uid' |
| 1052 | 1070 | AND meta_value != '' |
| 1053 | 1071 | LIMIT 1" |
| @@ -1507,11 +1525,13 @@ | ||
| 1507 | 1525 | # Disable Rank Math |
| 1508 | 1526 | if (is_plugin_active('seo-by-rank-math/rank-math.php') || |
| 1509 | 1527 | is_plugin_active('seo-by-rankmath/rank-math.php')) { |
| 1510 | 1528 | |
| 1511 | - if ($block_title) { | |
| 1512 | - add_filter('rank_math/frontend/title', '__return_empty_string', 999); | |
| 1513 | - } | |
| 1529 | + # TITLE: Never blank Rank Math's title output during SSR fetch. | |
| 1530 | + # rank_math/frontend/title also gates Rank Math's og:title/twitter:title | |
| 1531 | + # output, so forcing it empty blanks those social tags along with the | |
| 1532 | + # document title. Let Rank Math render its own title/og/twitter tags; | |
| 1533 | + # OTTO's replace_title() and the dedup/precedence pass clean up duplicates. | |
| 1514 | 1534 | |
| 1515 | 1535 | if ($block_description) { |
| 1516 | 1536 | add_filter('rank_math/frontend/description', '__return_false', 999); |
| 1517 | 1537 | add_filter('rank_math/frontend/show_keywords', '__return_false', 999); |
| @@ -1645,8 +1665,17 @@ | ||
| 1645 | 1665 | |
| 1646 | 1666 | # Handle AJAX Clear Cache request |
| 1647 | 1667 | # NOTE: Cache system removed - this is now a no-op |
| 1648 | 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 | + | |
| 1649 | 1678 | if (!empty($_GET['clear_otto_cache'])) { |
| 1650 | 1679 | delete_transient('metasync_otto_js_detected'); |
| 1651 | 1680 | # Re-run the JS detection in the background so the notice refreshes. |
| 1652 | 1681 | metasync_schedule_otto_js_check(); |
| @@ -1783,9 +1812,8 @@ | ||
| 1783 | 1812 | } |
| 1784 | 1813 | |
| 1785 | 1814 | # Pre-flight 404 check: exclude URLs that would return 404 before making API call |
| 1786 | 1815 | if (!metasync_otto_is_url_available($route)) { |
| 1787 | - error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404: {$route}"); | |
| 1788 | 1816 | metasync_otto_auto_exclude_404_url($route); |
| 1789 | 1817 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 1790 | 1818 | } |
| 1791 | 1819 | |
| @@ -1824,9 +1852,9 @@ | ||
| 1824 | 1852 | } |
| 1825 | 1853 | |
| 1826 | 1854 | # Mark this URL as crawled by OTTO for SSR |
| 1827 | 1855 | # Extract domain and path from route |
| 1828 | - $parsed_url = parse_url($route); | |
| 1856 | + $parsed_url = wp_parse_url($route); | |
| 1829 | 1857 | $domain_with_scheme = ($parsed_url['scheme'] ?? 'https') . '://' . ($parsed_url['host'] ?? ''); |
| 1830 | 1858 | $url_path = ($parsed_url['path'] ?? '/'); |
| 1831 | 1859 | |
| 1832 | 1860 | # Create crawl data structure |
| @@ -1859,9 +1887,9 @@ | ||
| 1859 | 1887 | |
| 1860 | 1888 | # Try to find WooCommerce product by URL if url_to_postid failed |
| 1861 | 1889 | if ((!$post_id || $post_id <= 0) && strpos($route, '/product/') !== false && function_exists('wc_get_products')) { |
| 1862 | 1890 | # Extract product slug from URL |
| 1863 | - $product_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 1891 | + $product_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 1864 | 1892 | |
| 1865 | 1893 | # Try to get product by slug |
| 1866 | 1894 | $products = wc_get_products(array( |
| 1867 | 1895 | 'name' => $product_slug, |
| @@ -1892,15 +1920,14 @@ | ||
| 1892 | 1920 | if (!$post_id || $post_id <= 0) { |
| 1893 | 1921 | # Check if this is a category page |
| 1894 | 1922 | if (strpos($route, '/category/') !== false) { |
| 1895 | 1923 | # Extract category slug from URL |
| 1896 | - $category_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 1924 | + $category_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 1897 | 1925 | $category = get_category_by_slug($category_slug); |
| 1898 | 1926 | |
| 1899 | 1927 | if ($category) { |
| 1900 | 1928 | # Check if category would return 404 before applying OTTO changes |
| 1901 | 1929 | if (metasync_would_term_return_404($category->term_id, 'category', $route)) { |
| 1902 | - error_log("MetaSync OTTO: Skipping SEO processing for category that would return 404: {$route} (Category ID: {$category->term_id})"); | |
| 1903 | 1930 | metasync_otto_auto_exclude_404_url($route); |
| 1904 | 1931 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 1905 | 1932 | } |
| 1906 | 1933 | |
| @@ -1965,8 +1992,12 @@ | ||
| 1965 | 1992 | $title = 'Category Structured Data Update'; |
| 1966 | 1993 | break; |
| 1967 | 1994 | } |
| 1968 | 1995 | |
| 1996 | + if (is_scalar($field_value) && trim((string) $field_value) === '') { | |
| 1997 | + $title = str_replace(' Update (...)', ' Cleanup (previous value removed)', $title); | |
| 1998 | + } | |
| 1999 | + | |
| 1969 | 2000 | if (!empty($title)) { |
| 1970 | 2001 | metasync_log_sync_history([ |
| 1971 | 2002 | 'title' => $title, |
| 1972 | 2003 | 'source' => 'OTTO SEO', |
| @@ -1992,15 +2023,14 @@ | ||
| 1992 | 2023 | |
| 1993 | 2024 | # Check if this is a WooCommerce product category |
| 1994 | 2025 | if (strpos($route, '/product-category/') !== false) { |
| 1995 | 2026 | # Extract product category slug from URL |
| 1996 | - $category_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 2027 | + $category_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 1997 | 2028 | $term = get_term_by('slug', $category_slug, 'product_cat'); |
| 1998 | 2029 | |
| 1999 | 2030 | if ($term && !is_wp_error($term)) { |
| 2000 | 2031 | # Check if product category would return 404 before applying OTTO changes |
| 2001 | 2032 | if (metasync_would_term_return_404($term->term_id, 'product_cat', $route)) { |
| 2002 | - error_log("MetaSync OTTO: Skipping SEO processing for product category that would return 404: {$route} (Term ID: {$term->term_id})"); | |
| 2003 | 2033 | metasync_otto_auto_exclude_404_url($route); |
| 2004 | 2034 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2005 | 2035 | } |
| 2006 | 2036 | |
| @@ -2065,8 +2095,12 @@ | ||
| 2065 | 2095 | $title = 'Product Category Structured Data Update'; |
| 2066 | 2096 | break; |
| 2067 | 2097 | } |
| 2068 | 2098 | |
| 2099 | + if (is_scalar($field_value) && trim((string) $field_value) === '') { | |
| 2100 | + $title = str_replace(' Update (...)', ' Cleanup (previous value removed)', $title); | |
| 2101 | + } | |
| 2102 | + | |
| 2069 | 2103 | if (!empty($title)) { |
| 2070 | 2104 | metasync_log_sync_history([ |
| 2071 | 2105 | 'title' => $title, |
| 2072 | 2106 | 'source' => 'OTTO SEO', |
| @@ -2109,9 +2143,8 @@ | ||
| 2109 | 2143 | |
| 2110 | 2144 | if ($home_page) { |
| 2111 | 2145 | # Check if home page would return 404 before applying OTTO changes |
| 2112 | 2146 | if (metasync_would_page_return_404($home_page->ID, $route)) { |
| 2113 | - 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})"); | |
| 2114 | 2147 | metasync_otto_auto_exclude_404_url($route); |
| 2115 | 2148 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2116 | 2149 | } |
| 2117 | 2150 | |
| @@ -2179,8 +2212,12 @@ | ||
| 2179 | 2212 | $title = 'Home Page Structured Data Update'; |
| 2180 | 2213 | break; |
| 2181 | 2214 | } |
| 2182 | 2215 | |
| 2216 | + if (is_scalar($field_value) && trim((string) $field_value) === '') { | |
| 2217 | + $title = str_replace(' Update (...)', ' Cleanup (previous value removed)', $title); | |
| 2218 | + } | |
| 2219 | + | |
| 2183 | 2220 | if (!empty($title)) { |
| 2184 | 2221 | metasync_log_sync_history([ |
| 2185 | 2222 | 'title' => $title, |
| 2186 | 2223 | 'source' => 'OTTO SEO', |
| @@ -2211,9 +2248,8 @@ | ||
| 2211 | 2248 | |
| 2212 | 2249 | if ($posts_page && $route_clean === $posts_page_url) { |
| 2213 | 2250 | # Check if blog page would return 404 |
| 2214 | 2251 | if (metasync_would_page_return_404($posts_page->ID, $route)) { |
| 2215 | - 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})"); | |
| 2216 | 2252 | metasync_otto_auto_exclude_404_url($route); |
| 2217 | 2253 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2218 | 2254 | } |
| 2219 | 2255 | |
| @@ -2281,8 +2317,12 @@ | ||
| 2281 | 2317 | $title = 'Blog Page Structured Data Update'; |
| 2282 | 2318 | break; |
| 2283 | 2319 | } |
| 2284 | 2320 | |
| 2321 | + if (is_scalar($field_value) && trim((string) $field_value) === '') { | |
| 2322 | + $title = str_replace(' Update (...)', ' Cleanup (previous value removed)', $title); | |
| 2323 | + } | |
| 2324 | + | |
| 2285 | 2325 | if (!empty($title)) { |
| 2286 | 2326 | metasync_log_sync_history([ |
| 2287 | 2327 | 'title' => $title, |
| 2288 | 2328 | 'source' => 'OTTO SEO', |
| @@ -2307,9 +2347,8 @@ | ||
| 2307 | 2347 | |
| 2308 | 2348 | # URL didn't resolve to any supported entity (post, category, home page, blog page) |
| 2309 | 2349 | # Treat as 404 and auto-exclude (e.g. deleted post, non-existent page) |
| 2310 | 2350 | if (!metasync_otto_is_url_available($route)) { |
| 2311 | - error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404 (no matching entity): {$route}"); | |
| 2312 | 2351 | metasync_otto_auto_exclude_404_url($route); |
| 2313 | 2352 | } |
| 2314 | 2353 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2315 | 2354 | } |
| @@ -2326,9 +2365,8 @@ | ||
| 2326 | 2365 | } |
| 2327 | 2366 | |
| 2328 | 2367 | # Check if page would return 404 before applying OTTO changes |
| 2329 | 2368 | if (metasync_would_page_return_404($post_id, $route)) { |
| 2330 | - error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404: {$route} (Post ID: {$post_id}, Status: {$post->post_status})"); | |
| 2331 | 2369 | metasync_otto_auto_exclude_404_url($route); |
| 2332 | 2370 | return Metasync_Otto_Job_Status::OUTCOME_PERMANENT; |
| 2333 | 2371 | } |
| 2334 | 2372 | |
| @@ -2396,8 +2434,12 @@ | ||
| 2396 | 2434 | $title = 'Structured Data Update'; |
| 2397 | 2435 | break; |
| 2398 | 2436 | } |
| 2399 | 2437 | |
| 2438 | + if (is_scalar($field_value) && trim((string) $field_value) === '') { | |
| 2439 | + $title = str_replace(' Update (...)', ' Cleanup (previous value removed)', $title); | |
| 2440 | + } | |
| 2441 | + | |
| 2400 | 2442 | if (!empty($title)) { |
| 2401 | 2443 | metasync_log_sync_history([ |
| 2402 | 2444 | 'title' => $title, |
| 2403 | 2445 | 'source' => 'OTTO SEO', |
| @@ -2442,11 +2484,10 @@ | ||
| 2442 | 2484 | |
| 2443 | 2485 | // Minimal duplicate prevention within short time window |
| 2444 | 2486 | if (!empty($data['title']) && !empty($data['source'])) { |
| 2445 | 2487 | global $wpdb; |
| 2446 | - $table = $wpdb->prefix . Metasync_Sync_History_Database::$table_name; | |
| 2447 | - $recent = $wpdb->get_var($wpdb->prepare( | |
| 2448 | - "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", | |
| 2449 | 2490 | $data['title'], |
| 2450 | 2491 | $data['source'], |
| 2451 | 2492 | gmdate('Y-m-d H:i:s', time() - 60) |
| 2452 | 2493 | )); |
| @@ -2457,9 +2498,9 @@ | ||
| 2457 | 2498 | |
| 2458 | 2499 | $sync_db->add($data); |
| 2459 | 2500 | |
| 2460 | 2501 | } catch (Exception $e) { |
| 2461 | - 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 | |
| 2462 | 2503 | } |
| 2463 | 2504 | } |
| 2464 | 2505 | |
| 2465 | 2506 | /** |
| @@ -2535,9 +2576,9 @@ | ||
| 2535 | 2576 | $db = new Metasync_Redirection_Database(); |
| 2536 | 2577 | $redirect = new Metasync_Redirection($db); |
| 2537 | 2578 | return $redirect->resolve_url_to_final_destination($url, 10); |
| 2538 | 2579 | } catch (Exception $e) { |
| 2539 | - 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 | |
| 2540 | 2581 | return $url; |
| 2541 | 2582 | } |
| 2542 | 2583 | } |
| 2543 | 2584 | |
| @@ -2568,9 +2609,9 @@ | ||
| 2568 | 2609 | 'status' => 'active', |
| 2569 | 2610 | 'auto_excluded' => 1, |
| 2570 | 2611 | ]); |
| 2571 | 2612 | } catch (Exception $e) { |
| 2572 | - 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 | |
| 2573 | 2614 | return false; |
| 2574 | 2615 | } |
| 2575 | 2616 | } |
| 2576 | 2617 | |
| @@ -2590,14 +2631,13 @@ | ||
| 2590 | 2631 | try { |
| 2591 | 2632 | require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php'; |
| 2592 | 2633 | $db = new Metasync_Otto_Excluded_URLs_Database(); |
| 2593 | 2634 | global $wpdb; |
| 2594 | - $table = $wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name; | |
| 2595 | 2635 | # Normalize the same way is_url_excluded() does |
| 2596 | 2636 | $url_normalized = rtrim(trim($url), '/'); |
| 2597 | - $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 | |
| 2598 | 2638 | $wpdb->prepare( |
| 2599 | - "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'", | |
| 2600 | 2640 | $url_normalized |
| 2601 | 2641 | ) |
| 2602 | 2642 | ); |
| 2603 | 2643 | if (!empty($records)) { |
| @@ -2627,24 +2667,26 @@ | ||
| 2627 | 2667 | } |
| 2628 | 2668 | try { |
| 2629 | 2669 | require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php'; |
| 2630 | 2670 | global $wpdb; |
| 2631 | - $table = $wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name; | |
| 2632 | 2671 | $url_normalized = rtrim(trim($url), '/'); |
| 2633 | 2672 | |
| 2634 | 2673 | $records = get_transient(METASYNC_OTTO_EXCLUDED_TRANSIENT_KEY); |
| 2635 | 2674 | |
| 2636 | 2675 | if ($records === false) { |
| 2637 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- no user input, table name from $wpdb->prefix | |
| 2638 | - $records = $wpdb->get_results( | |
| 2639 | - "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" | |
| 2640 | 2678 | ); |
| 2641 | 2679 | |
| 2642 | 2680 | // Graceful recovery: auto_excluded column missing on pre-v2.7.4 installs. |
| 2643 | - // 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). | |
| 2644 | 2685 | if ($wpdb->last_error && strpos($wpdb->last_error, 'auto_excluded') !== false) { |
| 2645 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared | |
| 2646 | - $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 | + } | |
| 2647 | 2689 | return false; |
| 2648 | 2690 | } |
| 2649 | 2691 | |
| 2650 | 2692 | set_transient(METASYNC_OTTO_EXCLUDED_TRANSIENT_KEY, $records ?: [], METASYNC_OTTO_EXCLUDED_TRANSIENT_TTL); |
| @@ -2720,9 +2762,9 @@ | ||
| 2720 | 2762 | } |
| 2721 | 2763 | |
| 2722 | 2764 | # WooCommerce product by slug |
| 2723 | 2765 | if ((!$post_id || $post_id <= 0) && strpos($route, '/product/') !== false && function_exists('wc_get_products')) { |
| 2724 | - $product_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 2766 | + $product_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 2725 | 2767 | $products = wc_get_products(array('name' => $product_slug, 'limit' => 1, 'status' => 'publish')); |
| 2726 | 2768 | if (!empty($products)) { |
| 2727 | 2769 | $post_id = $products[0]->get_id(); |
| 2728 | 2770 | } else { |
| @@ -2749,9 +2791,9 @@ | ||
| 2749 | 2791 | } |
| 2750 | 2792 | |
| 2751 | 2793 | # Category |
| 2752 | 2794 | if (strpos($route, '/category/') !== false) { |
| 2753 | - $category_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 2795 | + $category_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 2754 | 2796 | $category = get_category_by_slug($category_slug); |
| 2755 | 2797 | if ($category) { |
| 2756 | 2798 | return !metasync_would_term_return_404($category->term_id, 'category', $route); |
| 2757 | 2799 | } |
| @@ -2758,9 +2800,9 @@ | ||
| 2758 | 2800 | } |
| 2759 | 2801 | |
| 2760 | 2802 | # WooCommerce product category |
| 2761 | 2803 | if (strpos($route, '/product-category/') !== false) { |
| 2762 | - $category_slug = basename(parse_url($route, PHP_URL_PATH)); | |
| 2804 | + $category_slug = basename(wp_parse_url($route, PHP_URL_PATH)); | |
| 2763 | 2805 | $term = get_term_by('slug', $category_slug, 'product_cat'); |
| 2764 | 2806 | if ($term && !is_wp_error($term)) { |
| 2765 | 2807 | return !metasync_would_term_return_404($term->term_id, 'product_cat', $route); |
| 2766 | 2808 | } |
| @@ -2802,9 +2844,9 @@ | ||
| 2802 | 2844 | |
| 2803 | 2845 | $removed = 0; |
| 2804 | 2846 | $marked_permanent = 0; |
| 2805 | 2847 | $thirty_days_ago = strtotime('-30 days'); |
| 2806 | - $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); | |
| 2807 | 2849 | |
| 2808 | 2850 | foreach ($records as $record) { |
| 2809 | 2851 | $url = trim($record->url_pattern); |
| 2810 | 2852 | if (empty($url)) { |
| @@ -2825,16 +2867,10 @@ | ||
| 2825 | 2867 | } |
| 2826 | 2868 | } |
| 2827 | 2869 | } |
| 2828 | 2870 | |
| 2829 | - if ($removed > 0) { | |
| 2830 | - error_log("MetaSync OTTO: Recheck 404 exclusions - removed {$removed} URL(s) that are now available"); | |
| 2831 | - } | |
| 2832 | - if ($marked_permanent > 0) { | |
| 2833 | - error_log("MetaSync OTTO: Recheck 404 exclusions - marked {$marked_permanent} URL(s) as permanent (still 404 after 30 days)"); | |
| 2834 | - } | |
| 2835 | 2871 | } catch (Exception $e) { |
| 2836 | - 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 | |
| 2837 | 2873 | } |
| 2838 | 2874 | } |
| 2839 | 2875 | |
| 2840 | 2876 | add_action('metasync_otto_recheck_404_exclusions', 'metasync_otto_recheck_404_exclusions'); |
| @@ -2839,8 +2875,24 @@ | ||
| 2839 | 2875 | |
| 2840 | 2876 | add_action('metasync_otto_recheck_404_exclusions', 'metasync_otto_recheck_404_exclusions'); |
| 2841 | 2877 | |
| 2842 | 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 | +/** | |
| 2843 | 2895 | * Check if a post/page would return 404 without making HTTP request |
| 2844 | 2896 | * Uses WordPress database checks for fast validation |
| 2845 | 2897 | * |
| 2846 | 2898 | * @param int $post_id WordPress post ID |
| @@ -3016,9 +3068,9 @@ | ||
| 3016 | 3068 | } |
| 3017 | 3069 | } |
| 3018 | 3070 | |
| 3019 | 3071 | // WP Cloud's warmer also tags its requests with a query marker. |
| 3020 | - 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 | |
| 3021 | 3073 | $is_infra = true; |
| 3022 | 3074 | } |
| 3023 | 3075 | |
| 3024 | 3076 | /** |