TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Controller;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Core\RequestId;
|
||||
use TYPO3\CMS\Core\Information\Typo3Information;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
|
||||
/**
|
||||
* A class representing error messages shown on a page, rendered via fluid.
|
||||
* Classic Example: "No pages are found on rootlevel"
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class ErrorPageController
|
||||
{
|
||||
public function __construct(
|
||||
protected ViewFactoryInterface $viewFactory,
|
||||
protected RequestId $requestId,
|
||||
protected Typo3Information $typo3Information,
|
||||
protected ContentSecurityPolicy\PolicyRegistry $policyRegistry,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Renders the view and returns the content.
|
||||
*/
|
||||
public function errorAction(string $title, string $message, int $errorCode = 0, ?int $httpStatusCode = null): string
|
||||
{
|
||||
$viewFactoryData = new ViewFactoryData(
|
||||
templateRootPaths: ['EXT:core/Resources/Private/Templates'],
|
||||
);
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
$view->assignMultiple([
|
||||
'message' => $message,
|
||||
'title' => $title,
|
||||
'httpStatusCode' => $httpStatusCode,
|
||||
'errorCodeUrlPrefix' => Typo3Information::URL_EXCEPTION,
|
||||
'donationUrl' => Typo3Information::URL_DONATE,
|
||||
'errorCode' => $errorCode,
|
||||
'requestId' => GeneralUtility::makeInstance(RequestId::class),
|
||||
'copyrightYear' => $this->typo3Information->getCopyrightYear(),
|
||||
]);
|
||||
$this->policyRegistry->appendMutationCollection(
|
||||
new ContentSecurityPolicy\MutationCollection(
|
||||
new ContentSecurityPolicy\Mutation(
|
||||
ContentSecurityPolicy\MutationMode::Extend,
|
||||
ContentSecurityPolicy\Directive::StyleSrcElem,
|
||||
ContentSecurityPolicy\SourceKeyword::nonceProxy
|
||||
)
|
||||
)
|
||||
);
|
||||
return $view->render('ErrorPage/Error');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Controller;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
||||
use TYPO3\CMS\Core\Resource\Event\ModifyFileDumpEvent;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Resource\FileReference;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Resource\ResourceInterface;
|
||||
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class FileDumpController
|
||||
{
|
||||
public function __construct(
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected ResourceFactory $resourceFactory,
|
||||
protected ResponseFactoryInterface $responseFactory,
|
||||
protected HashService $hashService,
|
||||
private FileNameValidator $fileNameValidator,
|
||||
private ProcessedFileRepository $processedFileRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Main method to dump a file
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function dumpAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parameters = $this->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Imaging\IconState;
|
||||
|
||||
/**
|
||||
* Controller for icon handling
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[AsController]
|
||||
readonly class IconController
|
||||
{
|
||||
public function __construct(
|
||||
private IconFactory $iconFactory
|
||||
) {}
|
||||
|
||||
public function getIcon(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$requestedIcon = json_decode($parsedBody['icon'] ?? $queryParams['icon'], true);
|
||||
|
||||
[$identifier, $size, $overlayIdentifier, $iconState, $alternativeMarkupIdentifier] = $requestedIcon;
|
||||
|
||||
if (empty($overlayIdentifier)) {
|
||||
$overlayIdentifier = null;
|
||||
}
|
||||
|
||||
$iconState = IconState::tryFrom($iconState);
|
||||
$icon = $this->iconFactory->getIcon($identifier, IconSize::from($size), $overlayIdentifier, $iconState);
|
||||
|
||||
return new HtmlResponse($icon->render($alternativeMarkupIdentifier));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGeneratorInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal Only to be used within TYPO3. Might change in the future.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class PasswordGeneratorController
|
||||
{
|
||||
public function __construct(
|
||||
private ResponseFactoryInterface $responseFactory,
|
||||
private StreamFactoryInterface $streamFactory,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function generate(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$passwordPolicy = $request->getParsedBody()['passwordPolicy'] ?? null;
|
||||
|
||||
try {
|
||||
if (is_string($passwordPolicy) && $passwordPolicy !== 'null') {
|
||||
$generator = $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'][$passwordPolicy]['generator'] ?? null;
|
||||
if (empty($generator['className'])
|
||||
|| !is_string($generator['className'])
|
||||
|| !class_exists($generator['className'])
|
||||
|| !isset($generator['options'])
|
||||
|| !is_array($generator['options'])
|
||||
) {
|
||||
throw new \LogicException(
|
||||
'The TYPO3_CONF_VARS.SYS.passwordPolicies.' . $passwordPolicy . '.generator configuration is misconfigured.'
|
||||
. ' Please ensure that the sub key \'className\' is set, and the sub key \'options\' is an array of required option values.',
|
||||
1770142937
|
||||
);
|
||||
}
|
||||
|
||||
$passwordGeneratorClassName = $generator['className'];
|
||||
$passwordGeneratorOptions = $generator['options'];
|
||||
|
||||
$passwordGenerator = GeneralUtility::makeInstance($passwordGeneratorClassName);
|
||||
if (!$passwordGenerator instanceof PasswordGeneratorInterface) {
|
||||
throw new \LogicException('Class ' . $passwordGeneratorClassName . ' does not implement PasswordGeneratorInterface', 1770142966);
|
||||
}
|
||||
|
||||
$password = $passwordGenerator->generate($passwordGeneratorOptions);
|
||||
return $this->createResponse([
|
||||
'success' => true,
|
||||
'password' => $password,
|
||||
]);
|
||||
}
|
||||
} catch (\LogicException $exception) {
|
||||
$this->logger->error('Password generation failed', ['exception' => $exception]);
|
||||
}
|
||||
|
||||
return $this->createResponse([
|
||||
'success' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function createResponse(array $data): ResponseInterface
|
||||
{
|
||||
return $this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
->withBody($this->streamFactory->createStream((string)json_encode($data)));
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user