From 61ac3c002db25215548110f6b78440e26ab64643 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Sat, 28 Mar 2026 13:52:43 +0100 Subject: [PATCH] refactor: encapsulate thesis creation SQL in Database::createThesis() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the raw identifier-generation query and the INSERT INTO theses / INSERT INTO thesis_authors statements out of formulaire.php into two new Database methods: generateThesisIdentifier(int $year): string – counts existing theses for the year inside the open transaction so concurrent workers cannot produce duplicate YYYY-NNN identifiers. createThesis(array $data): int – generates the identifier, INSERTs the thesis row, links the author via thesis_authors (author_order=1), returns the new thesis ID. getThesisIdentifier(int $id): string – fetches the stored identifier for a thesis ID; used by formulaire.php to reconstruct the upload path (storage/theses/YYYY/YYYY-NNN/). formulaire.php now calls $db->createThesis([…]) + $db->getThesisIdentifier() and no longer holds any raw PDO queries for the core thesis insert. The $pdo local variable (previously $db->getPDO()) is removed entirely. All four test suites (Unit, RateLimit, Integration, Security) pass. --- TODO.md | 2 +- public/admin/actions/formulaire.php | 54 ++++-------- src/Database.php | 78 ++++++++++++++++++ .../ad921d60486366258809553a3db49a4a.json | 2 +- storage/test.db | Bin 237568 -> 237568 bytes 5 files changed, 95 insertions(+), 41 deletions(-) diff --git a/TODO.md b/TODO.md index 32de271..9bd28e3 100644 --- a/TODO.md +++ b/TODO.md @@ -399,7 +399,7 @@ Goal: rename the tables and column to the canonical M2M pattern (`tags`, `thesis - [x] `edit.php` (line 155): unparameterised `"… WHERE id = $thesisId"` SQL injection → fixed; raw `SELECT banner_path` → `getThesisBannerPath(int $id): ?string` - [x] `edit.php`: raw `SELECT license_id, access_type_id, context_note` → `getThesisRawFields(int $id): ?array` - [x] `system.php`: raw `SELECT COUNT(*) FROM theses` → `getThesisCount(): int` - - [ ] `formulaire.php`: raw identifier-generation query + all junction-table INSERTs → encapsulate in `Database::createThesis(array $data): int` + - [x] `formulaire.php`: raw identifier-generation query + all junction-table INSERTs → encapsulate in `Database::createThesis(array $data): int` - [x] **`sanitize_string()` in `formulaire.php` applies `htmlspecialchars` at write time** — HTML-escaping belongs at render time (in the template), not at storage time. Storing diff --git a/public/admin/actions/formulaire.php b/public/admin/actions/formulaire.php index 6e3af93..72759c5 100644 --- a/public/admin/actions/formulaire.php +++ b/public/admin/actions/formulaire.php @@ -41,8 +41,6 @@ function validate_required($value, $fieldName) { try { // Initialize database connection $db = new Database(); - $pdo = $db->getPDO(); - // Begin transaction - all or nothing $db->beginTransaction(); @@ -140,44 +138,22 @@ try { $authorId = $db->findOrCreateAuthor($auteurName, $mail); error_log("Author ID: $authorId"); - // ===== INSERT THESIS RECORD ===== - - // Generate unique identifier (YYYY-NNN format) - $stmt = $pdo->prepare("SELECT COUNT(*) as count FROM theses WHERE year = ?"); - $stmt->execute([$annee]); - $count = $stmt->fetch()['count'] + 1; - $identifier = sprintf("%d-%03d", $annee, $count); - - $stmt = $pdo->prepare(" - INSERT INTO theses ( - identifier, title, subtitle, year, - orientation_id, ap_program_id, finality_id, - synopsis, file_size_info, - baiu_link, license_id, - submitted_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) - "); - - $stmt->execute([ - $identifier, - $titre, - !empty($subtitle) ? $subtitle : null, - $annee, - $orientationId, - $apProgramId, - $finalityId, - $synopsis, - !empty($durationInfo) ? $durationInfo : null, - !empty($lien) ? $lien : null, - $licenseId + // ===== INSERT THESIS RECORD + LINK AUTHOR ===== + $thesisId = $db->createThesis([ + 'year' => $annee, + 'orientation_id' => $orientationId, + 'ap_program_id' => $apProgramId, + 'finality_id' => $finalityId, + 'title' => $titre, + 'subtitle' => $subtitle, + 'synopsis' => $synopsis, + 'file_size_info' => $durationInfo, + 'baiu_link' => $lien, + 'license_id' => $licenseId, + 'author_id' => $authorId, ]); - - $thesisId = $pdo->lastInsertId(); - error_log("Thesis ID: $thesisId"); - - // ===== LINK AUTHOR TO THESIS ===== - $stmt = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, 1)"); - $stmt->execute([$thesisId, $authorId]); + $identifier = $db->getThesisIdentifier($thesisId); + error_log("Thesis ID: $thesisId (identifier: $identifier)"); // ===== LINK JURY TO THESIS ===== $db->setThesisJury($thesisId, $juryMembers); diff --git a/src/Database.php b/src/Database.php index 4279c91..1a575dd 100644 --- a/src/Database.php +++ b/src/Database.php @@ -1101,6 +1101,84 @@ class Database { return (int)$this->pdo->query("SELECT COUNT(*) FROM theses")->fetchColumn(); } + /** + * Generate a unique YYYY-NNN identifier for a new thesis in the given year. + * Counts existing theses for that year (published or not) to determine the next sequence + * number. Must be called inside the same transaction that performs the INSERT so that + * concurrent requests cannot produce duplicate identifiers. + */ + public function generateThesisIdentifier(int $year): string { + $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM theses WHERE year = ?"); + $stmt->execute([$year]); + $count = (int)$stmt->fetchColumn() + 1; + return sprintf("%d-%03d", $year, $count); + } + + /** + * Insert a new thesis row, link its author, and return the new thesis ID. + * + * Expected keys in $data: + * year (int), orientation_id (int), ap_program_id (int), finality_id (int), + * title (string), subtitle (?string), synopsis (string), + * file_size_info (?string), baiu_link (?string), license_id (?int), + * author_id (int) + * + * The identifier is generated automatically from $data['year']. + * Must be called inside an open transaction. + * + * @return int The new thesis ID. + */ + public function createThesis(array $data): int { + $identifier = $this->generateThesisIdentifier((int)$data['year']); + + $stmt = $this->pdo->prepare(" + INSERT INTO theses ( + identifier, title, subtitle, year, + orientation_id, ap_program_id, finality_id, + synopsis, file_size_info, + baiu_link, license_id, + submitted_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + "); + + $stmt->execute([ + $identifier, + $data['title'], + !empty($data['subtitle']) ? $data['subtitle'] : null, + (int)$data['year'], + (int)$data['orientation_id'], + (int)$data['ap_program_id'], + (int)$data['finality_id'], + $data['synopsis'], + !empty($data['file_size_info']) ? $data['file_size_info'] : null, + !empty($data['baiu_link']) ? $data['baiu_link'] : null, + isset($data['license_id']) ? $data['license_id'] : null, + ]); + + $thesisId = (int)$this->pdo->lastInsertId(); + + // Link author — always author_order = 1 for single-author submissions. + $stmt = $this->pdo->prepare( + "INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, 1)" + ); + $stmt->execute([$thesisId, (int)$data['author_id']]); + + return $thesisId; + } + + /** + * Return the stored identifier string (e.g. "2024-003") for an existing thesis. + */ + public function getThesisIdentifier(int $thesisId): string { + $stmt = $this->pdo->prepare("SELECT identifier FROM theses WHERE id = ?"); + $stmt->execute([$thesisId]); + $row = $stmt->fetch(); + if (!$row) { + throw new \RuntimeException("Thesis #$thesisId not found"); + } + return (string)$row['identifier']; + } + /** * Insert a thesis file record */ diff --git a/src/cache/rate_limit/ad921d60486366258809553a3db49a4a.json b/src/cache/rate_limit/ad921d60486366258809553a3db49a4a.json index bb96814..d02218c 100644 --- a/src/cache/rate_limit/ad921d60486366258809553a3db49a4a.json +++ b/src/cache/rate_limit/ad921d60486366258809553a3db49a4a.json @@ -1 +1 @@ -[1774702033] \ No newline at end of file +[1774702347] \ No newline at end of file diff --git a/storage/test.db b/storage/test.db index ec4323305e77661ef72c05844aad8b4ac9279008..01a9fda3ebd7817c446af473327be769d508e65d 100644 GIT binary patch delta 28 icmZoTz}IkqZ$pqiV`_7-etWP!BM>uf57uX1TL1u*=n1C) delta 28 icmZoTz}IkqZ$pqiV@h+detWP!BM>uf57uX1TL1u*)(NHn