diff --git a/index.php b/index.php index 5b91226..e4c420a 100644 --- a/index.php +++ b/index.php @@ -1,253 +1,291 @@ 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)]; +// Mise à jour du cache si trop vieux ou absent +if (cache_expired()) { + if (!download_instances_json() || !extract_valid_urls()) { + error_log('Erreur lors de la génération du cache'); + http_response_code(500); + exit('Erreur interne'); } } -class BangManager +// Récupérer la requête +$query = $_GET['q'] ?? null; + +if ($query !== null) { + if (!is_valid_query($query)) { + http_response_code(400); + exit('Paramètre q invalide'); + } + + // Gestion bang : si retourne true, redirection faite et script termine + if (!try_redirect_bang($query)) { + redirect_search($query); + } +} else { + redirect_random_url(); +} + +// — Fonctions — // + +function cache_expired(): bool { - private array $bangs = []; - - public function __construct(private string $file = BANG_FILE) { - $this->loadBangs(); + if (!file_exists(URLS_TXT)) { + return true; } + $age = time() - filemtime(URLS_TXT); + return $age > CACHE_MAX_AGE; +} - 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 : []; +function download_instances_json(): bool +{ + $ch = curl_init(INSTANCES_URL); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_SSL_VERIFYPEER => true, + ]); + $data = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($data === false) { + error_log('curl_exec failed: ' . curl_error($ch)); } + curl_close($ch); - public function tryRedirect(string $query): bool { - if (!str_starts_with($query, '!')) return false; - if (!preg_match('/^!(\w+)\s?(.*)$/u', $query, $matches)) return false; + if ($code === 200 && $data !== false) { + return safe_file_put_contents(INSTANCES_JSON, $data); + } + error_log("Erreur téléchargement instances.json, HTTP $code"); + 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; - } - } +function extract_valid_urls(): bool +{ + if (!file_exists(INSTANCES_JSON)) { + error_log("instances.json introuvable"); return false; } + + $content = file_get_contents(INSTANCES_JSON); + if ($content === false) { + error_log("Impossible de lire instances.json"); + return false; + } + + $j = json_decode($content, true); + if (!isset($j['instances']) || !is_array($j['instances'])) { + error_log("JSON mal formé"); + return false; + } + + $valid = []; + foreach ($j['instances'] as $url => $data) { + if ( + ($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) + ) { + $valid[] = rtrim($url, '/'); + } + } + + if (empty($valid)) { + error_log("Aucune URL valide"); + return false; + } + + return safe_file_put_contents(URLS_TXT, implode("\n", $valid)); } -class EngineManager +function safe_file_put_contents(string $filename, string $data): bool { - 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; - } + $fp = fopen($filename, 'c'); + if (!$fp) { + error_log("Impossible d'ouvrir $filename en écriture"); + return false; } - - 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))); + if (!flock($fp, LOCK_EX)) { + fclose($fp); + error_log("Impossible de verrouiller $filename"); + return false; } + ftruncate($fp, 0); + $written = fwrite($fp, $data); + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); - 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)]; - } + return $written === strlen($data); } -class SearxHandler +function get_all_urls(): array { - public function __construct(private CacheManager $cache, private BangManager $bang) {} + if (!file_exists(URLS_TXT)) { + return []; + } - public function handle(): void - { - $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; - $query = $_REQUEST['q'] ?? null; + $fp = fopen(URLS_TXT, 'r'); + if (!$fp) { + error_log("Impossible d'ouvrir urls.txt"); + return []; + } - if ($query !== null && !$this->bang->tryRedirect($query)) { - $instance = rtrim($this->cache->getRandomUrl(), '/'); - $url = $instance . '/search?' . http_build_query(['q' => $query]); + if (!flock($fp, LOCK_SH)) { + fclose($fp); + error_log("Impossible de verrouiller urls.txt en lecture"); + return []; + } - 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'); - } + $urls = []; + while (($line = fgets($fp)) !== false) { + $line = trim($line); + if ($line !== '' && filter_var($line, FILTER_VALIDATE_URL)) { + $urls[] = $line; } } + flock($fp, LOCK_UN); + fclose($fp); + + return $urls; } -class EngineHandler +function redirect_random_url(): void { - 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'); - } - } + $urls = get_all_urls(); + if (empty($urls)) { + http_response_code(500); + exit('Instances indisponibles'); } + $target = $urls[array_rand($urls)]; + header("Location: $target"); + exit; } -// -------------------- EXECUTION -------------------- -try { - $bang = new BangManager(); - $engineManager = new EngineManager(); +function redirect_search(string $query): void +{ + $urls = get_all_urls(); + if (empty($urls)) { + http_response_code(500); + exit('Instances indisponibles pour recherche'); + } + $base = $urls[array_rand($urls)]; - 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); + $parsed = parse_url($base); + if ($parsed === false) { + error_log("Base URL invalide dans urls.txt"); + http_response_code(500); + exit('Erreur interne'); } - $handler->handle(); + $path = $parsed['path'] ?? ''; + $path = rtrim($path, '/') . '/search'; -} 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.'); -} \ No newline at end of file + $query_string = http_build_query(['q' => $query]); + + $target = $parsed['scheme'] . '://' . $parsed['host']; + if (isset($parsed['port'])) { + $target .= ':' . $parsed['port']; + } + $target .= $path . '?' . $query_string; + + header("Location: $target"); + exit; +} + +function is_valid_query(string $query): bool +{ + $query = trim($query); + if ($query === '') { + return false; + } + if (mb_strlen($query) > 200) { + return false; + } + + // Vérifier que la chaîne est en UTF-8 valide + if (!mb_check_encoding($query, 'UTF-8')) { + return false; + } + + return preg_match('/^[^\x00-\x1F\x7F]+$/u', $query) === 1; +} + +// --- Gestion bangs --- // + +function load_bangs(): array +{ + $file = __DIR__ . '/bang.json'; + if (!file_exists($file)) { + error_log("bang.json introuvable"); + return []; + } + $content = file_get_contents($file); + if ($content === false) { + error_log("Impossible de lire bang.json"); + return []; + } + $bangs = json_decode($content, true); + if (!is_array($bangs)) { + error_log("bang.json mal formé"); + return []; + } + return $bangs; +} + +function try_redirect_bang(string $query): bool +{ + if (substr($query, 0, 1) !== '!') { + return false; + } + + if (!preg_match('/^!(\w+)\s?(.*)$/u', $query, $matches)) { + return false; + } + $bang_code = $matches[1]; + $search_term = trim($matches[2]); + + if ($search_term === '') { + return false; + } + + $bangs = load_bangs(); + if (empty($bangs)) { + return false; + } + + foreach ($bangs as $bang) { + if (($bang['bang'] ?? '') === $bang_code) { + $base_url = rtrim($bang['url'], '/'); + $search_url = $base_url . '?q=' . rawurlencode($search_term); + header("Location: $search_url"); + exit; + } + } + return false; +} diff --git a/url.txt b/url.txt deleted file mode 100644 index 31b7970..0000000 --- a/url.txt +++ /dev/null @@ -1,8 +0,0 @@ -https://marginalia-search.com/search?query=%s -https://swisscows.com/fr/web?query=%s -https://www.qwant.com/?q=%s -https://search.brave.com/search?q=%s -https://www.ecosia.org/search?q=%s -https://www.mojeek.com/search?q=%s -https://duckduckgo.com/?q=%s -https://www.startpage.com/do/search?q=%s \ No newline at end of file