From a6df3c8c0e345ad34cc8b2aa2ce50d23c039e302 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 16 Apr 2026 12:00:28 +0200 Subject: [PATCH] fix: /partage/ routing (regex delimiter + nginx location) --- TODO.md | 36 +-- config/router.php | 26 ++ justfile | 2 +- nginx/posterg.conf | 5 + public/admin/acces-etudiante.php | 201 +++++++++++++++ ...student-access.php => acces-etudiante.php} | 18 +- public/admin/student-access.php | 244 ------------------ public/partage/index.php | 2 +- src/ShareLink.php | 6 +- storage/posterg.db | Bin 237568 -> 253952 bytes templates/header.php | 2 +- 11 files changed, 254 insertions(+), 288 deletions(-) create mode 100644 config/router.php create mode 100644 public/admin/acces-etudiante.php rename public/admin/actions/{student-access.php => acces-etudiante.php} (67%) delete mode 100644 public/admin/student-access.php diff --git a/TODO.md b/TODO.md index 379b2ab..42fd016 100644 --- a/TODO.md +++ b/TODO.md @@ -1,33 +1,7 @@ # TODO -- [x] Make thanks.php respect student mode (no header, centered "add new form" button) - - [x] Add hidden input `student_mode` in add.php form when in student mode - - [x] Append `mode=student` to thanks redirect in formulaire.php - - [x] Update thanks.php to detect student mode, hide header, show centered button -- [x] Cleanup public/admin/add.php — standardise fieldsets and add licence explanation sections from docs PDF - - [x] Organise all fields into `
/` blocks: Informations du TFE, Composition du jury, Cadre académique, Fichiers, Métadonnées complémentaires - - [x] Remove double-wrapping of jury-fieldset (it has its own `
`) - - [x] Add "Degrés d'ouverture et licences" section (Libre / Interne / Interdit + Généralités) wrapped in `if ($studentMode)` — hidden in admin - -- [x] Migrate student mode form to shareable links system (/partage/) - - [x] Create `share_links` database table (id, slug YYYYMMDD-random, password_hash, is_active, usage_count, created_by, created_at, expires_at nullable) - - [x] Create `ShareLink` model — generate slugs, validate, verify password, CRUD - - [x] Create `public/partage/index.php` — public form page (no auth required, validates link active + password if set) - - [x] Create `public/partage/.htaccess` — RewriteRule to route all partage paths to index.php - - [x] Create `public/partage/thanks.php` — post-submission confirmation page - - [x] Move student-specific licence explanation fieldset to partage form template - - [x] Share-link specific CSRF token (session-scoped `share_csrf_`) instead of session CSRF - -- [x] Create admin page for managing student access links - - [x] Create `public/admin/student-access.php` — "Accès étudiant·e" page - - [x] Link to new page from admin navigation - - [x] Implement list view of all share links with status (active/disabled, password set, usage count, created date) - - [x] Implement create new link modal/form (optional expiration, password) - - [x] Implement toggle active/disabled status per link - - [x] Implement password set/change/clear per link - - [x] Implement delete link action - - [x] Copy-to-clipboard button for full partage URL - -- [x] Security and validation considerations - - [x] Rate limiting on form submissions per share link — integrate RateLimit into partage index.php POST handler - - [x] Add flash messages / error handling for invalid/disabled/password-protected links — replace plain die() with styled error pages and flash messages +## Completed +- [x] Fix share link slug regex mismatch (base64 chars vs base32 pattern) +- [x] Fix regex delimiter clash (`/` inside `[...]` broke the pattern) → switched to `#` delimiter +- [x] Add PHP dev server router for /partage/ URL rewriting +- [x] Add nginx location block for /partage/ pretty URLs diff --git a/config/router.php b/config/router.php new file mode 100644 index 0000000..148ace7 --- /dev/null +++ b/config/router.php @@ -0,0 +1,26 @@ + to public/partage/index.php, since the built-in + * server has no URL rewriting like nginx's try_files. + */ + +$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + +// Route /partage/ and /partage// to the partage entry +if (preg_match('#^/partage(/.*)?$#', $uri)) { + $_SERVER['SCRIPT_NAME'] = '/partage/index.php'; + require __DIR__ . '/../public/partage/index.php'; + return true; +} + +// Route /tfe/<...> to tfe.php +if (preg_match('#^/tfe(/.*)?$#', $uri)) { + $_SERVER['SCRIPT_NAME'] = '/tfe.php'; + require __DIR__ . '/../public/tfe.php'; + return true; +} + +// Default: serve static files if they exist +return false; diff --git a/justfile b/justfile index 97172ab..227f8c3 100644 --- a/justfile +++ b/justfile @@ -13,7 +13,7 @@ setup: [group('dev')] serve: migrate - @php -S 127.0.0.1:8000 -t public/ 2>&1 | stdbuf -oL grep -E '(Development Server|\[200\])' | stdbuf -oL grep -v 'live-reload\.php' || true + @php -S 127.0.0.1:8000 -t public/ config/router.php 2>&1 | stdbuf -oL grep -E '(Development Server|\[200\])' | stdbuf -oL grep -v 'live-reload\.php' || true [group('dev')] stop: diff --git a/nginx/posterg.conf b/nginx/posterg.conf index bcdc030..899be36 100644 --- a/nginx/posterg.conf +++ b/nginx/posterg.conf @@ -151,6 +151,11 @@ server { try_files $uri $uri/ =404; } + # Share-link (partage) — rewrite pretty URLs to index.php + location /partage/ { + try_files $uri /partage/index.php$is_args$args; + } + # Search endpoint - rate limiting location = /search.php { limit_req zone=search burst=10 nodelay; diff --git a/public/admin/acces-etudiante.php b/public/admin/acces-etudiante.php new file mode 100644 index 0000000..af4c91a --- /dev/null +++ b/public/admin/acces-etudiante.php @@ -0,0 +1,201 @@ +listAll(); + +$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; +$baseUrl = $protocol . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost'); +$pageTitle = 'Accès étudiant·e'; +$isAdmin = true; +$bodyClass = 'admin-body'; +?> + + + +
+ + +
+

Accès étudiant·e

+
+ +
+
+ + +

Aucun lien d'accès créé. Cliquez sur « Créer un lien » pour générer un lien partageable.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
LienStatutMot de passeUtilisationsExpirationCréé leActions
+ + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + +
+
+
+ +
+ + + +
+

Créer un lien d'accès

+ +
+
+ + +
+ + + Laissez vide pour un lien sans mot de passe. +
+
+ + + Laissez vide pour qu'il n'expire jamais. +
+ +
+
+ + + +
+

Mot de passe

+ +
+
+ + + +
+ + + Laissez vide pour supprimer le mot de passe. +

+
+ +
+
+ + + + diff --git a/public/admin/actions/student-access.php b/public/admin/actions/acces-etudiante.php similarity index 67% rename from public/admin/actions/student-access.php rename to public/admin/actions/acces-etudiante.php index a6095e4..0323ff3 100644 --- a/public/admin/actions/student-access.php +++ b/public/admin/actions/acces-etudiante.php @@ -28,19 +28,19 @@ switch ($action) { // datetime-local gives "YYYY-MM-DDTHH:MM" $expiresAt = date('Y-m-d H:i:s', strtotime($expiresRaw)); if ($expiresAt <= date('Y-m-d H:i:s')) { - App::redirect('/admin/student-access.php', error: "La date d'expiration doit être dans le futur."); + App::redirect('/admin/acces-etudiante.php', error: "La date d'expiration doit être dans le futur."); } } $shareLink->create(1, $password, $expiresAt); - App::redirect('/admin/student-access.php', success: 'Lien d\'accès créé.'); + App::redirect('/admin/acces-etudiante.php', success: 'Lien d\'accès créé.'); break; case 'toggle': if ($id > 0) { $shareLink->toggleActive($id); - App::redirect('/admin/student-access.php', success: 'Statut du lien modifié.'); + App::redirect('/admin/acces-etudiante.php', success: 'Statut du lien modifié.'); } else { - App::redirect('/admin/student-access.php', error: 'Lien introuvable.'); + App::redirect('/admin/acces-etudiante.php', error: 'Lien introuvable.'); } break; @@ -48,22 +48,22 @@ switch ($action) { if ($id > 0) { $password = isset($_POST['password']) && $_POST['password'] !== '' ? trim($_POST['password']) : null; $shareLink->setPassword($id, $password); - App::redirect('/admin/student-access.php', success: 'Mot de passe mis à jour.'); + App::redirect('/admin/acces-etudiante.php', success: 'Mot de passe mis à jour.'); } else { - App::redirect('/admin/student-access.php', error: 'Lien introuvable.'); + App::redirect('/admin/acces-etudiante.php', error: 'Lien introuvable.'); } break; case 'delete': if ($id > 0) { $shareLink->delete($id); - App::redirect('/admin/student-access.php', success: 'Lien supprimé.'); + App::redirect('/admin/acces-etudiante.php', success: 'Lien supprimé.'); } else { - App::redirect('/admin/student-access.php', error: 'Lien introuvable.'); + App::redirect('/admin/acces-etudiante.php', error: 'Lien introuvable.'); } break; default: - App::redirect('/admin/student-access.php', error: 'Action inconnue.'); + App::redirect('/admin/acces-etudiante.php', error: 'Action inconnue.'); break; } diff --git a/public/admin/student-access.php b/public/admin/student-access.php deleted file mode 100644 index 8196418..0000000 --- a/public/admin/student-access.php +++ /dev/null @@ -1,244 +0,0 @@ -listAll(); -$flash = App::consumeFlash(); - -$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; -$baseUrl = $protocol . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost'); -$pageTitle = 'Accès étudiant·e'; -$isAdmin = true; -$bodyClass = 'admin-body'; -?> - - - - - -
- -
- - -
- - -
-

Accès étudiant·e

- -
- - -
- -

Aucun lien d'accès créé.

-

Cliquez sur « Créer un lien » pour générer un lien partageable.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
LienStatutMot de passeUtilisationsExpirationCréé leActions
- - - -
- -
- - - - -
- -
- - - - -
-
-
- -
- - - -
-

Créer un lien d'accès

- -
-
- - -
- - -
- -
-
- - - -
-

Mot de passe

- -
-
- - - -
- -

-
- -
-
- - - - diff --git a/public/partage/index.php b/public/partage/index.php index 8ff726b..ca882fa 100644 --- a/public/partage/index.php +++ b/public/partage/index.php @@ -22,7 +22,7 @@ $slug = $parts[0] ?? ''; $action = $parts[1] ?? ''; // Validate slug format: YYYYMMDD-XXXXXXXX (17 chars) -if (!preg_match('/^\d{8}-[A-Z2-7]{8}$/', $slug)) { +if (!preg_match('#^\d{8}-[A-Z0-9+/]{8}$#', $slug)) { App::boot(); $_SESSION['_flash_error'] = 'Ce lien de partage n\'est pas valide.'; header('Location: /'); diff --git a/src/ShareLink.php b/src/ShareLink.php index ad41abe..f7c33b6 100644 --- a/src/ShareLink.php +++ b/src/ShareLink.php @@ -30,7 +30,11 @@ class ShareLink public static function generateSlug(): string { $date = date('Ymd'); - $random = substr(strtoupper(rtrim(base64_encode(random_bytes(7)), '=')), 0, 8); + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + $random = ''; + for ($i = 0; $i < 8; $i++) { + $random .= $chars[random_int(0, strlen($chars) - 1)]; + } return $date . '-' . $random; } diff --git a/storage/posterg.db b/storage/posterg.db index ef76e47a6a7f87d99670f2ab3d6b1b712a38ef24..e9afeee88469b6656af47ee8eddcac70f15f895a 100644 GIT binary patch delta 1123 zcmaJ=K}-`t6y538LW}Ik0Sgv&OfOVhvfBk>p%lyM>)ygcCHV z2V6EiN@I$n_`HQZ1QL4zxU?<|7L4X*xG}S z16?tW;}Y!qyToc(2C>@hr_ghm{p&rqeE{tP2)rx6XP?*tH32-RR_38pYU{363id0}_1fzKpj!E5;{^X4 z*!yvpu;0tS<-DB13H_Z~r|h2KKX9MvuV@<=ywQ0_bw&c{J|i2aMr-tB566Ng$qrw z(L?ggXck$REvb~MWsRaW6(ggdQsj)OuAS2fHb`E{Zm{WOQrA;+ zC_S4&X?EqKN#v2S!0riKwoHR+~c1LT3zFOZ(ob&q!xd~1NW(}HN22nG6qDNdtFna@K49;r wc??c_p__wW;Cr|WA3_t(!#M1N?po!C(BtQ2a15*0?o;^h6FXnUou8-v0QU$|xc~qF delta 257 zcmZoTz~69yZ-TU-1p@&$j6t225H!-2b>kxTU!g8FJZ0 zfyy(suP|U*%gMx)yjf5no@u&mKcnsT&o)eapT&Wea4;WX;P>O_=1b($;LYPTW^rIX z0#vt#dHWG#=DA$kL#&zRGfMD+RPr@5@So(L&7a9{&o9dNh;JKTGf-tVAFCk;v$OGZ ug?MJ^?Th`G6B$J~_+=RQ@A4nwU(DabU&>Pages statiques
  • >Mots-clés
  • >Système
  • -
  • >Accès étudiant·e
  • +
  • >Accès étudiant·e
  • >Paramètres
  • >Modifier