PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" ]); } catch (\PDOException $e) { echo json_encode(["error" => "Erreur BDD : " . $e->getMessage()]); exit; } // ── Chiffrement ────────────────────────────────────────────────── function encryptData($data) { $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc')); $encrypted = openssl_encrypt($data, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv); return base64_encode($encrypted . '::' . $iv); } function decryptData($data) { if (!$data) return ''; list($encrypted_data, $iv) = explode('::', base64_decode($data), 2); return openssl_decrypt($encrypted_data, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv); } // ── Génération d'ID stable et compatible bigint (PHP 32 et 64 bits) ── // Produit un entier sur 9 chiffres (< 2^31) → safe partout // crc32() PHP retourne un int signé 32 bits → on force positif avec & 0x7FFFFFFF // puis on module pour rester dans une plage confortable sans collision sur ~50k films function makeStableId($title, $year) { $key = strtolower(trim($title)) . '|' . trim($year); $hash = abs(crc32($key)); // abs() = toujours positif // Reste dans [100000000, 2099999999] → 9-10 chiffres, bien < BIGINT MAX return ($hash % 2000000000) + 100000000; } // Pour les ajouts manuels : timestamp Unix en secondes (10 chiffres, safe) function makeNewId() { // On ajoute un suffixe aléatoire sur 3 chiffres pour éviter les doublons // si deux films sont ajoutés dans la même seconde return intval(time() . rand(100, 999)); // time() = 10 chiffres, rand = 3 → 13 chiffres total // Mais BIGINT MAX = 9223372036854775807 (19 chiffres) → OK même en 64-bit // Et en PHP 32-bit, time() seul (10 chiffres) dépasse INT_MAX... // Solution : utiliser directement crc32 d'un uuid } function makeNewIdSafe() { // Génère un ID unique sur 9-10 chiffres safe en 32 ET 64 bits $unique = uniqid('', true) . rand(1000, 9999); return (abs(crc32($unique)) % 2000000000) + 100000000; } // ── Conversion note Letterboxd (/5 avec demi-points) → entier 1-5 ── function convertRating($rawRating) { $raw = floatval($rawRating); if ($raw <= 0) return 3; // pas de note → neutre // Arrondi au plus proche, clampé entre 1 et 5 $rounded = (int) round($raw); return max(1, min(5, $rounded)); } // ── Authentification ───────────────────────────────────────────── function checkAuth($pdo) { $stmtCheck = $pdo->query("SELECT COUNT(*) as total FROM users"); if ($stmtCheck->fetch()['total'] == 0) return true; $token = ''; if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $token = $_SERVER['HTTP_AUTHORIZATION']; } elseif (function_exists('apache_request_headers')) { $headers = apache_request_headers(); if (isset($headers['Authorization'])) $token = $headers['Authorization']; } $expectedToken = md5(ENCRYPTION_KEY . 'session'); if ($token !== $expectedToken) { http_response_code(403); echo json_encode(["error" => "Accès interdit. Session expirée."]); exit; } } // ── Requête TMDB ───────────────────────────────────────────────── function fetchTmdb($url) { if (function_exists('curl_init')) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 8); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_USERAGENT, 'MonCinemaApp/1.0'); $response = curl_exec($ch); curl_close($ch); return $response; } $opts = [ "http" => ["method" => "GET", "header" => "User-Agent: MonCinemaApp/1.0\r\n", "timeout" => 8], "ssl" => ["verify_peer" => false, "verify_peer_name" => false] ]; return @file_get_contents($url, false, stream_context_create($opts)); } // ════════════════════════════════════════════════════════════════ $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; switch ($method) { // ════════════════════════════════════════════════════════════ case 'GET': if ($action === 'get_films') { $stmtC = $pdo->query("SELECT *, 'critique' AS type FROM critiques ORDER BY created_at DESC"); $critiques = $stmtC->fetchAll(); $stmtV = $pdo->query("SELECT *, 'videotheque' AS type FROM videotheque ORDER BY created_at DESC"); $videotheque = $stmtV->fetchAll(); echo json_encode(array_merge($critiques, $videotheque), JSON_UNESCAPED_UNICODE); } elseif ($action === 'get_tmdb_key_status') { checkAuth($pdo); $stmt = $pdo->prepare( "SELECT COUNT(*) as total FROM config WHERE key_name = 'tmdb_api_key' AND key_value IS NOT NULL AND key_value != ''" ); $stmt->execute(); echo json_encode(["exists" => ($stmt->fetch()['total'] > 0)]); } elseif ($action === 'search_tmdb') { checkAuth($pdo); $query = $_GET['query'] ?? ''; $stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'"); $stmt->execute(); $res = $stmt->fetch(); $key = $res ? decryptData($res['key_value']) : ''; if (!$key) { echo json_encode(["error" => "Clé TMDB non configurée."]); exit; } $url = "https://api.themoviedb.org/3/search/movie?api_key={$key}&query=" . urlencode($query) . "&language=fr-FR"; $response = fetchTmdb($url); echo $response ?: json_encode(["results" => []]); } elseif ($action === 'check_security_status') { $stmt = $pdo->query("SELECT COUNT(*) as total FROM users"); echo json_encode(["is_blank" => ($stmt->fetch()['total'] == 0)]); } break; // ════════════════════════════════════════════════════════════ case 'POST': // ── Import CSV Letterboxd ───────────────────────────── if ($action === 'import_csv') { checkAuth($pdo); if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) { http_response_code(400); echo json_encode(["error" => "Fichier CSV invalide ou manquant."]); exit; } // Récupérer la clé TMDB (optionnelle) $stmtK = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'"); $stmtK->execute(); $resK = $stmtK->fetch(); $tmdbKey = $resK ? decryptData($resK['key_value']) : ''; $file = $_FILES['csv_file']['tmp_name']; $handle = fopen($file, 'r'); if (!$handle) { http_response_code(500); echo json_encode(["error" => "Impossible d'ouvrir le fichier."]); exit; } // Gestion BOM UTF-8 (présent ou non) $bom = fread($handle, 3); if ($bom !== "\xEF\xBB\xBF") rewind($handle); // Lire l'en-tête $header = fgetcsv($handle, 0, ',', '"'); if (!$header) { fclose($handle); echo json_encode(["error" => "Fichier CSV vide."]); exit; } // Nettoyer les noms de colonnes (espaces, BOM résiduels) $header = array_map(function($col) { return trim(preg_replace('/[\x00-\x1F\x7F\xEF\xBB\xBF]/u', '', $col)); }, $header); $colName = array_search('Name', $header); $colYear = array_search('Year', $header); $colRating = array_search('Rating', $header); $colReview = array_search('Review', $header); // absent dans ratings.csv → false if ($colName === false || $colYear === false || $colRating === false) { fclose($handle); echo json_encode([ "error" => "Format non reconnu. Colonnes attendues : Name, Year, Rating. Colonnes trouvées : " . implode(', ', $header) ]); exit; } $importedCount = 0; $errors = []; while (($row = fgetcsv($handle, 0, ',', '"')) !== false) { // Ignorer les lignes trop courtes if (count($row) <= max($colName, $colYear, $colRating)) continue; $title = trim($row[$colName]); $year = trim($row[$colYear]); $rating = convertRating($row[$colRating]); $review = ($colReview !== false && isset($row[$colReview])) ? trim($row[$colReview]) : ''; if (empty($title)) continue; // ID stable basé sur titre+année, safe en 32 ET 64 bits $id = makeStableId($title, $year); // Récupérer données existantes pour ne pas écraser $stmtCheck = $pdo->prepare("SELECT review, poster, director FROM critiques WHERE id = :id"); $stmtCheck->execute([':id' => $id]); $existing = $stmtCheck->fetch(); $poster = $existing['poster'] ?? ''; $director = $existing['director'] ?? ''; // Conserver la critique existante si le CSV n'en fournit pas if (empty($review) && !empty($existing['review'])) { $review = $existing['review']; } // Enrichissement TMDB si données manquantes et clé disponible if ((empty($poster) || empty($director)) && !empty($tmdbKey)) { $apiUrl = "https://api.themoviedb.org/3/search/movie" . "?api_key={$tmdbKey}" . "&query=" . urlencode($title) . ($year ? "&year={$year}" : '') . "&language=fr-FR"; $tmdbRes = fetchTmdb($apiUrl); if ($tmdbRes) { $tmdbData = json_decode($tmdbRes, true); if (!empty($tmdbData['results'][0])) { $movieData = $tmdbData['results'][0]; if (empty($poster) && !empty($movieData['poster_path'])) { $poster = "https://image.tmdb.org/t/p/w300" . $movieData['poster_path']; } if (empty($director) && !empty($movieData['id'])) { $creditsRes = fetchTmdb("https://api.themoviedb.org/3/movie/{$movieData['id']}/credits?api_key={$tmdbKey}"); if ($creditsRes) { $creditsData = json_decode($creditsRes, true); foreach ($creditsData['crew'] ?? [] as $member) { if ($member['job'] === 'Director') { $director = $member['name']; break; } } } } } } } try { $stmt = $pdo->prepare( "REPLACE INTO critiques (id, title, year, director, rating, review, poster, streaming) VALUES (:id, :title, :year, :director, :rating, :review, :poster, '')" ); $stmt->execute([ ':id' => $id, ':title' => $title, ':year' => $year, ':director' => $director, ':rating' => $rating, ':review' => $review, ':poster' => $poster, ]); $importedCount++; } catch (\PDOException $e) { $errors[] = "'{$title}' : " . $e->getMessage(); } } fclose($handle); $result = ["success" => true, "imported" => $importedCount]; if (!empty($errors)) $result['warnings'] = array_slice($errors, 0, 10); echo json_encode($result, JSON_UNESCAPED_UNICODE); exit; } // ── Lire le body JSON ───────────────────────────────── $data = json_decode(file_get_contents('php://input'), true) ?? []; // ── Connexion ───────────────────────────────────────── if ($action === 'login') { $stmtCheck = $pdo->query("SELECT COUNT(*) as total FROM users"); if ($stmtCheck->fetch()['total'] == 0) { echo json_encode([ "success" => true, "token" => md5(ENCRYPTION_KEY . 'session'), "blank" => true ]); exit; } $stmt = $pdo->prepare("SELECT password_hash FROM users WHERE username = 'admin'"); $stmt->execute(); $userRow = $stmt->fetch(); if ($userRow && password_verify($data['password'] ?? '', $userRow['password_hash'])) { echo json_encode([ "success" => true, "token" => md5(ENCRYPTION_KEY . 'session'), "blank" => false ]); } else { http_response_code(401); echo json_encode(["error" => "Mot de passe incorrect."]); } } // ── Sauvegarde film ─────────────────────────────────── elseif ($action === 'save_film') { checkAuth($pdo); $type = $data['type'] ?? 'critique'; $id = (!empty($data['id'])) ? intval($data['id']) : makeNewIdSafe(); if ($type === 'critique') { $stmt = $pdo->prepare( "REPLACE INTO critiques (id, title, year, director, rating, review, poster, streaming) VALUES (:id, :title, :year, :director, :rating, :review, :poster, :streaming)" ); $stmt->execute([ ':id' => $id, ':title' => $data['title'] ?? '', ':year' => $data['year'] ?? '', ':director' => $data['director'] ?? '', ':rating' => max(1, min(5, intval($data['rating'] ?? 3))), ':review' => $data['review'] ?? '', ':poster' => $data['poster'] ?? '', ':streaming' => $data['streaming'] ?? '', ]); } else { $stmt = $pdo->prepare( "REPLACE INTO videotheque (id, title, year, director, poster, format, length, publisher, ean_isbn13, number_of_discs, aspect_ratio, description) VALUES (:id, :title, :year, :director, :poster, :format, :length, :publisher, :ean_isbn13, :number_of_discs, :aspect_ratio, :description)" ); $stmt->execute([ ':id' => $id, ':title' => $data['title'] ?? '', ':year' => $data['year'] ?? '', ':director' => $data['director'] ?? '', ':poster' => $data['poster'] ?? '', ':format' => $data['format'] ?? '', ':length' => !empty($data['length']) ? intval($data['length']) : null, ':publisher' => $data['publisher'] ?? '', ':ean_isbn13' => $data['ean_isbn13'] ?? '', ':number_of_discs' => !empty($data['number_of_discs']) ? intval($data['number_of_discs']) : 1, ':aspect_ratio' => $data['aspect_ratio'] ?? '', ':description' => $data['description'] ?? '', ]); } echo json_encode(["success" => true]); } // ── Sauvegarde clé TMDB ─────────────────────────────── elseif ($action === 'save_tmdb_key') { checkAuth($pdo); $stmt = $pdo->prepare( "REPLACE INTO config (key_name, key_value) VALUES ('tmdb_api_key', :val)" ); $stmt->execute([':val' => encryptData($data['tmdb_key'] ?? '')]); echo json_encode(["success" => true]); } // ── Init compte admin ───────────────────────────────── elseif ($action === 'setup_admin') { $pwd = $data['password'] ?? ''; if (strlen($pwd) < 4) { http_response_code(400); echo json_encode(["error" => "Mot de passe trop court (min. 4 caractères)."]); exit; } $stmt = $pdo->prepare( "REPLACE INTO users (id, username, password_hash) VALUES (1, 'admin', :pass)" ); $stmt->execute([':pass' => password_hash($pwd, PASSWORD_BCRYPT)]); echo json_encode(["success" => "Compte admin initialisé."]); } // ── Changement mot de passe ─────────────────────────── elseif ($action === 'update_password') { checkAuth($pdo); $newPwd = $data['new_password'] ?? ''; if (strlen($newPwd) < 4) { http_response_code(400); echo json_encode(["error" => "Mot de passe trop court (min. 4 caractères)."]); exit; } $stmt = $pdo->prepare( "REPLACE INTO users (id, username, password_hash) VALUES (1, 'admin', :pass)" ); $stmt->execute([':pass' => password_hash($newPwd, PASSWORD_BCRYPT)]); echo json_encode(["success" => "Mot de passe mis à jour."]); } break; // ════════════════════════════════════════════════════════════ case 'DELETE': if ($action === 'delete_film') { checkAuth($pdo); $id = $_GET['id'] ?? 0; $type = $_GET['type'] ?? 'critique'; $table = ($type === 'videotheque') ? 'videotheque' : 'critiques'; $stmt = $pdo->prepare("DELETE FROM {$table} WHERE id = :id"); $stmt->execute([':id' => $id]); echo json_encode(["success" => true]); } elseif ($action === 'delete_multiple_films') { checkAuth($pdo); $input = json_decode(file_get_contents('php://input'), true) ?? []; $ids = $input['ids'] ?? []; $type = $_GET['type'] ?? 'critique'; $table = ($type === 'videotheque') ? 'videotheque' : 'critiques'; if (empty($ids) || !is_array($ids)) { http_response_code(400); echo json_encode(["error" => "Aucun identifiant fourni."]); exit; } $ids = array_map('intval', $ids); $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("DELETE FROM {$table} WHERE id IN ({$placeholders})"); $stmt->execute($ids); echo json_encode(["success" => true, "deleted_count" => $stmt->rowCount()]); } break; }