See file * ``` * * @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-link-file */ final class FileViewHelper extends AbstractTagBasedViewHelper { /** * @var string */ protected $tagName = 'a'; public function __construct( private readonly HashService $hashService ) { parent::__construct(); } public function initializeArguments(): void { parent::initializeArguments(); $this->registerArgument('file', FileInterface::class, 'Specifies the file to create a link to', true); $this->registerArgument('download', 'bool', 'Specifies if file should be downloaded instead of displayed'); $this->registerArgument('filename', 'string', 'Specifies an alternative filename. If filename contains a file extension, this must be the same as from \'file\'.'); } public function render(): string { $file = $this->arguments['file']; if (!($file instanceof FileInterface)) { throw new InvalidArgumentValueException('Argument \'file\' must be an instance of ' . FileInterface::class, 1621511632); } // Get the public URL. This url is either be defined by a GeneratePublicUrlForResourceEvent, // an OnlineMedia helper, the corresponding driver or using the file dump functionality. $publicUrl = $file->getPublicUrl(); // Early return in case public url is null as this indicates the file is // not accessible, e.g. because the corresponding storage is offline. if ($publicUrl === null) { return ''; } if (str_contains($publicUrl, 'dumpFile')) { // In case we deal with is a file dump URL, recreate the URL // by taking the defined view helper arguments into account. $publicUrl = $this->createFileDumpUrl($file); } elseif ($this->arguments['download'] ?? false) { // In case the URL directly links to the file (no eID) and // the file should be downloaded instead of displayed, this // must be set by the "download" tag attribute, which may // contain an alternative filename. $this->tag->addAttribute( 'download', $this->getAlternativeFilename($file) ); } $this->tag->addAttribute('href', $publicUrl); $childContent = $this->renderChildren(); $this->tag->setContent($childContent ? (string)$childContent : htmlspecialchars($file->getName())); $this->tag->forceClosingTag(true); return $this->tag->render(); } /** * Create a file dump URL, taking the view helper arguments into account */ private function createFileDumpUrl(FileInterface $file): string { $parameters = ['eID' => 'dumpFile']; if ($file instanceof File) { $parameters['t'] = 'f'; $parameters['f'] = $file->getUid(); } elseif ($file instanceof FileReference) { $parameters['t'] = 'r'; $parameters['r'] = $file->getUid(); } elseif ($file instanceof ProcessedFile) { $parameters['t'] = 'p'; $parameters['p'] = $file->getUid(); } if ($download = $this->arguments['download'] ?? false) { $parameters['dl'] = (int)$download; } if (($filename = $this->getAlternativeFilename($file)) !== '') { $parameters['fn'] = $filename; } $parameters['token'] = $this->hashService->hmac(implode('|', $parameters), 'resourceStorageDumpFile'); return GeneralUtility::locationHeaderUrl( PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'), $this->renderingContext->getAttribute(ServerRequestInterface::class) ) . '?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986); } private function getAlternativeFilename(FileInterface $file): string { $alternativeFilename = $this->arguments['filename'] ?? ''; // Return early if filename is empty or not valid if ($alternativeFilename === '' || !preg_match('/^[0-9a-z._\-]+$/i', $alternativeFilename)) { return ''; } $extension = pathinfo($alternativeFilename, PATHINFO_EXTENSION); if ($extension === '') { // Add original extension in case alternative filename did not contain any $alternativeFilename = rtrim($alternativeFilename, '.') . '.' . $file->getExtension(); } // Check if given or resolved extension matches the original one return $file->getExtension() === pathinfo($alternativeFilename, PATHINFO_EXTENSION) ? $alternativeFilename : ''; } }