otto_uuid = $otto_uuid; # load the html class $this->o_html = new Metasync_otto_html($otto_uuid); } # method to handle cache refresh # NOTE: Cache system removed - always process in real-time function refresh_cache($route){ # No-op: Cache system has been removed # All pages are processed in real-time from OTTO API return true; } # method to save crawl data into function save_crawl_data($data){ # get the option name $option_name = $this->option_name; # handle data $saved = get_option($option_name); # log saved # if saved false save if(empty($saved['urls'])){ # save the option update_option($option_name, $data); return; } # get new unique list of urls $new_list = array_unique(array_merge($data['urls'], $saved['urls'])); # set data $data['urls'] = $new_list; # log saved # save the option update_option($option_name, $data); } /** * Check if a URL has been crawled by Otto * - Validates domain against saved domain * - Ignores query strings * - Does exact path matching as Otto stores paths exactly as they appear * @param string $url - Full URL to check * @return bool - True if URL was crawled, false otherwise */ function is_url_crawled($url) { # Ensure valid input if (empty($url) || !is_string($url)) { return false; } # Get saved crawl data $saved = get_option($this->option_name); if ( empty($saved) || !is_array($saved) || empty($saved['domain']) || empty($saved['urls']) || !is_array($saved['urls']) ) { return false; } # Parse saved domain + incoming URL $saved_domain = parse_url($saved['domain'], PHP_URL_HOST); $incoming_domain = parse_url($url, PHP_URL_HOST); # Domain mismatch - not crawled if (empty($saved_domain) || empty($incoming_domain) || strcasecmp($saved_domain, $incoming_domain) !== 0) { return false; } # Parse incoming path (ignore query string) $parsed_url = parse_url($url); $url_path = $parsed_url['path'] ?? '/'; # Ensure path starts with / but don't modify trailing slashes # Otto stores paths exactly as they appear in URLs if (substr($url_path, 0, 1) !== '/') { $url_path = '/' . $url_path; } # Compare against crawled URLs - exact match foreach ($saved['urls'] as $crawled_url) { if (!is_string($crawled_url)) { continue; } # Direct comparison - Otto stores paths exactly as they are if (strcasecmp($crawled_url, $url_path) === 0) { return true; } } return false; } # get the current route function get_route(){ # check if we're in an HTTP context if(empty($_SERVER['HTTP_HOST']) || empty($_SERVER['REQUEST_URI'])){ # not in HTTP context (CLI, cron, etc.), return false return false; } # get req scheme $scheme = ( is_ssl() ? 'https' : 'http' ); # get req host $host = $_SERVER['HTTP_HOST']; # get the uri — strip query string before building the route. # OTTO suggestions are canonical-URL-based; query parameters (Kinsta cache-bypass # tokens, UTM params, tracking params, etc.) never produce different OTTO content. # Stripping them ensures that: # - The transient populated via a Kinsta BYPASS request (always has ?params) is # shared with the clean-URL MISS request (no params). # - The OTTO API is called with the canonical URL, not a noisy variant. $request_uri = strtok($_SERVER['REQUEST_URI'], '?') ?: $_SERVER['REQUEST_URI']; # return the formatted url return $scheme . '://' . $host . $request_uri; } # get the html for a route # OPTION 1 IMPLEMENTATION: Use transient cache for suggestions function get_route_html($route, $cache_track_key = null, $suggestions = null){ # If suggestions already provided (from render_route_html), use them directly if ($suggestions !== null && is_array($suggestions)) { # Process route with provided suggestions data return $this->o_html->process_route_with_data($route, $suggestions, ''); } # Get OTTO UUID from options global $metasync_options; $otto_uuid = $metasync_options['general']['otto_pixel_uuid'] ?? ''; if (empty($otto_uuid)) { return false; } # Get suggestions from transient cache (with API fallback) $transient_cache = new Metasync_Otto_Transient_Cache($otto_uuid); $track_key = $cache_track_key ?: md5($route); $suggestions = $transient_cache->get_suggestions($route, $track_key); if (!$suggestions || !$transient_cache->has_payload($suggestions)) { # No suggestions available return false; } # Process route with cached suggestions data return $this->o_html->process_route_with_data($route, $suggestions, ''); } # render route html function render_route_html(){ # Disable SG Cache for Brizy pages FIRST - before any other processing # Using global function defined in otto_pixel.php if (function_exists('metasync_otto_disable_sg_cache_for_brizy')) { metasync_otto_disable_sg_cache_for_brizy(); } # get the route $route = $this->get_route(); # get the current page from globas # this is to help us exclude the login page $page_now = $GLOBALS['pagenow'] ?? false; # check whether page now in excluded pages if(in_array($page_now , $this->no_cache_pages)){ # stop otto return; } /* #uncomment to test on local hosts if(!empty($_GET['otto_test'])){ # @dev $route = 'https://staging-perm.wp65.qa.internal.searchatlas.com/'; } */ # OPTION 1 IMPLEMENTATION: Check transient cache instead of notification data # This makes the system self-healing - works even if notifications fail # Get OTTO UUID from options global $metasync_options; $otto_uuid = $metasync_options['general']['otto_pixel_uuid'] ?? ''; if (empty($otto_uuid)) { return; } $transient_cache = new Metasync_Otto_Transient_Cache($otto_uuid); # Create tracking key for cache status $cache_track_key = 'otto_' . md5($route); # Check if URL has OTTO suggestions (checks transient, calls API if needed) # Use get_suggestions directly to track cache status $suggestions = $transient_cache->get_suggestions($route, $cache_track_key); if (!$suggestions || !$transient_cache->has_payload($suggestions)) { # No suggestions available for this URL # Set header to indicate cache status if (!headers_sent()) { $cache_status = Metasync_Otto_Transient_Cache::get_cache_status($cache_track_key); header('X-MetaSync-OTTO-Cache: ' . ($cache_status ?: 'NO_SUGGESTIONS')); header('X-MetaSync-OTTO-Method: NONE'); } return; } # comment out for fixing pagination issues # $route = rtrim($route, '/'); # Analyze what OTTO is providing for SEO plugin blocking $blocking_flags = $this->analyze_otto_blocking($suggestions); # Get cache status for headers $cache_status = Metasync_Otto_Transient_Cache::get_cache_status($cache_track_key); # RENDER PRIORITY: # 1. WP Rocket rocket_buffer filter — WP Rocket caches OTTO-modified HTML, # so Kinsta also caches the correct version. No exit(), no cache bypass. # 2. Output Buffer (fast) — for environments without WP Rocket. # 3. HTTP Request (last resort) — exits early, bypasses all caching. try { # Safety check: ensure render strategy class exists if (!class_exists('Metasync_Otto_Render_Strategy')) { # Class not loaded - use HTTP method as fallback $this->render_via_http($route, $suggestions, $cache_track_key, $cache_status); return; } # WP ROCKET PATH: highest priority when WP Rocket is active. # rocket_buffer fires inside WP Rocket's own cache pipeline, so the # cache file WP Rocket writes (and that Kinsta stores) is already # OTTO-modified. Eliminates the race where WP Rocket saved pre-OTTO HTML. if (class_exists('WP_Rocket')) { $wp_rocket_compat_mode = Metasync_Otto_Config::get_wp_rocket_compat_mode(); if ($wp_rocket_compat_mode !== 'http') { # disable_otto exits in metasync_start_otto() before we get here, so # only 'http' compat mode needs to skip the rocket_buffer path. $this->render_via_rocket_buffer($suggestions, $blocking_flags, $cache_status); return; } } $render_method = Metasync_Otto_Render_Strategy::determine_method(); if ($render_method === Metasync_Otto_Render_Strategy::METHOD_BUFFER) { # FAST PATH: Use output buffer approach $result = $this->render_via_buffer($route, $suggestions, $blocking_flags, $cache_status); if ($result === true) { # Buffer is now active, WordPress will continue rendering # OTTO modifications will be applied when buffer flushes return; } # Buffer approach failed, fall back to HTTP method $render_method = Metasync_Otto_Render_Strategy::METHOD_HTTP; } if ($render_method === Metasync_Otto_Render_Strategy::METHOD_HTTP) { # FALLBACK PATH: Use traditional HTTP request approach $this->render_via_http($route, $suggestions, $cache_track_key, $cache_status); } } catch (Exception $e) { # Any exception in the render strategy - fall back to HTTP method $this->render_via_http($route, $suggestions, $cache_track_key, $cache_status); } catch (Error $e) { # PHP 7+ Error (like TypeError) - fall back to HTTP method $this->render_via_http($route, $suggestions, $cache_track_key, $cache_status); } } /** * Render page using WP Rocket's rocket_buffer filter (PREFERRED for WP Rocket sites) * * When WP Rocket is active, hooking into rocket_buffer ensures WP Rocket * caches the OTTO-modified HTML. This means: * - WP Rocket's cache file = OTTO-modified HTML ✓ * - Kinsta FastCGI cache = WP Rocket output = OTTO-modified HTML ✓ * - No exit() = proper cache lifecycle ✓ * * The filter fires on every uncached PHP request. Once WP Rocket has cached * the result, subsequent requests are served directly from cache with zero PHP. * * @param array $suggestions OTTO suggestions data * @param array $blocking_flags SEO plugin blocking flags * @param string $cache_status Cache status for X-MetaSync-* headers */ private function render_via_rocket_buffer($suggestions, $blocking_flags, $cache_status) { $o_html = $this->o_html; # Hook into WP Rocket's HTML buffer at priority 1 so other rocket_buffer # filters (minifiers, CDN rewriters, etc.) run on already-OTTO-modified HTML. add_filter('rocket_buffer', function($html) use ($o_html, $suggestions, $blocking_flags) { if (empty($html) || strlen($html) < 100) { // Tell WP Rocket not to cache this empty/broken response. if (!defined('DONOTCACHEPAGE')) { define('DONOTCACHEPAGE', true); } return $html; } # Only process full HTML documents — skip partials, JSON, error responses if (stripos($html, 'process_html_directly($html, $data); # Sanity check: modified HTML must be at least 50% the size of original if ($modified && strlen($modified) > strlen($html) * 0.5) { return $modified; } } catch (Exception $e) { // Fall through — return original HTML on any failure } catch (Error $e) { // Fall through — return original HTML on any failure } return $html; }, 1); # Block SEO plugins before wp_head fires so duplicate tags aren't output $description_tags = $blocking_flags['block_description_tags'] ?? []; $has_description_tags = !empty($description_tags); if ($blocking_flags['block_title'] || $has_description_tags) { if (function_exists('metasync_otto_block_seo_plugins')) { metasync_otto_block_seo_plugins( $blocking_flags['block_title'], $has_description_tags, $description_tags ); } } # Send X-MetaSync-* diagnostic headers before WordPress outputs anything if (!headers_sent()) { Metasync_Otto_Render_Strategy::set_current_method(Metasync_Otto_Render_Strategy::METHOD_WP_ROCKET); Metasync_Otto_Render_Strategy::send_headers($cache_status); } # Return — WordPress continues its normal render lifecycle. # WP Rocket's buffer captures the full HTML, fires rocket_buffer, # our callback modifies it, and WP Rocket caches the OTTO version. } /** * Analyze OTTO suggestions to determine what to block from SEO plugins * * @param array $suggestions OTTO suggestions data * @return array Blocking flags */ private function analyze_otto_blocking($suggestions) { $has_otto_title = false; $otto_description_tags = []; // Track specific description tags Otto provides if (!empty($suggestions['header_replacements']) && is_array($suggestions['header_replacements'])) { foreach ($suggestions['header_replacements'] as $item) { if (!empty($item['type'])) { # Check if OTTO has title if ($item['type'] == 'title' && !empty($item['recommended_value'])) { $has_otto_title = true; } # Check if OTTO has description - track specific tag types if ($item['type'] == 'meta') { # Check for meta[name=description] if (!empty($item['name']) && $item['name'] == 'description' && !empty($item['recommended_value'])) { $otto_description_tags[] = 'meta[name=description]'; } # Check for meta[property=og:description] if (!empty($item['property']) && $item['property'] == 'og:description' && !empty($item['recommended_value'])) { $otto_description_tags[] = 'meta[property=og:description]'; } # Check for meta[name=twitter:description] if (!empty($item['name']) && $item['name'] == 'twitter:description' && !empty($item['recommended_value'])) { $otto_description_tags[] = 'meta[name=twitter:description]'; } } } } } # Check header_html_insertion for description # Must have a non-empty content value, otherwise Yoast would be blocked with nothing to replace it if (!empty($suggestions['header_html_insertion'])) { if (preg_match('/]*name=["\']description["\'][^>]*content=["\']([^"\']+)["\'][^>]*>/i', $suggestions['header_html_insertion'])) { $otto_description_tags[] = 'meta[name=description]'; } } # Remove duplicates $otto_description_tags = array_unique($otto_description_tags); return [ 'block_title' => $has_otto_title, 'block_description_tags' => $otto_description_tags, // Pass array of specific tags ]; } /** * Render page using output buffer approach (FAST) * Eliminates the internal HTTP request by capturing WordPress output directly * * @param string $route Current page route * @param array $suggestions OTTO suggestions data * @param array $blocking_flags SEO plugin blocking flags * @param string $cache_status Cache status for headers * @return bool True if buffer started successfully, false to fall back to HTTP */ private function render_via_buffer($route, $suggestions, $blocking_flags, $cache_status) { # Try to start output buffer $buffer_started = Metasync_Otto_Render_Strategy::start_buffer( $suggestions, $route, $this->o_html, $blocking_flags ); if (!$buffer_started) { # Buffer failed to start return false; } # Buffer is active - send headers now (before any output) if (!headers_sent()) { Metasync_Otto_Render_Strategy::send_headers($cache_status); } # Block SEO plugins if needed (for the buffered output) $description_tags = $blocking_flags['block_description_tags'] ?? []; $has_description_tags = !empty($description_tags); if ($blocking_flags['block_title'] || $has_description_tags) { if (function_exists('metasync_otto_block_seo_plugins')) { metasync_otto_block_seo_plugins( $blocking_flags['block_title'], $has_description_tags, $description_tags ); } } # Return true - WordPress will continue rendering, buffer will capture and process return true; } /** * Render page using HTTP request approach (FALLBACK) * Makes internal wp_remote_get request to fetch page HTML * * @param string $route Current page route * @param array $suggestions OTTO suggestions data * @param string $cache_track_key Cache tracking key * @param string $cache_status Cache status for headers */ private function render_via_http($route, $suggestions, $cache_track_key, $cache_status) { # Set method indicator Metasync_Otto_Render_Strategy::set_current_method(Metasync_Otto_Render_Strategy::METHOD_HTTP); # WP-315: On Divi sites, skip OTTO for the first HTTP render per page # per 24h. OTTO's internal wp_remote_get creates corrupted CSS cache # files in et-cache/{post_id}/ (wrong server context). By returning # false once, WordPress renders the page normally — building correct # Divi CSS caches. Subsequent OTTO renders within 24h use those caches. # The transient stores the activation timestamp so it auto-invalidates # when the plugin is deactivated/reactivated. if (defined('ET_CORE_VERSION')) { $cache_key = 'otto_divi_css_fix_' . md5($route); $activated = get_option('metasync_activated_at', ''); $cached = get_transient($cache_key); if ($cached === false || $cached !== $activated) { set_transient($cache_key, $activated, DAY_IN_SECONDS); return false; } } # check if we have the route html (pass suggestions to avoid duplicate API call) $route_html = $this->get_route_html($route, $cache_track_key, $suggestions); # check that route html is valid if(empty($route_html)){ return false; } $route_html_string = is_string($route_html) ? $route_html : $route_html->__toString(); # WP-315: Detect incomplete Divi rendering in OTTO's HTTP render output. # OTTO's internal wp_remote_get runs in a different server context than the # normal page load. This can cause Divi to produce incomplete output: # # 1. Cold CSS cache: Divi outputs inline '; $route_html_string = str_replace('', $style_tag . "\n" . '', $route_html_string); } } } } # Send response headers Metasync_Otto_Render_Strategy::send_headers($cache_status); # continue to render the html echo $route_html_string; # prevent further wp execution exit(); } }