buildParametersFromRequest($request); if (!$this->isTokenValid($parameters, $request)) { return $this->responseFactory->createResponse(403); } $file = $this->createFileObjectByParameters($parameters); if ($file === null) { return $this->responseFactory->createResponse(404); } // Allow some other process to do some security/access checks. // Event Listeners should return a 403 response if access is rejected $event = new ModifyFileDumpEvent($file, $request); $event = $this->eventDispatcher->dispatch($event); if ($event->isPropagationStopped()) { return $this->applyContentSecurityPolicy($event->getFile(), $event->getResponse()); } $file = $event->getFile(); $processingInstructions = []; // Apply cropping, if possible if (!empty($parameters['cv'])) { $cropVariant = $parameters['cv']; $cropString = $file instanceof FileReference ? $file->getProperty('crop') : ''; $cropArea = CropVariantCollection::create((string)$cropString)->getCropArea($cropVariant); $processingInstructions = array_merge( $processingInstructions, [ 'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($file), ] ); } // Apply width/height, if given if (!empty($parameters['s'])) { $size = GeneralUtility::trimExplode(':', $parameters['s']); $processingInstructions = array_merge( $processingInstructions, [ 'width' => $size[0] ?? null, 'height' => $size[1] ?? null, 'minWidth' => $size[2] ? (int)$size[2] : null, 'minHeight' => $size[3] ? (int)$size[3] : null, 'maxWidth' => $size[4] ? (int)$size[4] : null, 'maxHeight' => $size[5] ? (int)$size[5] : null, ] ); } if (!empty($processingInstructions) && !($file instanceof ProcessedFile)) { if (is_callable([$file, 'getOriginalFile'])) { // Get the original file from the file reference $file = $file->getOriginalFile(); } $file = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingInstructions); } return $this->applyContentSecurityPolicy( $file, $file->getStorage()->streamFile( $file, (bool)($parameters['dl'] ?? false), $parameters['fn'] ?? null ) ); } protected function buildParametersFromRequest(ServerRequestInterface $request): array { $parameters = ['eID' => 'dumpFile']; $queryParams = $request->getQueryParams(); // Identifier of what to process. f, r or p // Only needed while hash_equals $t = (string)($queryParams['t'] ?? ''); if ($t) { $parameters['t'] = $t; } // sys_file $f = (string)($queryParams['f'] ?? ''); if ($f) { $parameters['f'] = (int)$f; } // sys_file_reference $r = (string)($queryParams['r'] ?? ''); if ($r) { $parameters['r'] = (int)$r; } // Processed file $p = (string)($queryParams['p'] ?? ''); if ($p) { $parameters['p'] = (int)$p; } // File's width and height in this order: w:h:minW:minH:maxW:maxH $s = (string)($queryParams['s'] ?? ''); if ($s) { $parameters['s'] = $s; } // File's crop variant $cv = (string)($queryParams['cv'] ?? ''); if ($cv) { $parameters['cv'] = $cv; } // As download $dl = (string)($queryParams['dl'] ?? ''); if ($dl) { $parameters['dl'] = (int)$dl; } // Alternative file name $fn = (string)($queryParams['fn'] ?? ''); if ($fn) { $parameters['fn'] = $fn; } return $parameters; } protected function isTokenValid(array $parameters, ServerRequestInterface $request): bool { return hash_equals( $this->hashService->hmac(implode('|', $parameters), 'resourceStorageDumpFile', HashAlgo::SHA3_256), $request->getQueryParams()['token'] ?? '' ); } /** * @return File|FileReference|ProcessedFile|null */ protected function createFileObjectByParameters(array $parameters) { $file = null; if (isset($parameters['f'])) { try { $file = $this->resourceFactory->getFileObject($parameters['f']); if ($file->isDeleted() || $file->isMissing() || !$this->isFileValid($file)) { $file = null; } } catch (\Exception $e) { $file = null; } } elseif (isset($parameters['r'])) { try { $file = $this->resourceFactory->getFileReferenceObject($parameters['r']); if ($file->isMissing() || !$this->isFileValid($file->getOriginalFile())) { $file = null; } } catch (\Exception $e) { $file = null; } } elseif (isset($parameters['p'])) { try { $file = $this->processedFileRepository->findByUid((int)$parameters['p']); if ($file->isDeleted() || !$this->isFileValid($file->getOriginalFile())) { $file = null; } } catch (\Exception) { $file = null; } } return $file; } protected function isFileValid(FileInterface $file): bool { return $file->getStorage()->getDriverType() !== 'Local' || $this->fileNameValidator->isValid(basename($file->getIdentifier())); } /** * Applies hard-coded content-security-policy (CSP) for file to be dumped. */ protected function applyContentSecurityPolicy(ResourceInterface $file, ResponseInterface $response): ResponseInterface { $extension = PathUtility::pathinfo($file->getName(), PATHINFO_EXTENSION); // same as in `typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/resources-root-htaccess` if ($extension === 'pdf' || $response->getHeaderLine('content-type') === 'application/pdf') { $policy = "default-src 'self' 'unsafe-inline'; script-src 'none'; object-src 'self'; plugin-types application/pdf;"; } elseif ($extension === 'svg' || $response->getHeaderLine('content-type') === 'image/svg+xml') { $policy = "default-src 'self'; script-src 'none'; style-src 'unsafe-inline'; object-src 'none';"; } else { $policy = "default-src 'self'; script-src 'none'; style-src 'none'; object-src 'none';"; } return $response->withAddedHeader('content-security-policy', $policy); } }