single download object (key: "download") * - else settings.product set -> all downloads of that product (key: "downloads", * plus a small "product" context) * - neither -> empty (key removed by headless ifEmptyUnsetKey) * * `settings.layout` / magstyle / magheader / magtext / maglink / adddetaillink * are always passed through under the "settings" block for the frontend. * * `render()` performs page discovery; `renderForRecord()` processes a specific * tt_content row and is reused by ContainerChildrenProcessor / * ContentElementResolver. Exception-safe. */ class DownloadcardJsonRenderer { #[AsAllowedCallable] public function render(string $content, array $conf): string { $pageId = 0; $req = $GLOBALS["TYPO3_REQUEST"] ?? null; if ($req !== null) { $pi = $req->getAttribute("frontend.page.information"); if ($pi !== null) { $pageId = (int)$pi->getId(); } } if ($pageId <= 0) { $pageId = (int)($GLOBALS["TSFE"]->id ?? 0); } $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); $rows = $qb ->select('*') ->from('tt_content') ->where( $qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)), $qb->expr()->eq('CType', $qb->createNamedParameter('vitec_downloadcard', ParameterType::STRING)), $qb->expr()->eq('deleted', 0), $qb->expr()->eq('hidden', 0) ) ->executeQuery() ->fetchAllAssociative(); if (empty($rows)) { return ''; } return $this->renderForRecord($rows[0]); } /** * @param array $contentElement */ public function renderForRecord(array $contentElement): string { try { $pageId = (int)($GLOBALS['TSFE']->id ?? 0); $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); $settings = $flexFormData['settings'] ?? []; $downloadUid = (int)($settings['download'] ?? 0); $productUid = (int)($settings['product'] ?? 0); $debugMode = (bool)($settings['debug'] ?? false); // Settings always carried through to the frontend $settingsOut = [ 'layout' => (string)($settings['layout'] ?? ''), 'magstyle' => (bool)($settings['magstyle'] ?? false), 'magheader' => (string)($settings['magheader'] ?? ''), 'magtext' => (string)($settings['magtext'] ?? ''), 'maglink' => (string)($settings['maglink'] ?? ''), 'adddetaillink' => (bool)($settings['adddetaillink'] ?? false), ]; // --- Mode 1: single download --- if ($downloadUid > 0) { $download = $this->fetchDownload($downloadUid); if (!$download) { return $debugMode ? json_encode(['error' => 'Download not found', 'debug' => ['downloadUid' => $downloadUid]]) : ''; } $serializedDownload = $this->serializeDownload($download); $response = [ 'mode' => 'single', 'download' => $serializedDownload, 'settings' => $settingsOut, ]; if ($debugMode) { $response['debug'] = [ 'pageId' => $pageId, 'downloadUid' => $downloadUid, 'settings' => $settings, 'fallbackFiles' => $this->collectFallbackFilesFromDownloads([$serializedDownload]), ]; } return json_encode($response); } // --- Mode 2: all downloads of a product --- if ($productUid > 0) { $downloads = $this->fetchDownloadsForProduct($productUid); $product = $this->fetchProductContext($productUid); $serializedDownloads = array_map(fn($r) => $this->serializeDownload($r), $downloads); $response = [ 'mode' => 'product-downloads', 'product' => $product, 'downloads' => $serializedDownloads, 'settings' => $settingsOut, ]; if ($debugMode) { $response['debug'] = [ 'pageId' => $pageId, 'productUid' => $productUid, 'downloadCount' => count($downloads), 'settings' => $settings, 'fallbackFiles' => $this->collectFallbackFilesFromDownloads($serializedDownloads), ]; } return json_encode($response); } // Neither selected: nothing to render. return $debugMode ? json_encode(['error' => 'No download or product selected', 'debug' => ['settings' => $settings]]) : ''; } catch (\Throwable $e) { return ''; } } /** * @return array|false */ private function fetchDownload(int $uid) { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tx_vitec_domain_model_download'); return $qb ->select('*') ->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), $qb->expr()->eq('hideonwebsite', 0) ) ->executeQuery() ->fetchAssociative(); } /** * @return array> */ private function fetchDownloadsForProduct(int $productUid): array { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tx_vitec_domain_model_download'); return $qb ->select('d.*') ->from('tx_vitec_domain_model_download', 'd') ->join('d', 'tx_vitec_product_download_mm', 'mm', 'mm.uid_foreign = d.uid') ->where( $qb->expr()->eq('mm.uid_local', $qb->createNamedParameter($productUid, ParameterType::INTEGER)), $qb->expr()->eq('d.deleted', 0), $qb->expr()->eq('d.hidden', 0), $qb->expr()->eq('d.hideonwebsite', 0) ) ->orderBy('mm.sorting', 'ASC') ->executeQuery() ->fetchAllAssociative(); } /** * @return array|null */ private function fetchProductContext(int $productUid): ?array { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tx_vitec_domain_model_product'); $row = $qb ->select('uid', 'title', 'slug', 'subtitle', 'teaser') ->from('tx_vitec_domain_model_product') ->where( $qb->expr()->eq('uid', $qb->createNamedParameter($productUid, ParameterType::INTEGER)), $qb->expr()->eq('deleted', 0), $qb->expr()->eq('hidden', 0) ) ->executeQuery() ->fetchAssociative(); if (!$row) { return null; } $thumbnailUrl = null; try { $fileObj = GeneralUtility::makeInstance(TYPO3CMSCoreResourceResourceFactory::class) ->getFileObject((int)$row["file_uid"]); $processed = $fileObj->process( TYPO3CMSCoreResourceProcessedFile::CONTEXT_IMAGEPREVIEW, ["width" => 400, "height" => 566] ); $thumbnailUrl = $processed->getPublicUrl(); } catch (Throwable $e) { // ignore — keep null } return [ 'uid' => (int)$row['uid'], 'title' => (string)($row['title'] ?? ''), 'slug' => (string)($row['slug'] ?? ''), 'subtitle' => (string)($row['subtitle'] ?? ''), 'teaser' => (string)($row['teaser'] ?? ''), 'link' => '/product/' . (string)($row['slug'] ?? ''), ]; } /** * @param array $download * @return array */ protected function serializeDownload(array $download): array { $uid = (int)$download['uid']; $fileprefix = (string)($download['fileprefix'] ?? ''); $filetype = $this->getDownloadFileType($uid); return [ 'uid' => $uid, 'title' => (string)($download['title'] ?? ''), 'slug' => (string)($download['slug'] ?? ''), 'teaser' => (string)($download['teaser'] ?? ''), 'description' => (string)($download['description'] ?? ''), 'keywords' => (string)($download['keywords'] ?? ''), 'icon' => (string)($download['icon'] ?? ''), 'filepath' => (string)($download['filepath'] ?? ''), 'fileprefix' => $fileprefix, 'filetype' => $filetype, 'private_download' => (bool)($download['private_download'] ?? false), 'hideonapp' => (bool)($download['hideonapp'] ?? false), 'hideonwebsite' => (bool)($download['hideonwebsite'] ?? false), 'hideondatasheets' => (bool)($download['hideondatasheets'] ?? false), 'hideonproducts' => (bool)($download['hideonproducts'] ?? false), 'file' => $this->getDownloadFile($uid, $fileprefix, $filetype), ]; } private function getDownloadFileType(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 ''; } /** * Resolve the FAL file reference (fieldname=file) for a download. * * @return array|null */ protected function getDownloadFile(int $downloadUid, string $fileprefix = '', string $filetype = ''): ?array { $qb = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_file_reference'); $row = $qb ->select('fr.uid', 'fr.title', 'fr.description', 'f.uid AS file_uid', 'f.identifier', 'f.name', 'f.size', 'f.extension', 'f.mime_type') ->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(); if (!$row) { return $this->resolveFileByConvention($fileprefix, $filetype); } $thumbnailUrl = null; try { $fileObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\ResourceFactory::class) ->getFileObject((int)$row['file_uid']); $processed = $fileObj->process( \TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGEPREVIEW, ['width' => 400, 'height' => 566] ); $thumbnailUrl = $processed->getPublicUrl(); } catch (\Throwable $e) { // ignore — keep null } return [ 'uid' => (int)$row['file_uid'], 'name' => (string)($row['name'] ?? ''), 'url' => '/fileadmin' . ($row['identifier'] ?? ''), 'thumbnail' => $thumbnailUrl, 'size' => (int)($row['size'] ?? 0), 'extension' => (string)($row['extension'] ?? ''), 'mimeType' => (string)($row['mime_type'] ?? ''), 'title' => (string)($row['title'] ?? ''), 'description' => (string)($row['description'] ?? ''), ]; } /** * @return array|null */ private function resolveFileByConvention(string $fileprefix, string $filetype): ?array { $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; } $escapedPrefix = preg_quote($fileprefix, '/'); $escapedType = preg_quote($filetype, '/'); $pattern = '/^' . $escapedPrefix . '__' . $escapedType . '__(\\d+)-([A-Za-z]+)\\.([A-Za-z0-9]+)$/'; $bestFile = null; $bestNumber = -1; $bestLetterRank = -1; $entries = scandir($baseDir); if ($entries === false) { return null; } foreach ($entries as $entry) { if (!is_string($entry) || $entry === '.' || $entry === '..') { continue; } if (!preg_match($pattern, $entry, $matches)) { continue; } $number = (int)$matches[1]; $letterRank = $this->letterSequenceToRank((string)$matches[2]); if ($number > $bestNumber || ($number === $bestNumber && $letterRank > $bestLetterRank)) { $bestNumber = $number; $bestLetterRank = $letterRank; $bestFile = $entry; } } if ($bestFile === null) { return null; } $fullPath = $baseDir . '/' . $bestFile; if (!is_file($fullPath) || !is_readable($fullPath)) { return null; } $extension = strtolower(pathinfo($bestFile, PATHINFO_EXTENSION)); $mimeType = function_exists('mime_content_type') ? (string)(mime_content_type($fullPath) ?: '') : ''; return [ 'uid' => 0, 'name' => $bestFile, 'url' => '/fileadmin/downloads/Collateral/' . rawurlencode($bestFile), 'thumbnail' => null, 'size' => (int)(filesize($fullPath) ?: 0), 'extension' => $extension, 'mimeType' => $mimeType, 'title' => '', 'description' => '', ]; } private 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; } /** * @param array> $downloads * @return array> */ private function collectFallbackFilesFromDownloads(array $downloads): array { $fallbackFiles = []; foreach ($downloads as $download) { $file = $download['file'] ?? null; if (!is_array($file)) { continue; } if ((int)($file['uid'] ?? 1) !== 0) { continue; } $fallbackFiles[] = [ 'downloadUid' => (int)($download['uid'] ?? 0), 'name' => (string)($file['name'] ?? ''), 'url' => (string)($file['url'] ?? ''), 'source' => 'naming-convention-fallback', ]; } return $fallbackFiles; } }