mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
- reliable tab title /favicon wrapper, - Content-Disposition filename, - admin media route hardened to thesis-file prefixes only (defense-in-depth)
146 lines
5.6 KiB
PHP
146 lines
5.6 KiB
PHP
<?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"
|
|
);
|
|
}
|
|
}
|
|
}
|