$value) { $search_url .= '&' . urlencode($key) . '=' . urlencode($value); } } // Get cookies (use provided or default) $cookies = isset($options['cookies']) ? $options['cookies'] : self::$default_cookies; // Get headers (use provided or default) $headers = isset($options['headers']) ? $options['headers'] : self::get_default_headers(); // Initialize curl $ch = curl_init($search_url); // Set curl options curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, isset($options['timeout']) ? $options['timeout'] : 30); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_COOKIE, $cookies); // Execute request $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); curl_close($ch); // Check for curl errors if ($response === false || !empty($curl_error)) { return new WP_Error('curl_error', __('cURL Error: ', 'ai-auto-tool') . $curl_error); } // Check HTTP status code if ($http_code !== 200) { return new WP_Error('http_error', sprintf(__('HTTP Error: %d', 'ai-auto-tool'), $http_code)); } // Check if response is valid HTML if (empty($response) || strlen($response) < 100) { return new WP_Error('empty_response', __('Empty or too short response from Google', 'ai-auto-tool')); } // Check for CAPTCHA, blocking, or JavaScript challenge $blocked_indicators = array( 'captcha', 'unusual traffic', 'automated queries', 'knitsail', // Google's anti-bot system 'enablejs', // JavaScript challenge redirect 'SG_REL', // Security challenge 'If you\'re having trouble accessing Google Search', // Challenge message 'window.google.c.e', // Google challenge JavaScript 'challenge_version' ); $is_blocked = false; foreach ($blocked_indicators as $indicator) { if (stripos($response, $indicator) !== false) { $is_blocked = true; break; } } // Also check if response is mostly JavaScript (challenge page) $js_ratio = substr_count(strtolower($response), 'function') / max(strlen($response) / 1000, 1); if ($js_ratio > 0.1 && strlen($response) < 50000) { // Likely a challenge page (lots of JS, small size) $is_blocked = true; } if ($is_blocked) { return new WP_Error('blocked', __('Google detected automated requests and returned a challenge page. Please use Google Custom Search API or update cookies/headers.', 'ai-auto-tool')); } // Save HTML for debugging if requested if (isset($options['save_html']) && $options['save_html']) { $debug_file = isset($options['debug_file']) ? $options['debug_file'] : sys_get_temp_dir() . '/google-search-debug-' . time() . '.html'; // Ensure we save as text, not binary file_put_contents($debug_file, $response); } // Parse results $results = self::parse_search_results($response); return array( 'success' => true, 'query' => $query, 'url' => $search_url, 'http_code' => $http_code, 'results' => $results, 'raw_html' => $response ); } /** * Parse Google search results from HTML * * @param string $html HTML content * @return array Parsed results */ private static function parse_search_results($html) { $results = array( 'organic' => array(), 'ads' => array(), 'featured_snippet' => null, 'people_also_ask' => array(), 'related_searches' => array() ); // First try: Regex parsing as fallback (more reliable) $regex_results = self::parse_search_results_regex($html); if (!empty($regex_results['organic'])) { $results['organic'] = $regex_results['organic']; } // Second try: DOM parsing (more accurate but can fail) libxml_use_internal_errors(true); $dom = new DOMDocument(); @$dom->loadHTML('' . $html); $xpath = new DOMXPath($dom); // Multiple selectors for organic results (Google changes structure frequently) $organic_selectors = array( "//div[contains(@class, 'g')]", "//div[@data-ved]", "//div[contains(@class, 'tF2Cxc')]", "//div[contains(@class, 'MjjYud')]", "//div[@jscontroller='SC7lYd']", "//div[contains(@class, 'hlcw0c')]", "//div[contains(@class, 'g-blk')]" ); $processed_nodes = array(); $dom_results = array(); foreach ($organic_selectors as $selector) { $organic_nodes = $xpath->query($selector); foreach ($organic_nodes as $node) { // Skip if already processed $node_id = spl_object_hash($node); if (isset($processed_nodes[$node_id])) { continue; } $processed_nodes[$node_id] = true; $result = array(); // Extract title - multiple possible locations $title_selectors = array( ".//h3", ".//h3[contains(@class, 'LC20lb')]", ".//h3[contains(@class, 'DKV0Md')]", ".//h3[contains(@class, 'zBAuLc')]", ".//a//h3", ".//div[contains(@class, 'yuRUbf')]//h3", ".//div[contains(@class, 'N54PNb')]//h3" ); foreach ($title_selectors as $title_sel) { $title_node = $xpath->query($title_sel, $node)->item(0); if ($title_node && !empty(trim($title_node->textContent))) { $result['title'] = trim($title_node->textContent); break; } } // Extract URL - multiple possible locations $link_selectors = array( ".//a[@href]", ".//div[contains(@class, 'yuRUbf')]//a[@href]", ".//a[contains(@class, 'BVG0Nb')][@href]", ".//div[contains(@class, 'N54PNb')]//a[@href]" ); foreach ($link_selectors as $link_sel) { $link_nodes = $xpath->query($link_sel, $node); foreach ($link_nodes as $link_node) { $href = $link_node->getAttribute('href'); if (!empty($href)) { // Clean Google redirect URLs if (strpos($href, '/url?q=') === 0 || strpos($href, '/url?') === 0) { $parsed = parse_url($href); if (isset($parsed['query'])) { parse_str($parsed['query'], $params); if (isset($params['q'])) { $href = $params['q']; } } else { $href = urldecode(substr($href, 7)); $href = explode('&', $href)[0]; } } // Skip Google internal URLs and validate if ((strpos($href, 'http://') === 0 || strpos($href, 'https://') === 0 || strpos($href, '//') === 0) && strpos($href, 'google.com') === false && strpos($href, 'googleusercontent.com') === false) { $result['url'] = $href; break 2; // Break both loops } } } } // Extract snippet - multiple possible locations $snippet_selectors = array( ".//span[contains(@class, 'aCOpRe')]", ".//div[contains(@class, 'VwiC3b')]", ".//span[contains(@class, 'st')]", ".//span[contains(@class, 'aST4p')]", ".//div[contains(@class, 's')]//span", ".//div[contains(@class, 'IsZvec')]//span", ".//div[contains(@class, 'yXK7lf')]//span" ); foreach ($snippet_selectors as $snippet_sel) { $snippet_nodes = $xpath->query($snippet_sel, $node); if ($snippet_nodes->length > 0) { $snippet_text = trim($snippet_nodes->item(0)->textContent); if (!empty($snippet_text) && strlen($snippet_text) > 20) { $result['snippet'] = $snippet_text; break; } } } // Only add if we have at least title and URL if (!empty($result['title']) && !empty($result['url'])) { $dom_results[] = $result; } } } // Use DOM results if we got more than regex, otherwise use regex if (count($dom_results) > count($results['organic'])) { $results['organic'] = $dom_results; } elseif (empty($results['organic']) && !empty($dom_results)) { $results['organic'] = $dom_results; } // Parse featured snippet (answer box) $featured_node = $xpath->query("//div[contains(@class, 'xpdopen')] | //div[@data-attrid]")->item(0); if ($featured_node) { $featured = array(); $featured_title = $xpath->query(".//h2 | .//h3", $featured_node)->item(0); if ($featured_title) { $featured['title'] = trim($featured_title->textContent); } $featured_text = $xpath->query(".//span[contains(@class, 'hgKElc')] | .//div[contains(@class, 'Z0LcW')]", $featured_node)->item(0); if ($featured_text) { $featured['text'] = trim($featured_text->textContent); } if (!empty($featured)) { $results['featured_snippet'] = $featured; } } // Parse "People Also Ask" section $paa_nodes = $xpath->query("//div[contains(@class, 'related-question-pair')] | //div[@data-ved and .//div[contains(@class, 'cbphWd')]]"); foreach ($paa_nodes as $paa_node) { $question = $xpath->query(".//div[contains(@class, 'cbphWd')] | .//span[contains(@class, 'CSkcDe')]", $paa_node)->item(0); if ($question) { $results['people_also_ask'][] = trim($question->textContent); } } // Parse related searches $related_nodes = $xpath->query("//div[contains(@class, 'brs_col')]//a | //a[contains(@class, 's75CSd')]"); foreach ($related_nodes as $related_node) { $related_text = trim($related_node->textContent); if (!empty($related_text)) { $results['related_searches'][] = $related_text; } } return $results; } /** * Parse search results using regex (fallback method) * More reliable when DOM parsing fails * * @param string $html HTML content * @return array Parsed results */ private static function parse_search_results_regex($html) { $results = array( 'organic' => array() ); // Pattern 1: Extract from data-ved divs with h3 and links // Look for divs containing h3 tags followed by links preg_match_all('/