TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\Core\ApplicationInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractApplication implements ApplicationInterface, RequestHandlerInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
protected ?RequestHandlerInterface $requestHandler;
|
||||
|
||||
/**
|
||||
* Outputs content
|
||||
*/
|
||||
protected function sendResponse(ResponseInterface $response): void
|
||||
{
|
||||
if ($response instanceof NullResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @todo This requires some merge strategy or header callback handling
|
||||
if (!headers_sent()) {
|
||||
// If the response code was not changed by legacy code (still is 200)
|
||||
// then allow the PSR-7 response object to explicitly set it.
|
||||
// Otherwise let legacy code take precedence.
|
||||
// This code path can be deprecated once we expose the response object to third party code
|
||||
if (http_response_code() === 200) {
|
||||
header('HTTP/' . $response->getProtocolVersion() . ' ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
|
||||
}
|
||||
|
||||
foreach ($response->getHeaders() as $name => $values) {
|
||||
// Allow replacement for first occurrence of header but afterward do not replace
|
||||
// to allow multiple headers to be sent, e.g. for Set-Cookie headers.
|
||||
$replace = true;
|
||||
foreach ($values as $value) {
|
||||
header($name . ': ' . $value, $replace);
|
||||
$replace = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
$body = $response->getBody();
|
||||
if ($body instanceof SelfEmittableStreamInterface) {
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
// Optimization for streams that use php functions like readfile() as fastpath for serving files.
|
||||
$body->emit();
|
||||
} else {
|
||||
echo $body->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$response = $this->requestHandler->handle($request);
|
||||
} catch (ImmediateResponseException $exception) {
|
||||
$response = $exception->getResponse();
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the application and shut it down afterwards
|
||||
*/
|
||||
final public function run()
|
||||
{
|
||||
try {
|
||||
$request = ServerRequestFactory::fromGlobals();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->logger?->debug('Rejected invalid request: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
'exception' => $e,
|
||||
]);
|
||||
$this->sendResponse(new Response(null, 400));
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $this->handle($request);
|
||||
$this->sendResponse($response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\Error\MethodNotAllowedException;
|
||||
|
||||
trait AllowedMethodsTrait
|
||||
{
|
||||
/**
|
||||
* Assert if request method matches allowed HTTP methods and throw exception on mismatch.
|
||||
*
|
||||
* @param non-empty-string ...$allowedHttpMethods
|
||||
* @throws MethodNotAllowedException
|
||||
*/
|
||||
protected function assertAllowedHttpMethod(ServerRequestInterface $request, string ...$allowedHttpMethods): void
|
||||
{
|
||||
if (array_filter($allowedHttpMethods) === []) {
|
||||
throw new \LogicException(
|
||||
'Allowed HTTP methods cannot be empty.',
|
||||
1732188461,
|
||||
);
|
||||
}
|
||||
if (!in_array($request->getMethod(), $allowedHttpMethods, true)) {
|
||||
throw new MethodNotAllowedException($allowedHttpMethods, 1732193708);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
|
||||
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||
|
||||
/**
|
||||
* Entry point for TYPO3
|
||||
*/
|
||||
class Application extends AbstractApplication
|
||||
{
|
||||
public function __construct(
|
||||
#[AutowireInline(
|
||||
class: MiddlewareDispatcher::class,
|
||||
arguments: [
|
||||
'$kernel' => '@' . RequestHandler::class,
|
||||
'$middlewares' => '@core.middlewares',
|
||||
],
|
||||
)]
|
||||
RequestHandlerInterface $requestHandler,
|
||||
protected readonly ConfigurationManager $configurationManager,
|
||||
) {
|
||||
$this->requestHandler = $requestHandler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
|
||||
|
||||
/**
|
||||
* Helper class to answer "Is this a frontend or backend request?".
|
||||
*
|
||||
* It requires a PSR-7 ServerRequestInterface request object. Typical usage:
|
||||
*
|
||||
* ```
|
||||
* ApplicationType::fromRequest($request)->isFrontend()
|
||||
* ```
|
||||
*
|
||||
* Note the final request object is given to your controller by the frontend and
|
||||
* backend RequestHandler's. This request should be used in code calling this class.
|
||||
* However, various library parts of the TYPO3 core do not receive the request
|
||||
* object directly, so extensions may not receive it either. To work around
|
||||
* this technical debt for now, the RequestHandler's set the final request object
|
||||
* as $GLOBALS['TYPO3_REQUEST'], which can be used in those cases to feed this class.
|
||||
* Also note that CLI calls often do NOT create a request object, depending on their task.
|
||||
*
|
||||
* Classes that may be called from CLI without request object thus use this helper like:
|
||||
*
|
||||
* ```
|
||||
* // Do something special if this is a frontend request.
|
||||
* if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
|
||||
* && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()
|
||||
* ) {
|
||||
* ```
|
||||
*
|
||||
* Important: $GLOBALS['TYPO3_REQUEST'] is NOT available before the RequestHandler has been
|
||||
* called. This especially means the question "Is this a frontend or backend request?"
|
||||
* can NOT be answered in the TYPO3 bootstrap related extension files ext_localconf.php
|
||||
* and Configuration/TCA/* files.
|
||||
*/
|
||||
enum ApplicationType: string
|
||||
{
|
||||
// SystemEnvironmentBuilder::REQUESTTYPE_BE
|
||||
case BACKEND = 'backend';
|
||||
// SystemEnvironmentBuilder::REQUESTTYPE_FE
|
||||
case FRONTEND = 'frontend';
|
||||
// SystemEnvironmentBuilder::REQUESTTYPE_INSTALL
|
||||
case INSTALL = 'install';
|
||||
|
||||
/**
|
||||
* Create an ApplicationType object from a given PSR-7 request.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public static function fromRequest(ServerRequestInterface $request): self
|
||||
{
|
||||
$type = $request->getAttribute('applicationType');
|
||||
if (!is_int($type)) {
|
||||
// Request object has no valid type. Type is set by the Frontend / Backend / Install
|
||||
// application. If it's missing, we've not been called behind a legit TYPO3 application object.
|
||||
// This is bogus, we throw a generic RuntimeException that should not be caught.
|
||||
throw new \RuntimeException('No valid attribute "applicationType" found in request object.', 1606222812);
|
||||
}
|
||||
if (($type & SystemEnvironmentBuilder::REQUESTTYPE_FE) === SystemEnvironmentBuilder::REQUESTTYPE_FE) {
|
||||
return self::FRONTEND;
|
||||
}
|
||||
if (($type & SystemEnvironmentBuilder::REQUESTTYPE_BE) === SystemEnvironmentBuilder::REQUESTTYPE_BE) {
|
||||
return self::BACKEND;
|
||||
}
|
||||
if (($type & SystemEnvironmentBuilder::REQUESTTYPE_INSTALL) === SystemEnvironmentBuilder::REQUESTTYPE_INSTALL) {
|
||||
return self::INSTALL;
|
||||
}
|
||||
throw new \LogicException('Could not resolve application type to either frontend or backend', 1678875015);
|
||||
}
|
||||
|
||||
public function abbreviate(): ?string
|
||||
{
|
||||
return self::abbreviations()[$this] ?? null;
|
||||
}
|
||||
|
||||
public function isFrontend(): bool
|
||||
{
|
||||
return $this === self::FRONTEND;
|
||||
}
|
||||
|
||||
public function isBackend(): bool
|
||||
{
|
||||
return $this === self::BACKEND;
|
||||
}
|
||||
|
||||
private static function abbreviations(): \WeakMap
|
||||
{
|
||||
$map = new \WeakMap();
|
||||
$map[self::FRONTEND] = 'FE';
|
||||
$map[self::BACKEND] = 'BE';
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?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\Http\Client;
|
||||
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use GuzzleHttp\Promise\RejectedPromise;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Guzzle client middleware for filtering allowed request targets (SSRF prevention)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class AllowedHostsMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private string $context,
|
||||
private array $allowedHosts,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param callable(RequestInterface, array): PromiseInterface $nextHandler
|
||||
* @return callable(RequestInterface $request, array $options): PromiseInterface
|
||||
*/
|
||||
public function __invoke(callable $nextHandler): callable
|
||||
{
|
||||
return fn(RequestInterface $request, array $options): PromiseInterface
|
||||
=> $this->matches($request->getUri()->getHost())
|
||||
? $nextHandler($request, $options)
|
||||
: new RejectedPromise(sprintf(
|
||||
'Requested host \'%s\' is not allowed in $GLOBALS[\'TYPO3_CONF_VARS\'][\'HTTP\'][\'allowed_hosts\'][\'%s\']',
|
||||
$request->getUri()->getHost(),
|
||||
$this->context
|
||||
));
|
||||
}
|
||||
|
||||
private function matches(string $host): bool
|
||||
{
|
||||
foreach ($this->allowedHosts as $allowedHost) {
|
||||
// Match wildcards
|
||||
if (str_contains($allowedHost, '*')) {
|
||||
$expr = implode(
|
||||
'.+',
|
||||
array_map(
|
||||
static fn(string $part): string => preg_quote($part, '/'),
|
||||
explode('*', $allowedHost)
|
||||
)
|
||||
);
|
||||
if (preg_match('/^' . $expr . '$/', $host)) {
|
||||
return true;
|
||||
}
|
||||
} elseif ($allowedHost === $host) {
|
||||
// Exact matches
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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\Http\Client;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
readonly class GuzzleClientFactory
|
||||
{
|
||||
/**
|
||||
* Creates the client to do requests
|
||||
*/
|
||||
public function getClient(?string $context = null): ClientInterface
|
||||
{
|
||||
$httpOptions = $GLOBALS['TYPO3_CONF_VARS']['HTTP'];
|
||||
$httpOptions['verify'] = filter_var($httpOptions['verify'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $httpOptions['verify'];
|
||||
|
||||
// HEADS UP:
|
||||
// Passing a guzzle handler stack instead of an array of middlewares has never been documented,
|
||||
// but was theoretically possible since the introduction of handler middlewares.
|
||||
// This is not considered API (we can not control the ordering of the AllowedHosts Middleware)
|
||||
// but is preserved for maximum compatibility for now. It may vanish in a major release without notice.
|
||||
$stack = ($httpOptions['handler'] ?? null) instanceof HandlerStack ? $httpOptions['handler'] : HandlerStack::create();
|
||||
|
||||
if ($context !== null) {
|
||||
$allowedHosts = $httpOptions['allowed_hosts'][$context] ?? null;
|
||||
if (is_array($allowedHosts)) {
|
||||
$stack->push(
|
||||
new AllowedHostsMiddleware($context, array_filter($allowedHosts, is_string(...))),
|
||||
'typo3_allowed_hosts'
|
||||
);
|
||||
}
|
||||
}
|
||||
unset($httpOptions['allowed_hosts']);
|
||||
|
||||
if (is_array($httpOptions['handler'] ?? null)) {
|
||||
foreach ($httpOptions['handler'] as $name => $handler) {
|
||||
$stack->push($handler, (string)$name);
|
||||
}
|
||||
}
|
||||
|
||||
$httpOptions['handler'] = $stack;
|
||||
return new Client($httpOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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\Http;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
|
||||
trait CookieHeaderTrait
|
||||
{
|
||||
/**
|
||||
* @return Cookie::SAMESITE_*
|
||||
*/
|
||||
private function sanitizeSameSiteCookieValue(string $cookieSameSite): string
|
||||
{
|
||||
if (!in_array($cookieSameSite, [Cookie::SAMESITE_STRICT, Cookie::SAMESITE_LAX, Cookie::SAMESITE_NONE], true)) {
|
||||
$cookieSameSite = Cookie::SAMESITE_STRICT;
|
||||
}
|
||||
return $cookieSameSite;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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\Http;
|
||||
|
||||
final readonly class CookieScope
|
||||
{
|
||||
public function __construct(
|
||||
public string $domain,
|
||||
public bool $hostOnly,
|
||||
public string $path,
|
||||
) {}
|
||||
}
|
||||
@@ -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\Http;
|
||||
|
||||
trait CookieScopeTrait
|
||||
{
|
||||
/**
|
||||
* Returns the domain and path to be used for setting cookies.
|
||||
* The information is taken from the value in $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'] if set,
|
||||
* otherwise the normalized request params are used.
|
||||
*/
|
||||
private function getCookieScope(NormalizedParams $normalizedParams): CookieScope
|
||||
{
|
||||
$cookieDomain = $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'] ?? '';
|
||||
// If a specific cookie domain is defined for a given application type, use that domain
|
||||
if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'])) {
|
||||
$cookieDomain = $GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'];
|
||||
}
|
||||
if (!$cookieDomain) {
|
||||
return new CookieScope(
|
||||
domain: $normalizedParams->getRequestHostOnly(),
|
||||
hostOnly: true,
|
||||
// If no cookie domain is set, use the base path
|
||||
path: $normalizedParams->getSitePath(),
|
||||
);
|
||||
}
|
||||
if ($cookieDomain[0] === '/') {
|
||||
$match = [];
|
||||
$matchCount = @preg_match($cookieDomain, $normalizedParams->getRequestHostOnly(), $match);
|
||||
if ($matchCount === false) {
|
||||
$this->logger->critical(
|
||||
'The regular expression for the cookie domain ({domain}) contains errors. The session is not shared across sub-domains.',
|
||||
['domain' => $cookieDomain]
|
||||
);
|
||||
}
|
||||
if ($matchCount === false || $matchCount === 0) {
|
||||
return new CookieScope(
|
||||
domain: $normalizedParams->getRequestHostOnly(),
|
||||
hostOnly: true,
|
||||
// If no cookie domain could be matched, use the base path
|
||||
path: $normalizedParams->getSitePath(),
|
||||
);
|
||||
}
|
||||
$cookieDomain = $match[0];
|
||||
}
|
||||
|
||||
return new CookieScope(
|
||||
// Normalize cookie domain by removing leading and trailing dots,
|
||||
// see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.2.3
|
||||
// > Note that a leading %x2E ("."), if present, is ignored even though that character is not permitted,
|
||||
// > but a trailing %x2E ("."), if present, will cause the user agent to ignore the attribute.
|
||||
domain: trim($cookieDomain, '.'),
|
||||
hostOnly: false,
|
||||
path: '/',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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\Http;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Dispatcher which resolves a target, which was given to the request to call a controller and method (but also a callable)
|
||||
* where the request contains a "target" as attribute.
|
||||
*
|
||||
* Used in eID Frontend Requests, see EidHandler
|
||||
*/
|
||||
class Dispatcher implements DispatcherInterface
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method that fetches the target from the request and calls the target directly
|
||||
*
|
||||
* @param ServerRequestInterface $request the current server request
|
||||
* @return ResponseInterface the filled response by the callable/controller/action
|
||||
* @throws \InvalidArgumentException if the defined target is invalid
|
||||
*/
|
||||
public function dispatch(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$targetIdentifier = $request->getAttribute('target');
|
||||
$target = $this->getCallableFromTarget($targetIdentifier);
|
||||
$arguments = [$request];
|
||||
return $target(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callable out of the given parameter, which can be a string, a callable / closure or an array
|
||||
* which can be invoked as a function.
|
||||
*
|
||||
* @param array|string|callable $target the target which is being resolved.
|
||||
* @return callable
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function getCallableFromTarget($target)
|
||||
{
|
||||
if (is_array($target)) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
if ($target instanceof \Closure) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
// Only a class name is given
|
||||
if (is_string($target) && !str_contains($target, ':')) {
|
||||
$targetObject = $this->container->has($target) ? $this->container->get($target) : GeneralUtility::makeInstance($target);
|
||||
if (!method_exists($targetObject, '__invoke')) {
|
||||
throw new \InvalidArgumentException('Object "' . $target . '" doesn\'t implement an __invoke() method and cannot be used as target.', 1442431631);
|
||||
}
|
||||
return $targetObject;
|
||||
}
|
||||
|
||||
// Check if the target is a concatenated string of "className::actionMethod"
|
||||
if (is_string($target) && str_contains($target, '::')) {
|
||||
[$className, $methodName] = explode('::', $target, 2);
|
||||
$targetObject = $this->container->has($className) ? $this->container->get($className) : GeneralUtility::makeInstance($className);
|
||||
return [$targetObject, $methodName];
|
||||
}
|
||||
|
||||
// Closures needs to be checked at last as a string with object::method is recognized as callable
|
||||
if (is_callable($target)) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Invalid target for "' . $target . '", as it is not callable.', 1425381442);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* An interface for dispatcher that delegate requests to a certain callable, typically a
|
||||
* controller / action combination. Usually called from the RequestHandler.
|
||||
*
|
||||
* @internal This low level interface is used core internally only
|
||||
*/
|
||||
interface DispatcherInterface
|
||||
{
|
||||
/**
|
||||
* Main method to dispatch a request and its response to a callable object
|
||||
*/
|
||||
public function dispatch(ServerRequestInterface $request): ResponseInterface;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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\Http\Error;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class MethodNotAllowedException extends Exception
|
||||
{
|
||||
public readonly array $allowedMethods;
|
||||
|
||||
/**
|
||||
* @param non-empty-list<non-empty-string> $allowedMethods
|
||||
*/
|
||||
public function __construct(array $allowedMethods, int $code = 0, ?\Throwable $previous = null)
|
||||
{
|
||||
$this->allowedMethods = array_map('strtoupper', $allowedMethods);
|
||||
|
||||
parent::__construct(
|
||||
sprintf(
|
||||
'HTTP method is not allowed! Allowed method(s): %s',
|
||||
implode(', ', $this->allowedMethods),
|
||||
),
|
||||
$code,
|
||||
$previous,
|
||||
);
|
||||
}
|
||||
|
||||
public function createResponse(): ResponseInterface
|
||||
{
|
||||
return new HtmlResponse($this->message, 405, ['Allow' => implode(', ', $this->allowedMethods)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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\Http;
|
||||
|
||||
use GuzzleHttp\Psr7\StreamDecoratorTrait;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
|
||||
|
||||
/**
|
||||
* A lazy stream, that wraps the FAL dumpFileContents() method to send file contents
|
||||
* using emit(), as defined in SelfEmittableStreamInterface.
|
||||
* This call will fall back to the FAL getFileContents() method if the fastpath possibility
|
||||
* using SelfEmittableStreamInterface is not used.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class FalDumpFileContentsDecoratorStream implements StreamInterface, SelfEmittableStreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
|
||||
public function __construct(
|
||||
protected string $identifier,
|
||||
protected DriverInterface $driver,
|
||||
protected int $size
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Emit the response to stdout, as specified in SelfEmittableStreamInterface.
|
||||
* Offload to the driver method dumpFileContents.
|
||||
*/
|
||||
public function emit()
|
||||
{
|
||||
$this->driver->dumpFileContents($this->identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a stream (on demand). This method is consumed by the guzzle StreamDecoratorTrait
|
||||
* and is used when this stream is used without the emit() fastpath.
|
||||
*/
|
||||
protected function createStream(): StreamInterface
|
||||
{
|
||||
$stream = new Stream('php://temp', 'rw');
|
||||
$stream->write($this->driver->getFileContents($this->identifier));
|
||||
return $stream;
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function isWritable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function write(string $string): int
|
||||
{
|
||||
throw new \RuntimeException('Cannot write to a ' . self::class, 1538331852);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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\Http;
|
||||
|
||||
/**
|
||||
* A default HTML response object
|
||||
*
|
||||
* Highly inspired by ZF zend-diactoros
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
class HtmlResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Creates a HTML response object with a default 200 response code
|
||||
*
|
||||
* @param string $content HTML content written to the response
|
||||
* @param int $status status code for the response; defaults to 200.
|
||||
* @param array $headers Additional headers to be set.
|
||||
*/
|
||||
public function __construct(string $content, int $status = 200, array $headers = [])
|
||||
{
|
||||
$body = new Stream('php://temp', 'wb+');
|
||||
$body->write($content);
|
||||
$body->rewind();
|
||||
parent::__construct($body, $status, $headers);
|
||||
|
||||
// Ensure that text/html header is set, if Content-Type was not set before
|
||||
if (!$this->hasHeader('Content-Type')) {
|
||||
$this->headers['Content-Type'][] = 'text/html; charset=utf-8';
|
||||
$this->lowercasedHeaderNames['content-type'] = 'Content-Type';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Exception that has to be handled immediately in order to stop
|
||||
* current execution and provide the current response. This
|
||||
* exception is used as alternative to previous die() or exit().
|
||||
*
|
||||
* Note this exception is only caught by Application classes, throwing
|
||||
* it will bypass all outer middlewares. It should *not* be thrown by
|
||||
* controllers, those should usually throw a PropagateResponseException
|
||||
* instead, allowing outer middlewares to further process the response.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ImmediateResponseException extends \Exception
|
||||
{
|
||||
/**
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
private $response;
|
||||
|
||||
public function __construct(ResponseInterface $response, int $code = 0)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Http;
|
||||
|
||||
/**
|
||||
* Exception is thrown in case an invalid request url is used in CLI context
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class InvalidRequestUrlOnCliException extends \InvalidArgumentException {}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?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\Http;
|
||||
|
||||
/**
|
||||
* Standard values for a JSON response
|
||||
*
|
||||
* Highly inspired by ZF zend-diactoros
|
||||
*
|
||||
* @internal Note that this is not public API, use PSR-17 interfaces instead
|
||||
*/
|
||||
class JsonResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Default flags for json_encode; value of:
|
||||
*
|
||||
* <code>
|
||||
* JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES
|
||||
* </code>
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const DEFAULT_JSON_FLAGS = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES;
|
||||
|
||||
/**
|
||||
* Create a JSON response with the given data.
|
||||
*
|
||||
* Default JSON encoding is performed with the following options, which
|
||||
* produces RFC4627-compliant JSON, capable of embedding into HTML.
|
||||
*
|
||||
* - JSON_HEX_TAG
|
||||
* - JSON_HEX_APOS
|
||||
* - JSON_HEX_AMP
|
||||
* - JSON_HEX_QUOT
|
||||
* - JSON_UNESCAPED_SLASHES
|
||||
*
|
||||
* @param array|null $data Data to convert to JSON.
|
||||
* @param int $status Integer status code for the response; 200 by default.
|
||||
* @param array $headers Array of headers to use at initialization.
|
||||
* @param int $encodingOptions JSON encoding options to use.
|
||||
*/
|
||||
public function __construct(
|
||||
?array $data = [],
|
||||
int $status = 200,
|
||||
array $headers = [],
|
||||
int $encodingOptions = self::DEFAULT_JSON_FLAGS
|
||||
) {
|
||||
$body = new Stream('php://temp', 'wb+');
|
||||
parent::__construct($body, $status, $headers);
|
||||
|
||||
if ($data !== null) {
|
||||
$this->setPayload($data, $encodingOptions);
|
||||
}
|
||||
|
||||
// Ensure that application/json header is set, if Content-Type was not set before
|
||||
if (!$this->hasHeader('Content-Type')) {
|
||||
$this->headers['Content-Type'][] = 'application/json; charset=utf-8';
|
||||
$this->lowercasedHeaderNames['content-type'] = 'Content-Type';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the exiting content, takes an array as input
|
||||
*/
|
||||
public function setPayload(array $data = [], int $encodingOptions = self::DEFAULT_JSON_FLAGS): JsonResponse
|
||||
{
|
||||
$this->body->write($this->jsonEncode($data, $encodingOptions));
|
||||
$this->body->rewind();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the provided data to JSON.
|
||||
*
|
||||
* @throws \InvalidArgumentException if unable to encode the $data to JSON.
|
||||
*/
|
||||
private function jsonEncode(array $data, int $encodingOptions): string
|
||||
{
|
||||
// Clear json_last_error()
|
||||
json_encode(null);
|
||||
$json = json_encode($data, $encodingOptions);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Unable to encode data to JSON in %s: %s',
|
||||
__CLASS__,
|
||||
json_last_error_msg()
|
||||
), 1504972434);
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Default implementation for the MessageInterface of the PSR-7 standard
|
||||
* It is the base for any request or response for PSR-7.
|
||||
*
|
||||
* Highly inspired by https://github.com/phly/http/
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
class Message implements MessageInterface
|
||||
{
|
||||
/**
|
||||
* The HTTP Protocol version, defaults to 1.1
|
||||
*/
|
||||
protected string $protocolVersion = '1.1';
|
||||
|
||||
/**
|
||||
* Associative array containing all headers of this Message
|
||||
* This is a mixed-case list of the headers (as due to the specification)
|
||||
*/
|
||||
protected array $headers = [];
|
||||
|
||||
/**
|
||||
* Lowercased version of all headers, in order to check if a header is set or not
|
||||
* this way a lot of checks are easier to be set
|
||||
*/
|
||||
protected array $lowercasedHeaderNames = [];
|
||||
|
||||
/**
|
||||
* The body as a Stream object
|
||||
*/
|
||||
protected ?StreamInterface $body = null;
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP protocol version as a string.
|
||||
*
|
||||
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
|
||||
*
|
||||
* @return string HTTP protocol version.
|
||||
*/
|
||||
public function getProtocolVersion(): string
|
||||
{
|
||||
return $this->protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified HTTP protocol version.
|
||||
*
|
||||
* The version string MUST contain only the HTTP version number (e.g.,
|
||||
* "1.1", "1.0").
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new protocol version.
|
||||
*
|
||||
* @param string $version HTTP protocol version
|
||||
* @return static
|
||||
*/
|
||||
public function withProtocolVersion(string $version): MessageInterface
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->protocolVersion = $version;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all message header values.
|
||||
*
|
||||
* The keys represent the header name as it will be sent over the wire, and
|
||||
* each value is an array of strings associated with the header.
|
||||
*
|
||||
* ```
|
||||
* // Represent the headers as a string
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* echo $name . ": " . implode(", ", $values);
|
||||
* }
|
||||
*
|
||||
* // Emit headers iteratively:
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* foreach ($values as $value) {
|
||||
* header(sprintf('%s: %s', $name, $value), false);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||
* exact case in which headers were originally specified.
|
||||
*
|
||||
* @return array Returns an associative array of the message's headers. Each
|
||||
* key MUST be a header name, and each value MUST be an array of strings
|
||||
* for that header.
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a header exists by the given case-insensitive name.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return bool Returns true if any header names match the given header
|
||||
* name using a case-insensitive string comparison. Returns false if
|
||||
* no matching header name is found in the message.
|
||||
*/
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return isset($this->lowercasedHeaderNames[strtolower($name)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a message header value by the given case-insensitive name.
|
||||
*
|
||||
* This method returns an array of all the header values of the given
|
||||
* case-insensitive header name.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return an
|
||||
* empty array.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string[] An array of string values as provided for the given
|
||||
* header. If the header does not appear in the message, this method MUST
|
||||
* return an empty array.
|
||||
*/
|
||||
public function getHeader(string $name): array
|
||||
{
|
||||
if (!$this->hasHeader($name)) {
|
||||
return [];
|
||||
}
|
||||
$header = $this->lowercasedHeaderNames[strtolower($name)];
|
||||
$headerValue = $this->headers[$header];
|
||||
if (is_array($headerValue)) {
|
||||
return $headerValue;
|
||||
}
|
||||
return [$headerValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a comma-separated string of the values for a single header.
|
||||
*
|
||||
* This method returns all of the header values of the given
|
||||
* case-insensitive header name as a string concatenated together using
|
||||
* a comma.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return
|
||||
* an empty string.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string A string of values as provided for the given header
|
||||
* concatenated together using a comma. If the header does not appear in
|
||||
* the message, this method MUST return an empty string.
|
||||
*/
|
||||
public function getHeaderLine(string $name): string
|
||||
{
|
||||
$headerValue = $this->getHeader($name);
|
||||
if (empty($headerValue)) {
|
||||
return '';
|
||||
}
|
||||
return implode(',', $headerValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the provided value replacing the specified header.
|
||||
*
|
||||
* While header names are case-insensitive, the casing of the header will
|
||||
* be preserved by this function, and returned from getHeaders().
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new and/or updated header and value.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function withHeader(string $name, $value): MessageInterface
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
|
||||
throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The value must be a string or an array of strings.', 1436717266);
|
||||
}
|
||||
|
||||
$this->validateHeaderName($name);
|
||||
$this->validateHeaderValues($value);
|
||||
$lowercasedHeaderName = strtolower($name);
|
||||
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->headers[$name] = $value;
|
||||
$clonedObject->lowercasedHeaderNames[$lowercasedHeaderName] = $name;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified header appended with the given value.
|
||||
*
|
||||
* Existing values for the specified header will be maintained. The new
|
||||
* value(s) will be appended to the existing list. If the header did not
|
||||
* exist previously, it will be added.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new header and/or value.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to add.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function withAddedHeader(string $name, $value): MessageInterface
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
|
||||
throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The header value must be a string or array of strings', 1436717267);
|
||||
}
|
||||
$this->validateHeaderName($name);
|
||||
$this->validateHeaderValues($value);
|
||||
if (!$this->hasHeader($name)) {
|
||||
return $this->withHeader($name, $value);
|
||||
}
|
||||
$name = $this->lowercasedHeaderNames[strtolower($name)];
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->headers[$name] = array_merge($this->headers[$name], $value);
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance without the specified header.
|
||||
*
|
||||
* Header resolution MUST be done without case-sensitivity.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that removes
|
||||
* the named header.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to remove.
|
||||
* @return static
|
||||
*/
|
||||
public function withoutHeader(string $name): MessageInterface
|
||||
{
|
||||
if (!$this->hasHeader($name)) {
|
||||
return clone $this;
|
||||
}
|
||||
// fetch the original header from the lowercased version
|
||||
$lowercasedHeader = strtolower($name);
|
||||
$name = $this->lowercasedHeaderNames[$lowercasedHeader];
|
||||
$clonedObject = clone $this;
|
||||
unset($clonedObject->headers[$name], $clonedObject->lowercasedHeaderNames[$lowercasedHeader]);
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the body of the message.
|
||||
*
|
||||
* @return StreamInterface Returns the body as a stream.
|
||||
*/
|
||||
public function getBody(): StreamInterface
|
||||
{
|
||||
if ($this->body === null) {
|
||||
$this->body = new Stream('php://temp', 'r+');
|
||||
}
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified message body.
|
||||
*
|
||||
* The body MUST be a StreamInterface object.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return a new instance that has the
|
||||
* new body stream.
|
||||
*
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException When the body is not valid.
|
||||
*/
|
||||
public function withBody(StreamInterface $body): MessageInterface
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->body = $body;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure header names and values are valid.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function assertHeaders(array $headers): void
|
||||
{
|
||||
foreach ($headers as $name => $headerValues) {
|
||||
$this->validateHeaderName($name);
|
||||
// check if all values are correct
|
||||
array_walk($headerValues, static function (string $value, string $key, Message $messageObject): void {
|
||||
if (!$messageObject->isValidHeaderValue($value)) {
|
||||
throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717268);
|
||||
}
|
||||
}, $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a set of headers to ensure they are in the correct internal format.
|
||||
*
|
||||
* Used by message constructors to allow setting all initial headers at once.
|
||||
*
|
||||
* @param array $originalHeaders Headers to filter.
|
||||
* @return array Filtered headers and names.
|
||||
*/
|
||||
protected function filterHeaders(array $originalHeaders): array
|
||||
{
|
||||
$headerNames = $headers = [];
|
||||
foreach ($originalHeaders as $header => $value) {
|
||||
if (!is_string($header) || (!is_array($value) && !is_scalar($value))) {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($value)) {
|
||||
$value = [(string)$value];
|
||||
}
|
||||
$headerNames[strtolower($header)] = $header;
|
||||
$headers[$header] = $value;
|
||||
}
|
||||
return [$headerNames, $headers];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to test if an array contains only strings
|
||||
*/
|
||||
protected function arrayContainsOnlyStrings(array $data): bool
|
||||
{
|
||||
return array_reduce($data, static function ($original, $item) {
|
||||
return is_string($item) ? $original : false;
|
||||
}, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the provided header values are valid.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7230#section-3.2
|
||||
* @param string[] $values
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function validateHeaderValues(array $values): void
|
||||
{
|
||||
array_walk($values, static function (string $value, string $key, Message $messageObject): void {
|
||||
if (!$messageObject->isValidHeaderValue($value)) {
|
||||
throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717269);
|
||||
}
|
||||
}, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a header value
|
||||
*
|
||||
* Ensures CRLF header injection vectors are filtered.
|
||||
*
|
||||
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
|
||||
* tabs are allowed in values; header continuations MUST consist of
|
||||
* a single CRLF sequence followed by a space or horizontal tab.
|
||||
*
|
||||
* This method filters any values not allowed from the string, and is
|
||||
* lossy.
|
||||
*
|
||||
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
|
||||
* @todo: Unused? And why is this public? Maybe align with zend-diactoros again
|
||||
*/
|
||||
public function filter(string $value): string
|
||||
{
|
||||
$length = strlen($value);
|
||||
$string = '';
|
||||
for ($i = 0; $i < $length; $i += 1) {
|
||||
$ascii = ord($value[$i]);
|
||||
|
||||
// Detect continuation sequences
|
||||
if ($ascii === 13) {
|
||||
$lf = ord($value[$i + 1]);
|
||||
$ws = ord($value[$i + 2]);
|
||||
if ($lf === 10 && in_array($ws, [9, 32], true)) {
|
||||
$string .= $value[$i] . $value[$i + 1];
|
||||
$i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-visible, non-whitespace characters
|
||||
// 9 === horizontal tab
|
||||
// 32-126, 128-254 === visible
|
||||
// 127 === DEL
|
||||
// 255 === null byte
|
||||
if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$string .= $value[$i];
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a header name is valid and throw an exception.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7230#section-3.2
|
||||
* @throws \InvalidArgumentException
|
||||
* @todo: Review. Should be protected / private, maybe align with zend-diactoros again
|
||||
*/
|
||||
public function validateHeaderName(string $name): void
|
||||
{
|
||||
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
|
||||
throw new \InvalidArgumentException('Invalid header name, given "' . $name . '"', 1436717270);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an HTTP header value is valid.
|
||||
*
|
||||
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
|
||||
* tabs are allowed in values; header continuations MUST consist of
|
||||
* a single CRLF sequence followed by a space or horizontal tab.
|
||||
*
|
||||
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
|
||||
* @todo: Review. Should be protected / private, maybe align with zend-diactoros again
|
||||
*/
|
||||
public function isValidHeaderValue(string $value): bool
|
||||
{
|
||||
// Any occurrence of \r or \n is invalid
|
||||
if (strpbrk($value, "\r\n") !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (unpack('C*', $value) as $ascii) {
|
||||
// Non-visible, non-whitespace characters
|
||||
// 9 === horizontal tab
|
||||
// 32-126, 128-254 === visible
|
||||
// 127 === DEL
|
||||
// 255 === null byte
|
||||
if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* MiddlewareDispatcher
|
||||
*
|
||||
* This class manages and dispatches a PSR-15 middleware stack.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MiddlewareDispatcher implements RequestHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Tip of the middleware call stack
|
||||
*
|
||||
* @var RequestHandlerInterface
|
||||
*/
|
||||
protected $tip;
|
||||
|
||||
/**
|
||||
* @var ContainerInterface|null
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
public function __construct(
|
||||
RequestHandlerInterface $kernel,
|
||||
iterable $middlewares = [],
|
||||
?ContainerInterface $container = null
|
||||
) {
|
||||
$this->container = $container;
|
||||
$this->seedMiddlewareStack($kernel);
|
||||
|
||||
foreach ($middlewares as $middleware) {
|
||||
if (is_string($middleware)) {
|
||||
$this->lazy($middleware);
|
||||
} else {
|
||||
$this->add($middleware);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the middleware stack
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->tip->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the middleware stack with the inner request handler
|
||||
*/
|
||||
protected function seedMiddlewareStack(RequestHandlerInterface $kernel)
|
||||
{
|
||||
$this->tip = $kernel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new middleware to the stack
|
||||
*
|
||||
* Middlewares are organized as a stack. That means middlewares
|
||||
* that have been added before will be executed after the newly
|
||||
* added one (last in, first out).
|
||||
*/
|
||||
public function add(MiddlewareInterface $middleware): void
|
||||
{
|
||||
$next = $this->tip;
|
||||
$this->tip = new class ($middleware, $next) implements RequestHandlerInterface {
|
||||
public function __construct(private readonly MiddlewareInterface $middleware, private readonly RequestHandlerInterface $next) {}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->middleware->process($request, $this->next);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new middleware by class name
|
||||
*
|
||||
* Middlewares are organized as a stack. That means middlewares
|
||||
* that have been added before will be executed after the newly
|
||||
* added one (last in, first out).
|
||||
*
|
||||
* @param string $middleware
|
||||
*/
|
||||
public function lazy(string $middleware): void
|
||||
{
|
||||
$next = $this->tip;
|
||||
$this->tip = new class ($middleware, $next, $this->container) implements RequestHandlerInterface {
|
||||
public function __construct(
|
||||
private readonly string $middleware,
|
||||
private readonly RequestHandlerInterface $next,
|
||||
private readonly ?ContainerInterface $container = null
|
||||
) {}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($this->container !== null && $this->container->has($this->middleware)) {
|
||||
$middleware = $this->container->get($this->middleware);
|
||||
} else {
|
||||
$middleware = GeneralUtility::makeInstance($this->middleware);
|
||||
}
|
||||
|
||||
if (!$middleware instanceof MiddlewareInterface) {
|
||||
throw new \InvalidArgumentException(get_class($middleware) . ' does not implement ' . MiddlewareInterface::class, 1516821342);
|
||||
}
|
||||
return $middleware->process($request, $this->next);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend as PhpFrontendCache;
|
||||
use TYPO3\CMS\Core\Service\DependencyOrderingService;
|
||||
|
||||
/**
|
||||
* This class resolves middleware stacks from defined configuration in all active packages.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MiddlewareStackResolver
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var DependencyOrderingService
|
||||
*/
|
||||
protected $dependencyOrderingService;
|
||||
|
||||
/**
|
||||
* @var PhpFrontendCache
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
private string $baseCacheIdentifier;
|
||||
|
||||
public function __construct(
|
||||
ContainerInterface $container,
|
||||
DependencyOrderingService $dependencyOrderingService,
|
||||
#[Autowire(service: 'cache.core')]
|
||||
PhpFrontendCache $cache,
|
||||
#[Autowire(expression: 'service("package-dependent-cache-identifier").toString()')]
|
||||
string $baseCacheIdentifier
|
||||
) {
|
||||
$this->container = $container;
|
||||
$this->dependencyOrderingService = $dependencyOrderingService;
|
||||
$this->cache = $cache;
|
||||
$this->baseCacheIdentifier = '_' . $baseCacheIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the middleware stack registered in all packages within Configuration/RequestMiddlewares.php
|
||||
* which are sorted by given dependency requirements
|
||||
*
|
||||
* @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException
|
||||
* @throws \TYPO3\CMS\Core\Exception
|
||||
*/
|
||||
public function resolve(string $stackName): \ArrayObject
|
||||
{
|
||||
return new \ArrayObject($this->getFromCache($stackName) ?? $this->computeMiddlewareStack($stackName));
|
||||
}
|
||||
|
||||
protected function getFromCache(string $stackName): ?array
|
||||
{
|
||||
$cacheIdentifier = $this->getCacheIdentifier($stackName);
|
||||
if (!$this->cache->has($cacheIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
$result = $this->cache->require($cacheIdentifier);
|
||||
if ($result === false) {
|
||||
// Cache entry has been removed in the meantime
|
||||
return null;
|
||||
}
|
||||
if (!is_array($result)) {
|
||||
// An invalid result is to be ignored (cache will be recreated)
|
||||
return null;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function computeMiddlewareStack(string $stackName): array
|
||||
{
|
||||
$allMiddlewares = $this->loadConfiguration();
|
||||
$middlewares = $this->sanitizeMiddlewares($allMiddlewares);
|
||||
|
||||
// Ensure that we create a cache for $stackName, even if the stack is empty
|
||||
if (!isset($middlewares[$stackName])) {
|
||||
$middlewares[$stackName] = [];
|
||||
}
|
||||
|
||||
foreach ($middlewares as $stack => $middlewaresOfStack) {
|
||||
$this->cache->set($this->getCacheIdentifier($stack), 'return ' . var_export($middlewaresOfStack, true) . ';');
|
||||
}
|
||||
|
||||
return $middlewares[$stackName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load configuration from the container
|
||||
*/
|
||||
protected function loadConfiguration(): \ArrayObject
|
||||
{
|
||||
return $this->container->get('middlewares');
|
||||
}
|
||||
|
||||
/**
|
||||
* Order each stack and sanitize to a plain array
|
||||
*/
|
||||
protected function sanitizeMiddlewares(\ArrayObject $allMiddlewares): array
|
||||
{
|
||||
$middlewares = [];
|
||||
|
||||
foreach ($allMiddlewares as $stack => $middlewaresOfStack) {
|
||||
$middlewaresOfStack = $this->dependencyOrderingService->orderByDependencies($middlewaresOfStack);
|
||||
|
||||
$sanitizedMiddlewares = [];
|
||||
foreach ($middlewaresOfStack as $name => $middleware) {
|
||||
if (isset($middleware['disabled']) && $middleware['disabled'] === true) {
|
||||
// Skip this middleware if disabled by configuration
|
||||
continue;
|
||||
}
|
||||
$sanitizedMiddlewares[$name] = $middleware['target'];
|
||||
}
|
||||
|
||||
// Order reverse, MiddlewareDispatcher executes the last middleware in the array first (last in, first out).
|
||||
$middlewares[$stack] = array_reverse($sanitizedMiddlewares);
|
||||
}
|
||||
|
||||
return $middlewares;
|
||||
}
|
||||
|
||||
protected function getCacheIdentifier(string $stackName): string
|
||||
{
|
||||
return 'middlewares_' . $stackName . $this->baseCacheIdentifier;
|
||||
}
|
||||
|
||||
#[AsEventListener('typo3-core/middlewares')]
|
||||
public function warmupCaches(CacheWarmupEvent $event): void
|
||||
{
|
||||
if ($event->hasGroup('system')) {
|
||||
$allMiddlewares = $this->loadConfiguration();
|
||||
$middlewares = $this->sanitizeMiddlewares($allMiddlewares);
|
||||
|
||||
foreach ($middlewares as $stack => $middlewaresOfStack) {
|
||||
$this->cache->set($this->getCacheIdentifier($stack), 'return ' . var_export($middlewaresOfStack, true) . ';');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This class provides normalized server parameters in HTTP request context.
|
||||
* It normalizes reverse proxy scenarios and various other web server specific differences
|
||||
* of the native PSR-7 request object parameters (->getServerParams() / $GLOBALS['_SERVER']).
|
||||
*
|
||||
* An instance of this class is available as PSR-7 ServerRequestInterface attribute:
|
||||
*
|
||||
* ```
|
||||
* $normalizedParams = $request->getAttribute('normalizedParams')
|
||||
* ```
|
||||
*/
|
||||
class NormalizedParams
|
||||
{
|
||||
/**
|
||||
* Sanitized HTTP_HOST value
|
||||
*
|
||||
* host[:port]
|
||||
*
|
||||
* - www.domain.com
|
||||
* - www.domain.com:443
|
||||
* - 192.168.1.42:80
|
||||
*/
|
||||
protected string $httpHost = '';
|
||||
|
||||
/**
|
||||
* True if request has been done via HTTPS
|
||||
*/
|
||||
protected bool $isHttps = false;
|
||||
|
||||
/**
|
||||
* Sanitized HTTP_HOST with protocol
|
||||
*
|
||||
* scheme://host[:port]
|
||||
*
|
||||
* - https://www.domain.com
|
||||
*/
|
||||
protected string $requestHost = '';
|
||||
|
||||
/**
|
||||
* Host / domain part of HTTP_HOST, no port, no protocol
|
||||
*
|
||||
* - www.domain.com
|
||||
* - 192.168.1.42
|
||||
*/
|
||||
protected string $requestHostOnly = '';
|
||||
|
||||
/**
|
||||
* Port of HTTP_HOST if given
|
||||
*/
|
||||
protected int $requestPort = 0;
|
||||
|
||||
/**
|
||||
* Entry script path of URI, without domain and without query parameters, with leading /
|
||||
*
|
||||
* [path_script]
|
||||
*
|
||||
* - /index.php
|
||||
* - /typo3/index.php
|
||||
*/
|
||||
protected string $scriptName = '';
|
||||
|
||||
/**
|
||||
* REQUEST URI without domain and scheme, with trailing slash
|
||||
*
|
||||
* [path][?[query]]
|
||||
*
|
||||
* - /some/path?p1=parameter1&p2[key]=value
|
||||
* - /typo3/some/path?p1=parameter1&p2[key]=value
|
||||
*/
|
||||
protected string $requestUri = '';
|
||||
|
||||
/**
|
||||
* REQUEST URI with scheme, host, port, path and query
|
||||
*
|
||||
* scheme://host[:[port]][path][?[query]]
|
||||
*
|
||||
* - http://www.domain.com/some/path?p1=parameter1&p2[key]=value
|
||||
* - http://www.domain.com/typo3/some/path?id=42
|
||||
*/
|
||||
protected string $requestUrl = '';
|
||||
|
||||
/**
|
||||
* REQUEST URI with scheme, host, port and path, but *without* query part
|
||||
*
|
||||
* scheme://host[:[port]][path_script]
|
||||
*
|
||||
* - http://www.domain.com/index.php
|
||||
* - http://www.domain.com/typo3/index.php
|
||||
*/
|
||||
protected string $requestScript = '';
|
||||
|
||||
/**
|
||||
* Full Uri with path, but without script name and query parts
|
||||
*
|
||||
* scheme://host[:[port]][path_dir]
|
||||
*
|
||||
* - http://www.domain.com/
|
||||
* - http://www.domain.com/typo3/
|
||||
*/
|
||||
protected string $requestDir = '';
|
||||
|
||||
/**
|
||||
* True if request via a reverse proxy is detected
|
||||
*/
|
||||
protected bool $isBehindReverseProxy = false;
|
||||
|
||||
/**
|
||||
* IPv4 or IPv6 address of remote client with resolved proxy setup
|
||||
*/
|
||||
protected string $remoteAddress = '';
|
||||
|
||||
/**
|
||||
* Absolute server path to entry script on server filesystem
|
||||
*
|
||||
* - /var/www/index.php
|
||||
* - /var/www/typo3/index.php
|
||||
*/
|
||||
protected string $scriptFilename = '';
|
||||
|
||||
/**
|
||||
* Absolute server path to web document root without trailing slash
|
||||
*
|
||||
* - /var/www
|
||||
*/
|
||||
protected string $documentRoot = '';
|
||||
|
||||
/**
|
||||
* Website frontend URL.
|
||||
* Note this is note "safe" if called from Backend since sys_domain and
|
||||
* other factors are not taken into account.
|
||||
*
|
||||
* scheme://host[:[port]]/[path_dir]
|
||||
*
|
||||
* - https://www.domain.com/
|
||||
* - https://www.domain.com/some/sub/dir/
|
||||
*/
|
||||
protected string $siteUrl = '';
|
||||
|
||||
/**
|
||||
* Path part to frontend, no domain, no protocol
|
||||
*
|
||||
* - /
|
||||
* - /some/sub/dir/
|
||||
*/
|
||||
protected string $sitePath = '';
|
||||
|
||||
/**
|
||||
* Path to script, without sub path if TYPO3 is running in sub directory, without trailing slash
|
||||
*
|
||||
* - index.php?id=42
|
||||
* - /some/path?id=42
|
||||
* - typo3/some/path?id=411
|
||||
*/
|
||||
protected string $siteScript = '';
|
||||
|
||||
/**
|
||||
* Entry script path of URI, without domain and without query parameters, with leading /
|
||||
* This is often not set at all.
|
||||
* Will be deprecated later, use $scriptName instead as more reliable solution.
|
||||
*
|
||||
* [path_script]
|
||||
*/
|
||||
protected string $pathInfo = '';
|
||||
|
||||
/**
|
||||
* HTTP_REFERER
|
||||
* Will be deprecated later, use $request->getServerParams()['HTTP_REFERER'] instead
|
||||
*
|
||||
* scheme://host[:[port]][path]
|
||||
*
|
||||
* - https://www.domain.com/typo3/module/web/layout?id=42
|
||||
*/
|
||||
protected string $httpReferer = '';
|
||||
|
||||
/**
|
||||
* HTTP_USER_AGENT
|
||||
* Will be deprecated later, use $request->getServerParams()['HTTP_USER_AGENT'] instead
|
||||
*
|
||||
* - Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36
|
||||
*/
|
||||
protected string $httpUserAgent = '';
|
||||
|
||||
/**
|
||||
* HTTP_ACCEPT_ENCODING
|
||||
* Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_ENCODING'] instead
|
||||
*
|
||||
* - gzip, deflate
|
||||
*/
|
||||
protected string $httpAcceptEncoding = '';
|
||||
|
||||
/**
|
||||
* HTTP_ACCEPT_LANGUAGE
|
||||
* Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] instead
|
||||
*
|
||||
* - de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
|
||||
*/
|
||||
protected string $httpAcceptLanguage = '';
|
||||
|
||||
/**
|
||||
* REMOTE_HOST Resolved host name of REMOTE_ADDR if configured in web server
|
||||
* Will be deprecated later, use $request->getServerParams()['REMOTE_HOST'] instead
|
||||
*
|
||||
* - www.clientDomain.com
|
||||
*/
|
||||
protected string $remoteHost = '';
|
||||
|
||||
/**
|
||||
* QUERY_STRING
|
||||
* Will be deprecated later, use $request->getServerParams()['QUERY_STRING'] instead
|
||||
*
|
||||
* [query]
|
||||
*
|
||||
* - id=42&foo=bar
|
||||
*/
|
||||
protected string $queryString = '';
|
||||
|
||||
/**
|
||||
* Constructor calculates all values by incoming variables.
|
||||
*
|
||||
* This object is immutable.
|
||||
*
|
||||
* All determine*() "detail worker methods" in this class retrieve their dependencies
|
||||
* to other properties as method arguments, they are static, stateless and have no
|
||||
* dependency to $this. This ensures the chain of inter-property dependencies
|
||||
* is visible by only looking at the construct() method.
|
||||
*
|
||||
* @param array $serverParams , usually coming from $_SERVER or $request->getServerParams()
|
||||
* @param array $configuration $GLOBALS['TYPO3_CONF_VARS']['SYS']
|
||||
* @param string $pathThisScript Absolute server entry script path, usually found within Environment::getCurrentScript()
|
||||
* @param string $pathSite Absolute server path to document root, Environment::getPublicPath()
|
||||
*/
|
||||
public function __construct(array $serverParams, array $configuration, string $pathThisScript, string $pathSite)
|
||||
{
|
||||
$isBehindReverseProxy = $this->isBehindReverseProxy = self::determineIsBehindReverseProxy(
|
||||
$serverParams,
|
||||
$configuration
|
||||
);
|
||||
$httpHost = $this->httpHost = self::determineHttpHost($serverParams, $configuration, $isBehindReverseProxy);
|
||||
$isHttps = $this->isHttps = self::determineHttps($serverParams, $configuration);
|
||||
$requestHost = $this->requestHost = ($isHttps ? 'https://' : 'http://') . $httpHost;
|
||||
$requestHostOnly = $this->requestHostOnly = self::determineRequestHostOnly($httpHost);
|
||||
$this->requestPort = self::determineRequestPort($httpHost, $requestHostOnly);
|
||||
$scriptNameOnFileSystem = self::determineScriptName(
|
||||
$serverParams,
|
||||
$configuration,
|
||||
$isHttps,
|
||||
$isBehindReverseProxy
|
||||
);
|
||||
$scriptName = $this->scriptName = self::encodeFileSystemPathComponentForUrlPath($scriptNameOnFileSystem);
|
||||
$requestUri = $this->requestUri = self::determineRequestUri(
|
||||
$serverParams,
|
||||
$configuration,
|
||||
$isHttps,
|
||||
$scriptName,
|
||||
$isBehindReverseProxy
|
||||
);
|
||||
$requestUrl = $this->requestUrl = $requestHost . $requestUri;
|
||||
$this->requestScript = $requestHost . $scriptName;
|
||||
$requestDir = $this->requestDir = $requestHost . GeneralUtility::dirname($scriptName) . '/';
|
||||
$this->remoteAddress = self::determineRemoteAddress($serverParams, $configuration, $isBehindReverseProxy);
|
||||
$scriptFilename = $this->scriptFilename = $pathThisScript;
|
||||
$this->documentRoot = self::determineDocumentRoot($scriptNameOnFileSystem, $scriptFilename);
|
||||
$siteUrl = $this->siteUrl = self::determineSiteUrl($requestDir, $pathThisScript, $pathSite . '/');
|
||||
$this->sitePath = self::determineSitePath($requestHost, $siteUrl);
|
||||
$this->siteScript = self::determineSiteScript($requestUrl, $siteUrl);
|
||||
|
||||
// @deprecated Below variables can be fully deprecated as soon as core does not use them anymore
|
||||
$this->pathInfo = $serverParams['PATH_INFO'] ?? '';
|
||||
$this->httpReferer = $serverParams['HTTP_REFERER'] ?? '';
|
||||
$this->httpUserAgent = $serverParams['HTTP_USER_AGENT'] ?? '';
|
||||
$this->httpAcceptEncoding = $serverParams['HTTP_ACCEPT_ENCODING'] ?? '';
|
||||
$this->httpAcceptLanguage = $serverParams['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
||||
$this->remoteHost = $serverParams['REMOTE_HOST'] ?? '';
|
||||
$this->queryString = $serverParams['QUERY_STRING'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method, to allow TYPO3 to handle configuration options directly.
|
||||
*
|
||||
* @param array $serverParams - could be fulfilled by $_SERVER (on web requests)
|
||||
* @internal framework method. Not part of TYPO3 API.
|
||||
*/
|
||||
public static function createFromServerParams(array $serverParams, ?array $systemConfiguration = null): self
|
||||
{
|
||||
return new NormalizedParams(
|
||||
$serverParams,
|
||||
$systemConfiguration ?? $GLOBALS['TYPO3_CONF_VARS']['SYS'],
|
||||
Environment::getCurrentScript(),
|
||||
Environment::getPublicPath()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for creating normalized params from a PSR-7 server request object
|
||||
*
|
||||
* @internal framework method. Not part of TYPO3 API.
|
||||
*/
|
||||
public static function createFromRequest(ServerRequestInterface $request, ?array $systemConfiguration = null): self
|
||||
{
|
||||
return static::createFromServerParams(
|
||||
$request->getServerParams(),
|
||||
$systemConfiguration ?? $GLOBALS['TYPO3_CONF_VARS']['SYS']
|
||||
);
|
||||
}
|
||||
|
||||
private static function encodeFileSystemPathComponentForUrlPath(string $path): string
|
||||
{
|
||||
return implode('/', array_map(rawurlencode(...), explode('/', $path)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Sanitized HTTP_HOST value host[:port]
|
||||
*/
|
||||
public function getHttpHost(): string
|
||||
{
|
||||
return $this->httpHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool True if client request has been done using HTTPS
|
||||
*/
|
||||
public function isHttps(): bool
|
||||
{
|
||||
return $this->isHttps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Sanitized HTTP_HOST with protocol scheme://host[:port], eg. https://www.domain.com/
|
||||
*/
|
||||
public function getRequestHost(): string
|
||||
{
|
||||
return $this->requestHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Host / domain /IP only, eg. www.domain.com
|
||||
*/
|
||||
public function getRequestHostOnly(): string
|
||||
{
|
||||
return $this->requestHostOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Requested port if given, eg. 8080 - often not explicitly given, then 0
|
||||
*/
|
||||
public function getRequestPort(): int
|
||||
{
|
||||
return $this->requestPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Script path part of URI, eg. '/typo3/index.php'
|
||||
*/
|
||||
public function getScriptName(): string
|
||||
{
|
||||
return $this->scriptName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Request Uri without domain and protocol, eg. /index.php?id=42
|
||||
*/
|
||||
public function getRequestUri(): string
|
||||
{
|
||||
return $this->requestUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Full REQUEST_URI, eg. http://www.domain.com/typo3/foo/bar?id=42
|
||||
*/
|
||||
public function getRequestUrl(): string
|
||||
{
|
||||
return $this->requestUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string REQUEST URI without query part, eg. http://www.domain.com/typo3/index.php
|
||||
*/
|
||||
public function getRequestScript(): string
|
||||
{
|
||||
return $this->requestScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string REQUEST URI without script file name and query parts, eg. http://www.domain.com/typo3/
|
||||
*/
|
||||
public function getRequestDir(): string
|
||||
{
|
||||
return $this->requestDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool True if request comes from a configured reverse proxy
|
||||
*/
|
||||
public function isBehindReverseProxy(): bool
|
||||
{
|
||||
return $this->isBehindReverseProxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Client IP
|
||||
*/
|
||||
public function getRemoteAddress(): string
|
||||
{
|
||||
return $this->remoteAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Absolute entry script path on server, eg. /var/www/typo3/index.php
|
||||
*/
|
||||
public function getScriptFilename(): string
|
||||
{
|
||||
return $this->scriptFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Absolute path to web document root, eg. /var/www/typo3
|
||||
*/
|
||||
public function getDocumentRoot(): string
|
||||
{
|
||||
return $this->documentRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Website frontend url, eg. https://www.domain.com/some/sub/dir/
|
||||
*/
|
||||
public function getSiteUrl(): string
|
||||
{
|
||||
return $this->siteUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Path part to frontend, eg. /some/sub/dir/
|
||||
*/
|
||||
public function getSitePath(): string
|
||||
{
|
||||
return $this->sitePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Path part to entry script with parameters, without sub dir, eg 'typo3/index.php?id=42'
|
||||
*/
|
||||
public function getSiteScript(): string
|
||||
{
|
||||
return $this->siteScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be deprecated later, use getScriptName() as reliable solution instead
|
||||
*
|
||||
* @return string Script path part of URI, eg. 'typo3/index.php'
|
||||
*/
|
||||
public function getPathInfo(): string
|
||||
{
|
||||
return $this->pathInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be deprecated later, use $request->getServerParams()['HTTP_REFERER'] instead
|
||||
*
|
||||
* @return string HTTP_REFERER, eg. 'https://www.domain.com/typo3/index.php?id=42'
|
||||
*/
|
||||
public function getHttpReferer(): string
|
||||
{
|
||||
return $this->httpReferer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be deprecated later, use $request->getServerParams()['HTTP_USER_AGENT'] instead
|
||||
*
|
||||
* @return string HTTP_USER_AGENT identifier
|
||||
*/
|
||||
public function getHttpUserAgent(): string
|
||||
{
|
||||
return $this->httpUserAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_ENCODING'] instead
|
||||
*
|
||||
* @return string HTTP_ACCEPT_ENCODING, eg. 'gzip, deflate'
|
||||
*/
|
||||
public function getHttpAcceptEncoding(): string
|
||||
{
|
||||
return $this->httpAcceptEncoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] instead
|
||||
*
|
||||
* @return string HTTP_ACCEPT_LANGUAGE, eg. 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
|
||||
*/
|
||||
public function getHttpAcceptLanguage(): string
|
||||
{
|
||||
return $this->httpAcceptLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be deprecated later, use $request->getServerParams()['REMOTE_HOST'] instead
|
||||
*
|
||||
* @return string REMOTE_HOST if configured in web server, eg. 'www.clientDomain.com'
|
||||
*/
|
||||
public function getRemoteHost(): string
|
||||
{
|
||||
return $this->remoteHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be deprecated later, use $request->getServerParams()['QUERY_STRING'] instead
|
||||
*
|
||||
* @return string QUERY_STRING, eg 'id=42&foo=bar'
|
||||
*/
|
||||
public function getQueryString(): string
|
||||
{
|
||||
return $this->queryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize HTTP_HOST by taking proxy configuration into account.
|
||||
*
|
||||
* @param array $serverParams Basically the $_SERVER, but from $request object
|
||||
* @param array $configuration $TYPO3_CONF_VARS['SYS'] array
|
||||
* @param bool $isBehindReverseProxy True if reverse proxy setup is detected
|
||||
* @return string Normalized HTTP_HOST
|
||||
*/
|
||||
protected static function determineHttpHost(
|
||||
array $serverParams,
|
||||
array $configuration,
|
||||
bool $isBehindReverseProxy
|
||||
): string {
|
||||
$httpHost = $serverParams['HTTP_HOST'] ?? '';
|
||||
if ($isBehindReverseProxy) {
|
||||
// If the request comes from a configured proxy which has set HTTP_X_FORWARDED_HOST, then
|
||||
// evaluate reverseProxyHeaderMultiValue and
|
||||
$xForwardedHostArray = GeneralUtility::trimExplode(',', $serverParams['HTTP_X_FORWARDED_HOST'] ?? '', true);
|
||||
$xForwardedHost = '';
|
||||
// Choose which host in list to use
|
||||
if (!empty($xForwardedHostArray)) {
|
||||
$configuredReverseProxyHeaderMultiValue = trim($configuration['reverseProxyHeaderMultiValue'] ?? '');
|
||||
// Default if reverseProxyHeaderMultiValue is not set or set to 'none', instead of 'first' / 'last' is to
|
||||
// ignore $serverParams['HTTP_X_FORWARDED_HOST']
|
||||
// @todo: Maybe this default is stupid: Both SYS/reverseProxyIP hand SYS/reverseProxyHeaderMultiValue have to
|
||||
// be configured for a working setup. It would be easier to only configure SYS/reverseProxyIP and fall
|
||||
// back to "first" if SYS/reverseProxyHeaderMultiValue is not set.
|
||||
if ($configuredReverseProxyHeaderMultiValue === 'last') {
|
||||
$xForwardedHost = array_pop($xForwardedHostArray);
|
||||
} elseif ($configuredReverseProxyHeaderMultiValue === 'first') {
|
||||
$xForwardedHost = array_shift($xForwardedHostArray);
|
||||
}
|
||||
}
|
||||
if ($xForwardedHost) {
|
||||
$httpHost = $xForwardedHost;
|
||||
}
|
||||
}
|
||||
return $httpHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the client called via HTTPS. Takes proxy ssl terminator
|
||||
* configurations into account.
|
||||
*
|
||||
* How does TYPO3 determine if the connection was established via TLS/SSL/https?
|
||||
* 1. If reverseProxySSL matches, then we now that Client -> Proxy is SSL,
|
||||
* and Proxy -> App Server is non-SSL. SSL Termination happens at Proxy at ALL times.
|
||||
* 2. If reverseProxyIP matches, and HTTP_X_FORWARDED_PROTO is set, it is evaluated
|
||||
* 3. If no other matches, see webserverUsesHttps()
|
||||
*
|
||||
* Note: HTTP_X_FORWARDED_PROTO is ONLY evaluated at the point, where we know
|
||||
* that the incoming REMOTE_ADDR is a trusted proxy!
|
||||
*
|
||||
* @param array $serverParams Basically the $_SERVER, but from $request object
|
||||
* @param array $configuration $TYPO3_CONF_VARS['SYS'] array
|
||||
* @return bool True if request has been done via HTTPS
|
||||
*/
|
||||
protected static function determineHttps(array $serverParams, array $configuration): bool
|
||||
{
|
||||
$configuredProxySSL = trim($configuration['reverseProxySSL'] ?? '');
|
||||
$configuredProxyRegular = trim($configuration['reverseProxyIP'] ?? '');
|
||||
|
||||
if ($configuredProxySSL === '*') {
|
||||
$configuredProxySSL = $configuredProxyRegular;
|
||||
}
|
||||
$httpsParam = (string)($serverParams['HTTPS'] ?? '');
|
||||
if (GeneralUtility::cmpIP($serverParams['REMOTE_ADDR'] ?? '', $configuredProxySSL)) {
|
||||
$isHttps = true;
|
||||
} elseif (isset($serverParams['HTTP_X_FORWARDED_PROTO'])
|
||||
&& GeneralUtility::cmpIP($serverParams['REMOTE_ADDR'] ?? '', $configuredProxyRegular)) {
|
||||
// If the X-Forwarded-Proto header is set, and we can trust the RemoteAddr to be a proxy, check it
|
||||
$isHttps = strtolower($serverParams['HTTP_X_FORWARDED_PROTO']) === 'https';
|
||||
} else {
|
||||
// https://secure.php.net/manual/en/reserved.variables.server.php
|
||||
// "Set to a non-empty value if the script was queried through the HTTPS protocol."
|
||||
$isHttps = ($serverParams['SSL_SESSION_ID'] ?? '')
|
||||
|| ($httpsParam !== '' && $httpsParam !== 'off' && $httpsParam !== '0');
|
||||
}
|
||||
return $isHttps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine script name and path
|
||||
*
|
||||
* @param array $serverParams Basically the $_SERVER, but from $request object
|
||||
* @param array $configuration TYPO3_CONF_VARS['SYS'] array
|
||||
* @param bool $isHttps True if used protocol is HTTPS
|
||||
* @param bool $isBehindReverseProxy True if reverse proxy setup is detected
|
||||
* @return string Sanitized script name
|
||||
*/
|
||||
protected static function determineScriptName(
|
||||
array $serverParams,
|
||||
array $configuration,
|
||||
bool $isHttps,
|
||||
bool $isBehindReverseProxy
|
||||
): string {
|
||||
$scriptName = $serverParams['SCRIPT_NAME'] ?? '';
|
||||
if ($isBehindReverseProxy) {
|
||||
// Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
|
||||
if ($isHttps && !empty($configuration['reverseProxyPrefixSSL'])) {
|
||||
$scriptName = $configuration['reverseProxyPrefixSSL'] . $scriptName;
|
||||
} elseif (!empty($configuration['reverseProxyPrefix'])) {
|
||||
$scriptName = $configuration['reverseProxyPrefix'] . $scriptName;
|
||||
}
|
||||
}
|
||||
return $scriptName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine REQUEST_URI, taking proxy configuration and various web server
|
||||
* specifics into account.
|
||||
*
|
||||
* @param array $serverParams Basically the $_SERVER, but from $request object
|
||||
* @param array $configuration $TYPO3_CONF_VARS['SYS'] array
|
||||
* @param bool $isHttps True if used protocol is HTTPS
|
||||
* @param string $scriptName Script name
|
||||
* @param bool $isBehindReverseProxy True if reverse proxy setup is detected
|
||||
* @return string Sanitized REQUEST_URI
|
||||
*/
|
||||
protected static function determineRequestUri(
|
||||
array $serverParams,
|
||||
array $configuration,
|
||||
bool $isHttps,
|
||||
string $scriptName,
|
||||
bool $isBehindReverseProxy
|
||||
): string {
|
||||
$proxyPrefixApplied = false;
|
||||
if (!empty($configuration['requestURIvar'])) {
|
||||
// This is for URL rewriter that store the original URI in a server
|
||||
// variable (e.g. ISAPI Rewriter for IIS: HTTP_X_REWRITE_URL), a config then looks like:
|
||||
// requestURIvar = '_SERVER|HTTP_X_REWRITE_URL' which will access $GLOBALS['_SERVER']['HTTP_X_REWRITE_URL']
|
||||
[$firstLevel, $secondLevel] = GeneralUtility::trimExplode('|', $configuration['requestURIvar'], true);
|
||||
$requestUri = $GLOBALS[$firstLevel][$secondLevel];
|
||||
} elseif (empty($serverParams['REQUEST_URI'])) {
|
||||
// This is for ISS/CGI which does not have the REQUEST_URI available.
|
||||
$queryString = !empty($serverParams['QUERY_STRING']) ? '?' . $serverParams['QUERY_STRING'] : '';
|
||||
// script name already had the proxy prefix handling, we must not add it a second time
|
||||
$proxyPrefixApplied = true;
|
||||
$requestUri = '/' . ltrim($scriptName, '/') . $queryString;
|
||||
} else {
|
||||
$requestUri = '/' . ltrim($serverParams['REQUEST_URI'], '/');
|
||||
}
|
||||
if (!$proxyPrefixApplied && $isBehindReverseProxy) {
|
||||
// Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
|
||||
if ($isHttps && !empty($configuration['reverseProxyPrefixSSL'])) {
|
||||
$requestUri = $configuration['reverseProxyPrefixSSL'] . $requestUri;
|
||||
} elseif (!empty($configuration['reverseProxyPrefix'])) {
|
||||
$requestUri = $configuration['reverseProxyPrefix'] . $requestUri;
|
||||
}
|
||||
}
|
||||
return $requestUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine clients REMOTE_ADDR, even if there is a reverse proxy in between.
|
||||
*
|
||||
* @param array $serverParams Basically the $_SERVER, but from $request object
|
||||
* @param array $configuration $TYPO3_CONF_VARS[SYS] array
|
||||
* @param bool $isBehindReverseProxy True if reverse proxy setup is detected
|
||||
* @return string Resolved REMOTE_ADDR
|
||||
*/
|
||||
protected static function determineRemoteAddress(
|
||||
array $serverParams,
|
||||
array $configuration,
|
||||
bool $isBehindReverseProxy
|
||||
): string {
|
||||
$remoteAddress = trim($serverParams['REMOTE_ADDR'] ?? '');
|
||||
if ($isBehindReverseProxy) {
|
||||
$ip = GeneralUtility::trimExplode(',', $serverParams['HTTP_X_FORWARDED_FOR'] ?? '', true);
|
||||
// Choose which IP in list to use
|
||||
$configuredReverseProxyHeaderMultiValue = trim($configuration['reverseProxyHeaderMultiValue'] ?? '');
|
||||
if (!empty($ip) && $configuredReverseProxyHeaderMultiValue === 'last') {
|
||||
$ip = (string)array_pop($ip);
|
||||
} elseif (!empty($ip) && $configuredReverseProxyHeaderMultiValue === 'first') {
|
||||
$ip = (string)array_shift($ip);
|
||||
} else {
|
||||
$ip = '';
|
||||
}
|
||||
if (GeneralUtility::validIP($ip)) {
|
||||
$remoteAddress = $ip;
|
||||
}
|
||||
}
|
||||
return $remoteAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a configured reverse proxy setup is detected.
|
||||
*
|
||||
* @param array $serverParams Basically the $_SERVER, but from $request object
|
||||
* @param array $configuration $TYPO3_CONF_VARS[SYS] array
|
||||
* @return bool True if TYPO3 is behind a reverse proxy
|
||||
*/
|
||||
protected static function determineIsBehindReverseProxy($serverParams, $configuration): bool
|
||||
{
|
||||
return GeneralUtility::cmpIP(
|
||||
trim($serverParams['REMOTE_ADDR'] ?? ''),
|
||||
trim($configuration['reverseProxyIP'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP_HOST without port
|
||||
*
|
||||
* @param string $httpHost host[:[port]]
|
||||
* @return string Resolved host
|
||||
*/
|
||||
protected static function determineRequestHostOnly(string $httpHost): string
|
||||
{
|
||||
$httpHostBracketPosition = strpos($httpHost, ']');
|
||||
$httpHostParts = explode(':', $httpHost);
|
||||
return $httpHostBracketPosition !== false ? substr(
|
||||
$httpHost,
|
||||
0,
|
||||
$httpHostBracketPosition + 1
|
||||
) : array_shift($httpHostParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requested port if given
|
||||
*
|
||||
* @param string $httpHost host[:[port]]
|
||||
* @param string $httpHostOnly host
|
||||
* @return int Resolved port if given, else 0
|
||||
*/
|
||||
protected static function determineRequestPort(string $httpHost, string $httpHostOnly): int
|
||||
{
|
||||
return strlen($httpHost) > strlen($httpHostOnly) ? (int)substr($httpHost, strlen($httpHostOnly) + 1) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate absolute path to web document root
|
||||
*
|
||||
* @param string $scriptNameOnFileSystem Entry script path of URI on file system, without domain and without query parameters, with leading /
|
||||
* @param string $scriptFilename Absolute path to entry script on server filesystem
|
||||
* @return string Path to document root with trailing slash
|
||||
*/
|
||||
protected static function determineDocumentRoot(string $scriptNameOnFileSystem, string $scriptFilename): string
|
||||
{
|
||||
// Get the web root (it is not the root of the TYPO3 installation)
|
||||
// Some CGI-versions (LA13CGI) and mod-rewrite rules on MODULE versions will deliver a 'wrong'
|
||||
// DOCUMENT_ROOT (according to our description). Further various aliases/mod_rewrite rules can
|
||||
// disturb this as well. Therefore the DOCUMENT_ROOT is always calculated as the SCRIPT_FILENAME
|
||||
// minus the end part shared with SCRIPT_NAME.
|
||||
$webDocRoot = '';
|
||||
$scriptNameArray = explode('/', strrev($scriptNameOnFileSystem));
|
||||
$scriptFilenameArray = explode('/', strrev($scriptFilename));
|
||||
$path = [];
|
||||
foreach ($scriptNameArray as $segmentNumber => $segment) {
|
||||
if ($scriptFilenameArray[$segmentNumber] === (string)$segment) {
|
||||
$path[] = $segment;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$commonEnd = strrev(implode('/', $path));
|
||||
if ($commonEnd !== '') {
|
||||
$webDocRoot = substr($scriptFilename, 0, -(strlen($commonEnd) + 1));
|
||||
}
|
||||
return $webDocRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine frontend url
|
||||
*
|
||||
* @param string $requestDir Full Uri with path, but without script name and query parts
|
||||
* @param string $pathThisScript Absolute path to entry script on server filesystem
|
||||
* @param string $pathSite Absolute server path to document root
|
||||
* @return string Calculated Frontend Url
|
||||
*/
|
||||
protected static function determineSiteUrl(string $requestDir, string $pathThisScript, string $pathSite): string
|
||||
{
|
||||
$pathThisScriptDir = substr(dirname($pathThisScript), strlen($pathSite)) . '/';
|
||||
$siteUrl = substr($requestDir, 0, -strlen($pathThisScriptDir));
|
||||
|
||||
return rtrim($siteUrl, '/') . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine site path
|
||||
*
|
||||
* @param string $requestHost scheme://host[:port]
|
||||
* @param string $siteUrl Full Frontend Url
|
||||
*/
|
||||
protected static function determineSitePath(string $requestHost, string $siteUrl): string
|
||||
{
|
||||
return substr($siteUrl, strlen($requestHost));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine site script
|
||||
*/
|
||||
protected static function determineSiteScript(string $requestUrl, string $siteUrl): string
|
||||
{
|
||||
return substr($requestUrl, strlen($siteUrl));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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\Http;
|
||||
|
||||
/**
|
||||
* A null response object
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
class NullResponse extends Response
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(null);
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Http;
|
||||
|
||||
/**
|
||||
* Exception that has to be propagated back to the middleware stack
|
||||
* in order to stop current execution and respond with the given
|
||||
* response. This exception is used as alternative to previous
|
||||
* die() or exit() calls.
|
||||
*
|
||||
* Note this exception should be used in cases where a controller wants
|
||||
* to create an 'early' response. An example are failed access checks:
|
||||
* A controller wants to throw an early 'denied' response without further
|
||||
* local processing.
|
||||
*
|
||||
* When this exception is thrown by a controller, it will be caught by the
|
||||
* 'very inner' ResponsePropagation middleware. The response is then returned
|
||||
* to other 'outer' middlewares, allowing them to further operate on the
|
||||
* response (eg. adding a content-length header) and to do other jobs
|
||||
* (eg. releasing created locks).
|
||||
*
|
||||
* If this exception is thrown within a middleware (as opposed to be thrown
|
||||
* from within controllers), it will bypass other middlewares and will be
|
||||
* caught just like the parent ImmediateResponseException. This should be
|
||||
* used with care, there should be little reason to do so at all.
|
||||
*
|
||||
* In general, from within controllers, this response exception
|
||||
* should be preferred over ImmediateResponseException.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PropagateResponseException extends ImmediateResponseException {}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* A default redirect response object
|
||||
*
|
||||
* Highly inspired by ZF zend-diactoros
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
class RedirectResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Creates a redirect response object with a given URI and status code.
|
||||
* Also sets the "Location" response header.
|
||||
*
|
||||
* @param string|UriInterface $uri URI for the Location header.
|
||||
* @param int $status status code for the redirect; defaults to 302.
|
||||
* @param array $headers Additional headers to be set.
|
||||
*/
|
||||
public function __construct(string|UriInterface $uri, int $status = 302, array $headers = [])
|
||||
{
|
||||
$headers['location'] = [(string)$uri];
|
||||
parent::__construct('php://temp', $status, $headers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Default implementation for the RequestInterface of the PSR-7 standard
|
||||
* It is the base for any request sent BY PHP.
|
||||
*
|
||||
* Please see ServerRequest for the typical use cases in the framework.
|
||||
*
|
||||
* Highly inspired by https://github.com/phly/http/
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
class Request extends Message implements RequestInterface
|
||||
{
|
||||
/**
|
||||
* The request-target, if it has been provided or calculated.
|
||||
*/
|
||||
protected ?string $requestTarget = null;
|
||||
|
||||
/**
|
||||
* The HTTP method.
|
||||
*/
|
||||
protected string $method = 'GET';
|
||||
|
||||
/**
|
||||
* Supported HTTP methods
|
||||
*
|
||||
* @var non-empty-string[]
|
||||
*/
|
||||
protected array $supportedMethods = [
|
||||
'CONNECT',
|
||||
'DELETE',
|
||||
'GET',
|
||||
'HEAD',
|
||||
'OPTIONS',
|
||||
'PATCH',
|
||||
'POST',
|
||||
'PUT',
|
||||
'TRACE',
|
||||
// WebDAV methods
|
||||
'COPY',
|
||||
'LOCK',
|
||||
'MKCOL',
|
||||
'MOVE',
|
||||
'PROPFIND',
|
||||
'PROPPATCH',
|
||||
'REPORT',
|
||||
'UNLOCK',
|
||||
// Custom methods
|
||||
'PURGE',
|
||||
'BAN',
|
||||
];
|
||||
|
||||
/**
|
||||
* An instance of the Uri object
|
||||
*
|
||||
* @todo It is a PSR-7 spec violation for this to be null. This should be corrected.
|
||||
*/
|
||||
protected ?UriInterface $uri;
|
||||
|
||||
/**
|
||||
* Constructor, the only place to set all parameters of this Request
|
||||
*
|
||||
* @param string|UriInterface|null $uri URI for the request, if any.
|
||||
* @param string $method HTTP method for the request, if any.
|
||||
* @param string|resource|StreamInterface|null $body Message body, if any.
|
||||
* @param array $headers Headers for the message, if any.
|
||||
* @throws \InvalidArgumentException for any invalid value.
|
||||
*/
|
||||
public function __construct(UriInterface|string|null $uri = null, string $method = 'GET', $body = 'php://input', array $headers = [])
|
||||
{
|
||||
// Upcast the body to a streamable object, or error
|
||||
// if it's an invalid type.
|
||||
if ($body instanceof StreamInterface) {
|
||||
$this->body = $body;
|
||||
} else {
|
||||
$this->body = match (get_debug_type($body)) {
|
||||
'string', 'resource (stream)' => new Stream($body),
|
||||
'null' => null,
|
||||
default => throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717271),
|
||||
};
|
||||
}
|
||||
|
||||
if (is_string($uri)) {
|
||||
$uri = new Uri($uri);
|
||||
}
|
||||
|
||||
$this->validateMethod($method);
|
||||
|
||||
$this->method = $method;
|
||||
$this->uri = $uri;
|
||||
[$this->lowercasedHeaderNames, $headers] = $this->filterHeaders($headers);
|
||||
$this->assertHeaders($headers);
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all message header values.
|
||||
*
|
||||
* The keys represent the header name as it will be sent over the wire, and
|
||||
* each value is an array of strings associated with the header.
|
||||
*
|
||||
* ```
|
||||
* // Represent the headers as a string
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* echo $name . ": " . implode(", ", $values);
|
||||
* }
|
||||
*
|
||||
* // Emit headers iteratively:
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* foreach ($values as $value) {
|
||||
* header(sprintf('%s: %s', $name, $value), false);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||
* exact case in which headers were originally specified.
|
||||
*
|
||||
* @return array Returns an associative array of the message's headers. Each
|
||||
* key MUST be a header name, and each value MUST be an array of strings
|
||||
* for that header.
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
$headers = parent::getHeaders();
|
||||
if (!$this->hasHeader('host') && ($this->uri?->getHost())) {
|
||||
$headers['host'] = [$this->getHostFromUri()];
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a message header value by the given case-insensitive name.
|
||||
*
|
||||
* This method returns an array of all the header values of the given
|
||||
* case-insensitive header name.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return an
|
||||
* empty array.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string[] An array of string values as provided for the given
|
||||
* header. If the header does not appear in the message, this method MUST
|
||||
* return an empty array.
|
||||
*/
|
||||
public function getHeader(string $name): array
|
||||
{
|
||||
if (!$this->hasHeader($name) && strtolower($name) === 'host' && ($this->uri?->getHost())) {
|
||||
return [$this->getHostFromUri()];
|
||||
}
|
||||
return parent::getHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the host from the URI instance
|
||||
*/
|
||||
protected function getHostFromUri(): string
|
||||
{
|
||||
$host = $this->uri->getHost();
|
||||
$host .= $this->uri->getPort() ? ':' . $this->uri->getPort() : '';
|
||||
return $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the message's request target.
|
||||
*
|
||||
* Retrieves the message's request-target either as it will appear (for
|
||||
* clients), as it appeared at request (for servers), or as it was
|
||||
* specified for the instance (see withRequestTarget()).
|
||||
*
|
||||
* In most cases, this will be the origin-form of the composed URI,
|
||||
* unless a value was provided to the concrete implementation (see
|
||||
* withRequestTarget() below).
|
||||
*
|
||||
* If no URI is available, and no request-target has been specifically
|
||||
* provided, this method MUST return the string "/".
|
||||
*/
|
||||
public function getRequestTarget(): string
|
||||
{
|
||||
if ($this->requestTarget !== null) {
|
||||
return $this->requestTarget;
|
||||
}
|
||||
|
||||
if (!$this->uri) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
$target = $this->uri->getPath();
|
||||
|
||||
if ($this->uri->getQuery()) {
|
||||
$target .= '?' . $this->uri->getQuery();
|
||||
}
|
||||
|
||||
if (empty($target)) {
|
||||
$target = '/';
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specific request-target.
|
||||
*
|
||||
* If the request needs a non-origin-form request-target — e.g., for
|
||||
* specifying an absolute-form, authority-form, or asterisk-form —
|
||||
* this method may be used to create an instance with the specified
|
||||
* request-target, verbatim.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* changed request target.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc7230#section-2.7 (for the various
|
||||
* request-target forms allowed in request messages)
|
||||
*/
|
||||
public function withRequestTarget(mixed $requestTarget): static
|
||||
{
|
||||
if (preg_match('#\s#', $requestTarget)) {
|
||||
throw new \InvalidArgumentException('Invalid request target provided which contains whitespaces.', 1436717273);
|
||||
}
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->requestTarget = $requestTarget;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP method of the request, defaults to GET
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the provided HTTP method.
|
||||
*
|
||||
* While HTTP method names are typically all uppercase characters, HTTP
|
||||
* method names are case-sensitive and thus implementations SHOULD NOT
|
||||
* modify the given string.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* changed request method.
|
||||
*
|
||||
* @param string $method Case-sensitive method.
|
||||
* @throws \InvalidArgumentException for invalid HTTP methods.
|
||||
*/
|
||||
public function withMethod(string $method): static
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->method = $method;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the URI instance.
|
||||
*
|
||||
* This method MUST return a UriInterface instance.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc3986#section-4.3
|
||||
* @return UriInterface Returns a UriInterface instance
|
||||
* representing the URI of the request.
|
||||
*/
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the provided URI.
|
||||
*
|
||||
* This method MUST update the Host header of the returned request by
|
||||
* default if the URI contains a host component. If the URI does not
|
||||
* contain a host component, any pre-existing Host header MUST be carried
|
||||
* over to the returned request.
|
||||
*
|
||||
* You can opt-in to preserving the original state of the Host header by
|
||||
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
|
||||
* `true`, this method interacts with the Host header in the following ways:
|
||||
*
|
||||
* - If the the Host header is missing or empty, and the new URI contains
|
||||
* a host component, this method MUST update the Host header in the returned
|
||||
* request.
|
||||
* - If the Host header is missing or empty, and the new URI does not contain a
|
||||
* host component, this method MUST NOT update the Host header in the returned
|
||||
* request.
|
||||
* - If a Host header is present and non-empty, this method MUST NOT update
|
||||
* the Host header in the returned request.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new UriInterface instance.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc3986#section-4.3
|
||||
*
|
||||
* @param UriInterface $uri New request URI to use.
|
||||
* @param bool $preserveHost Preserve the original state of the Host header.
|
||||
*/
|
||||
public function withUri(UriInterface $uri, bool $preserveHost = false): static
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->uri = $uri;
|
||||
|
||||
if ($preserveHost) {
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
if (!$uri->getHost()) {
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
$host = $uri->getHost();
|
||||
|
||||
if ($uri->getPort()) {
|
||||
$host .= ':' . $uri->getPort();
|
||||
}
|
||||
|
||||
$clonedObject->lowercasedHeaderNames['host'] = 'Host';
|
||||
$clonedObject->headers['Host'] = [$host];
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the HTTP method, helper function.
|
||||
*
|
||||
* @throws \InvalidArgumentException on invalid HTTP method.
|
||||
*/
|
||||
protected function validateMethod(?string $method): void
|
||||
{
|
||||
if (is_null($method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$method = strtoupper($method);
|
||||
if (!in_array($method, $this->supportedMethods, true)) {
|
||||
throw new \InvalidArgumentException('Unsupported HTTP method "' . $method . '".', 1436717275);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use TYPO3\CMS\Core\Http\Client\GuzzleClientFactory;
|
||||
|
||||
/**
|
||||
* Front-end for sending requests, using PSR-7.
|
||||
*/
|
||||
#[AsAlias(RequestFactoryInterface::class, public: true)]
|
||||
readonly class RequestFactory implements RequestFactoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GuzzleClientFactory $guzzleFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new request.
|
||||
*
|
||||
* @param string $method The HTTP method associated with the request.
|
||||
* @param UriInterface|string $uri The URI associated with the request.
|
||||
*/
|
||||
public function createRequest(string $method, $uri): RequestInterface
|
||||
{
|
||||
return new Request($uri, $method, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Guzzle request object with our custom implementation
|
||||
*
|
||||
* @param string $uri the URI to request
|
||||
* @param string $method the HTTP method (defaults to GET)
|
||||
* @param array $options custom options for this request
|
||||
* @param ?string $context request context
|
||||
*/
|
||||
public function request(string $uri, string $method = 'GET', array $options = [], ?string $context = null): ResponseInterface
|
||||
{
|
||||
return $this->guzzleFactory->getClient($context)->request($method, $uri, $options);
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Http;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Backend\Http\Application as BackendApplication;
|
||||
use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
|
||||
use TYPO3\CMS\Frontend\Http\Application as FrontendApplication;
|
||||
|
||||
final readonly class RequestHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ContainerInterface $container,
|
||||
private BackendEntryPointResolver $backendEntryPointResolver,
|
||||
) {}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($this->container->has(BackendApplication::class) && $this->backendEntryPointResolver->isBackendRoute($request)) {
|
||||
return $this->container->get(BackendApplication::class)->handle($request);
|
||||
}
|
||||
|
||||
if ($this->container->has(FrontendApplication::class)) {
|
||||
return $this->container->get(FrontendApplication::class)->handle($request);
|
||||
}
|
||||
|
||||
throw new \Exception('TYPO3 is not configured. Please install typo3/cms-backend and/or typo3/cms-frontend', 1704788092);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Default implementation for the ResponseInterface of the PSR-7 standard.
|
||||
*
|
||||
* Highly inspired by https://github.com/phly/http/
|
||||
*
|
||||
* @internal Note that this is not public API, use PSR-17 interfaces instead
|
||||
*/
|
||||
class Response extends Message implements ResponseInterface
|
||||
{
|
||||
protected int $statusCode;
|
||||
protected string $reasonPhrase = '';
|
||||
|
||||
protected array $availableStatusCodes = [
|
||||
// INFORMATIONAL CODES
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
102 => 'Processing',
|
||||
103 => 'Early Hints',
|
||||
// SUCCESS CODES
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
207 => 'Multi-Status',
|
||||
208 => 'Already Reported',
|
||||
226 => 'IM Used',
|
||||
// REDIRECTION CODES
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
306 => 'Switch Proxy', // Deprecated
|
||||
307 => 'Temporary Redirect',
|
||||
308 => 'Permanent Redirect',
|
||||
// CLIENT ERROR
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required',
|
||||
408 => 'Request Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed',
|
||||
413 => 'Payload Too Large',
|
||||
414 => 'URI Too Long',
|
||||
415 => 'Unsupported Media Type',
|
||||
416 => 'Range Not Satisfiable',
|
||||
417 => 'Expectation Failed',
|
||||
418 => 'I\'m a teapot',
|
||||
421 => 'Misdirected Request',
|
||||
422 => 'Unprocessable Entity',
|
||||
423 => 'Locked',
|
||||
424 => 'Failed Dependency',
|
||||
425 => 'Unordered Collection',
|
||||
426 => 'Upgrade Required',
|
||||
428 => 'Precondition Required',
|
||||
429 => 'Too Many Requests',
|
||||
431 => 'Request Header Fields Too Large',
|
||||
451 => 'Unavailable For Legal Reasons',
|
||||
// SERVER ERROR
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
506 => 'Variant Also Negotiates',
|
||||
507 => 'Insufficient Storage',
|
||||
508 => 'Loop Detected',
|
||||
509 => 'Bandwidth Limit Exceeded',
|
||||
510 => 'Not Extended',
|
||||
511 => 'Network Authentication Required',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException if any of the given arguments are given
|
||||
*/
|
||||
public function __construct(
|
||||
StreamInterface|string|null $body = 'php://temp',
|
||||
int $statusCode = 200,
|
||||
array $headers = [],
|
||||
string $reasonPhrase = ''
|
||||
) {
|
||||
if (is_string($body)) {
|
||||
$body = new Stream($body, 'rw');
|
||||
}
|
||||
$this->body = $body;
|
||||
|
||||
if (!array_key_exists($statusCode, $this->availableStatusCodes)) {
|
||||
throw new \InvalidArgumentException('The given status code is not a valid HTTP status code.', 1436717278);
|
||||
}
|
||||
$this->statusCode = $statusCode;
|
||||
|
||||
$this->reasonPhrase = $reasonPhrase === '' ? $this->availableStatusCodes[$this->statusCode] : $reasonPhrase;
|
||||
[$this->lowercasedHeaderNames, $headers] = $this->filterHeaders($headers);
|
||||
$this->assertHeaders($headers);
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the response status code. The status code is a 3-digit integer result code of the server's attempt
|
||||
* to understand and satisfy the request.
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified status code and, optionally, reason phrase.
|
||||
*
|
||||
* If no reason phrase is specified, implementations MAY choose to default to the RFC 7231 or IANA recommended
|
||||
* reason phrase for the response's status code.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the immutability of the message, and MUST return an
|
||||
* instance that has the updated status and reason phrase.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc7231#section-6
|
||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
*
|
||||
* @param int $code The 3-digit integer result code to set.
|
||||
* @param string $reasonPhrase The reason phrase to use with the provided status code; if none is provided,
|
||||
* implementations MAY use the defaults as suggested in the HTTP specification.
|
||||
* @throws \InvalidArgumentException For invalid status code arguments.
|
||||
*/
|
||||
public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
|
||||
{
|
||||
if (!array_key_exists($code, $this->availableStatusCodes)) {
|
||||
throw new \InvalidArgumentException('The given status code is not a valid HTTP status code', 1436717279);
|
||||
}
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->statusCode = $code;
|
||||
$clonedObject->reasonPhrase = $reasonPhrase !== '' ? $reasonPhrase : $this->availableStatusCodes[$code];
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the response reason phrase associated with the status code.
|
||||
*
|
||||
* Because a reason phrase is not a required element in a response status line, the reason phrase value MAY be
|
||||
* null. Implementations MAY choose to return the default RFC 7231 recommended reason phrase (or those listed
|
||||
* in the IANA HTTP Status Code Registry) for the response's status code.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc7231#section-6
|
||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
*/
|
||||
public function getReasonPhrase(): string
|
||||
{
|
||||
return $this->reasonPhrase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
|
||||
/**
|
||||
* @internal Note that this is not public API, use PSR-17 interfaces instead.
|
||||
*/
|
||||
#[AsAlias(ResponseFactoryInterface::class, public: true)]
|
||||
readonly class ResponseFactory implements ResponseFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new response.
|
||||
*
|
||||
* @param int $code HTTP status code; defaults to 200
|
||||
* @param string $reasonPhrase Reason phrase to associate with status code
|
||||
*/
|
||||
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
|
||||
{
|
||||
return new Response(null, $code, [], $reasonPhrase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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\Http\Security;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when route requires referrer, which does not match current base URL.
|
||||
*/
|
||||
class InvalidReferrerException extends Exception {}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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\Http\Security;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when route requires referrer, which is missing.
|
||||
*/
|
||||
class MissingReferrerException extends Exception {}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?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\Http\Security;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
readonly class ReferrerEnforcer
|
||||
{
|
||||
private const int TYPE_REFERRER_EMPTY = 1;
|
||||
private const int TYPE_REFERRER_SAME_SITE = 2;
|
||||
private const int TYPE_REFERRER_SAME_ORIGIN = 4;
|
||||
|
||||
public function handle(ServerRequestInterface $request, array $options): ?ResponseInterface
|
||||
{
|
||||
$requestHost = rtrim($this->resolveRequestHost($request), '/') . '/';
|
||||
$requestDir = $this->resolveRequestDir($request);
|
||||
$referrerType = $this->resolveReferrerType($request, $requestHost, $requestDir);
|
||||
// valid referrer, no more actions required
|
||||
if ($referrerType & self::TYPE_REFERRER_SAME_ORIGIN) {
|
||||
return null;
|
||||
}
|
||||
$flags = $options['flags'] ?? [];
|
||||
$expiration = $options['expiration'] ?? 5;
|
||||
$nonce = $request->getAttribute('nonce');
|
||||
// referrer is missing and route requested to refresh
|
||||
// (created HTML refresh to enforce having referrer)
|
||||
if (($request->getQueryParams()['referrer-refresh'] ?? 0) <= time()
|
||||
&& (
|
||||
in_array('refresh-always', $flags, true)
|
||||
|| ($referrerType & self::TYPE_REFERRER_EMPTY && in_array('refresh-empty', $flags, true))
|
||||
|| ($referrerType & self::TYPE_REFERRER_SAME_SITE && in_array('refresh-same-site', $flags, true))
|
||||
)
|
||||
) {
|
||||
$refreshUri = $request->getUri();
|
||||
parse_str($refreshUri->getQuery(), $queryParams);
|
||||
$queryParams['referrer-refresh'] = time() + $expiration;
|
||||
$refreshUri = $refreshUri->withQuery(
|
||||
http_build_query($queryParams, '', '&', PHP_QUERY_RFC3986)
|
||||
);
|
||||
$scriptUri = $this->resolveAbsoluteWebPath(
|
||||
'EXT:core/Resources/Public/JavaScript/referrer-refresh.js',
|
||||
$request
|
||||
);
|
||||
$attributes = ['src' => $scriptUri];
|
||||
if ($nonce instanceof ConsumableNonce) {
|
||||
$attributes['nonce'] = $nonce->consumeStatic(Directive::ScriptSrcElem);
|
||||
}
|
||||
// simulating navigate event by clicking anchor link
|
||||
// since meta-refresh won't change `document.referrer` in e.g. Firefox
|
||||
return new HtmlResponse(sprintf(
|
||||
'<html>'
|
||||
. '<head><link rel="icon" href="data:image/svg+xml,"></head>'
|
||||
. '<body><a href="%s" id="referrer-refresh"> </a>'
|
||||
. '<script %s></script></body>'
|
||||
. '</html>',
|
||||
htmlspecialchars((string)$refreshUri),
|
||||
GeneralUtility::implodeAttributes($attributes, true)
|
||||
));
|
||||
}
|
||||
$subject = $options['subject'] ?? '';
|
||||
if ($referrerType & self::TYPE_REFERRER_EMPTY) {
|
||||
// still empty referrer or invalid referrer, deny route invocation
|
||||
throw new MissingReferrerException(
|
||||
sprintf('Missing referrer%s', $subject !== '' ? ' for ' . $subject : ''),
|
||||
1588095935
|
||||
);
|
||||
}
|
||||
// referrer is given, but does not match current base URL
|
||||
throw new InvalidReferrerException(
|
||||
sprintf('Invalid referrer%s', $subject !== '' ? ' for ' . $subject : ''),
|
||||
1588095936
|
||||
);
|
||||
}
|
||||
|
||||
protected function resolveAbsoluteWebPath(string $target, ServerRequestInterface $request): string
|
||||
{
|
||||
return (string)PathUtility::getSystemResourceUri($target, $request);
|
||||
}
|
||||
|
||||
protected function resolveReferrerType(ServerRequestInterface $request, string $requestHost, string $requestDir): int
|
||||
{
|
||||
$referrer = $request->getServerParams()['HTTP_REFERER'] ?? '';
|
||||
if ($referrer === '') {
|
||||
return self::TYPE_REFERRER_EMPTY;
|
||||
}
|
||||
if (str_starts_with($referrer, $requestDir)) {
|
||||
// same-origin implies same-site
|
||||
return self::TYPE_REFERRER_SAME_ORIGIN | self::TYPE_REFERRER_SAME_SITE;
|
||||
}
|
||||
if (str_starts_with($referrer, $requestHost)) {
|
||||
return self::TYPE_REFERRER_SAME_SITE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function resolveRequestHost(ServerRequestInterface $request): string
|
||||
{
|
||||
return $request->getAttribute('normalizedParams')->getRequestHost();
|
||||
}
|
||||
|
||||
protected function resolveRequestDir(ServerRequestInterface $request): string
|
||||
{
|
||||
return $request->getAttribute('normalizedParams')->getRequestDir();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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\Http;
|
||||
|
||||
use GuzzleHttp\Psr7\LazyOpenStream;
|
||||
use GuzzleHttp\Psr7\StreamDecoratorTrait;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* This class implements a stream that can be used like a usual PSR-7 stream
|
||||
* but is additionally able to provide a file-serving fastpath using readfile().
|
||||
* The file this stream refers to is opened on demand.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class SelfEmittableLazyOpenStream implements SelfEmittableStreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
|
||||
protected LazyOpenStream $stream;
|
||||
|
||||
/**
|
||||
* Constructor setting up the PHP resource
|
||||
*/
|
||||
public function __construct(protected string $filename)
|
||||
{
|
||||
$this->stream = new LazyOpenStream($filename, 'r');
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the contents of the file to the output buffer
|
||||
*/
|
||||
public function emit()
|
||||
{
|
||||
readfile($this->filename);
|
||||
}
|
||||
|
||||
public function isWritable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException on failure.
|
||||
*/
|
||||
public function write(string $string): int
|
||||
{
|
||||
throw new \RuntimeException('Cannot write to a ' . self::class, 1538331833);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the underlying stream lazily when required.
|
||||
*/
|
||||
protected function createStream(): StreamInterface
|
||||
{
|
||||
return $this->stream->stream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* A PSR-7 stream which allows to be emitted on its own.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface SelfEmittableStreamInterface extends StreamInterface
|
||||
{
|
||||
/**
|
||||
* Output the contents of the stream to the output buffer
|
||||
*/
|
||||
public function emit();
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Represents a typical request incoming from the server to be processed
|
||||
* by the TYPO3 Core. The original request is built from the ServerRequestFactory
|
||||
* inside TYPO3's Bootstrap.
|
||||
*
|
||||
* Note that the PSR-7 standard works with immutable value objects, meaning that
|
||||
* any modification to a Request object using the "with" methods will result
|
||||
* in a new Request object.
|
||||
*
|
||||
* Highly inspired by https://github.com/phly/http/
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
class ServerRequest extends Request implements ServerRequestInterface
|
||||
{
|
||||
protected array $attributes = [];
|
||||
protected array $cookieParams = [];
|
||||
protected array $queryParams = [];
|
||||
protected array $serverParams = [];
|
||||
protected array $uploadedFiles = [];
|
||||
|
||||
/**
|
||||
* @var array|object|null
|
||||
*/
|
||||
protected $parsedBody;
|
||||
|
||||
/**
|
||||
* Constructor, the only place to set all parameters of this Message/Request
|
||||
*
|
||||
* @param string|UriInterface|null $uri URI for the request, if any.
|
||||
* @param string|null $method HTTP method for the request, if any.
|
||||
* @param string|resource|StreamInterface|null $body Message body, if any.
|
||||
* @param array $headers Headers for the message, if any.
|
||||
* @param array $serverParams Server parameters, typically from $_SERVER
|
||||
* @param array|null $uploadedFiles Upload file information, a tree of UploadedFiles
|
||||
* @throws \InvalidArgumentException for any invalid value.
|
||||
*/
|
||||
public function __construct(string|UriInterface|null $uri = null, $method = null, $body = 'php://input', array $headers = [], array $serverParams = [], ?array $uploadedFiles = null)
|
||||
{
|
||||
if ($uploadedFiles !== null) {
|
||||
$this->validateUploadedFiles($uploadedFiles);
|
||||
}
|
||||
|
||||
parent::__construct($uri, $method ?? 'GET', $body, $headers);
|
||||
|
||||
$this->serverParams = $serverParams;
|
||||
$this->uploadedFiles = $uploadedFiles ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve server parameters.
|
||||
*
|
||||
* Retrieves data related to the incoming request environment,
|
||||
* typically derived from PHP's $_SERVER superglobal. The data IS NOT
|
||||
* REQUIRED to originate from $_SERVER.
|
||||
*/
|
||||
public function getServerParams(): array
|
||||
{
|
||||
return $this->serverParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve cookies.
|
||||
*
|
||||
* Retrieves cookies sent by the client to the server.
|
||||
*
|
||||
* The data MUST be compatible with the structure of the $_COOKIE
|
||||
* super global.
|
||||
*/
|
||||
public function getCookieParams(): array
|
||||
{
|
||||
return $this->cookieParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified cookies.
|
||||
*
|
||||
* The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
|
||||
* be compatible with the structure of $_COOKIE. Typically, this data will
|
||||
* be injected at instantiation.
|
||||
*
|
||||
* This method MUST NOT update the related Cookie header of the request
|
||||
* instance, nor related values in the server params.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated cookie values.
|
||||
*
|
||||
* @param array $cookies Array of key/value pairs representing cookies.
|
||||
*/
|
||||
public function withCookieParams(array $cookies): static
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->cookieParams = $cookies;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve query string arguments.
|
||||
*
|
||||
* Retrieves the deserialized query string arguments, if any.
|
||||
*
|
||||
* Note: the query params might not be in sync with the URI or server
|
||||
* params. If you need to ensure you are only getting the original
|
||||
* values, you may need to parse the query string from `getUri()->getQuery()`
|
||||
* or from the `QUERY_STRING` server param.
|
||||
*/
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified query string arguments.
|
||||
*
|
||||
* These values SHOULD remain immutable over the course of the incoming
|
||||
* request. They MAY be injected during instantiation, such as from PHP's
|
||||
* $_GET superglobal, or MAY be derived from some other value such as the
|
||||
* URI. In cases where the arguments are parsed from the URI, the data
|
||||
* MUST be compatible with what PHP's parse_str() would return for
|
||||
* purposes of how duplicate query parameters are handled, and how nested
|
||||
* sets are handled.
|
||||
*
|
||||
* Setting query string arguments MUST NOT change the URI stored by the
|
||||
* request, nor the values in the server params.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated query string arguments.
|
||||
*
|
||||
* @param array $query Array of query string arguments, typically from $_GET.
|
||||
*/
|
||||
public function withQueryParams(array $query): static
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->queryParams = $query;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve normalized file upload data.
|
||||
*
|
||||
* This method returns upload metadata in a normalized tree, with each leaf
|
||||
* an instance of Psr\Http\Message\UploadedFileInterface.
|
||||
*
|
||||
* These values MAY be prepared from $_FILES or the message body during
|
||||
* instantiation, or MAY be injected via withUploadedFiles().
|
||||
*
|
||||
* @return array An array tree of UploadedFileInterface instances; an empty
|
||||
* array MUST be returned if no data is present.
|
||||
*/
|
||||
public function getUploadedFiles(): array
|
||||
{
|
||||
return $this->uploadedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified uploaded files.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated body parameters.
|
||||
*
|
||||
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
|
||||
* @throws \InvalidArgumentException if an invalid structure is provided.
|
||||
*/
|
||||
public function withUploadedFiles(array $uploadedFiles): static
|
||||
{
|
||||
$this->validateUploadedFiles($uploadedFiles);
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->uploadedFiles = $uploadedFiles;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve any parameters provided in the request body.
|
||||
*
|
||||
* If the request Content-Type is either application/x-www-form-urlencoded
|
||||
* or multipart/form-data, and the request method is POST, this method MUST
|
||||
* return the contents of $_POST.
|
||||
*
|
||||
* Otherwise, this method may return any results of deserializing
|
||||
* the request body content; as parsing returns structured content, the
|
||||
* potential types MUST be arrays or objects only. A null value indicates
|
||||
* the absence of body content.
|
||||
*
|
||||
* @return array|object|null The deserialized body parameters, if any.
|
||||
* These will typically be an array or object.
|
||||
*/
|
||||
public function getParsedBody()
|
||||
{
|
||||
return $this->parsedBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified body parameters.
|
||||
*
|
||||
* These MAY be injected during instantiation.
|
||||
*
|
||||
* If the request Content-Type is either application/x-www-form-urlencoded
|
||||
* or multipart/form-data, and the request method is POST, use this method
|
||||
* ONLY to inject the contents of $_POST.
|
||||
*
|
||||
* The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
|
||||
* deserializing the request body content. Deserialization/parsing returns
|
||||
* structured data, and, as such, this method ONLY accepts arrays or objects,
|
||||
* or a null value if nothing was available to parse.
|
||||
*
|
||||
* As an example, if content negotiation determines that the request data
|
||||
* is a JSON payload, this method could be used to create a request
|
||||
* instance with the deserialized parameters.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated body parameters.
|
||||
*
|
||||
* @param array|object|null $data The deserialized body data. This will
|
||||
* typically be in an array or object.
|
||||
* @throws \InvalidArgumentException if an unsupported argument type is
|
||||
* provided.
|
||||
*/
|
||||
public function withParsedBody($data): static
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->parsedBody = $data;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve attributes derived from the request.
|
||||
*
|
||||
* The request "attributes" may be used to allow injection of any
|
||||
* parameters derived from the request: e.g., the results of path
|
||||
* match operations; the results of decrypting cookies; the results of
|
||||
* deserializing non-form-encoded message bodies; etc. Attributes
|
||||
* will be application and request specific, and CAN be mutable.
|
||||
*
|
||||
* @return array Attributes derived from the request.
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single derived request attribute.
|
||||
*
|
||||
* Retrieves a single derived request attribute as described in
|
||||
* getAttributes(). If the attribute has not been previously set, returns
|
||||
* the default value as provided.
|
||||
*
|
||||
* This method obviates the need for a hasAttribute() method, as it allows
|
||||
* specifying a default value to return if the attribute is not found.
|
||||
*
|
||||
* @see getAttributes()
|
||||
*
|
||||
* @param string $name The attribute name.
|
||||
* @param mixed $default Default value to return if the attribute does not exist.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAttribute(string $name, $default = null)
|
||||
{
|
||||
return $this->attributes[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified derived request attribute.
|
||||
*
|
||||
* This method allows setting a single derived request attribute as
|
||||
* described in getAttributes().
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated attribute.
|
||||
*
|
||||
* @see getAttributes()
|
||||
*
|
||||
* @param string $name The attribute name.
|
||||
* @param mixed $value The value of the attribute.
|
||||
*/
|
||||
public function withAttribute(string $name, $value): static
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->attributes[$name] = $value;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance that removes the specified derived request attribute.
|
||||
*
|
||||
* This method allows removing a single derived request attribute as
|
||||
* described in getAttributes().
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that removes
|
||||
* the attribute.
|
||||
*
|
||||
* @see getAttributes()
|
||||
*
|
||||
* @param string $name The attribute name.
|
||||
*/
|
||||
public function withoutAttribute(string $name): static
|
||||
{
|
||||
$clonedObject = clone $this;
|
||||
if (!isset($clonedObject->attributes[$name])) {
|
||||
return $clonedObject;
|
||||
}
|
||||
unset($clonedObject->attributes[$name]);
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively validate the structure in an uploaded files array.
|
||||
*
|
||||
* @throws \InvalidArgumentException if any leaf is not an UploadedFileInterface instance.
|
||||
*/
|
||||
protected function validateUploadedFiles(array $uploadedFiles): void
|
||||
{
|
||||
foreach ($uploadedFiles as $file) {
|
||||
if (is_array($file)) {
|
||||
$this->validateUploadedFiles($file);
|
||||
continue;
|
||||
}
|
||||
if (!$file instanceof UploadedFileInterface) {
|
||||
throw new \InvalidArgumentException('Invalid file in uploaded files structure.', 1436717281);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestFactoryInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
|
||||
/**
|
||||
* Class to create ServerRequest objects
|
||||
*
|
||||
* Highly inspired by https://github.com/phly/http/
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
#[AsAlias(ServerRequestFactoryInterface::class, public: true)]
|
||||
readonly class ServerRequestFactory implements ServerRequestFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new server request.
|
||||
*
|
||||
* Note that server-params are taken precisely as given - no parsing/processing
|
||||
* of the given values is performed, and, in particular, no attempt is made to
|
||||
* determine the HTTP method or URI, which must be provided explicitly.
|
||||
*
|
||||
* @param string $method The HTTP method associated with the request.
|
||||
* @param UriInterface|string $uri The URI associated with the request.
|
||||
* @param array $serverParams Array of SAPI parameters with which to seed the generated request instance.
|
||||
*/
|
||||
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
|
||||
{
|
||||
return new ServerRequest($uri, $method, null, [], $serverParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a request from the original superglobal variables.
|
||||
*
|
||||
* @return ServerRequest
|
||||
* @throws \InvalidArgumentException when invalid file values given
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
public static function fromGlobals()
|
||||
{
|
||||
$serverParameters = $_SERVER;
|
||||
$headers = static::prepareHeaders($serverParameters);
|
||||
|
||||
$method = $serverParameters['REQUEST_METHOD'] ?? 'GET';
|
||||
try {
|
||||
// Note an early middleware creates NormalizedParams again to attach it as request attribute.
|
||||
// Doing this twice per request is considered ok for now since NormalizedParams is constructed
|
||||
// quite quickly with just a few string operations in it. We also may not want ot attach NormalizedParams
|
||||
// as request attribute here already to not 'pollute' ServerRequest early. Another option is to
|
||||
// hand over NormalizedParams to this method and have callers handle the decision if the attribute
|
||||
// is attached as request attribute on their own.
|
||||
$uri = new Uri(NormalizedParams::createFromServerParams($serverParameters)->getRequestUrl());
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
if (Environment::isCli()) {
|
||||
throw new InvalidRequestUrlOnCliException(
|
||||
'Usage of ' . __METHOD__ . ' on CLI is discouraged. In case you rely on the method, you have to fake a valid request URL using $_SERVER.',
|
||||
1701105725,
|
||||
$e,
|
||||
);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$request = new ServerRequest(
|
||||
$uri,
|
||||
$method,
|
||||
'php://input',
|
||||
$headers,
|
||||
$serverParameters,
|
||||
static::normalizeUploadedFiles($_FILES)
|
||||
);
|
||||
if (!empty($_COOKIE)) {
|
||||
$request = $request->withCookieParams($_COOKIE);
|
||||
}
|
||||
if (!empty($_GET)) {
|
||||
$request = $request->withQueryParams($_GET);
|
||||
}
|
||||
$parsedBody = $_POST;
|
||||
if (empty($parsedBody) && in_array($method, ['PUT', 'PATCH', 'DELETE'])) {
|
||||
parse_str((string)file_get_contents('php://input'), $parsedBody);
|
||||
}
|
||||
if (!empty($parsedBody)) {
|
||||
$request = $request->withParsedBody($parsedBody);
|
||||
}
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch headers from $_SERVER variables
|
||||
* which are only the ones starting with HTTP_* and CONTENT_*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function prepareHeaders(array $server)
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($server as $key => $value) {
|
||||
if (!is_string($key)) {
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($key, 'HTTP_COOKIE')) {
|
||||
// Cookies are handled using the $_COOKIE superglobal
|
||||
continue;
|
||||
}
|
||||
if ($value !== '') {
|
||||
if (str_starts_with($key, 'HTTP_')) {
|
||||
$name = str_replace('_', ' ', substr($key, 5));
|
||||
$name = str_replace(' ', '-', ucwords(strtolower($name)));
|
||||
$name = strtolower($name);
|
||||
$headers[$name] = $value;
|
||||
} elseif (str_starts_with($key, 'CONTENT_')) {
|
||||
$name = substr($key, 8); // Content-
|
||||
$name = 'Content-' . (($name === 'MD5') ? $name : ucfirst(strtolower($name)));
|
||||
$name = strtolower($name);
|
||||
$headers[$name] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize uploaded files
|
||||
*
|
||||
* Transforms each value into an UploadedFileInterface instance, and ensures that nested arrays are normalized.
|
||||
*
|
||||
* @param array $files
|
||||
* @return array
|
||||
* @throws \InvalidArgumentException for unrecognized values
|
||||
*/
|
||||
protected static function normalizeUploadedFiles(array $files)
|
||||
{
|
||||
$normalizedFileUploads = [];
|
||||
foreach ($files as $key => $value) {
|
||||
if ($value instanceof UploadedFileInterface) {
|
||||
$normalizedFileUploads[$key] = $value;
|
||||
} elseif (is_array($value)) {
|
||||
if (isset($value['tmp_name'])) {
|
||||
$uploadedFiles = self::createUploadedFile($value);
|
||||
if ($uploadedFiles) {
|
||||
$normalizedFileUploads[$key] = $uploadedFiles;
|
||||
}
|
||||
} else {
|
||||
$normalizedFileUploads[$key] = self::normalizeUploadedFiles($value);
|
||||
}
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid value in files specification.', 1436717282);
|
||||
}
|
||||
}
|
||||
return $normalizedFileUploads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return an UploadedFile instance from a $_FILES specification.
|
||||
*
|
||||
* If the specification represents an array of values, this method will
|
||||
* recursively resolve uploaded files.
|
||||
*
|
||||
* @param array $value $_FILES structure
|
||||
* @return UploadedFileInterface[]|UploadedFileInterface|null
|
||||
*/
|
||||
protected static function createUploadedFile(array $value)
|
||||
{
|
||||
if (is_array($value['tmp_name'])) {
|
||||
$files = [];
|
||||
foreach (array_keys($value['tmp_name']) as $key) {
|
||||
$data = [
|
||||
'tmp_name' => $value['tmp_name'][$key],
|
||||
'error' => $value['error'][$key],
|
||||
'name' => $value['name'][$key],
|
||||
'type' => $value['type'][$key],
|
||||
];
|
||||
if (isset($value['size'][$key])) {
|
||||
$data['size'] = $value['size'][$key];
|
||||
}
|
||||
$result = self::createUploadedFile($data);
|
||||
if ($result) {
|
||||
$files[$key] = $result;
|
||||
}
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
if (!empty($value['tmp_name'])) {
|
||||
return new UploadedFile($value['tmp_name'], $value['size'] ?? 0, $value['error'], $value['name'], $value['type']);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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\Http;
|
||||
|
||||
/**
|
||||
* Define whether a Set-Cookie header should be sent (it's a Directive)
|
||||
*/
|
||||
enum SetCookieBehavior
|
||||
{
|
||||
case None;
|
||||
case Send;
|
||||
case Remove;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use TYPO3\CMS\Core\Log\LogManager;
|
||||
use TYPO3\CMS\Core\Session\UserSession;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class that is used to apply a SetCookie to a response,
|
||||
* based on the current (authenticated or stateful) session,
|
||||
* in order to use the same session across multiple HTTP requests.
|
||||
*/
|
||||
class SetCookieService
|
||||
{
|
||||
use CookieHeaderTrait;
|
||||
use CookieScopeTrait;
|
||||
|
||||
protected readonly LoggerInterface $logger;
|
||||
|
||||
public static function create(string $name, string $type): self
|
||||
{
|
||||
$lifetime = (int)($GLOBALS['TYPO3_CONF_VARS'][$type]['lifetime'] ?? 0);
|
||||
return new self($name, $type, $lifetime);
|
||||
}
|
||||
|
||||
private function __construct(
|
||||
protected readonly string $name,
|
||||
protected readonly string $loginType,
|
||||
/**
|
||||
* Lifetime for the session-cookie (on the client)
|
||||
*
|
||||
* If >0: permanent cookie with given lifetime
|
||||
* If 0: session-cookie
|
||||
* Session-cookie means the browser will remove it when the browser is closed.
|
||||
*/
|
||||
protected readonly int $lifetime
|
||||
) {
|
||||
$this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the session cookie for the current disposal.
|
||||
*/
|
||||
public function setSessionCookie(UserSession $userSession, NormalizedParams $normalizedParams): ?Cookie
|
||||
{
|
||||
$setCookie = null;
|
||||
$isRefreshTimeBasedCookie = $this->isRefreshTimeBasedCookie($userSession);
|
||||
if ($this->isSetSessionCookie($userSession) || $isRefreshTimeBasedCookie) {
|
||||
// Get the domain to be used for the cookie (if any):
|
||||
$cookieScope = $this->getCookieScope($normalizedParams);
|
||||
// If the cookie lifetime is set, use it:
|
||||
$cookieExpire = $isRefreshTimeBasedCookie ? $GLOBALS['EXEC_TIME'] + $this->lifetime : 0;
|
||||
// Valid options are "strict", "lax" or "none", whereas "none" only works in HTTPS requests (default & fallback is "strict")
|
||||
$cookieSameSite = $this->sanitizeSameSiteCookieValue(
|
||||
strtolower($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieSameSite'] ?? Cookie::SAMESITE_STRICT)
|
||||
);
|
||||
// Use the secure option when the current request is served by a secure connection:
|
||||
// SameSite "none" needs the secure option (only allowed on HTTPS)
|
||||
$isSecure = $cookieSameSite === Cookie::SAMESITE_NONE || $normalizedParams->isHttps();
|
||||
$sessionId = $userSession->getIdentifier();
|
||||
$cookieValue = $userSession->getJwt($cookieScope);
|
||||
$setCookie = new Cookie(
|
||||
$this->name,
|
||||
$cookieValue,
|
||||
$cookieExpire,
|
||||
$cookieScope->path,
|
||||
// Host-Only cookies need to be provided without an explicit domain,
|
||||
// see https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.2.3
|
||||
// and https://datatracker.ietf.org/doc/html/rfc6265#section-5.3
|
||||
// | * If the value of the Domain attribute is "example.com", the user agent will include the cookie
|
||||
// | in the Cookie header when making HTTP requests to example.com, www.example.com, and www.corp.example.com
|
||||
// | * If the server omits the Domain attribute, the user agent will return the cookie only to the origin server.
|
||||
$cookieScope->hostOnly ? null : $cookieScope->domain,
|
||||
$isSecure,
|
||||
true,
|
||||
false,
|
||||
$cookieSameSite
|
||||
);
|
||||
$message = $isRefreshTimeBasedCookie ? 'Updated Cookie: {session}, {domain}' : 'Set Cookie: {session}, {domain}';
|
||||
$this->logger->debug($message, [
|
||||
'session' => sha1($sessionId),
|
||||
'domain' => $cookieScope->domain,
|
||||
]);
|
||||
}
|
||||
return $setCookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a session cookie needs to be set (lifetime=0)
|
||||
*/
|
||||
public function isSetSessionCookie(UserSession $userSession, bool $forceSetCookie = false): bool
|
||||
{
|
||||
if ($this->loginType === 'FE') {
|
||||
return ($userSession->isNew() || $forceSetCookie)
|
||||
&& ($this->lifetime === 0 || !$userSession->isPermanent());
|
||||
}
|
||||
return $userSession->isNew() && $this->lifetime === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a non-session cookie needs to be set (lifetime>0)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function isRefreshTimeBasedCookie(UserSession $userSession): bool
|
||||
{
|
||||
if ($this->loginType === 'FE') {
|
||||
return $this->lifetime > 0 && $userSession->isPermanent();
|
||||
}
|
||||
return $this->lifetime > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this request is going to set a cookie
|
||||
* or a cookie was already found in the system
|
||||
*
|
||||
* @return bool Returns TRUE if a cookie is set
|
||||
*/
|
||||
public function isCookieSet(?ServerRequestInterface $request, ?UserSession $userSession): bool
|
||||
{
|
||||
$isRefreshTimeBasedCookie = $userSession && $this->isRefreshTimeBasedCookie($userSession);
|
||||
if ($isRefreshTimeBasedCookie || $this->isSetSessionCookie($userSession)) {
|
||||
return true;
|
||||
}
|
||||
if ($request && isset($request->getCookieParams()[$this->name])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty / unset the cookie
|
||||
*/
|
||||
public function removeCookie(NormalizedParams $normalizedParams): Cookie
|
||||
{
|
||||
$scope = $this->getCookieScope($normalizedParams);
|
||||
return new Cookie(
|
||||
$this->name,
|
||||
'',
|
||||
-1,
|
||||
$scope->path,
|
||||
$scope->domain
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Default implementation for the StreamInterface of the PSR-7 standard
|
||||
* Acts mainly as a decorator class for streams/resources.
|
||||
*
|
||||
* Highly inspired by https://github.com/phly/http/
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
class Stream implements StreamInterface
|
||||
{
|
||||
/**
|
||||
* The actual PHP resource
|
||||
* @var resource|null
|
||||
*/
|
||||
protected $resource;
|
||||
|
||||
/**
|
||||
* @var string|resource
|
||||
*/
|
||||
protected $stream;
|
||||
|
||||
/**
|
||||
* Constructor setting up the PHP resource
|
||||
*
|
||||
* @param string|resource $stream
|
||||
* @param string $mode Mode with which to open stream
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct($stream, string $mode = 'r')
|
||||
{
|
||||
$this->stream = $stream;
|
||||
if (is_resource($stream)) {
|
||||
$this->resource = $stream;
|
||||
} elseif (is_string($stream)) {
|
||||
$this->resource = fopen($stream, $mode) ?: null;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717284);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all data from the stream into a string, from the beginning to end.
|
||||
*
|
||||
* This method MUST attempt to seek to the beginning of the stream before
|
||||
* reading data and read the stream until the end is reached.
|
||||
*
|
||||
* Warning: This could attempt to load a large amount of data into memory.
|
||||
*
|
||||
* This method MUST NOT raise an exception in order to conform with PHP's
|
||||
* string casting operations.
|
||||
*
|
||||
* @see https://php.net/manual/en/language.oop5.magic.php#object.tostring
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
if (!$this->isReadable()) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
$this->rewind();
|
||||
return $this->getContents();
|
||||
} catch (\RuntimeException $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the stream and any underlying resources.
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
return;
|
||||
}
|
||||
$resource = $this->detach();
|
||||
if ($resource === null) {
|
||||
return;
|
||||
}
|
||||
fclose($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Separates any underlying resources from the stream.
|
||||
*
|
||||
* After the stream has been detached, the stream is in an unusable state.
|
||||
*
|
||||
* @return resource|null Underlying PHP stream, if any
|
||||
*/
|
||||
public function detach()
|
||||
{
|
||||
$resource = $this->resource;
|
||||
$this->resource = null;
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the stream if known.
|
||||
*
|
||||
* @return int|null Returns the size in bytes if known, or null if unknown.
|
||||
*/
|
||||
public function getSize(): ?int
|
||||
{
|
||||
if ($this->resource === null) {
|
||||
return null;
|
||||
}
|
||||
$stats = fstat($this->resource);
|
||||
if ($stats === false) {
|
||||
return null;
|
||||
}
|
||||
return $stats['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current position of the file read/write pointer
|
||||
*
|
||||
* @return int Position of the file pointer
|
||||
* @throws \RuntimeException on error.
|
||||
*/
|
||||
public function tell(): int
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
throw new \RuntimeException('No resource available; cannot tell position', 1436717285);
|
||||
}
|
||||
$result = ftell($this->resource);
|
||||
if (!is_int($result)) {
|
||||
throw new \RuntimeException('Error occurred during tell operation', 1436717286);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the stream is at the end of the stream.
|
||||
*/
|
||||
public function eof(): bool
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
return true;
|
||||
}
|
||||
return feof($this->resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the stream is seekable.
|
||||
*/
|
||||
public function isSeekable(): bool
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$this->getMetadata('seekable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a position in the stream.
|
||||
*
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
*
|
||||
* @param int $offset Stream offset
|
||||
* @param int $whence Specifies how the cursor position will be calculated
|
||||
* based on the seek offset. Valid values are identical to the built-in
|
||||
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
||||
* offset bytes SEEK_CUR: Set position to current location plus offset
|
||||
* SEEK_END: Set position to end-of-stream plus offset.
|
||||
*
|
||||
* @throws \RuntimeException on failure.
|
||||
*/
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
throw new \RuntimeException('No resource available; cannot seek position', 1436717287);
|
||||
}
|
||||
|
||||
if (!$this->isSeekable()) {
|
||||
throw new \RuntimeException('Stream is not seekable', 1436717288);
|
||||
}
|
||||
$result = fseek($this->resource, $offset, $whence);
|
||||
if ($result !== 0) {
|
||||
throw new \RuntimeException('Error seeking within stream', 1436717289);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to the beginning of the stream.
|
||||
*
|
||||
* If the stream is not seekable, this method will raise an exception;
|
||||
* otherwise, it will perform a seek(0).
|
||||
*
|
||||
* @see seek()
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @throws \RuntimeException on failure.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the stream is writable.
|
||||
*/
|
||||
public function isWritable(): bool
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
return false;
|
||||
}
|
||||
$uri = $this->getMetadata('uri');
|
||||
$mode = $this->getMetadata('mode');
|
||||
return is_writable($uri) || strpbrk($mode, 'aw+') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to the stream.
|
||||
*
|
||||
* @param string $string The string that is to be written.
|
||||
* @return int Returns the number of bytes written to the stream.
|
||||
* @throws \RuntimeException on failure.
|
||||
*/
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
throw new \RuntimeException('No resource available; cannot write', 1436717290);
|
||||
}
|
||||
$result = fwrite($this->resource, $string);
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException('Error writing to stream', 1436717291);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the stream is readable.
|
||||
*/
|
||||
public function isReadable(): bool
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
return false;
|
||||
}
|
||||
$mode = $this->getMetadata('mode');
|
||||
return str_contains($mode, 'r') || str_contains($mode, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from the stream.
|
||||
*
|
||||
* @param int $length Read up to $length bytes from the object and return
|
||||
* them. Fewer than $length bytes may be returned if underlying stream
|
||||
* call returns fewer bytes.
|
||||
* @return string Returns the data read from the stream, or an empty string
|
||||
* if no bytes are available.
|
||||
* @throws \RuntimeException if an error occurs.
|
||||
*/
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
throw new \RuntimeException('No resource available; cannot read', 1436717292);
|
||||
}
|
||||
if (!$this->isReadable()) {
|
||||
throw new \RuntimeException('Stream is not readable', 1436717293);
|
||||
}
|
||||
$result = fread($this->resource, $length);
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException('Error reading stream', 1436717294);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remaining contents in a string
|
||||
*/
|
||||
public function getContents(): string
|
||||
{
|
||||
if (!is_resource($this->resource) || !$this->isReadable()) {
|
||||
return '';
|
||||
}
|
||||
// Will always return a string; "false" was only possible if offset > 0, which we do not use.
|
||||
return stream_get_contents($this->resource, null, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stream metadata as an associative array or retrieve a specific key.
|
||||
*
|
||||
* The keys returned are identical to the keys returned from PHP's
|
||||
* stream_get_meta_data() function.
|
||||
*
|
||||
* @link https://php.net/manual/en/function.stream-get-meta-data.php
|
||||
*
|
||||
* @param string $key Specific metadata to retrieve.
|
||||
*
|
||||
* @return array|mixed|null Returns an associative array if no key is
|
||||
* provided. Returns a specific key value if a key is provided and the
|
||||
* value is found, or null if the key is not found.
|
||||
*/
|
||||
public function getMetadata(?string $key = null)
|
||||
{
|
||||
if (!is_resource($this->resource)) {
|
||||
return null;
|
||||
}
|
||||
$metadata = stream_get_meta_data($this->resource);
|
||||
if ($key === null) {
|
||||
return $metadata;
|
||||
}
|
||||
if (!isset($metadata[$key])) {
|
||||
return null;
|
||||
}
|
||||
return $metadata[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a new stream/resource to the instance.
|
||||
*
|
||||
* @param string|resource $resource
|
||||
* @param string $mode
|
||||
* @throws \InvalidArgumentException for stream identifier that cannot be cast to a resource
|
||||
* @throws \InvalidArgumentException for non-resource stream
|
||||
*/
|
||||
public function attach($resource, string $mode = 'r')
|
||||
{
|
||||
$error = null;
|
||||
if (!is_resource($resource) && is_string($resource)) {
|
||||
set_error_handler(static function ($e) use (&$error): bool {
|
||||
$error = $e;
|
||||
return true;
|
||||
}, E_WARNING);
|
||||
$resource = fopen($resource, $mode);
|
||||
restore_error_handler();
|
||||
}
|
||||
if ($error) {
|
||||
throw new \InvalidArgumentException('Invalid stream reference provided', 1436717296);
|
||||
}
|
||||
if (!is_resource($resource)) {
|
||||
throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717297);
|
||||
}
|
||||
$this->resource = $resource;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Http;
|
||||
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
|
||||
/**
|
||||
* @internal Note that this is not public API, use PSR-17 interfaces instead.
|
||||
*/
|
||||
#[AsAlias(StreamFactoryInterface::class, public: true)]
|
||||
readonly class StreamFactory implements StreamFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new stream from a string.
|
||||
*
|
||||
* @param string $content String content with which to populate the stream.
|
||||
*/
|
||||
public function createStream(string $content = ''): StreamInterface
|
||||
{
|
||||
$stream = new Stream('php://temp', 'r+');
|
||||
if ($content !== '') {
|
||||
$stream->write($content);
|
||||
}
|
||||
return $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a stream from an existing file.
|
||||
*
|
||||
* The `$filename` MAY be any string supported by `fopen()`.
|
||||
*
|
||||
* @param string $filename Filename or stream URI to use as basis of stream.
|
||||
* @param string $mode Mode with which to open the underlying filename/stream.
|
||||
* @throws \RuntimeException If the file cannot be opened.
|
||||
* @throws \InvalidArgumentException If the mode is invalid.
|
||||
*/
|
||||
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
|
||||
{
|
||||
$resource = @fopen($filename, $mode);
|
||||
if ($resource === false) {
|
||||
if ($mode === '' || in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true) === false) {
|
||||
throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.', 1566823434);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('The file ' . $filename . ' cannot be opened.', 1566823435);
|
||||
}
|
||||
|
||||
return new Stream($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new stream from an existing resource.
|
||||
*
|
||||
* The stream MUST be readable and may be writable.
|
||||
*
|
||||
* @param resource $resource PHP resource to use as basis of stream.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function createStreamFromResource($resource): StreamInterface
|
||||
{
|
||||
if (!is_resource($resource) || get_resource_type($resource) !== 'stream') {
|
||||
throw new \InvalidArgumentException('Invalid stream provided; must be a stream resource', 1566853697);
|
||||
}
|
||||
return new Stream($resource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use TYPO3\CMS\Core\Resource\Exception\UploadSizeException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class which represents one uploaded file, usually coming
|
||||
* from $_FILES, according to PSR-7 standard.
|
||||
*
|
||||
* Highly inspired by https://github.com/phly/http/
|
||||
*/
|
||||
class UploadedFile implements UploadedFileInterface
|
||||
{
|
||||
protected ?string $file = null;
|
||||
protected ?StreamInterface $stream = null;
|
||||
protected ?string $clientFilename;
|
||||
protected ?string $clientMediaType;
|
||||
protected int $error;
|
||||
protected bool $moved = false;
|
||||
protected int $size;
|
||||
|
||||
/**
|
||||
* Constructor method
|
||||
*
|
||||
* @param string|resource|StreamInterface $input is either a stream or a filename
|
||||
* @param int $size see $_FILES['size'] from PHP
|
||||
* @param int $errorStatus see $_FILES['error']
|
||||
* @param string|null $clientFilename the original filename handed over from the client
|
||||
* @param string|null $clientMediaType the media type (optional)
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct($input, int $size, int $errorStatus, ?string $clientFilename = null, ?string $clientMediaType = null)
|
||||
{
|
||||
if (is_string($input)) {
|
||||
$this->file = $input;
|
||||
}
|
||||
|
||||
if (is_resource($input)) {
|
||||
$this->stream = new Stream($input);
|
||||
} elseif ($input instanceof StreamInterface) {
|
||||
$this->stream = $input;
|
||||
}
|
||||
|
||||
if (!$this->file && !$this->stream) {
|
||||
throw new \InvalidArgumentException('The input given was not a valid stream or file.', 1436717301);
|
||||
}
|
||||
|
||||
$this->size = $size;
|
||||
|
||||
if ($errorStatus < 0 || $errorStatus > 8) {
|
||||
throw new \InvalidArgumentException('Invalid error status for an uploaded file. See UPLOAD_ERR_* constant in PHP.', 1436717303);
|
||||
}
|
||||
$this->error = $errorStatus;
|
||||
|
||||
if ($clientFilename !== null) {
|
||||
$clientFilename = \Normalizer::normalize($clientFilename);
|
||||
}
|
||||
$this->clientFilename = is_string($clientFilename) ? $clientFilename : null;
|
||||
$this->clientMediaType = $clientMediaType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a stream representing the uploaded file.
|
||||
* Returns a StreamInterface instance, representing the uploaded file. The purpose of this method
|
||||
* is to allow utilizing native PHP stream functionality to manipulate the file upload, such as
|
||||
* stream_copy_to_stream() (though the result will need to be decorated in a native PHP stream wrapper
|
||||
* to work with such functions).
|
||||
*
|
||||
* If the moveTo() method has been called previously, this method raises an exception.
|
||||
*
|
||||
* @return StreamInterface Stream representation of the uploaded file.
|
||||
* @throws \RuntimeException in cases when no stream is available or can be created.
|
||||
*/
|
||||
public function getStream(): StreamInterface
|
||||
{
|
||||
if ($this->moved) {
|
||||
throw new \RuntimeException('Cannot retrieve stream as it was moved.', 1436717306);
|
||||
}
|
||||
|
||||
if ($this->stream instanceof StreamInterface) {
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
$this->stream = new Stream((string)$this->file);
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the uploaded file to a new location.
|
||||
*
|
||||
* Use this method as an alternative to move_uploaded_file(). This method is
|
||||
* guaranteed to work in both SAPI and non-SAPI environments.
|
||||
* Implementations must determine which environment they are in, and use the
|
||||
* appropriate method (move_uploaded_file(), rename(), or a stream
|
||||
* operation) to perform the operation.
|
||||
*
|
||||
* $targetPath may be an absolute path, or a relative path. If it is a
|
||||
* relative path, resolution should be the same as used by PHP's rename()
|
||||
* function.
|
||||
*
|
||||
* The original file or stream MUST be removed on completion.
|
||||
*
|
||||
* If this method is called more than once, any subsequent calls MUST raise
|
||||
* an exception.
|
||||
*
|
||||
* When used in an SAPI environment where $_FILES is populated, when writing
|
||||
* files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
|
||||
* used to ensure permissions and upload status are verified correctly.
|
||||
*
|
||||
* If you wish to move to a stream, use getStream(), as SAPI operations
|
||||
* cannot guarantee writing to stream destinations.
|
||||
*
|
||||
* @see https://php.net/is_uploaded_file
|
||||
* @see https://php.net/move_uploaded_file
|
||||
* @param string $targetPath Path to which to move the uploaded file.
|
||||
* @throws \InvalidArgumentException if the $path specified is invalid.
|
||||
* @throws \RuntimeException on any error during the move operation, or on the second or subsequent call to the method.
|
||||
*/
|
||||
public function moveTo(string $targetPath): void
|
||||
{
|
||||
if (empty($targetPath)) {
|
||||
throw new \InvalidArgumentException('Invalid path while moving an uploaded file.', 1436717307);
|
||||
}
|
||||
|
||||
if ($this->moved) {
|
||||
throw new \RuntimeException('Cannot move uploaded file, as it was already moved.', 1436717308);
|
||||
}
|
||||
|
||||
// Check if the target path is inside the allowed paths of TYPO3, and make it absolute.
|
||||
$targetPath = GeneralUtility::getFileAbsFileName($targetPath);
|
||||
if (empty($targetPath)) {
|
||||
throw new \RuntimeException('Cannot move uploaded file, as the target path is empty or invalid.', 1436717309);
|
||||
}
|
||||
|
||||
// Max upload size (kb) for files.
|
||||
$maxUploadFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
|
||||
if ($this->size > 0 && $maxUploadFileSize > 0 && $this->size >= $maxUploadFileSize) {
|
||||
unlink($this->file);
|
||||
throw new UploadSizeException('The uploaded file exceeds the size-limit of ' . $maxUploadFileSize . ' bytes', 1647338094);
|
||||
}
|
||||
|
||||
if (!empty($this->file) && is_uploaded_file($this->file)) {
|
||||
if (GeneralUtility::upload_copy_move($this->file, $targetPath) === false) {
|
||||
throw new \RuntimeException('An error occurred while moving uploaded file', 1436717310);
|
||||
}
|
||||
} elseif ($this->stream) {
|
||||
$handle = fopen($targetPath, 'wb+');
|
||||
if ($handle === false) {
|
||||
throw new \RuntimeException('Unable to write to target path.', 1436717311);
|
||||
}
|
||||
|
||||
$this->stream->rewind();
|
||||
while (!$this->stream->eof()) {
|
||||
fwrite($handle, $this->stream->read(4096));
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
$this->moved = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file size.
|
||||
* Usually returns the value stored in the "size" key of
|
||||
* the file in the $_FILES array if available, as PHP calculates this based
|
||||
* on the actual size transmitted.
|
||||
*
|
||||
* @return int|null The file size in bytes or null if unknown.
|
||||
*/
|
||||
public function getSize(): ?int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the error associated with the uploaded file.
|
||||
* Usually returns the value stored in the "error" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
|
||||
*
|
||||
* If the file was uploaded successfully, this method MUST return
|
||||
* UPLOAD_ERR_OK.
|
||||
*
|
||||
* @see https://php.net/manual/en/features.file-upload.errors.php
|
||||
* @return int One of PHP's UPLOAD_ERR_XXX constants.
|
||||
*/
|
||||
public function getError(): int
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the filename sent by the client.
|
||||
* Usually returns the value stored in the "name" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* Do not trust the value returned by this method. A client could send
|
||||
* a malicious filename with the intention to corrupt or hack your
|
||||
* application.
|
||||
*
|
||||
* @return string|null The filename sent by the client or null if none was provided.
|
||||
*/
|
||||
public function getClientFilename(): ?string
|
||||
{
|
||||
return $this->clientFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the temporary file name (for example /tmp/tmp_foo_filexyz
|
||||
* If the file has been moved (by moveTo) an exception is thrown.
|
||||
*
|
||||
* @internal Not part of the PSR interface - used for legacy code in the core
|
||||
*/
|
||||
public function getTemporaryFileName(): ?string
|
||||
{
|
||||
if ($this->moved) {
|
||||
throw new \RuntimeException('Cannot return temporary file name, as it was already moved.', 1436717337);
|
||||
}
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the media type sent by the client.
|
||||
* Usually returns the value stored in the "type" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* Do not trust the value returned by this method. A client could send
|
||||
* a malicious media type with the intention to corrupt or hack your
|
||||
* application.
|
||||
*
|
||||
* @return string|null The media type sent by the client or null if none was provided.
|
||||
*/
|
||||
public function getClientMediaType(): ?string
|
||||
{
|
||||
return $this->clientMediaType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileFactoryInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
|
||||
/**
|
||||
* @internal Note that this is not public API, use PSR-17 interfaces instead.
|
||||
*/
|
||||
#[AsAlias(UploadedFileFactoryInterface::class, public: true)]
|
||||
readonly class UploadedFileFactory implements UploadedFileFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new uploaded file.
|
||||
*
|
||||
* If a size is not provided it will be determined by checking the size of
|
||||
* the file.
|
||||
*
|
||||
* @see https://php.net/manual/features.file-upload.post-method.php
|
||||
* @see https://php.net/manual/features.file-upload.errors.php
|
||||
*
|
||||
* @param StreamInterface $stream Underlying stream representing the uploaded file content.
|
||||
* @param int|null $size in bytes
|
||||
* @param int $error PHP file upload error
|
||||
* @param string|null $clientFilename Filename as provided by the client, if any.
|
||||
* @param string|null $clientMediaType Media type as provided by the client, if any.
|
||||
* @throws \InvalidArgumentException If the file resource is not readable.
|
||||
*/
|
||||
public function createUploadedFile(
|
||||
StreamInterface $stream,
|
||||
?int $size = null,
|
||||
int $error = \UPLOAD_ERR_OK,
|
||||
?string $clientFilename = null,
|
||||
?string $clientMediaType = null
|
||||
): UploadedFileInterface {
|
||||
if ($size === null) {
|
||||
$size = $stream->getSize();
|
||||
if ($size === null) {
|
||||
throw new \InvalidArgumentException('Stream size could not be determined.', 1566823423);
|
||||
}
|
||||
}
|
||||
|
||||
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,761 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Represents a URI based on the PSR-7 Standard.
|
||||
*
|
||||
* Highly inspired by https://github.com/phly/http/
|
||||
*
|
||||
* @internal Note that this is not public API yet.
|
||||
*/
|
||||
class Uri implements UriInterface
|
||||
{
|
||||
/**
|
||||
* Sub-delimiters used in query strings and fragments.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const SUBDELIMITER_CHARLIST = '!\$&\'\(\)\*\+,;=';
|
||||
|
||||
/**
|
||||
* Unreserved characters used in paths, query strings, and fragments.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const UNRESERVED_CHARLIST = 'a-zA-Z0-9_\-\.~';
|
||||
|
||||
/**
|
||||
* The default scheme for the URI
|
||||
*/
|
||||
protected string $scheme = '';
|
||||
|
||||
/**
|
||||
* @var int[] Associative array containing schemes and their default ports.
|
||||
*/
|
||||
protected array $supportedSchemes = [
|
||||
'http' => 80,
|
||||
'https' => 443,
|
||||
'ws' => 80,
|
||||
'wss' => 443,
|
||||
];
|
||||
|
||||
/**
|
||||
* The authority part of the URI
|
||||
*/
|
||||
protected string $authority = '';
|
||||
|
||||
/**
|
||||
* The userInfo part of the URI
|
||||
*/
|
||||
protected string $userInfo = '';
|
||||
|
||||
/**
|
||||
* The host part of the URI
|
||||
*/
|
||||
protected string $host = '';
|
||||
|
||||
/**
|
||||
* The port of the URI (empty if it is the standard port for the scheme)
|
||||
*/
|
||||
protected ?int $port = null;
|
||||
|
||||
/**
|
||||
* The path part of the URI (can be empty or /)
|
||||
*/
|
||||
protected string $path = '';
|
||||
|
||||
/**
|
||||
* The query part of the URI without the ?
|
||||
*/
|
||||
protected string $query = '';
|
||||
|
||||
/**
|
||||
* The fragment part of the URI without the # before
|
||||
*/
|
||||
protected string $fragment = '';
|
||||
|
||||
/**
|
||||
* Instructs the parser to skip the scheme validation.
|
||||
*/
|
||||
protected bool $allowAnyScheme = false;
|
||||
|
||||
/**
|
||||
* Instructs the parser to skip the validation for `$supportedSchemes`.
|
||||
* Use this factory method carefully in web contexts, since URIs
|
||||
* might contain PHP stream wrappers (`phar://`, `php://`), which
|
||||
* have a different meaning and are not considered as URI.
|
||||
*
|
||||
* @param string $uri The full URI including query string and fragment
|
||||
*/
|
||||
public static function fromAnyScheme(string $uri = ''): self
|
||||
{
|
||||
$target = new self();
|
||||
$target->allowAnyScheme = true;
|
||||
if (!empty($uri)) {
|
||||
$target->parseUri($uri);
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uri The full URI including query string and fragment
|
||||
* @throws \InvalidArgumentException if the URI is malformed.
|
||||
*/
|
||||
public function __construct(string $uri = '')
|
||||
{
|
||||
if (!empty($uri)) {
|
||||
$this->parseUri($uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to parse the full URI string
|
||||
* @throws \InvalidArgumentException if the URI is malformed.
|
||||
*/
|
||||
protected function parseUri(string $uri): void
|
||||
{
|
||||
$uriParts = parse_url($uri);
|
||||
|
||||
if ($uriParts === false) {
|
||||
throw new \InvalidArgumentException('The parsedUri "' . $uri . '" appears to be malformed', 1436717322);
|
||||
}
|
||||
|
||||
if (isset($uriParts['scheme'])) {
|
||||
$this->scheme = $this->sanitizeScheme($uriParts['scheme']);
|
||||
}
|
||||
if (isset($uriParts['user'])) {
|
||||
$this->userInfo = $uriParts['user'];
|
||||
if (isset($uriParts['pass'])) {
|
||||
$this->userInfo .= ':' . $uriParts['pass'];
|
||||
}
|
||||
}
|
||||
if (isset($uriParts['host'])) {
|
||||
$this->host = $uriParts['host'];
|
||||
if (filter_var($this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) {
|
||||
$this->host = '[' . $this->host . ']';
|
||||
}
|
||||
}
|
||||
if (isset($uriParts['port'])) {
|
||||
$port = (int)$uriParts['port'];
|
||||
if (!$this->validatePort($port)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'The URI "' . $uri . '" appears to be malformed, invalid port "' . $port . '" specified, must be a valid TCP/UDP port',
|
||||
1728057215
|
||||
);
|
||||
}
|
||||
$this->port = $port;
|
||||
}
|
||||
if (isset($uriParts['path'])) {
|
||||
$this->path = $this->sanitizePath($uriParts['path']);
|
||||
}
|
||||
if (isset($uriParts['query'])) {
|
||||
$this->query = $this->sanitizeQuery($uriParts['query']);
|
||||
}
|
||||
if (isset($uriParts['fragment'])) {
|
||||
$this->fragment = $this->sanitizeFragment($uriParts['fragment']);
|
||||
}
|
||||
|
||||
if (!$this->validate()) {
|
||||
throw new \InvalidArgumentException('The URI "' . $uri . '" appears to be malformed', 1728057216);
|
||||
}
|
||||
}
|
||||
|
||||
protected function validate(): bool
|
||||
{
|
||||
$url = clone $this;
|
||||
|
||||
if ($url->scheme === '') {
|
||||
// filter_var will mark //example.com/ as invalid, let's pretend it's https in this case
|
||||
$url->scheme = 'https';
|
||||
}
|
||||
|
||||
if ($url->host === '') {
|
||||
// filter_var will mark /mypath/ as invalid, let's pretend it's localhost in this case
|
||||
$url->host = 'localhost';
|
||||
} else {
|
||||
// filter_var can not validate UTF8 encoded hosts
|
||||
$host = idn_to_ascii($url->host);
|
||||
if ($host !== false) {
|
||||
$url->host = $host;
|
||||
}
|
||||
}
|
||||
|
||||
return filter_var($url->__toString(), FILTER_VALIDATE_URL) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the scheme component of the URI.
|
||||
*
|
||||
* If no scheme is present, this method MUST return an empty string.
|
||||
*
|
||||
* The value returned MUST be normalized to lowercase, per RFC 3986
|
||||
* Section 3.1.
|
||||
*
|
||||
* The trailing ":" character is not part of the scheme and MUST NOT be
|
||||
* added.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.1
|
||||
* @return string The URI scheme.
|
||||
*/
|
||||
public function getScheme(): string
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the authority component of the URI.
|
||||
*
|
||||
* If no authority information is present, this method MUST return an empty
|
||||
* string.
|
||||
*
|
||||
* The authority syntax of the URI is:
|
||||
*
|
||||
* <pre>
|
||||
* [user-info@]host[:port]
|
||||
* </pre>
|
||||
*
|
||||
* If the port component is not set or is the standard port for the current
|
||||
* scheme, it SHOULD NOT be included.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.2
|
||||
* @return string The URI authority, in "[user-info@]host[:port]" format.
|
||||
*/
|
||||
public function getAuthority(): string
|
||||
{
|
||||
if (empty($this->host)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$authority = $this->host;
|
||||
if (!empty($this->userInfo)) {
|
||||
$authority = $this->userInfo . '@' . $authority;
|
||||
}
|
||||
|
||||
if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
|
||||
$authority .= ':' . $this->port;
|
||||
}
|
||||
|
||||
return $authority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the user information component of the URI.
|
||||
*
|
||||
* If no user information is present, this method MUST return an empty
|
||||
* string.
|
||||
*
|
||||
* If a user is present in the URI, this will return that value;
|
||||
* additionally, if the password is also present, it will be appended to the
|
||||
* user value, with a colon (":") separating the values.
|
||||
*
|
||||
* The trailing "@" character is not part of the user information and MUST
|
||||
* NOT be added.
|
||||
*
|
||||
* @return string The URI user information, in "username[:password]" format.
|
||||
*/
|
||||
public function getUserInfo(): string
|
||||
{
|
||||
return $this->userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the host component of the URI.
|
||||
*
|
||||
* If no host is present, this method MUST return an empty string.
|
||||
*
|
||||
* The value returned MUST be normalized to lowercase, per RFC 3986
|
||||
* Section 3.2.2.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
|
||||
* @return string The URI host.
|
||||
*/
|
||||
public function getHost(): string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the port component of the URI.
|
||||
*
|
||||
* If a port is present, and it is non-standard for the current scheme,
|
||||
* this method MUST return it as an integer. If the port is the standard port
|
||||
* used with the current scheme, this method SHOULD return null.
|
||||
*
|
||||
* If no port is present, and no scheme is present, this method MUST return
|
||||
* a null value.
|
||||
*
|
||||
* If no port is present, but a scheme is present, this method MAY return
|
||||
* the standard port for that scheme, but SHOULD return null.
|
||||
*
|
||||
* @return int|null The URI port.
|
||||
*/
|
||||
public function getPort(): ?int
|
||||
{
|
||||
return $this->isNonStandardPort($this->scheme, $this->host, $this->port) ? $this->port : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the path component of the URI.
|
||||
*
|
||||
* The path can either be empty or absolute (starting with a slash) or
|
||||
* rootless (not starting with a slash). Implementations MUST support all
|
||||
* three syntaxes.
|
||||
*
|
||||
* Normally, the empty path "" and absolute path "/" are considered equal as
|
||||
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
|
||||
* do this normalization because in contexts with a trimmed base path, e.g.
|
||||
* the front controller, this difference becomes significant. It's the task
|
||||
* of the user to handle both "" and "/".
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.3.
|
||||
*
|
||||
* As an example, if the value should include a slash ("/") not intended as
|
||||
* delimiter between path segments, that value MUST be passed in encoded
|
||||
* form (e.g., "%2F") to the instance.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.3
|
||||
* @return string The URI path.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the query string of the URI.
|
||||
*
|
||||
* If no query string is present, this method MUST return an empty string.
|
||||
*
|
||||
* The leading "?" character is not part of the query and MUST NOT be
|
||||
* added.
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.4.
|
||||
*
|
||||
* As an example, if a value in a key/value pair of the query string should
|
||||
* include an ampersand ("&") not intended as a delimiter between values,
|
||||
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.4
|
||||
* @return string The URI query string.
|
||||
*/
|
||||
public function getQuery(): string
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the fragment component of the URI.
|
||||
*
|
||||
* If no fragment is present, this method MUST return an empty string.
|
||||
*
|
||||
* The leading "#" character is not part of the fragment and MUST NOT be
|
||||
* added.
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.5.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.5
|
||||
* @return string The URI fragment.
|
||||
*/
|
||||
public function getFragment(): string
|
||||
{
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified scheme.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified scheme.
|
||||
*
|
||||
* Implementations MUST support the schemes "http" and "https" case
|
||||
* insensitively, and MAY accommodate other schemes if required.
|
||||
*
|
||||
* An empty scheme is equivalent to removing the scheme.
|
||||
*
|
||||
* @param string $scheme The scheme to use with the new instance.
|
||||
* @return static A new instance with the specified scheme.
|
||||
* @throws \InvalidArgumentException for invalid or unsupported schemes.
|
||||
*/
|
||||
public function withScheme(string $scheme): UriInterface
|
||||
{
|
||||
$scheme = $this->sanitizeScheme($scheme);
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->scheme = $scheme;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified user information.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified user information.
|
||||
*
|
||||
* Password is optional, but the user information MUST include the
|
||||
* user; an empty string for the user is equivalent to removing user
|
||||
* information.
|
||||
*
|
||||
* @param string $user The username to use for authority.
|
||||
* @param string|null $password The password associated with $user.
|
||||
*
|
||||
* @return static A new instance with the specified user information.
|
||||
*/
|
||||
public function withUserInfo(string $user, ?string $password = null): UriInterface
|
||||
{
|
||||
$userInfo = $user;
|
||||
if (!empty($password)) {
|
||||
$userInfo .= ':' . $password;
|
||||
}
|
||||
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->userInfo = $userInfo;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified host.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified host.
|
||||
*
|
||||
* An empty host value is equivalent to removing the host.
|
||||
*
|
||||
* @param string $host The hostname to use with the new instance.
|
||||
* @return static A new instance with the specified host.
|
||||
* @throws \InvalidArgumentException for invalid hostnames.
|
||||
*/
|
||||
public function withHost(string $host): UriInterface
|
||||
{
|
||||
if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) {
|
||||
$host = '[' . $host . ']';
|
||||
}
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->host = $host;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified port.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified port.
|
||||
*
|
||||
* Implementations MUST raise an exception for ports outside the
|
||||
* established TCP and UDP port ranges.
|
||||
*
|
||||
* A null value provided for the port is equivalent to removing the port
|
||||
* information.
|
||||
*
|
||||
* @param int|null $port The port to use with the new instance; a null value
|
||||
* removes the port information.
|
||||
* @return static A new instance with the specified port.
|
||||
* @throws \InvalidArgumentException for invalid ports.
|
||||
*/
|
||||
public function withPort(?int $port): UriInterface
|
||||
{
|
||||
if ($port !== null && !$this->validatePort($port)) {
|
||||
throw new \InvalidArgumentException('Invalid port "' . $port . '" specified, must be a valid TCP/UDP port.', 1436717326);
|
||||
}
|
||||
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->port = $port;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
protected function validatePort(int $port): bool
|
||||
{
|
||||
if ($port < 1 || $port > 65535) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified path.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified path.
|
||||
*
|
||||
* The path can either be empty or absolute (starting with a slash) or
|
||||
* rootless (not starting with a slash). Implementations MUST support all
|
||||
* three syntaxes.
|
||||
*
|
||||
* If the path is intended to be domain-relative rather than path relative then
|
||||
* it must begin with a slash ("/"). Paths not starting with a slash ("/")
|
||||
* are assumed to be relative to some base path known to the application or
|
||||
* consumer.
|
||||
*
|
||||
* Users can provide both encoded and decoded path characters.
|
||||
* Implementations ensure the correct encoding as outlined in getPath().
|
||||
*
|
||||
* @param string $path The path to use with the new instance.
|
||||
* @return static A new instance with the specified path.
|
||||
* @throws \InvalidArgumentException for invalid paths.
|
||||
*/
|
||||
public function withPath(string $path): UriInterface
|
||||
{
|
||||
if (str_contains($path, '?')) {
|
||||
throw new \InvalidArgumentException('Invalid path provided. Must not contain a query string.', 1436717330);
|
||||
}
|
||||
|
||||
if (str_contains($path, '#')) {
|
||||
throw new \InvalidArgumentException('Invalid path provided; must not contain a URI fragment', 1436717332);
|
||||
}
|
||||
|
||||
$path = $this->sanitizePath($path);
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->path = $path;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified query string.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified query string.
|
||||
*
|
||||
* Users can provide both encoded and decoded query characters.
|
||||
* Implementations ensure the correct encoding as outlined in getQuery().
|
||||
*
|
||||
* An empty query string value is equivalent to removing the query string.
|
||||
*
|
||||
* @param string $query The query string to use with the new instance.
|
||||
* @return static A new instance with the specified query string.
|
||||
* @throws \InvalidArgumentException for invalid query strings.
|
||||
*/
|
||||
public function withQuery(string $query): UriInterface
|
||||
{
|
||||
if (str_contains($query, '#')) {
|
||||
throw new \InvalidArgumentException('Query string must not include a URI fragment.', 1436717336);
|
||||
}
|
||||
|
||||
$query = $this->sanitizeQuery($query);
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->query = $query;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified URI fragment.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified URI fragment.
|
||||
*
|
||||
* Users can provide both encoded and decoded fragment characters.
|
||||
* Implementations ensure the correct encoding as outlined in getFragment().
|
||||
*
|
||||
* An empty fragment value is equivalent to removing the fragment.
|
||||
*
|
||||
* @param string $fragment The fragment to use with the new instance.
|
||||
* @return static A new instance with the specified fragment.
|
||||
*/
|
||||
public function withFragment(string $fragment): UriInterface
|
||||
{
|
||||
$fragment = $this->sanitizeFragment($fragment);
|
||||
$clonedObject = clone $this;
|
||||
$clonedObject->fragment = $fragment;
|
||||
return $clonedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string representation as a URI reference.
|
||||
*
|
||||
* Depending on which components of the URI are present, the resulting
|
||||
* string is either a full URI or relative reference according to RFC 3986,
|
||||
* Section 4.1. The method concatenates the various components of the URI,
|
||||
* using the appropriate delimiters:
|
||||
*
|
||||
* - If a scheme is present, it MUST be suffixed by ":".
|
||||
* - If an authority is present, it MUST be prefixed by "//".
|
||||
* - The path can be concatenated without delimiters. But there are two
|
||||
* cases where the path has to be adjusted to make the URI reference
|
||||
* valid as PHP does not allow to throw an exception in __toString():
|
||||
* - If the path is rootless and an authority is present, the path MUST
|
||||
* be prefixed by "/".
|
||||
* - If the path is starting with more than one "/" and no authority is
|
||||
* present, the starting slashes MUST be reduced to one.
|
||||
* - If a query is present, it MUST be prefixed by "?".
|
||||
* - If a fragment is present, it MUST be prefixed by "#".
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-4.1
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$uri = '';
|
||||
|
||||
if (!empty($this->scheme)) {
|
||||
$uri .= $this->scheme . ':';
|
||||
}
|
||||
|
||||
$authority = $this->getAuthority();
|
||||
if (!empty($authority)) {
|
||||
$uri .= '//' . $authority;
|
||||
}
|
||||
|
||||
$uri .= $this->normalizePathForStringification($authority, $this->getPath());
|
||||
|
||||
if ($this->query) {
|
||||
$uri .= '?' . $this->query;
|
||||
}
|
||||
if ($this->fragment) {
|
||||
$uri .= '#' . $this->fragment;
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
private function normalizePathForStringification(string $authority, string $path): string
|
||||
{
|
||||
$isRootless = $path !== '' && !str_starts_with($path, '/');
|
||||
if ($isRootless) {
|
||||
if ($authority === '') {
|
||||
// See: https://datatracker.ietf.org/doc/html/rfc3986#page-26:~:text=A%20path%20segment%20that%20contains%20a%20colon%20character%20(e.g.%2C%20%22this%3Athat%22)
|
||||
$pathParts = explode('/', $path, 2);
|
||||
if (str_contains($pathParts[0], ':')) {
|
||||
$path = './' . $path;
|
||||
}
|
||||
} else {
|
||||
$path = '/' . $path;
|
||||
}
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a given port non-standard for the current scheme?
|
||||
*/
|
||||
protected function isNonStandardPort(string $scheme, string $host, ?int $port): bool
|
||||
{
|
||||
if (empty($scheme)) {
|
||||
return empty($host) || !empty($port);
|
||||
}
|
||||
if (empty($host) || empty($port)) {
|
||||
return false;
|
||||
}
|
||||
return !isset($this->supportedSchemes[$scheme]) || $port !== $this->supportedSchemes[$scheme];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the scheme to ensure it is a valid scheme.
|
||||
*
|
||||
* @param string $scheme Scheme name.
|
||||
* @return string Filtered scheme.
|
||||
* @throws \InvalidArgumentException when a scheme is given which is not supported
|
||||
*/
|
||||
protected function sanitizeScheme(string $scheme): string
|
||||
{
|
||||
$scheme = strtolower($scheme);
|
||||
$scheme = preg_replace('#:(//)?$#', '', $scheme);
|
||||
|
||||
if (empty($scheme)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->allowAnyScheme && !array_key_exists($scheme, $this->supportedSchemes)) {
|
||||
throw new \InvalidArgumentException('Unsupported scheme "' . $scheme . '"; must be any empty string or in the set (' . implode(', ', array_keys($this->supportedSchemes)) . ')', 1436717338);
|
||||
}
|
||||
|
||||
return $scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the path of a URI to ensure it is properly encoded.
|
||||
*/
|
||||
protected function sanitizePath(string $path): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'/(?:[^' . self::UNRESERVED_CHARLIST . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
|
||||
static function ($matches) {
|
||||
return rawurlencode($matches[0]);
|
||||
},
|
||||
$path
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a query string to ensure it is properly encoded.
|
||||
* Ensures that the values in the query string are properly urlencoded.
|
||||
*/
|
||||
protected function sanitizeQuery(string $query): string
|
||||
{
|
||||
if (!empty($query) && str_starts_with($query, '?')) {
|
||||
$query = substr($query, 1);
|
||||
}
|
||||
|
||||
$parts = explode('&', $query);
|
||||
foreach ($parts as $index => $part) {
|
||||
[$key, $value] = $this->splitQueryValue($part);
|
||||
if ($value === null) {
|
||||
$parts[$index] = $this->sanitizeQueryOrFragment($key);
|
||||
continue;
|
||||
}
|
||||
$parts[$index] = $this->sanitizeQueryOrFragment($key) . '=' . $this->sanitizeQueryOrFragment($value);
|
||||
}
|
||||
|
||||
return implode('&', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a query value into a key/value tuple.
|
||||
*
|
||||
* @return array A value with exactly two elements, key and value
|
||||
*/
|
||||
protected function splitQueryValue(string $value): array
|
||||
{
|
||||
$data = explode('=', $value, 2);
|
||||
if (count($data) === 1) {
|
||||
$data[] = null;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a fragment value to ensure it is properly encoded.
|
||||
*/
|
||||
protected function sanitizeFragment(string $fragment): string
|
||||
{
|
||||
if (!empty($fragment) && str_starts_with($fragment, '#')) {
|
||||
$fragment = substr($fragment, 1);
|
||||
}
|
||||
return $this->sanitizeQueryOrFragment($fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a query string key or value, or a fragment.
|
||||
*/
|
||||
protected function sanitizeQueryOrFragment(string $value): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'/(?:[^' . self::UNRESERVED_CHARLIST . self::SUBDELIMITER_CHARLIST . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/',
|
||||
static function ($matches) {
|
||||
return rawurlencode($matches[0]);
|
||||
},
|
||||
$value
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\UriFactoryInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
|
||||
/**
|
||||
* @internal Note that this is not public API, use PSR-17 interfaces instead.
|
||||
*/
|
||||
#[AsAlias(UriFactoryInterface::class, public: true)]
|
||||
readonly class UriFactory implements UriFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new URI.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the given URI cannot be parsed.
|
||||
*/
|
||||
public function createUri(string $uri = ''): UriInterface
|
||||
{
|
||||
return new Uri($uri);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user