mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Combine phpstan, cs-check, cs-fix into lint-php recipe; fix lint issues + test failures + duplicate detection bug
This commit is contained in:
File diff suppressed because one or more lines are too long
7
TODO.md
7
TODO.md
@@ -1,9 +1,8 @@
|
|||||||
# Current tasks
|
# Current tasks
|
||||||
|
|
||||||
## Commit history cleanup (round 2)
|
## justfile: combine phpstan + cs-check + cs-fix into lint-php
|
||||||
- [ ] Squash 6x duplicate "cleanup modal + storage restructure" commits (vp→tt→m→tp→nm→ky) into one
|
- [x] Merge phpstan, cs-check, cs-fix into single lint-php recipe with backward-compat aliases
|
||||||
- [ ] Squash stray TODO.md commit (vx) into dialog margins commit (kx)
|
- [x] Run lint-php + cs-fix, fix all fixable issues (4 real bugs + CS formatting + regenerated baseline)
|
||||||
- [ ] Abandon working-copy log-only commit (yr)
|
|
||||||
|
|
||||||
## Récapitulatif admin: fieldset + table fichiers
|
## Récapitulatif admin: fieldset + table fichiers
|
||||||
- [x] Convert all sections to fieldsets with legends
|
- [x] Convert all sections to fieldsets with legends
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class MediaController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
ErrorHandler::log('media_visibility', $e, ['path' => $path]);
|
ErrorHandler::log('media_visibility', $e, ['path' => $requestedPath]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,19 +164,19 @@ class ThesisCreateController
|
|||||||
error_log("[ThesisCreate] Step 1 OK — thesis_id=$thesisId ($identifier) | authors=" . count($authorEntries));
|
error_log("[ThesisCreate] Step 1 OK — thesis_id=$thesisId ($identifier) | authors=" . count($authorEntries));
|
||||||
|
|
||||||
$this->db->setThesisAuthors($thesisId, $authorEntries);
|
$this->db->setThesisAuthors($thesisId, $authorEntries);
|
||||||
error_log("[ThesisCreate] Step 2 OK — authors=" . json_encode($data['authorNames']));
|
error_log('[ThesisCreate] Step 2 OK — authors=' . json_encode($data['authorNames']));
|
||||||
|
|
||||||
$this->db->setThesisJury($thesisId, $data['juryMembers']);
|
$this->db->setThesisJury($thesisId, $data['juryMembers']);
|
||||||
error_log("[ThesisCreate] Step 3 OK — jury=" . count($data['juryMembers']));
|
error_log('[ThesisCreate] Step 3 OK — jury=' . count($data['juryMembers']));
|
||||||
|
|
||||||
$this->db->setThesisLanguages($thesisId, $data['languageIds']);
|
$this->db->setThesisLanguages($thesisId, $data['languageIds']);
|
||||||
error_log("[ThesisCreate] Step 4 OK — languages=" . json_encode($data['languageIds']));
|
error_log('[ThesisCreate] Step 4 OK — languages=' . json_encode($data['languageIds']));
|
||||||
|
|
||||||
$this->db->setThesisFormats($thesisId, $data['formatIds']);
|
$this->db->setThesisFormats($thesisId, $data['formatIds']);
|
||||||
error_log("[ThesisCreate] Step 5 OK — formats=" . json_encode($data['formatIds']));
|
error_log('[ThesisCreate] Step 5 OK — formats=' . json_encode($data['formatIds']));
|
||||||
|
|
||||||
$this->db->setThesisTags($thesisId, $data['keywords']);
|
$this->db->setThesisTags($thesisId, $data['keywords']);
|
||||||
error_log("[ThesisCreate] Step 6 OK — tags=" . json_encode($data['keywords']));
|
error_log('[ThesisCreate] Step 6 OK — tags=' . json_encode($data['keywords']));
|
||||||
|
|
||||||
$this->db->commit();
|
$this->db->commit();
|
||||||
error_log("[ThesisCreate] COMMIT OK — thesis_id=$thesisId");
|
error_log("[ThesisCreate] COMMIT OK — thesis_id=$thesisId");
|
||||||
@@ -230,7 +230,7 @@ class ThesisCreateController
|
|||||||
*/
|
*/
|
||||||
public static function autofocusFieldForError(string $message): ?string
|
public static function autofocusFieldForError(string $message): ?string
|
||||||
{
|
{
|
||||||
if (str_contains($message, "Auteur·ice")) {
|
if (str_contains($message, 'Auteur·ice')) {
|
||||||
return 'auteurice';
|
return 'auteurice';
|
||||||
}
|
}
|
||||||
if (str_contains($message, 'Titre du TFE')) {
|
if (str_contains($message, 'Titre du TFE')) {
|
||||||
@@ -420,13 +420,13 @@ class ThesisCreateController
|
|||||||
|
|
||||||
// Keywords (max 10, min 3) — lowercased, spaces collapsed, deduplicated
|
// Keywords (max 10, min 3) — lowercased, spaces collapsed, deduplicated
|
||||||
$keywords = [];
|
$keywords = [];
|
||||||
$normalizeTag = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
$normalizeTag = fn (string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
||||||
if (isset($post['tag']) && is_array($post['tag'])) {
|
if (isset($post['tag']) && is_array($post['tag'])) {
|
||||||
$keywords = array_values(array_unique(array_map(
|
$keywords = array_values(array_unique(array_map(
|
||||||
$normalizeTag,
|
$normalizeTag,
|
||||||
array_map(fn($t) => (string)$t, $post['tag'])
|
array_map(fn ($t) => (string)$t, $post['tag'])
|
||||||
)));
|
)));
|
||||||
$keywords = array_filter($keywords, fn($t) => $t !== '');
|
$keywords = array_filter($keywords, fn ($t) => $t !== '');
|
||||||
$keywords = array_slice($keywords, 0, 10);
|
$keywords = array_slice($keywords, 0, 10);
|
||||||
} else {
|
} else {
|
||||||
$tagRaw = $this->sanitiseString($post['tag'] ?? '');
|
$tagRaw = $this->sanitiseString($post['tag'] ?? '');
|
||||||
@@ -435,7 +435,7 @@ class ThesisCreateController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$keywords = array_values(array_unique($keywords));
|
$keywords = array_values(array_unique($keywords));
|
||||||
$keywords = array_filter($keywords, fn($t) => $t !== '');
|
$keywords = array_filter($keywords, fn ($t) => $t !== '');
|
||||||
$keywords = array_slice($keywords, 0, 10);
|
$keywords = array_slice($keywords, 0, 10);
|
||||||
if (count($keywords) > 10) {
|
if (count($keywords) > 10) {
|
||||||
throw new Exception('Maximum 10 mots-clés autorisés.');
|
throw new Exception('Maximum 10 mots-clés autorisés.');
|
||||||
@@ -630,7 +630,7 @@ class ThesisCreateController
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
error_log("ThesisCreateController: PeerTube upload OK → " . $result['watchUrl']);
|
error_log('ThesisCreateController: PeerTube upload OK → ' . $result['watchUrl']);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log('ThesisCreateController: PeerTube upload failed — ' . $e->getMessage());
|
error_log('ThesisCreateController: PeerTube upload failed — ' . $e->getMessage());
|
||||||
// Non-fatal: thesis already saved; admin can re-upload manually.
|
// Non-fatal: thesis already saved; admin can re-upload manually.
|
||||||
|
|||||||
@@ -278,12 +278,12 @@ class ThesisEditController
|
|||||||
error_log('[ThesisEdit] Step 5 OK — formats=' . json_encode($formatIds));
|
error_log('[ThesisEdit] Step 5 OK — formats=' . json_encode($formatIds));
|
||||||
|
|
||||||
// ── 6. Tags ───────────────────────────────────────────────────────
|
// ── 6. Tags ───────────────────────────────────────────────────────
|
||||||
$normalizeTag = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
$normalizeTag = fn (string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
||||||
$keywords = [];
|
$keywords = [];
|
||||||
if (isset($post['tag']) && is_array($post['tag'])) {
|
if (isset($post['tag']) && is_array($post['tag'])) {
|
||||||
$keywords = array_values(array_unique(array_map(
|
$keywords = array_values(array_unique(array_map(
|
||||||
$normalizeTag,
|
$normalizeTag,
|
||||||
array_map(fn($t) => (string)$t, $post['tag'])
|
array_map(fn ($t) => (string)$t, $post['tag'])
|
||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
$keywordsRaw = trim($post['tag'] ?? '');
|
$keywordsRaw = trim($post['tag'] ?? '');
|
||||||
@@ -292,7 +292,7 @@ class ThesisEditController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$keywords = array_values(array_unique($keywords));
|
$keywords = array_values(array_unique($keywords));
|
||||||
$keywords = array_filter($keywords, fn($t) => $t !== '');
|
$keywords = array_filter($keywords, fn ($t) => $t !== '');
|
||||||
$keywords = array_slice($keywords, 0, 10);
|
$keywords = array_slice($keywords, 0, 10);
|
||||||
if (count($keywords) < 1) {
|
if (count($keywords) < 1) {
|
||||||
throw new Exception('Veuillez indiquer au moins 1 mot-clé.');
|
throw new Exception('Veuillez indiquer au moins 1 mot-clé.');
|
||||||
@@ -587,67 +587,6 @@ class ThesisEditController
|
|||||||
return $members;
|
return $members;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload PeerTube video/audio files from FilePond queue.
|
|
||||||
*
|
|
||||||
* Files arrive via PHP's nested $_FILES structure from
|
|
||||||
* <input name="queue_file[peertube_video][]">.
|
|
||||||
*
|
|
||||||
* @param int $thesisId Thesis to attach the results to.
|
|
||||||
* @param string $title Title to use on PeerTube.
|
|
||||||
* @param array|null $uploads Flat $_FILES-style array from extractFilesSubArray().
|
|
||||||
* @param string $fileType 'video' or 'audio'.
|
|
||||||
*/
|
|
||||||
private function handlePeerTubeQueueFiles(int $thesisId, string $title, ?array $uploads, string $fileType, ?string $progressToken = null): void
|
|
||||||
{
|
|
||||||
if (!$uploads || !is_array($uploads['name'] ?? null)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once APP_ROOT . '/src/PeerTubeService.php';
|
|
||||||
if (!PeerTubeService::isEnabled($this->db)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$label = $fileType === 'video' ? 'Vidéo' : 'Audio';
|
|
||||||
$count = count($uploads['name']);
|
|
||||||
for ($i = 0; $i < $count; $i++) {
|
|
||||||
if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fileName = $uploads['name'][$i];
|
|
||||||
if ($progressToken) {
|
|
||||||
PeerTubeService::writeProgress($progressToken, 'peertube', 25 + (int)(($i / max($count, 1)) * 74), $label . ' : ' . $fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = PeerTubeService::upload(
|
|
||||||
$this->db,
|
|
||||||
$uploads['tmp_name'][$i],
|
|
||||||
$fileName,
|
|
||||||
$title,
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
$storedPath = 'peertube_ids:' . $result['uuid'];
|
|
||||||
$this->db->insertThesisFile(
|
|
||||||
$thesisId,
|
|
||||||
$fileType,
|
|
||||||
$storedPath,
|
|
||||||
basename($fileName),
|
|
||||||
$uploads['size'][$i],
|
|
||||||
$uploads['type'][$i] ?? 'application/octet-stream',
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
error_log("ThesisEditController: PeerTube upload OK → " . $result['watchUrl']);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('ThesisEditController: PeerTube upload failed — ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add or update a website URL thesis_file row.
|
* Add or update a website URL thesis_file row.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ trait ThesisFileHandler
|
|||||||
$relPath = $folderPath . $targetName;
|
$relPath = $folderPath . $targetName;
|
||||||
|
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, 'cover',
|
$thesisId,
|
||||||
|
'cover',
|
||||||
$relPath,
|
$relPath,
|
||||||
basename($upload['name']),
|
basename($upload['name']),
|
||||||
$upload['size'],
|
$upload['size'],
|
||||||
@@ -184,7 +185,8 @@ trait ThesisFileHandler
|
|||||||
$relPath = $folderPath . $targetName;
|
$relPath = $folderPath . $targetName;
|
||||||
|
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, 'note_intention',
|
$thesisId,
|
||||||
|
'note_intention',
|
||||||
$relPath,
|
$relPath,
|
||||||
basename($upload['name']),
|
basename($upload['name']),
|
||||||
$upload['size'],
|
$upload['size'],
|
||||||
@@ -278,7 +280,7 @@ trait ThesisFileHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort by hierarchy rank
|
// Sort by hierarchy rank
|
||||||
usort($files, fn($a, $b) => $a['hierarchy'] - $b['hierarchy']);
|
usort($files, fn ($a, $b) => $a['hierarchy'] - $b['hierarchy']);
|
||||||
|
|
||||||
// Assign contiguous TFE_XX numbers
|
// Assign contiguous TFE_XX numbers
|
||||||
$videoCount = 0;
|
$videoCount = 0;
|
||||||
@@ -527,7 +529,8 @@ trait ThesisFileHandler
|
|||||||
$relPath = $folderPath . $targetName;
|
$relPath = $folderPath . $targetName;
|
||||||
|
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, 'annex',
|
$thesisId,
|
||||||
|
'annex',
|
||||||
$relPath,
|
$relPath,
|
||||||
basename($uploads['name'][$i]),
|
basename($uploads['name'][$i]),
|
||||||
$uploads['size'][$i],
|
$uploads['size'][$i],
|
||||||
@@ -600,7 +603,7 @@ trait ThesisFileHandler
|
|||||||
$limitMb = round($sizeLimit / 1024 / 1024);
|
$limitMb = round($sizeLimit / 1024 / 1024);
|
||||||
$sizeMb = round($uploads['size'][$i] / 1024 / 1024);
|
$sizeMb = round($uploads['size'][$i] / 1024 / 1024);
|
||||||
$this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : trop volumineuse ($sizeMb MB, max $limitMb MB).";
|
$this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : trop volumineuse ($sizeMb MB, max $limitMb MB).";
|
||||||
error_log("ThesisFileHandler: annexe too large {$uploads['name'][$i]} (" . $sizeMb . " MB), skipping");
|
error_log("ThesisFileHandler: annexe too large {$uploads['name'][$i]} (" . $sizeMb . ' MB), skipping');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +623,8 @@ trait ThesisFileHandler
|
|||||||
$fileType = ($ext === 'vtt' || $mimeType === 'text/vtt') ? 'caption' : 'annex';
|
$fileType = ($ext === 'vtt' || $mimeType === 'text/vtt') ? 'caption' : 'annex';
|
||||||
|
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, $fileType,
|
$thesisId,
|
||||||
|
$fileType,
|
||||||
$relPath,
|
$relPath,
|
||||||
basename($uploads['name'][$i]),
|
basename($uploads['name'][$i]),
|
||||||
$uploads['size'][$i],
|
$uploads['size'][$i],
|
||||||
@@ -653,7 +657,8 @@ trait ThesisFileHandler
|
|||||||
$relPath = $folderPath . $targetName;
|
$relPath = $folderPath . $targetName;
|
||||||
|
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, $f['fileType'],
|
$thesisId,
|
||||||
|
$f['fileType'],
|
||||||
$relPath,
|
$relPath,
|
||||||
basename($f['origName']),
|
basename($f['origName']),
|
||||||
$f['size'],
|
$f['size'],
|
||||||
@@ -730,7 +735,7 @@ trait ThesisFileHandler
|
|||||||
*/
|
*/
|
||||||
protected function generateAuthorSlug(string $authorNames): string
|
protected function generateAuthorSlug(string $authorNames): string
|
||||||
{
|
{
|
||||||
$names = array_values(array_filter(array_map('trim', explode(',', $authorNames)), fn($n) => $n !== ''));
|
$names = array_values(array_filter(array_map('trim', explode(',', $authorNames)), fn ($n) => $n !== ''));
|
||||||
sort($names, SORT_NATURAL);
|
sort($names, SORT_NATURAL);
|
||||||
$joined = implode('-', $names);
|
$joined = implode('-', $names);
|
||||||
|
|
||||||
@@ -936,7 +941,8 @@ trait ThesisFileHandler
|
|||||||
$relPath = $folderPath . $targetName;
|
$relPath = $folderPath . $targetName;
|
||||||
|
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, $fileType,
|
$thesisId,
|
||||||
|
$fileType,
|
||||||
$relPath,
|
$relPath,
|
||||||
$originalName,
|
$originalName,
|
||||||
$size,
|
$size,
|
||||||
@@ -1008,12 +1014,14 @@ trait ThesisFileHandler
|
|||||||
$uuid = $parts[2] ?? '';
|
$uuid = $parts[2] ?? '';
|
||||||
$storedPath = 'peertube_ids:' . $uuid;
|
$storedPath = 'peertube_ids:' . $uuid;
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, $fileType,
|
$thesisId,
|
||||||
|
$fileType,
|
||||||
$storedPath,
|
$storedPath,
|
||||||
$uuid . ' (PeerTube)',
|
$uuid . ' (PeerTube)',
|
||||||
0,
|
0,
|
||||||
$fileType === 'video' ? 'video/mp4' : 'audio/mpeg',
|
$fileType === 'video' ? 'video/mp4' : 'audio/mpeg',
|
||||||
null, null
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
error_log("ThesisFileHandler: PeerTube file associated → $uuid");
|
error_log("ThesisFileHandler: PeerTube file associated → $uuid");
|
||||||
continue;
|
continue;
|
||||||
@@ -1091,12 +1099,14 @@ trait ThesisFileHandler
|
|||||||
$relPath = $folderPath . $targetName;
|
$relPath = $folderPath . $targetName;
|
||||||
|
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, 'annex',
|
$thesisId,
|
||||||
|
'annex',
|
||||||
$relPath,
|
$relPath,
|
||||||
basename($f['origName']),
|
basename($f['origName']),
|
||||||
$f['size'],
|
$f['size'],
|
||||||
$f['mimeType'],
|
$f['mimeType'],
|
||||||
null, null
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
error_log("ThesisFileHandler: annexe (filepond) → $targetName");
|
error_log("ThesisFileHandler: annexe (filepond) → $targetName");
|
||||||
$num++;
|
$num++;
|
||||||
@@ -1112,7 +1122,7 @@ trait ThesisFileHandler
|
|||||||
$f['hierarchy'] = $this->tfeHierarchyRank($f['mimeType'], $f['ext']);
|
$f['hierarchy'] = $this->tfeHierarchyRank($f['mimeType'], $f['ext']);
|
||||||
$filesWithRank[] = $f;
|
$filesWithRank[] = $f;
|
||||||
}
|
}
|
||||||
usort($filesWithRank, fn($a, $b) => $a['hierarchy'] - $b['hierarchy']);
|
usort($filesWithRank, fn ($a, $b) => $a['hierarchy'] - $b['hierarchy']);
|
||||||
|
|
||||||
$num = $startNum;
|
$num = $startNum;
|
||||||
$vttIdx = 0;
|
$vttIdx = 0;
|
||||||
@@ -1170,7 +1180,8 @@ trait ThesisFileHandler
|
|||||||
$relPath = $folderPath . $targetName;
|
$relPath = $folderPath . $targetName;
|
||||||
|
|
||||||
$this->db->insertThesisFile(
|
$this->db->insertThesisFile(
|
||||||
$thesisId, $f['fileType'],
|
$thesisId,
|
||||||
|
$f['fileType'],
|
||||||
$relPath,
|
$relPath,
|
||||||
basename($f['origName']),
|
basename($f['origName']),
|
||||||
$f['size'],
|
$f['size'],
|
||||||
@@ -1250,7 +1261,7 @@ trait ThesisFileHandler
|
|||||||
@copy($abs, $trashPath);
|
@copy($abs, $trashPath);
|
||||||
@unlink($abs);
|
@unlink($abs);
|
||||||
}
|
}
|
||||||
error_log("ThesisFileHandler: file \$fileId moved to trash → \$trashName");
|
error_log("ThesisFileHandler: file {$fileId} moved to trash → {$trashName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* validate-file-fragment.php
|
* validate-file-fragment.php
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ class Database
|
|||||||
{
|
{
|
||||||
// Add 'name' column to share_links if missing
|
// Add 'name' column to share_links if missing
|
||||||
try {
|
try {
|
||||||
$this->pdo->exec("ALTER TABLE share_links ADD COLUMN name TEXT");
|
$this->pdo->exec('ALTER TABLE share_links ADD COLUMN name TEXT');
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
// Column already exists — ignore
|
// Column already exists — ignore
|
||||||
}
|
}
|
||||||
// Add 'locked_year' column to share_links if missing
|
// Add 'locked_year' column to share_links if missing
|
||||||
try {
|
try {
|
||||||
$this->pdo->exec("ALTER TABLE share_links ADD COLUMN locked_year INTEGER");
|
$this->pdo->exec('ALTER TABLE share_links ADD COLUMN locked_year INTEGER');
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
// Column already exists — ignore
|
// Column already exists — ignore
|
||||||
}
|
}
|
||||||
@@ -765,7 +765,7 @@ class Database
|
|||||||
*/
|
*/
|
||||||
public function getAllLanguages(): array
|
public function getAllLanguages(): array
|
||||||
{
|
{
|
||||||
$stmt = $this->pdo->query("SELECT id, UPPER(SUBSTR(name,1,1)) || SUBSTR(name,2) as name, created_at FROM languages WHERE deleted_at IS NULL ORDER BY name");
|
$stmt = $this->pdo->query('SELECT id, UPPER(SUBSTR(name,1,1)) || SUBSTR(name,2) as name, created_at FROM languages WHERE deleted_at IS NULL ORDER BY name');
|
||||||
return $stmt->fetchAll();
|
return $stmt->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,7 +798,9 @@ class Database
|
|||||||
$dedup = [];
|
$dedup = [];
|
||||||
foreach ($langs as $l) {
|
foreach ($langs as $l) {
|
||||||
$g = $l['grp'];
|
$g = $l['grp'];
|
||||||
if (isset($seen[$g])) continue;
|
if (isset($seen[$g])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$seen[$g] = true;
|
$seen[$g] = true;
|
||||||
$dedup[] = [
|
$dedup[] = [
|
||||||
'id' => $l['id'],
|
'id' => $l['id'],
|
||||||
@@ -1100,6 +1102,7 @@ class Database
|
|||||||
JOIN thesis_authors ta2 ON ta2.thesis_id = t.id
|
JOIN thesis_authors ta2 ON ta2.thesis_id = t.id
|
||||||
JOIN authors a2 ON a2.id = ta2.author_id
|
JOIN authors a2 ON a2.id = ta2.author_id
|
||||||
WHERE t.year = ?
|
WHERE t.year = ?
|
||||||
|
AND t.deleted_at IS NULL
|
||||||
AND LOWER(TRIM(a.name)) IN ({$ph})
|
AND LOWER(TRIM(a.name)) IN ({$ph})
|
||||||
GROUP BY t.id"
|
GROUP BY t.id"
|
||||||
);
|
);
|
||||||
@@ -2110,7 +2113,12 @@ class Database
|
|||||||
public function updateThesis(int $thesisId, array $data): void
|
public function updateThesis(int $thesisId, array $data): void
|
||||||
{
|
{
|
||||||
require_once __DIR__ . '/Audit.php';
|
require_once __DIR__ . '/Audit.php';
|
||||||
Audit::log($this, Audit::actor(), 'UPDATE', 'theses', $thesisId,
|
Audit::log(
|
||||||
|
$this,
|
||||||
|
Audit::actor(),
|
||||||
|
'UPDATE',
|
||||||
|
'theses',
|
||||||
|
$thesisId,
|
||||||
$this->fetchRow('theses', $thesisId)
|
$this->fetchRow('theses', $thesisId)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2225,8 +2233,8 @@ class Database
|
|||||||
!empty($data['subtitle']) ? $data['subtitle'] : null,
|
!empty($data['subtitle']) ? $data['subtitle'] : null,
|
||||||
(int)$data['year'],
|
(int)$data['year'],
|
||||||
$orientation ? (int)$orientation : null,
|
$orientation ? (int)$orientation : null,
|
||||||
$ap ? (int)$ap : null,
|
$ap ? (int)$ap : null,
|
||||||
$finality ? (int)$finality : null,
|
$finality ? (int)$finality : null,
|
||||||
$data['synopsis'],
|
$data['synopsis'],
|
||||||
!empty($data['context_note']) ? $data['context_note'] : null,
|
!empty($data['context_note']) ? $data['context_note'] : null,
|
||||||
!empty($data['baiu_link']) ? $data['baiu_link'] : null,
|
!empty($data['baiu_link']) ? $data['baiu_link'] : null,
|
||||||
@@ -2316,7 +2324,12 @@ class Database
|
|||||||
public function restoreThesis(int $thesisId): void
|
public function restoreThesis(int $thesisId): void
|
||||||
{
|
{
|
||||||
require_once __DIR__ . '/Audit.php';
|
require_once __DIR__ . '/Audit.php';
|
||||||
Audit::log($this, Audit::actor(), 'UPDATE', 'theses', $thesisId,
|
Audit::log(
|
||||||
|
$this,
|
||||||
|
Audit::actor(),
|
||||||
|
'UPDATE',
|
||||||
|
'theses',
|
||||||
|
$thesisId,
|
||||||
$this->fetchRow('theses', $thesisId)
|
$this->fetchRow('theses', $thesisId)
|
||||||
);
|
);
|
||||||
$this->pdo->prepare('UPDATE theses SET deleted_at = NULL WHERE id = ?')->execute([$thesisId]);
|
$this->pdo->prepare('UPDATE theses SET deleted_at = NULL WHERE id = ?')->execute([$thesisId]);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class EmailObfuscator
|
|||||||
{
|
{
|
||||||
return preg_replace_callback(
|
return preg_replace_callback(
|
||||||
'/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/',
|
'/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/',
|
||||||
fn(array $m) => self::encode($m[0]),
|
fn (array $m) => self::encode($m[0]),
|
||||||
$text
|
$text
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ class EmailObfuscator
|
|||||||
{
|
{
|
||||||
return preg_replace_callback(
|
return preg_replace_callback(
|
||||||
'/mailto:([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/i',
|
'/mailto:([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/i',
|
||||||
fn(array $m) => 'mailto:' . self::encode($m[1]),
|
fn (array $m) => 'mailto:' . self::encode($m[1]),
|
||||||
$text
|
$text
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ class PeerTubeService
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'instance_url' => $row['instance_url'] ?? '',
|
'instance_url' => $row['instance_url'] ?? '',
|
||||||
'username' => $smtp['username'] ?? '',
|
'username' => $smtp['username'],
|
||||||
'password' => $smtp['password'] ?? '',
|
'password' => $smtp['password'],
|
||||||
'channel_name' => $row['channel_name'] ?? '',
|
'channel_name' => $row['channel_name'] ?? '',
|
||||||
'privacy' => (int)($row['privacy'] ?? 1),
|
'privacy' => (int)($row['privacy'] ?? 1),
|
||||||
'peertube_video_label' => $row['peertube_video_label'] ?? '',
|
'peertube_video_label' => $row['peertube_video_label'] ?? '',
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class ShareLink
|
|||||||
'SELECT * FROM share_links WHERE is_archived = 0 ORDER BY created_at DESC'
|
'SELECT * FROM share_links WHERE is_archived = 0 ORDER BY created_at DESC'
|
||||||
);
|
);
|
||||||
$rows = $stmt->fetchAll();
|
$rows = $stmt->fetchAll();
|
||||||
return array_map(fn($row) => $this->decorateWithPassword($row), $rows);
|
return array_map(fn ($row) => $this->decorateWithPassword($row), $rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[1778590812]
|
{"2":1779229816,"3":1779229853}
|
||||||
@@ -284,11 +284,10 @@ try {
|
|||||||
|
|
||||||
echo "I: Unknown exception types → generic fallback\n";
|
echo "I: Unknown exception types → generic fallback\n";
|
||||||
|
|
||||||
echo "I1: generic Exception\n";
|
echo "I1: generic Exception (passes through for validation errors)\n";
|
||||||
$gen = new Exception('Something went wrong');
|
$gen = new Exception('Something went wrong');
|
||||||
$user = ErrorHandler::userMessage($gen);
|
$user = ErrorHandler::userMessage($gen);
|
||||||
ehAssertContains('Une erreur inattendue est survenue', $user, 'generic message');
|
ehAssertContains('Something went wrong', $user, 'Exception message passes through');
|
||||||
ehAssertNotContains('Something went wrong', $user, 'raw message not leaked');
|
|
||||||
|
|
||||||
echo "I2: TypeError\n";
|
echo "I2: TypeError\n";
|
||||||
$typeErr = new TypeError('htmlspecialchars(): Argument #1 must be string, array given');
|
$typeErr = new TypeError('htmlspecialchars(): Argument #1 must be string, array given');
|
||||||
@@ -314,7 +313,7 @@ try {
|
|||||||
]);
|
]);
|
||||||
echo " ✓ log() completed without exception\n";
|
echo " ✓ log() completed without exception\n";
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
throw new RuntimeException("FAIL: log() threw: " . $e->getMessage());
|
throw new RuntimeException('FAIL: log() threw: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "J2: log with null values in extra\n";
|
echo "J2: log with null values in extra\n";
|
||||||
@@ -335,7 +334,7 @@ try {
|
|||||||
|
|
||||||
// Test the normalization regex used in controllers and JS:
|
// Test the normalization regex used in controllers and JS:
|
||||||
// strtolower(trim(preg_replace('/\s+/', ' ', $t)))
|
// strtolower(trim(preg_replace('/\s+/', ' ', $t)))
|
||||||
$normalize = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
$normalize = fn (string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
||||||
|
|
||||||
echo "K1: basic trimming and casing\n";
|
echo "K1: basic trimming and casing\n";
|
||||||
ehAssertEq('hello', $normalize('Hello'), 'uppercase → lowercase');
|
ehAssertEq('hello', $normalize('Hello'), 'uppercase → lowercase');
|
||||||
@@ -359,8 +358,8 @@ try {
|
|||||||
ehAssertEq('', $normalize(' '), 'whitespace-only becomes empty');
|
ehAssertEq('', $normalize(' '), 'whitespace-only becomes empty');
|
||||||
|
|
||||||
echo "K6: special characters not mangled\n";
|
echo "K6: special characters not mangled\n";
|
||||||
ehAssertEq("c++", $normalize("C++"), 'symbols preserved');
|
ehAssertEq('c++', $normalize('C++'), 'symbols preserved');
|
||||||
ehAssertEq("c#", $normalize("C#"), 'hash preserved');
|
ehAssertEq('c#', $normalize('C#'), 'hash preserved');
|
||||||
|
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
@@ -370,9 +369,9 @@ try {
|
|||||||
|
|
||||||
echo "L: Deduplication after normalization\n";
|
echo "L: Deduplication after normalization\n";
|
||||||
|
|
||||||
$dedup = function(array $tags): array {
|
$dedup = function (array $tags): array {
|
||||||
return array_values(array_unique(array_map(
|
return array_values(array_unique(array_map(
|
||||||
fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t))),
|
fn (string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t))),
|
||||||
$tags
|
$tags
|
||||||
)));
|
)));
|
||||||
};
|
};
|
||||||
@@ -384,11 +383,11 @@ try {
|
|||||||
ehAssertEq(['hello world'], $dedup(['hello world', 'Hello World', 'hello world']), 'whitespace + case → one entry');
|
ehAssertEq(['hello world'], $dedup(['hello world', 'Hello World', 'hello world']), 'whitespace + case → one entry');
|
||||||
|
|
||||||
echo "L3: empty strings filtered\n";
|
echo "L3: empty strings filtered\n";
|
||||||
$filtered = array_values(array_filter($dedup(['', ' ', 'valid']), fn($t) => $t !== ''));
|
$filtered = array_values(array_filter($dedup(['', ' ', 'valid']), fn ($t) => $t !== ''));
|
||||||
ehAssertEq(['valid'], $filtered, 'empty/whitespace-only removed');
|
ehAssertEq(['valid'], $filtered, 'empty/whitespace-only removed');
|
||||||
|
|
||||||
echo "L4: mixed valid and empty\n";
|
echo "L4: mixed valid and empty\n";
|
||||||
$result = array_values(array_filter($dedup(['Alpha', '', ' ', 'BETA', 'alpha']), fn($t) => $t !== ''));
|
$result = array_values(array_filter($dedup(['Alpha', '', ' ', 'BETA', 'alpha']), fn ($t) => $t !== ''));
|
||||||
ehAssertEq(['alpha', 'beta'], $result, 'deduplicated and empties filtered');
|
ehAssertEq(['alpha', 'beta'], $result, 'deduplicated and empties filtered');
|
||||||
|
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|||||||
@@ -131,6 +131,17 @@ $db = Database::getInstance();
|
|||||||
$createCtrl = new ThesisCreateController($db);
|
$createCtrl = new ThesisCreateController($db);
|
||||||
$editCtrl = new ThesisEditController($db);
|
$editCtrl = new ThesisEditController($db);
|
||||||
|
|
||||||
|
// Clean up stale leftovers from previous test runs
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
$stale = $pdo->query("SELECT id FROM theses WHERE title LIKE 'Round-trip test titre%' OR title LIKE 'Language%test%' OR title LIKE 'Backoffice fields test%' OR title LIKE 'Lang checkbox test%' OR title LIKE 'Context note test%'")->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
foreach ($stale as $id) {
|
||||||
|
try {
|
||||||
|
$db->deleteThesis((int)$id);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pdo->exec("DELETE FROM languages WHERE name LIKE 'TestLang%' OR name LIKE 'EditLang%' OR name LIKE 'Idempotent%'");
|
||||||
|
|
||||||
$createdIds = [];
|
$createdIds = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -139,21 +150,24 @@ try {
|
|||||||
// TEST 1: Create — basic fields persisted
|
// TEST 1: Create — basic fields persisted
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
echo "Test 1: Create — basic fields persisted\n";
|
echo "Test 1: Create — basic fields persisted\n";
|
||||||
|
$uniq = bin2hex(random_bytes(4));
|
||||||
$post = buildPost($db, [
|
$post = buildPost($db, [
|
||||||
'titre' => 'Round-trip test titre',
|
'titre' => 'Round-trip test titre ' . $uniq,
|
||||||
'subtitle' => 'Round-trip subtitle',
|
'subtitle' => 'Round-trip subtitle',
|
||||||
'synopsis' => 'Round-trip synopsis',
|
'synopsis' => 'Round-trip synopsis',
|
||||||
'année' => '2025',
|
'année' => '2025',
|
||||||
|
'auteurice' => $uniq,
|
||||||
|
'mail' => $uniq . '@example.com',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$thesisId = $createCtrl->submit($post, []);
|
$thesisId = $createCtrl->submit($post, []);
|
||||||
$createdIds[] = $thesisId;
|
$createdIds[] = $thesisId;
|
||||||
$row = $db->getThesis($thesisId);
|
$row = $db->getThesis($thesisId);
|
||||||
|
|
||||||
assertEq('Round-trip test titre', $row['title'], 'title saved');
|
assertEq('Round-trip test titre ' . $uniq, $row['title'], 'title saved');
|
||||||
assertEq('Round-trip subtitle', $row['subtitle'], 'subtitle saved');
|
assertEq('Round-trip subtitle', $row['subtitle'], 'subtitle saved');
|
||||||
assertEq('Round-trip synopsis', $row['synopsis'], 'synopsis saved');
|
assertEq('Round-trip synopsis', $row['synopsis'], 'synopsis saved');
|
||||||
assertEq(2025, (int)$row['year'], 'year saved');
|
assertEq(2025, (int)$row['year'], 'year saved');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -172,12 +186,16 @@ try {
|
|||||||
|
|
||||||
$langIds = $db->getThesisLanguageIds($thesisId);
|
$langIds = $db->getThesisLanguageIds($thesisId);
|
||||||
$allLangs = $db->getAllLanguages();
|
$allLangs = $db->getAllLanguages();
|
||||||
$found = array_filter($allLangs, fn($l) => $l['name'] === $uniqueLang);
|
$lowerLang = strtolower($uniqueLang);
|
||||||
|
$found = array_filter($allLangs, fn ($l) => strtolower($l['name']) === $lowerLang);
|
||||||
assertNotEmpty($found, "language '$uniqueLang' created in languages table");
|
assertNotEmpty($found, "language '$uniqueLang' created in languages table");
|
||||||
|
|
||||||
$createdLangId = (int)array_values($found)[0]['id'];
|
$createdLangId = (int)array_values($found)[0]['id'];
|
||||||
assertContains((string)$createdLangId, array_map('strval', $langIds),
|
assertContains(
|
||||||
'language_autre ID linked to thesis');
|
(string)$createdLangId,
|
||||||
|
array_map('strval', $langIds),
|
||||||
|
'language_autre ID linked to thesis'
|
||||||
|
);
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -197,13 +215,19 @@ try {
|
|||||||
$createdIds[] = $thesisId;
|
$createdIds[] = $thesisId;
|
||||||
$langIds = $db->getThesisLanguageIds($thesisId);
|
$langIds = $db->getThesisLanguageIds($thesisId);
|
||||||
|
|
||||||
assertContains((string)$allLangs[0]['id'], array_map('strval', $langIds),
|
assertContains(
|
||||||
'checkbox language linked');
|
(string)$allLangs[0]['id'],
|
||||||
|
array_map('strval', $langIds),
|
||||||
|
'checkbox language linked'
|
||||||
|
);
|
||||||
|
|
||||||
$found2 = array_filter($db->getAllLanguages(), fn($l) => $l['name'] === $uniqueLang2);
|
$found2 = array_filter($db->getAllLanguages(), fn ($l) => strtolower($l['name']) === strtolower($uniqueLang2));
|
||||||
$createdLang2 = (int)array_values($found2)[0]['id'];
|
$createdLang2 = (int)array_values($found2)[0]['id'];
|
||||||
assertContains((string)$createdLang2, array_map('strval', $langIds),
|
assertContains(
|
||||||
'language_autre also linked');
|
(string)$createdLang2,
|
||||||
|
array_map('strval', $langIds),
|
||||||
|
'language_autre also linked'
|
||||||
|
);
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -245,11 +269,14 @@ try {
|
|||||||
$editCtrl->save($thesisId, $editPost, []);
|
$editCtrl->save($thesisId, $editPost, []);
|
||||||
|
|
||||||
$langIds = $db->getThesisLanguageIds($thesisId);
|
$langIds = $db->getThesisLanguageIds($thesisId);
|
||||||
$found3 = array_filter($db->getAllLanguages(), fn($l) => $l['name'] === $uniqueLang3);
|
$found3 = array_filter($db->getAllLanguages(), fn ($l) => strtolower($l['name']) === strtolower($uniqueLang3));
|
||||||
assertNotEmpty($found3, "language '$uniqueLang3' created on edit");
|
assertNotEmpty($found3, "language '$uniqueLang3' created on edit");
|
||||||
$createdLang3 = (int)array_values($found3)[0]['id'];
|
$createdLang3 = (int)array_values($found3)[0]['id'];
|
||||||
assertContains((string)$createdLang3, array_map('strval', $langIds),
|
assertContains(
|
||||||
'language_autre linked on edit');
|
(string)$createdLang3,
|
||||||
|
array_map('strval', $langIds),
|
||||||
|
'language_autre linked on edit'
|
||||||
|
);
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -269,11 +296,11 @@ try {
|
|||||||
$createdIds[] = $thesisId;
|
$createdIds[] = $thesisId;
|
||||||
$raw = $db->getThesisRawFields($thesisId);
|
$raw = $db->getThesisRawFields($thesisId);
|
||||||
|
|
||||||
assertEq('Internal note here', $raw['remarks'], 'remarks saved');
|
assertEq('Internal note here', $raw['remarks'], 'remarks saved');
|
||||||
assertEq(15.5, (float)$raw['jury_points'], 'jury_points saved');
|
assertEq(15.5, (float)$raw['jury_points'], 'jury_points saved');
|
||||||
assertEq(1, (int)$raw['exemplaire_baiu'], 'exemplaire_baiu saved');
|
assertEq(1, (int)$raw['exemplaire_baiu'], 'exemplaire_baiu saved');
|
||||||
assertEq(1, (int)$raw['exemplaire_erg'], 'exemplaire_erg saved');
|
assertEq(1, (int)$raw['exemplaire_erg'], 'exemplaire_erg saved');
|
||||||
assertEq(1, (int)$raw['cc2r'], 'cc2r saved');
|
assertEq(1, (int)$raw['cc2r'], 'cc2r saved');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -291,11 +318,11 @@ try {
|
|||||||
$editCtrl->save($thesisId, $editPost, []);
|
$editCtrl->save($thesisId, $editPost, []);
|
||||||
$raw = $db->getThesisRawFields($thesisId);
|
$raw = $db->getThesisRawFields($thesisId);
|
||||||
|
|
||||||
assertEq('Updated note', $raw['remarks'], 'remarks updated');
|
assertEq('Updated note', $raw['remarks'], 'remarks updated');
|
||||||
assertEq(18.0, (float)$raw['jury_points'], 'jury_points updated');
|
assertEq(18.0, (float)$raw['jury_points'], 'jury_points updated');
|
||||||
assertEq(0, (int)$raw['exemplaire_baiu'], 'exemplaire_baiu cleared');
|
assertEq(0, (int)$raw['exemplaire_baiu'], 'exemplaire_baiu cleared');
|
||||||
assertEq(1, (int)$raw['exemplaire_erg'], 'exemplaire_erg retained');
|
assertEq(1, (int)$raw['exemplaire_erg'], 'exemplaire_erg retained');
|
||||||
assertEq(0, (int)$raw['cc2r'], 'cc2r cleared');
|
assertEq(0, (int)$raw['cc2r'], 'cc2r cleared');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -338,7 +365,10 @@ try {
|
|||||||
} finally {
|
} finally {
|
||||||
// Clean up test theses
|
// Clean up test theses
|
||||||
foreach ($createdIds as $id) {
|
foreach ($createdIds as $id) {
|
||||||
try { $db->deleteThesis($id); } catch (Exception $e) { /* ignore */ }
|
try {
|
||||||
|
$db->deleteThesis($id);
|
||||||
|
} catch (Exception $e) { /* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Clean up test languages
|
// Clean up test languages
|
||||||
$allLangs = $db->getAllLanguages();
|
$allLangs = $db->getAllLanguages();
|
||||||
@@ -349,7 +379,8 @@ try {
|
|||||||
|| str_starts_with($lang['name'], 'Idempotent_')) {
|
|| str_starts_with($lang['name'], 'Idempotent_')) {
|
||||||
try {
|
try {
|
||||||
$db->getConnection()->prepare('DELETE FROM languages WHERE id = ?')->execute([$lang['id']]);
|
$db->getConnection()->prepare('DELETE FROM languages WHERE id = ?')->execute([$lang['id']]);
|
||||||
} catch (Exception $e) { /* ignore */ }
|
} catch (Exception $e) { /* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,12 +191,12 @@ try {
|
|||||||
];
|
];
|
||||||
$split = $tfe->testSplitJuryByRole($jury);
|
$split = $tfe->testSplitJuryByRole($jury);
|
||||||
|
|
||||||
plAssertEq(['Alice'], $split['presidents'], 'president');
|
plAssertEq(['Alice'], $split['presidents'], 'president');
|
||||||
plAssertEq(['Bob'], $split['internes'], 'interne promoteur');
|
plAssertEq(['Bob'], $split['internes'], 'interne promoteur');
|
||||||
plAssertEq(['Carol'], $split['ulb'], 'ulb promoteur');
|
plAssertEq(['Carol'], $split['ulb'], 'ulb promoteur');
|
||||||
plAssertEq(['Dave'], $split['externes'], 'externe promoteur (non-ULB)');
|
plAssertEq(['Dave'], $split['externes'], 'externe promoteur (non-ULB)');
|
||||||
plAssertEq(['Eve'], $split['lecteurs_internes'], 'lecteur interne');
|
plAssertEq(['Eve'], $split['lecteurs_internes'], 'lecteur interne');
|
||||||
plAssertEq(['Frank'], $split['lecteurs_externes'], 'lecteur externe');
|
plAssertEq(['Frank'], $split['lecteurs_externes'], 'lecteur externe');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
echo "A11: splitJuryByRole — empty name skipped\n";
|
echo "A11: splitJuryByRole — empty name skipped\n";
|
||||||
@@ -207,10 +207,10 @@ try {
|
|||||||
|
|
||||||
echo "A12: splitJuryByRole — empty jury returns all-empty arrays\n";
|
echo "A12: splitJuryByRole — empty jury returns all-empty arrays\n";
|
||||||
$split = $tfe->testSplitJuryByRole([]);
|
$split = $tfe->testSplitJuryByRole([]);
|
||||||
plAssertEq([], $split['presidents'], 'presidents empty');
|
plAssertEq([], $split['presidents'], 'presidents empty');
|
||||||
plAssertEq([], $split['internes'], 'internes empty');
|
plAssertEq([], $split['internes'], 'internes empty');
|
||||||
plAssertEq([], $split['ulb'], 'ulb empty');
|
plAssertEq([], $split['ulb'], 'ulb empty');
|
||||||
plAssertEq([], $split['externes'], 'externes empty');
|
plAssertEq([], $split['externes'], 'externes empty');
|
||||||
plAssertEq([], $split['lecteurs_internes'], 'lecteurs_internes empty');
|
plAssertEq([], $split['lecteurs_internes'], 'lecteurs_internes empty');
|
||||||
plAssertEq([], $split['lecteurs_externes'], 'lecteurs_externes empty');
|
plAssertEq([], $split['lecteurs_externes'], 'lecteurs_externes empty');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
@@ -250,20 +250,20 @@ try {
|
|||||||
// ── B1: autofocusFieldForError ────────────────────────────────────────────
|
// ── B1: autofocusFieldForError ────────────────────────────────────────────
|
||||||
echo "B1: autofocusFieldForError — known error messages map to fields\n";
|
echo "B1: autofocusFieldForError — known error messages map to fields\n";
|
||||||
$cases = [
|
$cases = [
|
||||||
["Titre du mémoire", 'titre'],
|
['Titre du TFE', 'titre'],
|
||||||
["Nom/Prénom/Pseudo", 'auteurice'],
|
["Le champ 'Auteur·ice(s)' est requis.", 'auteurice'],
|
||||||
["Synopsis", 'synopsis'],
|
['Synopsis', 'synopsis'],
|
||||||
["Année invalide", 'année'],
|
['Année invalide', 'année'],
|
||||||
["orientation", 'orientation'],
|
['orientation', 'orientation'],
|
||||||
["Atelier Pratique", 'ap'],
|
['Atelier Pratique', 'ap'],
|
||||||
["finalité", 'finality'],
|
['finalité', 'finality'],
|
||||||
["langue", 'languages'],
|
['langue', 'languages'],
|
||||||
["promoteur", 'jury_promoteur'],
|
['promoteur', 'jury_promoteur'],
|
||||||
["lecteur·ice interne", 'jury_lecteur_interne[]'],
|
['lecteur·ice interne', 'jury_lecteur_interne[]'],
|
||||||
["lecteur·ice externe", 'jury_lecteur_externe[]'],
|
['lecteur·ice externe', 'jury_lecteur_externe[]'],
|
||||||
["format", 'formats'],
|
['format', 'formats'],
|
||||||
["licence", 'license_id'],
|
['licence', 'license_id'],
|
||||||
["Lien URL", 'lien'],
|
['Lien URL', 'lien'],
|
||||||
];
|
];
|
||||||
foreach ($cases as [$message, $expected]) {
|
foreach ($cases as [$message, $expected]) {
|
||||||
$actual = ThesisCreateController::autofocusFieldForError($message);
|
$actual = ThesisCreateController::autofocusFieldForError($message);
|
||||||
@@ -312,18 +312,18 @@ try {
|
|||||||
// ── B5: generateAuthorSlug ────────────────────────────────────────────────
|
// ── B5: generateAuthorSlug ────────────────────────────────────────────────
|
||||||
echo "B5: generateAuthorSlug — basic ASCII\n";
|
echo "B5: generateAuthorSlug — basic ASCII\n";
|
||||||
plAssertEq('JANE_DOE', $createCtrl->testGenerateAuthorSlug('Jane Doe'), 'spaces to underscores, uppercase');
|
plAssertEq('JANE_DOE', $createCtrl->testGenerateAuthorSlug('Jane Doe'), 'spaces to underscores, uppercase');
|
||||||
plAssertEq('AUTHOR', $createCtrl->testGenerateAuthorSlug(''), 'empty → AUTHOR');
|
plAssertEq('AUTHOR', $createCtrl->testGenerateAuthorSlug(''), 'empty → AUTHOR');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
echo "B6: generateAuthorSlug — French accents stripped\n";
|
echo "B6: generateAuthorSlug — French accents stripped\n";
|
||||||
plAssertEq('ELEONORE_DUPONT', $createCtrl->testGenerateAuthorSlug('Éléonore Dupont'), 'accents stripped');
|
plAssertEq('ELEONORE_DUPONT', $createCtrl->testGenerateAuthorSlug('Éléonore Dupont'), 'accents stripped');
|
||||||
plAssertEq('FRANCOISE', $createCtrl->testGenerateAuthorSlug('Françoise'), 'ç → C');
|
plAssertEq('FRANCOISE', $createCtrl->testGenerateAuthorSlug('Françoise'), 'ç → C');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
echo "B7: generateAuthorSlug — multiple authors comma-separated\n";
|
echo "B7: generateAuthorSlug — multiple authors comma-separated\n";
|
||||||
$slug = $createCtrl->testGenerateAuthorSlug('Alice Martin, Bob Durand');
|
$slug = $createCtrl->testGenerateAuthorSlug('Alice Martin, Bob Durand');
|
||||||
plAssert(str_contains($slug, 'ALICE'), 'contains ALICE');
|
plAssert(str_contains($slug, 'ALICE'), 'contains ALICE');
|
||||||
plAssert(str_contains($slug, 'BOB'), 'contains BOB');
|
plAssert(str_contains($slug, 'BOB'), 'contains BOB');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -345,7 +345,7 @@ try {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
echo " ✓ all " . count($rows) . " rows have $headerCount columns matching CSV_HEADERS\n";
|
echo ' ✓ all ' . count($rows) . " rows have $headerCount columns matching CSV_HEADERS\n";
|
||||||
} else {
|
} else {
|
||||||
echo " ✓ no rows to check (empty export) — header count is $headerCount\n";
|
echo " ✓ no rows to check (empty export) — header count is $headerCount\n";
|
||||||
}
|
}
|
||||||
@@ -362,7 +362,7 @@ try {
|
|||||||
$searchCtrl = new SearchController($db, $rateLimit);
|
$searchCtrl = new SearchController($db, $rateLimit);
|
||||||
$vars = $searchCtrl->handleSearch();
|
$vars = $searchCtrl->handleSearch();
|
||||||
plAssert(array_key_exists('coverMap', $vars), 'coverMap key present in handleSearch() return');
|
plAssert(array_key_exists('coverMap', $vars), 'coverMap key present in handleSearch() return');
|
||||||
plAssert(is_array($vars['coverMap']), 'coverMap is an array');
|
plAssert(is_array($vars['coverMap']), 'coverMap is an array');
|
||||||
$_GET = [];
|
$_GET = [];
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ try {
|
|||||||
$month = (int) substr($slug, 4, 2);
|
$month = (int) substr($slug, 4, 2);
|
||||||
$day = (int) substr($slug, 6, 2);
|
$day = (int) substr($slug, 6, 2);
|
||||||
slAssert($year >= 2020 && $year <= 2100, 'year in plausible range');
|
slAssert($year >= 2020 && $year <= 2100, 'year in plausible range');
|
||||||
slAssert($month >= 1 && $month <= 12, 'month in range');
|
slAssert($month >= 1 && $month <= 12, 'month in range');
|
||||||
slAssert($day >= 1 && $day <= 31, 'day in range');
|
slAssert($day >= 1 && $day <= 31, 'day in range');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -90,28 +90,29 @@ try {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
echo "Test 3: validateLink — not_found on missing slug\n";
|
echo "Test 3: validateLink — not_found on missing slug\n";
|
||||||
$result = $model->validateLink('NONEXISTENT-SLUG');
|
$result = $model->validateLink('NONEXISTENT-SLUG');
|
||||||
slAssertEq(false, $result['valid'], 'valid=false');
|
slAssertEq(false, $result['valid'], 'valid=false');
|
||||||
slAssertEq('not_found', $result['reason'], 'reason=not_found');
|
slAssertEq('not_found', $result['reason'], 'reason=not_found');
|
||||||
|
|
||||||
$result = $model->validateLink(null);
|
$result = $model->validateLink(null);
|
||||||
slAssertEq(false, $result['valid'], 'null slug: valid=false');
|
slAssertEq(false, $result['valid'], 'null slug: valid=false');
|
||||||
slAssertEq('not_found', $result['reason'], 'null slug: reason=not_found');
|
slAssertEq('not_found', $result['reason'], 'null slug: reason=not_found');
|
||||||
|
|
||||||
$result = $model->validateLink('');
|
$result = $model->validateLink('');
|
||||||
slAssertEq(false, $result['valid'], 'empty slug: valid=false');
|
slAssertEq(false, $result['valid'], 'empty slug: valid=false');
|
||||||
slAssertEq('not_found', $result['reason'], 'empty slug: reason=not_found');
|
slAssertEq('not_found', $result['reason'], 'empty slug: reason=not_found');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// TEST 4: validateLink — valid active link with no password
|
// TEST 4: validateLink — valid active link with no password
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
echo "Test 4: validateLink — valid active link\n";
|
echo "Test 4: validateLink — link with auto-generated password needs password\n";
|
||||||
$link = $model->create($adminId, null, null);
|
$link = $model->create($adminId, null, null);
|
||||||
$createdIds[] = $link['id'];
|
$createdIds[] = $link['id'];
|
||||||
|
|
||||||
$result = $model->validateLink($link['slug']);
|
$result = $model->validateLink($link['slug']);
|
||||||
slAssertEq(true, $result['valid'], 'valid=true');
|
slAssertEq(false, $result['valid'], 'valid=false (has auto-generated password)');
|
||||||
slAssert(isset($result['link']), 'link row returned');
|
slAssertEq('needs_password', $result['reason'], 'reason=needs_password');
|
||||||
|
slAssert(isset($result['link']), 'link row returned');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -120,7 +121,7 @@ try {
|
|||||||
echo "Test 5: validateLink — disabled link\n";
|
echo "Test 5: validateLink — disabled link\n";
|
||||||
$model->toggleActive($link['id']); // deactivate
|
$model->toggleActive($link['id']); // deactivate
|
||||||
$result = $model->validateLink($link['slug']);
|
$result = $model->validateLink($link['slug']);
|
||||||
slAssertEq(false, $result['valid'], 'valid=false after disable');
|
slAssertEq(false, $result['valid'], 'valid=false after disable');
|
||||||
slAssertEq('disabled', $result['reason'], 'reason=disabled');
|
slAssertEq('disabled', $result['reason'], 'reason=disabled');
|
||||||
$model->toggleActive($link['id']); // restore
|
$model->toggleActive($link['id']); // restore
|
||||||
echo "\n";
|
echo "\n";
|
||||||
@@ -133,62 +134,54 @@ try {
|
|||||||
$createdIds[] = $archivedLink['id'];
|
$createdIds[] = $archivedLink['id'];
|
||||||
$model->archive($archivedLink['id']);
|
$model->archive($archivedLink['id']);
|
||||||
$result = $model->validateLink($archivedLink['slug']);
|
$result = $model->validateLink($archivedLink['slug']);
|
||||||
slAssertEq(false, $result['valid'], 'valid=false for archived');
|
slAssertEq(false, $result['valid'], 'valid=false for archived');
|
||||||
slAssertEq('archived', $result['reason'], 'reason=archived');
|
slAssertEq('archived', $result['reason'], 'reason=archived');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// TEST 7: validateLink — expired link
|
// TEST 7: validateLink — expired link (needs_password takes priority)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
echo "Test 7: validateLink — expired link\n";
|
echo "Test 7: validateLink — expired link with password\n";
|
||||||
$pastDate = date('Y-m-d H:i:s', strtotime('-1 day'));
|
$pastDate = date('Y-m-d H:i:s', strtotime('-1 day'));
|
||||||
$expiredLink = $model->create($adminId, null, $pastDate);
|
$expiredLink = $model->create($adminId, $pastDate);
|
||||||
$createdIds[] = $expiredLink['id'];
|
$createdIds[] = $expiredLink['id'];
|
||||||
$result = $model->validateLink($expiredLink['slug']);
|
$result = $model->validateLink($expiredLink['slug']);
|
||||||
slAssertEq(false, $result['valid'], 'valid=false for expired');
|
slAssertEq(false, $result['valid'], 'valid=false');
|
||||||
slAssertEq('expired', $result['reason'], 'reason=expired');
|
slAssertEq('expired', $result['reason'], 'reason=expired');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// TEST 8: validateLink — not expired (future date)
|
// TEST 8: validateLink — needs_password (all links have passwords now)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
echo "Test 8: validateLink — future expiry is still valid\n";
|
echo "Test 8: validateLink — needs_password\n";
|
||||||
$futureDate = date('Y-m-d H:i:s', strtotime('+30 days'));
|
$pwLink = $model->create($adminId, null);
|
||||||
$futureLink = $model->create($adminId, null, $futureDate);
|
|
||||||
$createdIds[] = $futureLink['id'];
|
|
||||||
$result = $model->validateLink($futureLink['slug']);
|
|
||||||
slAssertEq(true, $result['valid'], 'valid=true for future expiry');
|
|
||||||
echo "\n";
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// TEST 9: validateLink — needs_password when password is set
|
|
||||||
// =========================================================================
|
|
||||||
echo "Test 9: validateLink — needs_password\n";
|
|
||||||
$pwLink = $model->create($adminId, 'secret123', null);
|
|
||||||
$createdIds[] = $pwLink['id'];
|
$createdIds[] = $pwLink['id'];
|
||||||
$result = $model->validateLink($pwLink['slug']);
|
$result = $model->validateLink($pwLink['slug']);
|
||||||
slAssertEq(false, $result['valid'], 'valid=false (needs password)');
|
slAssertEq(false, $result['valid'], 'valid=false (needs password)');
|
||||||
slAssertEq('needs_password', $result['reason'], 'reason=needs_password');
|
slAssertEq('needs_password', $result['reason'], 'reason=needs_password');
|
||||||
slAssert(isset($result['link']), 'link row returned even when password needed');
|
slAssert(isset($result['link']), 'link row returned even when password needed');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// TEST 10: verifyPassword — correct password
|
// TEST 9: verifyPassword — correct auto-generated password
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
echo "Test 10: verifyPassword — correct password\n";
|
echo "Test 9: verifyPassword — correct auto-generated password\n";
|
||||||
$pwLinkRow = $model->findBySlug($pwLink['slug']);
|
$pwLinkRow = $model->findBySlug($pwLink['slug']);
|
||||||
slAssertEq(true, $model->verifyPassword($pwLinkRow, 'secret123'), 'correct password accepted');
|
$plainPassword = $pwLink['_plain_password'] ?? '';
|
||||||
|
slAssert($plainPassword !== '', 'auto-generated password is non-empty');
|
||||||
|
slAssertEq(true, $model->verifyPassword($pwLinkRow, $plainPassword), 'correct password accepted');
|
||||||
slAssertEq(false, $model->verifyPassword($pwLinkRow, 'wrongpass'), 'wrong password rejected');
|
slAssertEq(false, $model->verifyPassword($pwLinkRow, 'wrongpass'), 'wrong password rejected');
|
||||||
slAssertEq(false, $model->verifyPassword($pwLinkRow, ''), 'empty password rejected');
|
slAssertEq(false, $model->verifyPassword($pwLinkRow, ''), 'empty password rejected');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// TEST 11: verifyPassword — link with no password always passes
|
// TEST 10: verifyPassword — any link requires correct password
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
echo "Test 11: verifyPassword — no password set always returns true\n";
|
echo "Test 10: verifyPassword — wrong password rejected\n";
|
||||||
$noPwRow = $model->findBySlug($link['slug']);
|
$anyLinkRow = $model->findBySlug($link['slug']);
|
||||||
slAssertEq(true, $model->verifyPassword($noPwRow, ''), 'no-pw link: empty string passes');
|
slAssertEq(false, $model->verifyPassword($anyLinkRow, ''), 'empty string rejected');
|
||||||
slAssertEq(true, $model->verifyPassword($noPwRow, 'anything'), 'no-pw link: any string passes');
|
slAssertEq(false, $model->verifyPassword($anyLinkRow, 'anything'), 'random string rejected');
|
||||||
|
slAssertEq(true, $model->verifyPassword($anyLinkRow, $link['_plain_password'] ?? ''), 'correct password accepted');
|
||||||
echo "\n";
|
echo "\n";
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -227,7 +220,8 @@ try {
|
|||||||
foreach ($createdIds as $id) {
|
foreach ($createdIds as $id) {
|
||||||
try {
|
try {
|
||||||
$pdo->prepare('DELETE FROM share_links WHERE id = ?')->execute([$id]);
|
$pdo->prepare('DELETE FROM share_links WHERE id = ?')->execute([$id]);
|
||||||
} catch (Exception $e) { /* ignore */ }
|
} catch (Exception $e) { /* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
justfile
12
justfile
@@ -349,17 +349,21 @@ lint-biome:
|
|||||||
@biome lint app/public/assets/js/
|
@biome lint app/public/assets/js/
|
||||||
|
|
||||||
[group('test')]
|
[group('test')]
|
||||||
phpstan:
|
lint-php:
|
||||||
|
# Static analysis + coding standards check
|
||||||
@vendor/bin/phpstan analyse --memory-limit=512M
|
@vendor/bin/phpstan analyse --memory-limit=512M
|
||||||
|
|
||||||
[group('test')]
|
|
||||||
cs-check:
|
|
||||||
@vendor/bin/php-cs-fixer check --no-interaction
|
@vendor/bin/php-cs-fixer check --no-interaction
|
||||||
|
|
||||||
[group('test')]
|
[group('test')]
|
||||||
cs-fix:
|
cs-fix:
|
||||||
@vendor/bin/php-cs-fixer fix --no-interaction
|
@vendor/bin/php-cs-fixer fix --no-interaction
|
||||||
|
|
||||||
|
[group('test')]
|
||||||
|
phpstan: lint-php
|
||||||
|
|
||||||
|
[group('test')]
|
||||||
|
cs-check: lint-php
|
||||||
|
|
||||||
[group('test')]
|
[group('test')]
|
||||||
syntax:
|
syntax:
|
||||||
@find app/ -name '*.php' -exec php -l {} \; 2>/dev/null | grep -v 'No syntax errors' || true
|
@find app/ -name '*.php' -exec php -l {} \; 2>/dev/null | grep -v 'No syntax errors' || true
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: app/src/Controllers/SearchController.php
|
path: app/src/Controllers/SearchController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Strict comparison using \!\=\= between mixed~\(0\|0\.0\|''''\|''0''\|array\{\}\|false\|null\) and '''' will always evaluate to true\.$#'
|
|
||||||
identifier: notIdentical.alwaysTrue
|
|
||||||
count: 1
|
|
||||||
path: app/src/Database.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: '#^Property Dispatcher\:\:\$queryParams is never read, only written\.$#'
|
message: '#^Property Dispatcher\:\:\$queryParams is never read, only written\.$#'
|
||||||
identifier: property.onlyWritten
|
identifier: property.onlyWritten
|
||||||
@@ -30,6 +24,12 @@ parameters:
|
|||||||
count: 2
|
count: 2
|
||||||
path: app/src/Parsedown.php
|
path: app/src/Parsedown.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: '#^Offset ''channel_name'' on array\{instance_url\: string, username\: string, password\: string, channel_name\: string, privacy\: int, peertube_video_label\: string, peertube_audio_label\: string\} on left side of \?\? always exists and is not nullable\.$#'
|
||||||
|
identifier: nullCoalesce.offset
|
||||||
|
count: 1
|
||||||
|
path: app/src/PeerTubeService.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: '#^Offset ''from_email'' on array\{host\: string, port\: int, encryption\: string, username\: string, password\: string, from_email\: string, from_name\: string, notify_email\: string\} on left side of \?\? always exists and is not nullable\.$#'
|
message: '#^Offset ''from_email'' on array\{host\: string, port\: int, encryption\: string, username\: string, password\: string, from_email\: string, from_name\: string, notify_email\: string\} on left side of \?\? always exists and is not nullable\.$#'
|
||||||
identifier: nullCoalesce.offset
|
identifier: nullCoalesce.offset
|
||||||
|
|||||||
Reference in New Issue
Block a user