CACHE_MAX_AGE; } public function downloadInstances(): bool { $data = file_get_contents(INSTANCES_URL); if ($data === false) return false; return $this->safeWrite(INSTANCES_JSON, $data); } public function extractValidUrls(): bool { if (!file_exists(INSTANCES_JSON)) return false; $json = json_decode(file_get_contents(INSTANCES_JSON), true); if (!isset($json['instances']) || !is_array($json['instances'])) return false; $valid = array_filter(array_keys($json['instances']), function($url) use ($json) { $data = $json['instances'][$url] ?? []; return ($data['network_type'] ?? '') === 'normal' && ($data['http']['status_code'] ?? 0) === 200 && ($data['timing']['search']['success_percentage'] ?? 0) === 100.0 && ($data['timing']['initial']['success_percentage'] ?? 0) === 100.0 && filter_var($url, FILTER_VALIDATE_URL) && in_array(parse_url($url, PHP_URL_SCHEME), ['http','https'], true); }); if (empty($valid)) return false; return $this->safeWrite(URLS_TXT, implode("\n", array_map('rtrim', $valid))); } private function safeWrite(string $file, string $data): bool { $written = file_put_contents($file, $data, LOCK_EX); return $written !== false && $written === strlen($data); } public function loadUrls(): array { if (empty($this->urlsCache)) { if (!file_exists(URLS_TXT)) return []; $urls = file(URLS_TXT, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $this->urlsCache = array_filter($urls, fn($url) => filter_var($url, FILTER_VALIDATE_URL)); } return $this->urlsCache; } public function getRandomUrl(): string { $urls = $this->loadUrls(); if (empty($urls)) throw new RuntimeException("Aucune URL disponible"); return $urls[array_rand($urls)]; } } class BangManager { private array $bangs = []; public function __construct(private string $file = BANG_FILE) { $this->loadBangs(); } private function loadBangs(): void { if (!file_exists($this->file)) return; $data = json_decode(file_get_contents($this->file), true); $this->bangs = is_array($data) ? $data : []; } public function tryRedirect(string $query): bool { if (!str_starts_with($query, '!')) return false; if (!preg_match('/^!(\w+)\s?(.*)$/u', $query, $matches)) return false; [$bangCode, $searchTerm] = [$matches[1], trim($matches[2])]; if ($searchTerm === '') return false; foreach ($this->bangs as $bang) { if (($bang['bang'] ?? '') === $bangCode) { header("Location: " . rtrim($bang['url'], '/') . '?q=' . rawurlencode($searchTerm)); exit; } } return false; } } class EngineManager { private static ?array $cachedEngines = null; private array $engines = []; public function __construct(private string $file = __DIR__ . '/engines.txt') { if (self::$cachedEngines === null) { $this->loadEngines(); self::$cachedEngines = $this->engines; } else { $this->engines = self::$cachedEngines; } } private function loadEngines(): void { if (!file_exists($this->file)) { throw new RuntimeException("Fichier engines.txt introuvable"); } $lines = file($this->file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $this->engines = array_filter($lines, fn($line) => !empty(trim($line))); } public function getRandomEngine(): string { if (empty($this->engines)) { throw new RuntimeException("Aucun moteur de recherche configuré dans engines.txt"); } return $this->engines[array_rand($this->engines)]; } } class SearxHandler { public function __construct(private CacheManager $cache, private BangManager $bang) {} public function handle(): void { $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $query = $_REQUEST['q'] ?? null; if ($query !== null && !$this->bang->tryRedirect($query)) { $instance = rtrim($this->cache->getRandomUrl(), '/'); $url = $instance . '/search?' . http_build_query(['q' => $query]); if ($method === 'GET') { header("Location: $url"); exit; } elseif ($method === 'POST') { header("Location: $url"); // Redirige en GET pour simplifier exit; } else { http_response_code(405); exit('Méthode non autorisée'); } } elseif ($query === null) { $instance = rtrim($this->cache->getRandomUrl(), '/'); if ($method === 'GET') { header("Location: $instance"); exit; } elseif ($method === 'POST') { header("Location: $instance"); exit; } else { http_response_code(405); exit('Méthode non autorisée'); } } } } class EngineHandler { public function __construct(private EngineManager $engineManager, private BangManager $bang) {} public function handle(): void { $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $query = $_REQUEST['q'] ?? null; if ($query !== null && !$this->bang->tryRedirect($query)) { $engineTemplate = $this->engineManager->getRandomEngine(); $url = sprintf($engineTemplate, rawurlencode($query)); if ($method === 'GET') { header("Location: $url"); exit; } elseif ($method === 'POST') { header("Location: $url"); // Redirige en GET exit; } else { http_response_code(405); exit('Méthode non autorisée'); } } elseif ($query === null) { // Rediriger vers la page d'accueil du premier moteur $engineTemplate = $this->engineManager->getRandomEngine(); $homepage = str_replace('?query=%s', '', str_replace('?q=%s', '', $engineTemplate)); if ($method === 'GET') { header("Location: $homepage"); exit; } elseif ($method === 'POST') { header("Location: $homepage"); exit; } else { http_response_code(405); exit('Méthode non autorisée'); } } } } // -------------------- EXECUTION -------------------- try { $bang = new BangManager(); $engineManager = new EngineManager(); if (USE_SEARX_INSTANCES) { $cache = new CacheManager(); if ($cache->isExpired()) { if (!$cache->downloadInstances() || !$cache->extractValidUrls()) { http_response_code(500); exit('Erreur interne : veuillez réessayer plus tard.'); } } $handler = new SearxHandler($cache, $bang); } else { $handler = new EngineHandler($engineManager, $bang); } $handler->handle(); } catch (\Throwable $e) { // Ne pas exposer les détails techniques → Point 3 error_log('Erreur interne : ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString()); http_response_code(500); exit('Erreur interne : veuillez réessayer plus tard.'); }