', # Page title indicator
+ 'id="rso"', # Results container
+ 'class="g"', # Result class (without div tag)
+]
+
+
+def read_user_agents(file_path: str) -> List[str]:
+ """Read user agent strings from a file, one per line."""
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ user_agents = [line.strip() for line in f if line.strip()]
+ return user_agents
+ except FileNotFoundError:
+ print(f"Error: File '{file_path}' not found.", file=sys.stderr)
+ sys.exit(1)
+ except Exception as e:
+ print(f"Error reading file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+
+def test_user_agent(user_agent: str, query: str = "test", timeout: float = 10.0) -> Tuple[bool, str]:
+ """
+ Test a user agent against Google search.
+
+ Returns:
+ Tuple of (is_working: bool, reason: str)
+ """
+ url = "https://www.google.com/search"
+ params = {"q": query, "gbv": "1", "num": "10"}
+
+ headers = {
+ "User-Agent": user_agent,
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Connection": "keep-alive",
+ "Upgrade-Insecure-Requests": "1",
+ }
+
+ try:
+ response = requests.get(url, params=params, headers=headers, timeout=timeout)
+
+ # Check HTTP status
+ if response.status_code == 429:
+ # Rate limited - raise this so we can handle it specially
+ raise Exception(f"Rate limited (429)")
+ if response.status_code >= 500:
+ return False, f"Server error ({response.status_code})"
+ if response.status_code == 403:
+ return False, f"Blocked ({response.status_code})"
+ if response.status_code >= 400:
+ return False, f"HTTP {response.status_code}"
+
+ body_lower = response.text.lower()
+
+ # Check for block markers
+ for marker in BLOCK_MARKERS:
+ if marker.lower() in body_lower:
+ return False, f"Blocked: {marker}"
+
+ # Check for redirect indicators first - these indicate non-working responses
+ has_redirect = ("window.location" in body_lower or "location.href" in body_lower) and "google.com" not in body_lower
+ if has_redirect:
+ return False, "JavaScript redirect detected"
+
+ # Check for noscript redirect (another indicator of JS-only page)
+ if 'noscript' in body_lower and 'http-equiv="refresh"' in body_lower:
+ return False, "NoScript redirect page"
+
+ # Check for success markers (actual search results)
+ # We need at least one strong indicator of search results
+ has_results = any(marker in response.text for marker in SUCCESS_MARKERS)
+
+ if has_results:
+ return True, "OK - Has search results"
+ else:
+ # Check for very short responses (likely error pages)
+ if len(response.text) < 1000:
+ return False, "Response too short (likely error page)"
+ # If we don't have success markers, it's not a working response
+ # Even if it's substantial and doesn't have block markers, it might be a JS-only page
+ return False, "No search results found"
+
+ except requests.Timeout:
+ return False, "Request timeout"
+ except requests.HTTPError as e:
+ if e.response and e.response.status_code == 429:
+ # Rate limited - raise this so we can handle it specially
+ raise Exception(f"Rate limited (429) - {str(e)}")
+ return False, f"HTTP error: {str(e)}"
+ except requests.RequestException as e:
+ # Check if it's a 429 in the response
+ if hasattr(e, 'response') and e.response and e.response.status_code == 429:
+ raise Exception(f"Rate limited (429) - {str(e)}")
+ return False, f"Request error: {str(e)}"
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Test User Agent strings against Google to find working ones.",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ python test_google_user_agents.py UAs.txt
+ python test_google_user_agents.py UAs.txt --output working_uas.txt
+ python test_google_user_agents.py UAs.txt --query "python programming"
+ """
+ )
+ parser.add_argument(
+ "user_agent_file",
+ help="Path to file containing user agent strings (one per line)"
+ )
+ parser.add_argument(
+ "--output", "-o",
+ help="Output file to write working user agents (default: stdout)"
+ )
+ parser.add_argument(
+ "--query", "-q",
+ default=None,
+ help="Search query to use for testing (default: cycles through random queries)"
+ )
+ parser.add_argument(
+ "--random-queries", "-r",
+ action="store_true",
+ help="Use random queries from a predefined list (default: True if --query not specified)"
+ )
+ parser.add_argument(
+ "--timeout", "-t",
+ type=float,
+ default=10.0,
+ help="Request timeout in seconds (default: 10.0)"
+ )
+ parser.add_argument(
+ "--delay", "-d",
+ type=float,
+ default=0.5,
+ help="Delay between requests in seconds (default: 0.5)"
+ )
+ parser.add_argument(
+ "--verbose", "-v",
+ action="store_true",
+ help="Show detailed results for each user agent"
+ )
+
+ args = parser.parse_args()
+
+ # Determine query strategy
+ use_random_queries = args.random_queries or (args.query is None)
+ if use_random_queries:
+ search_queries = DEFAULT_SEARCH_QUERIES.copy()
+ random.shuffle(search_queries) # Shuffle for variety
+ current_query_idx = 0
+ query_display = f"cycling through {len(search_queries)} random queries"
+ else:
+ search_queries = [args.query]
+ query_display = f"'{args.query}'"
+
+ # Read user agents
+ user_agents = read_user_agents(args.user_agent_file)
+ if not user_agents:
+ print("No user agents found in file.", file=sys.stderr)
+ sys.exit(1)
+
+ print(f"Testing {len(user_agents)} user agents against Google...", file=sys.stderr)
+ print(f"Query: {query_display}", file=sys.stderr)
+ if args.output:
+ print(f"Output file: {args.output} (appending results incrementally)", file=sys.stderr)
+ print(file=sys.stderr)
+
+ # Load existing working user agents from output file to avoid duplicates
+ existing_working = set()
+ if args.output:
+ try:
+ with open(args.output, 'r', encoding='utf-8') as f:
+ existing_working = {line.strip() for line in f if line.strip()}
+ if existing_working:
+ print(f"Found {len(existing_working)} existing user agents in output file", file=sys.stderr)
+ except FileNotFoundError:
+ # File doesn't exist yet, that's fine
+ pass
+ except Exception as e:
+ print(f"Warning: Could not read existing output file: {e}", file=sys.stderr)
+
+ # Open output file for incremental writing if specified (append mode)
+ output_file = None
+ if args.output:
+ try:
+ output_file = open(args.output, 'a', encoding='utf-8')
+ except Exception as e:
+ print(f"Error opening output file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ working_agents = []
+ failed_count = 0
+ skipped_count = 0
+ last_successful_idx = 0
+
+ try:
+ for idx, ua in enumerate(user_agents, 1):
+ # Skip testing if this UA is already in the working file
+ if args.output and ua in existing_working:
+ skipped_count += 1
+ if args.verbose:
+ print(f"[{idx}/{len(user_agents)}] ā SKIPPED - Already in working file", file=sys.stderr)
+ last_successful_idx = idx
+ continue
+
+ try:
+ # Get the next query (cycle through if using random queries)
+ if use_random_queries:
+ query = search_queries[current_query_idx % len(search_queries)]
+ current_query_idx += 1
+ else:
+ query = args.query
+
+ is_working, reason = test_user_agent(ua, query, args.timeout)
+
+ if is_working:
+ working_agents.append(ua)
+ status = "ā"
+ # Write immediately to output file if specified (skip if duplicate)
+ if output_file:
+ if ua not in existing_working:
+ output_file.write(ua + '\n')
+ output_file.flush() # Ensure it's written to disk
+ existing_working.add(ua) # Track it to avoid duplicates
+ else:
+ if args.verbose:
+ print(f"[{idx}/{len(user_agents)}] {status} WORKING (duplicate, skipped) - {reason}", file=sys.stderr)
+ # Also print to stdout if no output file
+ if not args.output:
+ print(ua)
+
+ if args.verbose:
+ print(f"[{idx}/{len(user_agents)}] {status} WORKING - {reason}", file=sys.stderr)
+ else:
+ failed_count += 1
+ status = "ā"
+ if args.verbose:
+ print(f"[{idx}/{len(user_agents)}] {status} FAILED - {reason}", file=sys.stderr)
+
+ last_successful_idx = idx
+
+ # Progress indicator for non-verbose mode
+ if not args.verbose and idx % 10 == 0:
+ print(f"Progress: {idx}/{len(user_agents)} tested ({len(working_agents)} working, {failed_count} failed)", file=sys.stderr)
+
+ # Delay between requests to avoid rate limiting
+ if idx < len(user_agents):
+ time.sleep(args.delay)
+
+ except KeyboardInterrupt:
+ print(file=sys.stderr)
+ print(f"\nInterrupted by user at index {idx}/{len(user_agents)}", file=sys.stderr)
+ print(f"Last successful test: {last_successful_idx}/{len(user_agents)}", file=sys.stderr)
+ break
+ except Exception as e:
+ # Handle unexpected errors (like network issues or rate limits)
+ error_msg = str(e)
+ if "429" in error_msg or "Rate limited" in error_msg:
+ print(file=sys.stderr)
+ print(f"\nā ļø RATE LIMIT DETECTED at index {idx}/{len(user_agents)}", file=sys.stderr)
+ print(f"Last successful test: {last_successful_idx}/{len(user_agents)}", file=sys.stderr)
+ print(f"Working user agents found so far: {len(working_agents)}", file=sys.stderr)
+ if args.output:
+ print(f"Results saved to: {args.output}", file=sys.stderr)
+ print(f"\nTo resume later, you can skip the first {last_successful_idx} user agents.", file=sys.stderr)
+ raise # Re-raise to exit the loop
+ else:
+ print(f"[{idx}/{len(user_agents)}] ERROR - {error_msg}", file=sys.stderr)
+ failed_count += 1
+ last_successful_idx = idx
+ if idx < len(user_agents):
+ time.sleep(args.delay)
+ continue
+
+ finally:
+ # Close output file if opened
+ if output_file:
+ output_file.close()
+
+ # Summary
+ print(file=sys.stderr)
+ tested_count = last_successful_idx - skipped_count
+ print(f"Summary: {len(working_agents)} working, {failed_count} failed, {skipped_count} skipped out of {last_successful_idx} processed (of {len(user_agents)} total)", file=sys.stderr)
+ if last_successful_idx < len(user_agents):
+ print(f"Note: Processing stopped at index {last_successful_idx}. {len(user_agents) - last_successful_idx} user agents not processed.", file=sys.stderr)
+ if args.output:
+ print(f"Results saved to: {args.output}", file=sys.stderr)
+
+ return 0 if working_agents else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
+
diff --git a/misc/generate_uas.py b/misc/generate_uas.py
new file mode 100755
index 0000000..df46577
--- /dev/null
+++ b/misc/generate_uas.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+"""
+Standalone Opera User Agent String Generator
+
+This tool generates Opera-based User Agent strings that can be used with Whoogle.
+It can be run independently to generate and display UA strings on demand.
+
+Usage:
+ python misc/generate_uas.py [count]
+
+Arguments:
+ count: Number of UA strings to generate (default: 10)
+
+Examples:
+ python misc/generate_uas.py # Generate 10 UAs
+ python misc/generate_uas.py 20 # Generate 20 UAs
+"""
+
+import sys
+import os
+
+# Default fallback UA if generation fails
+DEFAULT_FALLBACK_UA = "Opera/9.30 (Nintendo Wii; U; ; 3642; en)"
+
+# Try to import from the app module if available
+try:
+ # Add parent directory to path to allow imports
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+ from app.utils.ua_generator import generate_ua_pool
+ USE_APP_MODULE = True
+except ImportError:
+ USE_APP_MODULE = False
+ # Self-contained version if app module is not available
+ import random
+
+ # Opera UA Pattern Templates
+ OPERA_PATTERNS = [
+ "Opera/9.80 (J2ME/MIDP; Opera Mini/{version}/{build}; U; {lang}) Presto/{presto} Version/{final}",
+ "Opera/9.80 (Android; Linux; Opera Mobi/{build}; U; {lang}) Presto/{presto} Version/{final}",
+ "Opera/9.80 (iPhone; Opera Mini/{version}/{build}; U; {lang}) Presto/{presto} Version/{final}",
+ "Opera/9.80 (iPad; Opera Mini/{version}/{build}; U; {lang}) Presto/{presto} Version/{final}",
+ "Opera/9.30 (Nintendo Wii; U; ; {code}; {lang})",
+ "Opera/9.80 (S60; SymbOS; Opera Mobi/{build}; U; {lang}) Presto/{presto} Version/{final}",
+ "Opera/9.80 (Series 60; Opera Mini/{version}/{build}; U; {lang}) Presto/{presto} Version/{final}",
+ "Opera/9.80 (BlackBerry; Opera Mini/{version}/{build}; U; {lang}) Presto/{presto} Version/{final}",
+ "Opera/9.80 (Windows Mobile; Opera Mini/{version}/{build}; U; {lang}) Presto/{presto} Version/{final}",
+ ]
+
+ OPERA_MINI_VERSIONS = [
+ "4.0", "4.1.11321", "4.2.13337", "4.2.14912", "4.2.15410", "4.3.24214",
+ "5.0.18741", "5.1.22296", "5.1.22783", "6.0.24095", "6.24093", "7.1.32444",
+ "7.6.35766", "36.2.2254"
+ ]
+
+ OPERA_MOBI_BUILDS = [
+ "27", "49", "447", "1209", "3730", "ADR-1012221546", "SYB-1107071606"
+ ]
+
+ BUILD_NUMBERS = [
+ "22.387", "22.478", "23.334", "23.377", "24.746", "24.783", "25.657",
+ "27.1407", "28.2647", "35.5706", "119.132", "870", "886"
+ ]
+
+ PRESTO_VERSIONS = [
+ "2.4.15", "2.4.18", "2.5.25", "2.8.119", "2.12.423"
+ ]
+
+ FINAL_VERSIONS = [
+ "10.00", "10.1", "10.54", "11.10", "12.16", "13.00"
+ ]
+
+ LANGUAGES = [
+ # English variants
+ "en", "en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-ZA", "en-IN", "en-SG",
+ # Western European
+ "de", "de-DE", "de-AT", "de-CH",
+ "fr", "fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU",
+ "es", "es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE", "es-LA",
+ "it", "it-IT", "it-CH",
+ "pt", "pt-PT", "pt-BR",
+ "nl", "nl-NL", "nl-BE",
+ # Nordic languages
+ "da", "da-DK",
+ "sv", "sv-SE",
+ "no", "no-NO", "nb", "nn",
+ "fi", "fi-FI",
+ "is", "is-IS",
+ # Eastern European
+ "pl", "pl-PL",
+ "cs", "cs-CZ",
+ "sk", "sk-SK",
+ "hu", "hu-HU",
+ "ro", "ro-RO",
+ "bg", "bg-BG",
+ "hr", "hr-HR",
+ "sr", "sr-RS",
+ "sl", "sl-SI",
+ "uk", "uk-UA",
+ "ru", "ru-RU",
+ # Asian languages
+ "zh", "zh-CN", "zh-TW", "zh-HK",
+ "ja", "ja-JP",
+ "ko", "ko-KR",
+ "th", "th-TH",
+ "vi", "vi-VN",
+ "id", "id-ID",
+ "ms", "ms-MY",
+ "fil", "tl",
+ # Middle Eastern
+ "tr", "tr-TR",
+ "ar", "ar-SA", "ar-AE", "ar-EG",
+ "he", "he-IL",
+ "fa", "fa-IR",
+ # Other
+ "hi", "hi-IN",
+ "bn", "bn-IN",
+ "ta", "ta-IN",
+ "te", "te-IN",
+ "mr", "mr-IN",
+ "el", "el-GR",
+ "ca", "ca-ES",
+ "eu", "eu-ES"
+ ]
+
+ WII_CODES = [
+ "1038-58", "1621", "2047-7", "2077-4", "3642"
+ ]
+
+ def generate_opera_ua():
+ """Generate a single random Opera User Agent string."""
+ pattern = random.choice(OPERA_PATTERNS)
+ params = {'lang': random.choice(LANGUAGES)}
+
+ if "Nintendo Wii" in pattern:
+ params['code'] = random.choice(WII_CODES)
+ else:
+ if '{version}' in pattern:
+ params['version'] = random.choice(OPERA_MINI_VERSIONS)
+ if '{build}' in pattern:
+ if "Opera Mobi" in pattern:
+ params['build'] = random.choice(OPERA_MOBI_BUILDS)
+ else:
+ params['build'] = random.choice(BUILD_NUMBERS)
+ if '{presto}' in pattern:
+ params['presto'] = random.choice(PRESTO_VERSIONS)
+ if '{final}' in pattern:
+ params['final'] = random.choice(FINAL_VERSIONS)
+
+ return pattern.format(**params)
+
+ def generate_ua_pool(count=10):
+ """Generate a pool of unique Opera User Agent strings."""
+ ua_pool = set()
+ max_attempts = count * 100
+ attempts = 0
+
+ try:
+ while len(ua_pool) < count and attempts < max_attempts:
+ ua = generate_opera_ua()
+ ua_pool.add(ua)
+ attempts += 1
+ except Exception:
+ # If generation fails entirely, return at least the default fallback
+ if not ua_pool:
+ return [DEFAULT_FALLBACK_UA]
+
+ # If we couldn't generate enough, fill remaining with default
+ result = list(ua_pool)
+ while len(result) < count:
+ result.append(DEFAULT_FALLBACK_UA)
+
+ return result
+
+
+def main():
+ """Main function to generate and display UA strings."""
+ # Parse command line argument
+ count = 10 # Default
+ if len(sys.argv) > 1:
+ try:
+ count = int(sys.argv[1])
+ if count < 1:
+ print("Error: Count must be a positive integer", file=sys.stderr)
+ sys.exit(1)
+ except ValueError:
+ print(f"Error: Invalid count '{sys.argv[1]}'. Must be an integer.", file=sys.stderr)
+ sys.exit(1)
+
+ # Show which mode we're using (to stderr so it doesn't interfere with output)
+ if USE_APP_MODULE:
+ print(f"# Using app.utils.ua_generator module", file=sys.stderr)
+ else:
+ print(f"# Using standalone generator (app module not available)", file=sys.stderr)
+
+ print(f"# Generating {count} Opera User Agent strings...\n", file=sys.stderr)
+
+ # Generate UAs
+ uas = generate_ua_pool(count)
+
+ # Display them (one per line, no numbering)
+ for ua in uas:
+ print(ua)
+
+ # Summary to stderr so it doesn't interfere with piping
+ print(f"\n# Generated {len(uas)} unique User Agent strings", file=sys.stderr)
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/test/conftest.py b/test/conftest.py
index cec3def..bd9017a 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,5 +1,8 @@
from app import app
+from app.request import Request
from app.utils.session import generate_key
+from test.mock_google import build_mock_response
+import httpx
import pytest
import random
@@ -13,6 +16,38 @@ demo_config = {
}
+@pytest.fixture(autouse=True)
+def mock_google(monkeypatch):
+ original_send = Request.send
+
+ def fake_send(self, base_url='', query='', attempt=0,
+ force_mobile=False, user_agent=''):
+ use_mock = not base_url or 'google.com/search' in base_url
+ if not use_mock:
+ return original_send(self, base_url, query, attempt,
+ force_mobile, user_agent)
+
+ html = build_mock_response(query, getattr(self, 'language', ''), getattr(self, 'country', ''))
+ request_url = (base_url or self.search_url) + query
+ request = httpx.Request('GET', request_url)
+ return httpx.Response(200, request=request, text=html)
+
+ def fake_autocomplete(self, q):
+ normalized = q.replace('+', ' ').lower()
+ suggestions = []
+ if 'green eggs and' in normalized:
+ suggestions.append('green eggs and ham')
+ if 'the cat in the' in normalized:
+ suggestions.append('the cat in the hat')
+ if normalized.startswith('who'):
+ suggestions.extend(['whoogle', 'whoogle search'])
+ return suggestions
+
+ monkeypatch.setattr(Request, 'send', fake_send)
+ monkeypatch.setattr(Request, 'autocomplete', fake_autocomplete)
+ yield
+
+
@pytest.fixture
def client():
with app.test_client() as client:
diff --git a/test/mock_google.py b/test/mock_google.py
new file mode 100644
index 0000000..36a5c71
--- /dev/null
+++ b/test/mock_google.py
@@ -0,0 +1,136 @@
+from urllib.parse import parse_qs, unquote, quote
+
+from app.models.config import Config
+
+DEFAULT_RESULTS = [
+ ('Example Domain', 'https://example.com/{slug}', 'Example information about {term}.'),
+ ('Whoogle Search', 'https://github.com/benbusby/whoogle-search', 'Private self-hosted Google proxy'),
+ ('Wikipedia', 'https://en.wikipedia.org/wiki/{title}', '{title} ā encyclopedia entry.'),
+]
+
+
+def _result_block(title, href, snippet):
+ encoded_href = quote(href, safe=':/')
+ return (
+ f'
'
+ )
+
+
+def _main_results(query, params, language='', country=''):
+ term = query.lower()
+ slug = query.replace(' ', '-')
+ results = []
+
+ pref_lang = ''
+ pref_country = ''
+ if 'preferences' in params:
+ try:
+ pref_data = Config(**{})._decode_preferences(params['preferences'][0])
+ pref_lang = str(pref_data.get('lang_interface', '') or '').lower()
+ pref_country = str(pref_data.get('country', '') or '').lower()
+ except Exception:
+ pref_lang = pref_country = ''
+ else:
+ pref_lang = pref_country = ''
+
+ if 'wikipedia' in term:
+ hl = str(params.get('hl', [''])[0] or '').lower()
+ gl = str(params.get('gl', [''])[0] or '').lower()
+ lr = str(params.get('lr', [''])[0] or '').lower()
+ language_code = str(language or '').lower()
+ country_code = str(country or '').lower()
+ is_japanese = (
+ hl.startswith('ja') or
+ gl.startswith('jp') or
+ lr.endswith('lang_ja') or
+ language_code.endswith('lang_ja') or
+ country_code.startswith('jp') or
+ pref_lang.endswith('lang_ja') or
+ pref_country.startswith('jp')
+ )
+ if is_japanese:
+ results.append((
+ 'ć¦ć£ćććć£ć¢',
+ 'https://ja.wikipedia.org/wiki/ć¦ć£ćććć£ć¢',
+ 'ę„ę¬čŖēć¦ć£ćććć£ć¢ć®čØäŗć§ćć'
+ ))
+ else:
+ results.append((
+ 'Wikipedia',
+ 'https://www.wikipedia.org/wiki/Wikipedia',
+ 'Wikipedia is a free online encyclopedia.'
+ ))
+
+ if 'pinterest' in term:
+ results.append((
+ 'Pinterest',
+ 'https://www.pinterest.com/ideas/',
+ 'Discover recipes, home ideas, style inspiration and other ideas.'
+ ))
+
+ if 'whoogle' in term:
+ results.append((
+ 'Whoogle Search GitHub',
+ 'https://github.com/benbusby/whoogle-search',
+ 'Source code for Whoogle Search.'
+ ))
+
+ if 'github' in term:
+ results.append((
+ 'GitHub',
+ f'https://github.com/search?q={slug}',
+ 'GitHub is a development platform to host and review code.'
+ ))
+
+ for title, url, snippet in DEFAULT_RESULTS:
+ formatted_url = url.format(slug=slug, term=term, title=title.replace(' ', '_'))
+ formatted_snippet = snippet.format(term=query, title=title)
+ results.append((title, formatted_url, formatted_snippet))
+
+ unique = []
+ seen = set()
+ for entry in results:
+ if entry[1] in seen:
+ continue
+ seen.add(entry[1])
+ unique.append(entry)
+
+ return ''.join(_result_block(*entry) for entry in unique)
+
+
+def build_mock_response(raw_query, language='', country=''):
+ if '&' in raw_query:
+ q_part, extra = raw_query.split('&', 1)
+ else:
+ q_part, extra = raw_query, ''
+
+ query = unquote(q_part)
+ params = parse_qs(extra)
+
+ results_html = _main_results(query, params, language, country)
+ safe_query = query.replace('"', '')
+ pagination = (
+ f'
Next'
+ f'
More'
+ )
+
+ return (
+ ''
+ '
Mock Google Results'
+ ''
+ f'
{results_html}
'
+ f'
'
+ f'
'
+ ''
+ ''
+ )