82 lines
2.8 KiB
PHP
82 lines
2.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Core\Database;
|
|
|
|
/** Resolves only approved assets belonging to a currently published bundle. */
|
|
final class PublishedContentService
|
|
{
|
|
private const STORAGE_ROOT = __DIR__ . '/../../storage/curriculum';
|
|
|
|
public static function findPublishedAsset(string $assetUuid): ?array
|
|
{
|
|
if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $assetUuid)) {
|
|
return null;
|
|
}
|
|
|
|
return Database::selectOne(
|
|
"SELECT a.uuid, a.asset_type, a.storage_driver, a.storage_key, a.mime_type,
|
|
a.byte_size, a.sha256, cl.uuid AS curriculum_lesson_uuid,
|
|
cl.grade_key, cl.subject_key, cl.semester_key, cl.unit_key,
|
|
cl.lesson_key, pb.bundle_version
|
|
FROM content_assets a
|
|
JOIN publication_bundle_assets pba ON pba.content_asset_id = a.id
|
|
JOIN publication_bundles pb ON pb.id = pba.publication_bundle_id
|
|
JOIN curriculum_lessons cl ON cl.id = pb.curriculum_lesson_id
|
|
WHERE a.uuid = ?
|
|
AND a.review_status = 'approved'
|
|
AND a.rights_status = 'cleared'
|
|
AND pb.status = 'published'
|
|
AND cl.source_status = 'approved'
|
|
ORDER BY pb.published_at DESC, pb.id DESC
|
|
LIMIT 1",
|
|
[$assetUuid]
|
|
);
|
|
}
|
|
|
|
public static function readLocalAsset(array $asset): ?string
|
|
{
|
|
if (($asset['storage_driver'] ?? '') !== 'local') {
|
|
return null;
|
|
}
|
|
$root = realpath(self::STORAGE_ROOT);
|
|
if (!$root) {
|
|
return null;
|
|
}
|
|
$storageKey = ltrim((string)($asset['storage_key'] ?? ''), '/');
|
|
if ($storageKey === '') {
|
|
return null;
|
|
}
|
|
|
|
$candidates = [
|
|
$root . '/' . $storageKey,
|
|
$root . '/_incoming/' . $storageKey,
|
|
dirname($root) . '/' . $storageKey,
|
|
dirname($root, 2) . '/' . $storageKey,
|
|
];
|
|
|
|
if (preg_match('#staged/grade_10/[^/]+/(.+)#', $storageKey, $matches)) {
|
|
$inner = $matches[1];
|
|
$candidates[] = $root . '/' . $inner;
|
|
$candidates[] = $root . '/_incoming/' . $inner;
|
|
}
|
|
|
|
$prefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
|
$projectRoot = realpath(dirname(__DIR__, 2)) ?: '';
|
|
|
|
foreach ($candidates as $candidate) {
|
|
$real = realpath($candidate);
|
|
if ($real && is_file($real)) {
|
|
// Ensure candidate is strictly within the project root to prevent traversal
|
|
if ($projectRoot !== '' && str_starts_with($real, $projectRoot)) {
|
|
return $real;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|