Files
VITEC-website/public/collateral-fallback.php
o-rasche bb7c03235d Checkpoint: headless JSON architecture documented + cleanup (Stufe 1+2)
Restore point before Stufe 3 (central resolver-service refactoring).

Includes this session's work:
- Success Story (usecase) rebuild: new fields/tabs, UsecaseSerializer,
  thin List/Show renderers, MM relations, inline content elements.
- vitec_columns content block (two-column layout with per-item content)
  incl. unified header section; special-cased JSON resolution.
- Container per-column flex (items[].config align/justify) + gap on parent.
- Unified header section across CEs/plugins/containers; header fields in JSON.
- ISO-style architecture spec: Documentation/Headless-JSON-Architecture.md.
- Cleanup: removed local scratch + verified .bak backups.
- Fixes: B-6 (undefined thumbnail variable in Downloadcardcollection image
  resolver), B-7 (unsatisfiable legacy CType OR branch in Downloadcard/Datasheets).

Rollback: git reset --hard snapshot-2026-07-09-pre-stufe3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:01:56 +02:00

141 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
/**
* Fallback resolver for deleted versioned files in fileadmin/downloads/Collateral.
*
* Expected requested filename format:
* [fileprefix]__[filetype]__[number]-[letter].ext
*
* If a matching file no longer exists, this resolver redirects to the newest
* available version (highest number, then highest letter sequence).
*/
$requestedRaw = isset($_GET['requested']) ? (string)$_GET['requested'] : '';
$requestedRaw = str_replace('\\', '/', $requestedRaw);
$requestedFilename = basename(rawurldecode($requestedRaw));
if ($requestedFilename === '' || $requestedFilename === '.' || $requestedFilename === '..') {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Not found';
exit;
}
if (!preg_match('/^(?P<prefix>.+?)__(?P<type>.+?)__(?P<number>\\d+)-(?P<letters>[A-Za-z]+)\\.(?P<ext>[A-Za-z0-9]+)$/', $requestedFilename, $parts)) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Not found';
exit;
}
$prefix = (string)$parts['prefix'];
$type = (string)$parts['type'];
$baseDir = __DIR__ . '/fileadmin/downloads/Collateral';
if (!is_dir($baseDir) || !is_readable($baseDir)) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Not found';
exit;
}
$escapedPrefix = preg_quote($prefix, '/');
$escapedType = preg_quote($type, '/');
$pattern = '/^' . $escapedPrefix . '__' . $escapedType . '__(\\d+)-([A-Za-z]+)\\.([A-Za-z0-9]+)$/';
$bestFilename = null;
$bestNumber = -1;
$bestLetterRank = -1;
$entries = scandir($baseDir);
if ($entries === false) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Not found';
exit;
}
foreach ($entries as $entry) {
if (!is_string($entry) || $entry === '.' || $entry === '..') {
continue;
}
if (!preg_match($pattern, $entry, $matches)) {
continue;
}
$number = (int)$matches[1];
$letterRank = letterSequenceToRank((string)$matches[2]);
if ($number > $bestNumber || ($number === $bestNumber && $letterRank > $bestLetterRank)) {
$bestNumber = $number;
$bestLetterRank = $letterRank;
$bestFilename = $entry;
}
}
if ($bestFilename === null) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Not found';
exit;
}
$bestPath = $baseDir . '/' . $bestFilename;
if (!is_file($bestPath) || !is_readable($bestPath)) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Not found';
exit;
}
$clientIp = isset($_SERVER['REMOTE_ADDR']) ? (string)$_SERVER['REMOTE_ADDR'] : '-';
$userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? (string)$_SERVER['HTTP_USER_AGENT'] : '-';
logFallbackRecovery($requestedFilename, $bestFilename, $clientIp, $userAgent);
$target = '/fileadmin/downloads/Collateral/' . rawurlencode($bestFilename);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Location: ' . $target, true, 302);
exit;
function letterSequenceToRank(string $letters): int
{
$rank = 0;
$letters = strtoupper($letters);
$length = strlen($letters);
for ($i = 0; $i < $length; $i++) {
$char = ord($letters[$i]);
if ($char < 65 || $char > 90) {
continue;
}
$rank = ($rank * 26) + ($char - 64);
}
return $rank;
}
function logFallbackRecovery(string $requested, string $resolved, string $clientIp, string $userAgent): void
{
$logDir = dirname(__DIR__) . '/var/log';
if (!is_dir($logDir)) {
return;
}
$timestamp = gmdate('c');
$line = sprintf(
"%s collateral-fallback requested=\"%s\" resolved=\"%s\" ip=%s ua=\"%s\"\n",
$timestamp,
str_replace('"', '\\"', $requested),
str_replace('"', '\\"', $resolved),
$clientIp,
str_replace('"', '\\"', $userAgent)
);
@file_put_contents($logDir . '/collateral-fallback.log', $line, FILE_APPEND | LOCK_EX);
}