mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +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
|
`src/Database.php` and `src/AdminAuth.php` via `APP_ROOT` (the constant already
|
||||||
defined in `bootstrap.php`), removing the fragile relative-path `../../` chains.
|
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
|
`Database::searchTheses()` and `Database::countSearchResults()` share identical
|
||||||
WHERE-clause construction logic (~80 lines each). Extract a private
|
WHERE-clause construction logic (~80 lines each). Extract a private
|
||||||
`buildSearchConditions(array $params): array` helper that returns `[$conditions,
|
`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
|
instead promoting the most-used raw queries into `Database` methods, reducing
|
||||||
direct PDO exposure.
|
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
|
`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
|
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.
|
$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
|
## 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;
|
The function `include_template($name)` in `config/bootstrap.php` is dead code;
|
||||||
pages use direct `include APP_ROOT . '/templates/...'` instead.
|
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
|
`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.
|
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
|
`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
|
`test.db`. It appears to be a leftover from an earlier structure. If confirmed
|
||||||
unused, delete it.
|
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/`.
|
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
|
## 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
|
`$db->searchTheses('art')` passes a string, but `searchTheses()` expects
|
||||||
`array $params`. This test would throw a TypeError at runtime. Fix the call to
|
`array $params`. This test would throw a TypeError at runtime. Fix the call to
|
||||||
`$db->searchTheses(['query' => 'art'])`.
|
`$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');
|
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)
|
// Load admin credentials if available (defines ADMIN_PASSWORD_HASH for AdminAuth)
|
||||||
if (file_exists(APP_ROOT . '/config/admin_credentials.php')) {
|
if (file_exists(APP_ROOT . '/config/admin_credentials.php')) {
|
||||||
require_once 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 {
|
try {
|
||||||
$db = new Database();
|
$db = new Database();
|
||||||
$pdo = $db->getPDO();
|
|
||||||
|
|
||||||
// Get filter parameters
|
// Get filter parameters
|
||||||
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
|
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||||
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
|
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
|
||||||
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
|
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
|
||||||
|
|
||||||
// Build query
|
$filters = [];
|
||||||
$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 ($searchQuery) {
|
if ($searchQuery) {
|
||||||
$sql .= " AND (t.title LIKE ? OR t.subtitle LIKE ? OR a.name LIKE ?)";
|
$filters['search'] = $searchQuery;
|
||||||
$searchParam = "%$searchQuery%";
|
|
||||||
$params[] = $searchParam;
|
|
||||||
$params[] = $searchParam;
|
|
||||||
$params[] = $searchParam;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($yearFilter) {
|
if ($yearFilter) {
|
||||||
$sql .= " AND t.year = ?";
|
$filters['year'] = $yearFilter;
|
||||||
$params[] = $yearFilter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($orientationFilter) {
|
if ($orientationFilter) {
|
||||||
$sql .= " AND t.orientation_id = ?";
|
$filters['orientation'] = $orientationFilter;
|
||||||
$params[] = $orientationFilter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC";
|
$theses = $db->getThesesList($filters);
|
||||||
|
$years = $db->getAllYears();
|
||||||
$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
|
|
||||||
$orientations = $db->getAllOrientations();
|
$orientations = $db->getAllOrientations();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log("Error loading theses list: " . $e->getMessage());
|
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) {
|
private function buildSearchConditions(array $params): array {
|
||||||
$params = $this->validateSearchParams($params);
|
|
||||||
$limit = max(1, min(100, intval($limit)));
|
|
||||||
$offset = max(0, intval($offset));
|
|
||||||
|
|
||||||
$conditions = ["is_published = 1"];
|
$conditions = ["is_published = 1"];
|
||||||
$bindings = [];
|
$bindings = [];
|
||||||
|
|
||||||
@@ -319,6 +319,19 @@ class Database {
|
|||||||
$bindings[':is_doctoral'] = $params['is_doctoral'] ? 1 : 0;
|
$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);
|
$whereClause = implode(' AND ', $conditions);
|
||||||
$sql = "SELECT * FROM v_theses_public WHERE $whereClause ORDER BY year DESC, title ASC LIMIT :limit OFFSET :offset";
|
$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
|
* Count search results
|
||||||
*/
|
*/
|
||||||
public function countSearchResults($params = []) {
|
public function countSearchResults(array $params = []) {
|
||||||
$params = $this->validateSearchParams($params);
|
$params = $this->validateSearchParams($params);
|
||||||
$conditions = ["is_published = 1"];
|
|
||||||
$bindings = [];
|
|
||||||
|
|
||||||
if (!empty($params['query'])) {
|
[$conditions, $bindings] = $this->buildSearchConditions($params);
|
||||||
$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);
|
$whereClause = implode(' AND ', $conditions);
|
||||||
$sql = "SELECT COUNT(*) as count FROM v_theses_public WHERE $whereClause";
|
$sql = "SELECT COUNT(*) as count FROM v_theses_public WHERE $whereClause";
|
||||||
@@ -508,6 +469,71 @@ class Database {
|
|||||||
return $this->getLanguages();
|
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)
|
// CRUD METHODS (from formulaire)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ try {
|
|||||||
|
|
||||||
// Test 1: Search with empty query
|
// Test 1: Search with empty query
|
||||||
echo "Test 1: Empty Search Query\n";
|
echo "Test 1: Empty Search Query\n";
|
||||||
$results = $db->searchTheses('');
|
$results = $db->searchTheses([]);
|
||||||
if (is_array($results)) {
|
if (is_array($results)) {
|
||||||
echo "✓ PASS: Empty query handled (returned " . count($results) . " results)\n\n";
|
echo "✓ PASS: Empty query handled (returned " . count($results) . " results)\n\n";
|
||||||
} else {
|
} else {
|
||||||
@@ -24,7 +24,7 @@ try {
|
|||||||
// Test 2: Search for specific term
|
// Test 2: Search for specific term
|
||||||
echo "Test 2: Search for Specific Term\n";
|
echo "Test 2: Search for Specific Term\n";
|
||||||
$searchTerm = 'art'; // Common word likely to appear
|
$searchTerm = 'art'; // Common word likely to appear
|
||||||
$results = $db->searchTheses($searchTerm);
|
$results = $db->searchTheses(['query' => $searchTerm]);
|
||||||
if (is_array($results)) {
|
if (is_array($results)) {
|
||||||
echo "✓ PASS: Search for '$searchTerm' returned " . count($results) . " results\n\n";
|
echo "✓ PASS: Search for '$searchTerm' returned " . count($results) . " results\n\n";
|
||||||
} else {
|
} else {
|
||||||
@@ -33,7 +33,7 @@ try {
|
|||||||
|
|
||||||
// Test 3: Search with special characters
|
// Test 3: Search with special characters
|
||||||
echo "Test 3: Search with Special Characters\n";
|
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)) {
|
if (is_array($results)) {
|
||||||
echo "✓ PASS: Special characters handled safely\n\n";
|
echo "✓ PASS: Special characters handled safely\n\n";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user