| @@ -7,27 +7,24 @@ | ||
| 7 | 7 | /** |
| 8 | 8 | * This class handles the Otto Pixel Functions |
| 9 | 9 | */ |
| 10 | 10 | Class Metasync_otto_pixel{ |
| 11 | - | |
| 11 | + | |
| 12 | 12 | #otto html class |
| 13 | 13 | public $o_html; |
| 14 | 14 | |
| 15 | - # crawl data option | |
| 16 | - public $option_name = 'metasync_otto_crawldata'; | |
| 17 | - | |
| 18 | - # no cache wp pages | |
| 15 | + # no cache wp pages | |
| 19 | 16 | public $no_cache_pages = ['wp-login.php']; |
| 20 | 17 | |
| 21 | 18 | # OTTO UUID |
| 22 | 19 | private $otto_uuid; |
| 23 | 20 | |
| 24 | - # | |
| 21 | + # | |
| 25 | 22 | function __construct($otto_uuid){ |
| 26 | - | |
| 23 | + | |
| 27 | 24 | # store the UUID |
| 28 | 25 | $this->otto_uuid = $otto_uuid; |
| 29 | - | |
| 26 | + | |
| 30 | 27 | # load the html class |
| 31 | 28 | $this->o_html = new Metasync_otto_html($otto_uuid); |
| 32 | 29 | |
| 33 | 30 | } |
| @@ -39,102 +36,8 @@ | ||
| 39 | 36 | # All pages are processed in real-time from OTTO API |
| 40 | 37 | return true; |
| 41 | 38 | } |
| 42 | 39 | |
| 43 | - | |
| 44 | - # method to save crawl data into | |
| 45 | - function save_crawl_data($data){ | |
| 46 | - | |
| 47 | - # get the option name | |
| 48 | - $option_name = $this->option_name; | |
| 49 | - | |
| 50 | - # handle data | |
| 51 | - $saved = get_option($option_name); | |
| 52 | - | |
| 53 | - # log saved | |
| 54 | - | |
| 55 | - # if saved false save | |
| 56 | - if(empty($saved['urls'])){ | |
| 57 | - | |
| 58 | - # save the option | |
| 59 | - update_option($option_name, $data); | |
| 60 | - | |
| 61 | - return; | |
| 62 | - } | |
| 63 | - | |
| 64 | - # get new unique list of urls | |
| 65 | - $new_list = array_unique(array_merge($data['urls'], $saved['urls'])); | |
| 66 | - | |
| 67 | - # set data | |
| 68 | - $data['urls'] = $new_list; | |
| 69 | - | |
| 70 | - # log saved | |
| 71 | - | |
| 72 | - # save the option | |
| 73 | - update_option($option_name, $data); | |
| 74 | - } | |
| 75 | - | |
| 76 | - /** | |
| 77 | - * Check if a URL has been crawled by Otto | |
| 78 | - * - Validates domain against saved domain | |
| 79 | - * - Ignores query strings | |
| 80 | - * - Does exact path matching as Otto stores paths exactly as they appear | |
| 81 | - * @param string $url - Full URL to check | |
| 82 | - * @return bool - True if URL was crawled, false otherwise | |
| 83 | - */ | |
| 84 | - function is_url_crawled($url) { | |
| 85 | - # Ensure valid input | |
| 86 | - if (empty($url) || !is_string($url)) { | |
| 87 | - return false; | |
| 88 | - } | |
| 89 | - | |
| 90 | - # Get saved crawl data | |
| 91 | - $saved = get_option($this->option_name); | |
| 92 | - | |
| 93 | - if ( | |
| 94 | - empty($saved) || | |
| 95 | - !is_array($saved) || | |
| 96 | - empty($saved['domain']) || | |
| 97 | - empty($saved['urls']) || | |
| 98 | - !is_array($saved['urls']) | |
| 99 | - ) { | |
| 100 | - return false; | |
| 101 | - } | |
| 102 | - | |
| 103 | - # Parse saved domain + incoming URL | |
| 104 | - $saved_domain = parse_url($saved['domain'], PHP_URL_HOST); | |
| 105 | - $incoming_domain = parse_url($url, PHP_URL_HOST); | |
| 106 | - | |
| 107 | - # Domain mismatch - not crawled | |
| 108 | - if (empty($saved_domain) || empty($incoming_domain) || strcasecmp($saved_domain, $incoming_domain) !== 0) { | |
| 109 | - return false; | |
| 110 | - } | |
| 111 | - | |
| 112 | - # Parse incoming path (ignore query string) | |
| 113 | - $parsed_url = parse_url($url); | |
| 114 | - $url_path = $parsed_url['path'] ?? '/'; | |
| 115 | - | |
| 116 | - # Ensure path starts with / but don't modify trailing slashes | |
| 117 | - # Otto stores paths exactly as they appear in URLs | |
| 118 | - if (substr($url_path, 0, 1) !== '/') { | |
| 119 | - $url_path = '/' . $url_path; | |
| 120 | - } | |
| 121 | - | |
| 122 | - # Compare against crawled URLs - exact match | |
| 123 | - foreach ($saved['urls'] as $crawled_url) { | |
| 124 | - if (!is_string($crawled_url)) { | |
| 125 | - continue; | |
| 126 | - } | |
| 127 | - | |
| 128 | - # Direct comparison - Otto stores paths exactly as they are | |
| 129 | - if (strcasecmp($crawled_url, $url_path) === 0) { | |
| 130 | - return true; | |
| 131 | - } | |
| 132 | - } | |
| 133 | - | |
| 134 | - return false; | |
| 135 | - } | |
| 136 | - | |
| 137 | 40 | # get the current route |
| 138 | 41 | function get_route(){ |
| 139 | 42 | |
| 140 | 43 | # check if we're in an HTTP context |
| @@ -196,8 +99,21 @@ | ||
| 196 | 99 | |
| 197 | 100 | # render route html |
| 198 | 101 | function render_route_html(){ |
| 199 | 102 | |
| 103 | + # Headless frontends do not consume WordPress-rendered HTML, so avoid | |
| 104 | + # suggestion lookups, buffering and internal fetches altogether. | |
| 105 | + # Falls back to a guarded direct read rather than skipping the check: | |
| 106 | + # silently skipping would render OTTO markup on a headless site. | |
| 107 | + # @phpstan-ignore-next-line function.alreadyNarrowedType | |
| 108 | + $headless = function_exists('metasync_headless_is_active') | |
| 109 | + ? metasync_headless_is_active() | |
| 110 | + : (class_exists('Metasync_Headless_Config') && Metasync_Headless_Config::is_active()); | |
| 111 | + | |
| 112 | + if ($headless) { | |
| 113 | + return; | |
| 114 | + } | |
| 115 | + | |
| 200 | 116 | # Clear per-request statics before any path runs. Harmless under PHP-FPM |
| 201 | 117 | # (the process ends with the request) but required under persistent-worker |
| 202 | 118 | # SAPIs, where a latched flag would silently disable cache capping and |
| 203 | 119 | # failure reporting for every later request in that worker. |
| @@ -310,8 +226,30 @@ | ||
| 310 | 226 | |
| 311 | 227 | # Get cache status for headers |
| 312 | 228 | $cache_status = Metasync_Otto_Transient_Cache::get_cache_status($cache_track_key); |
| 313 | 229 | |
| 230 | + # POSTMETA BACKSTOP: a MISS means this request already paid for a live | |
| 231 | + # OTTO fetch, which makes it the cheapest possible place to repair the | |
| 232 | + # stored SEO meta when the crawl-notify webhook never ran. The sync is | |
| 233 | + # deferred to a shutdown callback that runs after the response HTML is | |
| 234 | + # built, so the rendered page and its timing are untouched. A HIT never | |
| 235 | + # reaches this branch — steady-state requests keep doing zero extra | |
| 236 | + # work. The class_exists()/method_exists() pair mirrors the guard | |
| 237 | + # above: during a plugin update an opcache can hold this file without | |
| 238 | + # the new class, and an unguarded static call would fatal the page. | |
| 239 | + # The whole hook sits in try/catch: in the mixed-opcache window a missing | |
| 240 | + # class constant is an Error, and the backstop must never fatal the page. | |
| 241 | + try { | |
| 242 | + $is_cache_miss = ($cache_status === Metasync_Otto_Transient_Cache::STATUS_MISS); | |
| 243 | + # @phpstan-ignore-next-line function.alreadyNarrowedType | |
| 244 | + if ($is_cache_miss && class_exists('Metasync_Otto_Backstop_Sync') && method_exists('Metasync_Otto_Backstop_Sync', 'maybe_defer_sync')) { | |
| 245 | + Metasync_Otto_Backstop_Sync::maybe_defer_sync($route, $suggestions, $cache_status); | |
| 246 | + } | |
| 247 | + } catch (Throwable $backstop_error) { | |
| 248 | + // Best-effort repair only; rendering continues untouched. | |
| 249 | + unset($backstop_error); | |
| 250 | + } | |
| 251 | + | |
| 314 | 252 | # RENDER PRIORITY: |
| 315 | 253 | # 1. WP Rocket rocket_buffer filter — WP Rocket caches OTTO-modified HTML, |
| 316 | 254 | # so Kinsta also caches the correct version. No exit(), no cache bypass. |
| 317 | 255 | # 2. Output Buffer (fast) — for environments without WP Rocket. |
| @@ -327,9 +265,9 @@ | ||
| 327 | 265 | # WP ROCKET PATH: highest priority when WP Rocket is active. |
| 328 | 266 | # rocket_buffer fires inside WP Rocket's own cache pipeline, so the |
| 329 | 267 | # cache file WP Rocket writes (and that Kinsta stores) is already |
| 330 | 268 | # OTTO-modified. Eliminates the race where WP Rocket saved pre-OTTO HTML. |
| 331 | - if (class_exists('WP_Rocket')) { | |
| 269 | + if (defined('WP_ROCKET_VERSION') || class_exists('WP_Rocket')) { | |
| 332 | 270 | $wp_rocket_compat_mode = Metasync_Otto_Config::get_wp_rocket_compat_mode(); |
| 333 | 271 | if ($wp_rocket_compat_mode !== 'http') { |
| 334 | 272 | # disable_otto exits in metasync_start_otto() before we get here, so |
| 335 | 273 | # only 'http' compat mode needs to skip the rocket_buffer path. |
| @@ -827,8 +765,9 @@ | ||
| 827 | 765 | Metasync_Otto_Render_Strategy::block_http_cache(); |
| 828 | 766 | } |
| 829 | 767 | |
| 830 | 768 | # continue to render the html |
| 769 | + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- server-rendered route HTML from the OTTO render strategy; escaping would corrupt the document | |
| 831 | 770 | echo $route_html_string; |
| 832 | 771 | |
| 833 | 772 | # prevent further wp execution |
| 834 | 773 | exit(); |