Files
xamxam/src/Database.php
Pontoporeia eaad740574 refactor: extract buildSearchConditions, add getThesesList, remove dead code, fix SearchTest
- Database: extract private buildSearchConditions(array $params): array shared by
  searchTheses() and countSearchResults(), eliminating ~80 lines of duplication;
  add array type hints to both public methods
- Database: add getThesesList(array $filters) and getAllYears() so admin/index.php
  no longer builds raw SQL inline
- admin/index.php: replace inline PDO query block with $db->getThesesList() /
  $db->getAllYears(); drop the now-unused $pdo local
- config/bootstrap.php: remove dead include_template() helper and the
  vendor/autoload.php Composer stub (no vendor/ directory exists)
- apps/: delete entire directory (leftover artefact, no code references it)
- tests/Integration/SearchTest.php: fix three searchTheses() calls from bare
  strings to proper array params to match the method signature (prevented TypeError)
2026-02-24 23:21:44 +01:00

679 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/config.php';
/**
* Unified Database connection class for Post-ERG thesis database
* Combines functionality from both front-backend and formulaire
* Supports both singleton (front-backend) and direct instantiation (formulaire)
*/
class Database {
private static $instance = null;
private $pdo;
private $dbPath;
/**
* Constructor - public to support both singleton and direct instantiation
* @param string $dbPath Optional database path override
*/
public function __construct($dbPath = null) {
$this->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;
}
/**
* Build WHERE conditions and named bindings from validated search params.
* Always includes the `is_published = 1` guard.
*
* @param array $params Already-validated params (output of validateSearchParams)
* @return array{0: string[], 1: array<string,mixed>} [$conditions, $bindings]
*/
private function buildSearchConditions(array $params): array {
$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;
}
return [$conditions, $bindings];
}
/**
* Search theses with filters (secure implementation)
*/
public function searchTheses(array $params = [], $limit = 20, $offset = 0) {
$params = $this->validateSearchParams($params);
$limit = max(1, min(100, intval($limit)));
$offset = max(0, intval($offset));
[$conditions, $bindings] = $this->buildSearchConditions($params);
$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(array $params = []) {
$params = $this->validateSearchParams($params);
[$conditions, $bindings] = $this->buildSearchConditions($params);
$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();
}
// ========================================================================
// ADMIN LIST METHOD
// ========================================================================
/**
* Return theses for the admin list view, with optional filters.
*
* Filters (all optional):
* 'search' string matches title, subtitle, or author name (LIKE)
* 'year' int exact year match
* 'orientation' int orientation_id exact match
*
* @param array $filters
* @return array
*/
public function getThesesList(array $filters = []): array {
$sql = "SELECT
t.id, t.identifier, t.title, t.subtitle, t.year,
o.name as orientation,
ap.name as ap_program,
GROUP_CONCAT(DISTINCT a.name) as authors,
t.submitted_at,
t.is_published
FROM theses t
LEFT JOIN orientations o ON t.orientation_id = o.id
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
LEFT JOIN authors a ON ta.author_id = a.id
WHERE 1=1";
$params = [];
if (!empty($filters['search'])) {
$sql .= " AND (t.title LIKE ? OR t.subtitle LIKE ? OR a.name LIKE ?)";
$searchParam = '%' . $filters['search'] . '%';
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
if (!empty($filters['year'])) {
$sql .= " AND t.year = ?";
$params[] = intval($filters['year']);
}
if (!empty($filters['orientation'])) {
$sql .= " AND t.orientation_id = ?";
$params[] = intval($filters['orientation']);
}
$sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
/**
* Get distinct years present in the theses table (admin, includes unpublished).
*/
public function getAllYears(): array {
$stmt = $this->pdo->query("SELECT DISTINCT year FROM theses ORDER BY year DESC");
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
// ========================================================================
// 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");
}
}