php oriented objet

This commit is contained in:
cedric 2025-12-09 20:27:03 +00:00
parent ae35a21e31
commit 92ac79b4c6

401
index.php
View file

@ -1,291 +1,190 @@
<?php <?php
// index.php declare(strict_types=1);
// Gère génération du cache, redirection classique et gestion des bangs (!code)
// Constantes // -------------------- CONSTANTES --------------------
define('CACHE_DIR', __DIR__ . '/cache'); const CACHE_DIR = __DIR__ . '/cache';
define('INSTANCES_JSON', CACHE_DIR . '/instances.json'); const INSTANCES_JSON = CACHE_DIR . '/instances.json';
define('URLS_TXT', CACHE_DIR . '/urls.txt'); const URLS_TXT = CACHE_DIR . '/urls.txt';
define('INSTANCES_URL', 'https://searx.space/data/instances.json'); const INSTANCES_URL = 'https://searx.space/data/instances.json';
define('CACHE_MAX_AGE', 3600); // 1 heure const CACHE_MAX_AGE = 3600; // 1 heure
const BANG_FILE = __DIR__ . '/bang.json';
// Headers de sécurité // -------------------- HEADERS DE SÉCURITÉ --------------------
header('X-Frame-Options: SAMEORIGIN'); header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff'); header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer-when-downgrade'); header('Referrer-Policy: no-referrer-when-downgrade');
header("Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; sandbox"); header("Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; sandbox");
// Création dossier cache si nécessaire // -------------------- CLASSES --------------------
if (!is_dir(CACHE_DIR) && !mkdir(CACHE_DIR, 0755, true)) { class CacheManager
error_log('Erreur création dossier cache');
http_response_code(500);
exit('Erreur interne');
}
// 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');
}
}
// 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
{ {
if (!file_exists(URLS_TXT)) { private array $urlsCache = [];
return true;
}
$age = time() - filemtime(URLS_TXT);
return $age > CACHE_MAX_AGE;
}
function download_instances_json(): bool public function __construct(private string $cacheDir = CACHE_DIR) {
{ if (!is_dir($cacheDir) && !mkdir($cacheDir, 0755, true)) {
$ch = curl_init(INSTANCES_URL); throw new RuntimeException("Impossible de créer le dossier cache");
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);
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;
}
function extract_valid_urls(): bool
{
if (!file_exists(INSTANCES_JSON)) {
error_log("instances.json introuvable");
return false;
} }
$content = file_get_contents(INSTANCES_JSON); public function isExpired(): bool {
if ($content === false) { return !file_exists(URLS_TXT) || (time() - filemtime(URLS_TXT)) > CACHE_MAX_AGE;
error_log("Impossible de lire instances.json");
return false;
} }
$j = json_decode($content, true); public function downloadInstances(): bool {
if (!isset($j['instances']) || !is_array($j['instances'])) { $data = file_get_contents(INSTANCES_URL);
error_log("JSON mal formé"); if ($data === false) return false;
return false; return $this->safeWrite(INSTANCES_JSON, $data);
} }
$valid = []; public function extractValidUrls(): bool {
foreach ($j['instances'] as $url => $data) { if (!file_exists(INSTANCES_JSON)) return false;
if (
($data['network_type'] ?? '') === 'normal' $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['http']['status_code'] ?? 0) === 200
&& ($data['timing']['search']['success_percentage'] ?? 0) === 100.0 && ($data['timing']['search']['success_percentage'] ?? 0) === 100.0
&& ($data['timing']['initial']['success_percentage'] ?? 0) === 100.0 && ($data['timing']['initial']['success_percentage'] ?? 0) === 100.0
&& filter_var($url, FILTER_VALIDATE_URL) && filter_var($url, FILTER_VALIDATE_URL)
&& in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true) && in_array(parse_url($url, PHP_URL_SCHEME), ['http','https'], true);
) { });
$valid[] = rtrim($url, '/');
} if (empty($valid)) return false;
return $this->safeWrite(URLS_TXT, implode("\n", array_map('rtrim', $valid)));
} }
if (empty($valid)) { private function safeWrite(string $file, string $data): bool {
error_log("Aucune URL valide"); $written = file_put_contents($file, $data, LOCK_EX);
return false; return $written !== false && $written === strlen($data);
} }
return safe_file_put_contents(URLS_TXT, implode("\n", $valid)); 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)];
}
} }
function safe_file_put_contents(string $filename, string $data): bool class BangManager
{ {
$fp = fopen($filename, 'c'); private array $bangs = [];
if (!$fp) {
error_log("Impossible d'ouvrir $filename en écriture");
return false;
}
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);
return $written === strlen($data); public function __construct(private string $file = BANG_FILE) {
} $this->loadBangs();
function get_all_urls(): array
{
if (!file_exists(URLS_TXT)) {
return [];
} }
$fp = fopen(URLS_TXT, 'r'); private function loadBangs(): void {
if (!$fp) { if (!file_exists($this->file)) return;
error_log("Impossible d'ouvrir urls.txt"); $data = json_decode(file_get_contents($this->file), true);
return []; $this->bangs = is_array($data) ? $data : [];
} }
if (!flock($fp, LOCK_SH)) { public function tryRedirect(string $query): bool {
fclose($fp); if (!str_starts_with($query, '!')) return false;
error_log("Impossible de verrouiller urls.txt en lecture"); if (!preg_match('/^!(\w+)\s?(.*)$/u', $query, $matches)) return false;
return [];
}
$urls = []; [$bangCode, $searchTerm] = [$matches[1], trim($matches[2])];
while (($line = fgets($fp)) !== false) { if ($searchTerm === '') return false;
$line = trim($line);
if ($line !== '' && filter_var($line, FILTER_VALIDATE_URL)) {
$urls[] = $line;
}
}
flock($fp, LOCK_UN);
fclose($fp);
return $urls; foreach ($this->bangs as $bang) {
} if (($bang['bang'] ?? '') === $bangCode) {
header("Location: " . rtrim($bang['url'], '/') . '?q=' . rawurlencode($searchTerm));
function redirect_random_url(): void
{
$urls = get_all_urls();
if (empty($urls)) {
http_response_code(500);
exit('Instances indisponibles');
}
$target = $urls[array_rand($urls)];
header("Location: $target");
exit;
}
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)];
$parsed = parse_url($base);
if ($parsed === false) {
error_log("Base URL invalide dans urls.txt");
http_response_code(500);
exit('Erreur interne');
}
$path = $parsed['path'] ?? '';
$path = rtrim($path, '/') . '/search';
$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; exit;
} }
} }
return false; return false;
}
}
class RequestHandler
{
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(), '/');
if ($method === 'GET') {
$this->redirectGet($instance, $query);
} elseif ($method === 'POST') {
$this->proxyPost($instance, $_POST);
} 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') {
$this->proxyPost($instance, $_POST);
} else {
http_response_code(405);
exit('Méthode non autorisée');
}
}
}
private function redirectGet(string $instance, string $query): void {
$url = $instance . '/search?' . http_build_query(['q' => $query]);
header("Location: $url");
exit;
}
private function proxyPost(string $urlBase, array $postData): void {
$url = $urlBase . '/search';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($postData),
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
http_response_code($code);
echo $response;
exit;
}
}
// -------------------- EXECUTION --------------------
try {
$cache = new CacheManager();
if ($cache->isExpired()) {
if (!$cache->downloadInstances() || !$cache->extractValidUrls()) {
http_response_code(500);
exit('Erreur interne : impossible de générer le cache.');
}
}
$bang = new BangManager();
$handler = new RequestHandler($cache, $bang);
$handler->handle();
} catch (\Throwable $e) {
http_response_code(500);
exit('Erreur interne : ' . $e->getMessage());
} }