mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-27 00:59:18 +02:00
feat: fix file deletion on save + trash policy + documents/ prefix + relink browser
1. note_intention: Delete old file only when a genuinely new upload arrives
(32-char hex file_id), not when the FilePond pool preserves an existing
file by sending its DB integer ID. Previously the DB integer ID
triggered $hasNewNote=true, which deleted the existing note_intention
from disk+DB, then handleFilePondSingleFile couldn't re-process it
because the regex requires a hex pattern. Same fix applied to cover.
2. All file deletions now use deleteThesisFileToTrash() which renames
files to tmp/_trash/ instead of unlinking. The trash preserves
original filenames prefixed with DB id for traceability. Skips
website URLs and PeerTube refs (no disk file).
3. Storage prefix changed from theses/ to documents/ to reflect that
the folder holds all document types (determined by file_type in DB).
MediaController visibility gate supports both prefixes for backward
compat with existing files.
4. File browser + relink feature for orphaned files:
- /admin/fragments/file-browser.php — HTMX tree browser for
storage/documents/ and storage/theses/
- /admin/actions/filepond/relink.php — POST endpoint that inserts
a thesis_files row pointing to existing on-disk file
- Per-pool "📂 Relier" buttons (edit mode only)
- JS: XamxamOpenFileBrowser / XamxamRelinkFile with FilePond integration
- CSS: .relink-modal dialog + .file-browser tree styles
This commit is contained in:
@@ -47,6 +47,12 @@ class Database
|
||||
} catch (\PDOException $e) {
|
||||
// Column already exists — ignore
|
||||
}
|
||||
// Add 'locked_year' column to share_links if missing
|
||||
try {
|
||||
$this->pdo->exec("ALTER TABLE share_links ADD COLUMN locked_year INTEGER");
|
||||
} catch (\PDOException $e) {
|
||||
// Column already exists — ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1013,19 +1019,39 @@ class Database
|
||||
$email = null;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('SELECT id FROM authors WHERE name = ?');
|
||||
$cleanEmail = ($email !== null && $email !== '') ? $email : null;
|
||||
|
||||
// Try to find by name first
|
||||
$stmt = $this->pdo->prepare('SELECT id, email FROM authors WHERE name = ?');
|
||||
$stmt->execute([$name]);
|
||||
$author = $stmt->fetch();
|
||||
|
||||
if ($author) {
|
||||
// Always update email (may be null to clear) and show_contact.
|
||||
// Update email and show_contact unless that email belongs to another author.
|
||||
if ($cleanEmail !== null) {
|
||||
$dup = $this->pdo->prepare('SELECT id FROM authors WHERE email = ? AND id != ?');
|
||||
$dup->execute([$cleanEmail, $author['id']]);
|
||||
if ($dup->fetch()) {
|
||||
$cleanEmail = null; // don't steal another author's email
|
||||
}
|
||||
}
|
||||
$updateStmt = $this->pdo->prepare('UPDATE authors SET email = ?, show_contact = ? WHERE id = ?');
|
||||
$updateStmt->execute([$email && $email !== '' ? $email : null, $showContact ? 1 : 0, $author['id']]);
|
||||
$updateStmt->execute([$cleanEmail, $showContact ? 1 : 0, $author['id']]);
|
||||
return $author['id'];
|
||||
}
|
||||
|
||||
// If an author with this email already exists (different name), reuse that record.
|
||||
if ($cleanEmail !== null) {
|
||||
$byEmail = $this->pdo->prepare('SELECT id FROM authors WHERE email = ?');
|
||||
$byEmail->execute([$cleanEmail]);
|
||||
$existing = $byEmail->fetch();
|
||||
if ($existing) {
|
||||
return $existing['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('INSERT INTO authors (name, email, show_contact) VALUES (?, ?, ?)');
|
||||
$stmt->execute([$name, $email, $showContact ? 1 : 0]);
|
||||
$stmt->execute([$name, $cleanEmail, $showContact ? 1 : 0]);
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
@@ -1194,6 +1220,67 @@ class Database
|
||||
// TAG MANAGEMENT (admin)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Search supervisors by name prefix. Returns up to 10 matching supervisors.
|
||||
* If $query is empty, returns the most-used ones (up to 10).
|
||||
*
|
||||
* @param string $role Optional role filter: 'promoteur_interne' (is_external=0, role=promoteur),
|
||||
* 'promoteur_externe' (is_external=1, role=promoteur),
|
||||
* 'lecteur_interne' (is_external=0, role=lecteur),
|
||||
* 'lecteur_externe' (is_external=1, role=lecteur)
|
||||
*/
|
||||
public function searchSupervisors(string $query = '', string $role = ''): array
|
||||
{
|
||||
$query = trim($query);
|
||||
|
||||
// Map role to WHERE conditions
|
||||
$roleWhere = '';
|
||||
$roleParams = [];
|
||||
switch ($role) {
|
||||
case 'promoteur_interne':
|
||||
$roleWhere = 'AND ts.is_external = 0 AND ts.role = \'promoteur\'';
|
||||
break;
|
||||
case 'promoteur_externe':
|
||||
$roleWhere = 'AND ts.is_external = 1 AND ts.role = \'promoteur\'';
|
||||
break;
|
||||
case 'lecteur_interne':
|
||||
$roleWhere = 'AND ts.is_external = 0 AND ts.role = \'lecteur\'';
|
||||
break;
|
||||
case 'lecteur_externe':
|
||||
$roleWhere = 'AND ts.is_external = 1 AND ts.role = \'lecteur\'';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if ($query === '') {
|
||||
$stmt = $this->pdo->query('
|
||||
SELECT s.id, s.name,
|
||||
COUNT(DISTINCT ts.thesis_id) as thesis_count
|
||||
FROM supervisors s
|
||||
LEFT JOIN thesis_supervisors ts ON s.id = ts.supervisor_id
|
||||
' . $roleWhere . '
|
||||
GROUP BY s.id
|
||||
ORDER BY thesis_count DESC, s.name COLLATE NOCASE
|
||||
LIMIT 10
|
||||
');
|
||||
} else {
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT s.id, s.name,
|
||||
COUNT(DISTINCT ts.thesis_id) as thesis_count
|
||||
FROM supervisors s
|
||||
LEFT JOIN thesis_supervisors ts ON s.id = ts.supervisor_id
|
||||
WHERE s.name LIKE ?
|
||||
' . $roleWhere . '
|
||||
GROUP BY s.id
|
||||
ORDER BY s.name = ? DESC, thesis_count DESC, s.name COLLATE NOCASE
|
||||
LIMIT 10
|
||||
');
|
||||
$stmt->execute([$query . '%', $query]);
|
||||
}
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search tags by name prefix. Returns up to 10 matching tags.
|
||||
* If $query is empty, returns the most-used tags (up to 10).
|
||||
@@ -1794,13 +1881,11 @@ class Database
|
||||
}
|
||||
|
||||
// 2. Accent-tolerant fallback: strip accents and re-compare.
|
||||
// iconv 'ASCII//TRANSLIT' turns é→e, ç→c, etc.
|
||||
$asciiName = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
|
||||
if ($asciiName !== false && $asciiName !== $name) {
|
||||
$asciiName = self::stripAccents($name);
|
||||
if ($asciiName !== $name) {
|
||||
$all = $this->pdo->query('SELECT id, name FROM languages WHERE deleted_at IS NULL')->fetchAll();
|
||||
foreach ($all as $row) {
|
||||
$rowAscii = @iconv('UTF-8', 'ASCII//TRANSLIT', strtolower($row['name']));
|
||||
if ($rowAscii !== false && $rowAscii === $asciiName) {
|
||||
if (self::stripAccents(strtolower($row['name'])) === $asciiName) {
|
||||
return (int)$row['id'];
|
||||
}
|
||||
}
|
||||
@@ -1952,7 +2037,7 @@ class Database
|
||||
}
|
||||
|
||||
/**
|
||||
* Check visibility for a file path under theses/.
|
||||
* Check visibility for a file path under documents/ or theses/.
|
||||
* Returns the access_type_id of the owning thesis, or null if the file
|
||||
* is not found or the path does not belong to a thesis file.
|
||||
*
|
||||
@@ -3071,4 +3156,50 @@ class Database
|
||||
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip accents from a UTF-8 string (é→e, ç→c, etc.).
|
||||
* Pure PHP fallback when iconv extension is not available.
|
||||
*/
|
||||
private static function stripAccents(string $str): string
|
||||
{
|
||||
if (function_exists('iconv')) {
|
||||
$result = @iconv('UTF-8', 'ASCII//TRANSLIT', $str);
|
||||
if ($result !== false) {
|
||||
return strtolower($result);
|
||||
}
|
||||
}
|
||||
|
||||
// Manual transliteration table for common accented chars
|
||||
static $map = null;
|
||||
if ($map === null) {
|
||||
$utf8 = [
|
||||
'À','Á','Â','Ã','Ä','Å','Æ','à','á','â','ã','ä','å','æ',
|
||||
'Ç','ç',
|
||||
'È','É','Ê','Ë','è','é','ê','ë',
|
||||
'Ì','Í','Î','Ï','ì','í','î','ï',
|
||||
'Ð','ð',
|
||||
'Ñ','ñ',
|
||||
'Ò','Ó','Ô','Õ','Ö','Ø','ò','ó','ô','õ','ö','ø',
|
||||
'Ù','Ú','Û','Ü','ù','ú','û','ü',
|
||||
'Ý','ý','ÿ',
|
||||
'Š','š','Ž','ž','Þ','þ','Œ','œ','ß',
|
||||
];
|
||||
$ascii = [
|
||||
'A','A','A','A','A','A','AE','a','a','a','a','a','a','ae',
|
||||
'C','c',
|
||||
'E','E','E','E','e','e','e','e',
|
||||
'I','I','I','I','i','i','i','i',
|
||||
'D','d',
|
||||
'N','n',
|
||||
'O','O','O','O','O','O','o','o','o','o','o','o',
|
||||
'U','U','U','U','u','u','u','u',
|
||||
'Y','y','y',
|
||||
'S','s','Z','z','TH','th','OE','oe','ss',
|
||||
];
|
||||
$map = array_combine($utf8, $ascii);
|
||||
}
|
||||
|
||||
return strtolower(strtr($str, $map));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user