TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/vendor/
|
||||
@@ -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\Extbase\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
readonly class Authorize
|
||||
{
|
||||
public function __construct(
|
||||
public string|array|null $callback = null,
|
||||
public bool $requireLogin = false,
|
||||
public array $requireGroups = [],
|
||||
) {
|
||||
if ($callback === null && !$requireLogin && $requireGroups === []) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Authorize attribute requires at least one of: callback, requireLogin, or requireGroups',
|
||||
1761287265
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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\Extbase\Attribute;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
class FileUpload
|
||||
{
|
||||
public function __construct(
|
||||
public readonly array $validation,
|
||||
public readonly string $uploadFolder = '',
|
||||
public readonly bool $addRandomSuffix = true,
|
||||
public readonly bool $createUploadFolderIfNotExist = true,
|
||||
public readonly DuplicationBehavior $duplicationBehavior = DuplicationBehavior::REPLACE,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Extbase\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||
class IgnoreValidation {}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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\Extbase\Attribute\ORM;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
class Cascade
|
||||
{
|
||||
/**
|
||||
* Currently, Extbase does only support "remove".
|
||||
*
|
||||
* Other possible cascade operations would be: "persist", "merge", "detach", "refresh", "all"
|
||||
* @see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html#transitive-persistence-cascade-operations
|
||||
*
|
||||
* @param 'remove'|null $value
|
||||
*/
|
||||
public function __construct(public readonly ?string $value = null) {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Extbase\Attribute\ORM;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
class Lazy {}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Extbase\Attribute\ORM;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
class Transient {}
|
||||
@@ -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\Extbase\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
readonly class RateLimit
|
||||
{
|
||||
public function __construct(
|
||||
public int $limit = 5,
|
||||
public string $interval = '15 minutes',
|
||||
public string $policy = 'sliding_window',
|
||||
public string $message = ''
|
||||
) {
|
||||
if ($this->limit < 1) {
|
||||
throw new \RuntimeException('Invalid "limit" property for rate limit. Ensure, that the value is greater than 0.', 1771074438);
|
||||
}
|
||||
if ($this->interval === '') {
|
||||
throw new \RuntimeException('Invalid "interval" property for rate limit.', 1771074439);
|
||||
}
|
||||
if ($this->policy === '') {
|
||||
throw new \RuntimeException('Invalid "policy" property for rate limit.', 1771074440);
|
||||
}
|
||||
}
|
||||
|
||||
public function getConfiguration(string $identifier): array
|
||||
{
|
||||
return [
|
||||
'id' => 'extbase-' . $identifier,
|
||||
'policy' => $this->policy,
|
||||
'limit' => $this->limit,
|
||||
'interval' => $this->interval,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)]
|
||||
class Validate
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $validator,
|
||||
public readonly array $options = []
|
||||
) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$strings = [];
|
||||
$strings[] = $this->validator;
|
||||
|
||||
if (count($this->options) > 0) {
|
||||
$validatorOptionsStrings = [];
|
||||
foreach ($this->options as $optionKey => $optionValue) {
|
||||
$validatorOptionsStrings[] = $optionKey . '=' . $optionValue;
|
||||
}
|
||||
|
||||
$strings[] = '(' . implode(', ', $validatorOptionsStrings) . ')';
|
||||
}
|
||||
|
||||
return trim(implode(' ', $strings));
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Authorization;
|
||||
|
||||
enum AuthorizationFailureReason: string
|
||||
{
|
||||
case NOT_LOGGED_IN = 'not_logged_in';
|
||||
case MISSING_GROUP = 'missing_group';
|
||||
case CALLBACK_DENIED = 'callback_denied';
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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\Extbase\Authorization;
|
||||
|
||||
use TYPO3\CMS\Extbase\Attribute\Authorize;
|
||||
|
||||
/**
|
||||
* Immutable value object representing the result of an authorization check
|
||||
*/
|
||||
final readonly class AuthorizationResult
|
||||
{
|
||||
private function __construct(
|
||||
public bool $authorized,
|
||||
public ?AuthorizationFailureReason $failureReason = null,
|
||||
public ?Authorize $failedAttribute = null,
|
||||
) {}
|
||||
|
||||
public static function allowed(): self
|
||||
{
|
||||
return new self(authorized: true);
|
||||
}
|
||||
|
||||
public static function denied(AuthorizationFailureReason $reason, Authorize $attribute): self
|
||||
{
|
||||
return new self(
|
||||
authorized: false,
|
||||
failureReason: $reason,
|
||||
failedAttribute: $attribute
|
||||
);
|
||||
}
|
||||
|
||||
public function isAllowed(): bool
|
||||
{
|
||||
return $this->authorized;
|
||||
}
|
||||
|
||||
public function isDenied(): bool
|
||||
{
|
||||
return !$this->authorized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?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\Extbase\Configuration;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Site\Entity\NullSite;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Site\Set\SetRegistry;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
|
||||
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\Utility\RootlineUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* Load TypoScript of a page in backend mode.
|
||||
*
|
||||
* Extbase Backend modules can be configured with Frontend TypoScript. This is of course a very
|
||||
* bad thing, but it is how it is ^^ (we'll get rid of this at some point, promised!)
|
||||
*
|
||||
* First, this means Backend extbase module performance scales with the amount of Frontend
|
||||
* TypoScript. Furthermore, in contrast to Frontend, Backend Modules are not necessarily bound to
|
||||
* pages in the first place - they may not have a page tree und thus no page id at all, like
|
||||
* for instance the ext:beuser module.
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
final readonly class BackendConfigurationManager
|
||||
{
|
||||
public function __construct(
|
||||
private TypoScriptService $typoScriptService,
|
||||
#[Autowire(service: 'cache.typoscript')]
|
||||
private PhpFrontend $typoScriptCache,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
private FrontendInterface $runtimeCache,
|
||||
private SysTemplateRepository $sysTemplateRepository,
|
||||
private SiteFinder $siteFinder,
|
||||
private FrontendTypoScriptFactory $frontendTypoScriptFactory,
|
||||
private ConnectionPool $connectionPool,
|
||||
private SetRegistry $setRegistry,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Loads the Extbase Framework configuration.
|
||||
*
|
||||
* The Extbase framework configuration HAS TO be retrieved using this method, as they are come from different places than the normal settings.
|
||||
* Framework configuration is, in contrast to normal settings, needed for the Extbase framework to operate correctly.
|
||||
*
|
||||
* @param array $configuration low level configuration from outside, typically ContentObjectRenderer TypoScript element config
|
||||
* @param string|null $extensionName if specified, the configuration for the given extension will be returned (plugin.tx_extensionname)
|
||||
* @param string|null $pluginName if specified, the configuration for the given plugin will be returned (plugin.tx_extensionname_pluginname)
|
||||
* @return array the Extbase framework configuration
|
||||
*/
|
||||
public function getConfiguration(ServerRequestInterface $request, array $configuration, ?string $extensionName = null, ?string $pluginName = null): array
|
||||
{
|
||||
$extensionNameFromConfig = $configuration['extensionName'] ?? null;
|
||||
$pluginNameFromConfig = $configuration['pluginName'] ?? null;
|
||||
$configuration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($configuration);
|
||||
|
||||
$typoscriptSetup = $this->getTypoScriptSetup($request);
|
||||
$frameworkConfiguration = [];
|
||||
if (isset($typoscriptSetup['config.']['tx_extbase.'])) {
|
||||
$frameworkConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($typoscriptSetup['config.']['tx_extbase.']);
|
||||
}
|
||||
|
||||
if (!isset($frameworkConfiguration['persistence']['storagePid'])) {
|
||||
$currentPageId = $this->getCurrentPageId($request);
|
||||
$frameworkConfiguration['persistence']['storagePid'] = $currentPageId;
|
||||
}
|
||||
// only merge $configuration and override controller configuration when retrieving configuration of the current plugin
|
||||
if ($extensionName === null || $extensionName === $extensionNameFromConfig && $pluginName === $pluginNameFromConfig) {
|
||||
$pluginConfiguration = $this->getPluginConfiguration($request, (string)$extensionNameFromConfig, (string)$pluginNameFromConfig);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($pluginConfiguration, $configuration);
|
||||
$pluginConfiguration['controllerConfiguration'] = [];
|
||||
} else {
|
||||
$pluginConfiguration = $this->getPluginConfiguration($request, $extensionName, (string)$pluginName);
|
||||
$pluginConfiguration['controllerConfiguration'] = [];
|
||||
}
|
||||
ArrayUtility::mergeRecursiveWithOverrule($frameworkConfiguration, $pluginConfiguration);
|
||||
|
||||
if (!empty($frameworkConfiguration['persistence']['storagePid'])) {
|
||||
if (is_array($frameworkConfiguration['persistence']['storagePid'])) {
|
||||
// We simulate the frontend to enable the use of cObjects in
|
||||
// stdWrap. We then convert the configuration to normal TypoScript
|
||||
// and apply the stdWrap to the storagePid
|
||||
// Use makeInstance here since extbase Bootstrap always setContentObject(null) in Backend, no need to call getContentObject().
|
||||
$conf = $this->typoScriptService->convertPlainArrayToTypoScriptArray($frameworkConfiguration['persistence']);
|
||||
$frameworkConfiguration['persistence']['storagePid'] = GeneralUtility::makeInstance(ContentObjectRenderer::class)->stdWrapValue('storagePid', $conf);
|
||||
}
|
||||
|
||||
if (!empty($frameworkConfiguration['persistence']['recursive'])) {
|
||||
$storagePids = $this->getRecursiveStoragePids(
|
||||
GeneralUtility::intExplode(',', (string)($frameworkConfiguration['persistence']['storagePid'] ?? '')),
|
||||
(int)$frameworkConfiguration['persistence']['recursive']
|
||||
);
|
||||
$frameworkConfiguration['persistence']['storagePid'] = implode(',', $storagePids);
|
||||
}
|
||||
}
|
||||
return $frameworkConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TypoScript Setup array from current Environment.
|
||||
*
|
||||
* @return array the raw TypoScript setup
|
||||
*/
|
||||
public function getTypoScriptSetup(ServerRequestInterface $request): array
|
||||
{
|
||||
$currentPageId = $this->getCurrentPageId($request);
|
||||
|
||||
$cacheIdentifier = 'extbase-backend-typoscript-pageId-' . $currentPageId;
|
||||
$setupArray = $this->runtimeCache->get($cacheIdentifier);
|
||||
if (is_array($setupArray)) {
|
||||
return $setupArray;
|
||||
}
|
||||
|
||||
$site = $request->getAttribute('site');
|
||||
if (($site === null || $site instanceof NullSite) && $currentPageId > 0) {
|
||||
// Due to the weird magic of getting the pid of the first root template when
|
||||
// not having a pageId (extbase BE modules without page tree / no page selected),
|
||||
// we also have no proper site in this case.
|
||||
// So we try to get the site for this pageId. This way, site settings for this
|
||||
// first TS page are turned into constants and can be used in setup and setup
|
||||
// conditions.
|
||||
try {
|
||||
$site = $this->siteFinder->getSiteByPageId($currentPageId);
|
||||
} catch (SiteNotFoundException) {
|
||||
// Keep null / NullSite when no site could be determined for whatever reason.
|
||||
}
|
||||
}
|
||||
if ($site === null) {
|
||||
// If still no site object, have NullSite (usually pid 0).
|
||||
$site = new NullSite();
|
||||
}
|
||||
|
||||
$rootLine = [];
|
||||
$sysTemplateRows = [];
|
||||
if ($currentPageId > 0) {
|
||||
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $currentPageId)->get();
|
||||
// When the site acts as a TypoScript root, limit sys_template lookup to
|
||||
// pages within this site by truncating the rootline at the site root page.
|
||||
// This mirrors the frontend behavior and prevents sys_template records from
|
||||
// parent sites from leaking into the backend TypoScript evaluation.
|
||||
// @see \TYPO3\CMS\Frontend\Page\PageInformationFactory::setSysTemplateRows()
|
||||
$rootLineForSysTemplates = $rootLine;
|
||||
if ($site instanceof Site && $site->isTypoScriptRoot()) {
|
||||
$rootLineForSysTemplates = [];
|
||||
foreach ($rootLine as $index => $rootlinePage) {
|
||||
$rootLineForSysTemplates[$index] = $rootlinePage;
|
||||
if ((int)($rootlinePage['uid'] ?? 0) === $site->getRootPageId()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootline($rootLineForSysTemplates, $request);
|
||||
ksort($rootLine);
|
||||
}
|
||||
$sets = $site instanceof Site ? $this->setRegistry->getSets(...$site->getSets()) : [];
|
||||
if (empty($sysTemplateRows) && $sets === []) {
|
||||
// If no page with sys_template rows or site sets could be derived, we
|
||||
// "fake" a row to trigger inclusion of 'global' TypoScript only.
|
||||
$sysTemplateFakeRow = [
|
||||
'uid' => 0,
|
||||
'pid' => 0,
|
||||
'title' => 'Fake sys_template row to force global TypoScript loading',
|
||||
'root' => 1,
|
||||
'clear' => 3,
|
||||
'include_static_file' => '',
|
||||
'basedOn' => '',
|
||||
'includeStaticAfterBasedOn' => 0,
|
||||
'static_file_mode' => false,
|
||||
'constants' => '',
|
||||
'config' => '',
|
||||
'deleted' => 0,
|
||||
'hidden' => 0,
|
||||
'starttime' => 0,
|
||||
'endtime' => 0,
|
||||
'sorting' => 0,
|
||||
];
|
||||
$sysTemplateRows[] = $sysTemplateFakeRow;
|
||||
}
|
||||
|
||||
$expressionMatcherVariables = [
|
||||
'request' => $request,
|
||||
'pageId' => $currentPageId,
|
||||
'page' => !empty($rootLine) ? array_first($rootLine) : [],
|
||||
'fullRootLine' => $rootLine,
|
||||
'site' => $site,
|
||||
];
|
||||
|
||||
$typoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions($site, $sysTemplateRows, $expressionMatcherVariables, $this->typoScriptCache);
|
||||
$typoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(true, $typoScript, $site, $sysTemplateRows, $expressionMatcherVariables, '0', $this->typoScriptCache, null);
|
||||
$setupArray = $typoScript->getSetupArray();
|
||||
$this->runtimeCache->set($cacheIdentifier, $setupArray);
|
||||
return $setupArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TypoScript configuration found in module.tx_yourextension_yourmodule
|
||||
* merged with the global configuration of your extension from module.tx_yourextension
|
||||
*
|
||||
* @param string|null $pluginName in BE mode this is actually the module signature. But we're using it just like the plugin name in FE
|
||||
*/
|
||||
private function getPluginConfiguration(ServerRequestInterface $request, string $extensionName, ?string $pluginName = null): array
|
||||
{
|
||||
$setup = $this->getTypoScriptSetup($request);
|
||||
$pluginConfiguration = [];
|
||||
if (is_array($setup['module.']['tx_' . strtolower($extensionName) . '.'] ?? false)) {
|
||||
$pluginConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($setup['module.']['tx_' . strtolower($extensionName) . '.']);
|
||||
}
|
||||
if ($pluginName !== null) {
|
||||
$pluginSignature = strtolower($extensionName . '_' . $pluginName);
|
||||
if (is_array($setup['module.']['tx_' . $pluginSignature . '.'] ?? false)) {
|
||||
$overruleConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($setup['module.']['tx_' . $pluginSignature . '.']);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($pluginConfiguration, $overruleConfiguration);
|
||||
}
|
||||
}
|
||||
return $pluginConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page id from the request, accessing POST / GET 'id'
|
||||
*/
|
||||
private function getCurrentPageId(ServerRequestInterface $request): int
|
||||
{
|
||||
// @todo: This misuses 'id' as a broken convention for pages-uid. The filelist module for instance
|
||||
// uses 'id' as "storage-uid:path", which is only mitigated here by testing the argument
|
||||
// with MU:canBeInterpretedAsInteger().
|
||||
// This is in-line with a similar misuse in BackendModuleValidator.
|
||||
$id = 0;
|
||||
$potentialId = $request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0;
|
||||
if (MathUtility::canBeInterpretedAsInteger($potentialId) && $potentialId > 0) {
|
||||
$id = (int)$potentialId;
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of storagePIDs that are below a list of storage pids.
|
||||
*
|
||||
* @param int[] $storagePids Storage PIDs to start at; multiple PIDs possible as comma-separated list
|
||||
* @param int $recursionDepth Maximum number of levels to search, 0 to disable recursive lookup
|
||||
* @return int[] Uid list including the start $storagePids
|
||||
*/
|
||||
private function getRecursiveStoragePids(array $storagePids, int $recursionDepth = 0): array
|
||||
{
|
||||
if ($recursionDepth <= 0) {
|
||||
return $storagePids;
|
||||
}
|
||||
$permsClause = QueryHelper::stripLogicalOperatorPrefix(
|
||||
$this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
|
||||
);
|
||||
$recursiveStoragePids = [];
|
||||
foreach ($storagePids as $startPid) {
|
||||
$startPid = abs($startPid);
|
||||
$recursiveStoragePids = array_merge(
|
||||
$recursiveStoragePids,
|
||||
[ $startPid ],
|
||||
$this->getPageChildrenRecursive($startPid, $recursionDepth, 0, $permsClause)
|
||||
);
|
||||
}
|
||||
return array_unique($recursiveStoragePids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively fetch all children of a given page
|
||||
*
|
||||
* @return int[] List of child row $uid's
|
||||
*/
|
||||
private function getPageChildrenRecursive(int $pid, int $depth, int $begin, string $permsClause): array
|
||||
{
|
||||
$children = [];
|
||||
if ($pid && $depth > 0) {
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$statement = $queryBuilder->select('uid')
|
||||
->from('pages')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->eq('language_tag', 0),
|
||||
$permsClause
|
||||
)
|
||||
->orderBy('uid')
|
||||
->executeQuery();
|
||||
while ($row = $statement->fetchAssociative()) {
|
||||
if ($begin <= 0) {
|
||||
$children[] = (int)$row['uid'];
|
||||
}
|
||||
if ($depth > 1) {
|
||||
$theSubList = $this->getPageChildrenRecursive((int)$row['uid'], $depth - 1, $begin - 1, $permsClause);
|
||||
$children = array_merge($children, $theSubList);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $children;
|
||||
}
|
||||
|
||||
private function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?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\Extbase\Configuration;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\ApplicationType;
|
||||
use TYPO3\CMS\Extbase\Configuration\Exception\NoServerRequestGivenException;
|
||||
|
||||
/**
|
||||
* Generic ConfigurationManager implementation. Uses BackendConfigurationManager
|
||||
* or FrontendConfigurationManager depending on request type.
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class ConfigurationManager implements ConfigurationManagerInterface
|
||||
{
|
||||
private ?ServerRequestInterface $request = null;
|
||||
private array $configuration = [];
|
||||
|
||||
/**
|
||||
* @todo Use runtime cache
|
||||
*/
|
||||
private array $feConfigCache = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly FrontendConfigurationManager $feConfigManager,
|
||||
private readonly BackendConfigurationManager $beConfigManager,
|
||||
) {}
|
||||
|
||||
public function setRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function setConfiguration(array $configuration = []): void
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->feConfigCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified configuration.
|
||||
* The actual configuration will be merged from different sources in a defined order.
|
||||
*
|
||||
* You can get the following types of configuration invoking:
|
||||
* CONFIGURATION_TYPE_SETTINGS: Extbase settings
|
||||
* CONFIGURATION_TYPE_FRAMEWORK: the current module/plugin settings
|
||||
* CONFIGURATION_TYPE_FULL_TYPOSCRIPT: a raw TS array
|
||||
*
|
||||
* Note that this is a low level method and only makes sense to be used by Extbase internally.
|
||||
*
|
||||
* @param string $configurationType The kind of configuration to fetch - must be one of the CONFIGURATION_TYPE_* constants
|
||||
* @param string|null $extensionName if specified, the configuration for the given extension will be returned.
|
||||
* @param string|null $pluginName if specified, the configuration for the given plugin will be returned.
|
||||
* @return array The configuration
|
||||
*/
|
||||
public function getConfiguration(string $configurationType, ?string $extensionName = null, ?string $pluginName = null): array
|
||||
{
|
||||
$request = $this->request;
|
||||
$configuration = $this->configuration;
|
||||
if ($request === null && ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) {
|
||||
// @todo: deprecate
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
}
|
||||
if ($request === null) {
|
||||
// This is a *specific* exception (as opposed to a global one) to allow consumers to opt-out
|
||||
// of a Request dependency. The extbase persistence layer is an example: It can be useful to
|
||||
// only have a loose Request / TypoScript dependency in it, and the TS config values / toggles
|
||||
// used within the persistence layer are not crucially important and can fall back to hard
|
||||
// coded defaults.
|
||||
// Note custom extensions should typically not catch this exception. The dependency to
|
||||
// the current request is still an important dependency in most extbase places, e.g. in
|
||||
// controller and view related code.
|
||||
throw new NoServerRequestGivenException('No request given. ConfigurationManager has not been initialized properly.', 1721920500);
|
||||
}
|
||||
if (ApplicationType::fromRequest($request)->isFrontend()) {
|
||||
if ($configurationType === self::CONFIGURATION_TYPE_FULL_TYPOSCRIPT) {
|
||||
return $this->feConfigManager->getTypoScriptSetup($request);
|
||||
}
|
||||
// @todo Throw if empty to not end up with '_': Invalid setup/call!
|
||||
$feConfigCacheKey = strtolower(
|
||||
($extensionName ?? $configuration['extensionName'] ?? null)
|
||||
. '_'
|
||||
. ($pluginName ?? $configuration['pluginName'] ?? null)
|
||||
);
|
||||
if ($configurationType === self::CONFIGURATION_TYPE_SETTINGS) {
|
||||
if (isset($this->feConfigCache[$feConfigCacheKey])) {
|
||||
return $this->feConfigCache[$feConfigCacheKey]['settings'] ?? [];
|
||||
}
|
||||
$this->feConfigCache[$feConfigCacheKey] = $this->feConfigManager->getConfiguration($request, $this->configuration, $extensionName, $pluginName);
|
||||
return $this->feConfigCache[$feConfigCacheKey]['settings'] ?? [];
|
||||
}
|
||||
if ($configurationType === self::CONFIGURATION_TYPE_FRAMEWORK) {
|
||||
if (isset($this->feConfigCache[$feConfigCacheKey])) {
|
||||
return $this->feConfigCache[$feConfigCacheKey];
|
||||
}
|
||||
$this->feConfigCache[$feConfigCacheKey] = $this->feConfigManager->getConfiguration($request, $this->configuration, $extensionName, $pluginName);
|
||||
return $this->feConfigCache[$feConfigCacheKey];
|
||||
}
|
||||
throw new \RuntimeException('Invalid configuration type "' . $configurationType . '"', 1206031879);
|
||||
} else {
|
||||
return match ($configurationType) {
|
||||
self::CONFIGURATION_TYPE_SETTINGS => $this->beConfigManager->getConfiguration($request, $this->configuration, $extensionName, $pluginName)['settings'] ?? [],
|
||||
self::CONFIGURATION_TYPE_FRAMEWORK => $this->beConfigManager->getConfiguration($request, $this->configuration, $extensionName, $pluginName),
|
||||
self::CONFIGURATION_TYPE_FULL_TYPOSCRIPT => $this->beConfigManager->getTypoScriptSetup($request),
|
||||
default => throw new \RuntimeException('Invalid configuration type "' . $configurationType . '"', 1721928055),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Configuration;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Extbase\Configuration\Exception\NoServerRequestGivenException;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
interface ConfigurationManagerInterface extends SingletonInterface
|
||||
{
|
||||
public const CONFIGURATION_TYPE_FRAMEWORK = 'Framework';
|
||||
public const CONFIGURATION_TYPE_SETTINGS = 'Settings';
|
||||
public const CONFIGURATION_TYPE_FULL_TYPOSCRIPT = 'FullTypoScript';
|
||||
|
||||
/**
|
||||
* Returns the specified configuration.
|
||||
* The actual configuration will be merged from different sources in a defined order.
|
||||
*
|
||||
* Note that this is a low level method and only makes sense to be used by Extbase internally.
|
||||
*
|
||||
* @param string $configurationType The kind of configuration to fetch - must be one of the CONFIGURATION_TYPE_* constants
|
||||
* @param string|null $extensionName if specified, the configuration for the given extension will be returned.
|
||||
* @param string|null $pluginName if specified, the configuration for the given plugin will be returned.
|
||||
* @return array The configuration
|
||||
* @throws NoServerRequestGivenException
|
||||
*/
|
||||
public function getConfiguration(string $configurationType, ?string $extensionName = null, ?string $pluginName = null): array;
|
||||
|
||||
/**
|
||||
* Sets the specified raw configuration coming from the outside.
|
||||
* Note that this is a low level method and only makes sense to be used by Extbase internally.
|
||||
*
|
||||
* @param array $configuration The new configuration
|
||||
* @internal Set by extbase bootstrap internally.
|
||||
* @todo: It may be possible to remove this in v13?!
|
||||
*/
|
||||
public function setConfiguration(array $configuration = []): void;
|
||||
|
||||
/**
|
||||
* Set the current request. The ConfigurationManager needs this to
|
||||
* determine which concrete ConfigurationManager (BE / FE) has to be
|
||||
* created, and the concrete ConfigurationManager need this to
|
||||
* access current site and similar.
|
||||
*
|
||||
* This state is updated by extbase bootstrap.
|
||||
*
|
||||
* Note this makes this singleton stateful! This is ugly, but can't
|
||||
* be avoided since the ConfigurationManager is injected into services
|
||||
* that are injected itself. This stateful singleton is of course an
|
||||
* anti-pattern, but it is very hard to get rid of until a re-design
|
||||
* of the extbase configuration logic.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @internal Set by extbase bootstrap internally.
|
||||
*/
|
||||
public function setRequest(ServerRequestInterface $request): void;
|
||||
}
|
||||
@@ -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\Extbase\Configuration;
|
||||
|
||||
use TYPO3\CMS\Extbase\Exception as ExtbaseException;
|
||||
|
||||
/**
|
||||
* A generic Configuration Exception
|
||||
*/
|
||||
class Exception extends ExtbaseException {}
|
||||
@@ -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\Extbase\Configuration\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Exception;
|
||||
|
||||
/**
|
||||
* Thrown when the ConfigurationManager has not been initialized with a PSR-7 ServerRequestInterface.
|
||||
*/
|
||||
final class NoServerRequestGivenException extends Exception {}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?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\Extbase\Configuration;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\TypoScript\FrontendTypoScript;
|
||||
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Event\Configuration\BeforeFlexFormConfigurationOverrideEvent;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* A general purpose configuration manager used in frontend mode.
|
||||
*
|
||||
* Should NOT be singleton, as a new configuration manager is needed per plugin.
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
final readonly class FrontendConfigurationManager
|
||||
{
|
||||
public function __construct(
|
||||
private TypoScriptService $typoScriptService,
|
||||
private FlexFormTools $flexFormTools,
|
||||
private PageRepository $pageRepository,
|
||||
private EventDispatcherInterface $eventDispatcher
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Loads the Extbase Framework configuration.
|
||||
*
|
||||
* The Extbase framework configuration HAS TO be retrieved using this method, as they are come from different places than the normal settings.
|
||||
* Framework configuration is, in contrast to normal settings, needed for the Extbase framework to operate correctly.
|
||||
*
|
||||
* @param array $configuration low level configuration from outside, typically ContentObjectRenderer TypoScript element config
|
||||
* @param string|null $extensionName if specified, the configuration for the given extension will be returned (plugin.tx_extensionname)
|
||||
* @param string|null $pluginName if specified, the configuration for the given plugin will be returned (plugin.tx_extensionname_pluginname)
|
||||
* @return array the Extbase framework configuration
|
||||
*/
|
||||
public function getConfiguration(ServerRequestInterface $request, array $configuration, ?string $extensionName = null, ?string $pluginName = null): array
|
||||
{
|
||||
$extensionNameFromConfig = $configuration['extensionName'] ?? null;
|
||||
$pluginNameFromConfig = $configuration['pluginName'] ?? null;
|
||||
$configuration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($configuration);
|
||||
|
||||
$frameworkConfiguration = $this->getExtbaseConfiguration($request);
|
||||
if (!isset($frameworkConfiguration['persistence']['storagePid'])) {
|
||||
$frameworkConfiguration['persistence']['storagePid'] = 0;
|
||||
}
|
||||
// only merge $configuration and override controller configuration when retrieving configuration of the current plugin
|
||||
if ($extensionName === null || $extensionName === $extensionNameFromConfig && $pluginName === $pluginNameFromConfig) {
|
||||
$pluginConfiguration = $this->getPluginConfiguration($request, (string)$extensionNameFromConfig, (string)$pluginNameFromConfig);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($pluginConfiguration, $configuration);
|
||||
$pluginConfiguration['controllerConfiguration'] = $this->getControllerConfiguration((string)$extensionNameFromConfig, (string)$pluginNameFromConfig);
|
||||
} else {
|
||||
$pluginConfiguration = $this->getPluginConfiguration($request, $extensionName, (string)$pluginName);
|
||||
$pluginConfiguration['controllerConfiguration'] = $this->getControllerConfiguration($extensionName, (string)$pluginName);
|
||||
}
|
||||
ArrayUtility::mergeRecursiveWithOverrule($frameworkConfiguration, $pluginConfiguration);
|
||||
// only load context specific configuration when retrieving configuration of the current plugin
|
||||
if ($extensionName === null || $extensionName === $extensionNameFromConfig && $pluginName === $pluginNameFromConfig) {
|
||||
$frameworkConfiguration = $this->getContextSpecificFrameworkConfiguration($request, $frameworkConfiguration);
|
||||
}
|
||||
|
||||
if (!empty($frameworkConfiguration['persistence']['storagePid'])) {
|
||||
if (is_array($frameworkConfiguration['persistence']['storagePid'])) {
|
||||
$conf = $this->typoScriptService->convertPlainArrayToTypoScriptArray($frameworkConfiguration['persistence']);
|
||||
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
||||
$contentObjectRenderer->setRequest($request);
|
||||
$contentObjectRenderer->start($request->getAttribute('frontend.page.information')->getPageRecord(), 'pages');
|
||||
$frameworkConfiguration['persistence']['storagePid'] = $contentObjectRenderer->stdWrapValue('storagePid', $conf);
|
||||
}
|
||||
if (!empty($frameworkConfiguration['persistence']['recursive'])) {
|
||||
$storagePids = $this->getRecursiveStoragePids(
|
||||
GeneralUtility::intExplode(',', (string)($frameworkConfiguration['persistence']['storagePid'] ?? '')),
|
||||
(int)$frameworkConfiguration['persistence']['recursive']
|
||||
);
|
||||
$frameworkConfiguration['persistence']['storagePid'] = implode(',', $storagePids);
|
||||
}
|
||||
}
|
||||
return $frameworkConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns full Frontend TypoScript setup array calculated by FE middlewares.
|
||||
*/
|
||||
public function getTypoScriptSetup(ServerRequestInterface $request): array
|
||||
{
|
||||
$frontendTypoScript = $request->getAttribute('frontend.typoscript');
|
||||
if (!($frontendTypoScript instanceof FrontendTypoScript)) {
|
||||
throw new \RuntimeException(
|
||||
'Setup array has not been initialized. This happens in cached Frontend scope where full TypoScript'
|
||||
. ' is not needed by the system.',
|
||||
1700841298
|
||||
);
|
||||
}
|
||||
return $frontendTypoScript->getSetupArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TypoScript configuration found in config.tx_extbase
|
||||
*/
|
||||
private function getExtbaseConfiguration(ServerRequestInterface $request): array
|
||||
{
|
||||
$setup = $this->getTypoScriptSetup($request);
|
||||
$extbaseConfiguration = [];
|
||||
if (isset($setup['config.']['tx_extbase.'])) {
|
||||
$extbaseConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($setup['config.']['tx_extbase.']);
|
||||
}
|
||||
return $extbaseConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TypoScript configuration found in plugin.tx_yourextension_yourplugin
|
||||
* merged with the global configuration of your extension from plugin.tx_yourextension
|
||||
*
|
||||
* @param string|null $pluginName in FE mode this is the specified plugin name
|
||||
*/
|
||||
private function getPluginConfiguration(ServerRequestInterface $request, string $extensionName, ?string $pluginName = null): array
|
||||
{
|
||||
$setup = $this->getTypoScriptSetup($request);
|
||||
$pluginConfiguration = [];
|
||||
if (isset($setup['plugin.']['tx_' . strtolower($extensionName) . '.']) && is_array($setup['plugin.']['tx_' . strtolower($extensionName) . '.'])) {
|
||||
$pluginConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($setup['plugin.']['tx_' . strtolower($extensionName) . '.']);
|
||||
}
|
||||
if ($pluginName !== null) {
|
||||
$pluginSignature = strtolower($extensionName . '_' . $pluginName);
|
||||
if (isset($setup['plugin.']['tx_' . $pluginSignature . '.']) && is_array($setup['plugin.']['tx_' . $pluginSignature . '.'])) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$pluginConfiguration,
|
||||
$this->typoScriptService->convertTypoScriptArrayToPlainArray($setup['plugin.']['tx_' . $pluginSignature . '.'])
|
||||
);
|
||||
}
|
||||
}
|
||||
return $pluginConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured controller/action configuration of the specified plugin in the format
|
||||
* array(
|
||||
* 'Controller1' => array('action1', 'action2'),
|
||||
* 'Controller2' => array('action3', 'action4')
|
||||
* )
|
||||
*
|
||||
* @param string $pluginName in FE mode this is the specified plugin name
|
||||
*/
|
||||
private function getControllerConfiguration(string $extensionName, string $pluginName): array
|
||||
{
|
||||
$controllerConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'] ?? [];
|
||||
if (!is_array($controllerConfiguration)) {
|
||||
$controllerConfiguration = [];
|
||||
}
|
||||
return $controllerConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context specific framework configuration.
|
||||
* - Overrides storage PID with setting "Startingpoint"
|
||||
* - merge flexForm configuration, if needed
|
||||
*
|
||||
* @param array $frameworkConfiguration The framework configuration to modify
|
||||
* @return array the modified framework configuration
|
||||
*/
|
||||
private function getContextSpecificFrameworkConfiguration(ServerRequestInterface $request, array $frameworkConfiguration): array
|
||||
{
|
||||
$frameworkConfiguration = $this->overrideStoragePidIfStartingPointIsSet($request, $frameworkConfiguration);
|
||||
$frameworkConfiguration = $this->overrideConfigurationFromPlugin($request, $frameworkConfiguration);
|
||||
return $this->overrideConfigurationFromFlexForm($request, $frameworkConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the storage PID settings, in case the "Startingpoint" settings
|
||||
* is set in the plugin configuration.
|
||||
*
|
||||
* @param array $frameworkConfiguration the framework configurations
|
||||
* @return array the framework configuration with overridden storagePid
|
||||
*/
|
||||
private function overrideStoragePidIfStartingPointIsSet(ServerRequestInterface $request, array $frameworkConfiguration): array
|
||||
{
|
||||
$contentObject = $request->getAttribute('currentContentObject');
|
||||
$pages = (string)($contentObject?->data['pages'] ?? '');
|
||||
if ($pages !== '') {
|
||||
$storagePids = GeneralUtility::intExplode(',', $pages, true);
|
||||
$recursionDepth = (int)($contentObject?->data['recursive'] ?? 0);
|
||||
$recursiveStoragePids = $this->pageRepository->getPageIdsRecursive($storagePids, $recursionDepth);
|
||||
$pages = implode(',', $recursiveStoragePids);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($frameworkConfiguration, [
|
||||
'persistence' => [
|
||||
'storagePid' => $pages,
|
||||
],
|
||||
]);
|
||||
}
|
||||
return $frameworkConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides configuration settings from the plugin typoscript (plugin.tx_myext_pi1.)
|
||||
*
|
||||
* @param array $frameworkConfiguration the framework configuration
|
||||
* @return array the framework configuration with overridden data from typoscript
|
||||
*/
|
||||
private function overrideConfigurationFromPlugin(ServerRequestInterface $request, array $frameworkConfiguration): array
|
||||
{
|
||||
if (!isset($frameworkConfiguration['extensionName']) || !isset($frameworkConfiguration['pluginName'])) {
|
||||
return $frameworkConfiguration;
|
||||
}
|
||||
|
||||
$setup = $this->getTypoScriptSetup($request);
|
||||
$pluginSignature = strtolower($frameworkConfiguration['extensionName'] . '_' . $frameworkConfiguration['pluginName']);
|
||||
$pluginConfiguration = $setup['plugin.']['tx_' . $pluginSignature . '.'] ?? null;
|
||||
if (is_array($pluginConfiguration)) {
|
||||
$pluginConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($pluginConfiguration);
|
||||
$frameworkConfiguration = $this->mergeConfigurationIntoFrameworkConfiguration($frameworkConfiguration, $pluginConfiguration, 'settings');
|
||||
$frameworkConfiguration = $this->mergeConfigurationIntoFrameworkConfiguration($frameworkConfiguration, $pluginConfiguration, 'persistence');
|
||||
$frameworkConfiguration = $this->mergeConfigurationIntoFrameworkConfiguration($frameworkConfiguration, $pluginConfiguration, 'view');
|
||||
}
|
||||
return $frameworkConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides configuration settings from flexForms. This merges the whole flexForm data.
|
||||
*
|
||||
* @param array $frameworkConfiguration the framework configuration
|
||||
* @return array the framework configuration with overridden data from flexForm
|
||||
*/
|
||||
private function overrideConfigurationFromFlexForm(ServerRequestInterface $request, array $frameworkConfiguration): array
|
||||
{
|
||||
$contentObject = $request->getAttribute('currentContentObject');
|
||||
$flexFormConfiguration = $contentObject?->data['pi_flexform'] ?? [];
|
||||
if (is_string($flexFormConfiguration)) {
|
||||
if ($flexFormConfiguration !== '') {
|
||||
$flexFormConfiguration = $this->flexFormTools->convertFlexFormContentToArray($flexFormConfiguration);
|
||||
} else {
|
||||
$flexFormConfiguration = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Early return, if flexForm configuration is empty
|
||||
if (!is_array($flexFormConfiguration) || empty($flexFormConfiguration)) {
|
||||
return $frameworkConfiguration;
|
||||
}
|
||||
|
||||
// Remove flexForm settings if empty for fields defined in `ignoreFlexFormSettingsIfEmpty`
|
||||
$originalFlexFormConfiguration = $flexFormConfiguration;
|
||||
$ignoredSettingsConfig = (string)($frameworkConfiguration['ignoreFlexFormSettingsIfEmpty'] ?? '');
|
||||
if ($ignoredSettingsConfig !== '') {
|
||||
$ignoredSettings = GeneralUtility::trimExplode(',', $ignoredSettingsConfig, true);
|
||||
$flexFormConfiguration = $this->removeIgnoredFlexFormSettingsIfEmpty($flexFormConfiguration, $ignoredSettings);
|
||||
}
|
||||
|
||||
// PSR-14 event for extension authors to modify flexForm configuration before the merge process
|
||||
$event = new BeforeFlexFormConfigurationOverrideEvent($frameworkConfiguration, $originalFlexFormConfiguration, $flexFormConfiguration);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
$flexFormConfiguration = $event->getFlexFormConfiguration();
|
||||
|
||||
$frameworkConfiguration = $this->mergeConfigurationIntoFrameworkConfiguration($frameworkConfiguration, $flexFormConfiguration, 'settings');
|
||||
$frameworkConfiguration = $this->mergeConfigurationIntoFrameworkConfiguration($frameworkConfiguration, $flexFormConfiguration, 'persistence');
|
||||
return $this->mergeConfigurationIntoFrameworkConfiguration($frameworkConfiguration, $flexFormConfiguration, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a configuration into the framework configuration.
|
||||
*
|
||||
* @param array $frameworkConfiguration the framework configuration to merge the data on
|
||||
* @param array $configuration The configuration
|
||||
* @param string $configurationPartName The name of the configuration part which should be merged.
|
||||
* @return array the processed framework configuration
|
||||
*/
|
||||
private function mergeConfigurationIntoFrameworkConfiguration(array $frameworkConfiguration, array $configuration, string $configurationPartName): array
|
||||
{
|
||||
if (isset($configuration[$configurationPartName]) && is_array($configuration[$configurationPartName])) {
|
||||
if (isset($frameworkConfiguration[$configurationPartName]) && is_array($frameworkConfiguration[$configurationPartName])) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($frameworkConfiguration[$configurationPartName], $configuration[$configurationPartName]);
|
||||
} else {
|
||||
$frameworkConfiguration[$configurationPartName] = $configuration[$configurationPartName];
|
||||
}
|
||||
}
|
||||
return $frameworkConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a comma separated list of storagePid that are below a certain storage pid.
|
||||
*
|
||||
* @param array|int[] $storagePids Storage PIDs to start at; multiple PIDs possible as comma-separated list
|
||||
* @param int $recursionDepth Maximum number of levels to search, 0 to disable recursive lookup
|
||||
* @return int[] storage PIDs
|
||||
*/
|
||||
private function getRecursiveStoragePids(array $storagePids, int $recursionDepth = 0): array
|
||||
{
|
||||
return $this->pageRepository->getPageIdsRecursive($storagePids, $recursionDepth);
|
||||
}
|
||||
|
||||
private function removeIgnoredFlexFormSettingsIfEmpty(array $flexFormConfiguration, array $ignoredSettings): array
|
||||
{
|
||||
foreach ($ignoredSettings as $ignoredSetting) {
|
||||
$ignoredSettingName = 'settings.' . $ignoredSetting;
|
||||
if (!ArrayUtility::isValidPath($flexFormConfiguration, $ignoredSettingName, '.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldValue = ArrayUtility::getValueByPath($flexFormConfiguration, $ignoredSettingName, '.');
|
||||
if ($fieldValue === '' || $fieldValue === '0') {
|
||||
$flexFormConfiguration = ArrayUtility::removeByPath($flexFormConfiguration, $ignoredSettingName, '.');
|
||||
}
|
||||
}
|
||||
|
||||
return $flexFormConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\ConfigurationModuleProvider;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\ClassesConfiguration;
|
||||
use TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\AbstractProvider;
|
||||
|
||||
final class ClassConfigurationProvider extends AbstractProvider
|
||||
{
|
||||
public function __construct(private ClassesConfiguration $classesConfiguration) {}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->classesConfiguration->getConfiguration();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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\Extbase\ContentObject;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
use TYPO3\CMS\Extbase\Core\Bootstrap;
|
||||
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* Contains EXTBASEPLUGIN class object.
|
||||
*
|
||||
* Creates a request and dispatches it to the controller which was specified
|
||||
* by TS Setup and returns the content, currently handed over to the
|
||||
* Extbase Bootstrap.
|
||||
*
|
||||
* This class is the main entry point for extbase extensions in the TYPO3 Frontend.
|
||||
*/
|
||||
class ExtbasePluginContentObject extends AbstractContentObject
|
||||
{
|
||||
public function render($conf = [])
|
||||
{
|
||||
$extbaseBootstrap = GeneralUtility::makeInstance(Bootstrap::class);
|
||||
$extbaseBootstrap->setContentObjectRenderer($this->getContentObjectRenderer());
|
||||
if ($this->cObj->getUserObjectType() === false) {
|
||||
// Come here only if we are not called as non-cached element
|
||||
$this->cObj->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER);
|
||||
}
|
||||
$request = $extbaseBootstrap->initialize($conf, $this->request);
|
||||
$content = $extbaseBootstrap->handleFrontendRequest($request);
|
||||
// Rendering is deferred, as the action should not be cached. Register as non cached element.
|
||||
if ($this->cObj->doConvertToUserIntObject) {
|
||||
$this->cObj->doConvertToUserIntObject = false;
|
||||
// @todo: this should be removed in the future when FE chains allows more "uncacheables" than USER_INTs
|
||||
// also, the handleFrontendRequest() should return the full response in the future
|
||||
$conf['userFunc'] = Bootstrap::class . '->run';
|
||||
$this->cObj->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER_INT);
|
||||
$pageParts = $request->getAttribute('frontend.page.parts');
|
||||
$substKey = 'INT_SCRIPT.' . md5(StringUtility::getUniqueId());
|
||||
$content = '<!--' . $substKey . '-->';
|
||||
$pageParts->addNotCachedContentElement([
|
||||
'substKey' => $substKey,
|
||||
'conf' => $conf,
|
||||
'cObjData' => serialize($this->cObj->getState()),
|
||||
'type' => 'FUNC',
|
||||
]);
|
||||
} elseif (isset($conf['stdWrap.'])) {
|
||||
// Only executed when the element is not converted to USER_INT
|
||||
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
|
||||
}
|
||||
$this->cObj->setUserObjectType(false);
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?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\Extbase\Core;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Dispatcher;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder;
|
||||
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Service\CacheService;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
use TYPO3\CMS\Frontend\Response\ResponseData;
|
||||
|
||||
/**
|
||||
* Creates a request and dispatches it to the controller which was specified
|
||||
* by TS Setup and returns the content.
|
||||
*
|
||||
* This class is the main entry point for extbase extensions.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[Autoconfigure(public: true, shared: false)]
|
||||
class Bootstrap
|
||||
{
|
||||
protected ?ContentObjectRenderer $cObj = null;
|
||||
|
||||
public function __construct(
|
||||
protected readonly ContainerInterface $container,
|
||||
protected readonly ConfigurationManagerInterface $configurationManager,
|
||||
protected readonly PersistenceManagerInterface $persistenceManager,
|
||||
protected readonly CacheService $cacheService,
|
||||
protected readonly Dispatcher $dispatcher,
|
||||
protected readonly RequestBuilder $extbaseRequestBuilder
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The current (!) cObj.
|
||||
* Called for frontend plugins from UserContentObject via ContentObjectRenderer->callUserFunction().
|
||||
*/
|
||||
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
|
||||
{
|
||||
$this->cObj = $cObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly initializes all necessary Extbase objects by invoking the various initialize* methods.
|
||||
*
|
||||
* Usually this method is only called from unit tests or other applications which need a more fine-grained control over
|
||||
* the initialization and request handling process. Most other applications just call the run() method.
|
||||
*
|
||||
* @param array $configuration The TS configuration array
|
||||
* @throws \RuntimeException
|
||||
* @see run()
|
||||
*/
|
||||
public function initialize(array $configuration, ServerRequestInterface $request): ServerRequestInterface
|
||||
{
|
||||
if (!Environment::isCli()) {
|
||||
if (!isset($configuration['extensionName']) || $configuration['extensionName'] === '') {
|
||||
throw new \RuntimeException('Invalid configuration: "extensionName" is not set', 1290623020);
|
||||
}
|
||||
if (!isset($configuration['pluginName']) || $configuration['pluginName'] === '') {
|
||||
throw new \RuntimeException('Invalid configuration: "pluginName" is not set', 1290623027);
|
||||
}
|
||||
}
|
||||
return $this->initializeConfiguration($configuration, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Object framework.
|
||||
*
|
||||
* @see initialize()
|
||||
* @internal
|
||||
*/
|
||||
public function initializeConfiguration(array $configuration, ServerRequestInterface $request): ServerRequestInterface
|
||||
{
|
||||
if ($this->cObj === null) {
|
||||
// @todo: While the frontend sets the current cObj, a backend extbase request does not.
|
||||
// It is currently not clear if the backend should have a dummy cObj as well.
|
||||
// For now, extbase initializes one.
|
||||
$this->cObj = $this->container->get(ContentObjectRenderer::class);
|
||||
$this->cObj->setRequest($request);
|
||||
}
|
||||
$this->configurationManager->setRequest($request);
|
||||
$this->configurationManager->setConfiguration($configuration);
|
||||
return $request;
|
||||
// todo: Outdated todo, recheck in v13.
|
||||
// Shouldn't the configuration manager object – which is a singleton – be stateless?
|
||||
// At this point we give the configuration manager a state, while we could directly pass the
|
||||
// configuration (i.e. controllerName, actionName and such), directly to the request
|
||||
// handler, which then creates stateful request objects.
|
||||
// Once this has changed, \TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder::loadDefaultValues does not need
|
||||
// to fetch this configuration from the configuration manager.
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Extbase Framework by resolving an appropriate Request Handler and passing control to it.
|
||||
* If the Framework is not initialized yet, it will be initialized.
|
||||
*
|
||||
* This is usually used in Frontend plugins.
|
||||
* This method will be marked as internal in the future, use EXTBASEPLUGIN in TypoScript to execute an Extbase plugin
|
||||
* instead.
|
||||
*
|
||||
* @param string $content The content. Not used
|
||||
* @param array $configuration The TS configuration array
|
||||
* @param ServerRequestInterface $request the incoming server request
|
||||
* @return string $content The processed content
|
||||
*/
|
||||
#[AsAllowedCallable]
|
||||
public function run(string $content, array $configuration, ServerRequestInterface $request): string
|
||||
{
|
||||
$request = $this->initialize($configuration, $request);
|
||||
return $this->handleFrontendRequest($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for any Extbase Plugin in the Frontend, be sure to run $this->initialize() before.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function handleFrontendRequest(ServerRequestInterface $request): string
|
||||
{
|
||||
$extbaseRequest = $this->extbaseRequestBuilder->build($request);
|
||||
if (!$this->isExtbaseRequestCacheable($extbaseRequest)) {
|
||||
if ($this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) {
|
||||
// ContentObjectRenderer::convertToUserIntObject() will recreate the object,
|
||||
// so we have to stop the request here before the action is actually called
|
||||
$this->cObj->convertToUserIntObject();
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the extbase request
|
||||
$response = $this->dispatcher->dispatch($extbaseRequest);
|
||||
if ($response->getStatusCode() >= 300) {
|
||||
// Avoid caching the plugin when we issue a redirect or error response
|
||||
// This means that even when an action is configured as cachable
|
||||
// we avoid the plugin to be cached, but keep the page cache untouched
|
||||
if ($this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) {
|
||||
$this->cObj->convertToUserIntObject();
|
||||
}
|
||||
}
|
||||
// Usually coming from an error action, ensure all caches are cleared
|
||||
if ($response->getStatusCode() === 400) {
|
||||
$this->clearCacheOnError($request);
|
||||
}
|
||||
|
||||
if ($response->hasHeader('Content-Type')) {
|
||||
// Typically used when extbase for instance created a json response.
|
||||
$request->getAttribute('frontend.page.parts')->setHttpContentType($response->getHeaderLine('Content-Type'));
|
||||
// Do not send the header directly (see below)
|
||||
$response = $response->withoutHeader('Content-Type');
|
||||
}
|
||||
|
||||
$responseData = $request->getAttribute('frontend.response.data');
|
||||
if ($responseData instanceof ResponseData) {
|
||||
foreach ($response->getHeaders() as $name => $values) {
|
||||
$responseData->setHeader($name, $values);
|
||||
}
|
||||
// @todo: Get rid of this in TYPO3 v15. See todos in ResponseData.
|
||||
if ($response->getStatusCode() >= 300) {
|
||||
$responseData->setProtocolVersion($response->getProtocolVersion());
|
||||
$responseData->setStatusCode($response->getStatusCode());
|
||||
$responseData->setReasonPhrase($response->getReasonPhrase());
|
||||
}
|
||||
}
|
||||
|
||||
$body = $response->getBody();
|
||||
$body->rewind();
|
||||
$content = $body->getContents();
|
||||
$this->resetSingletons();
|
||||
$this->cacheService->clearCachesOfRegisteredPageIds();
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint for backend modules, handling PSR-7 requests/responses.
|
||||
*
|
||||
* Creates an Extbase Request, dispatches it and then returns the Response
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function handleBackendRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// build the configuration from the module, included in the current request
|
||||
$module = $request->getAttribute('module');
|
||||
$configuration = [
|
||||
'extensionName' => $module?->getExtensionName(),
|
||||
'pluginName' => $module?->getIdentifier(),
|
||||
];
|
||||
|
||||
$request = $this->initialize($configuration, $request);
|
||||
$extbaseRequest = $this->extbaseRequestBuilder->build($request);
|
||||
$response = $this->dispatcher->dispatch($extbaseRequest);
|
||||
$this->resetSingletons();
|
||||
$this->cacheService->clearCachesOfRegisteredPageIds();
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache of current page on error. Needed because we want a re-evaluation of the data.
|
||||
*/
|
||||
protected function clearCacheOnError(ServerRequestInterface $request): void
|
||||
{
|
||||
$extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
|
||||
$pageId = $request->getAttribute('frontend.page.information')?->getId();
|
||||
if ($pageId !== null) {
|
||||
$this->cacheService->clearPageCache([$pageId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets global singletons for the next plugin
|
||||
*/
|
||||
protected function resetSingletons(): void
|
||||
{
|
||||
$this->persistenceManager->persistAll();
|
||||
}
|
||||
|
||||
protected function isExtbaseRequestCacheable(RequestInterface $extbaseRequest): bool
|
||||
{
|
||||
$controllerClassName = $extbaseRequest->getControllerObjectName();
|
||||
$actionName = $extbaseRequest->getControllerActionName();
|
||||
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
$nonCacheableActions = $frameworkConfiguration['controllerConfiguration'][$controllerClassName]['nonCacheableActions'] ?? null;
|
||||
if (!is_array($nonCacheableActions)) {
|
||||
return true;
|
||||
}
|
||||
return !in_array($actionName, $nonCacheableActions, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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\Extbase\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use TYPO3\CMS\Extbase\Attribute\Authorize;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\AuthorizeRegistry;
|
||||
|
||||
/**
|
||||
* Scans all extbase action controllers for #[Authorize] attributes
|
||||
* and registers their configurations in the {@see AuthorizeRegistry}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class AuthorizePass implements CompilerPassInterface
|
||||
{
|
||||
public function __construct(private string $tagName) {}
|
||||
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->hasDefinition(AuthorizeRegistry::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$registryDefinition = $container->findDefinition(AuthorizeRegistry::class);
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) {
|
||||
$definition = $container->findDefinition($serviceName);
|
||||
if ($definition->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = $definition->getClass() ?? $serviceName;
|
||||
$reflectionClass = $container->getReflectionClass($className);
|
||||
if ($reflectionClass === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
if (!str_ends_with($method->getName(), 'Action')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes = $method->getAttributes(Authorize::class);
|
||||
if ($attributes === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$authorize = $attribute->newInstance();
|
||||
$registryDefinition->addMethodCall('add', [
|
||||
$className,
|
||||
$method->getName(),
|
||||
$authorize->callback,
|
||||
$authorize->requireLogin,
|
||||
$authorize->requireGroups,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use TYPO3\CMS\Extbase\Attribute\RateLimit;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\RateLimitRegistry;
|
||||
|
||||
/**
|
||||
* Scans all extbase action controllers for #[RateLimit] attributes
|
||||
* and registers their configurations in the {@see RateLimitRegistry}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class RateLimitPass implements CompilerPassInterface
|
||||
{
|
||||
public function __construct(private string $tagName) {}
|
||||
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->hasDefinition(RateLimitRegistry::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$registryDefinition = $container->findDefinition(RateLimitRegistry::class);
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) {
|
||||
$definition = $container->findDefinition($serviceName);
|
||||
if ($definition->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = $definition->getClass() ?? $serviceName;
|
||||
$reflectionClass = $container->getReflectionClass($className);
|
||||
if ($reflectionClass === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
if (!str_ends_with($method->getName(), 'Action')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes = $method->getAttributes(RateLimit::class);
|
||||
if ($attributes === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rateLimit = $attributes[0]->newInstance();
|
||||
$registryDefinition->addMethodCall('add', [
|
||||
$className,
|
||||
$method->getName(),
|
||||
$rateLimit->limit,
|
||||
$rateLimit->interval,
|
||||
$rateLimit->policy,
|
||||
$rateLimit->message,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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\Extbase\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Property\Exception\InvalidTypeConverterConfigurationException;
|
||||
use TYPO3\CMS\Extbase\Property\TypeConverterRegistry;
|
||||
|
||||
/**
|
||||
* Find all TypeConverters for Extbase's Property Mapper.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class TypeConverterPass implements CompilerPassInterface
|
||||
{
|
||||
private string $tagName;
|
||||
|
||||
public function __construct(string $tagName)
|
||||
{
|
||||
$this->tagName = $tagName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidTypeConverterConfigurationException
|
||||
*/
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$typeConverterRegistryDefinition = $container->findDefinition(TypeConverterRegistry::class);
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) {
|
||||
$definition = $container->findDefinition($serviceName);
|
||||
if (!$definition->isAutoconfigured() || $definition->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$definition->setPublic(true);
|
||||
|
||||
foreach ($tags as $attributes) {
|
||||
if (!isset($attributes['sources'])) {
|
||||
throw new InvalidTypeConverterConfigurationException(
|
||||
sprintf(
|
||||
'Configuration for TypeConverter "%s" misses the "sources" attribute.',
|
||||
$serviceName
|
||||
),
|
||||
1638376684
|
||||
);
|
||||
}
|
||||
|
||||
$sources = GeneralUtility::trimExplode(',', (string)$attributes['sources'], true);
|
||||
|
||||
if ($sources === []) {
|
||||
throw new InvalidTypeConverterConfigurationException(
|
||||
sprintf(
|
||||
'The sources attribute of the configuration of TypeConverter "%s" contains an empty list.',
|
||||
$serviceName
|
||||
),
|
||||
1638376687
|
||||
);
|
||||
}
|
||||
|
||||
if (!($attributes['target'] ?? false)) {
|
||||
throw new InvalidTypeConverterConfigurationException(
|
||||
sprintf(
|
||||
'Configuration for TypeConverter "%s" misses a valid "target" attribute.',
|
||||
$serviceName
|
||||
),
|
||||
1638376689
|
||||
);
|
||||
}
|
||||
|
||||
$typeConverterRegistryDefinition->addMethodCall('add', [
|
||||
$definition,
|
||||
(int)($attributes['priority'] ?? 10),
|
||||
$sources,
|
||||
$attributes['target'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\Extbase\Domain\Model;
|
||||
|
||||
use TYPO3\CMS\Extbase\Attribute as Extbase;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
|
||||
|
||||
/**
|
||||
* This model represents a category (for anything).
|
||||
*/
|
||||
class Category extends AbstractEntity
|
||||
{
|
||||
#[Extbase\Validate(validator: 'NotEmpty')]
|
||||
protected string $title = '';
|
||||
|
||||
protected string $description = '';
|
||||
|
||||
#[Extbase\ORM\Lazy]
|
||||
protected Category|LazyLoadingProxy|null $parent = null;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): void
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): void
|
||||
{
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
public function getParent(): ?Category
|
||||
{
|
||||
if ($this->parent instanceof LazyLoadingProxy) {
|
||||
$this->parent->_loadRealInstance();
|
||||
}
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function setParent(Category $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Domain\Model;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
|
||||
|
||||
/**
|
||||
* A file object (File Abstraction Layer)
|
||||
*/
|
||||
class File extends AbstractEntity
|
||||
{
|
||||
private ?\TYPO3\CMS\Core\Resource\File $originalResource = null;
|
||||
|
||||
public function getOriginalResource(): \TYPO3\CMS\Core\Resource\File
|
||||
{
|
||||
if ($this->originalResource === null) {
|
||||
$this->originalResource = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($this->getUid());
|
||||
}
|
||||
|
||||
return $this->originalResource;
|
||||
}
|
||||
|
||||
public function setOriginalResource(\TYPO3\CMS\Core\Resource\File $originalResource): void
|
||||
{
|
||||
$this->originalResource = $originalResource;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Domain\Model;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
|
||||
|
||||
/**
|
||||
* A file reference object (File Abstraction Layer)
|
||||
*/
|
||||
class FileReference extends AbstractEntity
|
||||
{
|
||||
/**
|
||||
* Uid of the referenced sys_file. Needed for extbase to serialize the
|
||||
* reference correctly.
|
||||
*/
|
||||
protected ?int $uidLocal = null;
|
||||
|
||||
protected ?\TYPO3\CMS\Core\Resource\FileReference $originalResource = null;
|
||||
|
||||
public function setOriginalResource(\TYPO3\CMS\Core\Resource\FileReference $originalResource): void
|
||||
{
|
||||
$this->originalResource = $originalResource;
|
||||
$this->uidLocal = $originalResource->getOriginalFile()->getUid();
|
||||
}
|
||||
|
||||
public function getOriginalResource(): \TYPO3\CMS\Core\Resource\FileReference
|
||||
{
|
||||
if ($this->originalResource === null) {
|
||||
$uid = $this->_localizedUid;
|
||||
$this->originalResource = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject($uid);
|
||||
}
|
||||
|
||||
return $this->originalResource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\Extbase\Domain\Model;
|
||||
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
|
||||
|
||||
/**
|
||||
* A folder object (File Abstraction Layer)
|
||||
*/
|
||||
class Folder extends AbstractEntity
|
||||
{
|
||||
private ?\TYPO3\CMS\Core\Resource\Folder $originalResource = null;
|
||||
|
||||
public function setOriginalResource(\TYPO3\CMS\Core\Resource\Folder $originalResource): void
|
||||
{
|
||||
$this->originalResource = $originalResource;
|
||||
}
|
||||
|
||||
public function getOriginalResource(): ?\TYPO3\CMS\Core\Resource\Folder
|
||||
{
|
||||
return $this->originalResource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
<?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\Extbase\DomainObject;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\TooDirtyException;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectMonitoringInterface;
|
||||
|
||||
/**
|
||||
* A generic Domain Object.
|
||||
*
|
||||
* All Model domain objects need to inherit from either `AbstractEntity` or `AbstractValueObject` (instead of from this
|
||||
* class), as this provides important framework information.
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
abstract class AbstractDomainObject implements DomainObjectInterface
|
||||
{
|
||||
public const PROPERTY_UID = 'uid';
|
||||
public const PROPERTY_PID = 'pid';
|
||||
public const PROPERTY_LOCALIZED_UID = '_localizedUid';
|
||||
public const PROPERTY_LANGUAGE_UID = '_languageUid';
|
||||
public const PROPERTY_VERSIONED_UID = '_versionedUid';
|
||||
|
||||
/**
|
||||
* @var int<1, max>|null The uid of the record. The uid is only unique in the context of the database table.
|
||||
*/
|
||||
protected ?int $uid = null;
|
||||
|
||||
/**
|
||||
* @var int<0, max>|null The uid of the localized record. Holds the uid of the record in default language (the translationOrigin).
|
||||
*
|
||||
* @internal
|
||||
* @todo make private in 13.0 and expose value via getter
|
||||
*/
|
||||
protected ?int $_localizedUid = null;
|
||||
|
||||
/**
|
||||
* @var int<-1, max>|null The uid of the language of the object. This is the id of the corresponding sing language.
|
||||
*
|
||||
* @internal
|
||||
* @todo make private in 13.0 and expose value via getter
|
||||
*/
|
||||
protected ?int $_languageUid = null;
|
||||
|
||||
/**
|
||||
* The uid of the versioned record.
|
||||
*
|
||||
* @internal
|
||||
* @todo make private in 13.0 and expose value via getter
|
||||
*/
|
||||
protected ?int $_versionedUid = null;
|
||||
|
||||
/**
|
||||
* @var int<0, max>|null The id of the page the record is "stored".
|
||||
*/
|
||||
protected ?int $pid = null;
|
||||
|
||||
/**
|
||||
* TRUE if the object is a clone
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
private bool $_isClone = false;
|
||||
|
||||
/**
|
||||
* @var array<non-empty-string, mixed>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
private array $_cleanProperties = [];
|
||||
|
||||
/**
|
||||
* @return int<1, max>|null
|
||||
*/
|
||||
public function getUid(): ?int
|
||||
{
|
||||
if ($this->uid !== null) {
|
||||
return (int)$this->uid;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int<0, max> $pid
|
||||
*/
|
||||
public function setPid(int $pid): void
|
||||
{
|
||||
$this->pid = $pid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>|null
|
||||
*/
|
||||
public function getPid(): ?int
|
||||
{
|
||||
if ($this->pid === null) {
|
||||
return null;
|
||||
}
|
||||
return (int)$this->pid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function _setProperty(string $propertyName, mixed $value): bool
|
||||
{
|
||||
if ($this->_hasProperty($propertyName)) {
|
||||
$this->{$propertyName} = $value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function _getProperty(string $propertyName): mixed
|
||||
{
|
||||
return $this->_hasProperty($propertyName) && isset($this->{$propertyName})
|
||||
? $this->{$propertyName}
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<non-empty-string, mixed> a hash map of property names and property values.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _getProperties(): array
|
||||
{
|
||||
$properties = get_object_vars($this);
|
||||
foreach ($properties as $propertyName => $propertyValue) {
|
||||
if (str_starts_with($propertyName, '_')) {
|
||||
unset($properties[$propertyName]);
|
||||
}
|
||||
}
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $propertyName
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _hasProperty(string $propertyName): bool
|
||||
{
|
||||
return property_exists($this, $propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the object is new (the uid was not set, yet)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _isNew(): bool
|
||||
{
|
||||
return $this->uid === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an object's clean state, e.g. after it has been reconstituted
|
||||
* from the database.
|
||||
*
|
||||
* @param non-empty-string|null $propertyName The name of the property to be memorized. If omitted all persistable properties are memorized.
|
||||
*/
|
||||
public function _memorizeCleanState(?string $propertyName = null): void
|
||||
{
|
||||
if ($propertyName !== null) {
|
||||
$this->_memorizePropertyCleanState($propertyName);
|
||||
} else {
|
||||
$this->_cleanProperties = [];
|
||||
foreach ($this->_getProperties() as $propertyName => $propertyValue) {
|
||||
$this->_memorizePropertyCleanState($propertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a property's clean state, e.g. after it has been reconstituted
|
||||
* from the database.
|
||||
*
|
||||
* @param non-empty-string $propertyName The name of the property to be memorized. If omitted all persistable properties are memorized.
|
||||
*/
|
||||
public function _memorizePropertyCleanState(string $propertyName): void
|
||||
{
|
||||
$propertyValue = $this->_getProperty($propertyName);
|
||||
if (is_object($propertyValue) && !($propertyValue instanceof \UnitEnum)) {
|
||||
$propertyValueClone = clone $propertyValue;
|
||||
// We need to make sure the clone and the original object
|
||||
// are identical when compared with == (see _isDirty()).
|
||||
// After the cloning, the Domain Object will have the property
|
||||
// "isClone" set to TRUE, so we manually have to set it to FALSE
|
||||
// again. Possible fix: Somehow get rid of the "isClone" property,
|
||||
// which is currently needed in Fluid.
|
||||
if ($propertyValueClone instanceof AbstractDomainObject) {
|
||||
$propertyValueClone->_setClone(false);
|
||||
}
|
||||
|
||||
$this->_cleanProperties[$propertyName] = $propertyValueClone;
|
||||
} else {
|
||||
$this->_cleanProperties[$propertyName] = $propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash map of clean properties and $values.
|
||||
*
|
||||
* @return array<non-empty-string, mixed>
|
||||
*/
|
||||
public function _getCleanProperties(): array
|
||||
{
|
||||
return $this->_cleanProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the clean value of the given property. The returned value will be NULL if the clean state was not memorized before, or
|
||||
* if the clean value is NULL.
|
||||
*
|
||||
* @param non-empty-string $propertyName The name of the property to be memorized.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _getCleanProperty(string $propertyName): mixed
|
||||
{
|
||||
return $this->_cleanProperties[$propertyName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the properties were modified after reconstitution
|
||||
*
|
||||
* @param non-empty-string|null $propertyName An optional name of a property to be checked if its value is dirty
|
||||
*
|
||||
* @throws TooDirtyException
|
||||
*/
|
||||
public function _isDirty(?string $propertyName = null): bool
|
||||
{
|
||||
if ($this->uid !== null && $this->_getCleanProperty(self::PROPERTY_UID) !== null && $this->uid != $this->_getCleanProperty(self::PROPERTY_UID)) {
|
||||
throw new TooDirtyException('The ' . self::PROPERTY_UID . ' "' . $this->uid . '" has been modified, that is simply too much.', 1222871239);
|
||||
}
|
||||
|
||||
if ($propertyName === null) {
|
||||
foreach ($this->_getCleanProperties() as $propertyName => $cleanPropertyValue) {
|
||||
if ($this->isPropertyDirty($cleanPropertyValue, $this->_getProperty($propertyName)) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isPropertyDirty($this->_getCleanProperty($propertyName), $this->_getProperty($propertyName)) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the $value against the $cleanState.
|
||||
*/
|
||||
protected function isPropertyDirty(mixed $previousValue, mixed $currentValue): bool
|
||||
{
|
||||
// In case it is an object and it implements the ObjectMonitoringInterface, we call _isDirty() instead of a simple comparison of objects.
|
||||
// We do this, because if the object itself contains a lazy loaded property, the comparison of the objects might fail even if the object didn't change
|
||||
if (is_object($currentValue)) {
|
||||
$currentTypeString = null;
|
||||
if ($currentValue instanceof LazyLoadingProxy) {
|
||||
$currentTypeString = $currentValue->_getTypeAndUidString();
|
||||
} elseif ($currentValue instanceof DomainObjectInterface) {
|
||||
$currentTypeString = $currentValue::class . ':' . $currentValue->getUid();
|
||||
}
|
||||
|
||||
if ($currentTypeString !== null) {
|
||||
$previousTypeString = null;
|
||||
if ($previousValue instanceof LazyLoadingProxy) {
|
||||
$previousTypeString = $previousValue->_getTypeAndUidString();
|
||||
} elseif ($previousValue instanceof DomainObjectInterface) {
|
||||
$previousTypeString = $previousValue::class . ':' . $previousValue->getUid();
|
||||
}
|
||||
|
||||
$result = $currentTypeString !== $previousTypeString;
|
||||
} elseif ($currentValue instanceof ObjectMonitoringInterface) {
|
||||
$result = !is_object($previousValue) || $currentValue->_isDirty() || $previousValue::class !== $currentValue::class;
|
||||
} else {
|
||||
// For all other objects we do only a simple comparison (!=) as we want cloned objects to return the same values.
|
||||
$result = $previousValue != $currentValue;
|
||||
}
|
||||
} else {
|
||||
$result = $previousValue !== $currentValue;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function _isClone(): bool
|
||||
{
|
||||
return $this->_isClone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter whether this Domain Object is a clone of another one.
|
||||
* NEVER SET THIS PROPERTY DIRECTLY. We currently need it to make the
|
||||
* _isDirty check inside AbstractEntity work, but it is just a work-
|
||||
* around right now.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _setClone(bool $clone)
|
||||
{
|
||||
$this->_isClone = $clone;
|
||||
}
|
||||
|
||||
public function __clone(): void
|
||||
{
|
||||
$this->_isClone = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return static::class . ':' . $this->uid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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\Extbase\DomainObject;
|
||||
|
||||
/**
|
||||
* An abstract Entity. An Entity is an object fundamentally defined not by its attributes,
|
||||
* but by a thread of continuity and identity (e.g. a person).
|
||||
*/
|
||||
abstract class AbstractEntity extends AbstractDomainObject {}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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\Extbase\DomainObject;
|
||||
|
||||
/**
|
||||
* An abstract Value Object. A Value Object is an object that describes some characteristic
|
||||
* or attribute (e.g. a color) but carries no concept of identity.
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
abstract class AbstractValueObject extends AbstractDomainObject
|
||||
{
|
||||
/**
|
||||
* Returns the value of the Value Object. Must be overwritten by a concrete value object.
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->__toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?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\Extbase\DomainObject;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectMonitoringInterface;
|
||||
|
||||
/**
|
||||
* A Domain Object Interface. All domain objects which should be persisted need to implement this interface.
|
||||
*
|
||||
* Usually you will need to subclass `AbstractEntity` or `AbstractValueObject` instead of implementing this interface
|
||||
* directly, though.
|
||||
*
|
||||
* @see \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
|
||||
* @see \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject
|
||||
*/
|
||||
interface DomainObjectInterface extends ObjectMonitoringInterface
|
||||
{
|
||||
public function getUid(): ?int;
|
||||
|
||||
public function setPid(int $pid): void;
|
||||
|
||||
public function getPid(): ?int;
|
||||
|
||||
/**
|
||||
* Returns TRUE if the object is new (the uid was not set, yet).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _isNew(): bool;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $propertyName
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _hasProperty(string $propertyName): bool;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $propertyName
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _setProperty(string $propertyName, mixed $value);
|
||||
|
||||
/**
|
||||
* @param non-empty-string $propertyName
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _getProperty(string $propertyName): mixed;
|
||||
|
||||
/**
|
||||
* @return array<non-empty-string, mixed>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _getProperties(): array;
|
||||
|
||||
/**
|
||||
* Returns the clean value of the given property. The returned value will be NULL if the clean state was not memorized before, or
|
||||
* if the clean value is NULL.
|
||||
*
|
||||
* @param non-empty-string $propertyName
|
||||
* @return mixed The clean property value or NULL
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _getCleanProperty(string $propertyName): mixed;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\Extbase\Error;
|
||||
|
||||
/**
|
||||
* An object representation of a generic error. Subclass this to create
|
||||
* more specific errors if necessary.
|
||||
*/
|
||||
class Error extends Message
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Unknown error';
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?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\Extbase\Error;
|
||||
|
||||
/**
|
||||
* An object representation of a generic message. Usually, you will use Error, Warning or Notice instead of this one.
|
||||
*/
|
||||
class Message
|
||||
{
|
||||
/**
|
||||
* The default (english) error message
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Unknown message';
|
||||
|
||||
/**
|
||||
* The error code
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* The message arguments. Will be replaced in the message body.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $arguments = [];
|
||||
|
||||
/**
|
||||
* An optional title for the message (used eg. in flashMessages).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = '';
|
||||
|
||||
/**
|
||||
* Constructs this error
|
||||
*
|
||||
* @param string $message An english error message which is used if no other error message can be resolved
|
||||
* @param int $code A unique error code
|
||||
* @param array $arguments Array of arguments to be replaced in message
|
||||
* @param string $title optional title for the message
|
||||
*/
|
||||
public function __construct(string $message, int $code, array $arguments = [], string $title = '')
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->code = $code;
|
||||
$this->arguments = $arguments;
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message
|
||||
*
|
||||
* @return string The error message
|
||||
*/
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code
|
||||
*
|
||||
* @return int The error code
|
||||
*/
|
||||
public function getCode(): int
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get arguments
|
||||
*/
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rendered message
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
if (count($this->arguments) > 0) {
|
||||
return vsprintf($this->message, $this->arguments);
|
||||
}
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this error into a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\Extbase\Error;
|
||||
|
||||
/**
|
||||
* An object representation of a generic notice. Subclass this to create
|
||||
* more specific notices if necessary.
|
||||
*/
|
||||
class Notice extends Message
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Unknown notice';
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
<?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\Extbase\Error;
|
||||
|
||||
/**
|
||||
* Result object for operations dealing with objects, such as the Property Mapper or the Validators.
|
||||
*/
|
||||
class Result
|
||||
{
|
||||
/**
|
||||
* @var Error[]
|
||||
*/
|
||||
protected $errors = [];
|
||||
|
||||
/**
|
||||
* Caches the existence of errors
|
||||
* @var bool
|
||||
*/
|
||||
protected $errorsExist = false;
|
||||
|
||||
/**
|
||||
* @var Warning[]
|
||||
*/
|
||||
protected $warnings = [];
|
||||
|
||||
/**
|
||||
* Caches the existence of warning
|
||||
* @var bool
|
||||
*/
|
||||
protected $warningsExist = false;
|
||||
|
||||
/**
|
||||
* @var Notice[]
|
||||
*/
|
||||
protected $notices = [];
|
||||
|
||||
/**
|
||||
* Caches the existence of notices
|
||||
* @var bool
|
||||
*/
|
||||
protected $noticesExist = false;
|
||||
|
||||
/**
|
||||
* The result objects for the sub properties
|
||||
*
|
||||
* @var Result[]
|
||||
*/
|
||||
protected $propertyResults = [];
|
||||
|
||||
/**
|
||||
* @var Result
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* Injects the parent result and propagates the
|
||||
* cached error states upwards
|
||||
*/
|
||||
public function setParent(Result $parent): void
|
||||
{
|
||||
if ($this->parent !== $parent) {
|
||||
$this->parent = $parent;
|
||||
if ($this->hasErrors()) {
|
||||
$parent->setErrorsExist();
|
||||
}
|
||||
if ($this->hasWarnings()) {
|
||||
$parent->setWarningsExist();
|
||||
}
|
||||
if ($this->hasNotices()) {
|
||||
$parent->setNoticesExist();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error to the current Result object
|
||||
*/
|
||||
public function addError(Error $error): void
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
$this->setErrorsExist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a warning to the current Result object
|
||||
*/
|
||||
public function addWarning(Warning $warning): void
|
||||
{
|
||||
$this->warnings[] = $warning;
|
||||
$this->setWarningsExist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a notice to the current Result object
|
||||
*/
|
||||
public function addNotice(Notice $notice): void
|
||||
{
|
||||
$this->notices[] = $notice;
|
||||
$this->setNoticesExist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all errors in the current Result object (non-recursive)
|
||||
*
|
||||
* @return Error[]
|
||||
*/
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all warnings in the current Result object (non-recursive)
|
||||
*
|
||||
* @return Warning[]
|
||||
*/
|
||||
public function getWarnings(): array
|
||||
{
|
||||
return $this->warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notices in the current Result object (non-recursive)
|
||||
*
|
||||
* @return Notice[]
|
||||
*/
|
||||
public function getNotices(): array
|
||||
{
|
||||
return $this->notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first error object of the current Result object (non-recursive)
|
||||
*
|
||||
* @return bool|Error
|
||||
*/
|
||||
public function getFirstError()
|
||||
{
|
||||
reset($this->errors);
|
||||
return current($this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first warning object of the current Result object (non-recursive)
|
||||
*
|
||||
* @return bool|Warning
|
||||
*/
|
||||
public function getFirstWarning()
|
||||
{
|
||||
reset($this->warnings);
|
||||
return current($this->warnings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first notice object of the current Result object (non-recursive)
|
||||
*
|
||||
* @return bool|Notice
|
||||
*/
|
||||
public function getFirstNotice()
|
||||
{
|
||||
reset($this->notices);
|
||||
return current($this->notices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Result object for the given property path. This is
|
||||
* a fluent interface, so you will probably use it like:
|
||||
* $result->forProperty('foo.bar')->getErrors() -- to get all errors
|
||||
* for property "foo.bar"
|
||||
*/
|
||||
public function forProperty(?string $propertyPath): Result
|
||||
{
|
||||
if ($propertyPath === '' || $propertyPath === null) {
|
||||
return $this;
|
||||
}
|
||||
if (str_contains($propertyPath, '.')) {
|
||||
return $this->recurseThroughResult(explode('.', $propertyPath));
|
||||
}
|
||||
if (!isset($this->propertyResults[$propertyPath])) {
|
||||
$this->propertyResults[$propertyPath] = new self();
|
||||
$this->propertyResults[$propertyPath]->setParent($this);
|
||||
}
|
||||
return $this->propertyResults[$propertyPath];
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo: consider making this method protected as it will and should not be called from an outside scope
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function recurseThroughResult(array $pathSegments): Result
|
||||
{
|
||||
if (count($pathSegments) === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$propertyName = array_shift($pathSegments);
|
||||
|
||||
if (!isset($this->propertyResults[$propertyName])) {
|
||||
$this->propertyResults[$propertyName] = new self();
|
||||
$this->propertyResults[$propertyName]->setParent($this);
|
||||
}
|
||||
|
||||
return $this->propertyResults[$propertyName]->recurseThroughResult($pathSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the error cache to TRUE and propagates the information
|
||||
* upwards the Result-Object Tree
|
||||
*/
|
||||
protected function setErrorsExist(): void
|
||||
{
|
||||
$this->errorsExist = true;
|
||||
if ($this->parent !== null) {
|
||||
$this->parent->setErrorsExist();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the warning cache to TRUE and propagates the information
|
||||
* upwards the Result-Object Tree
|
||||
*/
|
||||
protected function setWarningsExist(): void
|
||||
{
|
||||
$this->warningsExist = true;
|
||||
if ($this->parent !== null) {
|
||||
$this->parent->setWarningsExist();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the notices cache to TRUE and propagates the information
|
||||
* upwards the Result-Object Tree
|
||||
*/
|
||||
protected function setNoticesExist(): void
|
||||
{
|
||||
$this->noticesExist = true;
|
||||
if ($this->parent !== null) {
|
||||
$this->parent->setNoticesExist();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the current Result object have Notices, Errors or Warnings? (Recursively)
|
||||
*/
|
||||
public function hasMessages(): bool
|
||||
{
|
||||
return $this->errorsExist || $this->noticesExist || $this->warningsExist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the result
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->errors = [];
|
||||
$this->notices = [];
|
||||
$this->warnings = [];
|
||||
|
||||
$this->warningsExist = false;
|
||||
$this->noticesExist = false;
|
||||
$this->errorsExist = false;
|
||||
|
||||
$this->propertyResults = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the current Result object have Errors? (Recursively)
|
||||
*/
|
||||
public function hasErrors(): bool
|
||||
{
|
||||
if (count($this->errors) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->propertyResults as $subResult) {
|
||||
if ($subResult->hasErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the current Result object have Warnings? (Recursively)
|
||||
*/
|
||||
public function hasWarnings(): bool
|
||||
{
|
||||
if (count($this->warnings) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->propertyResults as $subResult) {
|
||||
if ($subResult->hasWarnings()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the current Result object have Notices? (Recursively)
|
||||
*/
|
||||
public function hasNotices(): bool
|
||||
{
|
||||
if (count($this->notices) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->propertyResults as $subResult) {
|
||||
if ($subResult->hasNotices()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all Error objects recursively. The result is an array,
|
||||
* where the key is the property path where the error occurred, and the
|
||||
* value is a list of all errors (stored as array)
|
||||
*
|
||||
* @return array<string,array<Error>>
|
||||
*/
|
||||
public function getFlattenedErrors(): array
|
||||
{
|
||||
$result = [];
|
||||
$this->flattenErrorTree($result, []);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all Warning objects recursively. The result is an array,
|
||||
* where the key is the property path where the warning occurred, and the
|
||||
* value is a list of all warnings (stored as array)
|
||||
*
|
||||
* @return array<string,array<Warning>>
|
||||
*/
|
||||
public function getFlattenedWarnings(): array
|
||||
{
|
||||
$result = [];
|
||||
$this->flattenWarningsTree($result, []);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all Notice objects recursively. The result is an array,
|
||||
* where the key is the property path where the notice occurred, and the
|
||||
* value is a list of all notices (stored as array)
|
||||
*
|
||||
* @return array<string,array<Notice>>
|
||||
*/
|
||||
public function getFlattenedNotices(): array
|
||||
{
|
||||
$result = [];
|
||||
$this->flattenNoticesTree($result, []);
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function flattenErrorTree(array &$result, array $level): void
|
||||
{
|
||||
if (count($this->errors) > 0) {
|
||||
$result[implode('.', $level)] = $this->errors;
|
||||
}
|
||||
foreach ($this->propertyResults as $subPropertyName => $subResult) {
|
||||
$level[] = $subPropertyName;
|
||||
$subResult->flattenErrorTree($result, $level);
|
||||
array_pop($level);
|
||||
}
|
||||
}
|
||||
|
||||
protected function flattenWarningsTree(array &$result, array $level): void
|
||||
{
|
||||
if (count($this->warnings) > 0) {
|
||||
$result[implode('.', $level)] = $this->warnings;
|
||||
}
|
||||
foreach ($this->propertyResults as $subPropertyName => $subResult) {
|
||||
$level[] = $subPropertyName;
|
||||
$subResult->flattenWarningsTree($result, $level);
|
||||
array_pop($level);
|
||||
}
|
||||
}
|
||||
|
||||
protected function flattenNoticesTree(array &$result, array $level): void
|
||||
{
|
||||
if (count($this->notices) > 0) {
|
||||
$result[implode('.', $level)] = $this->notices;
|
||||
}
|
||||
foreach ($this->propertyResults as $subPropertyName => $subResult) {
|
||||
$level[] = $subPropertyName;
|
||||
$subResult->flattenNoticesTree($result, $level);
|
||||
array_pop($level);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given Result object into this one.
|
||||
*/
|
||||
public function merge(Result $otherResult): void
|
||||
{
|
||||
if ($otherResult->errorsExist) {
|
||||
$this->mergeProperty($otherResult, 'getErrors', 'addError');
|
||||
}
|
||||
if ($otherResult->warningsExist) {
|
||||
$this->mergeProperty($otherResult, 'getWarnings', 'addWarning');
|
||||
}
|
||||
if ($otherResult->noticesExist) {
|
||||
$this->mergeProperty($otherResult, 'getNotices', 'addNotice');
|
||||
}
|
||||
|
||||
foreach ($otherResult->getSubResults() as $subPropertyName => $subResult) {
|
||||
/** @var Result $subResult */
|
||||
if (array_key_exists($subPropertyName, $this->propertyResults) && $this->propertyResults[$subPropertyName]->hasMessages()) {
|
||||
$this->forProperty((string)$subPropertyName)->merge($subResult);
|
||||
} else {
|
||||
$this->propertyResults[$subPropertyName] = $subResult;
|
||||
$subResult->setParent($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a single property from the other result object.
|
||||
*/
|
||||
protected function mergeProperty(Result $otherResult, string $getterName, string $adderName): void
|
||||
{
|
||||
$getter = [$otherResult, $getterName];
|
||||
$adder = [$this, $adderName];
|
||||
|
||||
if (!is_callable($getter) || !is_callable($adder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($getter() as $messageInOtherResult) {
|
||||
$adder($messageInOtherResult);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all sub Result objects available.
|
||||
*
|
||||
* @return Result[]
|
||||
*/
|
||||
public function getSubResults(): array
|
||||
{
|
||||
return $this->propertyResults;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\Extbase\Error;
|
||||
|
||||
/**
|
||||
* An object representation of a generic warning. Subclass this to create
|
||||
* more specific warnings if necessary.
|
||||
*/
|
||||
class Warning extends Message
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Unknown warning';
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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\Extbase\Event\Configuration;
|
||||
|
||||
/**
|
||||
* Event which is dispatched before flexForm configuration overrides framework configuration. Possible core flexForm
|
||||
* overrides have already been processed in `$flexFormConfiguration`.
|
||||
*
|
||||
* Listeners can implement a custom flexForm override process by using the original flexForm configuration available
|
||||
* in `$originalFlexFormConfiguration`.
|
||||
*/
|
||||
final class BeforeFlexFormConfigurationOverrideEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly array $frameworkConfiguration,
|
||||
private readonly array $originalFlexFormConfiguration,
|
||||
private array $flexFormConfiguration
|
||||
) {}
|
||||
|
||||
public function getFrameworkConfiguration(): array
|
||||
{
|
||||
return $this->frameworkConfiguration;
|
||||
}
|
||||
|
||||
public function getOriginalFlexFormConfiguration(): array
|
||||
{
|
||||
return $this->originalFlexFormConfiguration;
|
||||
}
|
||||
|
||||
public function getFlexFormConfiguration(): array
|
||||
{
|
||||
return $this->flexFormConfiguration;
|
||||
}
|
||||
|
||||
public function setFlexFormConfiguration(array $flexFormConfiguration): void
|
||||
{
|
||||
$this->flexFormConfiguration = $flexFormConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Mvc;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired after the dispatcher has successfully dispatched a request to a controller/action.
|
||||
*/
|
||||
final readonly class AfterRequestDispatchedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private RequestInterface $request,
|
||||
private ResponseInterface $response
|
||||
) {}
|
||||
|
||||
public function getRequest(): RequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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\Extbase\Event\Mvc;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Extbase\Attribute\Authorize;
|
||||
use TYPO3\CMS\Extbase\Authorization\AuthorizationFailureReason;
|
||||
|
||||
/**
|
||||
* Event that is triggered when an extbase action authorization check fails and before the authorization denied
|
||||
* PropagateResponseException is thrown. Extension developers can use this event to prevent the default behavior
|
||||
* and provide a custom response.
|
||||
*
|
||||
* Security notice: When providing a custom response, it must be ensured, that no objects are persisted using
|
||||
* the extbase persistence layer. Additionally it is recommended to only use a custom response for non-cached
|
||||
* actions, because otherwise the response result will get cached.
|
||||
*/
|
||||
final class BeforeActionAuthorizationDeniedEvent
|
||||
{
|
||||
private ?ResponseInterface $response = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly ServerRequestInterface $request,
|
||||
private readonly string $controllerClassName,
|
||||
private readonly string $actionMethodName,
|
||||
private readonly Authorize $authorize,
|
||||
private readonly AuthorizationFailureReason $failureReason
|
||||
) {}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function getControllerClassName(): string
|
||||
{
|
||||
return $this->controllerClassName;
|
||||
}
|
||||
|
||||
public function getActionMethodName(): string
|
||||
{
|
||||
return $this->actionMethodName;
|
||||
}
|
||||
|
||||
public function getAuthorize(): Authorize
|
||||
{
|
||||
return $this->authorize;
|
||||
}
|
||||
|
||||
public function getFailureReason(): AuthorizationFailureReason
|
||||
{
|
||||
return $this->failureReason;
|
||||
}
|
||||
|
||||
public function getResponse(): ?ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function setResponse(ResponseInterface $response): void
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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\Extbase\Event\Mvc;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Event that is triggered before any Extbase Action is called within the ActionController or one
|
||||
* of its subclasses.
|
||||
*/
|
||||
final readonly class BeforeActionCallEvent
|
||||
{
|
||||
public function __construct(
|
||||
private string $controllerClassName,
|
||||
private string $actionMethodName,
|
||||
private array $preparedArguments,
|
||||
private ServerRequestInterface $request,
|
||||
) {}
|
||||
|
||||
public function getControllerClassName(): string
|
||||
{
|
||||
return $this->controllerClassName;
|
||||
}
|
||||
|
||||
public function getActionMethodName(): string
|
||||
{
|
||||
return $this->actionMethodName;
|
||||
}
|
||||
|
||||
public function getPreparedArguments(): array
|
||||
{
|
||||
return $this->preparedArguments;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\Extbase\Event\Mvc;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Extbase\Attribute\RateLimit;
|
||||
|
||||
/**
|
||||
* Event that is triggered, when an extbase action is rate limitied and before the rate limit response is sent.
|
||||
* Extension developers can use this event to provide an alternative response or to implement a custom logic.
|
||||
*/
|
||||
final class BeforeActionRateLimitResponseEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ServerRequestInterface $request,
|
||||
private readonly string $controllerClassName,
|
||||
private readonly string $actionMethodName,
|
||||
private readonly RateLimit $rateLimit,
|
||||
private ResponseInterface $response
|
||||
) {}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function getControllerClassName(): string
|
||||
{
|
||||
return $this->controllerClassName;
|
||||
}
|
||||
|
||||
public function getActionMethodName(): string
|
||||
{
|
||||
return $this->actionMethodName;
|
||||
}
|
||||
|
||||
public function getRateLimit(): RateLimit
|
||||
{
|
||||
return $this->rateLimit;
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function setResponse(ResponseInterface $response): void
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
|
||||
/**
|
||||
* Allows to modify values when creating domain objects.
|
||||
*/
|
||||
final readonly class AfterObjectThawedEvent
|
||||
{
|
||||
public function __construct(private DomainObjectInterface $mappedObject, private array $record) {}
|
||||
|
||||
public function getObject(): DomainObjectInterface
|
||||
{
|
||||
return $this->mappedObject;
|
||||
}
|
||||
|
||||
public function getRecord(): array
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired after an object/entity was sent to persistence layer to be added,
|
||||
* but before updating the reference index and current session.
|
||||
*/
|
||||
final readonly class EntityAddedToPersistenceEvent
|
||||
{
|
||||
public function __construct(private DomainObjectInterface $persistedObject) {}
|
||||
|
||||
public function getObject(): DomainObjectInterface
|
||||
{
|
||||
return $this->persistedObject;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired after an object/entity was sent to persistence layer to be added
|
||||
* including update of reference index and current session.
|
||||
*/
|
||||
final readonly class EntityFinalizedAfterPersistenceEvent
|
||||
{
|
||||
public function __construct(private DomainObjectInterface $persistedObject) {}
|
||||
|
||||
public function getObject(): DomainObjectInterface
|
||||
{
|
||||
return $this->persistedObject;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired after an object was pushed to the storage backend
|
||||
*/
|
||||
final readonly class EntityPersistedEvent
|
||||
{
|
||||
public function __construct(private DomainObjectInterface $persistedObject) {}
|
||||
|
||||
public function getObject(): DomainObjectInterface
|
||||
{
|
||||
return $this->persistedObject;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired after an object/entity was sent to persistence layer to be removed.
|
||||
*/
|
||||
final readonly class EntityRemovedFromPersistenceEvent
|
||||
{
|
||||
public function __construct(private DomainObjectInterface $persistedObject) {}
|
||||
|
||||
public function getObject(): DomainObjectInterface
|
||||
{
|
||||
return $this->persistedObject;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired after an object/entity was sent to persistence layer to be updated.
|
||||
*/
|
||||
final readonly class EntityUpdatedInPersistenceEvent
|
||||
{
|
||||
public function __construct(private DomainObjectInterface $persistedObject) {}
|
||||
|
||||
public function getObject(): DomainObjectInterface
|
||||
{
|
||||
return $this->persistedObject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired before the storage backend is asked for a result count from a given query.
|
||||
*/
|
||||
final class ModifyQueryBeforeFetchingObjectCountEvent
|
||||
{
|
||||
public function __construct(private QueryInterface $query) {}
|
||||
|
||||
public function getQuery(): QueryInterface
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
public function setQuery(QueryInterface $query): void
|
||||
{
|
||||
$this->query = $query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired before the storage backend is asked for results from a given query.
|
||||
*/
|
||||
final class ModifyQueryBeforeFetchingObjectDataEvent
|
||||
{
|
||||
public function __construct(private QueryInterface $query) {}
|
||||
|
||||
public function getQuery(): QueryInterface
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
public function setQuery(QueryInterface $query): void
|
||||
{
|
||||
$this->query = $query;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired after the storage backend has counted the results of a given query.
|
||||
*/
|
||||
final class ModifyResultAfterFetchingObjectCountEvent
|
||||
{
|
||||
public function __construct(private readonly QueryInterface $query, private int $result) {}
|
||||
|
||||
public function getQuery(): QueryInterface
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
public function getResult(): int
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function setResult(int $result): void
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
|
||||
|
||||
/**
|
||||
* Event which is fired after the storage backend has pulled results from a given query.
|
||||
*/
|
||||
final class ModifyResultAfterFetchingObjectDataEvent
|
||||
{
|
||||
public function __construct(private readonly QueryInterface $query, private array $result) {}
|
||||
|
||||
public function getQuery(): QueryInterface
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
public function getResult(): array
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function setResult(array $result): void
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Event\Service;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\FileUploadConfiguration;
|
||||
|
||||
/**
|
||||
* Allows to modify the target filename of uploaded files when handled
|
||||
* by the extbase fileupload service
|
||||
*/
|
||||
final class ModifyUploadedFileTargetFilenameEvent
|
||||
{
|
||||
public function __construct(
|
||||
private string $targetFilename,
|
||||
private readonly FileUploadConfiguration $configuration
|
||||
) {}
|
||||
|
||||
public function getTargetFilename(): string
|
||||
{
|
||||
return $this->targetFilename;
|
||||
}
|
||||
|
||||
public function setTargetFilename(string $targetFilename): void
|
||||
{
|
||||
$this->targetFilename = $targetFilename;
|
||||
}
|
||||
|
||||
public function getConfiguration(): FileUploadConfiguration
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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\Extbase\EventListener;
|
||||
|
||||
use TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
|
||||
/**
|
||||
* Set default extbase icon for extbase modules
|
||||
*/
|
||||
final class AddDefaultModuleIcon
|
||||
{
|
||||
#[AsEventListener('extbase/add-default-extbase-module-icon')]
|
||||
public function __invoke(BeforeModuleCreationEvent $event): void
|
||||
{
|
||||
if (!$event->hasConfigurationValue('controllerActions')
|
||||
|| $event->getConfigurationValue('icon')
|
||||
|| $event->getConfigurationValue('iconIdentifier')
|
||||
) {
|
||||
// Either no extbase module or icon / iconIdentifier is already set
|
||||
return;
|
||||
}
|
||||
|
||||
$event->setConfigurationValue('icon', 'EXT:extbase/Resources/Public/Icons/Extension.svg');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?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\Extbase\EventListener;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\DateTimeAspect;
|
||||
use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\Exception\UndefinedSchemaException;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\EntityAddedToPersistenceEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\EntityRemovedFromPersistenceEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\EntityUpdatedInPersistenceEvent;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
|
||||
|
||||
/**
|
||||
* Event listener that automatically tracks history for all Extbase domain entities
|
||||
* by listening to Extbase persistence events and storing them in sys_history.
|
||||
*/
|
||||
final readonly class ExtbaseHistoryTracker
|
||||
{
|
||||
public function __construct(
|
||||
private DataMapFactory $dataMapFactory,
|
||||
private Context $context,
|
||||
private TcaSchemaFactory $tcaSchemaFactory,
|
||||
private Features $features,
|
||||
) {}
|
||||
|
||||
#[AsEventListener('extbase-history-tracker-persisted')]
|
||||
public function onEntityPersisted(EntityAddedToPersistenceEvent $event): void
|
||||
{
|
||||
$this->trackEntityHistory($event, RecordHistoryStore::ACTION_ADD);
|
||||
}
|
||||
|
||||
#[AsEventListener('extbase-history-tracker-updated')]
|
||||
public function onEntityUpdated(EntityUpdatedInPersistenceEvent $event): void
|
||||
{
|
||||
$this->trackEntityHistory($event, RecordHistoryStore::ACTION_MODIFY);
|
||||
}
|
||||
|
||||
#[AsEventListener('extbase-history-tracker-removed')]
|
||||
public function onEntityRemoved(EntityRemovedFromPersistenceEvent $event): void
|
||||
{
|
||||
$this->trackEntityHistory($event, RecordHistoryStore::ACTION_DELETE);
|
||||
}
|
||||
|
||||
private function trackEntityHistory(
|
||||
EntityAddedToPersistenceEvent|EntityUpdatedInPersistenceEvent|EntityRemovedFromPersistenceEvent $event,
|
||||
int $action
|
||||
): void {
|
||||
// Skip history tracking if feature flag is disabled. TCA does not matter in this case.
|
||||
if (!$this->features->isFeatureEnabled('extbase.enableHistoryTracking')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$object = $event->getObject();
|
||||
|
||||
// Skip if object doesn't have a UID (not persisted yet)
|
||||
if ($object->getUid() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dataMap = $this->dataMapFactory->buildDataMap($object::class);
|
||||
$tableName = $dataMap->getTableName();
|
||||
|
||||
// Skip if table doesn't exist in TCA schema
|
||||
try {
|
||||
$schema = $this->tcaSchemaFactory->get($tableName);
|
||||
} catch (UndefinedSchemaException) {
|
||||
// Table not found in TCA schema, skip history tracking
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip history tracking if TCA ctrl setting is disabled (defaults to "enabled")
|
||||
if (!$schema->hasCapability(TcaSchemaCapability::ExtbaseHistoryTracking)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$historyStore = $this->createHistoryStore();
|
||||
|
||||
match ($action) {
|
||||
RecordHistoryStore::ACTION_ADD => $historyStore->addRecord(
|
||||
$tableName,
|
||||
$object->getUid(),
|
||||
$this->extractObjectData($object, $dataMap)
|
||||
),
|
||||
RecordHistoryStore::ACTION_MODIFY => $historyStore->modifyRecord(
|
||||
$tableName,
|
||||
$object->getUid(),
|
||||
[
|
||||
'oldRecord' => $this->extractObjectData($object, $dataMap, false, true),
|
||||
'newRecord' => $this->extractObjectData($object, $dataMap, false),
|
||||
'_pid' => $object->getPid(),
|
||||
'_extbase_class' => $object::class,
|
||||
],
|
||||
),
|
||||
RecordHistoryStore::ACTION_DELETE => $historyStore->deleteRecord(
|
||||
$tableName,
|
||||
$object->getUid()
|
||||
),
|
||||
default => throw new \InvalidArgumentException(
|
||||
sprintf('Unsupported history action: %d', $action),
|
||||
1774123762
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private function createHistoryStore(): RecordHistoryStore
|
||||
{
|
||||
/** @var DateTimeAspect $dateTimeAspect */
|
||||
$dateTimeAspect = $this->context->getAspect('date');
|
||||
$currentTimestamp = $dateTimeAspect->get('timestamp');
|
||||
|
||||
$userAspect = $this->context->getAspect('frontend.user');
|
||||
|
||||
if ($userAspect->isLoggedIn()) {
|
||||
return new RecordHistoryStore(
|
||||
RecordHistoryStore::USER_FRONTEND,
|
||||
$userAspect->get('id'),
|
||||
null,
|
||||
$currentTimestamp
|
||||
);
|
||||
}
|
||||
|
||||
// Check for backend user context
|
||||
$backendUserAspect = $this->context->getAspect('backend.user');
|
||||
if ($backendUserAspect->isLoggedIn()) {
|
||||
return new RecordHistoryStore(
|
||||
RecordHistoryStore::USER_BACKEND,
|
||||
$backendUserAspect->get('id'),
|
||||
null,
|
||||
$currentTimestamp
|
||||
);
|
||||
}
|
||||
|
||||
// Anonymous user
|
||||
return new RecordHistoryStore(
|
||||
RecordHistoryStore::USER_ANONYMOUS,
|
||||
null,
|
||||
null,
|
||||
$currentTimestamp
|
||||
);
|
||||
}
|
||||
|
||||
private function extractObjectData(DomainObjectInterface $object, DataMap $dataMap, bool $appendMetadata = true, bool $fetchPropertiesBeforePersistence = false): array
|
||||
{
|
||||
$data = [];
|
||||
if ($fetchPropertiesBeforePersistence && $object instanceof AbstractDomainObject) {
|
||||
$properties = $object->_getCleanProperties();
|
||||
} else {
|
||||
$properties = $object->_getProperties();
|
||||
}
|
||||
|
||||
foreach ($properties as $propertyName => $propertyValue) {
|
||||
// Get actual database column name:
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
if ($columnMap !== null) {
|
||||
$propertyName = $columnMap->columnName;
|
||||
} else {
|
||||
$propertyName = GeneralUtility::camelCaseToLowerCaseUnderscored($propertyName);
|
||||
}
|
||||
|
||||
// Convert objects and complex types to string representation
|
||||
if (is_object($propertyValue)) {
|
||||
if ($propertyValue instanceof DomainObjectInterface) {
|
||||
$data[$propertyName] = $propertyValue->getUid();
|
||||
} elseif (method_exists($propertyValue, '__toString')) {
|
||||
$data[$propertyName] = (string)$propertyValue;
|
||||
} else {
|
||||
$data[$propertyName] = get_class($propertyValue);
|
||||
}
|
||||
} elseif (is_array($propertyValue)) {
|
||||
$data[$propertyName] = json_encode($propertyValue);
|
||||
} else {
|
||||
$data[$propertyName] = $propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
if ($appendMetadata) {
|
||||
$data['_extbase_class'] = $object::class;
|
||||
$data['_pid'] = $object->getPid();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase;
|
||||
|
||||
use TYPO3\CMS\Core\Exception as CoreException;
|
||||
|
||||
/**
|
||||
* A generic Extbase exception
|
||||
*/
|
||||
class Exception extends CoreException {}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?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\Extbase\Http;
|
||||
|
||||
use TYPO3\CMS\Core\Http\Response;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
|
||||
class ForwardResponse extends Response
|
||||
{
|
||||
private ?string $controllerName = null;
|
||||
private ?string $extensionName = null;
|
||||
private ?array $arguments = null;
|
||||
private Result $argumentsValidationResult;
|
||||
/**
|
||||
* @var list<FlashMessage>
|
||||
*/
|
||||
private array $flashMessages = [];
|
||||
|
||||
public function __construct(private readonly string $actionName)
|
||||
{
|
||||
$this->argumentsValidationResult = new Result();
|
||||
parent::__construct('php://temp', 204);
|
||||
}
|
||||
|
||||
public function withControllerName(string $controllerName): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->controllerName = $controllerName;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function withoutControllerName(): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->controllerName = null;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function withExtensionName(string $extensionName): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->extensionName = $extensionName;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function withoutExtensionName(): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$this->extensionName = null;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function withArguments(array $arguments): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->arguments = $arguments;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function withoutArguments(): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$this->arguments = null;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function withArgumentsValidationResult(Result $argumentsValidationResult): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->argumentsValidationResult = $argumentsValidationResult;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function withFlashMessages(FlashMessage ...$flashMessages): self
|
||||
{
|
||||
if ($flashMessages === []) {
|
||||
return $this;
|
||||
}
|
||||
$clone = clone $this;
|
||||
$clone->flashMessages = array_merge($this->flashMessages, $flashMessages);
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function getActionName(): string
|
||||
{
|
||||
return $this->actionName;
|
||||
}
|
||||
|
||||
public function getControllerName(): ?string
|
||||
{
|
||||
return $this->controllerName;
|
||||
}
|
||||
|
||||
public function getExtensionName(): ?string
|
||||
{
|
||||
return $this->extensionName;
|
||||
}
|
||||
|
||||
public function getArguments(): ?array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
public function getArgumentsValidationResult(): Result
|
||||
{
|
||||
return $this->argumentsValidationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<FlashMessage>
|
||||
*/
|
||||
public function getFlashMessages(): array
|
||||
{
|
||||
return $this->flashMessages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,967 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\PropagateResponseException;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
use TYPO3\CMS\Extbase\Authorization\AuthorizationFailureReason;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionAuthorizationDeniedEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionRateLimitResponseEvent;
|
||||
use TYPO3\CMS\Extbase\Http\ForwardResponse;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\Exception\RequiredArgumentMissingException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException;
|
||||
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
||||
use TYPO3\CMS\Extbase\Mvc\Request;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
|
||||
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
|
||||
use TYPO3\CMS\Extbase\Property\PropertyMapper;
|
||||
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
|
||||
use TYPO3\CMS\Extbase\Security\HashScope;
|
||||
use TYPO3\CMS\Extbase\Service\ExtensionService;
|
||||
use TYPO3\CMS\Extbase\Service\FileHandlingService;
|
||||
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
|
||||
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
|
||||
/**
|
||||
* A multi action controller. This is by far the most common base class for Controllers.
|
||||
*/
|
||||
abstract class ActionController implements ControllerInterface
|
||||
{
|
||||
protected ResponseFactoryInterface $responseFactory;
|
||||
protected StreamFactoryInterface $streamFactory;
|
||||
protected HashService $hashService;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected ReflectionService $reflectionService;
|
||||
|
||||
/**
|
||||
* The current view, as resolved by resolveView()
|
||||
*/
|
||||
protected ViewInterface $view;
|
||||
|
||||
/**
|
||||
* The default view class to use. Keep this 'null' for default fluid
|
||||
* view, or set to 'JsonView::class' or some inheriting class.
|
||||
*
|
||||
* @var class-string|null
|
||||
*/
|
||||
protected ?string $defaultViewObjectName = null;
|
||||
|
||||
/**
|
||||
* Name of the action method
|
||||
* @var non-empty-string
|
||||
* @internal
|
||||
*/
|
||||
protected string $actionMethodName = 'indexAction';
|
||||
|
||||
/**
|
||||
* Name of the special error action method which is called in case of errors
|
||||
*/
|
||||
protected string $errorMethodName = 'errorAction';
|
||||
|
||||
protected MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService;
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
protected FileHandlingService $fileHandlingService;
|
||||
protected RequestInterface $request;
|
||||
protected UriBuilder $uriBuilder;
|
||||
protected RateLimitRegistry $rateLimitRegistry;
|
||||
protected AuthorizeRegistry $authorizeRegistry;
|
||||
|
||||
/**
|
||||
* Contains the settings of the current extension
|
||||
*/
|
||||
protected array $settings;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected ValidatorResolver $validatorResolver;
|
||||
|
||||
private ViewFactoryInterface $viewFactory;
|
||||
|
||||
protected Arguments $arguments;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected ConfigurationManagerInterface $configurationManager;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private PropertyMapper $propertyMapper;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private FlashMessageService $internalFlashMessageService;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private ExtensionService $internalExtensionService;
|
||||
|
||||
final public function injectResponseFactory(ResponseFactoryInterface $responseFactory): void
|
||||
{
|
||||
$this->responseFactory = $responseFactory;
|
||||
}
|
||||
|
||||
final public function injectStreamFactory(StreamFactoryInterface $streamFactory): void
|
||||
{
|
||||
$this->streamFactory = $streamFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
|
||||
{
|
||||
$this->configurationManager = $configurationManager;
|
||||
$this->settings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS);
|
||||
$this->arguments = GeneralUtility::makeInstance(Arguments::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectValidatorResolver(ValidatorResolver $validatorResolver): void
|
||||
{
|
||||
$this->validatorResolver = $validatorResolver;
|
||||
}
|
||||
|
||||
final public function injectViewFactory(ViewFactoryInterface $viewFactory): void
|
||||
{
|
||||
$this->viewFactory = $viewFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectReflectionService(ReflectionService $reflectionService): void
|
||||
{
|
||||
$this->reflectionService = $reflectionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectHashService(HashService $hashService): void
|
||||
{
|
||||
$this->hashService = $hashService;
|
||||
}
|
||||
|
||||
public function injectMvcPropertyMappingConfigurationService(MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService): void
|
||||
{
|
||||
$this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
|
||||
}
|
||||
|
||||
public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public function injectFileHandlingService(FileHandlingService $fileHandlingService): void
|
||||
{
|
||||
$this->fileHandlingService = $fileHandlingService;
|
||||
}
|
||||
|
||||
public function injectRateLimitRegistry(RateLimitRegistry $rateLimitRegistry): void
|
||||
{
|
||||
$this->rateLimitRegistry = $rateLimitRegistry;
|
||||
}
|
||||
|
||||
public function injectAuthorizeRegistry(AuthorizeRegistry $authorizeRegistry): void
|
||||
{
|
||||
$this->authorizeRegistry = $authorizeRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectPropertyMapper(PropertyMapper $propertyMapper): void
|
||||
{
|
||||
$this->propertyMapper = $propertyMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final public function injectInternalFlashMessageService(FlashMessageService $flashMessageService): void
|
||||
{
|
||||
$this->internalFlashMessageService = $flashMessageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final public function injectInternalExtensionService(ExtensionService $extensionService): void
|
||||
{
|
||||
$this->internalExtensionService = $extensionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller before invoking an action method.
|
||||
*
|
||||
* Override this method to solve tasks which all actions have in
|
||||
* common.
|
||||
*/
|
||||
protected function initializeAction(): void {}
|
||||
|
||||
/**
|
||||
* Implementation of the arguments initialization in the action controller:
|
||||
* Automatically registers arguments of the current action
|
||||
*
|
||||
* Don't override this method - use initializeAction() instead.
|
||||
*
|
||||
* @throws InvalidArgumentTypeException
|
||||
* @see initializeArguments()
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function initializeActionMethodArguments(): void
|
||||
{
|
||||
$methodParameters = $this->reflectionService
|
||||
->getClassSchema(static::class)
|
||||
->getMethod($this->actionMethodName)->getParameters();
|
||||
|
||||
foreach ($methodParameters as $parameterName => $parameter) {
|
||||
$dataType = null;
|
||||
if ($parameter->getType() !== null) {
|
||||
$dataType = $parameter->getType();
|
||||
} elseif ($parameter->isArray()) {
|
||||
$dataType = 'array';
|
||||
}
|
||||
if ($dataType === null) {
|
||||
throw new InvalidArgumentTypeException('The argument type for parameter $' . $parameterName . ' of method ' . static::class . '->' . $this->actionMethodName . '() could not be detected.', 1253175643);
|
||||
}
|
||||
$defaultValue = $parameter->hasDefaultValue() ? $parameter->getDefaultValue() : null;
|
||||
$this->arguments->addNewArgument($parameterName, $dataType, !$parameter->isOptional(), $defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the needed validators to the Arguments:
|
||||
*
|
||||
* - Validators checking the data type from the param annotation
|
||||
* - Custom validators specified with #[Validate] attributes.
|
||||
* - Model-based validators (#[Validate] attributes in the model)
|
||||
* - Custom model validator classes
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function initializeActionMethodValidators(): void
|
||||
{
|
||||
if ($this->arguments->count() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$classSchemaMethod = $this->reflectionService->getClassSchema(static::class)->getMethod($this->actionMethodName);
|
||||
|
||||
/** @var Argument $argument */
|
||||
foreach ($this->arguments as $argument) {
|
||||
$classSchemaMethodParameter = $classSchemaMethod->getParameter($argument->getName());
|
||||
// At this point validation is skipped if there is an #[IgnoreValidation] attribute.
|
||||
// @todo: IgnoreValidation attributes could be evaluated in the ClassSchema and result in
|
||||
// no validators being applied to the method parameter.
|
||||
if ($classSchemaMethodParameter->ignoreValidation()) {
|
||||
continue;
|
||||
}
|
||||
/** @var ConjunctionValidator $validator */
|
||||
$validator = $this->validatorResolver->createValidator(ConjunctionValidator::class);
|
||||
foreach ($classSchemaMethodParameter->getValidators() as $validatorDefinition) {
|
||||
if (isset($validatorDefinition['constraint'])) {
|
||||
$validatorInstance = $validatorDefinition['constraint'];
|
||||
} else {
|
||||
$validatorInstance = $this->validatorResolver->createValidator(
|
||||
$validatorDefinition['className'],
|
||||
$validatorDefinition['options'],
|
||||
$this->request,
|
||||
);
|
||||
}
|
||||
if ($validatorInstance !== null) {
|
||||
$validator->addValidator($validatorInstance);
|
||||
}
|
||||
}
|
||||
$baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction(
|
||||
$argument->getDataType(),
|
||||
$this->request
|
||||
);
|
||||
if ($baseValidatorConjunction->count() > 0) {
|
||||
$validator->addValidator($baseValidatorConjunction);
|
||||
}
|
||||
$argument->setValidator($validator);
|
||||
}
|
||||
}
|
||||
|
||||
protected function initializeStateFromExtbaseRequestParameters(): void
|
||||
{
|
||||
$extbaseRequestParameters = $this->request->getAttribute('extbase');
|
||||
if (!$extbaseRequestParameters instanceof ExtbaseRequestParameters) {
|
||||
return;
|
||||
}
|
||||
$flashMessageQueue = $this->getFlashMessageQueue();
|
||||
foreach ($extbaseRequestParameters->getOriginalFlashMessages() as $flashMessage) {
|
||||
$flashMessage->setStoreInSession(false);
|
||||
$flashMessageQueue->enqueue($flashMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an incoming request and returns a response object
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function processRequest(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
/** @var Request $request */
|
||||
$this->request = $request;
|
||||
$this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$this->uriBuilder->setRequest($request);
|
||||
$this->actionMethodName = $this->resolveActionMethodName();
|
||||
$this->initializeActionMethodArguments();
|
||||
$this->initializeActionMethodValidators();
|
||||
$this->initializeStateFromExtbaseRequestParameters();
|
||||
$this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments);
|
||||
$this->fileHandlingService->initializeFileUploadConfigurationsFromRequest($request, $this->arguments);
|
||||
$this->initializeAction();
|
||||
$actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
|
||||
/** @var callable|null $callable */
|
||||
$callable = [$this, $actionInitializationMethodName];
|
||||
if (is_callable($callable)) {
|
||||
$callable();
|
||||
}
|
||||
$this->mapRequestArgumentsToControllerArguments();
|
||||
$this->view = $this->resolveView();
|
||||
if (method_exists($this, 'initializeView')) {
|
||||
// @todo: We may want to get rid of this and declare actions should actively create own
|
||||
// views using ViewFactoryInterface instead. See comment on resolveView() below.
|
||||
// Currently, this method is pretty much only helpful in 'xclass' scenarios,
|
||||
// since actions can already do whatever happens here within their action body.
|
||||
$this->initializeView($this->view);
|
||||
}
|
||||
$response = $this->callActionMethod($request);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and checks the current action method name
|
||||
*
|
||||
* @throws NoSuchActionException if the action specified in the request object does not exist (and if there's no default action either).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function resolveActionMethodName(): string
|
||||
{
|
||||
$actionMethodName = $this->request->getControllerActionName() . 'Action';
|
||||
if (!method_exists($this, $actionMethodName)) {
|
||||
throw new NoSuchActionException('An action "' . $actionMethodName . '" does not exist in controller "' . static::class . '".', 1186669086);
|
||||
}
|
||||
return $actionMethodName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the specified action method and passes the arguments.
|
||||
*
|
||||
* If the action returns a string, it is appended to the content in the
|
||||
* response object. If the action doesn't return anything and a valid
|
||||
* view exists, the view is rendered automatically.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function callActionMethod(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
// incoming request is not needed yet but can be passed into the action in the future like in symfony
|
||||
// todo: support this via method-reflection
|
||||
|
||||
$this->fileHandlingService->initializeFileUploadDeletionConfigurationsFromRequest($request, $this->arguments);
|
||||
$validationResult = $this->arguments->validate();
|
||||
if (!$validationResult->hasErrors()) {
|
||||
$preparedArguments = [];
|
||||
/** @var Argument $argument */
|
||||
foreach ($this->arguments as $argument) {
|
||||
$this->fileHandlingService->applyDeletionsToArgument($argument);
|
||||
$this->fileHandlingService->mapUploadedFilesToArgument($argument);
|
||||
$preparedArguments[] = $argument->getValue();
|
||||
}
|
||||
|
||||
if (($authorizeResponse = $this->performAuthorizationChecks($request, $preparedArguments)) !== null) {
|
||||
return $authorizeResponse;
|
||||
}
|
||||
|
||||
if (($rateLimitResponse = $this->handleRateLimit($request)) !== null) {
|
||||
return $rateLimitResponse;
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(new BeforeActionCallEvent(static::class, $this->actionMethodName, $preparedArguments, $this->request));
|
||||
$actionResult = $this->{$this->actionMethodName}(...$preparedArguments);
|
||||
} else {
|
||||
$actionResult = $this->{$this->errorMethodName}();
|
||||
}
|
||||
|
||||
if ($actionResult instanceof ResponseInterface) {
|
||||
return $actionResult;
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
'Controller action %s did not return an instance of %s.',
|
||||
static::class . '::' . $this->actionMethodName,
|
||||
ResponseInterface::class
|
||||
),
|
||||
1638554283
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a view for the current action.
|
||||
*
|
||||
* @internal
|
||||
* @todo We may want to decide in extbase to go away from the automatic view preparation via
|
||||
* processRequest() and this method for actions. We could very well postulate actions
|
||||
* should take care of creating "their" view on their own using a ViewFactoryInterface
|
||||
* implementation, similar to what is done with request creation already (which needs
|
||||
* further work, too), and have a helper in this class to easily create a standard view.
|
||||
* This would dissolve the ugly $this->defaultViewObjectName property, which is more
|
||||
* a burden than helpful since controllers then need to have an initializeFooAction()
|
||||
* just to set this property when different actions want different views. Also, it does
|
||||
* not allow actions to have no view prepared at all, for instance when they just want to
|
||||
* create a json response by json_encode()'ing stuff. We should look at this in v14, which
|
||||
* renders property defaultViewObjectName even more useless.
|
||||
*/
|
||||
protected function resolveView(): ViewInterface
|
||||
{
|
||||
if ($this->defaultViewObjectName !== null && is_a($this->defaultViewObjectName, JsonView::class, true)) {
|
||||
// @todo: JsonView is a very extbase specific thing. It comes with setVariablesToRender() and
|
||||
// setConfiguration(). We don't let it run through a factory here, since consumers need
|
||||
// to deal with these specialities anyways. Often, one would rather want to either have
|
||||
// an own view prepared in a controller (or action), or have a custom factory that deals
|
||||
// with stuff and returns a ViewInterface, or directly json_encode() data in an action.
|
||||
// This is related to the comment above, too.
|
||||
$view = new JsonView();
|
||||
$view->assign('settings', $this->settings);
|
||||
return $view;
|
||||
}
|
||||
$configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
$extensionKey = $this->request->getControllerExtensionKey();
|
||||
$templateRootPaths = $this->addDefaultPathToPaths($configuration['view']['templateRootPaths'] ?? [], 'EXT:' . $extensionKey . '/Resources/Private/Templates/');
|
||||
$layoutRootPaths = $this->addDefaultPathToPaths($configuration['view']['layoutRootPaths'] ?? [], 'EXT:' . $extensionKey . '/Resources/Private/Layouts/');
|
||||
$partialRootPaths = $this->addDefaultPathToPaths($configuration['view']['partialRootPaths'] ?? [], 'EXT:' . $extensionKey . '/Resources/Private/Partials/');
|
||||
if ($this->defaultViewObjectName === null) {
|
||||
$viewFactoryData = new ViewFactoryData(
|
||||
templateRootPaths: $templateRootPaths,
|
||||
partialRootPaths: $partialRootPaths,
|
||||
layoutRootPaths: $layoutRootPaths,
|
||||
request: $this->request,
|
||||
format: $this->request->getFormat(),
|
||||
);
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
if ($view instanceof FluidViewAdapter) {
|
||||
// This specific magic is tailored to Fluid. Ignore if we're not dealing with a fluid view here.
|
||||
$renderingContext = $view->getRenderingContext();
|
||||
$renderingContext->setControllerName($this->request->getControllerName());
|
||||
$renderingContext->setControllerAction($this->request->getControllerActionName());
|
||||
}
|
||||
$view->assign('settings', $this->settings);
|
||||
return $view;
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
'The only allowed values for $this->defaultViewObjectName are null or extbase JsonView::class.'
|
||||
. ' Please create an own view in your action if that is not sufficient, or inject a different'
|
||||
. ' ViewFactoryInterface',
|
||||
1729780151
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds extbase's default template path to the configured list of
|
||||
* template paths. The default path is usually used as a fallback if
|
||||
* no paths are specified or if the template cannot be found in any
|
||||
* of the configured paths. However, if the default path is already
|
||||
* present in the configured paths, the specified position takes
|
||||
* precedence. This allows the default path to be "moved" within
|
||||
* the list of paths via configuration.
|
||||
*
|
||||
* @return string[]
|
||||
* @internal
|
||||
*/
|
||||
protected function addDefaultPathToPaths(mixed $paths, string $defaultPath): array
|
||||
{
|
||||
if (!is_array($paths) || empty($paths)) {
|
||||
$paths = [$defaultPath];
|
||||
} else {
|
||||
$paths = ArrayUtility::sortArrayWithIntegerKeys($paths);
|
||||
if (!in_array($defaultPath, $paths)) {
|
||||
$paths = array_merge([$defaultPath], $paths);
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* A special action which is called if the originally intended action could
|
||||
* not be called, for example if the arguments were not valid.
|
||||
*
|
||||
* The default implementation sets a flash message, request errors and forwards back
|
||||
* to the originating action. This is suitable for most actions dealing with form input.
|
||||
*/
|
||||
protected function errorAction(): ResponseInterface
|
||||
{
|
||||
if (($response = $this->forwardToReferringRequest()) !== null) {
|
||||
if ($response instanceof ForwardResponse) {
|
||||
// Add flash messages to queue
|
||||
$this->addErrorFlashMessage();
|
||||
// Extract all pending flash messages out of th queue and ensure they
|
||||
// are passed along the response but without invoking the session.
|
||||
$flashMessages = $this->getFlashMessageQueue()->getAllMessagesAndFlush();
|
||||
$response = $response->withFlashMessages(...$flashMessages);
|
||||
}
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
$response = $this->htmlResponse($this->getFlattenedValidationErrorMessage());
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* If an error occurred during this request, this adds a flash message describing the error to the flash
|
||||
* message container.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function addErrorFlashMessage(): void
|
||||
{
|
||||
$errorFlashMessage = $this->getErrorFlashMessage();
|
||||
if (is_string($errorFlashMessage)) {
|
||||
$this->addFlashMessage($errorFlashMessage, '', ContextualFeedbackSeverity::ERROR, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A template method for displaying custom error flash messages, or to
|
||||
* display no flash message at all on errors. Override this to customize
|
||||
* the flash message in your action controller.
|
||||
*
|
||||
* Returns either the flash message or "false" if no flash message should be set
|
||||
*/
|
||||
protected function getErrorFlashMessage(): bool|string
|
||||
{
|
||||
return 'An error occurred while trying to call ' . static::class . '->' . $this->actionMethodName . '()';
|
||||
}
|
||||
|
||||
/**
|
||||
* If information on the request before the current request was sent, this method forwards back
|
||||
* to the originating request. This effectively ends processing of the current request, so do not
|
||||
* call this method before you have finished the necessary business logic!
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function forwardToReferringRequest(): ?ResponseInterface
|
||||
{
|
||||
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = $this->request->getAttribute('extbase');
|
||||
$referringRequestArguments = $extbaseRequestParameters->getInternalArgument('__referrer') ?? null;
|
||||
if (is_string($referringRequestArguments['@request'] ?? null)) {
|
||||
$referrerArray = json_decode(
|
||||
$this->hashService->validateAndStripHmac($referringRequestArguments['@request'], HashScope::ReferringRequest->prefix(), HashAlgo::SHA3_256),
|
||||
true
|
||||
);
|
||||
$arguments = [];
|
||||
if (is_string($referringRequestArguments['arguments'] ?? null)) {
|
||||
/* @phpstan-ignore unserialize.allowedClasses.insecure (Integrity check already happens via HMAC validation) */
|
||||
$arguments = unserialize(
|
||||
base64_decode($this->hashService->validateAndStripHmac(
|
||||
$referringRequestArguments['arguments'],
|
||||
HashScope::ReferringArguments->prefix(),
|
||||
HashAlgo::SHA3_256
|
||||
)),
|
||||
['allowed_classes' => true]
|
||||
);
|
||||
}
|
||||
$replacedArguments = array_replace_recursive($arguments, $referrerArray);
|
||||
$nonExtbaseBaseArguments = [];
|
||||
foreach ($replacedArguments as $argumentName => $argumentValue) {
|
||||
if (!is_string($argumentName) || $argumentName === '') {
|
||||
throw new InvalidArgumentNameException('Invalid argument name.', 1623940985);
|
||||
}
|
||||
if (str_starts_with($argumentName, '__')
|
||||
|| in_array($argumentName, ['@extension', '@subpackage', '@controller', '@action', '@format'], true)
|
||||
) {
|
||||
// Don't handle internalArguments here, not needed for forwardResponse()
|
||||
continue;
|
||||
}
|
||||
$nonExtbaseBaseArguments[$argumentName] = $argumentValue;
|
||||
}
|
||||
return (new ForwardResponse((string)($replacedArguments['@action'] ?? 'index')))
|
||||
->withControllerName((string)($replacedArguments['@controller'] ?? 'Standard'))
|
||||
->withExtensionName((string)($replacedArguments['@extension'] ?? ''))
|
||||
->withArguments($nonExtbaseBaseArguments)
|
||||
->withArgumentsValidationResult($this->arguments->validate());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with a basic error message about validation failure.
|
||||
* We may add all validation error messages to a log file in the future,
|
||||
* but for security reasons (@see #54074) we do not return these here.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function getFlattenedValidationErrorMessage(): string
|
||||
{
|
||||
return 'Validation failed while trying to call ' . static::class . '->' . $this->actionMethodName . '().' . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Message object and adds it to the FlashMessageQueue.
|
||||
*
|
||||
* @throws \InvalidArgumentException if the message body is no string
|
||||
* @see FlashMessage
|
||||
*/
|
||||
public function addFlashMessage(
|
||||
string $messageBody,
|
||||
string $messageTitle = '',
|
||||
ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::OK,
|
||||
bool $storeInSession = true
|
||||
): void {
|
||||
$flashMessage = new FlashMessage(
|
||||
$messageBody,
|
||||
$messageTitle,
|
||||
$severity,
|
||||
$storeInSession
|
||||
);
|
||||
|
||||
$this->getFlashMessageQueue()->enqueue($flashMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* todo: As soon as the incoming request contains the compiled plugin namespace, extbase will offer a trait to
|
||||
* create a flash message identifier from the current request. Users then should inject the flash message
|
||||
* service themselves if needed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function getFlashMessageQueue(?string $identifier = null): FlashMessageQueue
|
||||
{
|
||||
if ($identifier === null) {
|
||||
$pluginNamespace = $this->internalExtensionService->getPluginNamespace(
|
||||
$this->request->getControllerExtensionName(),
|
||||
$this->request->getPluginName()
|
||||
);
|
||||
$identifier = 'extbase.flashmessages.' . $pluginNamespace;
|
||||
}
|
||||
|
||||
return $this->internalFlashMessageService->getMessageQueueByIdentifier($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the request to another action and / or controller.
|
||||
*
|
||||
* Redirect will be sent to the client which then performs another request to the new URI.
|
||||
*
|
||||
* @param string|null $actionName Name of the action to forward to
|
||||
* @param string|null $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used.
|
||||
* @param string|null $extensionName Name of the extension containing the controller to forward to. If not specified, the current extension is assumed.
|
||||
* @param array|null $arguments Arguments to pass to the target action
|
||||
* @param int|null $pageUid Target page uid. If NULL, the current page uid is used
|
||||
* @param null $_ (optional) Unused
|
||||
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
|
||||
*/
|
||||
protected function redirect(
|
||||
?string $actionName,
|
||||
?string $controllerName = null,
|
||||
?string $extensionName = null,
|
||||
?array $arguments = null,
|
||||
?int $pageUid = null,
|
||||
$_ = null,
|
||||
int $statusCode = 303
|
||||
): ResponseInterface {
|
||||
if ($controllerName === null) {
|
||||
$controllerName = $this->request->getControllerName();
|
||||
}
|
||||
$this->uriBuilder->reset()->setCreateAbsoluteUri(true);
|
||||
if (MathUtility::canBeInterpretedAsInteger($pageUid)) {
|
||||
$this->uriBuilder->setTargetPageUid((int)$pageUid);
|
||||
}
|
||||
if ($this->request->getAttribute('normalizedParams')->isHttps()) {
|
||||
$this->uriBuilder->setAbsoluteUriScheme('https');
|
||||
}
|
||||
$uri = $this->uriBuilder->uriFor($actionName, $arguments, $controllerName, $extensionName);
|
||||
return $this->redirectToUri($uri, null, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the web request to another uri.
|
||||
*
|
||||
* @param string|UriInterface $uri A string representation of a URI
|
||||
* @param null $_ (optional) Unused
|
||||
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other"
|
||||
*/
|
||||
protected function redirectToUri(string|UriInterface $uri, $_ = null, int $statusCode = 303): ResponseInterface
|
||||
{
|
||||
$uri = $this->addBaseUriIfNecessary((string)$uri);
|
||||
return new RedirectResponse($uri, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the base uri if not already in place.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function addBaseUriIfNecessary(string $uri): string
|
||||
{
|
||||
return GeneralUtility::locationHeaderUrl($uri, $this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the specified HTTP status immediately and only stops to run back through the middleware stack.
|
||||
* Note: If any other plugin or content or hook is used within a frontend request, this is skipped by design.
|
||||
*
|
||||
* @param int $statusCode The HTTP status code
|
||||
* @param string $statusMessage A custom HTTP status message
|
||||
* @param string|null $content Body content which further explains the status
|
||||
* @throws PropagateResponseException
|
||||
*/
|
||||
public function throwStatus(int $statusCode, string $statusMessage = '', ?string $content = null): never
|
||||
{
|
||||
if ($content === null) {
|
||||
$content = $statusCode . ' ' . $statusMessage;
|
||||
}
|
||||
$response = $this->responseFactory
|
||||
->createResponse($statusCode, $statusMessage)
|
||||
->withBody($this->streamFactory->createStream((string)$content));
|
||||
throw new PropagateResponseException($response, 1476045871);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method processes exceptions that occur due to missing or not found targets or arguments during argument
|
||||
* mapping. Based on configuration settings, either a "page not found" response is triggered or the original
|
||||
* exception is propagated.
|
||||
*
|
||||
* Extension authors can override this function to implement additional/custom argument mapping exception handling
|
||||
*/
|
||||
protected function handleArgumentMappingExceptions(\Exception $exception): void
|
||||
{
|
||||
$configuration = $this->configurationManager->getConfiguration(
|
||||
ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
|
||||
);
|
||||
|
||||
$handleTargetNotFoundException = $exception instanceof TargetNotFoundException
|
||||
&& (bool)($configuration['mvc']['showPageNotFoundIfTargetNotFoundException'] ?? false);
|
||||
$handleRequiredArgumentMissingException = $exception instanceof RequiredArgumentMissingException
|
||||
&& (bool)($configuration['mvc']['showPageNotFoundIfRequiredArgumentIsMissingException'] ?? false);
|
||||
|
||||
if ($handleTargetNotFoundException || $handleRequiredArgumentMissingException) {
|
||||
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$this->request,
|
||||
$exception->getMessage()
|
||||
);
|
||||
throw new PropagateResponseException($response, 1720242346);
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps arguments delivered by the request object to the local controller arguments.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function mapRequestArgumentsToControllerArguments(): void
|
||||
{
|
||||
try {
|
||||
/** @var Argument $argument */
|
||||
foreach ($this->arguments as $argument) {
|
||||
$argumentName = $argument->getName();
|
||||
if ($this->request->hasArgument($argumentName)) {
|
||||
$this->setArgumentValue($argument, $this->request->getArgument($argumentName));
|
||||
} elseif ($argument->isRequired()) {
|
||||
throw new RequiredArgumentMissingException('Required argument "' . $argumentName . '" is not set for ' . $this->request->getControllerObjectName() . '->' . $this->request->getControllerActionName() . '.', 1298012500);
|
||||
}
|
||||
|
||||
if ($this->request->getMethod() === 'POST') {
|
||||
$uploadedFiles = $this->request->getUploadedFiles()[$argumentName] ?? [];
|
||||
$argument->setUploadedFiles($uploadedFiles);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
$this->handleArgumentMappingExceptions($exception);
|
||||
}
|
||||
}
|
||||
|
||||
private function setArgumentValue(Argument $argument, mixed $rawValue): void
|
||||
{
|
||||
if ($rawValue === null) {
|
||||
$argument->setValue(null);
|
||||
return;
|
||||
}
|
||||
$dataType = $argument->getDataType();
|
||||
if ($rawValue instanceof $dataType) {
|
||||
$argument->setValue($rawValue);
|
||||
return;
|
||||
}
|
||||
$this->propertyMapper->resetMessages();
|
||||
try {
|
||||
$argument->setValue(
|
||||
$this->propertyMapper->convert(
|
||||
$rawValue,
|
||||
$dataType,
|
||||
$argument->getPropertyMappingConfiguration()
|
||||
)
|
||||
);
|
||||
} catch (TargetNotFoundException $e) {
|
||||
// for optional arguments no exception is thrown.
|
||||
if ($argument->isRequired()) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$argument->getValidationResults()->merge($this->propertyMapper->getMessages());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a response object with either the given html string or the current rendered view as content.
|
||||
*/
|
||||
protected function htmlResponse(?string $html = null): ResponseInterface
|
||||
{
|
||||
return $this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
->withBody($this->streamFactory->createStream(($html ?? $this->view->render())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a response object with either the given json string or the current rendered
|
||||
* view as content. Mainly to be used for actions / controllers using the JsonView.
|
||||
*/
|
||||
protected function jsonResponse(?string $json = null): ResponseInterface
|
||||
{
|
||||
return $this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
->withBody($this->streamFactory->createStream(($json ?? $this->view->render())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles rate-limiting for the given action request. Checks if the current request exceeds
|
||||
* a possible defined rate limit for the action method and generates an appropriate response
|
||||
* if the limit is reached.
|
||||
*
|
||||
* @internal
|
||||
* @return ResponseInterface|null The rate-limited response if the limit is exceeded, or null if no rate-limiting applies.
|
||||
*/
|
||||
protected function handleRateLimit(RequestInterface $request): ?ResponseInterface
|
||||
{
|
||||
$rateLimiter = $this->rateLimitRegistry->createLimiter(static::class, $this->actionMethodName, $this->request);
|
||||
if ($rateLimiter === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rateLimit = $this->rateLimitRegistry->getRateLimit(static::class, $this->actionMethodName);
|
||||
$limit = $rateLimiter->consume();
|
||||
if ($limit->isAccepted()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customMessage = null;
|
||||
if ($rateLimit->message !== '') {
|
||||
$customMessage = LocalizationUtility::translate($rateLimit->message, $this->request->getControllerExtensionName());
|
||||
}
|
||||
$message = $customMessage ?? LocalizationUtility::translate('ratelimit.action.defaultmessage', 'extbase');
|
||||
|
||||
$response = $this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
->withStatus(429)
|
||||
->withBody($this->streamFactory->createStream($message));
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeActionRateLimitResponseEvent($request, static::class, $this->actionMethodName, $rateLimit, $response)
|
||||
);
|
||||
|
||||
return $event->getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs authorization checks for actions with the #[Authorize] attribute. If access is denied, a HTTP 403
|
||||
* response is propagated. This behavior can be customized by implementing a event listener for the
|
||||
* {@see BeforeActionAuthorizationDeniedEvent}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function performAuthorizationChecks(RequestInterface $request, array $preparedArguments): ?ResponseInterface
|
||||
{
|
||||
$result = $this->authorizeRegistry->checkAuthorization($this, $this->actionMethodName, $preparedArguments);
|
||||
|
||||
if ($result === null || $result->isAllowed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$message = match ($result->failureReason) {
|
||||
AuthorizationFailureReason::NOT_LOGGED_IN => 'Access denied: Login required',
|
||||
AuthorizationFailureReason::MISSING_GROUP => 'Access denied: Insufficient permissions',
|
||||
AuthorizationFailureReason::CALLBACK_DENIED, null => 'Access denied',
|
||||
};
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeActionAuthorizationDeniedEvent(
|
||||
$request,
|
||||
static::class,
|
||||
$this->actionMethodName,
|
||||
$result->failedAttribute,
|
||||
$result->failureReason,
|
||||
)
|
||||
);
|
||||
|
||||
if (!$event->getResponse()) {
|
||||
$response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
|
||||
$this->request,
|
||||
$message,
|
||||
[
|
||||
'code' => PageAccessFailureReasons::ACCESS_DENIED_GENERAL,
|
||||
]
|
||||
);
|
||||
throw new PropagateResponseException($response, 1761287264);
|
||||
}
|
||||
|
||||
return $event->getResponse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use TYPO3\CMS\Core\Http\UploadedFile;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* A controller argument
|
||||
*/
|
||||
class Argument
|
||||
{
|
||||
protected MvcPropertyMappingConfiguration $propertyMappingConfiguration;
|
||||
protected FileHandlingServiceConfiguration $fileHandlingServiceConfiguration;
|
||||
protected string $name = '';
|
||||
protected string $shortName = '';
|
||||
protected string $dataType = '';
|
||||
protected bool $isRequired = false;
|
||||
protected mixed $value = null;
|
||||
private bool $hasBeenValidated = false;
|
||||
|
||||
/**
|
||||
* Uploaded files for the argument
|
||||
* @var array<string, UploadedFileInterface|list<UploadedFileInterface>>
|
||||
*/
|
||||
protected array $uploadedFiles = [];
|
||||
|
||||
/**
|
||||
* Default value. Used if argument is optional.
|
||||
*/
|
||||
protected mixed $defaultValue = null;
|
||||
|
||||
/**
|
||||
* A custom validator, used supplementary to the base validation
|
||||
*/
|
||||
protected ?ValidatorInterface $validator = null;
|
||||
|
||||
/**
|
||||
* The validation results. This can be asked if the argument has errors.
|
||||
*/
|
||||
protected Result $validationResults;
|
||||
|
||||
/**
|
||||
* Constructs this controller argument
|
||||
*
|
||||
* @throws \InvalidArgumentException if $name is empty string
|
||||
*/
|
||||
public function __construct(string $name, string $dataType)
|
||||
{
|
||||
if ($name === '') {
|
||||
throw new \InvalidArgumentException('$name must be a non-empty string.', 1232551853);
|
||||
}
|
||||
$this->name = $name;
|
||||
$this->dataType = TypeHandlingUtility::normalizeType($dataType);
|
||||
|
||||
$this->validationResults = new Result();
|
||||
$this->propertyMappingConfiguration = GeneralUtility::makeInstance(MvcPropertyMappingConfiguration::class);
|
||||
$this->fileHandlingServiceConfiguration = GeneralUtility::makeInstance(FileHandlingServiceConfiguration::class);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException if $shortName is not a character
|
||||
*/
|
||||
public function setShortName(string $shortName): Argument
|
||||
{
|
||||
if (strlen($shortName) !== 1) {
|
||||
throw new \InvalidArgumentException('$shortName must be a single character or NULL', 1195824959);
|
||||
}
|
||||
$this->shortName = $shortName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getShortName(): string
|
||||
{
|
||||
return $this->shortName;
|
||||
}
|
||||
|
||||
public function getDataType(): string
|
||||
{
|
||||
return $this->dataType;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): Argument
|
||||
{
|
||||
$this->isRequired = $required;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->isRequired;
|
||||
}
|
||||
|
||||
public function setDefaultValue(mixed $defaultValue): Argument
|
||||
{
|
||||
$this->defaultValue = $defaultValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefaultValue(): mixed
|
||||
{
|
||||
return $this->defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom validator which is used supplementary to the base validation
|
||||
*/
|
||||
public function setValidator(ValidatorInterface $validator): Argument
|
||||
{
|
||||
$this->validator = $validator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidator(): ?ValidatorInterface
|
||||
{
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
public function setValue(mixed $rawValue): Argument
|
||||
{
|
||||
$this->value = $rawValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): mixed
|
||||
{
|
||||
if ($this->value === null) {
|
||||
return $this->defaultValue;
|
||||
}
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Property Mapping Configuration used for this argument; can be used by the initialize*action to modify the Property Mapping.
|
||||
*/
|
||||
public function getPropertyMappingConfiguration(): MvcPropertyMappingConfiguration
|
||||
{
|
||||
return $this->propertyMappingConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FileHandlingServiceConfiguration used for this argument; can be used by the
|
||||
* initialize*action to modify the file upload configuration for properties.
|
||||
*/
|
||||
public function getFileHandlingServiceConfiguration(): FileHandlingServiceConfiguration
|
||||
{
|
||||
return $this->fileHandlingServiceConfiguration;
|
||||
}
|
||||
|
||||
public function getUploadedFiles(): array
|
||||
{
|
||||
return $this->uploadedFiles;
|
||||
}
|
||||
|
||||
public function setUploadedFiles(array $uploadedFiles): void
|
||||
{
|
||||
$this->uploadedFiles = $uploadedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool TRUE if the argument is valid, FALSE otherwise
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return !$this->validate()->hasErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this argument's value
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string)$this->value;
|
||||
}
|
||||
|
||||
public function validate(): Result
|
||||
{
|
||||
if ($this->hasBeenValidated) {
|
||||
return $this->validationResults;
|
||||
}
|
||||
|
||||
if ($this->validator !== null) {
|
||||
$validationMessages = $this->validator->validate($this->value);
|
||||
$this->validationResults->merge($validationMessages);
|
||||
}
|
||||
|
||||
if ($this->fileHandlingServiceConfiguration->hasfileUploadConfigurations()) {
|
||||
$fileOperationValidationResults = $this->fileHandlingServiceConfiguration->validateFileOperations($this);
|
||||
$this->validationResults->merge($fileOperationValidationResults);
|
||||
}
|
||||
|
||||
$this->hasBeenValidated = true;
|
||||
return $this->validationResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of possible UploadedFile objects for the given property
|
||||
* @return list<UploadedFileInterface>
|
||||
*/
|
||||
public function getUploadedFilesForProperty(string $propertyName): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
try {
|
||||
$uploadedFiles = ArrayUtility::getValueByPath($this->uploadedFiles, $propertyName, '.');
|
||||
if ($uploadedFiles instanceof UploadedFile) {
|
||||
$result = [$uploadedFiles];
|
||||
} elseif (is_iterable($uploadedFiles)) {
|
||||
foreach ($uploadedFiles as $uploadedFile) {
|
||||
$result[] = $uploadedFile;
|
||||
}
|
||||
}
|
||||
} catch (MissingArrayPathException) {
|
||||
// Do nothing, empty array will be returned
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getValidationResults(): Result
|
||||
{
|
||||
return $this->validationResults;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
|
||||
|
||||
/**
|
||||
* A composite of controller arguments
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class Arguments extends \ArrayObject
|
||||
{
|
||||
protected array $argumentNames = [];
|
||||
protected array $argumentShortNames = [];
|
||||
|
||||
/**
|
||||
* Constructor. If this one is removed, reflection breaks.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or replaces the argument specified by $value. The argument's name is taken from the
|
||||
* argument object itself, therefore the $offset does not have any meaning in this context.
|
||||
*
|
||||
* @param mixed $offset Offset - not used here
|
||||
* @throws \InvalidArgumentException if the argument is not a valid Controller Argument object
|
||||
*/
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
if (!$value instanceof Argument) {
|
||||
throw new \InvalidArgumentException('Controller arguments must be valid TYPO3\\CMS\\Extbase\\Mvc\\Controller\\Argument objects.', 1187953786);
|
||||
}
|
||||
$argumentName = $value->getName();
|
||||
parent::offsetSet($argumentName, $value);
|
||||
$this->argumentNames[$argumentName] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an argument, aliased to offsetSet()
|
||||
*
|
||||
* @throws \InvalidArgumentException if the argument is not a valid Controller Argument object
|
||||
*/
|
||||
public function append(mixed $value): void
|
||||
{
|
||||
if (!$value instanceof Argument) {
|
||||
throw new \InvalidArgumentException('Controller arguments must be valid TYPO3\\CMS\\Extbase\\Mvc\\Controller\\Argument objects.', 1187953787);
|
||||
}
|
||||
$this->offsetSet(null, $value);
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
$translatedOffset = $this->translateToLongArgumentName($offset);
|
||||
parent::offsetUnset($translatedOffset);
|
||||
unset($this->argumentNames[$translatedOffset]);
|
||||
if ($offset != $translatedOffset) {
|
||||
unset($this->argumentShortNames[$offset]);
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
$translatedOffset = $this->translateToLongArgumentName($offset);
|
||||
return parent::offsetExists($translatedOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified index
|
||||
*
|
||||
* @throws NoSuchArgumentException if the argument does not exist
|
||||
*/
|
||||
public function offsetGet(mixed $offset): Argument
|
||||
{
|
||||
$translatedOffset = $this->translateToLongArgumentName($offset);
|
||||
if ($translatedOffset === '') {
|
||||
throw new NoSuchArgumentException('The argument "' . $offset . '" does not exist.', 1216909923);
|
||||
}
|
||||
return parent::offsetGet($translatedOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates, adds and returns a new controller argument to this composite object.
|
||||
* If an argument with the same name exists already, it will be replaced by the
|
||||
* new argument object.
|
||||
*/
|
||||
public function addNewArgument(string $name, string $dataType = 'Text', bool $isRequired = false, mixed $defaultValue = null): Argument
|
||||
{
|
||||
$argument = GeneralUtility::makeInstance(Argument::class, $name, $dataType);
|
||||
$argument->setRequired($isRequired);
|
||||
$argument->setDefaultValue($defaultValue);
|
||||
$this->addArgument($argument);
|
||||
return $argument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified controller argument to this composite object.
|
||||
* If an argument with the same name exists already, it will be replaced by the
|
||||
* new argument object.
|
||||
*
|
||||
* Note that the argument will be cloned, not referenced.
|
||||
*/
|
||||
public function addArgument(Argument $argument): void
|
||||
{
|
||||
$this->offsetSet(null, $argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an argument specified by name
|
||||
*
|
||||
* @throws NoSuchArgumentException
|
||||
*/
|
||||
public function getArgument(string $argumentName): Argument
|
||||
{
|
||||
if (!$this->offsetExists($argumentName)) {
|
||||
throw new NoSuchArgumentException('An argument "' . $argumentName . '" does not exist.', 1195815178);
|
||||
}
|
||||
return $this->offsetGet($argumentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an argument with the specified name exists
|
||||
*
|
||||
* @see offsetExists()
|
||||
*/
|
||||
public function hasArgument(string $argumentName): bool
|
||||
{
|
||||
return $this->offsetExists($argumentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of all arguments contained in this object
|
||||
*/
|
||||
public function getArgumentNames(): array
|
||||
{
|
||||
return array_keys($this->argumentNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the short names of all arguments contained in this object that have one.
|
||||
*/
|
||||
public function getArgumentShortNames(): array
|
||||
{
|
||||
$argumentShortNames = [];
|
||||
/** @var Argument $argument */
|
||||
foreach ($this as $argument) {
|
||||
$argumentShortNames[$argument->getShortName()] = true;
|
||||
}
|
||||
return array_keys($argumentShortNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic setter method for the argument values. Each argument
|
||||
* value can be set by just calling the setArgumentName() method.
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function __call(string $methodName, array $arguments): void
|
||||
{
|
||||
if (!str_starts_with($methodName, 'set')) {
|
||||
throw new \LogicException('Unknown method "' . $methodName . '".', 1210858451);
|
||||
}
|
||||
$firstLowerCaseArgumentName = $this->translateToLongArgumentName(strtolower($methodName[3]) . substr($methodName, 4));
|
||||
$firstUpperCaseArgumentName = $this->translateToLongArgumentName(ucfirst(substr($methodName, 3)));
|
||||
if (in_array($firstLowerCaseArgumentName, $this->getArgumentNames())) {
|
||||
$argument = parent::offsetGet($firstLowerCaseArgumentName);
|
||||
$argument->setValue($arguments[0]);
|
||||
} elseif (in_array($firstUpperCaseArgumentName, $this->getArgumentNames())) {
|
||||
$argument = parent::offsetGet($firstUpperCaseArgumentName);
|
||||
$argument->setValue($arguments[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a short argument name to its corresponding long name. If the
|
||||
* specified argument name is a real argument name already, it will be returned again.
|
||||
*
|
||||
* If an argument with the specified name or short name does not exist, an empty
|
||||
* string is returned.
|
||||
*/
|
||||
protected function translateToLongArgumentName(string $argumentName): string
|
||||
{
|
||||
if (in_array($argumentName, $this->getArgumentNames())) {
|
||||
return $argumentName;
|
||||
}
|
||||
/** @var Argument $argument */
|
||||
foreach ($this as $argument) {
|
||||
if ($argumentName === $argument->getShortName()) {
|
||||
return $argument->getName();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all arguments and resets this object
|
||||
*/
|
||||
public function removeAll(): void
|
||||
{
|
||||
foreach ($this->argumentNames as $argumentName => $booleanValue) {
|
||||
parent::offsetUnset($argumentName);
|
||||
}
|
||||
$this->argumentNames = [];
|
||||
}
|
||||
|
||||
public function validate(): Result
|
||||
{
|
||||
$results = new Result();
|
||||
/** @var Argument $argument */
|
||||
foreach ($this as $argument) {
|
||||
$argumentValidationResults = $argument->validate();
|
||||
$results->forProperty($argument->getName())->merge($argumentValidationResults);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Extbase\Attribute\Authorize;
|
||||
use TYPO3\CMS\Extbase\Authorization\AuthorizationResult;
|
||||
use TYPO3\CMS\Extbase\Service\ActionAuthorizationService;
|
||||
|
||||
/**
|
||||
* Registry for authorization configurations of extbase controller actions,
|
||||
* populated at compile time via {@see \TYPO3\CMS\Extbase\DependencyInjection\AuthorizePass}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class AuthorizeRegistry
|
||||
{
|
||||
/** @var array<string, array<string, list<Authorize>>> */
|
||||
private array $authorizations = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly ActionAuthorizationService $authorizationService,
|
||||
) {}
|
||||
|
||||
public function add(string $controllerClass, string $actionMethod, string|array|null $callback, bool $requireLogin, array $requireGroups): void
|
||||
{
|
||||
$this->authorizations[$controllerClass][$actionMethod][] = new Authorize($callback, $requireLogin, $requireGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Authorize>
|
||||
*/
|
||||
public function getAuthorizeAttributes(string $controllerClass, string $actionMethod): array
|
||||
{
|
||||
return $this->authorizations[$controllerClass][$actionMethod] ?? [];
|
||||
}
|
||||
|
||||
public function checkAuthorization(ActionController $controller, string $actionMethod, array $preparedArguments): ?AuthorizationResult
|
||||
{
|
||||
$authorizeAttributes = $this->getAuthorizeAttributes($controller::class, $actionMethod);
|
||||
if ($authorizeAttributes === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->authorizationService->checkAuthorization($controller, $authorizeAttributes, $preparedArguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
|
||||
/**
|
||||
* Interface for controllers
|
||||
*/
|
||||
interface ControllerInterface
|
||||
{
|
||||
/**
|
||||
* Processes a general request. The result can be returned by altering the given response.
|
||||
*
|
||||
* @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request The request object
|
||||
*/
|
||||
public function processRequest(RequestInterface $request): ResponseInterface;
|
||||
}
|
||||
@@ -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\Extbase\Mvc\Controller\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Argument Name" exception
|
||||
*/
|
||||
class RequiredArgumentMissingException extends Exception {}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||
use TYPO3\CMS\Extbase\Error\Error;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
|
||||
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\FileExtensionMimeTypeConsistencyValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\FileNameValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @internal Only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class FileHandlingServiceConfiguration
|
||||
{
|
||||
/**
|
||||
* @var ObjectStorage<FileUploadConfiguration>
|
||||
*/
|
||||
protected ObjectStorage $fileUploadConfigurations;
|
||||
|
||||
/**
|
||||
* @var ObjectStorage<FileUploadDeletionConfiguration>
|
||||
*/
|
||||
protected ObjectStorage $fileUploadDeletionConfigurations;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->fileUploadConfigurations = new ObjectStorage();
|
||||
$this->fileUploadDeletionConfigurations = new ObjectStorage();
|
||||
}
|
||||
|
||||
public function addFileUploadConfiguration(FileUploadConfiguration $configuration): void
|
||||
{
|
||||
$this->fileUploadConfigurations->attach($configuration);
|
||||
}
|
||||
|
||||
public function getFileUploadConfigurations(): ObjectStorage
|
||||
{
|
||||
return $this->fileUploadConfigurations;
|
||||
}
|
||||
|
||||
public function hasfileUploadConfigurations(): bool
|
||||
{
|
||||
return $this->fileUploadConfigurations->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FileUploadConfiguration for the given propertyName
|
||||
*/
|
||||
public function getFileUploadConfigurationForProperty(string $propertyName): ?FileUploadConfiguration
|
||||
{
|
||||
foreach ($this->fileUploadConfigurations as $configuration) {
|
||||
if ($configuration->getPropertyName() === $propertyName) {
|
||||
return $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a file deletion for the given property and file reference uid
|
||||
*/
|
||||
public function registerFileDeletion(string $property, int $fileReferenceUid): void
|
||||
{
|
||||
$fileUploadDeletionConfiguration = $this->getFileUploadDeletionConfigurationForProperty($property);
|
||||
if (!$fileUploadDeletionConfiguration) {
|
||||
$fileUploadDeletionConfiguration = GeneralUtility::makeInstance(FileUploadDeletionConfiguration::class, $property);
|
||||
$this->fileUploadDeletionConfigurations->attach($fileUploadDeletionConfiguration);
|
||||
}
|
||||
$fileUploadDeletionConfiguration->addFileReferenceUid($fileReferenceUid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all file deletion configurations
|
||||
*/
|
||||
public function getFileUploadDeletionConfigurations(): ObjectStorage
|
||||
{
|
||||
return $this->fileUploadDeletionConfigurations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FileUploadDeletionConfiguration for the given propertyName
|
||||
*/
|
||||
public function getFileUploadDeletionConfigurationForProperty(string $propertyName): ?FileUploadDeletionConfiguration
|
||||
{
|
||||
foreach ($this->fileUploadDeletionConfigurations as $configuration) {
|
||||
if ($configuration->getPropertyName() === $propertyName) {
|
||||
return $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of configured file deletions for the given property
|
||||
*/
|
||||
private function getFileUploadDeletionCountForProperty(string $propertyName): int
|
||||
{
|
||||
$fileUploadDeletionConfiguration = $this->getFileUploadDeletionConfigurationForProperty($propertyName);
|
||||
if ($fileUploadDeletionConfiguration) {
|
||||
return count($fileUploadDeletionConfiguration->getFileReferenceUids());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file operations for the given argument by checking file upload and file deletion configurations and
|
||||
* returning the validation result.
|
||||
*/
|
||||
public function validateFileOperations(Argument $argument): Result
|
||||
{
|
||||
$validationResults = new Result();
|
||||
$value = $argument->getValue();
|
||||
|
||||
foreach ($this->fileUploadConfigurations as $configuration) {
|
||||
$uploadedFilesForProperty = $argument->getUploadedFilesForProperty(
|
||||
$configuration->getPropertyName()
|
||||
);
|
||||
$fileDeletionCount = $this->getFileUploadDeletionCountForProperty($configuration->getPropertyName());
|
||||
$currentPropertyValue = null;
|
||||
if ($value) {
|
||||
$currentPropertyValue = ObjectAccess::getPropertyPath($value, $configuration->getPropertyName());
|
||||
}
|
||||
$validationResult = $this->getValidationResultsForProperty(
|
||||
$configuration,
|
||||
$configuration->getPropertyName(),
|
||||
$currentPropertyValue,
|
||||
$uploadedFilesForProperty,
|
||||
$fileDeletionCount
|
||||
);
|
||||
$validationResults->merge($validationResult);
|
||||
}
|
||||
|
||||
return $validationResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file uploads and file deletions for the given propertyPath and currentPropertyValue and returns
|
||||
* the validation result.
|
||||
*/
|
||||
private function getValidationResultsForProperty(
|
||||
FileUploadConfiguration $configuration,
|
||||
string $propertyPath,
|
||||
mixed $currentPropertyValue,
|
||||
array $uploadedFiles,
|
||||
int $fileDeletionCount
|
||||
): Result {
|
||||
$validationResults = new Result();
|
||||
|
||||
if ($currentPropertyValue instanceof FileReference) {
|
||||
$currentAmount = 1;
|
||||
} elseif ($currentPropertyValue instanceof ObjectStorage) {
|
||||
$currentAmount = $currentPropertyValue->count();
|
||||
} else {
|
||||
$currentAmount = 0;
|
||||
}
|
||||
|
||||
// Validate, that minimum files requirement is valid after file deletion(s)
|
||||
if ($fileDeletionCount > 0
|
||||
&& ($currentPropertyValue instanceof FileReference || $currentPropertyValue instanceof ObjectStorage)
|
||||
) {
|
||||
$newAmount = $currentAmount - $fileDeletionCount + count($uploadedFiles);
|
||||
if ($newAmount < $configuration->getMinFiles()) {
|
||||
$minFilesError = new Error(
|
||||
$this->translateErrorMessage(
|
||||
'filehandlingserviceconfiguration.minfiles.delete.notvalid',
|
||||
'extbase',
|
||||
),
|
||||
1714557062
|
||||
);
|
||||
$validationResults->forProperty($propertyPath)
|
||||
->addError($minFilesError);
|
||||
}
|
||||
}
|
||||
|
||||
// If the given $currentPropertyValue (which is the target property for file upload) is either a FileReference
|
||||
// or a non empty ObjectStorage and no uploaded files are available, the rest of the validation can be skipped.
|
||||
if ($uploadedFiles === []
|
||||
&& ($currentPropertyValue instanceof FileReference
|
||||
|| ($currentPropertyValue instanceof ObjectStorage && $currentPropertyValue->count() > 0))
|
||||
) {
|
||||
return $validationResults;
|
||||
}
|
||||
|
||||
if (count($uploadedFiles) < $configuration->getMinFiles()) {
|
||||
$minFilesError = new Error(
|
||||
$this->translateErrorMessage(
|
||||
'filehandlingserviceconfiguration.minfiles.notvalid',
|
||||
'extbase',
|
||||
[$configuration->getMinFiles()]
|
||||
),
|
||||
1708596527
|
||||
);
|
||||
$validationResults->forProperty($propertyPath)
|
||||
->addError($minFilesError);
|
||||
}
|
||||
|
||||
if ((count($uploadedFiles) + $currentAmount - $fileDeletionCount) > $configuration->getMaxFiles()) {
|
||||
$minFilesError = new Error(
|
||||
$this->translateErrorMessage(
|
||||
'filehandlingserviceconfiguration.maxfiles.notvalid',
|
||||
'extbase',
|
||||
[$configuration->getMaxFiles()]
|
||||
),
|
||||
1708596528
|
||||
);
|
||||
$validationResults->forProperty($propertyPath)
|
||||
->addError($minFilesError);
|
||||
}
|
||||
|
||||
$validators = $this->enforceDefaultValidators(
|
||||
...$configuration->getValidators()
|
||||
);
|
||||
foreach ($validators as $validator) {
|
||||
foreach ($uploadedFiles as $uploadedFile) {
|
||||
$validatorResult = $validator->validate($uploadedFile);
|
||||
if ($validatorResult->hasErrors()) {
|
||||
$validationResults->forProperty($propertyPath)->merge($validatorResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $validationResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<ValidatorInterface>
|
||||
*/
|
||||
private function enforceDefaultValidators(ValidatorInterface ...$validators): array
|
||||
{
|
||||
$enforceValidators = [
|
||||
FileNameValidator::class,
|
||||
FileExtensionMimeTypeConsistencyValidator::class,
|
||||
];
|
||||
$existingValidators = array_map(get_class(...), $validators);
|
||||
$missingValidators = array_diff($enforceValidators, $existingValidators);
|
||||
foreach ($missingValidators as $missingValidator) {
|
||||
$validators[] = GeneralUtility::makeInstance($missingValidator);
|
||||
}
|
||||
return $validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to translate error messages
|
||||
*/
|
||||
private function translateErrorMessage(string $translateKey, string $extensionName, array $arguments = []): string
|
||||
{
|
||||
return LocalizationUtility::translate(
|
||||
$translateKey,
|
||||
$extensionName,
|
||||
$arguments
|
||||
) ?? '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\FileExtensionValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\FileSizeValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ImageDimensionsValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\MimeTypeValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @internal Only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class FileUploadConfiguration
|
||||
{
|
||||
protected string $uploadFolder = '';
|
||||
protected int $minFiles = 0;
|
||||
protected int $maxFiles = PHP_INT_MAX;
|
||||
protected bool $addRandomSuffix = true;
|
||||
protected bool $createUploadFolderIfNotExist = true;
|
||||
protected DuplicationBehavior $duplicationBehavior = DuplicationBehavior::RENAME;
|
||||
|
||||
/**
|
||||
* @var array<ValidatorInterface>
|
||||
*/
|
||||
protected array $validators = [];
|
||||
|
||||
public function __construct(protected readonly string $propertyName) {}
|
||||
|
||||
/**
|
||||
* Initializes the object with the given configuration array. Typically used with configuration from
|
||||
* #[FileUpload] attribute.
|
||||
*/
|
||||
public function initializeWithConfiguration(array $configuration): self
|
||||
{
|
||||
if (!isset($configuration['validation']) || $configuration['validation'] === []) {
|
||||
throw new \RuntimeException('Extbase file upload must at least define one validation rule.', 1711947120);
|
||||
}
|
||||
|
||||
$this->initializeUploadValidation($configuration['validation']);
|
||||
|
||||
if (isset($configuration['uploadFolder']) && $configuration['uploadFolder'] !== '') {
|
||||
$this->uploadFolder = $configuration['uploadFolder'];
|
||||
}
|
||||
|
||||
if (isset($configuration['addRandomSuffix'])) {
|
||||
$this->addRandomSuffix = (bool)$configuration['addRandomSuffix'];
|
||||
}
|
||||
|
||||
if (isset($configuration['duplicationBehavior'])) {
|
||||
$this->duplicationBehavior = $configuration['duplicationBehavior'];
|
||||
}
|
||||
|
||||
if (isset($configuration['createUploadFolderIfNotExist'])) {
|
||||
$this->createUploadFolderIfNotExist = $configuration['createUploadFolderIfNotExist'];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addValidator(ValidatorInterface $validator): self
|
||||
{
|
||||
$this->validators[] = $validator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidators(): array
|
||||
{
|
||||
return $this->validators;
|
||||
}
|
||||
|
||||
public function resetValidators(): self
|
||||
{
|
||||
$this->validators = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPropertyName(): string
|
||||
{
|
||||
return $this->propertyName;
|
||||
}
|
||||
|
||||
public function setRequired(): self
|
||||
{
|
||||
$this->minFiles = 1;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinFiles(): int
|
||||
{
|
||||
return $this->minFiles;
|
||||
}
|
||||
|
||||
public function setMinFiles(int $minFiles): self
|
||||
{
|
||||
$this->minFiles = $minFiles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxFiles(): int
|
||||
{
|
||||
return $this->maxFiles;
|
||||
}
|
||||
|
||||
public function setMaxFiles(int $maxFiles): self
|
||||
{
|
||||
$this->maxFiles = $maxFiles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUploadFolder(): string
|
||||
{
|
||||
return $this->uploadFolder;
|
||||
}
|
||||
|
||||
public function setUploadFolder(string $uploadFolder): self
|
||||
{
|
||||
$this->uploadFolder = $uploadFolder;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAddRandomSuffix(): bool
|
||||
{
|
||||
return $this->addRandomSuffix;
|
||||
}
|
||||
|
||||
public function setAddRandomSuffix(bool $addRandomSuffix): self
|
||||
{
|
||||
$this->addRandomSuffix = $addRandomSuffix;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isCreateUploadFolderIfNotExist(): bool
|
||||
{
|
||||
return $this->createUploadFolderIfNotExist;
|
||||
}
|
||||
|
||||
public function setCreateUploadFolderIfNotExist(bool $createUploadFolderIfNotExist): self
|
||||
{
|
||||
$this->createUploadFolderIfNotExist = $createUploadFolderIfNotExist;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDuplicationBehavior(): DuplicationBehavior
|
||||
{
|
||||
return $this->duplicationBehavior;
|
||||
}
|
||||
|
||||
public function setDuplicationBehavior(DuplicationBehavior $duplicationBehavior): void
|
||||
{
|
||||
$this->duplicationBehavior = $duplicationBehavior;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current configuration is considered valid for the given target type and throws
|
||||
* an exception, if the configuration is invalid.
|
||||
*/
|
||||
public function ensureValidConfiguration(string $targetType): void
|
||||
{
|
||||
if ($targetType !== FileReference::class) {
|
||||
throw new \RuntimeException('The FileUploadConfiguration can only be used for properties of type FileReference.', 1721623184);
|
||||
}
|
||||
|
||||
if (str_contains($this->getPropertyName(), '.')) {
|
||||
throw new \RuntimeException('The property name for the FileUploadConfiguration must not contain any dot.', 1724585391);
|
||||
}
|
||||
|
||||
if ($this->getUploadFolder() === '') {
|
||||
throw new \RuntimeException('An upload folder must be defined for the FileUploadConfiguration.', 1711799735);
|
||||
}
|
||||
|
||||
if (!$this->isCombinedStoragePathIdentifier($this->getUploadFolder())) {
|
||||
throw new \RuntimeException('The upload folder must be a combined identifier - e.g. 1:/user_upload/', 1711801071);
|
||||
}
|
||||
|
||||
if ($this->getMaxFiles() < $this->getMinFiles()) {
|
||||
throw new \RuntimeException('Maximum number of files cannot be less than minimum number of files.', 1711799765);
|
||||
}
|
||||
}
|
||||
|
||||
private function isCombinedStoragePathIdentifier(string $identifier): bool
|
||||
{
|
||||
return str_contains($identifier, ':')
|
||||
&& !str_starts_with($identifier, ':')
|
||||
&& !str_ends_with($identifier, ':')
|
||||
&& MathUtility::canBeInterpretedAsInteger(substr($identifier, 0, strpos($identifier, ':')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes validators based on the given array of validation configuration
|
||||
*/
|
||||
private function initializeUploadValidation(array $validationConfiguration): void
|
||||
{
|
||||
if ($validationConfiguration['required'] ?? false) {
|
||||
$this->minFiles = 1;
|
||||
}
|
||||
|
||||
if ((int)($validationConfiguration['minFiles'] ?? PHP_INT_MAX) < PHP_INT_MAX) {
|
||||
$this->minFiles = (int)($validationConfiguration['minFiles']);
|
||||
}
|
||||
|
||||
if ((int)($validationConfiguration['maxFiles'] ?? PHP_INT_MAX) < PHP_INT_MAX) {
|
||||
$this->maxFiles = (int)($validationConfiguration['maxFiles']);
|
||||
}
|
||||
|
||||
// Migrate allowedMimeTypes to mimeType configuration, if mimeType configuration is not defined
|
||||
if (($validationConfiguration['allowedMimeTypes'] ?? false)
|
||||
&& is_array($validationConfiguration['allowedMimeTypes'])
|
||||
&& !isset($validationConfiguration['mimeType'])
|
||||
) {
|
||||
$validationConfiguration['mimeType'] = ['allowedMimeTypes' => $validationConfiguration['allowedMimeTypes']];
|
||||
unset($validationConfiguration['allowedMimeTypes']);
|
||||
}
|
||||
|
||||
if (($validationConfiguration['mimeType'] ?? false)
|
||||
&& is_array($validationConfiguration['mimeType'])
|
||||
) {
|
||||
$mimeTypeValidator = GeneralUtility::makeInstance(MimeTypeValidator::class);
|
||||
$mimeTypeValidator->setOptions($validationConfiguration['mimeType']);
|
||||
$this->addValidator($mimeTypeValidator);
|
||||
}
|
||||
|
||||
if (($validationConfiguration['fileExtension'] ?? false)
|
||||
&& is_array($validationConfiguration['fileExtension'])
|
||||
) {
|
||||
$fileExtensionValidator = GeneralUtility::makeInstance(FileExtensionValidator::class);
|
||||
$fileExtensionValidator->setOptions($validationConfiguration['fileExtension']);
|
||||
$this->addValidator($fileExtensionValidator);
|
||||
}
|
||||
|
||||
if (($validationConfiguration['fileSize'] ?? false)
|
||||
&& is_array($validationConfiguration['fileSize'])
|
||||
) {
|
||||
$fileSizeValidator = GeneralUtility::makeInstance(FileSizeValidator::class);
|
||||
$fileSizeValidator->setOptions($validationConfiguration['fileSize']);
|
||||
$this->addValidator($fileSizeValidator);
|
||||
}
|
||||
|
||||
if (($validationConfiguration['imageDimensions'] ?? false)
|
||||
&& is_array($validationConfiguration['imageDimensions'])
|
||||
) {
|
||||
$imageDimensionsValidator = GeneralUtility::makeInstance(ImageDimensionsValidator::class);
|
||||
$imageDimensionsValidator->setOptions($validationConfiguration['imageDimensions']);
|
||||
$this->addValidator($imageDimensionsValidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
/**
|
||||
* @internal Only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class FileUploadDeletionConfiguration
|
||||
{
|
||||
public function __construct(protected readonly string $propertyName, protected array $fileReferenceUids = []) {}
|
||||
|
||||
public function getPropertyName(): string
|
||||
{
|
||||
return $this->propertyName;
|
||||
}
|
||||
|
||||
public function addFileReferenceUid(int $fileReferenceUid): void
|
||||
{
|
||||
$this->fileReferenceUids[] = $fileReferenceUid;
|
||||
}
|
||||
|
||||
public function getFileReferenceUids(): array
|
||||
{
|
||||
return $this->fileReferenceUids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration;
|
||||
use TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter;
|
||||
|
||||
/**
|
||||
* The default property mapping configuration is available
|
||||
* inside the Argument-object.
|
||||
*/
|
||||
class MvcPropertyMappingConfiguration extends PropertyMappingConfiguration
|
||||
{
|
||||
/**
|
||||
* Allow creation of a certain sub property
|
||||
*
|
||||
* @param string $propertyPath
|
||||
*/
|
||||
public function allowCreationForSubProperty($propertyPath)
|
||||
{
|
||||
$this->forProperty($propertyPath)->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow modification for a given property path
|
||||
*
|
||||
* @param string $propertyPath
|
||||
*/
|
||||
public function allowModificationForSubProperty($propertyPath)
|
||||
{
|
||||
$this->forProperty($propertyPath)->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the target type for a certain property. Especially useful
|
||||
* if there is an object which has a nested object which is abstract,
|
||||
* and you want to instantiate a concrete object instead.
|
||||
*
|
||||
* @param string $propertyPath
|
||||
* @param string $targetType
|
||||
*/
|
||||
public function setTargetTypeForSubProperty($propertyPath, $targetType)
|
||||
{
|
||||
$this->forProperty($propertyPath)->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_TARGET_TYPE, $targetType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?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\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Error\Http\BadRequestException;
|
||||
use TYPO3\CMS\Core\Exception\Crypto\InvalidHashStringException;
|
||||
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
||||
use TYPO3\CMS\Extbase\Mvc\Request;
|
||||
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
|
||||
use TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter;
|
||||
use TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException;
|
||||
use TYPO3\CMS\Extbase\Security\HashScope;
|
||||
|
||||
/**
|
||||
* This is a service which can generate a request hash and check whether the currently given arguments
|
||||
* fit to the request hash.
|
||||
*
|
||||
* It is used when forms are generated and submitted:
|
||||
* After a form has been generated, the method "generateTrustedPropertiesToken" is called with the names of all form fields.
|
||||
* It cleans up the array of form fields and creates another representation of it, which is then json encoded and a hmac
|
||||
* is appended. This is called the request hash.
|
||||
*
|
||||
* The json encoded form field list and the appended hmac will be submitted with the form (as attribute __trustedProperties).
|
||||
*
|
||||
* On the validation side, the validation happens in two steps:
|
||||
* 1) Check if the request hash is consistent (the hmac value fits to the json encoded field list string)
|
||||
* 2) Check that _all_ GET/POST parameters submitted occur inside the form field list of the request hash.
|
||||
*
|
||||
* Note: It is crucially important that a private key is computed into the hash value! This is done inside the HashService.
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class MvcPropertyMappingConfigurationService
|
||||
{
|
||||
protected HashService $hashService;
|
||||
|
||||
public function injectHashService(HashService $hashService): void
|
||||
{
|
||||
$this->hashService = $hashService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a request hash for a list of form fields
|
||||
*/
|
||||
public function generateTrustedPropertiesToken(array $formFieldNames, string $fieldNamePrefix = ''): string
|
||||
{
|
||||
$formFieldArray = [];
|
||||
foreach ($formFieldNames as $formField) {
|
||||
$formFieldParts = explode('[', $formField);
|
||||
$currentPosition = &$formFieldArray;
|
||||
$formFieldPartsCount = count($formFieldParts);
|
||||
for ($i = 0; $i < $formFieldPartsCount; $i++) {
|
||||
$formFieldPart = $formFieldParts[$i];
|
||||
$formFieldPart = rtrim($formFieldPart, ']');
|
||||
if (!is_array($currentPosition)) {
|
||||
throw new InvalidArgumentForHashGenerationException('The form field "' . $formField . '" is declared as array, but it collides with a previous form field of the same name which declared the field as string. This is an inconsistency you need to fix inside your Fluid form. (String overridden by Array)', 1255072196);
|
||||
}
|
||||
if ($i === $formFieldPartsCount - 1) {
|
||||
if (isset($currentPosition[$formFieldPart]) && is_array($currentPosition[$formFieldPart])) {
|
||||
throw new InvalidArgumentForHashGenerationException('The form field "' . $formField . '" is declared as string, but it collides with a previous form field of the same name which declared the field as array. This is an inconsistency you need to fix inside your Fluid form. (Array overridden by String)', 1255072587);
|
||||
}
|
||||
// Last iteration - add a string
|
||||
if ($formFieldPart === '') {
|
||||
$currentPosition[] = 1;
|
||||
} else {
|
||||
$currentPosition[$formFieldPart] = 1;
|
||||
}
|
||||
} else {
|
||||
if ($formFieldPart === '') {
|
||||
throw new InvalidArgumentForHashGenerationException('The form field "' . $formField . '" is invalid. Reason: "[]" used not as last argument, but somewhere in the middle (like foo[][bar]).', 1255072832);
|
||||
}
|
||||
if (!isset($currentPosition[$formFieldPart])) {
|
||||
$currentPosition[$formFieldPart] = [];
|
||||
}
|
||||
$currentPosition = &$currentPosition[$formFieldPart];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($fieldNamePrefix !== '') {
|
||||
$formFieldArray = ($formFieldArray[$fieldNamePrefix] ?? []);
|
||||
}
|
||||
return $this->encodeAndHashFormFieldArray($formFieldArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode and hash the form field array
|
||||
*/
|
||||
protected function encodeAndHashFormFieldArray(array $formFieldArray): string
|
||||
{
|
||||
$encodedFormFieldArray = json_encode($formFieldArray);
|
||||
return $this->hashService->appendHmac($encodedFormFieldArray, HashScope::TrustedProperties->prefix(), HashAlgo::SHA3_256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the property mapping configuration in $controllerArguments if
|
||||
* the trusted properties are set inside the request.
|
||||
*
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
public function initializePropertyMappingConfigurationFromRequest(Request $request, Arguments $controllerArguments): void
|
||||
{
|
||||
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = $request->getAttribute('extbase');
|
||||
$trustedPropertiesToken = $extbaseRequestParameters->getInternalArgument('__trustedProperties');
|
||||
if (!is_string($trustedPropertiesToken)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$encodedTrustedProperties = $this->hashService->validateAndStripHmac($trustedPropertiesToken, HashScope::TrustedProperties->prefix(), HashAlgo::SHA3_256);
|
||||
} catch (InvalidHashStringException $e) {
|
||||
throw new BadRequestException('The HMAC of the form could not be validated.', 1581862822);
|
||||
}
|
||||
$trustedProperties = json_decode($encodedTrustedProperties, true);
|
||||
if (!is_array($trustedProperties)) {
|
||||
if (str_starts_with($encodedTrustedProperties, 'a:')) {
|
||||
throw new BadRequestException('Trusted properties used outdated serialization format instead json.', 1699604555);
|
||||
}
|
||||
throw new BadRequestException('The HMAC of the form could not be utilized.', 1691267306);
|
||||
}
|
||||
|
||||
foreach ($trustedProperties as $propertyName => $propertyConfiguration) {
|
||||
$propertyName = (string)$propertyName;
|
||||
if (!$controllerArguments->hasArgument($propertyName) || !is_array($propertyConfiguration)) {
|
||||
continue;
|
||||
}
|
||||
$propertyMappingConfiguration = $controllerArguments->getArgument($propertyName)->getPropertyMappingConfiguration();
|
||||
$this->modifyPropertyMappingConfiguration($propertyConfiguration, $propertyMappingConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the passed $propertyMappingConfiguration according to the $propertyConfiguration which
|
||||
* has been generated by Fluid. In detail, if the $propertyConfiguration contains
|
||||
* an __identity field, we allow modification of objects; else we allow creation.
|
||||
*
|
||||
* All other properties are specified as allowed properties.
|
||||
*/
|
||||
protected function modifyPropertyMappingConfiguration(
|
||||
array $propertyConfiguration,
|
||||
PropertyMappingConfigurationInterface $propertyMappingConfiguration
|
||||
): void {
|
||||
if (isset($propertyConfiguration['__identity'])) {
|
||||
$propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED, true);
|
||||
unset($propertyConfiguration['__identity']);
|
||||
} else {
|
||||
$propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, true);
|
||||
}
|
||||
|
||||
foreach ($propertyConfiguration as $innerKey => $innerValue) {
|
||||
if (is_array($innerValue)) {
|
||||
$this->modifyPropertyMappingConfiguration(
|
||||
$innerValue,
|
||||
$propertyMappingConfiguration->forProperty((string)$innerKey)
|
||||
);
|
||||
}
|
||||
$propertyMappingConfiguration->allowProperties($innerKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Mvc\Controller;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\RateLimiter\LimiterInterface;
|
||||
use TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface;
|
||||
use TYPO3\CMS\Extbase\Attribute\RateLimit;
|
||||
|
||||
/**
|
||||
* Registry for rate limit configurations of extbase controller actions,
|
||||
* populated at compile time via {@see \TYPO3\CMS\Extbase\DependencyInjection\RateLimitPass}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RateLimitRegistry
|
||||
{
|
||||
/** @var array<string, array<string, RateLimit>> */
|
||||
private array $rateLimits = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly RateLimiterFactoryInterface $rateLimiterFactory,
|
||||
) {}
|
||||
|
||||
public function add(string $controllerClass, string $actionMethod, int $limit, string $interval, string $policy, string $message): void
|
||||
{
|
||||
$this->rateLimits[$controllerClass][$actionMethod] = new RateLimit($limit, $interval, $policy, $message);
|
||||
}
|
||||
|
||||
public function getRateLimit(string $controllerClass, string $actionMethod): ?RateLimit
|
||||
{
|
||||
return $this->rateLimits[$controllerClass][$actionMethod] ?? null;
|
||||
}
|
||||
|
||||
public function createLimiter(string $controllerClass, string $actionMethod, ServerRequestInterface $request): ?LimiterInterface
|
||||
{
|
||||
$rateLimit = $this->getRateLimit($controllerClass, $actionMethod);
|
||||
if ($rateLimit === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$identifier = strtolower(str_replace('\\', '-', $controllerClass) . '-' . $actionMethod);
|
||||
return $this->rateLimiterFactory->createRequestBasedLimiter($request, $rateLimit->getConfiguration($identifier));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?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\Extbase\Mvc;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Attribute\IgnoreValidation;
|
||||
use TYPO3\CMS\Extbase\Event\Mvc\AfterRequestDispatchedEvent;
|
||||
use TYPO3\CMS\Extbase\Http\ForwardResponse;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InfiniteLoopException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerException;
|
||||
|
||||
/**
|
||||
* Dispatches requests to the controller which was specified by the request and
|
||||
* returns the response the controller generated.
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class Dispatcher
|
||||
{
|
||||
private ContainerInterface $container;
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(
|
||||
ContainerInterface $container,
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
) {
|
||||
$this->container = $container;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a request to a controller and initializes the security framework.
|
||||
*
|
||||
* @param RequestInterface $request The request to dispatch
|
||||
* @throws Exception\InfiniteLoopException
|
||||
*/
|
||||
public function dispatch(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
$dispatchLoopCount = 0;
|
||||
$isDispatched = false;
|
||||
while (!$isDispatched) {
|
||||
if ($dispatchLoopCount++ > 99) {
|
||||
throw new InfiniteLoopException(
|
||||
'Could not ultimately dispatch the request after ' . $dispatchLoopCount
|
||||
. ' iterations. Most probably, an #[' . IgnoreValidation::class . ']'
|
||||
. ' attribute is missing on re-displaying a form with validation errors.',
|
||||
1217839467
|
||||
);
|
||||
}
|
||||
$controller = $this->resolveController($request);
|
||||
$response = $controller->processRequest($request);
|
||||
if ($response instanceof ForwardResponse) {
|
||||
// The controller action returned an extbase internal Forward response:
|
||||
// Another action should be dispatched.
|
||||
$request = static::buildRequestFromCurrentRequestAndForwardResponse($request, $response);
|
||||
} else {
|
||||
// The controller action returned a casual or a HTTP redirect response.
|
||||
// Dispatching ends here and response is sent to client.
|
||||
$isDispatched = true;
|
||||
}
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new AfterRequestDispatchedEvent($request, $response));
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and instantiates a controller that matches the current request.
|
||||
* If no controller can be found, an instance of NotFoundControllerInterface is returned.
|
||||
*
|
||||
* @param RequestInterface $request The request to dispatch
|
||||
* @return Controller\ControllerInterface
|
||||
* @throws Exception\InvalidControllerException
|
||||
*/
|
||||
protected function resolveController(RequestInterface $request)
|
||||
{
|
||||
$controllerObjectName = $request->getControllerObjectName();
|
||||
$controller = $this->container->get($controllerObjectName);
|
||||
if (!$controller instanceof ControllerInterface) {
|
||||
throw new InvalidControllerException(
|
||||
'Invalid controller "' . $request->getControllerObjectName() . '". The controller must implement the TYPO3\\CMS\\Extbase\\Mvc\\Controller\\ControllerInterface.',
|
||||
1476109646
|
||||
);
|
||||
}
|
||||
return $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
* @todo: make this a private method again as soon as the tests, that fake the dispatching of requests, are refactored.
|
||||
*/
|
||||
public static function buildRequestFromCurrentRequestAndForwardResponse(RequestInterface $currentRequest, ForwardResponse $forwardResponse): RequestInterface
|
||||
{
|
||||
$request = $currentRequest->withControllerActionName($forwardResponse->getActionName());
|
||||
if ($forwardResponse->getControllerName() !== null) {
|
||||
$request = $request->withControllerName($forwardResponse->getControllerName());
|
||||
}
|
||||
if ($forwardResponse->getExtensionName() !== null) {
|
||||
$request = $request->withControllerExtensionName($forwardResponse->getExtensionName());
|
||||
}
|
||||
if ($forwardResponse->getArguments() !== null) {
|
||||
$request = $request->withArguments($forwardResponse->getArguments());
|
||||
}
|
||||
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = clone $request->getAttribute('extbase');
|
||||
$extbaseRequestParameters->setOriginalRequest($currentRequest);
|
||||
$extbaseRequestParameters->setOriginalRequestMappingResults($forwardResponse->getArgumentsValidationResult());
|
||||
$extbaseRequestParameters->setOriginalFlashMessages(...$forwardResponse->getFlashMessages());
|
||||
return $request->withAttribute('extbase', $extbaseRequestParameters);
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Mvc;
|
||||
|
||||
use TYPO3\CMS\Extbase\Exception as ExtbaseException;
|
||||
|
||||
/**
|
||||
* A generic MVC exception
|
||||
*/
|
||||
class Exception extends ExtbaseException
|
||||
{
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public static function fromPrevious(\Throwable $e): self
|
||||
{
|
||||
return new self($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Infinite Loop" exception
|
||||
*/
|
||||
class InfiniteLoopException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "invalid action name" exception
|
||||
*/
|
||||
class InvalidActionNameException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Argument Name" exception
|
||||
*/
|
||||
class InvalidArgumentMixingException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Argument Name" exception
|
||||
*/
|
||||
class InvalidArgumentNameException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Argument Type" exception
|
||||
*/
|
||||
class InvalidArgumentTypeException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Argument Value" exception
|
||||
*/
|
||||
class InvalidArgumentValueException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Controller" exception
|
||||
*/
|
||||
class InvalidControllerException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Controller Name" exception
|
||||
*/
|
||||
class InvalidControllerNameException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Extension Name" exception
|
||||
*/
|
||||
class InvalidExtensionNameException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* A "No Such Action" exception
|
||||
*/
|
||||
class NoSuchActionException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* A "No Such Argument" exception
|
||||
*/
|
||||
class NoSuchArgumentException extends Exception {}
|
||||
@@ -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\Extbase\Mvc\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* A "No Such Controller" exception
|
||||
*/
|
||||
class NoSuchControllerException extends Exception {}
|
||||
@@ -0,0 +1,349 @@
|
||||
<?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\Extbase\Mvc;
|
||||
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
|
||||
|
||||
/**
|
||||
* Extbase request related state.
|
||||
* Attached as 'extbase' attribute to PSR-7 ServerRequestInterface.
|
||||
*
|
||||
* @internal Sets up extbase internally, use TYPO3\CMS\Extbase\Mvc\Request instead.
|
||||
*/
|
||||
class ExtbaseRequestParameters
|
||||
{
|
||||
/**
|
||||
* Key of the plugin which identifies the plugin.
|
||||
* In frontend, it is the second argument of ExtensionUtility::configurePlugin(), example: "FormFramework" in ext:form.
|
||||
* In backend, it is the module identifier from the corresponding module configuration, for example
|
||||
* "web_FormFormbuilder" for the ext:form backend module.
|
||||
*/
|
||||
protected string $pluginName = '';
|
||||
|
||||
/**
|
||||
* Name of the extension which is supposed to handle this request. This is the extension key in UpperCamelCase.
|
||||
* This is typically defined by ExtensionUtility::configurePlugin() and friends as first argument.
|
||||
* Example: "IndexedSearch", when the extension key "directory name of extension" is indexed_search.
|
||||
*/
|
||||
protected string $controllerExtensionName = '';
|
||||
|
||||
/**
|
||||
* This is the FQDN of a controller, example: "TYPO3\CMS\Form\Controller\FormManagerController"
|
||||
* for ext:form backend module.
|
||||
*/
|
||||
protected string $controllerObjectName = '';
|
||||
|
||||
/**
|
||||
* Object name of the controller which is supposed to handle this request. This is the non-FQDN
|
||||
* version of $controllerObjectName, without the word "Controller", example: "FormManager".
|
||||
*/
|
||||
protected string $controllerName = 'Standard';
|
||||
|
||||
/**
|
||||
* A map $controllerName => $controllerObjectName
|
||||
*/
|
||||
protected array $controllerAliasToClassNameMapping = [];
|
||||
|
||||
/**
|
||||
* Name of the action the controller is supposed to execute. For example "create" with the
|
||||
* controller method name being "createAction()".
|
||||
* Action name must start with a lower case letter and is case-sensitive.
|
||||
*/
|
||||
protected string $controllerActionName = 'index';
|
||||
|
||||
/**
|
||||
* The arguments for this request. This receives only those arguments relevant and
|
||||
* prefixed for this extension/controller/plugin combination.
|
||||
*/
|
||||
protected array $arguments = [];
|
||||
|
||||
/**
|
||||
* Framework-internal arguments for this request, such as __referrer.
|
||||
* All framework-internal arguments start with double underscore (__),
|
||||
* and are only used from within the framework. Not for user consumption.
|
||||
* Internal Arguments can be objects, in contrast to public arguments
|
||||
*/
|
||||
protected array $internalArguments = [];
|
||||
|
||||
/**
|
||||
* The requested representation format, "html", "xml", "png", "json" or the like.
|
||||
* Can even be something like "rss.xml".
|
||||
*/
|
||||
protected string $format = 'html';
|
||||
|
||||
/**
|
||||
* If this request is a forward because of an error, the original request gets filled.
|
||||
*/
|
||||
protected ?RequestInterface $originalRequest = null;
|
||||
|
||||
/**
|
||||
* If the request is a forward because of an error, these mapping results get filled here.
|
||||
*/
|
||||
protected ?Result $originalRequestMappingResults = null;
|
||||
|
||||
/**
|
||||
* @var list<FlashMessage>
|
||||
*/
|
||||
protected array $originalFlashMessages = [];
|
||||
|
||||
/**
|
||||
* If files were uploaded, this array holds the files
|
||||
* prefixed for this extension/controller/plugin combination.
|
||||
*/
|
||||
protected array $uploadedFiles = [];
|
||||
|
||||
public function __construct(string $controllerClassName = '')
|
||||
{
|
||||
$this->controllerObjectName = $controllerClassName;
|
||||
}
|
||||
|
||||
public function getControllerObjectName(): string
|
||||
{
|
||||
return $this->controllerObjectName;
|
||||
}
|
||||
|
||||
public function setControllerObjectName(string $controllerObjectName): self
|
||||
{
|
||||
$nameParts = ClassNamingUtility::explodeObjectControllerName($controllerObjectName);
|
||||
$this->controllerExtensionName = $nameParts['extensionName'];
|
||||
$this->controllerName = $nameParts['controllerName'];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPluginName(string $pluginName): self
|
||||
{
|
||||
$this->pluginName = $pluginName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPluginName(): string
|
||||
{
|
||||
return $this->pluginName;
|
||||
}
|
||||
|
||||
public function setControllerExtensionName(string $controllerExtensionName): self
|
||||
{
|
||||
$this->controllerExtensionName = $controllerExtensionName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getControllerExtensionName(): string
|
||||
{
|
||||
return $this->controllerExtensionName;
|
||||
}
|
||||
|
||||
public function getControllerExtensionKey(): string
|
||||
{
|
||||
return GeneralUtility::camelCaseToLowerCaseUnderscored($this->controllerExtensionName);
|
||||
}
|
||||
|
||||
public function setControllerAliasToClassNameMapping(array $controllerAliasToClassNameMapping): self
|
||||
{
|
||||
// this is only needed as long as forwarded requests are altered and unless there
|
||||
// is no new request object created by the request builder.
|
||||
$this->controllerAliasToClassNameMapping = $controllerAliasToClassNameMapping;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setControllerName(string $controllerName): self
|
||||
{
|
||||
$this->controllerName = $controllerName;
|
||||
// There might be no Controller Class, for example for Fluid Templates.
|
||||
$this->controllerObjectName = $this->controllerAliasToClassNameMapping[$controllerName] ?? '';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getControllerName(): string
|
||||
{
|
||||
return $this->controllerName;
|
||||
}
|
||||
|
||||
public function setControllerActionName(string $actionName): self
|
||||
{
|
||||
$this->controllerActionName = $actionName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getControllerActionName(): string
|
||||
{
|
||||
return $this->controllerActionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value The new value
|
||||
* @throws InvalidArgumentNameException
|
||||
*/
|
||||
public function setArgument(string $argumentName, mixed $value): self
|
||||
{
|
||||
if ($argumentName === '') {
|
||||
throw new InvalidArgumentNameException('Invalid argument name.', 1210858767);
|
||||
}
|
||||
if (str_starts_with($argumentName, '__')) {
|
||||
$this->internalArguments[$argumentName] = $value;
|
||||
return $this;
|
||||
}
|
||||
if (!in_array($argumentName, ['@extension', '@subpackage', '@controller', '@action', '@format'], true)) {
|
||||
$this->arguments[$argumentName] = $value;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the whole arguments array and therefore replaces any arguments which existed before.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
* @throws InvalidArgumentNameException
|
||||
*/
|
||||
public function setArguments(array $arguments): self
|
||||
{
|
||||
$this->arguments = [];
|
||||
foreach ($arguments as $argumentName => $argumentValue) {
|
||||
$this->setArgument($argumentName, $argumentValue);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the specified argument.
|
||||
*
|
||||
* @return mixed Value of the argument
|
||||
* @throws NoSuchArgumentException if such an argument does not exist
|
||||
*/
|
||||
public function getArgument(string $argumentName): mixed
|
||||
{
|
||||
if (!isset($this->arguments[$argumentName])) {
|
||||
throw new NoSuchArgumentException('An argument "' . $argumentName . '" does not exist for this request.', 1176558158);
|
||||
}
|
||||
return $this->arguments[$argumentName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an argument of the given name exists (is set)
|
||||
*/
|
||||
public function hasArgument(string $argumentName = ''): bool
|
||||
{
|
||||
return isset($this->arguments[$argumentName]);
|
||||
}
|
||||
|
||||
public function setFormat(string $format): self
|
||||
{
|
||||
$this->format = $format;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFormat(): string
|
||||
{
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original request. Filled only if a property mapping error occurred.
|
||||
*/
|
||||
public function getOriginalRequest(): ?RequestInterface
|
||||
{
|
||||
return $this->originalRequest;
|
||||
}
|
||||
|
||||
public function setOriginalRequest(RequestInterface $originalRequest): self
|
||||
{
|
||||
$this->originalRequest = $originalRequest;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOriginalRequestMappingResults(): Result
|
||||
{
|
||||
if ($this->originalRequestMappingResults === null) {
|
||||
return new Result();
|
||||
}
|
||||
return $this->originalRequestMappingResults;
|
||||
}
|
||||
|
||||
public function setOriginalRequestMappingResults(Result $originalRequestMappingResults): self
|
||||
{
|
||||
$this->originalRequestMappingResults = $originalRequestMappingResults;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<FlashMessage>
|
||||
*/
|
||||
public function getOriginalFlashMessages(): array
|
||||
{
|
||||
return $this->originalFlashMessages;
|
||||
}
|
||||
|
||||
public function setOriginalFlashMessages(FlashMessage ...$originalFlashMessages): self
|
||||
{
|
||||
$this->originalFlashMessages = $originalFlashMessages;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the specified argument
|
||||
*
|
||||
* @return mixed Value of the argument, or NULL if not set.
|
||||
*/
|
||||
public function getInternalArgument($argumentName): mixed
|
||||
{
|
||||
if (!isset($this->internalArguments[$argumentName])) {
|
||||
return null;
|
||||
}
|
||||
return $this->internalArguments[$argumentName];
|
||||
}
|
||||
|
||||
public function getUploadedFiles(): array
|
||||
{
|
||||
return $this->uploadedFiles;
|
||||
}
|
||||
|
||||
public function setUploadedFiles(array $files): self
|
||||
{
|
||||
$this->validateUploadedFiles($files);
|
||||
$this->uploadedFiles = $files;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.', 1647338470);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
<?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\Extbase\Mvc;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* The extbase request.
|
||||
*
|
||||
* This is a decorator: The core PSR-7 request is hand over as constructor
|
||||
* argument, this class implements ServerRequestInterface, too.
|
||||
* Additionally, the extbase request details are attached as 'extbase'
|
||||
* attribute to the PSR-7 request and this class implements extbase RequestInterface.
|
||||
* This class has no state except the PSR-7 request, all operations are
|
||||
* hand down to the PSR-7 request.
|
||||
*/
|
||||
class Request implements RequestInterface
|
||||
{
|
||||
protected ServerRequestInterface $request;
|
||||
|
||||
final public function __construct(ServerRequestInterface $request)
|
||||
{
|
||||
if (!$request->getAttribute('extbase') instanceof ExtbaseRequestParameters) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Given request must have an attribute "extbase" of type ExtbaseAttribute',
|
||||
1624452070
|
||||
);
|
||||
}
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExtbaseAttribute attached as attribute 'extbase' to $request carries extbase
|
||||
* specific request values. This helper method type hints this attribute.
|
||||
*/
|
||||
protected function getExtbaseAttribute(): ExtbaseRequestParameters
|
||||
{
|
||||
return $this->request->getAttribute('extbase');
|
||||
}
|
||||
|
||||
public function getControllerObjectName(): string
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getControllerObjectName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified controller object name set.
|
||||
*/
|
||||
public function withControllerObjectName(string $controllerObjectName): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setControllerObjectName($controllerObjectName);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin key.
|
||||
*/
|
||||
public function getPluginName(): string
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getPluginName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified plugin name set.
|
||||
*/
|
||||
public function withPluginName(string $pluginName): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setPluginName($pluginName);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension name of the specified controller.
|
||||
*/
|
||||
public function getControllerExtensionName(): string
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getControllerExtensionName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified controller extension name set.
|
||||
*/
|
||||
public function withControllerExtensionName(string $controllerExtensionName): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setControllerExtensionName($controllerExtensionName);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension key of the specified controller.
|
||||
*/
|
||||
public function getControllerExtensionKey(): string
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getControllerExtensionKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the controller name supposed to handle this request, if one
|
||||
* was set already (if not, the name of the default controller is returned)
|
||||
*/
|
||||
public function getControllerName(): string
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getControllerName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified controller name set.
|
||||
*/
|
||||
public function withControllerName(string $controllerName): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setControllerName($controllerName);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the action the controller is supposed to execute.
|
||||
*/
|
||||
public function getControllerActionName(): string
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getControllerActionName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified controller action name set.
|
||||
*/
|
||||
public function withControllerActionName(string $actionName): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setControllerActionName($actionName);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getArguments();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified extbase arguments, replacing
|
||||
* any arguments which existed before.
|
||||
*/
|
||||
public function withArguments(array $arguments): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setArguments($arguments);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
public function getArgument(string $argumentName): mixed
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getArgument($argumentName);
|
||||
}
|
||||
|
||||
public function hasArgument(string $argumentName): bool
|
||||
{
|
||||
return $this->getExtbaseAttribute()->hasArgument($argumentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified argument set.
|
||||
*/
|
||||
public function withArgument(string $argumentName, mixed $value): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setArgument($argumentName, $value);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested representation format, something
|
||||
* like "html", "xml", "png", "json" or the like.
|
||||
*/
|
||||
public function getFormat(): string
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance with the specified derived request attribute.
|
||||
*
|
||||
* This method allows setting a single derived request attribute as
|
||||
* described in getFormat().
|
||||
*/
|
||||
public function withFormat(string $format): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setFormat($format);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods implementing ServerRequestInterface
|
||||
*/
|
||||
public function getServerParams(): array
|
||||
{
|
||||
return $this->request->getServerParams();
|
||||
}
|
||||
|
||||
public function getCookieParams(): array
|
||||
{
|
||||
return $this->request->getCookieParams();
|
||||
}
|
||||
|
||||
public function withCookieParams(array $cookies): static
|
||||
{
|
||||
$request = $this->request->withCookieParams($cookies);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return $this->request->getQueryParams();
|
||||
}
|
||||
|
||||
public function withQueryParams(array $query): static
|
||||
{
|
||||
$request = $this->request->withQueryParams($query);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function getUploadedFiles(): array
|
||||
{
|
||||
return $this->getExtbaseAttribute()->getUploadedFiles();
|
||||
}
|
||||
|
||||
public function withUploadedFiles(array $uploadedFiles): static
|
||||
{
|
||||
$attribute = clone $this->getExtbaseAttribute();
|
||||
$attribute->setUploadedFiles($uploadedFiles);
|
||||
return $this->withAttribute('extbase', $attribute);
|
||||
}
|
||||
|
||||
public function getParsedBody()
|
||||
{
|
||||
return $this->request->getParsedBody();
|
||||
}
|
||||
|
||||
public function withParsedBody($data): static
|
||||
{
|
||||
$request = $this->request->withParsedBody($data);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->request->getAttributes();
|
||||
}
|
||||
|
||||
public function getAttribute($name, $default = null)
|
||||
{
|
||||
return $this->request->getAttribute($name, $default);
|
||||
}
|
||||
|
||||
public function withAttribute($name, $value): static
|
||||
{
|
||||
$request = $this->request->withAttribute($name, $value);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ($name is 'extbase' ? ServerRequestInterface : static)
|
||||
*/
|
||||
public function withoutAttribute($name): ServerRequestInterface|static
|
||||
{
|
||||
$request = $this->request->withoutAttribute($name);
|
||||
if ($name === 'extbase') {
|
||||
return $request;
|
||||
}
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods implementing RequestInterface
|
||||
*/
|
||||
public function getRequestTarget(): string
|
||||
{
|
||||
return $this->request->getRequestTarget();
|
||||
}
|
||||
|
||||
public function withRequestTarget($requestTarget): static
|
||||
{
|
||||
$request = $this->request->withRequestTarget($requestTarget);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->request->getMethod();
|
||||
}
|
||||
|
||||
public function withMethod($method): static
|
||||
{
|
||||
$request = $this->request->withMethod($method);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return $this->request->getUri();
|
||||
}
|
||||
|
||||
public function withUri(UriInterface $uri, $preserveHost = false): static
|
||||
{
|
||||
$request = $this->request->withUri($uri, $preserveHost);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods implementing MessageInterface
|
||||
*/
|
||||
public function getProtocolVersion(): string
|
||||
{
|
||||
return $this->request->getProtocolVersion();
|
||||
}
|
||||
|
||||
public function withProtocolVersion($version): static
|
||||
{
|
||||
$request = $this->request->withProtocolVersion($version);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->request->getHeaders();
|
||||
}
|
||||
|
||||
public function hasHeader($name): bool
|
||||
{
|
||||
return $this->request->hasHeader($name);
|
||||
}
|
||||
|
||||
public function getHeader($name): array
|
||||
{
|
||||
return $this->request->getHeader($name);
|
||||
}
|
||||
|
||||
public function getHeaderLine($name): string
|
||||
{
|
||||
return $this->request->getHeaderLine($name);
|
||||
}
|
||||
|
||||
public function withHeader($name, $value): static
|
||||
{
|
||||
$request = $this->request->withHeader($name, $value);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function withAddedHeader($name, $value): static
|
||||
{
|
||||
$request = $this->request->withAddedHeader($name, $value);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function withoutHeader($name): static
|
||||
{
|
||||
$request = $this->request->withoutHeader($name);
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function getBody(): StreamInterface
|
||||
{
|
||||
return $this->request->getBody();
|
||||
}
|
||||
|
||||
public function withBody(StreamInterface $body): static
|
||||
{
|
||||
$request = $this->request->withBody($body);
|
||||
return new static($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?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\Extbase\Mvc;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Contract for an extbase request.
|
||||
*/
|
||||
interface RequestInterface extends ServerRequestInterface
|
||||
{
|
||||
/**
|
||||
* Returns the plugin key.
|
||||
*/
|
||||
public function getPluginName(): string;
|
||||
|
||||
/**
|
||||
* Return an instance with the specified plugin name set.
|
||||
*/
|
||||
public function withPluginName(string $pluginName): RequestInterface;
|
||||
|
||||
/**
|
||||
* Returns the extension name of the specified controller.
|
||||
*/
|
||||
public function getControllerExtensionName(): string;
|
||||
|
||||
/**
|
||||
* Return an instance with the specified controller extension name set.
|
||||
*/
|
||||
public function withControllerExtensionName(string $controllerExtensionName): RequestInterface;
|
||||
|
||||
/**
|
||||
* Returns the extension key of the specified controller.
|
||||
*/
|
||||
public function getControllerExtensionKey(): string;
|
||||
|
||||
/**
|
||||
* Returns the object name of the controller defined by the package
|
||||
* key and controller name.
|
||||
*/
|
||||
public function getControllerObjectName(): string;
|
||||
|
||||
/**
|
||||
* Return an instance with the specified controller object name set.
|
||||
*/
|
||||
public function withControllerObjectName(string $controllerObjectName): RequestInterface;
|
||||
|
||||
/**
|
||||
* Returns the object name of the controller supposed to handle this request, if one
|
||||
* was specified already (if not, the name of the default controller is returned)
|
||||
*/
|
||||
public function getControllerName(): string;
|
||||
|
||||
/**
|
||||
* Return an instance with the specified controller name set.
|
||||
*/
|
||||
public function withControllerName(string $controllerName): RequestInterface;
|
||||
|
||||
/**
|
||||
* Returns the name of the action the controller is supposed to execute.
|
||||
*/
|
||||
public function getControllerActionName(): string;
|
||||
|
||||
/**
|
||||
* Return an instance with the specified controller action name set.
|
||||
*
|
||||
* Note that the action name must start with a lower case letter and is case-sensitive.
|
||||
*/
|
||||
public function withControllerActionName(string $actionName): RequestInterface;
|
||||
|
||||
/**
|
||||
* Returns the value of the specified argument.
|
||||
*/
|
||||
public function getArgument(string $argumentName): mixed;
|
||||
|
||||
/**
|
||||
* Checks if an argument of the given name exists (is set).
|
||||
*/
|
||||
public function hasArgument(string $argumentName): bool;
|
||||
|
||||
/**
|
||||
* Return an instance with the specified argument set.
|
||||
*/
|
||||
public function withArgument(string $argumentName, mixed $value): RequestInterface;
|
||||
|
||||
/**
|
||||
* Returns an array of extbase arguments and their values.
|
||||
*/
|
||||
public function getArguments(): array;
|
||||
|
||||
/**
|
||||
* Return an instance with the specified extbase arguments, replacing
|
||||
* any arguments which existed before.
|
||||
*/
|
||||
public function withArguments(array $arguments): RequestInterface;
|
||||
|
||||
/**
|
||||
* Returns the requested representation format, something
|
||||
* like "html", "xml", "png", "json" or the like.
|
||||
*/
|
||||
public function getFormat(): string;
|
||||
|
||||
/**
|
||||
* Return an instance with the specified format.
|
||||
* This method allows setting the format as described in getFormat().
|
||||
*/
|
||||
public function withFormat(string $format): RequestInterface;
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
<?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\Extbase\Mvc\View;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
|
||||
|
||||
/**
|
||||
* A JSON view
|
||||
*/
|
||||
#[Autoconfigure(public: true, shared: false)]
|
||||
class JsonView implements ViewInterface
|
||||
{
|
||||
/**
|
||||
* Definition for the class name exposure configuration,
|
||||
* that is, if the class name of an object should also be
|
||||
* part of the output JSON, if configured.
|
||||
*
|
||||
* Setting this value, the object's class name is fully
|
||||
* put out, including the namespace.
|
||||
*/
|
||||
public const EXPOSE_CLASSNAME_FULLY_QUALIFIED = 1;
|
||||
|
||||
/**
|
||||
* Puts out only the actual class name without namespace.
|
||||
* See EXPOSE_CLASSNAME_FULL for the meaning of the constant at all.
|
||||
*/
|
||||
public const EXPOSE_CLASSNAME_UNQUALIFIED = 2;
|
||||
|
||||
/**
|
||||
* Only variables whose name is contained in this array will be rendered
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected array $variablesToRender = ['value'];
|
||||
|
||||
protected string $currentVariable = '';
|
||||
|
||||
/**
|
||||
* The rendering configuration for this JSON view which
|
||||
* determines which properties of each variable to render.
|
||||
*
|
||||
* The configuration array must have the following structure:
|
||||
*
|
||||
* Example 1:
|
||||
*
|
||||
* [
|
||||
* 'variable1' => [
|
||||
* '_only' => ['property1', 'property2', ...]
|
||||
* ],
|
||||
* 'variable2' => [
|
||||
* '_exclude' => ['property3', 'property4, ...]
|
||||
* ],
|
||||
* 'variable3' => [
|
||||
* '_exclude' => ['secretTitle'],
|
||||
* '_descend' => [
|
||||
* 'customer' => [
|
||||
* '_only' => ['firstName', 'lastName']
|
||||
* ]
|
||||
* ]
|
||||
* ],
|
||||
* 'somearrayvalue' => [
|
||||
* '_descendAll' => [
|
||||
* '_only' => ['property1']
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* Of variable1 only property1 and property2 will be included.
|
||||
* Of variable2 all properties except property3 and property4
|
||||
* are used.
|
||||
* Of variable3 all properties except secretTitle are included.
|
||||
*
|
||||
* If a property value is an array or object, it is not included
|
||||
* by default. If, however, such a property is listed in a "_descend"
|
||||
* section, the renderer will descend into this sub structure and
|
||||
* include all its properties (of the next level).
|
||||
*
|
||||
* The configuration of each property in "_descend" has the same syntax
|
||||
* as the top level. Therefore - theoretically - infinitely nested
|
||||
* structures can be configured.
|
||||
*
|
||||
* To export indexed arrays the "_descendAll" section can be used to
|
||||
* include all array keys for the output. The configuration inside a
|
||||
* "_descendAll" will be applied to each array element.
|
||||
*
|
||||
*
|
||||
* Example 2: exposing object identifier
|
||||
*
|
||||
* [
|
||||
* 'variableFoo' => [
|
||||
* '_exclude' => ['secretTitle'],
|
||||
* '_descend' => [
|
||||
* 'customer' => [ // consider 'customer' being a persisted entity
|
||||
* '_only' => ['firstName'],
|
||||
* '_exposeObjectIdentifier' => TRUE,
|
||||
* '_exposedObjectIdentifierKey' => 'guid'
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* Note for entity objects you are able to expose the object's identifier
|
||||
* also, just add an "_exposeObjectIdentifier" directive set to TRUE and
|
||||
* an additional property '__identity' will appear keeping the persistence
|
||||
* identifier. Renaming that property name instead of '__identity' is also
|
||||
* possible with the directive "_exposedObjectIdentifierKey".
|
||||
* Example 2 above would output (summarized):
|
||||
* {"customer":{"firstName":"John","guid":"892693e4-b570-46fe-af71-1ad32918fb64"}}
|
||||
*
|
||||
*
|
||||
* Example 3: exposing object's class name
|
||||
*
|
||||
* [
|
||||
* 'variableFoo' => [
|
||||
* '_exclude' => ['secretTitle'],
|
||||
* '_descend' => [
|
||||
* 'customer' => [ // consider 'customer' being an object
|
||||
* '_only' => ['firstName'],
|
||||
* '_exposeClassName' => \TYPO3\CMS\Extbase\Mvc\View\JsonView::EXPOSE_CLASSNAME_FULLY_QUALIFIED
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* The ``_exposeClassName`` is similar to the objectIdentifier one, but the class name is added to the
|
||||
* JSON object output, for example (summarized):
|
||||
* {"customer":{"firstName":"John","__class":"Acme\Foo\Domain\Model\Customer"}}
|
||||
*
|
||||
* The other option is EXPOSE_CLASSNAME_UNQUALIFIED which only will give the last part of the class
|
||||
* without the namespace, for example (summarized):
|
||||
* {"customer":{"firstName":"John","__class":"Customer"}}
|
||||
* This might be of interest to not provide information about the package or domain structure behind.
|
||||
*/
|
||||
protected array $configuration = [];
|
||||
|
||||
protected PersistenceManagerInterface $persistenceManager;
|
||||
|
||||
/**
|
||||
* View variables and their values
|
||||
*/
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager): void
|
||||
{
|
||||
$this->persistenceManager = $persistenceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a variable to $this->viewData.
|
||||
* Can be chained, so $this->view->assign(..., ...)->assign(..., ...); is possible
|
||||
*
|
||||
* @param string $key Key of variable
|
||||
* @param mixed $value Value of object
|
||||
* @return self an instance of $this, to enable chaining
|
||||
*/
|
||||
public function assign(string $key, mixed $value): ViewInterface
|
||||
{
|
||||
$this->variables[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple variables to $this->viewData.
|
||||
*
|
||||
* @param array $values array in the format array(key1 => value1, key2 => value2).
|
||||
* @return self an instance of $this, to enable chaining
|
||||
*/
|
||||
public function assignMultiple(array $values): ViewInterface
|
||||
{
|
||||
foreach ($values as $key => $value) {
|
||||
$this->assign($key, $value);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which variables this JsonView should render
|
||||
* By default only the variable 'value' will be rendered
|
||||
*
|
||||
* @param string[] $variablesToRender
|
||||
*/
|
||||
public function setVariablesToRender(array $variablesToRender): void
|
||||
{
|
||||
$this->variablesToRender = $variablesToRender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $configuration The rendering configuration for this JSON view
|
||||
*/
|
||||
public function setConfiguration(array $configuration): void
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the value view variable to a serializable
|
||||
* array representation using a YAML view configuration and JSON encodes
|
||||
* the result.
|
||||
*
|
||||
* @return string The JSON encoded variables
|
||||
*/
|
||||
public function render(string $templateFileName = ''): string
|
||||
{
|
||||
$propertiesToRender = $this->renderArray();
|
||||
return json_encode($propertiesToRender, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the configuration and transforms the value to a serializable array.
|
||||
*/
|
||||
protected function renderArray(): mixed
|
||||
{
|
||||
if (count($this->variablesToRender) === 1) {
|
||||
$firstLevel = false;
|
||||
$variableName = current($this->variablesToRender);
|
||||
$this->currentVariable = $variableName;
|
||||
$valueToRender = $this->variables[$variableName] ?? null;
|
||||
$configuration = $this->configuration[$variableName] ?? [];
|
||||
} else {
|
||||
$firstLevel = true;
|
||||
$valueToRender = [];
|
||||
foreach ($this->variablesToRender as $variableName) {
|
||||
$valueToRender[$variableName] = $this->variables[$variableName] ?? null;
|
||||
}
|
||||
$configuration = $this->configuration;
|
||||
}
|
||||
return $this->transformValue($valueToRender, $configuration, $firstLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a value depending on type recursively using the
|
||||
* supplied configuration.
|
||||
*
|
||||
* @param mixed $value The value to transform
|
||||
* @param array $configuration Configuration for transforming the value
|
||||
* @return mixed The transformed value
|
||||
*/
|
||||
protected function transformValue(mixed $value, array $configuration, bool $firstLevel = false): mixed
|
||||
{
|
||||
// ObjectStorage returns $key as string, which causes the resulting JSON to be an object instead of the expected array
|
||||
if ($value instanceof ObjectStorage) {
|
||||
$value = $value->toArray();
|
||||
}
|
||||
if (is_array($value) || $value instanceof \ArrayAccess) {
|
||||
$array = [];
|
||||
foreach ($value as $key => $element) {
|
||||
if ($firstLevel) {
|
||||
$this->currentVariable = $key;
|
||||
}
|
||||
if (isset($configuration['_descendAll']) && is_array($configuration['_descendAll'])) {
|
||||
$array[$key] = $this->transformValue($element, $configuration['_descendAll']);
|
||||
} else {
|
||||
if (isset($configuration['_only']) && is_array($configuration['_only']) && !in_array($key, $configuration['_only'], true)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($configuration['_exclude']) && is_array($configuration['_exclude']) && in_array($key, $configuration['_exclude'], true)) {
|
||||
continue;
|
||||
}
|
||||
$array[$key] = $this->transformValue($element, $configuration[$key] ?? []);
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
if (is_object($value)) {
|
||||
return $this->transformObject($value, $configuration);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the given object structure in order to transform it into an array structure.
|
||||
*
|
||||
* @param object $object Object to traverse
|
||||
* @param array $configuration Configuration for transforming the given object or NULL
|
||||
* @return array|string Object structure as an array or as a rendered string (for a DateTime instance)
|
||||
*/
|
||||
protected function transformObject(object $object, array $configuration): array|string
|
||||
{
|
||||
if ($object instanceof \DateTimeInterface) {
|
||||
return $object->format(\DateTimeInterface::ATOM);
|
||||
}
|
||||
$propertyNames = ObjectAccess::getGettablePropertyNames($object);
|
||||
$propertiesToRender = [];
|
||||
foreach ($propertyNames as $propertyName) {
|
||||
if (isset($configuration['_only']) && is_array($configuration['_only']) && !in_array($propertyName, $configuration['_only'], true)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($configuration['_exclude']) && is_array($configuration['_exclude']) && in_array($propertyName, $configuration['_exclude'], true)) {
|
||||
continue;
|
||||
}
|
||||
$propertyValue = ObjectAccess::getProperty($object, $propertyName);
|
||||
if (!is_array($propertyValue) && !is_object($propertyValue)) {
|
||||
$propertiesToRender[$propertyName] = $propertyValue;
|
||||
} elseif (isset($configuration['_descend']) && array_key_exists($propertyName, $configuration['_descend'])) {
|
||||
$propertiesToRender[$propertyName] = $this->transformValue($propertyValue, $configuration['_descend'][$propertyName]);
|
||||
} elseif (isset($configuration['_recursive']) && in_array($propertyName, $configuration['_recursive'])) {
|
||||
$propertiesToRender[$propertyName] = $this->transformValue($propertyValue, $this->configuration[$this->currentVariable]);
|
||||
}
|
||||
}
|
||||
if (isset($configuration['_exposeObjectIdentifier']) && $configuration['_exposeObjectIdentifier'] === true) {
|
||||
if (isset($configuration['_exposedObjectIdentifierKey']) && strlen($configuration['_exposedObjectIdentifierKey']) > 0) {
|
||||
$identityKey = $configuration['_exposedObjectIdentifierKey'];
|
||||
} else {
|
||||
$identityKey = '__identity';
|
||||
}
|
||||
$propertiesToRender[$identityKey] = $this->persistenceManager->getIdentifierByObject($object);
|
||||
}
|
||||
if (isset($configuration['_exposeClassName']) && ($configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_FULLY_QUALIFIED || $configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_UNQUALIFIED)) {
|
||||
$className = get_class($object);
|
||||
$classNameParts = explode('\\', $className);
|
||||
$propertiesToRender['__class'] = ($configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_FULLY_QUALIFIED ? $className : array_pop($classNameParts));
|
||||
}
|
||||
return $propertiesToRender;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?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\Extbase\Mvc\Web;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Module\ExtbaseModule;
|
||||
use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
|
||||
use TYPO3\CMS\Core\Http\UploadedFile;
|
||||
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception as MvcException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException;
|
||||
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
||||
use TYPO3\CMS\Extbase\Mvc\Request;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\Extbase\Service\ExtensionService;
|
||||
|
||||
/**
|
||||
* Builds an extbase web request.
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class RequestBuilder
|
||||
{
|
||||
public function __construct(
|
||||
protected ConfigurationManagerInterface $configurationManager,
|
||||
protected ExtensionService $extensionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Decorate a PSR-7 request as extbase web Request with the extbase attribute.
|
||||
*/
|
||||
public function build(ServerRequestInterface $mainRequest): RequestInterface
|
||||
{
|
||||
$configuration = [];
|
||||
// Parameters, which are not part of the request URL (e.g. due to "useArgumentsWithoutNamespace"), which however
|
||||
// need to be taken into account on building the extbase request. Usually those are "controller" and "action".
|
||||
$fallbackParameters = [];
|
||||
// To be used in TYPO3 Backend for Extbase modules that do not need the "namespaces" GET and POST parameters anymore.
|
||||
$useArgumentsWithoutNamespace = false;
|
||||
// Fetch requested module from the main request. This is only used for TYPO3 Backend Modules.
|
||||
$module = $mainRequest->getAttribute('module');
|
||||
if ($module instanceof ExtbaseModule) {
|
||||
$configuration = [
|
||||
'controllerConfiguration' => $module->getControllerActions(),
|
||||
];
|
||||
$useArgumentsWithoutNamespace = true;
|
||||
// Ensure the "controller" and "action" information are added as fallback parameters.
|
||||
if ($routeOptions = $mainRequest->getAttribute('route')?->getOptions()) {
|
||||
$fallbackParameters['controller'] = $routeOptions['controller'] ?? null;
|
||||
$fallbackParameters['action'] = $routeOptions['action'];
|
||||
}
|
||||
}
|
||||
$defaultValues = $this->loadDefaultValues($configuration);
|
||||
$pluginNamespace = $this->extensionService->getPluginNamespace(
|
||||
$defaultValues->getExtensionName(),
|
||||
$defaultValues->getPluginName()
|
||||
);
|
||||
$queryArguments = $mainRequest->getAttribute('routing');
|
||||
if ($useArgumentsWithoutNamespace) {
|
||||
$parameters = $mainRequest->getQueryParams();
|
||||
} elseif ($queryArguments instanceof PageArguments) {
|
||||
$parameters = $queryArguments->get($pluginNamespace) ?? [];
|
||||
} else {
|
||||
$parameters = $mainRequest->getQueryParams()[$pluginNamespace] ?? [];
|
||||
}
|
||||
$parameters = is_array($parameters) ? $parameters : [];
|
||||
if ($fallbackParameters !== []) {
|
||||
// Enhance with fallback parameters, such as "controller" and "action"
|
||||
$parameters = array_replace_recursive($fallbackParameters, $parameters);
|
||||
}
|
||||
if ($mainRequest->getMethod() === 'POST') {
|
||||
if ($useArgumentsWithoutNamespace) {
|
||||
$postParameters = $mainRequest->getParsedBody();
|
||||
} else {
|
||||
$postParameters = $mainRequest->getParsedBody()[$pluginNamespace] ?? [];
|
||||
}
|
||||
$postParameters = is_array($postParameters) ? $postParameters : [];
|
||||
$parameters = array_replace_recursive($parameters, $postParameters);
|
||||
}
|
||||
|
||||
$files = $mainRequest->getUploadedFiles();
|
||||
if (!$useArgumentsWithoutNamespace) {
|
||||
$files = $files[$pluginNamespace] ?? [];
|
||||
if ($files instanceof UploadedFile) {
|
||||
throw new InvalidArgumentNameException(
|
||||
'Using only the plugin namespace as argument name is not allowed for uploaded files. Please use plugin_namespace[argument_name] instead.',
|
||||
1722542546
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge UploadedFiles into request parameters, so that they are available as arguments
|
||||
// for property mapping (e.g. in ext:form or a custom file upload TypeConverter).
|
||||
$parameters = array_replace_recursive($parameters, $files);
|
||||
|
||||
$controllerClassName = $this->resolveControllerClassName($defaultValues, $parameters);
|
||||
$actionName = $this->resolveActionName($defaultValues, $controllerClassName, $parameters);
|
||||
|
||||
$extbaseAttribute = new ExtbaseRequestParameters();
|
||||
$extbaseAttribute->setPluginName($defaultValues->getPluginName());
|
||||
$extbaseAttribute->setControllerExtensionName($defaultValues->getExtensionName());
|
||||
$extbaseAttribute->setControllerAliasToClassNameMapping($defaultValues->getControllerAliasToClassMapping());
|
||||
$extbaseAttribute->setControllerName($defaultValues->getControllerAliasForControllerClassName($controllerClassName));
|
||||
$extbaseAttribute->setControllerActionName($actionName);
|
||||
$extbaseAttribute->setUploadedFiles($files);
|
||||
|
||||
if (isset($parameters['format']) && is_string($parameters['format']) && $parameters['format'] !== '') {
|
||||
$extbaseAttribute->setFormat(preg_replace('/[^a-zA-Z0-9]+/', '', $parameters['format']));
|
||||
} else {
|
||||
$extbaseAttribute->setFormat($defaultValues->getDefaultFormat());
|
||||
}
|
||||
foreach ($parameters as $argumentName => $argumentValue) {
|
||||
$extbaseAttribute->setArgument($argumentName, $argumentValue);
|
||||
}
|
||||
return new Request($mainRequest->withAttribute('extbase', $extbaseAttribute));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MvcException
|
||||
*/
|
||||
protected function loadDefaultValues(array $configuration = []): RequestBuilderDefaultValues
|
||||
{
|
||||
// todo: See comment in \TYPO3\CMS\Extbase\Core\Bootstrap::initializeConfiguration for further explanation
|
||||
// todo: on why we shouldn't use the configuration manager here.
|
||||
$configuration = array_replace_recursive($this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK), $configuration);
|
||||
try {
|
||||
return RequestBuilderDefaultValues::fromConfiguration($configuration);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw MvcException::fromPrevious($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current ControllerName extracted from given $parameters.
|
||||
* If no controller is specified, the defaultControllerName will be returned.
|
||||
* If that's not available, an exception is thrown.
|
||||
*
|
||||
* @throws InvalidControllerNameException
|
||||
* @throws MvcException if the controller could not be resolved
|
||||
* @throws PageNotFoundException
|
||||
* @return class-string
|
||||
*/
|
||||
protected function resolveControllerClassName(RequestBuilderDefaultValues $defaultValues, array $parameters): string
|
||||
{
|
||||
if (!isset($parameters['controller']) || $parameters['controller'] === '') {
|
||||
return $defaultValues->getDefaultControllerClassName();
|
||||
}
|
||||
$controllerClassName = $defaultValues->getControllerClassNameForAlias($parameters['controller']) ?? '';
|
||||
if ($defaultValues->getAllowedControllerActionsOfController($controllerClassName) === []) {
|
||||
$configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
if (isset($configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) && (bool)$configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) {
|
||||
throw new PageNotFoundException('The requested resource was not found', 1313857897);
|
||||
}
|
||||
if (isset($configuration['mvc']['callDefaultActionIfActionCantBeResolved']) && (bool)$configuration['mvc']['callDefaultActionIfActionCantBeResolved']) {
|
||||
return $defaultValues->getDefaultControllerClassName();
|
||||
}
|
||||
throw new InvalidControllerNameException(
|
||||
'The controller "' . $parameters['controller'] . '" is not allowed by plugin "' . $defaultValues->getPluginName() . '". Please check for TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configurePlugin() in your ext_localconf.php.',
|
||||
1313855173
|
||||
);
|
||||
}
|
||||
return preg_replace('/[^a-zA-Z0-9\\\\]+/', '', $controllerClassName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current actionName extracted from given $parameters.
|
||||
* If no action is specified, the defaultActionName will be returned.
|
||||
* If that's not available or the specified action is not defined in the current plugin, an exception is thrown.
|
||||
*
|
||||
* @param class-string $controllerClassName
|
||||
* @throws InvalidActionNameException
|
||||
* @throws MvcException
|
||||
* @throws PageNotFoundException
|
||||
* @return non-empty-string
|
||||
*/
|
||||
protected function resolveActionName(RequestBuilderDefaultValues $defaultValues, string $controllerClassName, array $parameters): string
|
||||
{
|
||||
$defaultActionName = $defaultValues->getDefaultActionName($controllerClassName);
|
||||
if (!isset($parameters['action']) || $parameters['action'] === '') {
|
||||
if ($defaultActionName === '') {
|
||||
throw new MvcException('The default action can not be determined for controller "' . $controllerClassName . '". Please check TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configurePlugin() in your ext_localconf.php.', 1295479651);
|
||||
}
|
||||
return $defaultActionName;
|
||||
}
|
||||
$actionName = $parameters['action'];
|
||||
$allowedActionNames = $defaultValues->getAllowedControllerActionsOfController($controllerClassName);
|
||||
if (!in_array($actionName, $allowedActionNames)) {
|
||||
$configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
if (isset($configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) && (bool)$configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) {
|
||||
throw new PageNotFoundException('The requested resource was not found', 1313857898);
|
||||
}
|
||||
if (isset($configuration['mvc']['callDefaultActionIfActionCantBeResolved']) && (bool)$configuration['mvc']['callDefaultActionIfActionCantBeResolved']) {
|
||||
if ($defaultActionName === '') {
|
||||
throw new MvcException('The default action can not be determined for controller "' . $controllerClassName . '". Please check TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configurePlugin() in your ext_localconf.php.', 1679048627);
|
||||
}
|
||||
return $defaultActionName;
|
||||
}
|
||||
throw new InvalidActionNameException('The action "' . $actionName . '" (controller "' . $controllerClassName . '") is not allowed by this plugin / module. Please check TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configurePlugin() in your ext_localconf.php / array key "controllerActions" defined in your Configuration/Backend/Modules.php.', 1313855175);
|
||||
}
|
||||
return preg_replace('/[^a-zA-Z0-9]+/', '', $actionName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?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\Extbase\Mvc\Web;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
final class RequestBuilderDefaultValues
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $extensionName
|
||||
* @param non-empty-string $pluginName
|
||||
* @param class-string $defaultControllerClassName
|
||||
* @param non-empty-string $defaultControllerAlias
|
||||
* @param non-empty-string $defaultFormat
|
||||
*/
|
||||
private function __construct(
|
||||
private readonly string $extensionName,
|
||||
private readonly string $pluginName,
|
||||
private readonly string $defaultControllerClassName,
|
||||
private readonly string $defaultControllerAlias,
|
||||
private readonly string $defaultFormat,
|
||||
private readonly array $allowedControllerActions,
|
||||
private readonly array $controllerAliasToClassMapping,
|
||||
private readonly array $controllerClassToAliasMapping,
|
||||
) {}
|
||||
|
||||
public static function fromConfiguration(array $configuration): self
|
||||
{
|
||||
$extensionName = $configuration['extensionName'] ?? null;
|
||||
$extensionName = is_string($extensionName) && $extensionName !== '' ? $extensionName : null;
|
||||
|
||||
$pluginName = $configuration['pluginName'] ?? null;
|
||||
$pluginName = is_string($pluginName) && $pluginName !== '' ? $pluginName : null;
|
||||
|
||||
$controllerConfigurations = $configuration['controllerConfiguration'] ?? [];
|
||||
$controllerConfigurations = is_array($controllerConfigurations) ? $controllerConfigurations : [];
|
||||
|
||||
if (!is_string($extensionName)) {
|
||||
throw new \InvalidArgumentException('"extensionName" is not properly configured. Request can\'t be dispatched!', 1289843275);
|
||||
}
|
||||
if (!is_string($pluginName)) {
|
||||
throw new \InvalidArgumentException('"pluginName" is not properly configured. Request can\'t be dispatched!', 1289843277);
|
||||
}
|
||||
|
||||
if ($controllerConfigurations === []) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf(
|
||||
'The default controller for extension "%s" and plugin "%s" can not be determined. '
|
||||
. 'Please check for TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin() in your ext_localconf.php.',
|
||||
$extensionName,
|
||||
$pluginName
|
||||
),
|
||||
1316104317
|
||||
);
|
||||
}
|
||||
|
||||
$defaultFormat = $configuration['format'] ?? null;
|
||||
$defaultFormat = is_string($defaultFormat) && $defaultFormat !== '' ? $defaultFormat : 'html';
|
||||
|
||||
$defaultControllerClassName = null;
|
||||
$defaultControllerAlias = null;
|
||||
|
||||
$allowedControllerActions = [];
|
||||
$controllerClassToAliasMapping = [];
|
||||
$controllerAliasToClassMapping = [];
|
||||
|
||||
$firstItem = true;
|
||||
foreach ($controllerConfigurations as $controllerClassName => $controllerConfiguration) {
|
||||
if (!is_string($controllerClassName) || $controllerClassName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_array($controllerConfiguration)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$actions = $controllerConfiguration['actions'] ?? [];
|
||||
$actions = is_array($actions) ? $actions : [];
|
||||
|
||||
if ($actions === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$controllerClassName = $controllerConfiguration['className'] ?? null;
|
||||
$controllerClassName = is_string($controllerClassName) && $controllerClassName !== '' ? $controllerClassName : null;
|
||||
|
||||
if ($controllerClassName === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$controllerAlias = $controllerConfiguration['alias'] ?? null;
|
||||
$controllerAlias = is_string($controllerAlias) && $controllerAlias !== '' ? $controllerAlias : null;
|
||||
|
||||
if ($controllerAlias === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allowedControllerActions[$controllerClassName] = $actions;
|
||||
$controllerClassToAliasMapping[$controllerClassName] = $controllerAlias;
|
||||
$controllerAliasToClassMapping[$controllerAlias] = $controllerClassName;
|
||||
|
||||
if ($firstItem) {
|
||||
$defaultControllerClassName = $controllerClassName;
|
||||
$defaultControllerAlias = $controllerAlias;
|
||||
}
|
||||
|
||||
$firstItem = false;
|
||||
}
|
||||
|
||||
if ($defaultControllerClassName === null || $defaultControllerAlias === null) {
|
||||
throw new \LogicException(
|
||||
'Either $defaultControllerClassName or $defaultControllerAlias are unexpectedly null',
|
||||
1679051921
|
||||
);
|
||||
}
|
||||
|
||||
if ($allowedControllerActions === []) {
|
||||
throw new \LengthException(
|
||||
'$allowedControllerActions is expected to not be empty',
|
||||
1679051891
|
||||
);
|
||||
}
|
||||
|
||||
return new self(
|
||||
$extensionName,
|
||||
$pluginName,
|
||||
$defaultControllerClassName,
|
||||
$defaultControllerAlias,
|
||||
$defaultFormat,
|
||||
$allowedControllerActions,
|
||||
$controllerAliasToClassMapping,
|
||||
$controllerClassToAliasMapping,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getExtensionName(): string
|
||||
{
|
||||
return $this->extensionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getPluginName(): string
|
||||
{
|
||||
return $this->pluginName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
public function getDefaultControllerClassName(): string
|
||||
{
|
||||
return $this->defaultControllerClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getDefaultControllerAlias(): string
|
||||
{
|
||||
return $this->defaultControllerAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getDefaultFormat(): string
|
||||
{
|
||||
return $this->defaultFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<class-string, list<string>>
|
||||
*/
|
||||
public function getAllowedControllerActions(): array
|
||||
{
|
||||
return $this->allowedControllerActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getAllowedControllerActionsOfController(string $controllerClassName): array
|
||||
{
|
||||
return $this->allowedControllerActions[$controllerClassName] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<non-empty-string, class-string>
|
||||
*/
|
||||
public function getControllerAliasToClassMapping(): array
|
||||
{
|
||||
return $this->controllerAliasToClassMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<class-string, non-empty-string>
|
||||
*/
|
||||
public function getControllerClassToAliasMapping(): array
|
||||
{
|
||||
return $this->controllerClassToAliasMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $controllerAlias
|
||||
* @return class-string|null
|
||||
*/
|
||||
public function getControllerClassNameForAlias(string $controllerAlias): ?string
|
||||
{
|
||||
return $this->controllerAliasToClassMapping[$controllerAlias] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $controllerClassName
|
||||
* @return non-empty-string|null
|
||||
*/
|
||||
public function getControllerAliasForControllerClassName(string $controllerClassName): ?string
|
||||
{
|
||||
return $this->controllerClassToAliasMapping[$controllerClassName] ?? null;
|
||||
}
|
||||
|
||||
public function getDefaultActionName(string $controllerClassName): ?string
|
||||
{
|
||||
$actions = $this->allowedControllerActions[$controllerClassName] ?? [];
|
||||
return $actions[0] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
<?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\Extbase\Mvc\Web\Routing;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
|
||||
use TYPO3\CMS\Backend\Routing\Route;
|
||||
use TYPO3\CMS\Core\Http\ApplicationType;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentValueException;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
|
||||
use TYPO3\CMS\Extbase\Service\ExtensionService;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* URI Builder for extbase requests.
|
||||
*/
|
||||
#[Autoconfigure(public: true, shared: false)]
|
||||
class UriBuilder
|
||||
{
|
||||
protected RequestInterface $request;
|
||||
|
||||
protected array $arguments = [];
|
||||
protected array $lastArguments = [];
|
||||
protected string $section = '';
|
||||
protected bool $createAbsoluteUri = false;
|
||||
protected ?string $absoluteUriScheme = null;
|
||||
protected bool|string|int $addQueryString = false;
|
||||
protected array $argumentsToBeExcludedFromQueryString = [];
|
||||
protected bool $linkAccessRestrictedPages = false;
|
||||
protected ?int $targetPageUid = null;
|
||||
protected int $targetPageType = 0;
|
||||
protected ?string $language = null;
|
||||
protected bool $noCache = false;
|
||||
protected string $format = '';
|
||||
protected ?string $argumentPrefix = null;
|
||||
|
||||
public function __construct(
|
||||
protected readonly ExtensionService $extensionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets the current request
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setRequest(RequestInterface $request): UriBuilder
|
||||
{
|
||||
$this->request = $request;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional query parameters.
|
||||
* If you want to "prefix" arguments, you can pass in multidimensional arrays:
|
||||
* array('prefix1' => array('foo' => 'bar')) gets "&prefix1[foo]=bar"
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setArguments(array $arguments): UriBuilder
|
||||
{
|
||||
$this->arguments = $arguments;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* If specified, adds a given HTML anchor to the URI (#...)
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setSection(string $section): UriBuilder
|
||||
{
|
||||
$this->section = $section;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSection(): string
|
||||
{
|
||||
return $this->section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the format of the target (e.g. "html" or "xml")
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setFormat(string $format): UriBuilder
|
||||
{
|
||||
$this->format = $format;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getFormat(): string
|
||||
{
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, the URI is prepended with the current base URI. Defaults to FALSE.
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setCreateAbsoluteUri(bool $createAbsoluteUri): UriBuilder
|
||||
{
|
||||
$this->createAbsoluteUri = $createAbsoluteUri;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getCreateAbsoluteUri(): bool
|
||||
{
|
||||
return $this->createAbsoluteUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getAbsoluteUriScheme(): ?string
|
||||
{
|
||||
return $this->absoluteUriScheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scheme that should be used for absolute URIs in FE mode
|
||||
*
|
||||
* @param string $absoluteUriScheme the scheme to be used for absolute URIs
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setAbsoluteUriScheme(string $absoluteUriScheme): UriBuilder
|
||||
{
|
||||
$this->absoluteUriScheme = $absoluteUriScheme;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces a URI / link to a page to a specific language (or use "current")
|
||||
*/
|
||||
public function setLanguage(?string $language): UriBuilder
|
||||
{
|
||||
$this->language = $language;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getLanguage(): ?string
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, the current query parameters will be merged with $this->arguments in backend context.
|
||||
* In frontend context, setting this property will only include mapped query arguments from the
|
||||
* Page Routing. To include any - possible "unsafe" - GET parameters, the property has to be set
|
||||
* to "untrusted". Defaults to FALSE.
|
||||
*
|
||||
* @param bool|string|int $addQueryString is set to "1", "true", "0", "false" or "untrusted"
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
* @see https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Functions/Typolink.html#addquerystring
|
||||
*/
|
||||
public function setAddQueryString(bool|string|int $addQueryString): UriBuilder
|
||||
{
|
||||
$this->addQueryString = $addQueryString;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getAddQueryString(): bool|string|int
|
||||
{
|
||||
return $this->addQueryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of arguments to be excluded from the query parameters
|
||||
* Only active if addQueryString is set
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
* @see https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Functions/Typolink.html#addquerystring
|
||||
* @see setAddQueryString()
|
||||
*/
|
||||
public function setArgumentsToBeExcludedFromQueryString(array $argumentsToBeExcludedFromQueryString): UriBuilder
|
||||
{
|
||||
$this->argumentsToBeExcludedFromQueryString = $argumentsToBeExcludedFromQueryString;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getArgumentsToBeExcludedFromQueryString(): array
|
||||
{
|
||||
return $this->argumentsToBeExcludedFromQueryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the prefix to be used for all arguments.
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setArgumentPrefix(string $argumentPrefix): UriBuilder
|
||||
{
|
||||
$this->argumentPrefix = $argumentPrefix;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getArgumentPrefix(): ?string
|
||||
{
|
||||
return $this->argumentPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, URIs for pages without access permissions will be created
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setLinkAccessRestrictedPages(bool $linkAccessRestrictedPages): UriBuilder
|
||||
{
|
||||
$this->linkAccessRestrictedPages = $linkAccessRestrictedPages;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getLinkAccessRestrictedPages(): bool
|
||||
{
|
||||
return $this->linkAccessRestrictedPages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uid of the target page
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setTargetPageUid(int $targetPageUid): UriBuilder
|
||||
{
|
||||
$this->targetPageUid = $targetPageUid;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getTargetPageUid(): ?int
|
||||
{
|
||||
return $this->targetPageUid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the page type of the target URI. Defaults to 0
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setTargetPageType(int $targetPageType): UriBuilder
|
||||
{
|
||||
$this->targetPageType = $targetPageType;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getTargetPageType(): int
|
||||
{
|
||||
return $this->targetPageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* by default FALSE; if TRUE, &no_cache=1 will be appended to the URI
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function setNoCache(bool $noCache): UriBuilder
|
||||
{
|
||||
$this->noCache = $noCache;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getNoCache(): bool
|
||||
{
|
||||
return $this->noCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the arguments being used for the last URI being built.
|
||||
* This is only set after build() / uriFor() has been called.
|
||||
*
|
||||
* @return array The last arguments
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getLastArguments(): array
|
||||
{
|
||||
return $this->lastArguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all UriBuilder options to their default value
|
||||
*
|
||||
* @return static the current UriBuilder to allow method chaining
|
||||
*/
|
||||
public function reset(): UriBuilder
|
||||
{
|
||||
$this->arguments = [];
|
||||
$this->section = '';
|
||||
$this->format = '';
|
||||
$this->language = null;
|
||||
$this->createAbsoluteUri = false;
|
||||
$this->addQueryString = false;
|
||||
$this->argumentsToBeExcludedFromQueryString = [];
|
||||
$this->linkAccessRestrictedPages = false;
|
||||
$this->targetPageUid = null;
|
||||
$this->targetPageType = 0;
|
||||
$this->noCache = false;
|
||||
$this->argumentPrefix = null;
|
||||
$this->absoluteUriScheme = null;
|
||||
// $this->request MUST NOT be reset here because the request is actually a hard dependency
|
||||
// and not part of the internal state of this object.
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URI used for linking to an Extbase action.
|
||||
* Works in Frontend and Backend mode of TYPO3.
|
||||
*
|
||||
* @param string|null $actionName Name of the action to be called
|
||||
* @param array|null $controllerArguments Additional query parameters. Will be "namespaced" and merged with $this->arguments.
|
||||
* @param string|null $controllerName Name of the target controller. If not set, current ControllerName is used.
|
||||
* @param string|null $extensionName Name of the target extension, without underscores. If not set, current ExtensionName is used.
|
||||
* @param string|null $pluginName Name of the target plugin. If not set, current PluginName is used.
|
||||
* @return string the rendered URI
|
||||
* @see build()
|
||||
*/
|
||||
public function uriFor(
|
||||
?string $actionName = null,
|
||||
?array $controllerArguments = null,
|
||||
?string $controllerName = null,
|
||||
?string $extensionName = null,
|
||||
?string $pluginName = null
|
||||
): string {
|
||||
$controllerArguments = $controllerArguments ?? [];
|
||||
|
||||
if ($actionName !== null) {
|
||||
$controllerArguments['action'] = $actionName;
|
||||
}
|
||||
if ($controllerName !== null) {
|
||||
$controllerArguments['controller'] = $controllerName;
|
||||
} else {
|
||||
$controllerArguments['controller'] = $this->request->getControllerName();
|
||||
}
|
||||
if ($extensionName === null) {
|
||||
$extensionName = $this->request->getControllerExtensionName();
|
||||
}
|
||||
$isFrontend = ApplicationType::fromRequest($this->request)->isFrontend();
|
||||
if ($pluginName === null && $isFrontend) {
|
||||
$pluginName = $this->extensionService->getPluginNameByAction($extensionName, $controllerArguments['controller'], $controllerArguments['action'] ?? null);
|
||||
}
|
||||
if ($pluginName === null) {
|
||||
$pluginName = $this->request->getPluginName();
|
||||
}
|
||||
if ($this->targetPageUid === null && $isFrontend) {
|
||||
$this->targetPageUid = $this->extensionService->getTargetPidByPlugin($extensionName, $pluginName);
|
||||
}
|
||||
if ($this->format !== '') {
|
||||
$controllerArguments['format'] = $this->format;
|
||||
}
|
||||
if ($this->argumentPrefix !== null) {
|
||||
$prefixedControllerArguments = [$this->argumentPrefix => $controllerArguments];
|
||||
} elseif (!$isFrontend) {
|
||||
$prefixedControllerArguments = $controllerArguments;
|
||||
// Backend UriBuilder needs the route, which usually maps to the "route" parameter, which can be
|
||||
// found in "Configuration/Backend/Modules.php" as the main key - that is the actual base route
|
||||
// for the backend module, which in Extbase-speak is called a "pluginName"
|
||||
$prefixedControllerArguments['route'] = $pluginName;
|
||||
} else {
|
||||
$pluginNamespace = $this->extensionService->getPluginNamespace($extensionName, $pluginName);
|
||||
$prefixedControllerArguments = [$pluginNamespace => $controllerArguments];
|
||||
}
|
||||
ArrayUtility::mergeRecursiveWithOverrule($this->arguments, $prefixedControllerArguments);
|
||||
return $this->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the URI
|
||||
* Depending on the current context this calls buildBackendUri() or buildFrontendUri()
|
||||
*
|
||||
* @return string The URI
|
||||
* @see buildBackendUri()
|
||||
* @see buildFrontendUri()
|
||||
*/
|
||||
public function build(): string
|
||||
{
|
||||
if (ApplicationType::fromRequest($this->request)->isBackend()) {
|
||||
return $this->buildBackendUri();
|
||||
}
|
||||
return $this->buildFrontendUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the URI, backend flavour
|
||||
* The settings pageUid, pageType, noCache & linkAccessRestrictedPages
|
||||
* will be ignored in the backend.
|
||||
*
|
||||
* @return string The URI
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function buildBackendUri(): string
|
||||
{
|
||||
$arguments = [];
|
||||
if ($this->addQueryString && $this->addQueryString !== 'false') {
|
||||
$arguments = $this->request->getQueryParams();
|
||||
foreach ($this->argumentsToBeExcludedFromQueryString as $argumentToBeExcluded) {
|
||||
$argumentArrayToBeExcluded = [];
|
||||
parse_str($argumentToBeExcluded, $argumentArrayToBeExcluded);
|
||||
$arguments = ArrayUtility::arrayDiffKeyRecursive($arguments, $argumentArrayToBeExcluded);
|
||||
}
|
||||
} else {
|
||||
$id = $this->request->getParsedBody()['id'] ?? $this->request->getQueryParams()['id'] ?? null;
|
||||
if ($id !== null) {
|
||||
$arguments['id'] = $id;
|
||||
}
|
||||
}
|
||||
if (($route = $this->request->getAttribute('route')) instanceof Route) {
|
||||
/** @var Route $route */
|
||||
$arguments['route'] = $route->getOption('_identifier');
|
||||
}
|
||||
$arguments = array_replace_recursive($arguments, $this->arguments);
|
||||
$arguments = $this->convertDomainObjectsToIdentityArrays($arguments);
|
||||
$this->lastArguments = $arguments;
|
||||
$routeIdentifier = $arguments['route'] ?? null;
|
||||
unset($arguments['route'], $arguments['token']);
|
||||
|
||||
// In case the current route identifier is an identifier of a sub route, remove the sub route
|
||||
// part to be able to add the actually requested sub route based on the current arguments.
|
||||
if ($routeIdentifier && str_contains($routeIdentifier, '.')) {
|
||||
[$routeIdentifier] = explode('.', $routeIdentifier);
|
||||
}
|
||||
// Build route identifier to the actually requested sub route (controller / action pair) - if any -
|
||||
// and unset corresponding arguments.
|
||||
if ($routeIdentifier && isset($arguments['controller'], $arguments['action'])) {
|
||||
$routeIdentifier .= '.' . $arguments['controller'] . '_' . $arguments['action'];
|
||||
unset($arguments['controller'], $arguments['action']);
|
||||
}
|
||||
$uri = '';
|
||||
if ($routeIdentifier) {
|
||||
$backendUriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
try {
|
||||
if ($this->createAbsoluteUri) {
|
||||
$uri = (string)$backendUriBuilder->buildUriFromRoute($routeIdentifier, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_URL);
|
||||
} else {
|
||||
$uri = (string)$backendUriBuilder->buildUriFromRoute($routeIdentifier, $arguments);
|
||||
}
|
||||
} catch (RouteNotFoundException) {
|
||||
// empty URL
|
||||
}
|
||||
}
|
||||
if ($this->section !== '') {
|
||||
$uri .= '#' . $this->section;
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the URI, frontend flavour
|
||||
*
|
||||
* @return string The URI
|
||||
* @see buildTypolinkConfiguration()
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function buildFrontendUri(): string
|
||||
{
|
||||
$typolinkConfiguration = $this->buildTypolinkConfiguration();
|
||||
if ($this->createAbsoluteUri === true) {
|
||||
$typolinkConfiguration['forceAbsoluteUrl'] = true;
|
||||
if ($this->absoluteUriScheme !== null) {
|
||||
$typolinkConfiguration['forceAbsoluteUrl.']['scheme'] = $this->absoluteUriScheme;
|
||||
}
|
||||
}
|
||||
/** @var ?ContentObjectRenderer $currentContentObject */
|
||||
$currentContentObject = $this->request->getAttribute('currentContentObject');
|
||||
return $currentContentObject?->createUrl($typolinkConfiguration) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a TypoLink configuration array from the current settings
|
||||
*
|
||||
* @return array typolink configuration array
|
||||
* @see https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Functions/Typolink.html
|
||||
*/
|
||||
protected function buildTypolinkConfiguration(): array
|
||||
{
|
||||
$typolinkConfiguration = [];
|
||||
$typolinkConfiguration['parameter'] = $this->targetPageUid ?? $this->request->getAttribute('frontend.page.information')?->getId() ?? '';
|
||||
if ($this->targetPageType !== 0) {
|
||||
$typolinkConfiguration['parameter'] .= ',' . $this->targetPageType;
|
||||
} elseif ($this->format !== '') {
|
||||
$targetPageType = $this->extensionService->getTargetPageTypeByFormat($this->request->getControllerExtensionName(), $this->format);
|
||||
$typolinkConfiguration['parameter'] .= ',' . $targetPageType;
|
||||
}
|
||||
if (!empty($this->arguments)) {
|
||||
$arguments = $this->convertDomainObjectsToIdentityArrays($this->arguments);
|
||||
$this->lastArguments = $arguments;
|
||||
$typolinkConfiguration['queryParameters'] = $arguments;
|
||||
}
|
||||
if ($this->addQueryString && $this->addQueryString !== 'false') {
|
||||
$typolinkConfiguration['addQueryString'] = $this->addQueryString;
|
||||
if (!empty($this->argumentsToBeExcludedFromQueryString)) {
|
||||
$typolinkConfiguration['addQueryString.'] = [
|
||||
'exclude' => implode(',', $this->argumentsToBeExcludedFromQueryString),
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($this->language !== null) {
|
||||
$typolinkConfiguration['language'] = $this->language;
|
||||
}
|
||||
if ($this->noCache === true) {
|
||||
$typolinkConfiguration['no_cache'] = 1;
|
||||
}
|
||||
if ($this->section !== '') {
|
||||
$typolinkConfiguration['section'] = $this->section;
|
||||
}
|
||||
if ($this->linkAccessRestrictedPages === true) {
|
||||
$typolinkConfiguration['linkAccessRestrictedPages'] = 1;
|
||||
}
|
||||
return $typolinkConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively iterates through the specified arguments and turns instances of type \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
|
||||
* into an arrays containing the uid of the domain object.
|
||||
*
|
||||
* @param array $arguments The arguments to be iterated
|
||||
* @throws InvalidArgumentValueException
|
||||
* @return array The modified arguments array
|
||||
*/
|
||||
protected function convertDomainObjectsToIdentityArrays(array $arguments): array
|
||||
{
|
||||
foreach ($arguments as $argumentKey => $argumentValue) {
|
||||
// if we have a LazyLoadingProxy here, make sure to get the real instance for further processing
|
||||
if ($argumentValue instanceof LazyLoadingProxy) {
|
||||
$argumentValue = $argumentValue->_loadRealInstance();
|
||||
// also update the value in the arguments array, because the lazyLoaded object could be
|
||||
// hidden and thus the $argumentValue would be NULL.
|
||||
$arguments[$argumentKey] = $argumentValue;
|
||||
}
|
||||
if ($argumentValue instanceof \Iterator) {
|
||||
$argumentValue = $this->convertIteratorToArray($argumentValue);
|
||||
}
|
||||
if ($argumentValue instanceof DomainObjectInterface) {
|
||||
if ($argumentValue->getUid() !== null) {
|
||||
$arguments[$argumentKey] = $argumentValue->getUid();
|
||||
} elseif ($argumentValue instanceof AbstractValueObject) {
|
||||
$arguments[$argumentKey] = $this->convertTransientObjectToArray($argumentValue);
|
||||
} else {
|
||||
throw new InvalidArgumentValueException('Could not serialize Domain Object ' . get_class($argumentValue) . '. It is neither an Entity with identity properties set, nor a Value Object.', 1260881688);
|
||||
}
|
||||
} elseif (is_array($argumentValue)) {
|
||||
$arguments[$argumentKey] = $this->convertDomainObjectsToIdentityArrays($argumentValue);
|
||||
} elseif ($argumentValue instanceof \UnitEnum) {
|
||||
$arguments[$argumentKey] = $argumentValue->value ?? $argumentValue->name;
|
||||
} elseif ($argumentValue instanceof \Stringable) {
|
||||
$arguments[$argumentKey] = (string)$argumentValue;
|
||||
}
|
||||
}
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
protected function convertIteratorToArray(\Iterator $iterator): array
|
||||
{
|
||||
if (method_exists($iterator, 'toArray')) {
|
||||
$array = $iterator->toArray();
|
||||
} else {
|
||||
$array = iterator_to_array($iterator);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given object recursively into an array.
|
||||
*
|
||||
* @todo Refactor this into convertDomainObjectsToIdentityArrays()
|
||||
*/
|
||||
protected function convertTransientObjectToArray(DomainObjectInterface $object): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($object->_getProperties() as $propertyName => $propertyValue) {
|
||||
if ($propertyValue instanceof \Iterator) {
|
||||
$propertyValue = $this->convertIteratorToArray($propertyValue);
|
||||
}
|
||||
if ($propertyValue instanceof DomainObjectInterface) {
|
||||
if ($propertyValue->getUid() !== null) {
|
||||
$result[$propertyName] = $propertyValue->getUid();
|
||||
} else {
|
||||
$result[$propertyName] = $this->convertTransientObjectToArray($propertyValue);
|
||||
}
|
||||
} elseif (is_array($propertyValue)) {
|
||||
$result[$propertyName] = $this->convertDomainObjectsToIdentityArrays($propertyValue);
|
||||
} else {
|
||||
$result[$propertyName] = $propertyValue;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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\Extbase\Pagination;
|
||||
|
||||
use TYPO3\CMS\Core\Pagination\AbstractPaginator;
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
|
||||
|
||||
final class QueryResultPaginator extends AbstractPaginator
|
||||
{
|
||||
private QueryResultInterface $paginatedQueryResult;
|
||||
|
||||
public function __construct(
|
||||
private readonly QueryResultInterface $queryResult,
|
||||
int $currentPageNumber = 1,
|
||||
int $itemsPerPage = 10
|
||||
) {
|
||||
$this->setCurrentPageNumber($currentPageNumber);
|
||||
$this->setItemsPerPage($itemsPerPage);
|
||||
|
||||
$this->updateInternalState();
|
||||
}
|
||||
|
||||
public function getPaginatedItems(): iterable
|
||||
{
|
||||
return $this->paginatedQueryResult;
|
||||
}
|
||||
|
||||
protected function updatePaginatedItems(int $limit, int $offset): void
|
||||
{
|
||||
$this->paginatedQueryResult = $this->queryResult
|
||||
->getQuery()
|
||||
->setLimit($limit)
|
||||
->setOffset($offset)
|
||||
->execute();
|
||||
}
|
||||
|
||||
protected function getTotalAmountOfItems(): int
|
||||
{
|
||||
return count($this->queryResult);
|
||||
}
|
||||
|
||||
protected function getAmountOfItemsOnCurrentPage(): int
|
||||
{
|
||||
return count($this->paginatedQueryResult);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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\Extbase\Persistence;
|
||||
|
||||
class ClassesConfiguration
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
public function __construct(array $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function hasClass(string $className): bool
|
||||
{
|
||||
return array_key_exists($className, $this->configuration);
|
||||
}
|
||||
|
||||
public function getConfigurationFor(string $className): ?array
|
||||
{
|
||||
return $this->configuration[$className] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all subclasses for the given set of (sub-)classes.
|
||||
* The whole classes configuration is used to determine all subclasses recursively.
|
||||
*
|
||||
* @return array A numeric array that contains all available subclasses-strings as values.
|
||||
*/
|
||||
public function getSubClasses(string $className): array
|
||||
{
|
||||
return $this->resolveSubClassesRecursive($className);
|
||||
}
|
||||
|
||||
private function resolveSubClassesRecursive(string $className, array $subClasses = []): array
|
||||
{
|
||||
foreach ($this->configuration[$className]['subclasses'] ?? [] as $subclass) {
|
||||
if (in_array($subclass, $subClasses, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subClasses[] = $subclass;
|
||||
$subClasses = $this->resolveSubClassesRecursive($subclass, $subClasses);
|
||||
}
|
||||
|
||||
return $subClasses;
|
||||
}
|
||||
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Persistence;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
|
||||
|
||||
final readonly class ClassesConfigurationFactory
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'cache.extbase')]
|
||||
private FrontendInterface $cache,
|
||||
private PackageManager $packageManager,
|
||||
#[Autowire(expression: 'service("package-dependent-cache-identifier").withPrefix("PersistenceClasses").toString()')]
|
||||
private string $cacheIdentifier,
|
||||
) {}
|
||||
|
||||
public function createClassesConfiguration(): ClassesConfiguration
|
||||
{
|
||||
$classesConfigurationCache = $this->cache->get($this->cacheIdentifier);
|
||||
if ($classesConfigurationCache !== false) {
|
||||
return new ClassesConfiguration($classesConfigurationCache);
|
||||
}
|
||||
|
||||
$classes = [];
|
||||
foreach ($this->packageManager->getActivePackages() as $activePackage) {
|
||||
$persistenceClassesFile = $activePackage->getPackagePath() . 'Configuration/Extbase/Persistence/Classes.php';
|
||||
if (file_exists($persistenceClassesFile)) {
|
||||
$definedClasses = require $persistenceClassesFile;
|
||||
if (is_array($definedClasses)) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$classes,
|
||||
$definedClasses,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$classes = $this->inheritPropertiesFromParentClasses($classes);
|
||||
|
||||
$this->cache->set($this->cacheIdentifier, $classes);
|
||||
|
||||
return new ClassesConfiguration($classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* todo: this method is flawed, see https://forge.typo3.org/issues/87566
|
||||
*/
|
||||
private function inheritPropertiesFromParentClasses(array $classes): array
|
||||
{
|
||||
foreach (array_keys($classes) as $className) {
|
||||
if (!isset($classes[$className]['properties'])) {
|
||||
$classes[$className]['properties'] = [];
|
||||
}
|
||||
|
||||
/*
|
||||
* At first we need to clean the list of parent classes.
|
||||
* This methods is expected to be called for models that either inherit
|
||||
* AbstractEntity or AbstractValueObject, therefore we want to know all
|
||||
* parents of $className until one of these parents.
|
||||
*/
|
||||
$relevantParentClasses = [];
|
||||
$parentClasses = class_parents($className) ?: [];
|
||||
while (null !== $parentClass = array_shift($parentClasses)) {
|
||||
if (in_array($parentClass, [AbstractEntity::class, AbstractValueObject::class], true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$relevantParentClasses[] = $parentClass;
|
||||
}
|
||||
|
||||
/*
|
||||
* Once we found all relevant parent classes of $class, we can check their
|
||||
* property configuration and merge theirs with the current one. This is necessary
|
||||
* to get the property configuration of parent classes in the current one to not
|
||||
* miss data in the model later on.
|
||||
*/
|
||||
foreach ($relevantParentClasses as $currentClassName) {
|
||||
if (null === $properties = $classes[$currentClassName]['properties'] ?? null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge new properties over existing ones.
|
||||
$classes[$className]['properties'] = array_replace_recursive($properties, $classes[$className]['properties'] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
}
|
||||
@@ -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\Extbase\Persistence;
|
||||
|
||||
use TYPO3\CMS\Extbase\Exception as ExtbaseException;
|
||||
|
||||
/**
|
||||
* A generic Persistence exception
|
||||
*/
|
||||
class Exception extends ExtbaseException {}
|
||||
@@ -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\Extbase\Persistence\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Object Type" exception
|
||||
*/
|
||||
class IllegalObjectTypeException extends Exception {}
|
||||
@@ -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\Extbase\Persistence\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\Exception;
|
||||
|
||||
/**
|
||||
* An "Illegal Relation Type" exception
|
||||
*/
|
||||
class IllegalRelationTypeException extends Exception {}
|
||||
@@ -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\Extbase\Persistence\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Query" Exception
|
||||
*/
|
||||
class InvalidQueryException extends Exception {}
|
||||
@@ -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\Extbase\Persistence\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\Exception;
|
||||
|
||||
/**
|
||||
* An "Unknown Object" exception
|
||||
*/
|
||||
class UnknownObjectException extends Exception {}
|
||||
@@ -0,0 +1,923 @@
|
||||
<?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\Extbase\Persistence\Generic;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspect;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
||||
use TYPO3\CMS\Core\Database\ReferenceIndex;
|
||||
use TYPO3\CMS\Core\DataHandling\TableColumnType;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Configuration\Exception\NoServerRequestGivenException;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\EntityAddedToPersistenceEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\EntityFinalizedAfterPersistenceEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\EntityPersistedEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\EntityRemovedFromPersistenceEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\EntityUpdatedInPersistenceEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectCountEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectDataEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectCountEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectDataEvent;
|
||||
use TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface as StorageBackendInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
|
||||
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
|
||||
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
|
||||
|
||||
/**
|
||||
* A persistence backend. This backend maps objects to the relational model of the storage backend.
|
||||
* It persists all added, removed and changed objects.
|
||||
*
|
||||
* Warning: This is a stateful-shared service!
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class Backend implements BackendInterface
|
||||
{
|
||||
protected PersistenceManagerInterface $persistenceManager;
|
||||
protected ObjectStorage $aggregateRootObjects;
|
||||
protected ObjectStorage $deletedEntities;
|
||||
protected ObjectStorage $changedEntities;
|
||||
protected ObjectStorage $visitedDuringPersistence;
|
||||
|
||||
public function __construct(
|
||||
protected readonly ConfigurationManagerInterface $configurationManager,
|
||||
protected readonly Session $session,
|
||||
protected readonly ReflectionService $reflectionService,
|
||||
protected readonly StorageBackendInterface $storageBackend,
|
||||
protected readonly DataMapFactory $dataMapFactory,
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly ReferenceIndex $referenceIndex,
|
||||
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
) {
|
||||
$this->aggregateRootObjects = new ObjectStorage();
|
||||
$this->deletedEntities = new ObjectStorage();
|
||||
$this->changedEntities = new ObjectStorage();
|
||||
}
|
||||
|
||||
public function setPersistenceManager(PersistenceManagerInterface $persistenceManager): void
|
||||
{
|
||||
$this->persistenceManager = $persistenceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of records matching the query.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getObjectCountByQuery(QueryInterface $query)
|
||||
{
|
||||
$event = new ModifyQueryBeforeFetchingObjectCountEvent($query);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
$query = $event->getQuery();
|
||||
$result = $this->storageBackend->getObjectCountByQuery($query);
|
||||
$event = new ModifyResultAfterFetchingObjectCountEvent($query, $result);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
return $event->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object data matching the $query.
|
||||
*
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
public function getObjectDataByQuery(QueryInterface $query)
|
||||
{
|
||||
$event = new ModifyQueryBeforeFetchingObjectDataEvent($query);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
$query = $event->getQuery();
|
||||
$result = $this->storageBackend->getObjectDataByQuery($query);
|
||||
$event = new ModifyResultAfterFetchingObjectDataEvent($query, $result);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
return $event->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the (internal) identifier for the object, if it is known to the
|
||||
* backend. Otherwise NULL is returned.
|
||||
*
|
||||
* The returned identifier is the base identifier (UID or UID_localizedUID)
|
||||
* without the language content identifier suffix, suitable for use as an external identifier.
|
||||
*
|
||||
* @param object $object
|
||||
* @return string|null The identifier for the object if it is known, or NULL
|
||||
*/
|
||||
public function getIdentifierByObject($object)
|
||||
{
|
||||
if ($object instanceof LazyLoadingProxy) {
|
||||
$object = $object->_loadRealInstance();
|
||||
}
|
||||
|
||||
if (!is_object($object)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$identifier = $this->session->getIdentifierByObject($object);
|
||||
if ($identifier === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->session->getBaseIdentifier($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object with the (internal) identifier, if it is known to the
|
||||
* backend. Otherwise NULL is returned.
|
||||
*
|
||||
* @param string $identifier
|
||||
* @param string $className
|
||||
* @return object|null The object for the identifier if it is known, or NULL
|
||||
*/
|
||||
public function getObjectByIdentifier($identifier, $className)
|
||||
{
|
||||
$query = $this->persistenceManager->createQueryForType($className);
|
||||
// This allows to fetch IDs for languages for default language AND language IDs
|
||||
// This is especially important when using the PropertyMapper of the Extbase MVC part to get
|
||||
// an object of the translated version of the incoming ID of a record.
|
||||
// "Free" mode (OVERLAYS_OFF) is mapped to OVERLAYS_MIXED - overlays need to be enabled for the
|
||||
// identity lookup, but hiding untranslated records is not a configured intent in free mode.
|
||||
// This is consistent with the same handling for related objects in DataMapper->getPreparedQuery().
|
||||
$languageAspect = $query->getQuerySettings()->getLanguageAspect();
|
||||
$languageAspect = new LanguageAspect(
|
||||
$languageAspect->getId(),
|
||||
$languageAspect->getContentId(),
|
||||
$languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_MIXED : $languageAspect->getOverlayType(),
|
||||
$languageAspect->getFallbackChain()
|
||||
);
|
||||
|
||||
// Build language-aware session identifier
|
||||
$sessionIdentifier = $this->session->buildIdentifier($identifier, $languageAspect);
|
||||
if ($this->session->hasIdentifier($sessionIdentifier, $className)) {
|
||||
return $this->session->getObjectByIdentifier($sessionIdentifier, $className);
|
||||
}
|
||||
|
||||
$query->getQuerySettings()->setLanguageAspect($languageAspect);
|
||||
$query->getQuerySettings()->setRespectStoragePage(false);
|
||||
$query->getQuerySettings()->setRespectSysLanguage(false);
|
||||
return $query->matching($query->equals('uid', $identifier))->execute()->getFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given object has ever been persisted.
|
||||
*
|
||||
* @param object $object The object to check
|
||||
* @return bool TRUE if the object is new, FALSE if the object exists in the repository
|
||||
*/
|
||||
public function isNewObject($object)
|
||||
{
|
||||
return $this->getIdentifierByObject($object) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the aggregate root objects
|
||||
*/
|
||||
public function setAggregateRootObjects(ObjectStorage $objects)
|
||||
{
|
||||
$this->aggregateRootObjects = $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the changed objects
|
||||
*/
|
||||
public function setChangedEntities(ObjectStorage $entities)
|
||||
{
|
||||
$this->changedEntities = $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the deleted objects
|
||||
*/
|
||||
public function setDeletedEntities(ObjectStorage $entities)
|
||||
{
|
||||
$this->deletedEntities = $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the current persistence session.
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
$this->persistObjects();
|
||||
$this->processDeletedObjects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse and persist all aggregate roots and their object graph.
|
||||
*/
|
||||
protected function persistObjects(): void
|
||||
{
|
||||
$this->visitedDuringPersistence = new ObjectStorage();
|
||||
foreach ($this->aggregateRootObjects as $object) {
|
||||
/** @var DomainObjectInterface $object */
|
||||
if ($object->_isNew()) {
|
||||
$this->insertObject($object);
|
||||
}
|
||||
$this->persistObject($object);
|
||||
}
|
||||
foreach ($this->changedEntities as $object) {
|
||||
$this->persistObject($object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the given object.
|
||||
*/
|
||||
protected function persistObject(DomainObjectInterface $object): void
|
||||
{
|
||||
if (isset($this->visitedDuringPersistence[$object])) {
|
||||
return;
|
||||
}
|
||||
$row = [];
|
||||
$queue = [];
|
||||
$className = get_class($object);
|
||||
$dataMap = $this->dataMapFactory->buildDataMap($className);
|
||||
$classSchema = $this->reflectionService->getClassSchema($className);
|
||||
foreach ($classSchema->getDomainObjectProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
if (!$dataMap->isPersistableProperty($propertyName)) {
|
||||
continue;
|
||||
}
|
||||
$propertyValue = $object->_getProperty($propertyName);
|
||||
if ($this->propertyValueIsLazyLoaded($propertyValue)) {
|
||||
continue;
|
||||
}
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
if ($propertyValue instanceof ObjectStorage) {
|
||||
$cleanProperty = $object->_getCleanProperty($propertyName);
|
||||
// objectstorage needs to be persisted if the object is new, the objectstorage is dirty, meaning it has
|
||||
// been changed after initial build, or an empty objectstorage is present and the cleanstate objectstorage
|
||||
// has childelements, meaning all elements should been removed from the objectstorage
|
||||
if ($object->_isNew() || $propertyValue->_isDirty() || ($propertyValue->count() === 0 && $cleanProperty && $cleanProperty->count() > 0)) {
|
||||
$this->persistObjectStorage($propertyValue, $object, $propertyName, $row);
|
||||
$propertyValue->_memorizeCleanState();
|
||||
}
|
||||
foreach ($propertyValue as $containedObject) {
|
||||
if ($containedObject instanceof DomainObjectInterface) {
|
||||
$queue[] = $containedObject;
|
||||
}
|
||||
}
|
||||
} elseif ($propertyValue instanceof DomainObjectInterface) {
|
||||
if ($object->_isDirty($propertyName)) {
|
||||
if ($propertyValue->_isNew()) {
|
||||
$this->insertObject($propertyValue, $object, $propertyName);
|
||||
}
|
||||
$row[$columnMap->columnName] = $this->getPlainValue($propertyValue, null, $property);
|
||||
}
|
||||
$queue[] = $propertyValue;
|
||||
} elseif ($object->_isNew() || $object->_isDirty($propertyName)) {
|
||||
$row[$columnMap->columnName] = $this->getPlainValue($propertyValue, $columnMap, $property);
|
||||
}
|
||||
}
|
||||
if (!empty($row)) {
|
||||
$this->updateObject($object, $row);
|
||||
$object->_memorizeCleanState();
|
||||
}
|
||||
$this->visitedDuringPersistence[$object] = $object->getUid();
|
||||
foreach ($queue as $queuedObject) {
|
||||
$this->persistObject($queuedObject);
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new EntityPersistedEvent($object));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if the property value is lazy loaded and was not initialized
|
||||
*/
|
||||
protected function propertyValueIsLazyLoaded(mixed $propertyValue): bool
|
||||
{
|
||||
if ($propertyValue instanceof LazyLoadingProxy) {
|
||||
return true;
|
||||
}
|
||||
if (($propertyValue instanceof LazyObjectStorage) && $propertyValue->isInitialized() === false) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists an object storage. Objects of a 1:n or m:n relation are queued and processed with the parent object.
|
||||
* A 1:1 relation gets persisted immediately. Objects which were removed from the property were detached from
|
||||
* the parent object. They will not be deleted by default. You have to add the attribute
|
||||
* #[\TYPO3\CMS\Extbase\Attribute\ORM\Cascade(['value' => 'remove'])] to the property if you want them to
|
||||
* be deleted as well.
|
||||
*
|
||||
* @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objectStorage The object storage to be persisted.
|
||||
* @param DomainObjectInterface $parentObject The parent object. One of the properties holds the object storage.
|
||||
* @param string $propertyName The name of the property holding the object storage.
|
||||
* @param array $row The row array of the parent object to be persisted. It's passed by reference and gets filled with either a comma separated list of uids (csv) or the number of contained objects.
|
||||
*/
|
||||
protected function persistObjectStorage(
|
||||
ObjectStorage $objectStorage,
|
||||
DomainObjectInterface $parentObject,
|
||||
string $propertyName,
|
||||
array &$row
|
||||
): void {
|
||||
$className = get_class($parentObject);
|
||||
$dataMapper = GeneralUtility::makeInstance(DataMapper::class);
|
||||
$columnMap = $this->dataMapFactory->buildDataMap($className)->getColumnMap($propertyName);
|
||||
$property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
|
||||
foreach ($this->getRemovedChildObjects($parentObject, $propertyName) as $removedObject) {
|
||||
$this->detachObjectFromParentObject($removedObject, $parentObject, $propertyName);
|
||||
if ($columnMap->typeOfRelation === Relation::HAS_MANY && $property->getCascadeValue() === 'remove') {
|
||||
$this->removeEntity($removedObject);
|
||||
}
|
||||
}
|
||||
|
||||
$currentUids = [];
|
||||
$sortingPosition = 1;
|
||||
$updateSortingOfFollowing = false;
|
||||
|
||||
foreach ($objectStorage as $object) {
|
||||
/** @var DomainObjectInterface $object */
|
||||
if (empty($currentUids)) {
|
||||
$sortingPosition = 1;
|
||||
} else {
|
||||
$sortingPosition++;
|
||||
}
|
||||
$cleanProperty = $parentObject->_getCleanProperty($propertyName);
|
||||
if ($object->_isNew()) {
|
||||
$this->insertObject($object, $parentObject);
|
||||
$this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
|
||||
// if a new object is inserted, all objects after this need to have their sorting updated
|
||||
$updateSortingOfFollowing = true;
|
||||
} elseif ($cleanProperty === null || $cleanProperty->getPosition($object) === null) {
|
||||
// if parent object is new then it doesn't have cleanProperty yet; before attaching object it's clean position is null
|
||||
$this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
|
||||
// if a relation is dirty (speaking the same object is removed and added again at a different position), all objects after this needs to be updated the sorting
|
||||
$updateSortingOfFollowing = true;
|
||||
} elseif ($objectStorage->isRelationDirty($object) || $cleanProperty->getPosition($object) !== $objectStorage->getPosition($object)) {
|
||||
$this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
|
||||
$updateSortingOfFollowing = true;
|
||||
} elseif ($updateSortingOfFollowing) {
|
||||
if ($sortingPosition > $objectStorage->getPosition($object)) {
|
||||
$this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
|
||||
} else {
|
||||
$sortingPosition = $objectStorage->getPosition($object);
|
||||
}
|
||||
}
|
||||
$currentUids[] = $object->getUid();
|
||||
}
|
||||
|
||||
if ($columnMap->parentKeyFieldName === null) {
|
||||
$row[$columnMap->columnName] = implode(',', $currentUids);
|
||||
} else {
|
||||
$row[$columnMap->columnName] = $dataMapper->countRelated($parentObject, $propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the removed objects determined by a comparison of the clean property value
|
||||
* with the actual property value.
|
||||
*/
|
||||
protected function getRemovedChildObjects(DomainObjectInterface $object, string $propertyName): array
|
||||
{
|
||||
$removedObjects = [];
|
||||
$cleanPropertyValue = $object->_getCleanProperty($propertyName);
|
||||
if (is_array($cleanPropertyValue) || $cleanPropertyValue instanceof \Iterator) {
|
||||
$propertyValue = $object->_getProperty($propertyName);
|
||||
foreach ($cleanPropertyValue as $containedObject) {
|
||||
if (!$propertyValue->contains($containedObject)) {
|
||||
$removedObjects[] = $containedObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $removedObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the fields defining the relation between the object and the parent object.
|
||||
*/
|
||||
protected function attachObjectToParentObject(
|
||||
DomainObjectInterface $object,
|
||||
DomainObjectInterface $parentObject,
|
||||
string $parentPropertyName,
|
||||
int $sortingPosition = 0
|
||||
): void {
|
||||
$parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
|
||||
$parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
|
||||
if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) {
|
||||
$this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
|
||||
} elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
|
||||
$this->insertRelationInRelationtable($object, $parentObject, $parentPropertyName, $sortingPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the fields defining the relation between the object and the parent object.
|
||||
*/
|
||||
protected function updateRelationOfObjectToParentObject(
|
||||
DomainObjectInterface $object,
|
||||
DomainObjectInterface $parentObject,
|
||||
string $parentPropertyName,
|
||||
int $sortingPosition = 0
|
||||
): void {
|
||||
$parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
|
||||
$parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
|
||||
if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) {
|
||||
$this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
|
||||
} elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
|
||||
$this->updateRelationInRelationTable($object, $parentObject, $parentPropertyName, $sortingPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates fields defining the relation between the object and the parent object in relation has-many.
|
||||
*
|
||||
* @throws IllegalRelationTypeException
|
||||
*/
|
||||
protected function attachObjectToParentObjectRelationHasMany(
|
||||
DomainObjectInterface $object,
|
||||
DomainObjectInterface $parentObject,
|
||||
string $parentPropertyName,
|
||||
int $sortingPosition = 0
|
||||
): void {
|
||||
$parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
|
||||
$parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
|
||||
if ($parentColumnMap->typeOfRelation !== Relation::HAS_MANY) {
|
||||
throw new IllegalRelationTypeException(
|
||||
'Parent column relation type is ' . Relation::class . '::' . $parentColumnMap->typeOfRelation->name
|
||||
. ' but should be ' . Relation::class . '::' . Relation::HAS_MANY->name,
|
||||
1345368105
|
||||
);
|
||||
}
|
||||
$row = [];
|
||||
if ($parentColumnMap->parentKeyFieldName !== null) {
|
||||
$row[$parentColumnMap->parentKeyFieldName] = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) ?: $parentObject->getUid();
|
||||
if ($parentColumnMap->parentTableFieldName !== null) {
|
||||
$row[$parentColumnMap->parentTableFieldName] = $parentDataMap->tableName;
|
||||
}
|
||||
$row = array_merge($parentColumnMap->relationTableMatchFields, $row);
|
||||
}
|
||||
$childSortByFieldName = $parentColumnMap->childSortByFieldName;
|
||||
if (!empty($childSortByFieldName)) {
|
||||
$row[$childSortByFieldName] = $sortingPosition;
|
||||
}
|
||||
if (!empty($row)) {
|
||||
$this->updateObject($object, $row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the fields defining the relation between the object and the parent object.
|
||||
*/
|
||||
protected function detachObjectFromParentObject(
|
||||
DomainObjectInterface $object,
|
||||
DomainObjectInterface $parentObject,
|
||||
string $parentPropertyName
|
||||
): void {
|
||||
$parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
|
||||
$parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
|
||||
if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) {
|
||||
$row = [];
|
||||
if ($parentColumnMap->parentKeyFieldName !== null) {
|
||||
$row[$parentColumnMap->parentKeyFieldName] = 0;
|
||||
if ($parentColumnMap->parentTableFieldName !== null) {
|
||||
$row[$parentColumnMap->parentTableFieldName] = '';
|
||||
}
|
||||
if (!empty($parentColumnMap->relationTableMatchFields)) {
|
||||
$row = array_merge(array_fill_keys(array_keys($parentColumnMap->relationTableMatchFields), ''), $row);
|
||||
}
|
||||
}
|
||||
if (!empty($parentColumnMap->childSortByFieldName)) {
|
||||
$row[$parentColumnMap->childSortByFieldName] = 0;
|
||||
}
|
||||
if (!empty($row)) {
|
||||
$this->updateObject($object, $row);
|
||||
}
|
||||
} elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
|
||||
$this->deleteRelationFromRelationtable($object, $parentObject, $parentPropertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an object in the storage backend
|
||||
*/
|
||||
protected function insertObject(
|
||||
DomainObjectInterface $object,
|
||||
?DomainObjectInterface $parentObject = null,
|
||||
string $parentPropertyName = ''
|
||||
): void {
|
||||
if ($object instanceof AbstractValueObject) {
|
||||
$result = $this->getUidOfAlreadyPersistedValueObject($object);
|
||||
if ($result !== null) {
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_UID, $result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$className = get_class($object);
|
||||
$dataMap = $this->dataMapFactory->buildDataMap($className);
|
||||
$row = [];
|
||||
$classSchema = $this->reflectionService->getClassSchema($className);
|
||||
foreach ($classSchema->getDomainObjectProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
if (!$dataMap->isPersistableProperty($propertyName)) {
|
||||
continue;
|
||||
}
|
||||
$propertyValue = $object->_getProperty($propertyName);
|
||||
if ($this->propertyValueIsLazyLoaded($propertyValue)) {
|
||||
continue;
|
||||
}
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
if ($columnMap->typeOfRelation === Relation::HAS_ONE) {
|
||||
$row[$columnMap->columnName] = 0;
|
||||
} elseif ($columnMap->typeOfRelation !== Relation::NONE) {
|
||||
if ($columnMap->parentKeyFieldName === null) {
|
||||
// CSV type relation
|
||||
$row[$columnMap->columnName] = '';
|
||||
} else {
|
||||
// MM type relation
|
||||
$row[$columnMap->columnName] = 0;
|
||||
}
|
||||
} elseif ($propertyValue !== null) {
|
||||
$row[$columnMap->columnName] = $this->getPlainValue($propertyValue, $columnMap, $property);
|
||||
}
|
||||
}
|
||||
$this->addCommonFieldsToRow($object, $row);
|
||||
if ($dataMap->languageIdColumnName !== null && $object->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID) === null) {
|
||||
$row[$dataMap->languageIdColumnName] = 0;
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID, 0);
|
||||
}
|
||||
if ($dataMap->translationOriginColumnName !== null) {
|
||||
$row[$dataMap->translationOriginColumnName] = 0;
|
||||
}
|
||||
if ($dataMap->translationOriginDiffSourceName !== null) {
|
||||
$row[$dataMap->translationOriginDiffSourceName] = '';
|
||||
}
|
||||
if ($parentObject !== null && $parentPropertyName) {
|
||||
$parentColumnDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject))->getColumnMap($parentPropertyName);
|
||||
$row = array_merge($parentColumnDataMap->relationTableMatchFields, $row);
|
||||
if ($parentColumnDataMap->parentKeyFieldName !== null) {
|
||||
$row[$parentColumnDataMap->parentKeyFieldName] = (int)$parentObject->getUid();
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentObject) {
|
||||
// Ensure a nested object respects the storage PID for new records or inherits the storage PID from
|
||||
// the parent object.
|
||||
$storagePidForObject = $this->determineStoragePageIdForNewRecord($object);
|
||||
if ($storagePidForObject === 0) {
|
||||
$storagePidForObject = $parentObject->getPid() ?? 0;
|
||||
}
|
||||
$row['pid'] = $storagePidForObject;
|
||||
}
|
||||
|
||||
$uid = $this->storageBackend->addRow($dataMap->tableName, $row);
|
||||
$localizedUid = $object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID);
|
||||
$identifier = $this->session->buildIdentifier(['uid' => $uid, '_LOCALIZED_UID' => $localizedUid]);
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_UID, $uid);
|
||||
$object->setPid((int)$row['pid']);
|
||||
if ($uid >= 1) {
|
||||
$this->eventDispatcher->dispatch(new EntityAddedToPersistenceEvent($object));
|
||||
}
|
||||
|
||||
$this->referenceIndex->updateRefIndexTable($dataMap->tableName, $uid);
|
||||
$this->session->registerObject($object, $identifier);
|
||||
if ($uid >= 1) {
|
||||
$this->eventDispatcher->dispatch(new EntityFinalizedAfterPersistenceEvent($object));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests, if the given Value Object already exists in the storage backend and if so, it returns the uid.
|
||||
*
|
||||
* @return int|null The matching uid if an object was found, else null
|
||||
*/
|
||||
protected function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object): ?int
|
||||
{
|
||||
return $this->storageBackend->getUidOfAlreadyPersistedValueObject($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts mm-relation into a relation table
|
||||
*
|
||||
* @return int The uid of the inserted row
|
||||
*/
|
||||
protected function insertRelationInRelationtable(
|
||||
DomainObjectInterface $object,
|
||||
DomainObjectInterface $parentObject,
|
||||
string $propertyName,
|
||||
?int $sortingPosition = null
|
||||
): int {
|
||||
$dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
$parentUid = $parentObject->getUid();
|
||||
if ($parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) !== null) {
|
||||
$parentUid = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID);
|
||||
}
|
||||
$row = [];
|
||||
if ($columnMap->parentKeyFieldName !== null) {
|
||||
$row[$columnMap->parentKeyFieldName] = (int)$parentUid;
|
||||
}
|
||||
if ($columnMap->childKeyFieldName !== null) {
|
||||
$row[$columnMap->childKeyFieldName] = (int)$object->getUid();
|
||||
}
|
||||
if ($columnMap->childSortByFieldName !== null) {
|
||||
$row[$columnMap->childSortByFieldName] = $sortingPosition ?? 0;
|
||||
}
|
||||
$relationTableName = $columnMap->relationTableName;
|
||||
if ($this->tcaSchemaFactory->has($relationTableName)) {
|
||||
$row[AbstractDomainObject::PROPERTY_PID] = $this->determineStoragePageIdForNewRecord();
|
||||
}
|
||||
$row = array_merge($columnMap->relationTableMatchFields, $row);
|
||||
return $this->storageBackend->addRow($relationTableName, $row, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates mm-relation in a relation table
|
||||
*
|
||||
* @return bool TRUE if update was successfully
|
||||
*/
|
||||
protected function updateRelationInRelationTable(
|
||||
DomainObjectInterface $object,
|
||||
DomainObjectInterface $parentObject,
|
||||
string $propertyName,
|
||||
int $sortingPosition = 0
|
||||
): bool {
|
||||
$dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
$row = [];
|
||||
if ($columnMap->parentKeyFieldName !== null) {
|
||||
$row[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid();
|
||||
}
|
||||
if ($columnMap->childKeyFieldName !== null) {
|
||||
$row[$columnMap->childKeyFieldName] = (int)$object->getUid();
|
||||
}
|
||||
if ($columnMap->childSortByFieldName !== null) {
|
||||
$row[$columnMap->childSortByFieldName] = $sortingPosition;
|
||||
}
|
||||
$relationTableName = $columnMap->relationTableName;
|
||||
$row = array_merge($columnMap->relationTableMatchFields, $row);
|
||||
$this->storageBackend->updateRelationTableRow($relationTableName, $row);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all mm-relations of a parent from a relation table
|
||||
*
|
||||
* @return bool TRUE if delete was successfully
|
||||
*/
|
||||
protected function deleteAllRelationsFromRelationtable(
|
||||
DomainObjectInterface $parentObject,
|
||||
string $parentPropertyName
|
||||
): bool {
|
||||
$dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
|
||||
$columnMap = $dataMap->getColumnMap($parentPropertyName);
|
||||
$relationTableName = $columnMap->relationTableName;
|
||||
$relationMatchFields = [];
|
||||
if ($columnMap->parentKeyFieldName !== null) {
|
||||
$relationMatchFields[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid();
|
||||
}
|
||||
$relationMatchFields = array_merge($columnMap->relationTableMatchFields, $relationMatchFields);
|
||||
$this->storageBackend->removeRow($relationTableName, $relationMatchFields);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an mm-relation from a relation table
|
||||
*/
|
||||
protected function deleteRelationFromRelationtable(
|
||||
DomainObjectInterface $relatedObject,
|
||||
DomainObjectInterface $parentObject,
|
||||
string $parentPropertyName
|
||||
): bool {
|
||||
$dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
|
||||
$columnMap = $dataMap->getColumnMap($parentPropertyName);
|
||||
$relationTableName = $columnMap->relationTableName;
|
||||
$relationMatchFields = [];
|
||||
if ($columnMap->parentKeyFieldName !== null) {
|
||||
$relationMatchFields[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid();
|
||||
}
|
||||
if ($columnMap->childKeyFieldName !== null) {
|
||||
$relationMatchFields[$columnMap->childKeyFieldName] = (int)$relatedObject->getUid();
|
||||
}
|
||||
$relationMatchFields = array_merge($columnMap->relationTableMatchFields, $relationMatchFields);
|
||||
$this->storageBackend->removeRow($relationTableName, $relationMatchFields);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a given object in the storage
|
||||
*/
|
||||
protected function updateObject(DomainObjectInterface $object, array $row): void
|
||||
{
|
||||
$dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
|
||||
$this->addCommonFieldsToRow($object, $row);
|
||||
$row['uid'] = $object->getUid();
|
||||
if ($dataMap->languageIdColumnName !== null) {
|
||||
$row[$dataMap->languageIdColumnName] = (int)$object->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID);
|
||||
if ($object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) !== null) {
|
||||
$row['uid'] = $object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID);
|
||||
}
|
||||
}
|
||||
$this->storageBackend->updateRow($dataMap->tableName, $row);
|
||||
$this->eventDispatcher->dispatch(new EntityUpdatedInPersistenceEvent($object));
|
||||
$this->referenceIndex->updateRefIndexTable($dataMap->tableName, (int)$row['uid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds common database fields to a row
|
||||
*/
|
||||
protected function addCommonFieldsToRow(DomainObjectInterface $object, array &$row): void
|
||||
{
|
||||
$dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
|
||||
$this->addCommonDateFieldsToRow($object, $row);
|
||||
if ($dataMap->recordTypeColumnName !== null && $dataMap->recordType !== null) {
|
||||
$row[$dataMap->recordTypeColumnName] = $dataMap->recordType;
|
||||
}
|
||||
if ($object->_isNew() && !isset($row['pid'])) {
|
||||
$row['pid'] = $this->determineStoragePageIdForNewRecord($object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the common date fields of the given row to the current time
|
||||
*/
|
||||
protected function addCommonDateFieldsToRow(DomainObjectInterface $object, array &$row): void
|
||||
{
|
||||
$dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
|
||||
if ($object->_isNew() && $dataMap->creationDateColumnName !== null) {
|
||||
$row[$dataMap->creationDateColumnName] = $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
if ($dataMap->modificationDateColumnName !== null) {
|
||||
$row[$dataMap->modificationDateColumnName] = $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over deleted aggregate root objects and process them
|
||||
*/
|
||||
protected function processDeletedObjects(): void
|
||||
{
|
||||
foreach ($this->deletedEntities as $entity) {
|
||||
if ($this->session->hasObject($entity)) {
|
||||
$this->removeEntity($entity);
|
||||
$this->session->unregisterReconstitutedEntity($entity);
|
||||
$this->session->unregisterObject($entity);
|
||||
}
|
||||
}
|
||||
$this->deletedEntities = new ObjectStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an object
|
||||
*/
|
||||
protected function removeEntity(DomainObjectInterface $object, bool $markAsDeleted = true): void
|
||||
{
|
||||
$dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
|
||||
if ($markAsDeleted === true && $dataMap->deletedFlagColumnName !== null) {
|
||||
$deletedColumnName = $dataMap->deletedFlagColumnName;
|
||||
$row = [
|
||||
'uid' => $object->getUid(),
|
||||
$deletedColumnName => 1,
|
||||
];
|
||||
$this->addCommonDateFieldsToRow($object, $row);
|
||||
$this->storageBackend->updateRow($dataMap->tableName, $row);
|
||||
} else {
|
||||
$this->storageBackend->removeRow($dataMap->tableName, ['uid' => $object->getUid()]);
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new EntityRemovedFromPersistenceEvent($object));
|
||||
|
||||
$this->removeRelatedObjects($object);
|
||||
$this->referenceIndex->updateRefIndexTable($dataMap->tableName, $object->getUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove related objects
|
||||
*/
|
||||
protected function removeRelatedObjects(DomainObjectInterface $object): void
|
||||
{
|
||||
$className = get_class($object);
|
||||
$dataMap = $this->dataMapFactory->buildDataMap($className);
|
||||
$classSchema = $this->reflectionService->getClassSchema($className);
|
||||
foreach ($classSchema->getDomainObjectProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
if ($columnMap === null) {
|
||||
continue;
|
||||
}
|
||||
$propertyValue = $object->_getProperty($propertyName);
|
||||
if ($property->getCascadeValue() === 'remove') {
|
||||
if ($columnMap->typeOfRelation === Relation::HAS_MANY) {
|
||||
foreach ($propertyValue as $containedObject) {
|
||||
$this->removeEntity($containedObject);
|
||||
}
|
||||
} elseif ($propertyValue instanceof DomainObjectInterface) {
|
||||
$this->removeEntity($propertyValue);
|
||||
}
|
||||
} elseif ($dataMap->deletedFlagColumnName === null
|
||||
&& $columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY
|
||||
) {
|
||||
$this->deleteAllRelationsFromRelationtable($object, $propertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the storage page ID for a given NEW record
|
||||
*
|
||||
* This does the following:
|
||||
* - If the domain object has an accessible property 'pid' (i.e. through a getPid() method), that is used to store the record.
|
||||
* - If there is a TypoScript configuration "classes.CLASSNAME.newRecordStoragePid", that is used to store new records.
|
||||
* - If there is no such TypoScript configuration, it uses the first value of The "storagePid" taken for reading records.
|
||||
*
|
||||
* @return int the storage Page ID where the object should be stored
|
||||
*/
|
||||
protected function determineStoragePageIdForNewRecord(?DomainObjectInterface $object = null): int
|
||||
{
|
||||
$frameworkConfiguration = [];
|
||||
try {
|
||||
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
} catch (NoServerRequestGivenException) {
|
||||
// Fallback to empty array if ConfigurationManager has not been initialized with a Request.
|
||||
// This implies storagePid 0. This is a measure to specifically allow running the extbase
|
||||
// persistence layer without a Request, which may be useful in some CLI scenarios (and can
|
||||
// be convenient in tests) when no other code branches of extbase that have a hard dependency
|
||||
// to the Request (e.g. controllers / view) are used.
|
||||
}
|
||||
|
||||
if ($object !== null) {
|
||||
if (ObjectAccess::isPropertyGettable($object, AbstractDomainObject::PROPERTY_PID)) {
|
||||
$pid = ObjectAccess::getProperty($object, AbstractDomainObject::PROPERTY_PID);
|
||||
if (isset($pid)) {
|
||||
return (int)$pid;
|
||||
}
|
||||
}
|
||||
$className = get_class($object);
|
||||
if (isset($frameworkConfiguration['persistence']['classes'][$className]) && !empty($frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'])) {
|
||||
return (int)$frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'];
|
||||
}
|
||||
}
|
||||
$storagePidList = GeneralUtility::intExplode(',', (string)($frameworkConfiguration['persistence']['storagePid'] ?? '0'));
|
||||
return $storagePidList[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a plain value, i.e. objects are flattened out if possible.
|
||||
* Checks explicitly for null values as DataMapper's getPlainValue would convert this to 'NULL'.
|
||||
* For null values, the expected DB null value will be considered.
|
||||
*
|
||||
* @param mixed $input The value that will be converted
|
||||
* @param ColumnMap|null $columnMap Optional column map for retrieving the date storage format
|
||||
* @param Property|null $property The current property
|
||||
* @return int|string|null
|
||||
*/
|
||||
protected function getPlainValue(mixed $input, ?ColumnMap $columnMap = null, ?Property $property = null)
|
||||
{
|
||||
if ($input !== null) {
|
||||
return GeneralUtility::makeInstance(DataMapper::class)->getPlainValue($input, $columnMap);
|
||||
}
|
||||
|
||||
if ($columnMap?->type === TableColumnType::DATETIME) {
|
||||
return QueryHelper::transformDateTimeToDatabaseValue(
|
||||
null,
|
||||
$columnMap->isNullable,
|
||||
$columnMap->dateTimeFormat ?? 'datetime',
|
||||
$columnMap->dateTimeStorageFormat
|
||||
);
|
||||
}
|
||||
|
||||
if ($property === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$className = $property->getPrimaryType()->getClassName() ?? null;
|
||||
|
||||
if ($className === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Nullable domain model property
|
||||
if (is_subclass_of($className, DomainObjectInterface::class)) {
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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\Extbase\Persistence\Generic;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
|
||||
|
||||
/**
|
||||
* A persistence backend interface
|
||||
*/
|
||||
interface BackendInterface
|
||||
{
|
||||
/**
|
||||
* Set a PersistenceManager instance.
|
||||
*/
|
||||
public function setPersistenceManager(PersistenceManagerInterface $persistenceManager);
|
||||
|
||||
/**
|
||||
* Sets the aggregate root objects
|
||||
*/
|
||||
public function setAggregateRootObjects(ObjectStorage $objects);
|
||||
|
||||
/**
|
||||
* Sets the deleted entities
|
||||
*/
|
||||
public function setDeletedEntities(ObjectStorage $entities);
|
||||
|
||||
/**
|
||||
* Sets the changed objects
|
||||
*/
|
||||
public function setChangedEntities(ObjectStorage $entities);
|
||||
|
||||
/**
|
||||
* Commits the current persistence session
|
||||
*/
|
||||
public function commit();
|
||||
|
||||
/**
|
||||
* Returns the (internal) identifier for the object, if it is known to the
|
||||
* backend. Otherwise NULL is returned.
|
||||
*
|
||||
* @param object $object
|
||||
* @return string|null The identifier for the object if it is known, or NULL
|
||||
*/
|
||||
public function getIdentifierByObject($object);
|
||||
|
||||
/**
|
||||
* Returns the object with the (internal) identifier, if it is known to the
|
||||
* backend. Otherwise NULL is returned.
|
||||
*
|
||||
* @param string $identifier
|
||||
* @param string $className
|
||||
* @return object|null The object for the identifier if it is known, or NULL
|
||||
*/
|
||||
public function getObjectByIdentifier($identifier, $className);
|
||||
|
||||
/**
|
||||
* Checks if the given object has ever been persisted.
|
||||
*
|
||||
* @param object $object The object to check
|
||||
* @return bool TRUE if the object is new, FALSE if the object exists in the repository
|
||||
*/
|
||||
public function isNewObject($object);
|
||||
|
||||
/**
|
||||
* Returns the number of records matching the query.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getObjectCountByQuery(QueryInterface $query);
|
||||
|
||||
/**
|
||||
* Returns the object data matching the $query.
|
||||
*
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
public function getObjectDataByQuery(QueryInterface $query);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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\Extbase\Persistence\Generic;
|
||||
|
||||
/**
|
||||
* A generic Persistence exception
|
||||
*/
|
||||
class Exception extends \TYPO3\CMS\Extbase\Persistence\Exception {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user