TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<?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\Install\SystemEnvironment\ServerResponse;
|
||||
|
||||
/**
|
||||
* Evaluates a Content-Security-Policy HTTP header.
|
||||
*
|
||||
* @internal should only be used from within TYPO3 Core
|
||||
*/
|
||||
class ContentSecurityPolicyDirective
|
||||
{
|
||||
protected const RULE_PATTERN = '#(?:\'(?<instruction>[^\']+)\')|(?<source>[^\s]+)#';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $instructions = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $sources = [];
|
||||
|
||||
public function __construct(string $name, string $rule)
|
||||
{
|
||||
$this->name = $name;
|
||||
if (preg_match_all(self::RULE_PATTERN, $rule, $matches)) {
|
||||
foreach (array_keys($matches[0]) as $index) {
|
||||
if ($matches['instruction'][$index] !== '') {
|
||||
$this->instructions[] = $matches['instruction'][$index];
|
||||
} elseif ($matches['source'][$index] !== '') {
|
||||
$this->sources[] = $matches['source'][$index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getInstructions(): array
|
||||
{
|
||||
return $this->instructions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSources(): array
|
||||
{
|
||||
return $this->sources;
|
||||
}
|
||||
|
||||
public function hasInstructions(string ...$instructions): bool
|
||||
{
|
||||
return array_intersect($this->instructions, $instructions) !== [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?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\Install\SystemEnvironment\ServerResponse;
|
||||
|
||||
/**
|
||||
* Evaluates a Content-Security-Policy HTTP header.
|
||||
*
|
||||
* @internal should only be used from within TYPO3 Core
|
||||
*/
|
||||
class ContentSecurityPolicyHeader
|
||||
{
|
||||
protected const HEADER_PATTERN = '#(?<directive>default-src|script-src|style-src|object-src)\h+(?<rule>[^;]+)(?:\s*;\s*|$)#';
|
||||
|
||||
/**
|
||||
* @var ContentSecurityPolicyDirective[]
|
||||
*/
|
||||
protected $directives = [];
|
||||
|
||||
public function __construct(string $header)
|
||||
{
|
||||
if (preg_match_all(self::HEADER_PATTERN, $header, $matches)) {
|
||||
foreach ($matches['directive'] as $index => $name) {
|
||||
$this->directives[$name] = new ContentSecurityPolicyDirective(
|
||||
$name,
|
||||
$matches['rule'][$index]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->directives);
|
||||
}
|
||||
|
||||
public function mitigatesCrossSiteScripting(?string $fileName = null): bool
|
||||
{
|
||||
$isSvg = str_ends_with($fileName ?? '', '.svg');
|
||||
$defaultSrc = isset($this->directives['default-src'])
|
||||
? $this->directiveMitigatesCrossSiteScripting($this->directives['default-src'])
|
||||
: null;
|
||||
$scriptSrc = isset($this->directives['script-src'])
|
||||
? $this->directiveMitigatesCrossSiteScripting($this->directives['script-src'])
|
||||
: null;
|
||||
$styleSrc = isset($this->directives['style-src'])
|
||||
? $this->directiveMitigatesCrossSiteScripting($this->directives['style-src'])
|
||||
|| ($isSvg && $this->directives['style-src']->hasInstructions('unsafe-inline'))
|
||||
: null;
|
||||
$objectSrc = isset($this->directives['object-src'])
|
||||
? $this->directiveMitigatesCrossSiteScripting($this->directives['object-src'])
|
||||
: null;
|
||||
return ($scriptSrc ?? $defaultSrc ?? false)
|
||||
&& ($styleSrc ?? $defaultSrc ?? false)
|
||||
&& ($objectSrc ?? $defaultSrc ?? false);
|
||||
}
|
||||
|
||||
protected function directiveMitigatesCrossSiteScripting(ContentSecurityPolicyDirective $directive): bool
|
||||
{
|
||||
return $directive->hasInstructions('none')
|
||||
&& !$directive->hasInstructions('unsafe-eval', 'unsafe-inline');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?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\Install\SystemEnvironment\ServerResponse;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Declares contents on server response expectations on a static file.
|
||||
*
|
||||
* @internal should only be used from within TYPO3 Core
|
||||
*/
|
||||
class FileDeclaration
|
||||
{
|
||||
public const FLAG_BUILD_HTML = 1;
|
||||
public const FLAG_BUILD_PHP = 2;
|
||||
public const FLAG_BUILD_SVG = 4;
|
||||
public const FLAG_BUILD_HTML_DOCUMENT = 64;
|
||||
public const FLAG_BUILD_SVG_DOCUMENT = 128;
|
||||
|
||||
/**
|
||||
* @var FileLocation
|
||||
*/
|
||||
protected $fileLocation;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $fileName;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $fail;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $expectedContentType;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $unexpectedContentType;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $expectedContent;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $unexpectedContent;
|
||||
|
||||
/**
|
||||
* @var \Closure|null
|
||||
*/
|
||||
protected $handler;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $buildFlags = self::FLAG_BUILD_HTML | self::FLAG_BUILD_HTML_DOCUMENT;
|
||||
|
||||
public function __construct(FileLocation $fileLocation, string $fileName, bool $fail = false)
|
||||
{
|
||||
$this->fileLocation = $fileLocation;
|
||||
$this->fileName = $fileName;
|
||||
$this->fail = $fail;
|
||||
}
|
||||
|
||||
public function buildContent(): string
|
||||
{
|
||||
$content = '';
|
||||
if ($this->buildFlags & self::FLAG_BUILD_HTML) {
|
||||
$content .= '<div>HTML content</div>';
|
||||
}
|
||||
if ($this->buildFlags & self::FLAG_BUILD_PHP) {
|
||||
// base64 encoded representation of 'PHP content'
|
||||
$content .= '<div><?php echo base64_decode(\'UEhQIGNvbnRlbnQ=\');?></div>';
|
||||
}
|
||||
if ($this->buildFlags & self::FLAG_BUILD_SVG) {
|
||||
$content .= '<text id="test" x="0" y="0">SVG content</text>';
|
||||
}
|
||||
if ($this->buildFlags & self::FLAG_BUILD_SVG_DOCUMENT) {
|
||||
return sprintf(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg">%s</svg>',
|
||||
$content
|
||||
);
|
||||
}
|
||||
return sprintf(
|
||||
'<!DOCTYPE html><html lang="en"><body>%s</body></html>',
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
public function matches(ResponseInterface $response): bool
|
||||
{
|
||||
return $this->getMismatches($response) === [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StatusMessage[]
|
||||
*/
|
||||
public function getMismatches(ResponseInterface $response): array
|
||||
{
|
||||
$mismatches = [];
|
||||
if ($this->handler instanceof \Closure) {
|
||||
$result = $this->handler->call($this, $this, $response);
|
||||
if ($result !== null) {
|
||||
$mismatches[] = $result;
|
||||
}
|
||||
return $mismatches;
|
||||
}
|
||||
|
||||
$body = (string)$response->getBody();
|
||||
$contentType = $response->getHeaderLine('content-type');
|
||||
if ($this->expectedContent !== null && !str_contains($body, $this->expectedContent)) {
|
||||
$mismatches[] = new StatusMessage(
|
||||
'content mismatch %s',
|
||||
$this->expectedContent,
|
||||
$body
|
||||
);
|
||||
}
|
||||
if ($this->unexpectedContent !== null && str_contains($body, $this->unexpectedContent)) {
|
||||
$mismatches[] = new StatusMessage(
|
||||
'unexpected content %s',
|
||||
$this->unexpectedContent,
|
||||
$body
|
||||
);
|
||||
}
|
||||
if ($this->expectedContentType !== null
|
||||
&& !str_starts_with($contentType . ';', $this->expectedContentType . ';')) {
|
||||
$mismatches[] = new StatusMessage(
|
||||
'content-type mismatch %s, got %s',
|
||||
$this->expectedContentType,
|
||||
$contentType
|
||||
);
|
||||
}
|
||||
if ($this->unexpectedContentType !== null
|
||||
&& str_starts_with($contentType . ';', $this->unexpectedContentType . ';')) {
|
||||
$mismatches[] = new StatusMessage(
|
||||
'unexpected content-type %s',
|
||||
$this->unexpectedContentType,
|
||||
$contentType
|
||||
);
|
||||
}
|
||||
return $mismatches;
|
||||
}
|
||||
|
||||
public function withExpectedContentType(string $contentType): self
|
||||
{
|
||||
$target = clone $this;
|
||||
$target->expectedContentType = $contentType;
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function withUnexpectedContentType(string $contentType): self
|
||||
{
|
||||
$target = clone $this;
|
||||
$target->unexpectedContentType = $contentType;
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function withExpectedContent(string $content): self
|
||||
{
|
||||
$target = clone $this;
|
||||
$target->expectedContent = $content;
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function withUnexpectedContent(string $content): self
|
||||
{
|
||||
$target = clone $this;
|
||||
$target->unexpectedContent = $content;
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function withHandler(\Closure $handler): self
|
||||
{
|
||||
$target = clone $this;
|
||||
$target->handler = $handler;
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function withBuildFlags(int $buildFlags): self
|
||||
{
|
||||
$target = clone $this;
|
||||
$target->buildFlags = $buildFlags;
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function getFileLocation(): FileLocation
|
||||
{
|
||||
return $this->fileLocation;
|
||||
}
|
||||
|
||||
public function getFileName(): string
|
||||
{
|
||||
return $this->fileName;
|
||||
}
|
||||
|
||||
public function getUrl(ServerRequestInterface $request): string
|
||||
{
|
||||
return $this->fileLocation->getBaseUrl($request) . $this->fileName;
|
||||
}
|
||||
|
||||
public function shallFail(): bool
|
||||
{
|
||||
return $this->fail;
|
||||
}
|
||||
|
||||
public function getExpectedContentType(): ?string
|
||||
{
|
||||
return $this->expectedContentType;
|
||||
}
|
||||
|
||||
public function getUnexpectedContentType(): ?string
|
||||
{
|
||||
return $this->unexpectedContentType;
|
||||
}
|
||||
|
||||
public function getExpectedContent(): ?string
|
||||
{
|
||||
return $this->expectedContent;
|
||||
}
|
||||
|
||||
public function getUnexpectedContent(): ?string
|
||||
{
|
||||
return $this->unexpectedContent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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\Install\SystemEnvironment\ServerResponse;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* References local file path and corresponding HTTP base URL
|
||||
*
|
||||
* @internal should only be used from within TYPO3 Core
|
||||
*/
|
||||
class FileLocation
|
||||
{
|
||||
protected string $filePath;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->filePath = Environment::getPublicPath() . $path;
|
||||
}
|
||||
|
||||
public function getFilePath(): string
|
||||
{
|
||||
return $this->filePath;
|
||||
}
|
||||
|
||||
public function getBaseUrl(ServerRequestInterface $request): string
|
||||
{
|
||||
return $request->getAttribute('normalizedParams')->getRequestHost()
|
||||
. PathUtility::getAbsoluteWebPath($this->filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
<?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\Install\SystemEnvironment\ServerResponse;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\BadResponseException;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use GuzzleHttp\Promise\Utils;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Http\Uri;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Install\SystemEnvironment\CheckInterface;
|
||||
use TYPO3\CMS\Reports\Status;
|
||||
|
||||
/**
|
||||
* Checks how use web server is interpreting static files concerning
|
||||
* their `content-type` and evaluated content in HTTP responses.
|
||||
*
|
||||
* @internal should only be used from within TYPO3 Core
|
||||
*/
|
||||
class ServerResponseCheck implements CheckInterface
|
||||
{
|
||||
protected const WRAP_FLAT = 1;
|
||||
protected const WRAP_NESTED = 2;
|
||||
|
||||
/**
|
||||
* @var FlashMessageQueue
|
||||
*/
|
||||
protected $messageQueue;
|
||||
|
||||
/**
|
||||
* @var FileLocation
|
||||
*/
|
||||
protected $assetLocation;
|
||||
|
||||
/**
|
||||
* @var FileLocation
|
||||
*/
|
||||
protected $fileadminLocation;
|
||||
|
||||
/**
|
||||
* @var FileDeclaration[]
|
||||
*/
|
||||
protected $fileDeclarations;
|
||||
|
||||
public function __construct(
|
||||
protected readonly UriBuilder $uriBuilder,
|
||||
protected readonly bool $useMarkup = true,
|
||||
) {
|
||||
$fileName = bin2hex(random_bytes(4));
|
||||
$folderName = bin2hex(random_bytes(4));
|
||||
$this->assetLocation = new FileLocation(sprintf('/typo3temp/assets/%s.tmp/', $folderName));
|
||||
$fileadminDir = rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] ?? 'fileadmin', '/');
|
||||
$this->fileadminLocation = new FileLocation(sprintf('/%s/%s.tmp/', $fileadminDir, $folderName));
|
||||
$this->fileDeclarations = $this->initializeFileDeclarations($fileName);
|
||||
}
|
||||
|
||||
public function asStatus(ServerRequestInterface $request): Status
|
||||
{
|
||||
$messageQueue = $this->getStatus($request);
|
||||
$messages = [];
|
||||
foreach ($messageQueue->getAllMessages() as $flashMessage) {
|
||||
$messages[] = $flashMessage->getMessage();
|
||||
}
|
||||
$detailsLink = sprintf(
|
||||
'<p><a href="%s" rel="noreferrer" target="_blank">%s</a></p>',
|
||||
'https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/9.5.x/Feature-91354-IntegrateServerResponseSecurityChecks.html',
|
||||
'Please see documentation for further details...'
|
||||
);
|
||||
if ($messageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR) !== []) {
|
||||
$title = 'Potential vulnerabilities';
|
||||
$label = $detailsLink;
|
||||
$severity = ContextualFeedbackSeverity::ERROR;
|
||||
} elseif ($messageQueue->getAllMessages(ContextualFeedbackSeverity::WARNING) !== []) {
|
||||
$title = 'Warnings';
|
||||
$label = $detailsLink;
|
||||
$severity = ContextualFeedbackSeverity::WARNING;
|
||||
}
|
||||
return new Status(
|
||||
'Server Response',
|
||||
$title ?? 'OK',
|
||||
$this->wrapList($messages, $label ?? '', self::WRAP_NESTED),
|
||||
$severity ?? ContextualFeedbackSeverity::OK
|
||||
);
|
||||
}
|
||||
|
||||
public function getStatus(?ServerRequestInterface $request = null): FlashMessageQueue
|
||||
{
|
||||
if ($request === null) {
|
||||
throw new \RuntimeException('ServerResponseCheck requires a request', 1775761298);
|
||||
}
|
||||
$messageQueue = new FlashMessageQueue('install-server-response-check');
|
||||
if (PHP_SAPI === 'cli-server') {
|
||||
$messageQueue->addMessage(
|
||||
new FlashMessage(
|
||||
'Skipped for PHP_SAPI=cli-server',
|
||||
'Checks skipped',
|
||||
ContextualFeedbackSeverity::WARNING
|
||||
)
|
||||
);
|
||||
return $messageQueue;
|
||||
}
|
||||
try {
|
||||
$this->buildFileDeclarations();
|
||||
$this->processHostCheck($messageQueue);
|
||||
$this->processFileDeclarations($messageQueue, $request);
|
||||
$this->finishMessageQueue($messageQueue);
|
||||
} finally {
|
||||
$this->purgeFileDeclarations();
|
||||
}
|
||||
return $messageQueue;
|
||||
}
|
||||
|
||||
protected function initializeFileDeclarations(string $fileName): array
|
||||
{
|
||||
$cspClosure = function (FileDeclaration $fileDeclaration, ResponseInterface $response): ?StatusMessage {
|
||||
$cspHeader = new ContentSecurityPolicyHeader(
|
||||
$response->getHeaderLine('content-security-policy')
|
||||
);
|
||||
|
||||
if ($cspHeader->isEmpty()) {
|
||||
return new StatusMessage(
|
||||
'missing Content-Security-Policy for this location'
|
||||
);
|
||||
}
|
||||
if (!$cspHeader->mitigatesCrossSiteScripting($fileDeclaration->getFileName())) {
|
||||
return new StatusMessage(
|
||||
'weak Content-Security-Policy for this location "%s"',
|
||||
$response->getHeaderLine('content-security-policy')
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return [
|
||||
(new FileDeclaration($this->assetLocation, $fileName . '.html'))
|
||||
->withExpectedContentType('text/html')
|
||||
->withExpectedContent('HTML content'),
|
||||
(new FileDeclaration($this->assetLocation, $fileName . '.wrong'))
|
||||
->withUnexpectedContentType('text/html')
|
||||
->withExpectedContent('HTML content'),
|
||||
(new FileDeclaration($this->assetLocation, $fileName . '.html.wrong'))
|
||||
->withUnexpectedContentType('text/html')
|
||||
->withExpectedContent('HTML content'),
|
||||
(new FileDeclaration($this->assetLocation, $fileName . '.1.svg.wrong'))
|
||||
->withBuildFlags(FileDeclaration::FLAG_BUILD_SVG | FileDeclaration::FLAG_BUILD_SVG_DOCUMENT)
|
||||
->withUnexpectedContentType('image/svg+xml')
|
||||
->withExpectedContent('SVG content'),
|
||||
(new FileDeclaration($this->assetLocation, $fileName . '.2.svg.wrong'))
|
||||
->withBuildFlags(FileDeclaration::FLAG_BUILD_SVG | FileDeclaration::FLAG_BUILD_SVG_DOCUMENT)
|
||||
->withUnexpectedContentType('image/svg')
|
||||
->withExpectedContent('SVG content'),
|
||||
(new FileDeclaration($this->assetLocation, $fileName . '.php.wrong', true))
|
||||
->withBuildFlags(FileDeclaration::FLAG_BUILD_PHP | FileDeclaration::FLAG_BUILD_HTML_DOCUMENT)
|
||||
->withUnexpectedContent('PHP content'),
|
||||
(new FileDeclaration($this->assetLocation, $fileName . '.html.txt'))
|
||||
->withExpectedContentType('text/plain')
|
||||
->withUnexpectedContentType('text/html')
|
||||
->withExpectedContent('HTML content'),
|
||||
(new FileDeclaration($this->assetLocation, $fileName . '.php.txt', true))
|
||||
->withBuildFlags(FileDeclaration::FLAG_BUILD_PHP | FileDeclaration::FLAG_BUILD_HTML_DOCUMENT)
|
||||
->withUnexpectedContent('PHP content'),
|
||||
(new FileDeclaration($this->fileadminLocation, $fileName . '.html'))
|
||||
->withBuildFlags(FileDeclaration::FLAG_BUILD_HTML_DOCUMENT)
|
||||
->withHandler($cspClosure),
|
||||
(new FileDeclaration($this->fileadminLocation, $fileName . '.svg'))
|
||||
->withBuildFlags(FileDeclaration::FLAG_BUILD_SVG | FileDeclaration::FLAG_BUILD_SVG_DOCUMENT)
|
||||
->withHandler($cspClosure),
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildFileDeclarations(): void
|
||||
{
|
||||
foreach ($this->fileDeclarations as $fileDeclaration) {
|
||||
$filePath = $fileDeclaration->getFileLocation()->getFilePath();
|
||||
if (!is_dir($filePath)) {
|
||||
GeneralUtility::mkdir_deep($filePath);
|
||||
}
|
||||
GeneralUtility::writeFile(
|
||||
$filePath . $fileDeclaration->getFileName(),
|
||||
$fileDeclaration->buildContent(),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function purgeFileDeclarations(): void
|
||||
{
|
||||
GeneralUtility::rmdir($this->assetLocation->getFilePath(), true);
|
||||
GeneralUtility::rmdir($this->fileadminLocation->getFilePath(), true);
|
||||
}
|
||||
|
||||
protected function processHostCheck(FlashMessageQueue $messageQueue): void
|
||||
{
|
||||
$random = GeneralUtility::makeInstance(Random::class);
|
||||
$randomHost = $random->generateRandomHexString(10) . '.random.example.org';
|
||||
$time = (string)time();
|
||||
$hashService = GeneralUtility::makeInstance(HashService::class);
|
||||
$url = $this->uriBuilder->buildUriFromRoute(
|
||||
'install.server-response-check.host',
|
||||
['src-time' => $time, 'src-hash' => $hashService->hmac($time, 'server-response-check')],
|
||||
UriBuilder::ABSOLUTE_URL
|
||||
);
|
||||
try {
|
||||
$client = new Client(['timeout' => 10]);
|
||||
$response = $client->request('GET', (string)$url, [
|
||||
'headers' => ['Host' => $randomHost],
|
||||
'allow_redirects' => false,
|
||||
'verify' => false,
|
||||
]);
|
||||
} catch (TransferException $exception) {
|
||||
// it is expected that the previous request fails
|
||||
return;
|
||||
}
|
||||
// in case we end up here, the server processed an HTTP request with invalid HTTP host header
|
||||
$messageParts = [];
|
||||
$locationHeader = $response->getHeaderLine('location');
|
||||
if (!empty($locationHeader) && (new Uri($locationHeader))->getHost() === $randomHost) {
|
||||
$messageParts[] = sprintf('HTTP Location header contained unexpected "%s"', $randomHost);
|
||||
}
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
$serverHttpHost = $data['server.HTTP_HOST'] ?? null;
|
||||
$serverServerName = $data['server.SERVER_NAME'] ?? null;
|
||||
if ($serverHttpHost === $randomHost) {
|
||||
$messageParts[] = sprintf('HTTP_HOST contained unexpected "%s"', $randomHost);
|
||||
}
|
||||
if ($serverServerName === $randomHost) {
|
||||
$messageParts[] = sprintf('SERVER_NAME contained unexpected "%s"', $randomHost);
|
||||
}
|
||||
if ($messageParts !== []) {
|
||||
$messageQueue->addMessage(
|
||||
new FlashMessage(
|
||||
$this->wrapList($messageParts, (string)$url, self::WRAP_FLAT),
|
||||
'Unexpected server response',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processFileDeclarations(FlashMessageQueue $messageQueue, ServerRequestInterface $request): void
|
||||
{
|
||||
$promises = [];
|
||||
$client = new Client(['timeout' => 10]);
|
||||
foreach ($this->fileDeclarations as $fileDeclaration) {
|
||||
$promises[] = $client->requestAsync('GET', $fileDeclaration->getUrl($request));
|
||||
}
|
||||
foreach (Utils::settle($promises)->wait() as $index => $response) {
|
||||
$fileDeclaration = $this->fileDeclarations[$index];
|
||||
if (($response['reason'] ?? null) instanceof BadResponseException) {
|
||||
$messageQueue->addMessage(
|
||||
new FlashMessage(
|
||||
sprintf(
|
||||
'(%d): %s',
|
||||
$response['reason']->getCode(),
|
||||
$response['reason']->getRequest()->getUri()
|
||||
),
|
||||
'HTTP warning',
|
||||
ContextualFeedbackSeverity::WARNING
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!($response['value'] ?? null) instanceof ResponseInterface || $fileDeclaration->matches($response['value'])) {
|
||||
continue;
|
||||
}
|
||||
$messageQueue->addMessage(
|
||||
new FlashMessage(
|
||||
$this->createMismatchMessage($fileDeclaration, $response['value'], $request),
|
||||
'Unexpected server response',
|
||||
$fileDeclaration->shallFail() ? ContextualFeedbackSeverity::ERROR : ContextualFeedbackSeverity::WARNING
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function finishMessageQueue(FlashMessageQueue $messageQueue): void
|
||||
{
|
||||
if ($messageQueue->getAllMessages(ContextualFeedbackSeverity::WARNING) !== []
|
||||
|| $messageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR) !== []) {
|
||||
return;
|
||||
}
|
||||
$messageQueue->addMessage(
|
||||
new FlashMessage(
|
||||
sprintf('All %d files processed correctly', count($this->fileDeclarations)),
|
||||
'Expected server response',
|
||||
ContextualFeedbackSeverity::OK
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function createMismatchMessage(FileDeclaration $fileDeclaration, ResponseInterface $response, ServerRequestInterface $request): string
|
||||
{
|
||||
$messageParts = array_map(
|
||||
function (StatusMessage $mismatch): string {
|
||||
return vsprintf(
|
||||
$mismatch->getMessage(),
|
||||
$this->wrapValues($mismatch->getValues(), '<code>', '</code>')
|
||||
);
|
||||
},
|
||||
$fileDeclaration->getMismatches($response)
|
||||
);
|
||||
return $this->wrapList($messageParts, $fileDeclaration->getUrl($request), self::WRAP_FLAT);
|
||||
}
|
||||
|
||||
protected function wrapList(array $items, string $label, int $style): string
|
||||
{
|
||||
if (!$this->useMarkup) {
|
||||
return sprintf(
|
||||
'%s%s',
|
||||
$label ? $label . ': ' : '',
|
||||
implode(', ', $items)
|
||||
);
|
||||
}
|
||||
if ($style === self::WRAP_NESTED) {
|
||||
return sprintf(
|
||||
'%s<ul>%s</ul>',
|
||||
$label,
|
||||
implode('', $this->wrapItems($items, '<li>', '</li>'))
|
||||
);
|
||||
}
|
||||
return sprintf(
|
||||
'<p>%s%s</p>',
|
||||
$label,
|
||||
implode('', $this->wrapItems($items, '<br>', ''))
|
||||
);
|
||||
}
|
||||
|
||||
protected function wrapItems(array $items, string $before, string $after): array
|
||||
{
|
||||
return array_map(
|
||||
function (string $item) use ($before, $after): string {
|
||||
return $before . $item . $after;
|
||||
},
|
||||
array_filter($items)
|
||||
);
|
||||
}
|
||||
|
||||
protected function wrapValues(array $values, string $before, string $after): array
|
||||
{
|
||||
return array_map(
|
||||
function (string $value) use ($before, $after): string {
|
||||
return $this->wrapValue($value, $before, $after);
|
||||
},
|
||||
array_filter($values)
|
||||
);
|
||||
}
|
||||
|
||||
protected function wrapValue(string $value, string $before, string $after): string
|
||||
{
|
||||
if ($this->useMarkup) {
|
||||
return $before . htmlspecialchars($value) . $after;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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\Install\SystemEnvironment\ServerResponse;
|
||||
|
||||
/**
|
||||
* @internal should only be used from within TYPO3 Core
|
||||
*/
|
||||
class StatusMessage
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $values;
|
||||
|
||||
public function __construct(protected readonly string $message, string ...$values)
|
||||
{
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getValues(): array
|
||||
{
|
||||
return $this->values;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user