diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache index b3778c7..7820353 100644 --- a/.php-cs-fixer.cache +++ b/.php-cs-fixer.cache @@ -1 +1 @@ -{"php":"8.5.5","version":"3.95.1","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_anonymous_functions":false,"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":true},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"clean_namespace":true,"no_unset_cast":true,"assign_null_coalescing_to_coalesce_equal":true,"normalize_index_brace":true,"heredoc_indentation":true,"no_whitespace_before_comma_in_array":{"after_heredoc":true},"trailing_comma_in_multiline":true,"list_syntax":true,"ternary_to_null_coalescing":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true,"single_quote":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"app\/src\/AdminAuth.php":"9281ad7ee564c8f0e321ba5f709c9a5a","app\/src\/ShareLink.php":"29e258f6344bff7d8d4db5e626d4ac51","app\/src\/StudentEmail.php":"4b522bf8921999866b2b56326e49d255","app\/tests\/Security\/SecurityTest.php":"ff9996affd0ca42095d34e0ac0650050","app\/tests\/run-tests.php":"89ccc4a9cff85fb8f2c92903eb866b79","app\/tests\/Integration\/SearchTest.php":"29aba0f9da3c115fb5d2722576815361","app\/tests\/Unit\/DatabaseTest.php":"2dd249e28ac632ba222a8cead3ae16a1","app\/tests\/Unit\/RateLimitTest.php":"4fc2ffe64d2c889835ef4fcd2e89d835","app\/src\/Database.php":"cc2b6873cfafab02dcb13931ff301753","app\/src\/Controllers\/ExportController.php":"12e717b606f36c3a3d49ad58f7120898","app\/src\/Controllers\/TfeController.php":"46f092d0ef7ee3d0bc609cb70d15ef97","app\/src\/Controllers\/SystemController.php":"aaf106e4c73fa846344513346a1236a6","app\/src\/Controllers\/SearchController.php":"cc2ecf70d496f4e05448f4ec9670a3bc","app\/src\/Controllers\/LicenceController.php":"be382174f80b1dbcda03450cabc531a7","app\/src\/Controllers\/FileAccessController.php":"9665edaa0ab1fd7c94b9a3d9fad95c5f","app\/src\/Controllers\/MediaController.php":"2cadef1d1d249ceaaf33abca2f79a8ad","app\/src\/Controllers\/HomeController.php":"d3a36adcdb969448c23cea34fb6d7896","app\/src\/Controllers\/AboutController.php":"79eac4bf3404a7c8d3ba736e3d2f32d6","app\/src\/Controllers\/LiveReloadController.php":"e2ff21e7155e769b2684a51accf1699d","app\/src\/Controllers\/ThesisEditController.php":"e02f5350f34949840d398d53423e7f18","app\/src\/Controllers\/ThesisCreateController.php":"37f2c13b34d49093289bae80d44104c9","app\/src\/Dispatcher.php":"d1d693bdacbe0006cb806b15d067cbbb","app\/src\/Parsedown.php":"d98c00dfbbb11933a86407ee9cf9215d","app\/src\/App.php":"abed36a5403738f071a5cdc3451b1e24","app\/src\/AppLogger.php":"8522037460732198e9e68d13ade7b13b","app\/src\/SystemCache.php":"4ead28637fa3a9281bdad42cdb9e00c2","app\/src\/RateLimit.php":"2e1df734570cb3eb584682bed33a2636","app\/src\/SmtpRelay.php":"22c9b92eab1575c14c393917138a6e3b"}} \ No newline at end of file +{"php":"8.5.6","version":"3.95.1","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_anonymous_functions":false,"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":true},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"clean_namespace":true,"no_unset_cast":true,"assign_null_coalescing_to_coalesce_equal":true,"normalize_index_brace":true,"heredoc_indentation":true,"no_whitespace_before_comma_in_array":{"after_heredoc":true},"trailing_comma_in_multiline":true,"list_syntax":true,"ternary_to_null_coalescing":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true,"single_quote":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"app\/src\/SystemCache.php":"4ead28637fa3a9281bdad42cdb9e00c2","app\/src\/SmtpRelay.php":"22c9b92eab1575c14c393917138a6e3b","app\/src\/AdminAuth.php":"14677d94cd04b7d455c43e74f7ec5d14","app\/src\/ShareLink.php":"84f06935f015221c39c517c1072795c9","app\/src\/StudentEmail.php":"84f7b438defaadf9d5d8a7821b3a4e78","app\/tests\/Security\/SecurityTest.php":"ff9996affd0ca42095d34e0ac0650050","app\/tests\/run-tests.php":"89ccc4a9cff85fb8f2c92903eb866b79","app\/tests\/Integration\/SearchTest.php":"29aba0f9da3c115fb5d2722576815361","app\/tests\/Unit\/DatabaseTest.php":"2dd249e28ac632ba222a8cead3ae16a1","app\/tests\/Unit\/RateLimitTest.php":"4fc2ffe64d2c889835ef4fcd2e89d835","app\/src\/Database.php":"57bcebbb33c8a7ba9e6c7b6f60a5ec23","app\/src\/Controllers\/ExportController.php":"8872052f3e9e171955df9c9198cfc708","app\/src\/Controllers\/TfeController.php":"7e67f11af8ae38ac683d2246717c4ecf","app\/src\/Controllers\/SystemController.php":"aaf106e4c73fa846344513346a1236a6","app\/src\/Controllers\/SearchController.php":"cc2ecf70d496f4e05448f4ec9670a3bc","app\/src\/Controllers\/LicenceController.php":"be382174f80b1dbcda03450cabc531a7","app\/src\/Controllers\/FileAccessController.php":"9665edaa0ab1fd7c94b9a3d9fad95c5f","app\/src\/Controllers\/MediaController.php":"2cadef1d1d249ceaaf33abca2f79a8ad","app\/src\/Controllers\/HomeController.php":"d3a36adcdb969448c23cea34fb6d7896","app\/src\/Controllers\/AboutController.php":"0be7251ba310770cb9702320026a8562","app\/src\/Controllers\/LiveReloadController.php":"e2ff21e7155e769b2684a51accf1699d","app\/src\/Controllers\/ThesisEditController.php":"34cd474e80c63acb99ded2bd46ff1dab","app\/src\/Controllers\/ThesisCreateController.php":"74aa55467df95e0efe5e30784baa2ac8","app\/src\/Dispatcher.php":"d1d693bdacbe0006cb806b15d067cbbb","app\/src\/Parsedown.php":"d98c00dfbbb11933a86407ee9cf9215d","app\/src\/App.php":"2fa0253736fdd6bfd28135e0c0ecb3f2","app\/src\/AppLogger.php":"139735566a1cc21d64eacc5b63de1d3c","app\/src\/DuplicateThesisException.php":"52abe5f40ef48cfbfd44c119d91309e9","app\/src\/RateLimit.php":"2e1df734570cb3eb584682bed33a2636","app\/src\/AdminLogger.php":"ba9ac9d222771b952ea6451b66ca6821"}} \ No newline at end of file diff --git a/TODO.md b/TODO.md index b269120..6426790 100644 --- a/TODO.md +++ b/TODO.md @@ -195,6 +195,21 @@ - [x] All three form pages (admin add, admin edit, partage) updated - [x] Controllers updated: `collectJuryMembers`, `validateAndSanitise`, `buildFileSizeInfo`, `license_custom`, `cc2r`→`cc4r` mapping +## Fix biome lint config + beforeunload-guard.js +- [x] `biome.json` — use `files.includes` negation patterns to exclude `htmx/overtype/sortable.min.js` +- [x] `justfile lint-biome` — lint entire `app/public/assets/js/` dir (no hardcoded file list) +- [x] `beforeunload-guard.js` — modernise: `var`→`const`/`let`, `function()`→arrow functions, `for` loop→`for…of` + +## Fix admin-filters wrapping +- [x] `.admin-list-toolbar .admin-filters` — restore `flex-wrap: wrap`, add `min-width: 0` +- [x] Search input: `flex: 1 1 10rem; min-width: 10rem` so it grows but doesn't collapse +- [x] Selects: `flex: 1 1 7rem; min-width: 7rem` for graceful wrap +- [x] Buttons: `flex-shrink: 0` so they never compress + +## Fix remote errors (2026-05-08) +- [x] `admin/index.php` — `fgetcsv()` enclosure `'\"'` (two chars) → `'"'` (single char); fatal on PHP 8.4 +- [x] `AdminLogger::write()` — guard `error_log()` with `is_writable()` check; silently skips file logging when `/var/log/xamxam.log` is not writable (DB mirror still runs) + ## Fix form field required states & missing fields per spec - [x] Admin add: add `contact_public` checkbox (matching edit form) - [x] Admin add + partage + admin edit: formats checkbox-list `$required = true` diff --git a/app/migrations/applied/015_license_custom.sql b/app/migrations/applied/015_license_custom.sql new file mode 100644 index 0000000..d11e1d9 --- /dev/null +++ b/app/migrations/applied/015_license_custom.sql @@ -0,0 +1 @@ +ALTER TABLE theses ADD COLUMN license_custom TEXT; diff --git a/app/migrations/run.php b/app/migrations/run.php index ac4efc0..b150e14 100644 --- a/app/migrations/run.php +++ b/app/migrations/run.php @@ -7,8 +7,8 @@ * * If no DB_PATH is given, defaults to storage/xamxam.db. * - * Each migration in migrations/pending/ is applied in alphabetical order. - * After success, the file is moved to migrations/applied/. + * Scans both migrations/pending/ and migrations/applied/ for .sql files. + * Each is applied in alphabetical order if not already tracked in _migrations. */ $root = dirname(__DIR__); @@ -34,17 +34,19 @@ $applied = $pdo->query("SELECT name FROM _migrations")->fetchAll(PDO::FETCH_COLU $pendingDir = $root . '/migrations/pending'; $appliedDir = $root . '/migrations/applied'; -if (!is_dir($pendingDir)) { - echo "No pending migrations directory.\n"; - exit(0); -} - if (!is_dir($appliedDir)) { mkdir($appliedDir, 0755, true); } -$files = glob($pendingDir . '/*.sql'); -sort($files); +// Collect .sql files from both pending and applied dirs +$files = []; +foreach ([$pendingDir, $appliedDir] as $dir) { + if (!is_dir($dir)) continue; + foreach (glob($dir . '/*.sql') as $f) { + $files[basename($f)] = $f; + } +} +ksort($files); if (empty($files)) { echo "No pending migration files.\n"; @@ -52,8 +54,7 @@ if (empty($files)) { } $count = 0; -foreach ($files as $file) { - $name = basename($file); +foreach ($files as $name => $file) { if (in_array($name, $applied, true)) { echo "Skip (already applied): $name\n"; @@ -63,21 +64,36 @@ foreach ($files as $file) { echo "Applying: $name\n"; $sql = file_get_contents($file); - try { - $pdo->exec($sql); - $pdo->prepare("INSERT INTO _migrations (name) VALUES (?)")->execute([$name]); - rename($file, $appliedDir . '/' . $name); - $count++; - } catch (PDOException $e) { - $msg = $e->getMessage(); - // SQLite: skip if column already exists - if (stripos($msg, 'duplicate column name') !== false) { - echo " Already exists (skipping): $name\n"; - $pdo->prepare("INSERT OR REPLACE INTO _migrations (name) VALUES (?)")->execute([$name]); - rename($file, $appliedDir . '/' . $name); - continue; + // Split into individual statements to handle partial failures gracefully + // (e.g. ALTER TABLE may fail with "duplicate column" but DROP VIEW must still run) + $statements = array_filter( + array_map('trim', explode(';', $sql)), + fn($s) => $s !== '' + ); + + $errors = []; + foreach ($statements as $stmt) { + try { + $pdo->exec($stmt . ';'); + } catch (PDOException $e) { + $msg = $e->getMessage(); + if (stripos($msg, 'duplicate column name') !== false) { + echo " Skipping (column exists): " . substr($stmt, 0, 60) . "...\n"; + continue; + } + $errors[] = $msg; } - throw $e; + } + + if (empty($errors)) { + $pdo->prepare("INSERT OR REPLACE INTO _migrations (name) VALUES (?)")->execute([$name]); + if (str_starts_with($file, $pendingDir)) { + rename($file, $appliedDir . '/' . $name); + } + $count++; + } else { + echo " FAILED: " . implode(' | ', $errors) . "\n"; + throw new PDOException(implode(' | ', $errors)); } } diff --git a/app/public/admin/index.php b/app/public/admin/index.php index c44734a..932d934 100644 --- a/app/public/admin/index.php +++ b/app/public/admin/index.php @@ -46,7 +46,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { 'autorisation', 'licence', 'license', 'taille', 'points', 'lien baiu', ]; for ($scan = 0; $scan < 8; $scan++) { - $hrow = fgetcsv($handle, 0, ',', '\"', ''); + $hrow = fgetcsv($handle, 0, ',', '"', ''); if ($hrow === false) break; $headerRowNum++; $normRow = array_map(fn($s) => strtolower(trim((string)$s)), $hrow); @@ -84,7 +84,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { $idPos = $colIdx['identifiant'] ?? 0; $peekRow = null; while (true) { - $peek = fgetcsv($handle, 0, ',', '\"', ''); + $peek = fgetcsv($handle, 0, ',', '"', ''); if ($peek === false) break; $headerRowNum++; $val = trim((string)($peek[$idPos] ?? '')); @@ -216,7 +216,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { $row = $peekRow; $usePeek = false; } else { - $row = fgetcsv($handle, 0, ',', '\"', ''); + $row = fgetcsv($handle, 0, ',', '"', ''); if ($row === false) break; } $lineNumber++; diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index 94fae24..d68267c 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -737,7 +737,22 @@ .admin-list-toolbar .admin-filters { flex: 1; margin-bottom: 0; - flex-wrap: nowrap; + flex-wrap: wrap; + min-width: 0; +} + +.admin-list-toolbar .admin-filters input[type="text"] { + min-width: 10rem; + flex: 1 1 10rem; +} + +.admin-list-toolbar .admin-filters select { + min-width: 7rem; + flex: 1 1 7rem; +} + +.admin-list-toolbar .admin-filters .btn { + flex-shrink: 0; } .admin-list-toolbar__right { diff --git a/app/public/assets/js/beforeunload-guard.js b/app/public/assets/js/beforeunload-guard.js index c123d18..6f908ec 100644 --- a/app/public/assets/js/beforeunload-guard.js +++ b/app/public/assets/js/beforeunload-guard.js @@ -4,20 +4,19 @@ * Attach to any form with a data-beforeunload-guard attribute. * No effect when JavaScript is unavailable (form posts normally). */ -(function () { - var forms = document.querySelectorAll('form[data-beforeunload-guard]'); +(() => { + const forms = document.querySelectorAll('form[data-beforeunload-guard]'); if (!forms.length) return; - var dirty = false; + let dirty = false; - for (var i = 0; i < forms.length; i++) { - var form = forms[i]; - form.addEventListener('input', function () { dirty = true; }); - form.addEventListener('change', function () { dirty = true; }); - form.addEventListener('submit', function () { dirty = false; }); + for (const form of forms) { + form.addEventListener('input', () => { dirty = true; }); + form.addEventListener('change', () => { dirty = true; }); + form.addEventListener('submit', () => { dirty = false; }); } - window.addEventListener('beforeunload', function (e) { + window.addEventListener('beforeunload', (e) => { if (dirty) { e.preventDefault(); } diff --git a/app/src/AdminLogger.php b/app/src/AdminLogger.php index 71e70c4..104fbc5 100644 --- a/app/src/AdminLogger.php +++ b/app/src/AdminLogger.php @@ -257,7 +257,9 @@ class AdminLogger } $line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; - error_log($line, 3, $this->logFile); + if (is_writable($this->logFile) || (!file_exists($this->logFile) && is_writable(dirname($this->logFile)))) { + error_log($line, 3, $this->logFile); + } if ($this->db !== null) { $this->insertDb($resource, $action, $status, $context); diff --git a/app/src/Controllers/ExportController.php b/app/src/Controllers/ExportController.php index 4f160af..e27f1e1 100644 --- a/app/src/Controllers/ExportController.php +++ b/app/src/Controllers/ExportController.php @@ -122,7 +122,7 @@ class ExportController */ public function createExportZip(?string $baseDir = null): string { - $baseDir = $baseDir ?? 'files'; + $baseDir ??= 'files'; $storageRoot = defined('STORAGE_ROOT') ? STORAGE_ROOT : APP_ROOT . '/storage'; $files = $this->getAllThesisFiles(); $manifest = $this->buildExportManifest(); diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index c03efa0..06fe049 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -196,7 +196,7 @@ class ThesisCreateController ]); $identifier = $this->db->getThesisIdentifier($thesisId); - error_log("ThesisCreateController: created thesis #$thesisId ($identifier) with " . count($authorEntries) . " author(s)"); + error_log("ThesisCreateController: created thesis #$thesisId ($identifier) with " . count($authorEntries) . ' author(s)'); $this->db->setThesisAuthors($thesisId, $authorEntries); $this->db->setThesisJury($thesisId, $data['juryMembers']); @@ -298,7 +298,7 @@ class ThesisCreateController $authorRaw = $this->sanitiseString($post['auteurice'] ?? ''); $authorNames = []; if ($authorRaw !== '') { - $authorNames = array_filter(array_map('trim', explode(',', $authorRaw)), fn($n) => $n !== ''); + $authorNames = array_filter(array_map('trim', explode(',', $authorRaw)), fn ($n) => $n !== ''); $authorNames = array_values($authorNames); sort($authorNames, SORT_NATURAL); } diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index 44eb34a..2e87940 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -165,46 +165,78 @@ class ThesisEditController // ── Basic validation (same required fields as create) ────────────────── $errors = []; $titre = trim($post['titre'] ?? ''); - if ($titre === '') $errors[] = 'Le titre est requis.'; + if ($titre === '') { + $errors[] = 'Le titre est requis.'; + } $auteurice = trim($post['auteurice'] ?? ''); - if ($auteurice === '') $errors[] = "L'auteur·ice est requis."; + if ($auteurice === '') { + $errors[] = "L'auteur·ice est requis."; + } $synopsis = trim($post['synopsis'] ?? ''); - if ($synopsis === '') $errors[] = 'Le synopsis est requis.'; + if ($synopsis === '') { + $errors[] = 'Le synopsis est requis.'; + } $annee = intval($post['année'] ?? 0); - if ($annee < 2000 || $annee > ((int)date('Y') + 1)) $errors[] = "L'année est invalide."; + if ($annee < 2000 || $annee > ((int)date('Y') + 1)) { + $errors[] = "L'année est invalide."; + } $orientationId = intval($post['orientation'] ?? 0); - if ($orientationId <= 0) $errors[] = "L'orientation est requise."; + if ($orientationId <= 0) { + $errors[] = "L'orientation est requise."; + } $apProgramId = intval($post['ap'] ?? 0); - if ($apProgramId <= 0) $errors[] = "L'atelier pluridisciplinaire est requis."; + if ($apProgramId <= 0) { + $errors[] = "L'atelier pluridisciplinaire est requis."; + } $finalityId = intval($post['finality'] ?? 0); - if ($finalityId <= 0) $errors[] = 'La finalité est requise.'; + if ($finalityId <= 0) { + $errors[] = 'La finalité est requise.'; + } // Languages $langIds = isset($post['languages']) && is_array($post['languages']) ? $post['languages'] : []; - if (empty($langIds)) $errors[] = 'Au moins une langue est requise.'; + if (empty($langIds)) { + $errors[] = 'Au moins une langue est requise.'; + } // Formats $fmtIds = isset($post['formats']) && is_array($post['formats']) ? $post['formats'] : []; - if (empty($fmtIds)) $errors[] = 'Au moins un format est requis.'; + if (empty($fmtIds)) { + $errors[] = 'Au moins un format est requis.'; + } // Licence $licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null; $licenseCustom = trim($post['license_custom'] ?? ''); - if (!$licenseId && $licenseCustom === '') $errors[] = 'Une licence est requise.'; + if (!$licenseId && $licenseCustom === '') { + $errors[] = 'Une licence est requise.'; + } // Jury $hasPromoteur = !empty(trim($post['jury_promoteur'] ?? '')); $hasLecteurInt = false; $hasLecteurExt = false; foreach ($post['jury_lecteur_interne'] ?? [] as $n) { - if (trim((string)$n) !== '') { $hasLecteurInt = true; break; } + if (trim((string)$n) !== '') { + $hasLecteurInt = true; + break; + } } foreach ($post['jury_lecteur_externe'] ?? [] as $n) { - if (trim((string)$n) !== '') { $hasLecteurExt = true; break; } + if (trim((string)$n) !== '') { + $hasLecteurExt = true; + break; + } + } + if (!$hasPromoteur) { + $errors[] = 'Un·e promoteur·ice interne est requis.'; + } + if (!$hasLecteurInt) { + $errors[] = 'Au moins un·e lecteur·ice interne est requis.'; + } + if (!$hasLecteurExt) { + $errors[] = 'Au moins un·e lecteur·ice externe est requis.'; } - if (!$hasPromoteur) $errors[] = 'Un·e promoteur·ice interne est requis.'; - if (!$hasLecteurInt) $errors[] = 'Au moins un·e lecteur·ice interne est requis.'; - if (!$hasLecteurExt) $errors[] = 'Au moins un·e lecteur·ice externe est requis.'; if (!empty($errors)) { throw new RuntimeException(implode(' ', $errors)); @@ -241,7 +273,7 @@ class ThesisEditController $showContact = !empty($post['contact_public']); $authorNames = []; if ($authorsRaw !== '') { - $authorNames = array_values(array_filter(array_map('trim', explode(',', $authorsRaw)), fn($n) => $n !== '')); + $authorNames = array_values(array_filter(array_map('trim', explode(',', $authorsRaw)), fn ($n) => $n !== '')); sort($authorNames, SORT_NATURAL); } $authorEntries = []; @@ -402,7 +434,7 @@ class ThesisEditController $authorName = trim($post['auteurice'] ?? 'unknown'); // Sort the raw comma-separated string alphabetically, then slugify. - $names = array_values(array_filter(array_map('trim', explode(',', $authorName)), fn($n) => $n !== '')); + $names = array_values(array_filter(array_map('trim', explode(',', $authorName)), fn ($n) => $n !== '')); sort($names, SORT_NATURAL); $authorSlug = $this->generateAuthorSlug(implode(', ', $names)); @@ -674,7 +706,7 @@ class ThesisEditController // Backwards compat: old jury_lecteurs[] if (isset($post['jury_lecteurs'])) { - foreach ($post['jury_lecteurs'] ?? [] as $i => $name) { + foreach ($post['jury_lecteurs'] as $i => $name) { $name = trim($name); if ($name !== '') { $members[] = [ diff --git a/app/src/Database.php b/app/src/Database.php index 6e77e74..71e46ab 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -1055,7 +1055,7 @@ class Database // Levenshtein distance ≤ 10 % of the longer string. // levenshtein() is limited to 255 chars; use substrings for safety. - $a = mb_substr($normNew, 0, 255); + $a = mb_substr($normNew, 0, 255); $b = mb_substr($normExisting, 0, 255); $dist = levenshtein($a, $b); $threshold = (int)ceil($maxLen * 0.10); diff --git a/app/templates/admin/contenus.php b/app/templates/admin/contenus.php index bf4281b..cf5fdac 100644 --- a/app/templates/admin/contenus.php +++ b/app/templates/admin/contenus.php @@ -105,7 +105,7 @@