getenv('VITE_SUPABASE_URL') ?: getenv('SUPABASE_URL') ?: '', 'secret' => getenv('AI_BOT_LOG_SECRET') ?: getenv('CRON_SECRET') ?: '', ]; return $cfg; } function ai_bot_detect(string $ua): ?array { $patterns = [ ['chatgpt', 'GPTBot', '/GPTBot/i'], ['chatgpt', 'OAI-SearchBot', '/OAI-SearchBot/i'], ['chatgpt', 'ChatGPT-User', '/ChatGPT-User/i'], ['claude', 'ClaudeBot', '/ClaudeBot|Claude-Web|anthropic-ai|Claude-SearchBot/i'], ['gemini', 'Google-Extended', '/Google-Extended/i'], ['gemini', 'Gemini', '/\bGemini\b/i'], ['perplexity', 'PerplexityBot', '/PerplexityBot|Perplexity-User/i'], ['bytespider', 'Bytespider', '/Bytespider/i'], ['meta', 'Meta-ExternalAgent', '/meta-externalagent|FacebookBot|MetaAI/i'], ['amazon', 'Amazonbot', '/Amazonbot/i'], ['apple', 'Applebot-Extended', '/Applebot-Extended/i'], ['cohere', 'Cohere', '/cohere-ai|CohereBot/i'], ['you', 'YouBot', '/YouBot/i'], ['diffbot', 'Diffbot', '/Diffbot/i'], ['commoncrawl', 'CCBot', '/CCBot/i'], ['ai2', 'AI2Bot', '/AI2Bot/i'], ['googlebot', 'Googlebot', '/Googlebot/i'], ['bingbot', 'Bingbot', '/bingbot/i'], ]; foreach ($patterns as [$family, $name, $re]) { if (preg_match($re, $ua)) { return ['family' => $family, 'name' => $name]; } } return null; } function ai_bot_normalize_path(string $raw): string { $p = trim($raw) ?: '/'; if (str_starts_with($p, 'http://') || str_starts_with($p, 'https://')) { $parts = parse_url($p); $p = $parts['path'] ?? '/'; } $p = explode('?', $p, 2)[0]; $p = explode('#', $p, 2)[0]; if ($p === '' || $p[0] !== '/') { $p = '/' . ltrim($p, '/'); } $p = preg_replace('#/+#', '/', $p) ?: '/'; // Anti path traversal if (str_contains($p, '..')) { return '/'; } return substr($p, 0, 500); } function ai_bot_resolve_file(string $path): ?string { $root = rtrim(str_replace('\\', '/', __DIR__), '/'); $rel = trim($path, '/'); $candidates = []; if ($rel === '' || $rel === '/') { $candidates[] = $root . '/index.html'; } else { $candidates[] = $root . '/' . $rel; $candidates[] = $root . '/' . $rel . '/index.html'; if (!str_ends_with($rel, '.html')) { $candidates[] = $root . '/' . $rel . '.html'; } } foreach ($candidates as $file) { $real = realpath($file); if ($real && str_starts_with(str_replace('\\', '/', $real), $root) && is_file($real)) { return $real; } } // Fallback SPA uniquement pour les routes applicatives connues (comme le .htaccess). // Toute autre URL doit renvoyer un vrai 404 : sinon Google voit un doublon de // l'accueil (soft-404 / « Autre page avec balise canonique »). if (ai_bot_is_spa_route($path)) { $fallback = realpath($root . '/index.html'); return $fallback ?: null; } return null; } function ai_bot_is_spa_route(string $path): bool { $rel = trim($path, '/'); if ($rel === '') { return true; } if (preg_match('#^(app|assets|images|fonts|downloads|fcm-push)(/|$)#', $rel)) { return true; } if (preg_match('#^(connexion|login|fonctionnalites|features|tarifs|pricing|a-propos|blog|contact|faq|cgv|privacy|support|mentions|install|demo|reset-password|verify-reset-code|payment-success|conection|connection|partenaire-connexion|partenaire-mot-de-passe-oublie|quotes|devis|brouillons|brouillon|factures|missions|logiciel-vtc|logiciel-dispatch-vtc|logiciel-taxi-vtc|logiciel-taxi|dispatch-automatique-vtc|facturation-vtc|gestion-flotte-vtc|application-dispatch-vtc|mobilic-vtc|tva-vtc|bon-de-commande-vtc|registre-vtc|macaron-vtc|facture-vtc|tarif-vtc-au-km|voiture-vtc|developers|docs/api|accueil)$#', $rel)) { return true; } if (preg_match('#^(accept|accept-course|bienvenue-chauffeur|bienvenue-flotte|inscription-partenaire|inscription-chauffeur|track|event-track|rate-agency|reserver|hotel)(/|$)#', $rel)) { return true; } return false; } function ai_bot_log_async(array $payload, array $cfg): void { $url = rtrim((string) ($cfg['supabase_url'] ?? ''), '/'); $secret = (string) ($cfg['secret'] ?? ''); if ($url === '' || $secret === '') { return; } $endpoint = $url . '/functions/v1/log-ai-bot-hit'; $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false) { return; } if (function_exists('curl_init')) { $ch = curl_init($endpoint); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'x-ai-bot-secret: ' . $secret, ], CURLOPT_POSTFIELDS => $json, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT_MS => 800, CURLOPT_CONNECTTIMEOUT_MS => 400, ]); @curl_exec($ch); @curl_close($ch); return; } $ctx = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\nx-ai-bot-secret: {$secret}\r\n", 'content' => $json, 'timeout' => 1, 'ignore_errors' => true, ], ]); @file_get_contents($endpoint, false, $ctx); } $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; $bot = ai_bot_detect($ua); $path = ai_bot_normalize_path((string) ($_GET['path'] ?? $_SERVER['REQUEST_URI'] ?? '/')); $pixelOnly = isset($_GET['pixel']) && $_GET['pixel'] === '1'; $cfg = ai_bot_config(); if ($bot) { ai_bot_log_async([ 'path' => $path, 'userAgent' => $ua, 'referer' => $_SERVER['HTTP_REFERER'] ?? null, 'ip' => $_SERVER['HTTP_CF_CONNECTING_IP'] ?? (isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? trim(explode(',', (string) $_SERVER['HTTP_X_FORWARDED_FOR'])[0]) : ($_SERVER['REMOTE_ADDR'] ?? null)), 'source' => $pixelOnly ? 'pixel' : 'server', 'botFamily' => $bot['family'], 'botName' => $bot['name'], ], $cfg); } if ($pixelOnly) { header('Content-Type: image/gif'); header('Cache-Control: no-store, no-cache, must-revalidate'); // GIF 1x1 transparent echo base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); exit; } $file = ai_bot_resolve_file($path); if ($file === null && preg_match('#^/blog/([^/]+)/?$#', $path, $bm)) { // Article publié après le dernier build : page générée à la volée (404 si inconnu). $_GET['slug'] = $bm[1]; require __DIR__ . '/blog-page.php'; exit; } // Lien réservation partenaire /{booking_slug} : vrai 404 si slug inconnu (anti soft-404). if ($file === null && preg_match('#^/([a-z0-9][a-z0-9_-]{0,63})/?$#', $path, $pm)) { $_GET['slug'] = $pm[1]; require __DIR__ . '/partner-slug.php'; exit; } if ($file === null) { http_response_code(404); $notFound = __DIR__ . '/404.html'; if (is_file($notFound)) { header('Content-Type: text/html; charset=UTF-8'); readfile($notFound); } else { echo '404'; } exit; } $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); $types = [ 'html' => 'text/html; charset=UTF-8', 'htm' => 'text/html; charset=UTF-8', 'txt' => 'text/plain; charset=UTF-8', 'xml' => 'application/xml; charset=UTF-8', 'json' => 'application/json; charset=UTF-8', 'js' => 'application/javascript; charset=UTF-8', 'css' => 'text/css; charset=UTF-8', ]; header('Content-Type: ' . ($types[$ext] ?? 'application/octet-stream')); header('Cache-Control: no-store, no-cache, must-revalidate'); readfile($file);