route. Slugs are unique (eval: uniqueInPid). * * @return array{path: string, name: string, size: int, mimeType: string}|null */ public static function resolveBySlug(string $slug): ?array { if ($slug === '') { return null; } try { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tx_vitec_domain_model_download'); $uid = $qb ->select('uid') ->from('tx_vitec_domain_model_download') ->where( $qb->expr()->eq('slug', $qb->createNamedParameter($slug, ParameterType::STRING)), $qb->expr()->eq('deleted', 0), $qb->expr()->eq('hidden', 0) ) ->setMaxResults(1) ->executeQuery() ->fetchOne(); return $uid ? self::resolve((int)$uid) : null; } catch (\Throwable $e) { return null; } } /** @return array|null */ private static function loadDownload(int $uid): ?array { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tx_vitec_domain_model_download'); $row = $qb ->select('uid', 'fileprefix', 'filepath') ->from('tx_vitec_domain_model_download') ->where( $qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)), $qb->expr()->eq('deleted', 0), $qb->expr()->eq('hidden', 0), // private downloads are never served publicly - the caller // cannot distinguish this from a missing file, by design $qb->expr()->eq('private_download', 0) ) ->setMaxResults(1) ->executeQuery() ->fetchAssociative(); return $row ?: null; } private static function resolveByFal(int $downloadUid): ?string { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_file_reference'); $row = $qb ->select('f.identifier') ->from('sys_file_reference', 'fr') ->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid') ->where( $qb->expr()->eq('fr.tablenames', $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING)), $qb->expr()->eq('fr.fieldname', $qb->createNamedParameter('file', ParameterType::STRING)), $qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), $qb->expr()->eq('fr.deleted', 0), $qb->expr()->eq('f.missing', 0) ) ->orderBy('fr.sorting_foreign', 'ASC') ->setMaxResults(1) ->executeQuery() ->fetchAssociative(); $identifier = (string)($row['identifier'] ?? ''); if ($identifier === '') { return null; } $path = Environment::getPublicPath() . '/fileadmin' . $identifier; return is_file($path) && is_readable($path) ? $path : null; } /** * Highest revision of ____-. in the * Collateral directory - same pattern and ranking as the renderers. */ private static function resolveByConvention(string $fileprefix, string $filetype): ?string { $fileprefix = trim($fileprefix); $filetype = trim($filetype); if ($fileprefix === '' || $filetype === '') { return null; } $baseDir = Environment::getPublicPath() . '/fileadmin/downloads/Collateral'; if (!is_dir($baseDir) || !is_readable($baseDir)) { return null; } $pattern = '/^' . preg_quote($fileprefix, '/') . '__' . preg_quote($filetype, '/') . '__(\\d+)-([A-Za-z]+)\\.([A-Za-z0-9]+)$/'; $entries = scandir($baseDir); if ($entries === false) { return null; } $bestFile = null; $bestNumber = -1; $bestLetterRank = -1; foreach ($entries as $entry) { if (!is_string($entry) || !preg_match($pattern, $entry, $matches)) { continue; } $number = (int)$matches[1]; $letterRank = self::letterSequenceToRank((string)$matches[2]); if ($number > $bestNumber || ($number === $bestNumber && $letterRank > $bestLetterRank)) { $bestNumber = $number; $bestLetterRank = $letterRank; $bestFile = $entry; } } if ($bestFile === null) { return null; } $path = $baseDir . '/' . $bestFile; return is_file($path) && is_readable($path) ? $path : null; } private static function resolveByFilepath(string $filepath): ?string { $filepath = trim($filepath); if ($filepath === '') { return null; } $path = Environment::getPublicPath() . '/' . ltrim($filepath, '/'); return is_file($path) && is_readable($path) ? $path : null; } /** Filetype segment from the assigned filetype category (parent 4). */ private static function fileType(int $downloadUid): string { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_category'); $categories = $qb ->select('c.filetype') ->from('sys_category', 'c') ->join( 'c', 'sys_category_record_mm', 'mm', 'mm.uid_local = c.uid AND mm.tablenames = ' . $qb->createNamedParameter('tx_vitec_domain_model_download', ParameterType::STRING) . ' AND mm.fieldname = ' . $qb->createNamedParameter('categories', ParameterType::STRING) ) ->where( $qb->expr()->eq('mm.uid_foreign', $qb->createNamedParameter($downloadUid, ParameterType::INTEGER)), $qb->expr()->eq('c.deleted', 0), $qb->expr()->eq('c.hidden', 0), $qb->expr()->eq('c.parent', $qb->createNamedParameter(4, ParameterType::INTEGER)) ) ->orderBy('mm.sorting', 'ASC') ->executeQuery() ->fetchAllAssociative(); foreach ($categories as $category) { $filetype = trim((string)($category['filetype'] ?? '')); if ($filetype !== '') { return $filetype; } } return ''; } /** "A" = 1 ... "Z" = 26, "AA" = 27 - revision letters as a rank. */ private static function letterSequenceToRank(string $letters): int { $rank = 0; foreach (str_split(strtoupper($letters)) as $char) { $rank = $rank * 26 + (ord($char) - 64); } return $rank; } /** @return array{path: string, name: string, size: int, mimeType: string} */ private static function describe(string $path): array { $mimeType = function_exists('mime_content_type') ? (string)(mime_content_type($path) ?: '') : ''; return [ 'path' => $path, 'name' => basename($path), 'size' => (int)(filesize($path) ?: 0), 'mimeType' => $mimeType, ]; } }