db = $db; $this->rateLimit = $rateLimit; } // ── Factory ─────────────────────────────────────────────────────────────── /** * Convenience factory: builds dependencies, checks rate limit (sends 429 * and exits if exceeded), then returns a ready-to-use controller instance. */ public static function create(): self { require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/RateLimit.php'; $rateLimit = new RateLimit(self::RATE_LIMIT_MAX, self::RATE_LIMIT_WINDOW); if (!$rateLimit->check()) { self::sendRateLimitResponse($rateLimit); } $rateLimit->sendHeaders(); // Probabilistic cleanup (1-in-100 requests) to prune stale entries if (rand(1, 100) === 1) { $rateLimit->cleanup(); } return new self(Database::getInstance(), $rateLimit); } // ── Main entry point ───────────────────────────────────────────────────── /** * Process the current request and return all variables needed by the view. * * @return array */ public function handle(): array { $searchParams = $this->collectSearchParams(); $hasSearch = !empty($searchParams); $page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1; $offset = ($page - 1) * self::ITEMS_PER_PAGE; $validationError = null; $results = []; $totalItems = 0; $totalPages = 0; $years = []; $orientations = []; $apPrograms = []; $keywords = []; $students = []; try { if ($hasSearch) { $results = $this->db->searchTheses($searchParams, self::ITEMS_PER_PAGE, $offset); $totalItems = $this->db->countSearchResults($searchParams); $totalPages = (int) ceil($totalItems / self::ITEMS_PER_PAGE); } $years = $this->db->getAvailableYears(); $orientations = $this->db->getAllOrientations(); $apPrograms = $this->db->getAllAPPrograms(); $keywords = $this->db->getUsedTags(); // Fetch id+authors only — lean query bypassing the fat v_theses_public view $students = $this->db->getPublishedAuthors(); } catch (InvalidArgumentException $e) { $validationError = $e->getMessage(); } catch (Exception $e) { error_log('SearchController: ' . $e->getMessage()); $validationError = 'Une erreur est survenue.'; } // Build the author index map (répertoire index view) $authorMap = $this->buildAuthorMap($students); // Preserve all active search/filter params, strip 'page' (pagination partial adds it) $baseParams = array_diff_key($_GET, ['page' => '']); return [ // Search state 'searchParams' => $searchParams, 'hasSearch' => $hasSearch, 'page' => $page, 'totalItems' => $totalItems, 'totalPages' => $totalPages, 'results' => $results, 'validationError' => $validationError, 'baseParams' => $baseParams, // Filter / index data 'years' => $years, 'orientations' => $orientations, 'apPrograms' => $apPrograms, 'keywords' => $keywords, 'authorMap' => $authorMap, // Page meta 'searchBarValue' => $_GET['query'] ?? '', 'pageTitle' => 'Répertoire – Posterg', 'metaDescription' => 'Parcourez le répertoire des mémoires de fin d\'études (TFE) de l\'erg – École de Recherches Graphiques de Bruxelles. Recherche par année, orientation, atelier et mots-clés.', 'ogTags' => [ 'type' => 'website', 'title' => 'Répertoire – Posterg', 'description' => 'Parcourez le répertoire des mémoires de fin d\'études (TFE) de l\'erg – École de Recherches Graphiques de Bruxelles. Recherche par année, orientation, atelier et mots-clés.', 'url' => 'https://posterg.erg.be/search.php', 'site_name' => 'Posterg – ERG', ], 'currentNav' => 'repertoire', 'extraCss' => ['/assets/css/search.css'], 'bodyClass' => 'search-body', ]; } // ── Private helpers ─────────────────────────────────────────────────────── /** * Sanitise and collect valid search parameters from $_GET. * * @return array */ private function collectSearchParams(): array { $params = []; if (!empty($_GET['query'])) { $params['query'] = trim((string) $_GET['query']); } if (!empty($_GET['year'])) { $params['year'] = (int) $_GET['year']; } if (!empty($_GET['orientation'])) { $params['orientation'] = (string) $_GET['orientation']; } if (!empty($_GET['ap_program'])) { $params['ap_program'] = (string) $_GET['ap_program']; } if (!empty($_GET['keyword'])) { $params['keyword'] = (string) $_GET['keyword']; } return $params; } /** * Build an alphabetically-sorted author → thesis-id map from the * published-authors list. Each author name maps to their first thesis id. * * @param array $students * @return array */ private function buildAuthorMap(array $students): array { $map = []; foreach ($students as $s) { if (empty($s['authors'])) { continue; } foreach (explode(',', $s['authors']) as $name) { $name = trim($name); if ($name !== '' && !isset($map[$name])) { $map[$name] = (int) $s['id']; } } } ksort($map); return $map; } // ── Rate-limit response ─────────────────────────────────────────────────── /** * Send a 429 response and exit. Never returns. */ private static function sendRateLimitResponse(RateLimit $rateLimit): never { http_response_code(429); header('Retry-After: ' . $rateLimit->getResetTime()); $retrySeconds = (int) $rateLimit->getResetTime(); echo << Trop de requêtes – Posterg

Trop de requêtes

Vous avez effectué trop de recherches en peu de temps.
Réessayez dans {$retrySeconds} secondes.

HTML; exit; } }