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>
141 lines
4.0 KiB
PHP
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);
|
|
}
|