mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
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)
This commit is contained in:
14
TODO.md
14
TODO.md
@@ -39,7 +39,7 @@ third-party dependencies. The tasks below are ordered from critical to nice-to-h
|
||||
`src/Database.php` and `src/AdminAuth.php` via `APP_ROOT` (the constant already
|
||||
defined in `bootstrap.php`), removing the fragile relative-path `../../` chains.
|
||||
|
||||
- [ ] **Eliminate the duplicate `searchTheses` / `countSearchResults` condition block**
|
||||
- [x] **Eliminate the duplicate `searchTheses` / `countSearchResults` condition block**
|
||||
`Database::searchTheses()` and `Database::countSearchResults()` share identical
|
||||
WHERE-clause construction logic (~80 lines each). Extract a private
|
||||
`buildSearchConditions(array $params): array` helper that returns `[$conditions,
|
||||
@@ -52,7 +52,7 @@ third-party dependencies. The tasks below are ordered from critical to nice-to-h
|
||||
instead promoting the most-used raw queries into `Database` methods, reducing
|
||||
direct PDO exposure.
|
||||
|
||||
- [ ] **Move inline SQL in `admin/index.php` into `Database`**
|
||||
- [x] **Move inline SQL in `admin/index.php` into `Database`**
|
||||
`admin/index.php` builds a raw SQL query with dynamic filter conditions directly in
|
||||
the page. This is the only admin page doing so. Add a `getThesesList(array
|
||||
$filters): array` method to `Database` to match the pattern used everywhere else.
|
||||
@@ -67,20 +67,20 @@ third-party dependencies. The tasks below are ordered from critical to nice-to-h
|
||||
|
||||
## What Can Be Removed / Simplified
|
||||
|
||||
- [ ] **Remove `include_template()` helper from `bootstrap.php` — it is never called**
|
||||
- [x] **Remove `include_template()` helper from `bootstrap.php` — it is never called**
|
||||
The function `include_template($name)` in `config/bootstrap.php` is dead code;
|
||||
pages use direct `include APP_ROOT . '/templates/...'` instead.
|
||||
|
||||
- [ ] **Remove the Composer autoload stub from `bootstrap.php`**
|
||||
- [x] **Remove the Composer autoload stub from `bootstrap.php`**
|
||||
`bootstrap.php` has `if (file_exists(APP_ROOT . '/vendor/autoload.php'))` — there
|
||||
is no Composer vendor directory and no plan for one. Remove this dead branch.
|
||||
|
||||
- [ ] **Delete `apps/admin/` directory**
|
||||
- [x] **Delete `apps/admin/` directory**
|
||||
`apps/admin/` contains only `data/` (empty with test data) and `error.log` and
|
||||
`test.db`. It appears to be a leftover from an earlier structure. If confirmed
|
||||
unused, delete it.
|
||||
|
||||
- [ ] **Remove `apps/` directory entirely if it contains only residual artefacts**
|
||||
- [x] **Remove `apps/` directory entirely if it contains only residual artefacts**
|
||||
Related to the above — verify no active code references `apps/`.
|
||||
|
||||
---
|
||||
@@ -102,7 +102,7 @@ third-party dependencies. The tasks below are ordered from critical to nice-to-h
|
||||
|
||||
## Testing Infrastructure
|
||||
|
||||
- [ ] **Fix `SearchTest.php` — it calls `searchTheses()` with a string, not an array**
|
||||
- [x] **Fix `SearchTest.php` — it calls `searchTheses()` with a string, not an array**
|
||||
`$db->searchTheses('art')` passes a string, but `searchTheses()` expects
|
||||
`array $params`. This test would throw a TypeError at runtime. Fix the call to
|
||||
`$db->searchTheses(['query' => 'art'])`.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB |
@@ -1,251 +0,0 @@
|
||||
[27-Jan-2026 14:57:08 UTC] FILES array: Array
|
||||
(
|
||||
[couverture] => Array
|
||||
(
|
||||
[name] =>
|
||||
[full_path] =>
|
||||
[type] =>
|
||||
[tmp_name] =>
|
||||
[error] => 4
|
||||
[size] => 0
|
||||
)
|
||||
|
||||
[files] => Array
|
||||
(
|
||||
[name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[full_path] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[type] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[tmp_name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[error] => Array
|
||||
(
|
||||
[0] => 4
|
||||
)
|
||||
|
||||
[size] => Array
|
||||
(
|
||||
[0] => 0
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
[27-Jan-2026 14:57:08 UTC] Form processing error: Veuillez sélectionner au moins une langue.
|
||||
[27-Jan-2026 15:16:43 UTC] FILES array: Array
|
||||
(
|
||||
[couverture] => Array
|
||||
(
|
||||
[name] =>
|
||||
[full_path] =>
|
||||
[type] =>
|
||||
[tmp_name] =>
|
||||
[error] => 4
|
||||
[size] => 0
|
||||
)
|
||||
|
||||
[files] => Array
|
||||
(
|
||||
[name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[full_path] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[type] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[tmp_name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[error] => Array
|
||||
(
|
||||
[0] => 4
|
||||
)
|
||||
|
||||
[size] => Array
|
||||
(
|
||||
[0] => 0
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
[27-Jan-2026 15:16:43 UTC] Form processing error: Lien URL invalide.
|
||||
[27-Jan-2026 15:30:28 UTC] FILES array: Array
|
||||
(
|
||||
[couverture] => Array
|
||||
(
|
||||
[name] =>
|
||||
[full_path] =>
|
||||
[type] =>
|
||||
[tmp_name] =>
|
||||
[error] => 4
|
||||
[size] => 0
|
||||
)
|
||||
|
||||
[files] => Array
|
||||
(
|
||||
[name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[full_path] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[type] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[tmp_name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[error] => Array
|
||||
(
|
||||
[0] => 4
|
||||
)
|
||||
|
||||
[size] => Array
|
||||
(
|
||||
[0] => 0
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
[27-Jan-2026 15:30:28 UTC] Author ID: 1
|
||||
[27-Jan-2026 15:30:28 UTC] Thesis ID: 1
|
||||
[27-Jan-2026 15:30:29 UTC] Thesis submission completed successfully: 2026-001
|
||||
[27-Jan-2026 15:33:11 UTC] FILES array: Array
|
||||
(
|
||||
[couverture] => Array
|
||||
(
|
||||
[name] =>
|
||||
[full_path] =>
|
||||
[type] =>
|
||||
[tmp_name] =>
|
||||
[error] => 4
|
||||
[size] => 0
|
||||
)
|
||||
|
||||
[files] => Array
|
||||
(
|
||||
[name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[full_path] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[type] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[tmp_name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[error] => Array
|
||||
(
|
||||
[0] => 4
|
||||
)
|
||||
|
||||
[size] => Array
|
||||
(
|
||||
[0] => 0
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
[27-Jan-2026 15:33:11 UTC] Author ID: 2
|
||||
[27-Jan-2026 15:33:11 UTC] Thesis ID: 2
|
||||
[27-Jan-2026 15:33:12 UTC] Thesis submission completed successfully: 2026-002
|
||||
[27-Jan-2026 15:48:51 UTC] FILES array: Array
|
||||
(
|
||||
[couverture] => Array
|
||||
(
|
||||
[name] =>
|
||||
[full_path] =>
|
||||
[type] =>
|
||||
[tmp_name] =>
|
||||
[error] => 4
|
||||
[size] => 0
|
||||
)
|
||||
|
||||
[files] => Array
|
||||
(
|
||||
[name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[full_path] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[type] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[tmp_name] => Array
|
||||
(
|
||||
[0] =>
|
||||
)
|
||||
|
||||
[error] => Array
|
||||
(
|
||||
[0] => 4
|
||||
)
|
||||
|
||||
[size] => Array
|
||||
(
|
||||
[0] => 0
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
[27-Jan-2026 15:48:51 UTC] Author ID: 14
|
||||
[27-Jan-2026 15:48:51 UTC] Thesis ID: 14
|
||||
[27-Jan-2026 15:48:51 UTC] Thesis submission completed successfully: 2026-003
|
||||
Binary file not shown.
@@ -24,21 +24,7 @@ if (php_sapi_name() === 'cli-server') {
|
||||
ini_set('log_errors', '1');
|
||||
}
|
||||
|
||||
// Simple helper function for including templates
|
||||
function include_template($name)
|
||||
{
|
||||
$path = APP_ROOT . '/templates/' . $name;
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
}
|
||||
}
|
||||
|
||||
// Load admin credentials if available (defines ADMIN_PASSWORD_HASH for AdminAuth)
|
||||
if (file_exists(APP_ROOT . '/config/admin_credentials.php')) {
|
||||
require_once APP_ROOT . '/config/admin_credentials.php';
|
||||
}
|
||||
|
||||
// Autoload Composer dependencies if available
|
||||
if (file_exists(APP_ROOT . '/vendor/autoload.php')) {
|
||||
require_once APP_ROOT . '/vendor/autoload.php';
|
||||
}
|
||||
|
||||
@@ -17,59 +17,25 @@ require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
$pdo = $db->getPDO();
|
||||
|
||||
// Get filter parameters
|
||||
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
|
||||
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
|
||||
|
||||
// Build query
|
||||
$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 = [];
|
||||
|
||||
$filters = [];
|
||||
if ($searchQuery) {
|
||||
$sql .= " AND (t.title LIKE ? OR t.subtitle LIKE ? OR a.name LIKE ?)";
|
||||
$searchParam = "%$searchQuery%";
|
||||
$params[] = $searchParam;
|
||||
$params[] = $searchParam;
|
||||
$params[] = $searchParam;
|
||||
$filters['search'] = $searchQuery;
|
||||
}
|
||||
|
||||
if ($yearFilter) {
|
||||
$sql .= " AND t.year = ?";
|
||||
$params[] = $yearFilter;
|
||||
$filters['year'] = $yearFilter;
|
||||
}
|
||||
|
||||
if ($orientationFilter) {
|
||||
$sql .= " AND t.orientation_id = ?";
|
||||
$params[] = $orientationFilter;
|
||||
$filters['orientation'] = $orientationFilter;
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$theses = $stmt->fetchAll();
|
||||
|
||||
// Get unique years for filter
|
||||
$yearsStmt = $pdo->query("SELECT DISTINCT year FROM theses ORDER BY year DESC");
|
||||
$years = $yearsStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Get orientations for filter
|
||||
$theses = $db->getThesesList($filters);
|
||||
$years = $db->getAllYears();
|
||||
$orientations = $db->getAllOrientations();
|
||||
} catch (Exception $e) {
|
||||
error_log("Error loading theses list: " . $e->getMessage());
|
||||
|
||||
146
src/Database.php
146
src/Database.php
@@ -257,13 +257,13 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search theses with filters (secure implementation)
|
||||
* 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]
|
||||
*/
|
||||
public function searchTheses($params = [], $limit = 20, $offset = 0) {
|
||||
$params = $this->validateSearchParams($params);
|
||||
$limit = max(1, min(100, intval($limit)));
|
||||
$offset = max(0, intval($offset));
|
||||
|
||||
private function buildSearchConditions(array $params): array {
|
||||
$conditions = ["is_published = 1"];
|
||||
$bindings = [];
|
||||
|
||||
@@ -319,6 +319,19 @@ class Database {
|
||||
$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";
|
||||
|
||||
@@ -336,62 +349,10 @@ class Database {
|
||||
/**
|
||||
* Count search results
|
||||
*/
|
||||
public function countSearchResults($params = []) {
|
||||
public function countSearchResults(array $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;
|
||||
}
|
||||
[$conditions, $bindings] = $this->buildSearchConditions($params);
|
||||
|
||||
$whereClause = implode(' AND ', $conditions);
|
||||
$sql = "SELECT COUNT(*) as count FROM v_theses_public WHERE $whereClause";
|
||||
@@ -508,6 +469,71 @@ class Database {
|
||||
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)
|
||||
// ========================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ try {
|
||||
|
||||
// Test 1: Search with empty query
|
||||
echo "Test 1: Empty Search Query\n";
|
||||
$results = $db->searchTheses('');
|
||||
$results = $db->searchTheses([]);
|
||||
if (is_array($results)) {
|
||||
echo "✓ PASS: Empty query handled (returned " . count($results) . " results)\n\n";
|
||||
} else {
|
||||
@@ -24,7 +24,7 @@ try {
|
||||
// Test 2: Search for specific term
|
||||
echo "Test 2: Search for Specific Term\n";
|
||||
$searchTerm = 'art'; // Common word likely to appear
|
||||
$results = $db->searchTheses($searchTerm);
|
||||
$results = $db->searchTheses(['query' => $searchTerm]);
|
||||
if (is_array($results)) {
|
||||
echo "✓ PASS: Search for '$searchTerm' returned " . count($results) . " results\n\n";
|
||||
} else {
|
||||
@@ -33,7 +33,7 @@ try {
|
||||
|
||||
// Test 3: Search with special characters
|
||||
echo "Test 3: Search with Special Characters\n";
|
||||
$results = $db->searchTheses("test's \"quotes\" & symbols");
|
||||
$results = $db->searchTheses(['query' => "test's \"quotes\" & symbols"]);
|
||||
if (is_array($results)) {
|
||||
echo "✓ PASS: Special characters handled safely\n\n";
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user