0, 'path' => '/', 'secure' => $isHttps, 'httponly' => true, 'samesite' => 'Lax']); session_start(); } unset($_sessionName); require_once BASE_PATH . '/src/helpers.php'; require_once BASE_PATH . '/src/auth.php'; require_once BASE_PATH . '/src/SiteSettings.php'; require_once BASE_PATH . '/src/ArticleManager.php'; require_once BASE_PATH . '/src/BookManager.php'; require_once BASE_PATH . '/src/DataGit.php'; $_dataGit = new DataGit(DATA_PATH); $articles = new ArticleManager(DATA_PATH, $_dataGit); $books = new BookManager(DATA_PATH . '/books', $_dataGit); // ─── Mode maintenance ────────────────────────────────────────────────────── if (file_exists(DATA_PATH . '/.maintenance')) { http_response_code(503); header('Retry-After: 60'); include BASE_PATH . '/templates/maintenance.php'; exit; } require_once BASE_PATH . '/src/UpdateChecker.php'; $_updateChecker = new UpdateChecker(DATA_PATH, BASE_PATH); $action = $_GET['action'] ?? 'list'; $uuid = $_GET['uuid'] ?? ''; $slug = $_GET['slug'] ?? ''; $_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links', 'react', 'comment', 'verify_comment', 'comment_moderate', 'comment_delete', 'comment_resend', 'create_tag_type', 'delete_tag_type', 'edit_tags', 'book_save', 'book_delete', 'admin_save_as_groups', 'admin_save_folio_config', 'run_engine_update', 'run_content_migrations', 'admin_delete_feed', 'rate', 'admin_save_ai_config']; $metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' : null; unset($_noindexActions); // ─── Recherche de l'article le plus proche et redirection 301 ──────────────── function log404(string $url): void { if (!defined('DATA_PATH')) { return; } $logDir = DATA_PATH . '/_logs'; $logFile = $logDir . '/not_found.jsonl'; if (!is_dir($logDir)) { @mkdir($logDir, 0755, true); } $entry = json_encode([ 'ts' => date('Y-m-d H:i:s'), 'url' => $url, 'ref' => $_SERVER['HTTP_REFERER'] ?? '', 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; @file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX); } function buildAutoSeoDesc(string $content, string $title = ''): string { require_once BASE_PATH . '/src/Parsedown.php'; $_pd = new Parsedown(); $_plain = trim((string)preg_replace('/\s+/', ' ', html_entity_decode(strip_tags($_pd->text($content)), ENT_QUOTES | ENT_HTML5, 'UTF-8') )); if ($title !== '' && stripos($_plain, $title) === 0) { $_plain = ltrim(substr($_plain, strlen($title))); } return mb_strimwidth($_plain, 0, 155, '…'); } function slugToSearchQuery(string $rawPath): string { return trim((string)preg_replace('/\s{2,}/', ' ', (string)preg_replace( '/[^a-zA-ZÀ-ÿ0-9\s]/u', ' ', str_replace(['-', '_', '/'], ' ', $rawPath) ))); } function searchAndRedirect(string $rawPath, ArticleManager $articles): void { require_once BASE_PATH . '/src/SearchEngine.php'; $query = slugToSearchQuery($rawPath); if ($query === '') { return; } $privateCats = $articles->getPrivateCategories(); $pool = array_values(array_filter( $articles->getAll(true), static function (array $a) use ($privateCats): bool { if (strtotime((string)($a['published_at'] ?? '')) > time()) { return false; } $cat = trim($a['category'] ?? ''); return $cat === '' || !in_array($cat, $privateCats, true); } )); $results = (new SearchEngine())->search($query, $pool); if (!empty($results)) { header('Location: /search?q=' . urlencode($query), true, 302); exit; } } // ─── Pages statiques depuis data/site/ ────────────────────────────────────── function loadSitePageData(string $slug): array { $base = DATA_PATH . '/site'; $meta = []; $raw = @file_get_contents($base . '/' . $slug . '.json'); if ($raw !== false) { $meta = json_decode($raw, true) ?? []; } $html = ''; $mdRaw = @file_get_contents($base . '/' . $slug . '.md'); if ($mdRaw !== false) { require_once BASE_PATH . '/src/Parsedown.php'; $html = (new Parsedown())->text($mdRaw); } return ['meta' => $meta, 'html' => $html]; } // ─── Extraction de métadonnées depuis une URL ──────────────────────────────── function fetchUrlMeta(string $url): array { if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $url)) { return ['ok' => false, 'error' => 'URL invalide']; } $tmpFile = tempnam(sys_get_temp_dir(), 'vl_meta_'); $fp = fopen($tmpFile, 'wb'); $downloaded = 0; $limit = 4 * 1024 * 1024; $contentLength = null; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_TIMEOUT => 20, CURLOPT_USERAGENT => 'Mozilla/5.0 varlog-meta/1.0', CURLOPT_SSL_VERIFYPEER => true, CURLOPT_ENCODING => '', // accepte gzip/deflate/br, décompresse automatiquement CURLOPT_HEADERFUNCTION => static function ($curl, $header) use (&$contentLength): int { if (preg_match('/^content-length:\s*(\d+)/i', $header, $m)) { $contentLength = (int) $m[1]; } return strlen($header); }, CURLOPT_WRITEFUNCTION => static function ($curl, $chunk) use ($fp, &$downloaded, $limit): int { $downloaded += strlen($chunk); if ($downloaded > $limit) { return -1; } fwrite($fp, $chunk); return strlen($chunk); }, ]); curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $mimeRaw = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE); $errno = curl_errno($ch); curl_close($ch); fclose($fp); // 403 = site accessible mais protégé (Cloudflare bot-check, etc.) : on laisse passer // avec métadonnées vides pour que l'utilisateur saisisse manuellement le titre. if ($httpCode === 403) { @unlink($tmpFile); return ['ok' => true, 'mime' => 'text/html', 'blocked' => true]; } if ($httpCode < 200 || $httpCode >= 400 || ($errno !== 0 && $errno !== 23)) { @unlink($tmpFile); return ['ok' => false, 'error' => "Téléchargement impossible (HTTP $httpCode)"]; } $mime = strtok($mimeRaw ?: 'application/octet-stream', '; '); $result = ['ok' => true, 'mime' => $mime, 'size' => $contentLength ?? $downloaded]; if (!str_starts_with($mime, 'text/html')) { $etJson = @shell_exec('exiftool -json -charset utf8 -struct ' . escapeshellarg($tmpFile) . ' 2>/dev/null'); if ($etJson) { $et = json_decode($etJson, true)[0] ?? []; $etVal = static function (string ...$keys) use ($et): ?string { foreach ($keys as $key) { $v = $et[$key] ?? null; if (is_array($v)) { $v = implode(', ', array_filter(array_map('trim', $v))); } if (is_string($v) && trim($v) !== '') { return trim($v); } } return null; }; if ($v = $etVal('Title', 'Headline', 'ObjectName')) { $result['title'] = $v; } if ($v = $etVal('Description', 'Caption-Abstract', 'ImageDescription')) { $result['description'] = $v; } if ($v = $etVal('Keywords')) { $result['keywords'] = $v; } if ($v = $etVal('Copyright', 'CopyrightNotice', 'Rights')) { $result['copyright'] = $v; } if ($v = $etVal('DateTimeOriginal', 'CreateDate', 'ModifyDate')) { $result['date'] = preg_replace('/^(\d{4}):(\d{2}):(\d{2})/', '$1-$2-$3', $v); } if (str_starts_with($mime, 'image/')) { if ($v = $etVal('Artist', 'Creator', 'By-line')) { $result['author'] = $v; } $w = $et['ImageWidth'] ?? $et['ExifImageWidth'] ?? null; $h = $et['ImageHeight'] ?? $et['ExifImageHeight'] ?? null; if ($w !== null && $h !== null) { $result['width'] = (int)$w; $result['height'] = (int)$h; } $camera = trim(($et['Make'] ?? '') . ' ' . ($et['Model'] ?? '')); if ($camera !== '') { $result['camera'] = $camera; } if ($v = $etVal('Credit')) { $result['credit'] = $v; } if ($v = $etVal('Source')) { $result['source'] = $v; } } if ($mime === 'application/pdf') { if ($v = $etVal('Author')) { $result['author'] = $v; } if ($v = $etVal('Subject')) { $result['subject'] = $v; } if ($v = $etVal('Creator', 'CreatorTool')) { $result['creator'] = $v; } if ($v = $etVal('Producer')) { $result['producer'] = $v; } if (isset($et['PageCount'])) { $result['pages'] = (int) $et['PageCount']; } if (isset($et['PDFVersion'])) { $result['pdf_version'] = 'PDF ' . $et['PDFVersion']; } $fhPdf = fopen($tmpFile, 'rb'); $pdfHead = fread($fhPdf, min(filesize($tmpFile), 65536)); fclose($fhPdf); if (preg_match('/\/MediaBox\s*\[\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\]/', $pdfHead, $mb)) { $wPt = (float)$mb[3] - (float)$mb[1]; $hPt = (float)$mb[4] - (float)$mb[2]; $wMm = (int) round($wPt * 25.4 / 72); $hMm = (int) round($hPt * 25.4 / 72); $landscape = $wMm > $hMm; if ($landscape) { [$wMm, $hMm] = [$hMm, $wMm]; } $paperSizes = ['A0' => [841,1189],'A1' => [594,841],'A2' => [420,594], 'A3' => [297,420],'A4' => [210,297],'A5' => [148,210], 'Letter' => [216,279],'Legal' => [216,356]]; $paperName = null; foreach ($paperSizes as $pname => [$pw, $ph]) { if (abs($wMm - $pw) <= 2 && abs($hMm - $ph) <= 2) { $paperName = $pname; break; } } $label = $paperName ? $paperName . ($landscape ? ' paysage' : '') : ($landscape ? 'Paysage' : 'Portrait'); $result['page_size'] = "$label ({$wMm}×{$hMm} mm)"; } } } } if (str_starts_with($mime, 'text/html')) { try { $fhHtml = fopen($tmpFile, 'rb'); $html = fread($fhHtml, min(filesize($tmpFile), 65536)); fclose($fhHtml); // Détection du charset : 1) en-tête HTTP, 2) , 3) $charset = null; if (preg_match('/charset=([^\s;]+)/i', $mimeRaw, $cm)) { $charset = trim($cm[1], '"\''); } if (!$charset && preg_match('/]+charset=["\']?\s*([^"\'\s;>]+)/i', $html, $cm)) { $charset = trim($cm[1]); } if (!$charset && preg_match('/]+content=["\'][^"\']*charset=([^"\'\s;]+)/i', $html, $cm)) { $charset = trim($cm[1]); } if ($charset && strtolower(str_replace(['-','_'], '', $charset)) !== 'utf8') { $converted = @mb_convert_encoding($html, 'UTF-8', $charset); if ($converted !== false && $converted !== '') { $html = $converted; } } preg_match('/
]*>(.*?)<\/head>/si', $html, $headMatch); $headHtml = $headMatch[1] ?? $html; if (preg_match('/