| @@ -44,8 +44,9 @@ | ||
| 44 | 44 | require_once plugin_dir_path( __FILE__ ) . '/Otto_pixel_class.php'; |
| 45 | 45 | require_once plugin_dir_path( __FILE__ ) . '/metasync-otto-seo-functions.php'; |
| 46 | 46 | require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-transient-cache.php'; |
| 47 | 47 | require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-job-status.php'; |
| 48 | +require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-webhook-audit.php'; | |
| 48 | 49 | require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-render-strategy.php'; |
| 49 | 50 | require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-config.php'; |
| 50 | 51 | require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-bot-detector.php'; |
| 51 | 52 | require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-bot-statistics-database.php'; |
| @@ -113,8 +114,9 @@ | ||
| 113 | 114 | # OPTIMIZED: use cached uuid |
| 114 | 115 | $otto_tag = '<meta name="otto" content="uuid='.esc_attr(Metasync_Otto_Config::get_otto_uuid()).'; type=wordpress; enabled='.esc_attr($string_enabled).'; version='.esc_attr($plugin_version).'">'; |
| 115 | 116 | |
| 116 | 117 | # out the otto tag |
| 118 | + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- meta tag assembled on the line above with esc_attr() on every interpolated value | |
| 117 | 119 | echo $otto_tag; |
| 118 | 120 | }, 1); # Priority 1 to output early in head |
| 119 | 121 | |
| 120 | 122 | /** |
| @@ -165,8 +167,13 @@ | ||
| 165 | 167 | |
| 166 | 168 | # function to register the route |
| 167 | 169 | function metasync_otto_crawl_notify($request){ |
| 168 | 170 | |
| 171 | + # receipt time is captured before validation and scheduling, so the audit | |
| 172 | + # record describes when WordPress received the delivery, not when its work | |
| 173 | + # happened to finish. | |
| 174 | + $received_at = time(); | |
| 175 | + | |
| 169 | 176 | # get request data params |
| 170 | 177 | $data = $request->get_json_params(); |
| 171 | 178 | |
| 172 | 179 | # fields |
| @@ -186,14 +193,29 @@ | ||
| 186 | 193 | } |
| 187 | 194 | |
| 188 | 195 | } |
| 189 | 196 | |
| 190 | - # load otto pixel | |
| 191 | - $otto_pixel = new Metasync_otto_pixel(false); | |
| 197 | + # The audit trail is deliberately after payload validation: invalid requests | |
| 198 | + # receive the existing 400 response and are not recorded as deliveries. | |
| 199 | + # It is also before any scheduling so a valid delivery is captured even if | |
| 200 | + # later work is deferred or rejected. The failure guard is intentionally | |
| 201 | + # broader than Exception: a broken option adapter or stale plugin copy must | |
| 202 | + # never turn an otherwise acknowledged webhook into a 500 response. | |
| 203 | + try { | |
| 204 | + if (metasync_otto_class_provides('Metasync_Otto_Webhook_Audit', 'record')) { | |
| 205 | + Metasync_Otto_Webhook_Audit::record( | |
| 206 | + $received_at, | |
| 207 | + $data['domain'], | |
| 208 | + $data['urls'] | |
| 209 | + ); | |
| 210 | + } | |
| 211 | + } catch (Throwable $metasync_otto_audit_failure) { | |
| 212 | + error_log( | |
| 213 | + 'MetaSync OTTO: failed to record crawl-notify webhook audit entry: ' | |
| 214 | + . $metasync_otto_audit_failure->getMessage() | |
| 215 | + ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- audit failure must not break webhook acknowledgement | |
| 216 | + } | |
| 192 | 217 | |
| 193 | - # save the otto data first (cheap local DB write — kept synchronous) | |
| 194 | - $otto_pixel->save_crawl_data($data); | |
| 195 | - | |
| 196 | 218 | # Defer the expensive per-URL work (OTTO API fetch, post meta sync, |
| 197 | 219 | # per-URL cache purge) to background jobs. The webhook caller enforces a |
| 198 | 220 | # strict response-time budget, so the response acknowledges SCHEDULING, |
| 199 | 221 | # never execution. The per-URL status store records what actually |
| @@ -424,10 +446,32 @@ | ||
| 424 | 446 | } |
| 425 | 447 | |
| 426 | 448 | Metasync_Otto_Job_Status::record($route, Metasync_Otto_Job_Status::STATE_COMPLETED, 'job', $retry_count, $outcome); |
| 427 | 449 | |
| 428 | - } catch (Exception $e) { | |
| 429 | - $schedule_retry('exception:' . $e->getMessage()); | |
| 450 | + } catch (\Throwable $e) { | |
| 451 | + # Cron must survive both Exception and PHP 7+ Error failures. Always use | |
| 452 | + # the same bounded retry path, and emit structured context without the URL | |
| 453 | + # payload or credentials. The retry is booked BEFORE the log attempt so a | |
| 454 | + # failing logger can never swallow the retry; logging is best-effort. | |
| 455 | + $schedule_retry('throwable:' . get_class($e)); | |
| 456 | + if (class_exists('Metasync_Error_Logger')) { | |
| 457 | + try { | |
| 458 | + Metasync_Error_Logger::log( | |
| 459 | + Metasync_Error_Logger::CATEGORY_OTTO_RENDER, | |
| 460 | + Metasync_Error_Logger::SEVERITY_ERROR, | |
| 461 | + 'OTTO crawl-url job threw; retry policy applied', | |
| 462 | + [ | |
| 463 | + 'operation' => 'crawl_url_job', | |
| 464 | + 'outcome' => 'throwable', | |
| 465 | + 'retry_count' => (int) $retry_count, | |
| 466 | + 'exception' => get_class($e), | |
| 467 | + ] | |
| 468 | + ); | |
| 469 | + } catch (\Throwable $logger_error) { | |
| 470 | + # Logging must never kill the handler after the retry is booked. | |
| 471 | + error_log('MetaSync OTTO: crawl-url job error log failed: ' . $logger_error->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 472 | + } | |
| 473 | + } | |
| 430 | 474 | } |
| 431 | 475 | } |
| 432 | 476 | |
| 433 | 477 | /** |
| @@ -889,9 +933,9 @@ | ||
| 889 | 933 | return; |
| 890 | 934 | } |
| 891 | 935 | |
| 892 | 936 | # OPTIMIZED: Check if Otto should be disabled for WP Rocket compatibility |
| 893 | - if (class_exists('WP_Rocket')) { | |
| 937 | + if (defined('WP_ROCKET_VERSION') || class_exists('WP_Rocket')) { | |
| 894 | 938 | $wp_rocket_compat_mode = Metasync_Otto_Config::get_wp_rocket_compat_mode(); |
| 895 | 939 | |
| 896 | 940 | if ($wp_rocket_compat_mode === 'disable_otto') { |
| 897 | 941 | return; # Exit early, Otto is disabled when WP Rocket is active |
| @@ -897,14 +941,25 @@ | ||
| 897 | 941 | return; # Exit early, Otto is disabled when WP Rocket is active |
| 898 | 942 | } |
| 899 | 943 | } |
| 900 | 944 | |
| 901 | - # Skip OTTO for Divi AJAX pagination and paginated archive requests. | |
| 902 | - # ?et_blog = Divi AJAX pagination callback | |
| 945 | + # Skip OTTO for Divi AJAX pagination, and for paginated archive requests | |
| 946 | + # on Divi-built sites. | |
| 947 | + # ?et_blog = Divi AJAX pagination callback — always skipped, on every | |
| 948 | + # theme, because Divi fires it regardless of the site layout. | |
| 903 | 949 | # /page/N/ = paginated blog/archive pages — OTTO's buffer/HTTP render causes |
| 904 | 950 | # module numbering mismatch between page 1 (with TB template) and page N |
| 905 | 951 | # (without TB template), breaking Divi's JS pagination selector matching. |
| 906 | - if (isset($_GET['et_blog']) || (is_paged() && !is_singular())) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 952 | + # That mismatch is a Divi rendering bug, so the /page/N/ skip is gated on | |
| 953 | + # Divi detection: non-Divi sites (Elementor + the7, ...) keep OTTO on real | |
| 954 | + # paginated archives. Detection covers the Divi theme and its child themes | |
| 955 | + # (get_template() returns the PARENT template dir, "Divi" either way), the | |
| 956 | + # Extra theme and the Divi Builder plugin (both define ET_BUILDER_VERSION | |
| 957 | + # / et_setup_theme — same TB machinery, same bug). | |
| 958 | + $is_divi = defined('ET_BUILDER_VERSION') | |
| 959 | + || function_exists('et_setup_theme') | |
| 960 | + || wp_get_theme()->get_template() === 'Divi'; | |
| 961 | + if (isset($_GET['et_blog']) || ($is_divi && is_paged() && !is_singular())) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end check that skips OTTO | |
| 907 | 962 | return; |
| 908 | 963 | } |
| 909 | 964 | |
| 910 | 965 | # Skip OTTO on search-results pages. A search request carries its |
| @@ -1047,9 +1102,9 @@ | ||
| 1047 | 1102 | # Detect active plugins. SG Optimizer detection now lives in |
| 1048 | 1103 | # metasync_otto_disable_sg_page_cache(), which is invoked later |
| 1049 | 1104 | # once OTTO confirms suggestions for the URL. |
| 1050 | 1105 | $brizy_active = class_exists('Brizy_Editor') || defined('BRIZY_VERSION'); |
| 1051 | - $wp_rocket_active = class_exists('WP_Rocket'); | |
| 1106 | + $wp_rocket_active = defined('WP_ROCKET_VERSION') || class_exists('WP_Rocket'); | |
| 1052 | 1107 | |
| 1053 | 1108 | # OPTIMIZED: Get configuration option using cached config |
| 1054 | 1109 | $wp_rocket_compat_mode = Metasync_Otto_Config::get_wp_rocket_compat_mode(); |
| 1055 | 1110 | |
| @@ -1122,9 +1177,9 @@ | ||
| 1122 | 1177 | * Only meaningful on SiteGround sites without WP Rocket; a no-op otherwise. |
| 1123 | 1178 | */ |
| 1124 | 1179 | function metasync_otto_disable_sg_page_cache() { |
| 1125 | 1180 | # Only relevant when SG Optimizer is active and WP Rocket is not present. |
| 1126 | - if (class_exists('WP_Rocket')) { | |
| 1181 | + if (defined('WP_ROCKET_VERSION') || class_exists('WP_Rocket')) { | |
| 1127 | 1182 | return; |
| 1128 | 1183 | } |
| 1129 | 1184 | if (!is_plugin_active('sg-cachepress/sg-cachepress.php')) { |
| 1130 | 1185 | return; |
| @@ -1849,24 +1904,8 @@ | ||
| 1849 | 1904 | metasync_record_failed_action( 'metasync_process_seo_job' ); |
| 1850 | 1905 | return Metasync_Otto_Job_Status::OUTCOME_RETRYABLE; |
| 1851 | 1906 | } |
| 1852 | 1907 | } |
| 1853 | - | |
| 1854 | - # Mark this URL as crawled by OTTO for SSR | |
| 1855 | - # Extract domain and path from route | |
| 1856 | - $parsed_url = wp_parse_url($route); | |
| 1857 | - $domain_with_scheme = ($parsed_url['scheme'] ?? 'https') . '://' . ($parsed_url['host'] ?? ''); | |
| 1858 | - $url_path = ($parsed_url['path'] ?? '/'); | |
| 1859 | - | |
| 1860 | - # Create crawl data structure | |
| 1861 | - $crawl_data = array( | |
| 1862 | - 'domain' => $domain_with_scheme, | |
| 1863 | - 'urls' => array($url_path) | |
| 1864 | - ); | |
| 1865 | - | |
| 1866 | - # Load Otto pixel class and save crawl data | |
| 1867 | - $otto_pixel = new Metasync_otto_pixel($otto_uuid); | |
| 1868 | - $otto_pixel->save_crawl_data($crawl_data); | |
| 1869 | 1908 | |
| 1870 | 1909 | # Get WordPress post ID from URL |
| 1871 | 1910 | $post_id = url_to_postid($route); |
| 1872 | 1911 | |