Fix issues with nginx access to pages

- fix: 403 on /language-autre-fragment.php — add explicit nginx location block

  The nginx catch-all  blocked direct access
  to all PHP files except /index.php and files inside /admin/.

  language-autre-fragment.php lives at the public root and is POSTed to by
  HTMX from both the admin edit form and the partage form. Added an explicit
   fastcgi block so it is executed
  rather than denied.

- fix: replace .php-suffixed public URLs blocked by nginx catch-all

  Audit of all client-facing PHP URL references against nginx routing:

  - fetch('/request-access.php') in tfe.php -> '/request-access'
    (clean URL already routed by Dispatcher)
  - /media.php?path= in form.php (x2) and admin/recapitulatif.php -> /media?path=
    (nginx only has location = /media, no location for /media.php)

  All these .php-suffixed URLs hit the nginx catch-all
    location ~ \.php$ { deny all; }
  which takes precedence over location / { try_files ... } for regex matches.
This commit is contained in:
Pontoporeia
2026-05-08 11:30:02 +02:00
parent 6ba13e00ea
commit 5735ccbc38
8 changed files with 77 additions and 51 deletions

12
TODO.md
View File

@@ -21,6 +21,18 @@
- [x] `duration_pages`/`duration_minutes` saved in `updateThesis()` and read back in `getThesisRawFields()` - [x] `duration_pages`/`duration_minutes` saved in `updateThesis()` and read back in `getThesisRawFields()`
- [x] `beforeunload-guard` applied to add and partage forms too - [x] `beforeunload-guard` applied to add and partage forms too
- [x] Audit + fix direct PHP URL references blocked by nginx catch-all `deny all`
- [x] `/request-access.php` fetch in `tfe.php``/request-access`
- [x] `/media.php?path=` in `form.php` (×2) and `admin/recapitulatif.php``/media?path=`
- [x] Fix 403 on `/language-autre-fragment.php` from `edit.php`
- [x] Root cause: standalone root-level PHP file blocked by nginx catch-all `deny all`
- [x] Moved logic to `partage/language-autre-fragment.php` (shared include)
- [x] Added route `/partage/language-autre-fragment` in `partage/index.php`
- [x] Added `admin/language-autre-fragment.php` (AdminAuth gated, includes shared logic)
- [x] `form.php` picks URL based on `$mode` (`partage` vs admin)
- [x] Deleted `public/language-autre-fragment.php`; nginx unchanged
- [x] Merge banner images into cover images - [x] Merge banner images into cover images
- [x] Migration 016: copy `storage/banners/*``storage/covers/`, insert `thesis_files` cover records, clear `banner_path`, remove banners dir - [x] Migration 016: copy `storage/banners/*``storage/covers/`, insert `thesis_files` cover records, clear `banner_path`, remove banners dir
- [x] Remove banner fieldset from edit form (`form.php`) - [x] Remove banner fieldset from edit form (`form.php`)

View File

@@ -0,0 +1,17 @@
<?php
/**
* language-autre-fragment.php (admin)
*
* HTMX fragment: returns the "Autre(s) langue(s)" input row.
* Called from the shared form partial when a language checkbox changes.
*
* Expected POST:
* languages[] — selected language IDs (may be absent)
* language_autre — current free-text value (for repopulation)
*/
require_once __DIR__ . '/../../bootstrap.php';
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
require_once APP_ROOT . '/public/partage/language-autre-fragment.php';

View File

@@ -1,46 +0,0 @@
<?php
/**
* language-autre-fragment.php
*
* HTMX fragment: returns the "Autre(s) langue(s)" input row when no standard
* language checkbox is checked, or an empty hidden placeholder when at least
* one is checked.
*
* Expected POST:
* languages[] — selected language IDs (may be absent)
* language_autre — current free-text value (for repopulation)
*/
require_once __DIR__ . '/../bootstrap.php';
$selectedIds = isset($_POST['languages']) && is_array($_POST['languages'])
? $_POST['languages']
: [];
$currentValue = htmlspecialchars(trim($_POST['language_autre'] ?? ''));
$anyChecked = !empty($selectedIds);
?>
<div id="language-autre-row">
<?php if (!$anyChecked): ?>
<div>
<label for="language_autre">Autre(s) langue(s) : <span class="asterisk">*</span></label>
<div>
<input type="text"
id="language_autre"
name="language_autre"
value="<?= $currentValue ?>"
required>
<small>Si votre TFE contient une langue absente de la liste, précisez-la ici.</small>
</div>
</div>
<?php else: ?>
<div>
<label for="language_autre">Autre(s) langue(s) :</label>
<div>
<input type="text"
id="language_autre"
name="language_autre"
value="<?= $currentValue ?>">
<small>Si votre TFE contient une langue absente de la liste, précisez-la ici.</small>
</div>
</div>
<?php endif; ?>
</div>

View File

@@ -21,6 +21,13 @@ $parts = explode('/', $path);
$slug = $parts[0] ?? ''; $slug = $parts[0] ?? '';
$action = $parts[1] ?? ''; $action = $parts[1] ?? '';
// Special route: /partage/language-autre-fragment (HTMX fragment, no auth needed)
if ($slug === 'language-autre-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
App::boot();
require_once __DIR__ . '/language-autre-fragment.php';
exit;
}
// Special route: /partage/format-website-fragment (HTMX fragment, no auth needed) // Special route: /partage/format-website-fragment (HTMX fragment, no auth needed)
if ($slug === 'format-website-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') { if ($slug === 'format-website-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
App::boot(); App::boot();

View File

@@ -0,0 +1,36 @@
<?php
/**
* language-autre-fragment.php
*
* Shared HTMX fragment include: returns the "Autre(s) langue(s)" input row
* when no standard language checkbox is checked, or the plain (non-required)
* variant when at least one is checked.
*
* Included by:
* - /admin/language-autre-fragment.php (AdminAuth gated)
* - partage/index.php special route (public, session already booted)
*
* Expected POST:
* languages[] — selected language IDs (may be absent)
* language_autre — current free-text value (for repopulation)
*/
$selectedIds = isset($_POST['languages']) && is_array($_POST['languages'])
? $_POST['languages']
: [];
$currentValue = htmlspecialchars(trim($_POST['language_autre'] ?? ''));
$anyChecked = !empty($selectedIds);
?>
<div id="language-autre-row">
<div>
<label for="language_autre">Autre(s) langue(s) :<?= !$anyChecked ? ' <span class="asterisk">*</span>' : '' ?></label>
<div>
<input type="text"
id="language_autre"
name="language_autre"
value="<?= $currentValue ?>"
<?= !$anyChecked ? 'required' : '' ?>>
<small>Si votre TFE contient une langue absente de la liste, précisez-la ici.</small>
</div>
</div>
</div>

View File

@@ -98,7 +98,7 @@
<?php <?php
$mime = $f['mime_type'] ?? ''; $mime = $f['mime_type'] ?? '';
$isImage = str_starts_with($mime, 'image/'); $isImage = str_starts_with($mime, 'image/');
$mediaUrl = '/media.php?path=' . urlencode($f['file_path']); $mediaUrl = '/media?path=' . urlencode($f['file_path']);
$fileName = htmlspecialchars($f['file_name'] ?? basename($f['file_path'])); $fileName = htmlspecialchars($f['file_name'] ?? basename($f['file_path']));
$fileType = htmlspecialchars($f['file_type']); $fileType = htmlspecialchars($f['file_type']);
?> ?>

View File

@@ -173,7 +173,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
$options = $languages; $options = $languages;
$checked = $formData["languages"] ?? []; $checked = $formData["languages"] ?? [];
$required = true; $required = true;
$hxPost = "/language-autre-fragment.php"; $hxPost = $mode === 'partage' ? "/partage/language-autre-fragment" : "/admin/language-autre-fragment.php";
$hxTarget = "#language-autre-row"; $hxTarget = "#language-autre-row";
$hxSwap = "outerHTML"; $hxSwap = "outerHTML";
include APP_ROOT . "/templates/partials/form/checkbox-list.php"; include APP_ROOT . "/templates/partials/form/checkbox-list.php";
@@ -252,7 +252,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<div class="admin-file-input"> <div class="admin-file-input">
<?php if (!empty($currentCover)): ?> <?php if (!empty($currentCover)): ?>
<div class="admin-banner-preview"> <div class="admin-banner-preview">
<img src="/media.php?path=<?= urlencode( <img src="/media?path=<?= urlencode(
$currentCover["file_path"], $currentCover["file_path"],
) ?>" ) ?>"
alt="Couverture actuelle" style="max-height:180px;"> alt="Couverture actuelle" style="max-height:180px;">
@@ -321,7 +321,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
str_starts_with($f["file_path"] ?? "", "https://"); str_starts_with($f["file_path"] ?? "", "https://");
$fLinkHref = $isExternalUrl $fLinkHref = $isExternalUrl
? htmlspecialchars($f["file_path"]) ? htmlspecialchars($f["file_path"])
: "/media.php?path=" . urlencode($f["file_path"]); : "/media?path=" . urlencode($f["file_path"]);
?> ?>
<li class="admin-file-list-item" data-file-id="<?= (int) $f[ <li class="admin-file-list-item" data-file-id="<?= (int) $f[
"id" "id"

View File

@@ -393,7 +393,7 @@
const formData = new FormData(form); const formData = new FormData(form);
formData.append('thesis_id', '<?= $thesisId ?>'); formData.append('thesis_id', '<?= $thesisId ?>');
fetch('/request-access.php', { fetch('/request-access', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })