Fix: email clearing in findOrCreateAuthor, htmlspecialchars(null) crash in old(), dead contact_interne field, access_type_id radio clearing

- findOrCreateAuthor: always update email column (pass null when empty/falsy) so clearing an email actually persists
- admin/add.php & admin/edit.php old(): add null guard before htmlspecialchars, cast to string
- jury-fieldset.php: guard against old() returning array for scalar-checked jury_lecteur keys
- formulaire.php: only suppress display_errors in production (not cli-server dev mode)
- Removed dead contact_interne field from backoffice form (no DB column, never saved)
- Removed dead contactInterne validation from ThesisCreateController
- Added "— Non défini" radio option for access_type_id in admin mode for clearing
- Fixed strict int-vs-string comparison breaking radio button checked detection
This commit is contained in:
Pontoporeia
2026-05-10 02:55:47 +02:00
parent 6cc0e407f3
commit 8a4b2541fb
10 changed files with 58 additions and 42 deletions

View File

@@ -69,3 +69,11 @@
- [x] Mots-clés: interactive tag search with HTMX suggestions, pill display, round bin-icon remove buttons
- [x] Mots-clés: lowercase enforcement, deduplication, absolute dropdown, keyboard arrows/enter/escape, blur hide, spacing + counter above input, CSV import lowercased, space-collapse normalization, minimum 3 keywords required
- [x] ErrorHandler: shared static helper for structured error_log + user-friendly messages with precise FK field extraction from SQLite errors. Applied to 12 action files + 6 public controllers + 2 form controllers + partage. Covers FK, UNIQUE, NOT NULL constraint types.
- [x] Fix: findOrCreateAuthor cannot clear email (empty string skips update, leaves old email)
- [ ] Fix: "NON" stored as literal email string in authors table (CSV import or old data)
- [x] Fix: contact_interne field in edit form never saved — removed dead field from form and dead validation from create controller
- [x] Fix: formulaire.php unconditionally suppresses display_errors even in dev mode
- [x] Fix: access_type_id radio has no "none" option — added "— Non défini" radio for admin mode
- [x] Fix: radio button checked detection broken (int vs string strict comparison in fieldset-licence-explanation.php)
- [x] Fix: htmlspecialchars(null) crash in old() on admin/add.php and admin/edit.php (null values in form data)
- [x] Fix: jury-fieldset.php old() return type confusion (array vs string) for jury_lecteur:_interne:_externe keys

View File

@@ -3,9 +3,12 @@
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', APP_ROOT . '/../error.log');
// Only suppress display_errors in production (cli-server = dev mode).
if (php_sapi_name() !== 'cli-server') {
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', APP_ROOT . '/../error.log');
}
AdminAuth::requireLogin();

View File

@@ -41,7 +41,8 @@ function old($key, $default = "") {
global $formData;
if (!isset($formData[$key])) return $default;
if (is_array($formData[$key])) return $formData[$key]; // Return raw array for callers that handle it
return htmlspecialchars($formData[$key]);
if ($formData[$key] === null) return $default;
return htmlspecialchars((string)$formData[$key]);
}
function wasSelected($key, $value) {

View File

@@ -25,7 +25,8 @@ function old($key, $default = "") {
global $formData;
if (!isset($formData[$key])) return $default;
if (is_array($formData[$key])) return $formData[$key]; // Return raw array for callers that handle it
return htmlspecialchars($formData[$key]);
if ($formData[$key] === null) return $default;
return htmlspecialchars((string)$formData[$key]);
}
try {

View File

@@ -507,15 +507,6 @@ class ThesisCreateController
}
}
// Contact interne (optional, admin-only)
$contactInterne = trim($post['contact_interne'] ?? '');
if ($contactInterne !== '') {
$contactInterne = filter_var($contactInterne, FILTER_VALIDATE_EMAIL);
if ($contactInterne === false) {
throw new Exception("L'adresse de contact interne n'est pas valide.");
}
}
// Note contextuelle (optional, max 1500 chars)
$contextNote = $this->sanitiseString($post['context_note'] ?? '');
if (strlen($contextNote) > 1500) {
@@ -539,7 +530,6 @@ class ThesisCreateController
'authorNames',
'mail',
'showContact',
'contactInterne',
'annee',
'orientationId',
'apProgramId',

View File

@@ -959,13 +959,9 @@ class Database
$author = $stmt->fetch();
if ($author) {
if ($email && $email !== '') {
$updateStmt = $this->pdo->prepare('UPDATE authors SET email = ?, show_contact = ? WHERE id = ?');
$updateStmt->execute([$email, $showContact ? 1 : 0, $author['id']]);
} else {
$updateStmt = $this->pdo->prepare('UPDATE authors SET show_contact = ? WHERE id = ?');
$updateStmt->execute([$showContact ? 1 : 0, $author['id']]);
}
// Always update email (may be null to clear) and show_contact.
$updateStmt = $this->pdo->prepare('UPDATE authors SET email = ?, show_contact = ? WHERE id = ?');
$updateStmt->execute([$email && $email !== '' ? $email : null, $showContact ? 1 : 0, $author['id']]);
return $author['id'];
}

View File

@@ -61,6 +61,19 @@
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: szktqmnn 29b3397f "Error tests, FK violations fix" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%% diff from: szktqmnn 29b3397f "Error tests, FK violations fix" (rebased revision)
\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\ to: vpwuyvyv 1573e164 "Fix: email clearing in findOrCreateAuthor, htmlspecialchars(null) crash in old(), dead contact_interne field, access_type_id radio clearing" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: vpwuyvyv f513921d "Fix: email clearing in findOrCreateAuthor, htmlspecialchars(null) crash in old(), dead contact_interne field, access_type_id radio clearing" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
?>
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">

View File

@@ -33,7 +33,24 @@ $adminMode = $adminMode ?? false;
<!-- Degré d'ouverture -->
<div class="licence-choice">
<p class="licence-prompt">J'autorise l'erg à archiver mon TFE de la manière suivante :</p>
<?php $selectedAccess = $formData['access_type_id'] ?? (string)$defaultAccessTypeId; ?>
<?php
// access_type_id may be null (meaning "not set"). Keep null to select "—" radio.
$selectedAccess = array_key_exists('access_type_id', $formData) ? $formData['access_type_id'] : $defaultAccessTypeId;
?>
<?php if ($adminMode): ?>
<div class="licence-degree">
<label class="admin-checkbox-label">
<input type="radio" name="access_type_id" value=""
hx-post="/admin/licence-fragment.php"
hx-target=".licence-license-choice"
hx-swap="outerHTML"
hx-include="closest fieldset"
<?= $selectedAccess === '' || $selectedAccess === null ? 'checked' : '' ?>>
<strong>—</strong> Non défini
</label>
</div>
<?php endif; ?>
<?php if ($libreEnabled): ?>
<div class="licence-degree">
@@ -43,7 +60,7 @@ $adminMode = $adminMode ?? false;
hx-target=".licence-license-choice"
hx-swap="outerHTML"
hx-include="closest fieldset"
<?= $selectedAccess === '1' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<?= (string)$selectedAccess === '1' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<strong>🔓 Libre</strong> — Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l'erg.
</label>
</div>
@@ -57,7 +74,7 @@ $adminMode = $adminMode ?? false;
hx-target=".licence-license-choice"
hx-swap="outerHTML"
hx-include="closest fieldset"
<?= $selectedAccess === '2' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<?= (string)$selectedAccess === '2' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<strong>🔒 Interne</strong> — Mon TFE n'est accessible que sur place en physique. Une note descriptive est disponible sur le site.
</label>
</div>
@@ -71,7 +88,7 @@ $adminMode = $adminMode ?? false;
hx-target=".licence-license-choice"
hx-swap="outerHTML"
hx-include="closest fieldset"
<?= $selectedAccess === '3' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<?= (string)$selectedAccess === '3' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<strong>🚫 Interdit</strong> — Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.
</label>
</div>

View File

@@ -511,20 +511,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<small>Case logistique : cocher si un exemplaire physique est disponible à l'ERG.</small>
</div>
<!-- 7. Contact interne -->
<div class="admin-form-group">
<label for="contact_interne">Contact interne :</label>
<input type="email" id="contact_interne" name="contact_interne"
value="<?= htmlspecialchars(
$currentRaw["contact_interne"] ??
($formData["contact_interne"] ??
($currentAuthorEmail ?? "")),
) ?>"
placeholder="ton.email@exemple.be">
<small>Adresse de contact interne (non visible publiquement).</small>
</div>
<!-- 8. Publication -->
<!-- 7. Publication -->
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="is_published" value="1"

View File

@@ -51,11 +51,11 @@ if ($addMode && function_exists('old')) {
}
for ($i = 0; $i < 10; $i++) {
$n = old("jury_lecteur_interne:$i");
if ($n !== '') $lecteursInternes[] = ['name' => $n];
if (is_string($n) && $n !== '') $lecteursInternes[] = ['name' => $n];
}
for ($i = 0; $i < 10; $i++) {
$n = old("jury_lecteur_externe:$i");
if ($n !== '') $lecteursExternes[] = ['name' => $n];
if (is_string($n) && $n !== '') $lecteursExternes[] = ['name' => $n];
}
}
?>