dbPath = $this->determineDatabasePath($dbPath); try { $this->pdo = new PDO('sqlite:' . $this->dbPath); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // Enable foreign key constraints $this->pdo->exec('PRAGMA foreign_keys = ON'); } catch (PDOException $e) { error_log("Database connection failed: " . $e->getMessage()); throw new Exception("Impossible de se connecter à la base de données."); } } /** * Determine database path * Uses centralized config from config.php * Priority: custom path > config.php settings */ private function determineDatabasePath($customPath = null) { // Allow explicit override if ($customPath !== null && file_exists($customPath)) { return $customPath; } // Use centralized configuration return getDatabasePath(); } /** * Get singleton instance (for front-backend) * @return Database */ public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Get PDO connection * @return PDO */ public function getConnection() { return $this->pdo; } /** * Get PDO instance (alias for formulaire compatibility) * @return PDO */ public function getPDO() { return $this->pdo; } // ======================================================================== // TRANSACTION SUPPORT (from formulaire) // ======================================================================== /** * Begin a transaction */ public function beginTransaction() { return $this->pdo->beginTransaction(); } /** * Commit a transaction */ public function commit() { return $this->pdo->commit(); } /** * Rollback a transaction */ public function rollback() { return $this->pdo->rollback(); } // ======================================================================== // PUBLIC SITE METHODS (from front-backend) // ======================================================================== /** * Get all published theses with pagination */ public function getPublishedTheses($limit = 10, $offset = 0) { $sql = "SELECT * FROM v_theses_public ORDER BY year DESC, title ASC LIMIT :limit OFFSET :offset"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } /** * Count all published theses */ public function countPublishedTheses() { $sql = "SELECT COUNT(*) as count FROM theses WHERE is_published = 1"; $stmt = $this->pdo->query($sql); $result = $stmt->fetch(); return $result['count']; } /** * Get thesis by ID with all related data (for public site) */ public function getThesisById($id) { $sql = "SELECT * FROM v_theses_full WHERE id = :id AND is_published = 1"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $thesis = $stmt->fetch(); if (!$thesis) { return null; } // Get associated files $thesis['files'] = $this->getThesisFiles($id); return $thesis; } /** * Get thesis by ID (for admin - includes unpublished) * @param int $id Thesis ID * @return array|null Thesis data or null if not found */ public function getThesis($id) { $stmt = $this->pdo->prepare("SELECT * FROM v_theses_full WHERE id = ?"); $stmt->execute([$id]); return $stmt->fetch(); } /** * Get files associated with a thesis */ public function getThesisFiles($thesisId) { $sql = "SELECT * FROM thesis_files WHERE thesis_id = :thesis_id ORDER BY file_type, uploaded_at"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':thesis_id', $thesisId, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } // ======================================================================== // SEARCH FUNCTIONALITY (from front-backend - secure implementation) // ======================================================================== /** * Escape LIKE wildcards in user input to prevent wildcard injection */ private function escapeLikeString($string) { return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $string); } /** * Validate and sanitize search parameters * @throws InvalidArgumentException if validation fails */ private function validateSearchParams($params) { $validated = []; if (!empty($params['query'])) { $query = trim($params['query']); if (strlen($query) > 200) { throw new InvalidArgumentException("Search query too long (max 200 characters)"); } $validated['query'] = $this->escapeLikeString($query); } if (!empty($params['year'])) { $year = intval($params['year']); if ($year < 1900 || $year > 2100) { throw new InvalidArgumentException("Invalid year"); } $validated['year'] = $year; } if (!empty($params['orientation'])) { $orientation = trim($params['orientation']); if (strlen($orientation) > 100) { throw new InvalidArgumentException("Orientation name too long"); } $validated['orientation'] = $this->escapeLikeString($orientation); } if (!empty($params['ap_program'])) { $ap = trim($params['ap_program']); if (strlen($ap) > 100) { throw new InvalidArgumentException("AP program name too long"); } $validated['ap_program'] = $this->escapeLikeString($ap); } if (!empty($params['finality'])) { $finality = trim($params['finality']); if (strlen($finality) > 100) { throw new InvalidArgumentException("Finality name too long"); } $validated['finality'] = $this->escapeLikeString($finality); } if (!empty($params['keyword'])) { $keyword = trim($params['keyword']); if (strlen($keyword) > 100) { throw new InvalidArgumentException("Keyword too long"); } $validated['keyword'] = $this->escapeLikeString($keyword); } if (!empty($params['format'])) { $format = trim($params['format']); if (strlen($format) > 100) { throw new InvalidArgumentException("Format name too long"); } $validated['format'] = $this->escapeLikeString($format); } if (!empty($params['language'])) { $language = trim($params['language']); if (strlen($language) > 50) { throw new InvalidArgumentException("Language name too long"); } $validated['language'] = $this->escapeLikeString($language); } if (isset($params['is_doctoral'])) { $validated['is_doctoral'] = (bool)$params['is_doctoral']; } return $validated; } /** * Search theses with filters (secure implementation) */ public function searchTheses($params = [], $limit = 20, $offset = 0) { $params = $this->validateSearchParams($params); $limit = max(1, min(100, intval($limit))); $offset = max(0, intval($offset)); $conditions = ["is_published = 1"]; $bindings = []; if (!empty($params['query'])) { $conditions[] = "( title LIKE :query ESCAPE '\\' OR subtitle LIKE :query ESCAPE '\\' OR synopsis LIKE :query ESCAPE '\\' OR authors LIKE :query ESCAPE '\\' OR supervisors LIKE :query ESCAPE '\\' OR keywords LIKE :query ESCAPE '\\' )"; $bindings[':query'] = '%' . $params['query'] . '%'; } if (!empty($params['year'])) { $conditions[] = "year = :year"; $bindings[':year'] = $params['year']; } if (!empty($params['orientation'])) { $conditions[] = "orientation LIKE :orientation ESCAPE '\\'"; $bindings[':orientation'] = '%' . $params['orientation'] . '%'; } if (!empty($params['ap_program'])) { $conditions[] = "ap_program LIKE :ap_program ESCAPE '\\'"; $bindings[':ap_program'] = '%' . $params['ap_program'] . '%'; } if (!empty($params['finality'])) { $conditions[] = "finality_type LIKE :finality ESCAPE '\\'"; $bindings[':finality'] = '%' . $params['finality'] . '%'; } if (!empty($params['keyword'])) { $conditions[] = "keywords LIKE :keyword ESCAPE '\\'"; $bindings[':keyword'] = '%' . $params['keyword'] . '%'; } if (!empty($params['format'])) { $conditions[] = "formats LIKE :format ESCAPE '\\'"; $bindings[':format'] = '%' . $params['format'] . '%'; } if (!empty($params['language'])) { $conditions[] = "languages LIKE :language ESCAPE '\\'"; $bindings[':language'] = '%' . $params['language'] . '%'; } if (isset($params['is_doctoral'])) { $conditions[] = "is_doctoral = :is_doctoral"; $bindings[':is_doctoral'] = $params['is_doctoral'] ? 1 : 0; } $whereClause = implode(' AND ', $conditions); $sql = "SELECT * FROM v_theses_public WHERE $whereClause ORDER BY year DESC, title ASC LIMIT :limit OFFSET :offset"; $stmt = $this->pdo->prepare($sql); foreach ($bindings as $key => $value) { $stmt->bindValue($key, $value); } $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } /** * Count search results */ public function countSearchResults($params = []) { $params = $this->validateSearchParams($params); $conditions = ["is_published = 1"]; $bindings = []; if (!empty($params['query'])) { $conditions[] = "( title LIKE :query ESCAPE '\\' OR subtitle LIKE :query ESCAPE '\\' OR synopsis LIKE :query ESCAPE '\\' OR authors LIKE :query ESCAPE '\\' OR supervisors LIKE :query ESCAPE '\\' OR keywords LIKE :query ESCAPE '\\' )"; $bindings[':query'] = '%' . $params['query'] . '%'; } if (!empty($params['year'])) { $conditions[] = "year = :year"; $bindings[':year'] = $params['year']; } if (!empty($params['orientation'])) { $conditions[] = "orientation LIKE :orientation ESCAPE '\\'"; $bindings[':orientation'] = '%' . $params['orientation'] . '%'; } if (!empty($params['ap_program'])) { $conditions[] = "ap_program LIKE :ap_program ESCAPE '\\'"; $bindings[':ap_program'] = '%' . $params['ap_program'] . '%'; } if (!empty($params['finality'])) { $conditions[] = "finality_type LIKE :finality ESCAPE '\\'"; $bindings[':finality'] = '%' . $params['finality'] . '%'; } if (!empty($params['keyword'])) { $conditions[] = "keywords LIKE :keyword ESCAPE '\\'"; $bindings[':keyword'] = '%' . $params['keyword'] . '%'; } if (!empty($params['format'])) { $conditions[] = "formats LIKE :format ESCAPE '\\'"; $bindings[':format'] = '%' . $params['format'] . '%'; } if (!empty($params['language'])) { $conditions[] = "languages LIKE :language ESCAPE '\\'"; $bindings[':language'] = '%' . $params['language'] . '%'; } if (isset($params['is_doctoral'])) { $conditions[] = "is_doctoral = :is_doctoral"; $bindings[':is_doctoral'] = $params['is_doctoral'] ? 1 : 0; } $whereClause = implode(' AND ', $conditions); $sql = "SELECT COUNT(*) as count FROM v_theses_public WHERE $whereClause"; $stmt = $this->pdo->prepare($sql); foreach ($bindings as $key => $value) { $stmt->bindValue($key, $value); } $stmt->execute(); $result = $stmt->fetch(); return $result['count']; } /** * Get all available years from published theses */ public function getAvailableYears() { $sql = "SELECT DISTINCT year FROM theses WHERE is_published = 1 ORDER BY year DESC"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(PDO::FETCH_COLUMN); } /** * Get all orientations */ public function getOrientations() { $sql = "SELECT * FROM orientations ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllOrientations() { return $this->getOrientations(); } /** * Get all AP programs */ public function getApPrograms() { $sql = "SELECT * FROM ap_programs ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllAPPrograms() { return $this->getApPrograms(); } /** * Get all finality types */ public function getFinalityTypes() { $sql = "SELECT * FROM finality_types ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllFinalityTypes() { return $this->getFinalityTypes(); } /** * Get all keywords used in published theses */ public function getUsedKeywords() { $sql = "SELECT DISTINCT k.* FROM keywords k INNER JOIN thesis_keywords tk ON k.id = tk.keyword_id INNER JOIN theses t ON tk.thesis_id = t.id WHERE t.is_published = 1 ORDER BY k.keyword"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Get all format types */ public function getFormatTypes() { $sql = "SELECT * FROM format_types ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllFormatTypes() { return $this->getFormatTypes(); } /** * Get all languages */ public function getLanguages() { $sql = "SELECT * FROM languages ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllLanguages() { return $this->getLanguages(); } // ======================================================================== // CRUD METHODS (from formulaire) // ======================================================================== /** * Find or create an author */ public function findOrCreateAuthor($name, $email = null) { $stmt = $this->pdo->prepare("SELECT id FROM authors WHERE name = ?"); $stmt->execute([$name]); $author = $stmt->fetch(); if ($author) { if ($email && $email !== '') { $updateStmt = $this->pdo->prepare("UPDATE authors SET email = ? WHERE id = ?"); $updateStmt->execute([$email, $author['id']]); } return $author['id']; } $stmt = $this->pdo->prepare("INSERT INTO authors (name, email) VALUES (?, ?)"); $stmt->execute([$name, $email]); return $this->pdo->lastInsertId(); } /** * Find or create a supervisor */ public function findOrCreateSupervisor($name) { $stmt = $this->pdo->prepare("SELECT id FROM supervisors WHERE name = ?"); $stmt->execute([$name]); $supervisor = $stmt->fetch(); if ($supervisor) { return $supervisor['id']; } $stmt = $this->pdo->prepare("INSERT INTO supervisors (name) VALUES (?)"); $stmt->execute([$name]); return $this->pdo->lastInsertId(); } /** * Find or create a keyword */ public function findOrCreateKeyword($keyword) { $keyword = trim($keyword); if (empty($keyword)) { return null; } $stmt = $this->pdo->prepare("SELECT id FROM keywords WHERE keyword = ?"); $stmt->execute([$keyword]); $kw = $stmt->fetch(); if ($kw) { return $kw['id']; } $stmt = $this->pdo->prepare("INSERT INTO keywords (keyword) VALUES (?)"); $stmt->execute([$keyword]); return $this->pdo->lastInsertId(); } /** * Get orientation ID by name */ public function getOrientationId($name) { $stmt = $this->pdo->prepare("SELECT id FROM orientations WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Get AP program ID by name */ public function getAPProgramId($name) { $stmt = $this->pdo->prepare("SELECT id FROM ap_programs WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Get finality type ID by name */ public function getFinalityId($name) { $stmt = $this->pdo->prepare("SELECT id FROM finality_types WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Get language ID by name */ public function getLanguageId($name) { $stmt = $this->pdo->prepare("SELECT id FROM languages WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Get format type ID by name */ public function getFormatId($name) { $stmt = $this->pdo->prepare("SELECT id FROM format_types WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Insert a thesis file record */ public function insertThesisFile($thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType) { $stmt = $this->pdo->prepare(" INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type) VALUES (?, ?, ?, ?, ?, ?) "); $stmt->execute([$thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType]); return $this->pdo->lastInsertId(); } // ======================================================================== // SINGLETON PATTERN ENFORCEMENT // ======================================================================== /** * Prevent cloning */ private function __clone() {} /** * Prevent unserialization */ public function __wakeup() { throw new Exception("Cannot unserialize singleton"); } }