mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
Add admin-only route to open Interdit thesis files from backoffice:
- reliable tab title /favicon wrapper, - Content-Disposition filename, - admin media route hardened to thesis-file prefixes only (defense-in-depth)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* MediaControllerVisibilityTest — integration tests for the file-visibility
|
||||
* gate that MediaController relies on (Database::getFileVisibility).
|
||||
*
|
||||
* The MediaController::handle() public route blocks thesis files whose owner is
|
||||
* 'Interdit' (access_type_id=3) with HTTP 403. The dedicated admin route
|
||||
* (/admin/media.php) calls handle(adminBypass: true) to lift that single gate
|
||||
* after AdminAuth::requireLogin() has confirmed an administrator session.
|
||||
*
|
||||
* These tests pin down the data contract (which access_type_id maps to which
|
||||
* outcome) so a change to the gate logic can't silently break either route.
|
||||
*/
|
||||
class MediaControllerVisibilityTest extends TestCase
|
||||
{
|
||||
private Database $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
TestDatabase::resetData();
|
||||
$this->db = TestDatabase::getInstance();
|
||||
require_once APP_ROOT . '/src/Controllers/MediaController.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* An 'Interdit' thesis file (access_type_id=3) must be flagged as
|
||||
* forbidden so the public route returns 403 (and only the admin bypass
|
||||
* may open it).
|
||||
*/
|
||||
public function testInterditThesisFileIsFlaggedForbidden(): void
|
||||
{
|
||||
[$authorId, $thesisId] = TestDatabase::seedBasicThesis('Interdit TFE', 'Author', 2024);
|
||||
|
||||
$pdo = TestDatabase::getPDO();
|
||||
$pdo->prepare('UPDATE theses SET access_type_id = 3 WHERE id = ?')
|
||||
->execute([$thesisId]);
|
||||
$pdo->prepare(
|
||||
"INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type)
|
||||
VALUES (?, 'main', ?, 'rapport.pdf', 0, 'application/pdf')"
|
||||
)->execute([$thesisId, 'documents/2024-001/rapport.pdf']);
|
||||
|
||||
$accessTypeId = $this->db->getFileVisibility('documents/2024-001/rapport.pdf');
|
||||
$this->assertSame(3, $accessTypeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* A 'Libre' thesis file (access_type_id=1) is served publicly by /media —
|
||||
* never blocked by the gate.
|
||||
*/
|
||||
public function testLibreThesisFileIsNotFlaggedForbidden(): void
|
||||
{
|
||||
[$authorId, $thesisId] = TestDatabase::seedBasicThesis('Libre TFE', 'Author', 2024);
|
||||
|
||||
$pdo = TestDatabase::getPDO();
|
||||
$pdo->prepare('UPDATE theses SET access_type_id = 1 WHERE id = ?')
|
||||
->execute([$thesisId]);
|
||||
|
||||
$accessTypeId = $this->db->getFileVisibility('documents/2024-001/cover.jpg');
|
||||
$this->assertSame(1, $accessTypeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* A path that belongs to no thesis file resolves to null — the gate is
|
||||
* a no-op (file still subject to MIME + jail checks in MediaController).
|
||||
*/
|
||||
public function testUnrelatedPathReturnsNull(): void
|
||||
{
|
||||
// No files seeded at all — seed an Interdit thesis without entries.
|
||||
$pdo = TestDatabase::getPDO();
|
||||
$pdo->prepare(
|
||||
"INSERT INTO theses (title, year, identifier, is_published, objet, access_type_id)
|
||||
VALUES ('Orphan', 2024, '2024-999', 1, 'tfe', 3)"
|
||||
)->execute();
|
||||
|
||||
$accessTypeId = $this->db->getFileVisibility('theses/2024-999/unknown.pdf');
|
||||
$this->assertNull($accessTypeId);
|
||||
}
|
||||
|
||||
// ── getFileDisplayName (tab title / download name) ────────────────────────
|
||||
|
||||
/**
|
||||
* getFileDisplayName returns the original uploaded file_name for a known
|
||||
* thesis file — used to set a useful browser tab title instead of 'media.php'.
|
||||
*/
|
||||
public function testFileDisplayNameFromStoredFileName(): void
|
||||
{
|
||||
[$authorId, $thesisId] = TestDatabase::seedBasicThesis('Display Name', 'Author', 2024);
|
||||
|
||||
$pdo = TestDatabase::getPDO();
|
||||
$pdo->prepare(
|
||||
"INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type)
|
||||
VALUES (?, 'main', 'tfe/2024-001/rapport.pdf', 'Note de synthese — Étude erg.pdf', 0, 'application/pdf')"
|
||||
)->execute([$thesisId]);
|
||||
|
||||
$this->assertSame(
|
||||
'Note de synthese — Étude erg.pdf',
|
||||
$this->db->getFileDisplayName('tfe/2024-001/rapport.pdf')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* getFileDisplayName returns null for a path that belongs to no thesis file,
|
||||
* so MediaController falls back to the basename of the requested path.
|
||||
*/
|
||||
public function testFileDisplayNameUnknownPathReturnsNull(): void
|
||||
{
|
||||
$this->assertNull($this->db->getFileDisplayName('theses/2024-999/unknown.pdf'));
|
||||
}
|
||||
|
||||
// ── isThesisFilePath (admin-bypass defense-in-depth) ──────────────────────
|
||||
|
||||
/**
|
||||
* The admin bypass route is restricted to thesis-file prefixes only.
|
||||
* All five storage layouts (tfe/ these/ frart/ documents/ theses/) count;
|
||||
* any other storage path (schema, db, tmp, backups…) is rejected.
|
||||
*/
|
||||
public function testIsThesisFilePathRecognisesAllThesisPrefixes(): void
|
||||
{
|
||||
foreach (['tfe', 'these', 'frart', 'documents', 'theses'] as $prefix) {
|
||||
$this->assertTrue(
|
||||
MediaController::isThesisFilePath($prefix . '/2024/2024-001/rapport.pdf'),
|
||||
"Expected '{$prefix}/…' to be a thesis file path"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testIsThesisFilePathRejectsNonThesisStoragePaths(): void
|
||||
{
|
||||
foreach ([
|
||||
'schema.sql',
|
||||
'xamxam.db',
|
||||
'tmp/evil.php',
|
||||
'backups/xamxam-2024.db',
|
||||
'cache/foo.png',
|
||||
] as $nonThesisPath) {
|
||||
$this->assertFalse(
|
||||
MediaController::isThesisFilePath($nonThesisPath),
|
||||
"Expected '{$nonThesisPath}' to be rejected as a non-thesis path"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user