and ).)*?\bimg\s*,\s*span\s*\{[^}]*position\s*:\s*absolute[^}]*\}(?:(?!).)*?#is', '', $html ); } /** * WP-465: Keep the charset declaration within the first 1024 bytes of . * * OTTO injects meta tags + JSON-LD schema at the top of , which can push * the theme's past the 1024-byte limit that browsers * enforce for in-document charset detection (HTML spec). When that happens — * and a cached/CDN response is served without an HTTP charset header — the * document falls back to the locale encoding (Windows-1252). External * stylesheets that declare no @charset of their own (e.g. a theme rule like * `content:"\2713"` written as a raw UTF-8 ✓) then inherit that wrong encoding * and render as mojibake ("âœ"" instead of "✓"). * * We guarantee a as the first child of whenever * one isn't already present within the first 1024 bytes. Harmless when a valid * early charset already exists (we skip), and a duplicate later declaration is * ignored by the browser (first one wins). * * @param string $html * @return string */ private function ensure_early_charset_meta($html) { if (!preg_match('/]*>/i', $html, $m, PREG_OFFSET_CAPTURE)) { return $html; } $inner_start = $m[0][1] + strlen($m[0][0]); # Already declared early enough? Leave it alone (also covers AMP, which # requires charset as the first child). $window = substr($html, $inner_start, 1024); if (preg_match('/]*charset/i', $window)) { return $html; } return substr($html, 0, $inner_start) . '' . substr($html, $inner_start); } # function __construct($otto_uuid){ # set the site uuid using the provided string $this->site_uuid = $otto_uuid; # Use endpoint manager if available, otherwise fallback to production if (class_exists('Metasync_Endpoint_Manager')) { $this->otto_end_point = Metasync_Endpoint_Manager::get_endpoint('OTTO_URL_DETAILS'); } else { $this->otto_end_point = 'https://sa.searchatlas.com/api/v2/otto-url-details'; } # laod the simple html dom parser with UTF-8 charset to handle special characters $this->dom = new HtmlDocument(null, true, true, 'UTF-8', false); } /** * Check Route Method * @param route : The route to check * @param path : The path of the html file to save */ function process_route($route, $file_path){ # Construct the full endpoint URL with query parameters $url_with_params = add_query_arg( [ 'url' => $route, 'uuid' => $this->site_uuid, ], $this->otto_end_point ); # PERFORMANCE FIX: Add timeout to prevent blocking $args = array( 'timeout' => 5, // 5 second max timeout (allow time for redirects) 'redirection' => 5, // CRITICAL FIX: Allow redirects (API returns 301) 'user-agent' => 'MetaSync-OTTO-SSR/2.0', 'sslverify' => true ); # Perform the GET request with timeout $response = wp_remote_get($url_with_params, $args); # Check for errors if (is_wp_error($response)) { error_log('MetaSync ' . Metasync::get_whitelabel_otto_name() . ': API call failed - ' . $response->get_error_message()); return false; } # get the response body $body = wp_remote_retrieve_body($response); # Get the response code $response_code = wp_remote_retrieve_response_code($response); # if no change data skip if (empty($body) || $response_code !== 200){ error_log('MetaSync ' . Metasync::get_whitelabel_otto_name() . ': API returned empty or non-200. Code: ' . $response_code); return false; } # set the html file path $this->html_file = $file_path; # load change data $change_data = json_decode($body, true); # Process with the fetched data return $this->process_route_with_data($route, $change_data, $file_path); } /** * Process route with pre-fetched suggestions data * OPTION 1: Used when data comes from transient cache * @param route : The route to check * @param change_data : Pre-fetched OTTO suggestions data * @param path : The path of the html file to save */ function process_route_with_data($route, $change_data, $file_path){ if (empty($change_data) || !is_array($change_data)) { return false; } # set the html file path $this->html_file = $file_path; # Analyze what Otto is providing and store for conditional SEO blocking $has_otto_title = false; $otto_description_tags = []; // Track specific description tags Otto provides if (!empty($change_data['header_replacements']) && is_array($change_data['header_replacements'])) { foreach ($change_data['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 non-empty content value if (!empty($change_data['header_html_insertion'])) { if (preg_match('/]*name=["\']description["\'][^>]*content=["\']([^"\']+)["\'][^>]*>/i', $change_data['header_html_insertion'])) { $otto_description_tags[] = 'meta[name=description]'; } } # Remove duplicates $otto_description_tags = array_unique($otto_description_tags); # Store blocking flags to pass to handle_route_html # This will be added to the internal fetch URL as parameters $change_data['_otto_blocking'] = array( 'block_title' => $has_otto_title, 'block_description_tags' => $otto_description_tags // Pass array of specific tags to remove ); # Process the route with the suggestions data return $this->handle_route_html($route, $change_data); } # function to get tag attributes function get_tag_attributes($tag){ # Extract existing attributes of the tag $attributes = []; # set the tag attributes $tag_attributes = []; # check that the tag attributes if(!is_object($tag) || !method_exists($tag, 'getAllAttributes')){ return ''; } # get the tag attributes $tag_attributes = $tag->getAllAttributes(); # loop all attributes foreach ($tag_attributes as $key => $value) { if ($value == 1) { # Handle boolean attributes $attributes[] = htmlspecialchars($key, ENT_QUOTES); } else { # Handle attributes with values $attributes[] = $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; } } # Convert attributes array to a string $attributes_string = !empty($attributes) ? ' ' . implode(' ', $attributes) : ''; # return the attributes string return $attributes_string; } function handle_route_html($route, $replacement_data){ # Detect if current page uses Brizy and disable SG Cache if so # 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(); } # lablel the Otto Route # label otto requests to avoid loops // $request_body = add_query_arg( // [ // 'is_otto_page_fetch' => 1 // ], // $route // ); # Add blocking flags as URL parameters (no database writes!) $url_params = ['is_otto_page_fetch' => 1]; # Add blocking flags if available if (!empty($replacement_data['_otto_blocking'])) { $url_params['otto_block_title'] = $replacement_data['_otto_blocking']['block_title'] ? '1' : '0'; # For HTTP fetch path, check if any description tags need blocking $block_description_tags = $replacement_data['_otto_blocking']['block_description_tags'] ?? []; $url_params['otto_block_desc'] = !empty($block_description_tags) ? '1' : '0'; } $request_body = add_query_arg($url_params, $route); # TUNNEL/PROXY SUPPORT: If site is behind a tunnel (ngrok, zrok, etc.) # and loopback requests fail, try using localhost instead $request_body = apply_filters('metasync_otto_internal_fetch_url', $request_body, $route); # set cookie header var $cookie_header = ''; # loop cookies to set header foreach ($_COOKIE as $name => $value) { # handle array values by converting to string $cookie_value = is_array($value) ? serialize($value) : $value; # add cookie to header # $cookie_header .= $name . '=' . $value . '; '; $cookie_header .= $name . '=' . $cookie_value . '; '; } # trim the string $cookie_header = rtrim($cookie_header, '; '); # Allow timeout customization for slow tunnel environments $fetch_timeout = apply_filters('metasync_otto_internal_fetch_timeout', 5); $args = array( 'sslverify' => false, // Disabled for localhost/tunnel environments 'timeout' => $fetch_timeout, // Configurable timeout for tunnels 'redirection' => 5, 'httpversion' => '1.1', 'headers' => array( 'Cookie' => $cookie_header, 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'X-OTTO-Internal-Fetch' => '1', 'User-Agent' => 'MetaSync-OTTO-SSR/3.0', 'X-Forwarded-Host' => $_SERVER['HTTP_HOST'] ?? '', // Preserve original host for tunnels ) ); # get the associateed route html $route_html = wp_remote_get($request_body, $args); # Check for timeout or connection errors if (is_wp_error($route_html)) { $error_msg = $route_html->get_error_message(); error_log('MetaSync ' . Metasync::get_whitelabel_otto_name() . ' DEBUG: FAILED - wp_remote_get error: ' . $error_msg . ' for route: ' . $route); return false; } # get body $html_body = wp_remote_retrieve_body($route_html); # Get the response code $response_code = wp_remote_retrieve_response_code($route_html); # check not empty if(empty($html_body) || $response_code !== 200){ error_log('MetaSync ' . Metasync::get_whitelabel_otto_name() . ' DEBUG: FAILED - Empty body or non-200 status for route: ' . $route); return false; } # Remove XML declaration $html_body = preg_replace('/<\?xml[^?]*\?>\s*/i', '', $html_body); # WP-355 / WP-315: Save ALL original