TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Adapter;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface;
|
||||
|
||||
#[AsAlias(SymfonyEventDispatcherInterface::class, public: true)]
|
||||
final readonly class EventDispatcherAdapter implements SymfonyEventDispatcherInterface
|
||||
{
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher) {}
|
||||
|
||||
public function dispatch(object $event, ?string $eventName = null): object
|
||||
{
|
||||
return $this->eventDispatcher->dispatch($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Attribute;
|
||||
|
||||
/**
|
||||
* Defines a method that is allowed to be used as a user function.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
|
||||
final readonly class AsAllowedCallable
|
||||
{
|
||||
public const string TAG_NAME = 'security.allowed-callable';
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure event listeners.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class AsEventListener
|
||||
{
|
||||
public const TAG_NAME = 'event.listener';
|
||||
|
||||
public function __construct(
|
||||
public ?string $identifier = null,
|
||||
public ?string $event = null,
|
||||
public ?string $method = null,
|
||||
public ?string $before = null,
|
||||
public ?string $after = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure file renderers for the RendererRegistry.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class AsFileRenderer
|
||||
{
|
||||
public const TAG_NAME = 'fal.file_renderer';
|
||||
|
||||
public function __construct(
|
||||
/**
|
||||
* The priority of the renderer. This way it is possible to
|
||||
* define/overrule a renderer for a specific file type/context,
|
||||
* for example a video renderer for a certain storage/driver type.
|
||||
*
|
||||
* Renderers with a higher priority are asked first whether they
|
||||
* can render a given file. All file renderers shipped with
|
||||
* TYPO3 Core use the default priority of 0.
|
||||
*/
|
||||
public int $priority = 0,
|
||||
) {}
|
||||
}
|
||||
@@ -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\Core\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure module access gates.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
final readonly class AsModuleAccessGate
|
||||
{
|
||||
public const string TAG_NAME = 'backend.module_access_gate';
|
||||
|
||||
/**
|
||||
* @param non-empty-string $identifier
|
||||
* @param list<non-empty-string> $before
|
||||
* @param list<non-empty-string> $after
|
||||
*/
|
||||
public function __construct(
|
||||
public string $identifier,
|
||||
public array $before = [],
|
||||
public array $after = [],
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure non-schedulable commands.
|
||||
* This must be used on top of the #[AsCommand] attribute.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class AsNonSchedulableCommand
|
||||
{
|
||||
public function __construct(
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure upgrade wizards
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class UpgradeWizard
|
||||
{
|
||||
public const TAG_NAME = 'install.upgradewizard';
|
||||
|
||||
public function __construct(
|
||||
public string $identifier
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to mark a message as a webhook-compatible message
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class WebhookMessage
|
||||
{
|
||||
public const TAG_NAME = 'core.webhook_message';
|
||||
|
||||
public function __construct(
|
||||
public string $identifier,
|
||||
public string $description,
|
||||
public ?string $method = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
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\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Authentication services class
|
||||
*/
|
||||
class AbstractAuthenticationService implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* User object
|
||||
*
|
||||
* @var AbstractUserAuthentication
|
||||
*/
|
||||
public $pObj;
|
||||
|
||||
/**
|
||||
* Subtype of the service which is used to call the service.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $mode;
|
||||
|
||||
/**
|
||||
* Submitted login form data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $login = [];
|
||||
|
||||
/**
|
||||
* Various data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $authInfo = [];
|
||||
|
||||
/**
|
||||
* User db table definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $db_user = [];
|
||||
|
||||
/**
|
||||
* If the writelog() functions is called if a login-attempt has be tried without success
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $writeAttemptLog = false;
|
||||
|
||||
/**
|
||||
* @var array service description array
|
||||
*/
|
||||
public $info = [];
|
||||
|
||||
/**
|
||||
* Initialize authentication service
|
||||
*
|
||||
* @param string $mode Subtype of the service which is used to call the service.
|
||||
* @param array $loginData Submitted login form data
|
||||
* @param array $authInfo Information array. Holds submitted form data etc.
|
||||
* @param AbstractUserAuthentication $pObj Parent object
|
||||
*/
|
||||
public function initAuth($mode, $loginData, $authInfo, $pObj)
|
||||
{
|
||||
$this->pObj = $pObj;
|
||||
// Sub type
|
||||
$this->mode = $mode;
|
||||
$this->login = $loginData;
|
||||
$this->authInfo = $authInfo;
|
||||
$this->db_user = $this->getServiceOption('db_user', $authInfo['db_user'] ?? [], false);
|
||||
$this->writeAttemptLog = $this->pObj->writeAttemptLog ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes to log database table in pObj
|
||||
*
|
||||
* @param int $type denotes which module that has submitted the entry. This is the current list: 1=tce_db; 2=tce_file; 3=system (eg. sys_history save); 4=modules; 254=Personal settings changed; 255=login / out action: 1=login, 2=logout, 3=failed login (+ errorcode 3), 4=failure_warning_email sent
|
||||
* @param int $action denotes which specific operation that wrote the entry (eg. 'delete', 'upload', 'update' and so on...). Specific for each $type. Also used to trigger update of the interface. (see the log-module for the meaning of each number !!)
|
||||
* @param int $error flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
|
||||
* @param null $_ unused
|
||||
* @param string $details Default text that follows the message
|
||||
* @param array $data Data that follows the log. Might be used to carry special information. If an array the first 5 entries (0-4) will be sprintf'ed the details-text...
|
||||
* @param string $tablename Special field used by tce_main.php. These ($tablename, $recuid) holds the reference to the record which the log-entry is about.
|
||||
* @param int|string $recuid Special field used by tce_main.php. These ($tablename, $recuid) holds the reference to the record which the log-entry is about.
|
||||
*/
|
||||
public function writelog($type, $action, $error, $_, $details, $data, $tablename = '', $recuid = '')
|
||||
{
|
||||
if ($this->writeAttemptLog) {
|
||||
$this->pObj->writelog($type, $action, $error, null, $details, $data, $tablename, $recuid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user from DB by username
|
||||
*
|
||||
* @param string $username User name
|
||||
* @param string $extraWhere Additional WHERE clause: " AND ...
|
||||
* @param array|string $dbUserSetup User db table definition, or empty string for $this->db_user
|
||||
* @return array<string, mixed>|false User array or FALSE
|
||||
*/
|
||||
public function fetchUserRecord($username, $extraWhere = '', $dbUserSetup = '')
|
||||
{
|
||||
$dbUser = is_array($dbUserSetup) ? $dbUserSetup : $this->db_user;
|
||||
$user = false;
|
||||
if ($username || $extraWhere) {
|
||||
$query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($dbUser['table']);
|
||||
$query->getRestrictions()->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$constraints = array_filter([
|
||||
QueryHelper::stripLogicalOperatorPrefix($dbUser['enable_clause']),
|
||||
QueryHelper::stripLogicalOperatorPrefix($extraWhere),
|
||||
]);
|
||||
if (!empty($username)) {
|
||||
array_unshift(
|
||||
$constraints,
|
||||
$query->expr()->eq(
|
||||
$dbUser['username_column'],
|
||||
$query->createNamedParameter($username)
|
||||
)
|
||||
);
|
||||
}
|
||||
$user = $query->select('*')
|
||||
->from($dbUser['table'])
|
||||
->where(...$constraints)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization of the service.
|
||||
* This is a stub as needed by GeneralUtility::makeInstanceService()
|
||||
* @internal this is part of the Service API which should be avoided to be used and only used within TYPO3 internally
|
||||
*/
|
||||
public function init(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the service.
|
||||
* This is a stub as needed by GeneralUtility::makeInstanceService()
|
||||
* @internal this is part of the Service API which should be avoided to be used and only used within TYPO3 internally
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the service key of the service
|
||||
*
|
||||
* @return string Service key
|
||||
* @internal this is part of the Service API which should be avoided to be used and only used within TYPO3 internally
|
||||
*/
|
||||
public function getServiceKey()
|
||||
{
|
||||
return $this->info['serviceKey'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title of the service
|
||||
*
|
||||
* @return string Service title
|
||||
* @internal this is part of the Service API which should be avoided to be used and only used within TYPO3 internally
|
||||
*/
|
||||
public function getServiceTitle()
|
||||
{
|
||||
return $this->info['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns service configuration values from the $TYPO3_CONF_VARS['SVCONF'] array
|
||||
*
|
||||
* @param string $optionName Name of the config option
|
||||
* @param mixed $defaultValue Default configuration if no special config is available
|
||||
* @param bool $includeDefaultConfig If set the 'default' config will be returned if no special config for this service is available (default: TRUE)
|
||||
* @return mixed Configuration value for the service
|
||||
* @internal this is part of the Service API which should be avoided to be used and only used within TYPO3 internally
|
||||
*/
|
||||
public function getServiceOption($optionName, $defaultValue = '', $includeDefaultConfig = true)
|
||||
{
|
||||
$config = null;
|
||||
$serviceType = $this->info['serviceType'] ?? '';
|
||||
$serviceKey = $this->info['serviceKey'] ?? '';
|
||||
$svOptions = $GLOBALS['TYPO3_CONF_VARS']['SVCONF'][$serviceType] ?? [];
|
||||
if (isset($svOptions[$serviceKey][$optionName])) {
|
||||
$config = $svOptions[$serviceKey][$optionName];
|
||||
} elseif ($includeDefaultConfig && isset($svOptions['default'][$optionName])) {
|
||||
$config = $svOptions['default'][$optionName];
|
||||
}
|
||||
if (!isset($config)) {
|
||||
$config = $defaultValue;
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal this is part of the Service API which should be avoided to be used and only used within TYPO3 internally
|
||||
*/
|
||||
public function getLastErrorArray(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
/**
|
||||
* Result object for access checks.
|
||||
*
|
||||
* Encapsulates the result of access checks in BackendUserAuthentication
|
||||
* without requiring state sharing via properties.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class AccessCheckResult
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isAllowed,
|
||||
public string $errorMessage = '',
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use Psr\Log\LogLevel;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
|
||||
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
|
||||
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
|
||||
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Authentication services class
|
||||
*/
|
||||
class AuthenticationService extends AbstractAuthenticationService implements MimicServiceInterface
|
||||
{
|
||||
/**
|
||||
* Process the submitted credentials. Returns true, if loginData was processed successfully, else false.
|
||||
* In addition, extensions overwriting this method or implement the context related methods `processLoginDataFE`
|
||||
* or `processLoginDataBE` may also return an int (e.g. >= 200) in order to stop loginData processing of other
|
||||
* services in the authentication service chain.
|
||||
*
|
||||
* @param array $loginData Credentials that are submitted and potentially modified by other services
|
||||
*/
|
||||
public function processLoginData(array &$loginData): bool|int
|
||||
{
|
||||
$loginData = array_map(trim(...), $loginData);
|
||||
$loginData['uident_text'] = $loginData['uident'];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user (eg. look up the user record in database when a login is sent)
|
||||
*
|
||||
* @return array<string, mixed>|false User array or FALSE
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
if (LoginType::tryFrom($this->login['status'] ?? '') !== LoginType::LOGIN) {
|
||||
return false;
|
||||
}
|
||||
if ((string)$this->login['uident_text'] === '') {
|
||||
// Failed Login attempt (no password given)
|
||||
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, null, 'Login-attempt from ###IP### for username \'%s\' with an empty password!', [
|
||||
$this->login['uname'],
|
||||
]);
|
||||
$this->logger->warning('Login-attempt from {ip}, for username "{username}" with an empty password!', [
|
||||
'ip' => $this->authInfo['REMOTE_ADDR'],
|
||||
'username' => $this->login['uname'],
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->fetchUserRecord($this->login['uname']);
|
||||
if (!is_array($user)) {
|
||||
// Failed login attempt (no username found)
|
||||
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, null, 'Login-attempt from ###IP###, username \'%s\' not found!', [$this->login['uname']]);
|
||||
$this->logger->info('Login-attempt from username "{username}" not found!', [
|
||||
'username' => $this->login['uname'],
|
||||
'REMOTE_ADDR' => $this->authInfo['REMOTE_ADDR'],
|
||||
]);
|
||||
} else {
|
||||
$this->logger->debug('User found', [
|
||||
$this->db_user['userid_column'] => $user[$this->db_user['userid_column']],
|
||||
$this->db_user['username_column'] => $user[$this->db_user['username_column']],
|
||||
]);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user: Check submitted user credentials against stored hashed password.
|
||||
*
|
||||
* Returns one of the following status codes:
|
||||
* >= 200: User authenticated successfully. No more checking is needed by other auth services.
|
||||
* >= 100: User not authenticated; this service is not responsible. Other auth services will be asked.
|
||||
* > 0: User authenticated successfully. Other auth services will still be asked.
|
||||
* <= 0: Authentication failed, no more checking needed by other auth services.
|
||||
*
|
||||
* @param array<string, mixed> $user User data
|
||||
* @return int Authentication status code, one of 0, 100, 200
|
||||
*/
|
||||
public function authUser(array $user): int
|
||||
{
|
||||
// Early 100 "not responsible, check other services" if username or password is empty
|
||||
if (!isset($this->login['uident_text']) || (string)$this->login['uident_text'] === ''
|
||||
|| !isset($this->login['uname']) || (string)$this->login['uname'] === '') {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (empty($this->db_user['table'])) {
|
||||
throw new \RuntimeException('User database table not set', 1533159150);
|
||||
}
|
||||
|
||||
$submittedUsername = (string)$this->login['uname'];
|
||||
$submittedPassword = (string)$this->login['uident_text'];
|
||||
$passwordHashInDatabase = $user['password'];
|
||||
$userDatabaseTable = $this->db_user['table'];
|
||||
|
||||
$isReHashNeeded = false;
|
||||
|
||||
$saltFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
|
||||
|
||||
// Get a hashed password instance for the hash stored in db of this user
|
||||
try {
|
||||
$hashInstance = $saltFactory->get($passwordHashInDatabase, $this->pObj->loginType);
|
||||
} catch (InvalidPasswordHashException $exception) {
|
||||
// Could not find a responsible hash algorithm for given password. This is unusual since other
|
||||
// authentication services would usually be called before this one with higher priority. We thus log
|
||||
// the failed login but still return '100' to proceed with other services that may follow.
|
||||
$message = 'Login-attempt from ###IP###, username \'%s\', no suitable hash method found!';
|
||||
$this->writeLogMessage($message, $submittedUsername);
|
||||
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, null, $message, [$submittedUsername]);
|
||||
// Not responsible, check other services
|
||||
return 100;
|
||||
}
|
||||
|
||||
// An instance of the currently configured salted password mechanism
|
||||
// Don't catch InvalidPasswordHashException here: Only install tool should handle those configuration failures
|
||||
$defaultHashInstance = $saltFactory->getDefaultHashInstance($this->pObj->loginType);
|
||||
|
||||
// We found a hash class that can handle this type of hash
|
||||
$isValidPassword = $hashInstance->checkPassword($submittedPassword, $passwordHashInDatabase);
|
||||
if ($isValidPassword) {
|
||||
if ($hashInstance->isHashUpdateNeeded($passwordHashInDatabase)
|
||||
|| $defaultHashInstance != $hashInstance
|
||||
) {
|
||||
// Lax object comparison intended: Rehash if old and new salt objects are not
|
||||
// instances of the same class.
|
||||
$isReHashNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isValidPassword) {
|
||||
// Failed login attempt - wrong password
|
||||
$message = 'Login-attempt from ###IP###, username \'%s\', password not accepted!';
|
||||
$this->writeLogMessage($message, $submittedUsername);
|
||||
$this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::ATTEMPT, SystemLogErrorClassification::SECURITY_NOTICE, null, $message, [$submittedUsername]);
|
||||
// Responsible, authentication failed, do NOT check other services
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($isReHashNeeded) {
|
||||
// Given password validated but a re-hash is needed. Do so.
|
||||
$this->updatePasswordHashInDatabase(
|
||||
$userDatabaseTable,
|
||||
(int)$user['uid'],
|
||||
$defaultHashInstance->getHashedPassword($submittedPassword)
|
||||
);
|
||||
}
|
||||
|
||||
// Responsible, authentication ok. Log successful login and return 'auth ok, do NOT check other services'
|
||||
$this->writeLogMessage($this->pObj->loginType . ' Authentication successful for username \'%s\'', $submittedUsername);
|
||||
return 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mimics password hashing for invalid authentication requests to mitigate
|
||||
* @link https://cwe.mitre.org/data/definitions/208.html: CWE-208: Observable Timing Discrepancy
|
||||
*/
|
||||
public function mimicAuthUser(): bool
|
||||
{
|
||||
try {
|
||||
$hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
|
||||
$defaultHashInstance = $hashFactory->getDefaultHashInstance($this->pObj->loginType);
|
||||
$defaultHashInstance->getHashedPassword(random_bytes(10));
|
||||
} catch (\Exception) {
|
||||
// no further processing here
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method updates a FE/BE user record - in this case a new password string will be set.
|
||||
*
|
||||
* @param string $table Database table of this user, usually 'be_users' or 'fe_users'
|
||||
* @param int $uid uid of user record that will be updated
|
||||
* @param string $newPassword Field values as key=>value pairs to be updated in database
|
||||
*/
|
||||
protected function updatePasswordHashInDatabase(string $table, int $uid, string $newPassword): void
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
|
||||
$connection->update(
|
||||
$table,
|
||||
['password' => $newPassword],
|
||||
['uid' => $uid]
|
||||
);
|
||||
$this->logger->notice('Automatic password update for user record in {table} with uid {uid}', [
|
||||
'table' => $table,
|
||||
'uid' => $uid,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes log message. Destination log depends on the current system mode.
|
||||
*
|
||||
* This function accepts variable number of arguments and can format
|
||||
* parameters. The syntax is the same as for sprintf()
|
||||
* If a marker ###IP### is present in the message, it is automatically replaced with the REMOTE_ADDR
|
||||
*
|
||||
* @param string $message Message to output
|
||||
* @param array<int,string> $params
|
||||
*/
|
||||
protected function writeLogMessage(string $message, ...$params): void
|
||||
{
|
||||
if (!empty($params)) {
|
||||
$message = vsprintf($message, $params);
|
||||
}
|
||||
$message = str_replace('###IP###', (string)($this->authInfo['REMOTE_ADDR'] ?? ''), $message);
|
||||
if ($this->pObj->loginType === 'FE') {
|
||||
$timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
|
||||
$timeTracker->setTSlogMessage($message, LogLevel::INFO);
|
||||
}
|
||||
$this->logger->notice($message);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* TYPO3 backend user authentication on a CLI level
|
||||
* Auto-logs in, only allowed on CLI
|
||||
*/
|
||||
class CommandLineUserAuthentication extends BackendUserAuthentication
|
||||
{
|
||||
/**
|
||||
/**
|
||||
* Constructor, only allowed in CLI mode
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (!Environment::isCli()) {
|
||||
throw new \RuntimeException('Creating a CLI-based user object on non-CLI level is not allowed', 1483971165);
|
||||
}
|
||||
if (!$this->isUserAllowedToLogin()) {
|
||||
throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1483971855);
|
||||
}
|
||||
$this->dontSetCookie = true;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement for AbstractUserAuthentication::start()
|
||||
*
|
||||
* We do not need support for sessions, cookies, $_GET-modes, the postUserLookup hook or
|
||||
* a database connection during CLI Bootstrap
|
||||
*
|
||||
* @param ServerRequestInterface|null $request
|
||||
*/
|
||||
public function start(?ServerRequestInterface $request = null)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement for AbstractUserAuthentication::checkAuthentication()
|
||||
*
|
||||
* Not required in CLI mode, therefore empty.
|
||||
*/
|
||||
public function checkAuthentication(ServerRequestInterface $request)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* On CLI there is no session and no switched user
|
||||
*/
|
||||
public function getOriginalUserIdWhenInSwitchUserMode(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs-in the _CLI_ user. It does not need to check for credentials.
|
||||
*
|
||||
* @throws \RuntimeException when the user could not log in or it is an admin
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
// check if a _CLI_ user exists, if not, create one
|
||||
$this->setBeUserByName(CommandLineUserCreation::CLI_USERNAME);
|
||||
if (empty($this->user['uid'])) {
|
||||
$userCreation = GeneralUtility::makeInstance(CommandLineUserCreation::class);
|
||||
// create a new BE user in the database
|
||||
if (!$userCreation->ensureCliUserExists()) {
|
||||
throw new \RuntimeException('No backend user named "_cli_" could be authenticated, maybe this user is "hidden"?', 1484050401);
|
||||
}
|
||||
$this->setBeUserByName(CommandLineUserCreation::CLI_USERNAME);
|
||||
}
|
||||
if (empty($this->user['uid'])) {
|
||||
throw new \RuntimeException('No backend user named "_cli_" could be created.', 1476107195);
|
||||
}
|
||||
$this->unpack_uc();
|
||||
// The groups are fetched and ready for permission checking in this initialization.
|
||||
$this->fetchGroupData();
|
||||
$this->backendSetUC();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in the TYPO3 Backend user "_cli_"
|
||||
*/
|
||||
public function backendCheckLogin(?ServerRequestInterface $request = null)
|
||||
{
|
||||
$this->authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a CLI backend user is allowed to access TYPO3.
|
||||
* Only when adminOnly is off (=0), and only allowed for admins and CLI users (=2)
|
||||
*
|
||||
* @return bool Whether the CLI user is allowed to access TYPO3
|
||||
* @internal
|
||||
*/
|
||||
public function isUserAllowedToLogin()
|
||||
{
|
||||
return in_array((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'], [0, 2], true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal Exclusively for use in CommandLineUserAuthentication and in TYPO3 setup code (cli and controller)
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class CommandLineUserCreation
|
||||
{
|
||||
public const string CLI_USERNAME = '_cli_';
|
||||
|
||||
public function __construct(
|
||||
private ConnectionPool $connectionPool,
|
||||
private PasswordHashFactory $passwordHashFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a record in the DB table be_users called "_cli_" with no other information,
|
||||
* when it does not exist already
|
||||
*/
|
||||
public function ensureCliUserExists(): bool
|
||||
{
|
||||
if ($this->cliUserExists()) {
|
||||
return false;
|
||||
}
|
||||
$userFields = [
|
||||
'username' => self::CLI_USERNAME,
|
||||
'password' => $this->generateHashedPassword(),
|
||||
'admin' => 1,
|
||||
'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
|
||||
'crdate' => $GLOBALS['EXEC_TIME'] ?? time(),
|
||||
];
|
||||
|
||||
$databaseConnection = $this->connectionPool->getConnectionForTable('be_users');
|
||||
$databaseConnection->insert('be_users', $userFields);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user with username "_cli_" exists. Deleted users are left out
|
||||
* but hidden and start / endtime restricted users are considered.
|
||||
*/
|
||||
private function cliUserExists(): bool
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$count = $queryBuilder
|
||||
->count('*')
|
||||
->from('be_users')
|
||||
->where($queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter(self::CLI_USERNAME)))
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
return (bool)$count;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function returns a salted hashed key.
|
||||
*/
|
||||
private function generateHashedPassword(): string
|
||||
{
|
||||
$cryptoService = GeneralUtility::makeInstance(Random::class);
|
||||
$password = $cryptoService->generateRandomBytes(20);
|
||||
return $this->passwordHashFactory
|
||||
->getDefaultHashInstance('BE')
|
||||
->getHashedPassword($password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
|
||||
/**
|
||||
* Class to be extended by events, fired after authentication has failed
|
||||
*/
|
||||
abstract class AbstractAuthenticationFailedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ServerRequestInterface $request
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the user, who failed to authenticate successfully
|
||||
*/
|
||||
abstract public function getUser(): AbstractUserAuthentication;
|
||||
|
||||
public function isFrontendAttempt(): bool
|
||||
{
|
||||
return !$this->isBackendAttempt();
|
||||
}
|
||||
|
||||
public function isBackendAttempt(): bool
|
||||
{
|
||||
return $this->getUser() instanceof BackendUserAuthentication;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Event;
|
||||
|
||||
/**
|
||||
* Event fired after user groups have been resolved for a specific user
|
||||
*/
|
||||
final class AfterGroupsResolvedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $sourceDatabaseTable,
|
||||
private array $groups,
|
||||
private readonly array $originalGroupIds,
|
||||
private readonly array $userData
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return string 'be_groups' or 'fe_groups' depending on context.
|
||||
*/
|
||||
public function getSourceDatabaseTable(): string
|
||||
{
|
||||
return $this->sourceDatabaseTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of group records including sub groups as resolved by core.
|
||||
*
|
||||
* Note order is important: A user with main groups "1,2", where 1 has sub group 3,
|
||||
* results in "3,1,2" as record list array - sub groups are listed before the group
|
||||
* that includes the sub group.
|
||||
*/
|
||||
public function getGroups(): array
|
||||
{
|
||||
return $this->groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of group records as manipulated by the event.
|
||||
*/
|
||||
public function setGroups(array $groups): void
|
||||
{
|
||||
$this->groups = $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of group uids directly attached to the user
|
||||
*/
|
||||
public function getOriginalGroupIds(): array
|
||||
{
|
||||
return $this->originalGroupIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full user record with all fields
|
||||
*/
|
||||
public function getUserData(): array
|
||||
{
|
||||
return $this->userData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
|
||||
/**
|
||||
* Event fired after a user has been actively logged in to backend (incl. possible MFA) or frontend.
|
||||
*/
|
||||
final readonly class AfterUserLoggedInEvent
|
||||
{
|
||||
public function __construct(
|
||||
private AbstractUserAuthentication $user,
|
||||
private ?ServerRequestInterface $request = null
|
||||
) {}
|
||||
|
||||
public function getUser(): AbstractUserAuthentication
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getRequest(): ?ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
|
||||
/**
|
||||
* Event fired after a user has been actively logged out.
|
||||
*/
|
||||
final readonly class AfterUserLoggedOutEvent
|
||||
{
|
||||
public function __construct(
|
||||
private AbstractUserAuthentication $user
|
||||
) {}
|
||||
|
||||
public function getUser(): AbstractUserAuthentication
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Authentication\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
use TYPO3\CMS\Core\Security\RequestToken;
|
||||
|
||||
/**
|
||||
* Event fired before request-token is processed.
|
||||
*/
|
||||
final class BeforeRequestTokenProcessedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbstractUserAuthentication $user,
|
||||
private readonly ServerRequestInterface $request,
|
||||
private RequestToken|false|null $requestToken
|
||||
) {}
|
||||
|
||||
public function getUser(): AbstractUserAuthentication
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function getRequestToken(): RequestToken|false|null
|
||||
{
|
||||
return $this->requestToken;
|
||||
}
|
||||
|
||||
public function setRequestToken(RequestToken|false|null $requestToken): void
|
||||
{
|
||||
$this->requestToken = $requestToken;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Authentication\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
use TYPO3\CMS\Core\Session\UserSession;
|
||||
|
||||
/**
|
||||
* Event fired before a user is going to be actively logged out.
|
||||
* An option to interrupt the regular logout flow from TYPO3 Core (so you can do this yourself)
|
||||
* is also available.
|
||||
*/
|
||||
final class BeforeUserLogoutEvent
|
||||
{
|
||||
private bool $shouldLogout = true;
|
||||
|
||||
public function __construct(
|
||||
private readonly AbstractUserAuthentication $user,
|
||||
private readonly ?UserSession $userSession
|
||||
) {}
|
||||
|
||||
public function getUser(): AbstractUserAuthentication
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function disableRegularLogoutProcess(): void
|
||||
{
|
||||
$this->shouldLogout = false;
|
||||
}
|
||||
|
||||
public function enableRegularLogoutProcess(): void
|
||||
{
|
||||
$this->shouldLogout = true;
|
||||
}
|
||||
|
||||
public function shouldLogout(): bool
|
||||
{
|
||||
return $this->shouldLogout;
|
||||
}
|
||||
|
||||
public function getUserSession(): ?UserSession
|
||||
{
|
||||
return $this->userSession;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
|
||||
/**
|
||||
* Event fired after a login attempt failed.
|
||||
*/
|
||||
final class LoginAttemptFailedEvent extends AbstractAuthenticationFailedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbstractUserAuthentication $user,
|
||||
private readonly ServerRequestInterface $request,
|
||||
private readonly array $loginData,
|
||||
) {
|
||||
parent::__construct($this->request);
|
||||
}
|
||||
|
||||
public function getUser(): AbstractUserAuthentication
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getLoginData(): array
|
||||
{
|
||||
return $this->loginData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
|
||||
|
||||
/**
|
||||
* Event fired after MFA verification failed.
|
||||
*/
|
||||
final class MfaVerificationFailedEvent extends AbstractAuthenticationFailedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ServerRequestInterface $request,
|
||||
private readonly MfaProviderPropertyManager $propertyManager,
|
||||
private readonly MfaProviderManifestInterface $mfaProvider,
|
||||
) {
|
||||
parent::__construct($this->request);
|
||||
}
|
||||
|
||||
public function getUser(): AbstractUserAuthentication
|
||||
{
|
||||
return $this->propertyManager->getUser();
|
||||
}
|
||||
|
||||
public function getProvider(): MfaProviderManifestInterface
|
||||
{
|
||||
return $this->mfaProvider;
|
||||
}
|
||||
|
||||
public function getProviderIdentifier(): string
|
||||
{
|
||||
return $this->mfaProvider->getIdentifier();
|
||||
}
|
||||
|
||||
public function getProviderProperties(): array
|
||||
{
|
||||
return $this->propertyManager->getProperties();
|
||||
}
|
||||
|
||||
public function isProviderLocked(): bool
|
||||
{
|
||||
return $this->mfaProvider->isLocked($this->propertyManager);
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Authentication\Exception;
|
||||
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
class UserSettingsNotFoundException extends Exception implements NotFoundExceptionInterface {}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Authentication\Event\AfterGroupsResolvedEvent;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A provider for resolving fe_groups / be_groups, including nested sub groups.
|
||||
*
|
||||
* When fetching subgroups, the current group (parent group) is handed in recursive.
|
||||
* Duplicates are suppressed: If a subgroup is including in multiple parent groups,
|
||||
* it will be resolved only once.
|
||||
*
|
||||
* @internal this is not part of TYPO3 Core API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class GroupResolver
|
||||
{
|
||||
private const string SOURCE_FIELD = 'usergroup';
|
||||
private const string RECURSIVE_SOURCE_FIELD = 'subgroup';
|
||||
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fetch all group records for a given user recursive.
|
||||
*
|
||||
* Note order is important: A user with main groups "1,2", where 1 has sub group 3,
|
||||
* results in "3,1,2" as record list array - sub groups are listed before the group
|
||||
* that includes the sub group.
|
||||
*
|
||||
* @param array $userRecord Used for context in PSR-14 event
|
||||
* @param string $sourceTable The database table to look up: be_groups / fe_groups depending on context
|
||||
* @return array List of group records. Note the ordering note above.
|
||||
*/
|
||||
public function resolveGroupsForUser(array $userRecord, string $sourceTable): array
|
||||
{
|
||||
$originalGroupIds = GeneralUtility::intExplode(',', (string)($userRecord[self::SOURCE_FIELD] ?? ''), true);
|
||||
$resolvedGroups = $this->fetchGroupsRecursive($sourceTable, $originalGroupIds);
|
||||
$event = $this->eventDispatcher->dispatch(new AfterGroupsResolvedEvent($sourceTable, $resolvedGroups, $originalGroupIds, $userRecord));
|
||||
return $event->getGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* This works the other way around: Find all users that belong to some groups. Because groups are nested,
|
||||
* we need to find all groups and subgroups first, because maybe a user is only part of a higher group,
|
||||
* instead of a "All editors" group.
|
||||
*
|
||||
* @param int[] $groupIds a list of IDs of groups
|
||||
* @param string $sourceTable e.g. be_groups or fe_groups
|
||||
* @param string $userSourceTable e.g. be_users or fe_users
|
||||
* @return array full user records
|
||||
*/
|
||||
public function findAllUsersInGroups(array $groupIds, string $sourceTable, string $userSourceTable): array
|
||||
{
|
||||
// Ensure the given groups exist
|
||||
$mainGroups = $this->fetchRowsFromDatabase($sourceTable, $groupIds);
|
||||
$groupIds = array_map(intval(...), array_column($mainGroups, 'uid'));
|
||||
if (empty($groupIds)) {
|
||||
return [];
|
||||
}
|
||||
$parentGroupIds = $this->fetchParentGroupsRecursive($sourceTable, $groupIds, $groupIds);
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($userSourceTable);
|
||||
$queryBuilder
|
||||
->select('*')
|
||||
->from($userSourceTable);
|
||||
|
||||
$constraints = [];
|
||||
foreach ($groupIds as $groupUid) {
|
||||
$constraints[] = $queryBuilder->expr()->inSet(self::SOURCE_FIELD, (string)$groupUid);
|
||||
}
|
||||
foreach ($parentGroupIds as $groupUid) {
|
||||
$constraints[] = $queryBuilder->expr()->inSet(self::SOURCE_FIELD, (string)$groupUid);
|
||||
}
|
||||
|
||||
$users = $queryBuilder
|
||||
->where(
|
||||
$queryBuilder->expr()->or(...$constraints)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
return !empty($users) ? $users : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a list of group uids, and take into account if groups have been loaded before.
|
||||
*
|
||||
* @param int[] $groupIds
|
||||
*/
|
||||
protected function fetchGroupsRecursive(string $sourceTable, array $groupIds, array $processedGroupIds = []): array
|
||||
{
|
||||
if (empty($groupIds)) {
|
||||
return [];
|
||||
}
|
||||
$foundGroups = $this->fetchRowsFromDatabase($sourceTable, $groupIds);
|
||||
$validGroups = [];
|
||||
foreach ($groupIds as $groupId) {
|
||||
// Database did not find the record
|
||||
if (!is_array($foundGroups[$groupId] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
// Record was already processed, continue to avoid adding this group again
|
||||
if (in_array($groupId, $processedGroupIds, true)) {
|
||||
continue;
|
||||
}
|
||||
// Add sub groups first
|
||||
$subgroupIds = GeneralUtility::intExplode(',', (string)($foundGroups[$groupId][self::RECURSIVE_SOURCE_FIELD] ?? ''), true);
|
||||
if (!empty($subgroupIds)) {
|
||||
$subgroups = $this->fetchGroupsRecursive($sourceTable, $subgroupIds, array_merge($processedGroupIds, [$groupId]));
|
||||
$validGroups = array_merge($validGroups, $subgroups);
|
||||
}
|
||||
// Add main group after sub groups have been added
|
||||
$validGroups[] = $foundGroups[$groupId];
|
||||
}
|
||||
return $validGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the database query. Does not care about ordering, this is done by caller.
|
||||
*
|
||||
* @return array Full records with record uid as key
|
||||
*/
|
||||
protected function fetchRowsFromDatabase(string $sourceTable, array $groupIds): array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($sourceTable);
|
||||
$result = $queryBuilder
|
||||
->select('*')
|
||||
->from($sourceTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->in(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter(
|
||||
$groupIds,
|
||||
Connection::PARAM_INT_ARRAY
|
||||
)
|
||||
)
|
||||
)
|
||||
->executeQuery();
|
||||
$groups = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$groups[(int)$row['uid']] = $row;
|
||||
}
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a list of group uids, and take into account if groups have been loaded before as part of recursive detection.
|
||||
*
|
||||
* @param int[] $groupIds a list of groups to find THEIR ancestors
|
||||
* @param array $processedGroupIds helper function to avoid recursive detection
|
||||
* @return array a list of parent groups and thus, grand grand parent groups as well
|
||||
*/
|
||||
protected function fetchParentGroupsRecursive(string $sourceTable, array $groupIds, array $processedGroupIds = []): array
|
||||
{
|
||||
if (empty($groupIds)) {
|
||||
return [];
|
||||
}
|
||||
$parentGroups = $this->fetchParentGroupsFromDatabase($sourceTable, $groupIds);
|
||||
$validParentGroupIds = [];
|
||||
foreach ($parentGroups as $parentGroup) {
|
||||
$parentGroupId = (int)$parentGroup['uid'];
|
||||
// Record was already processed, continue to avoid adding this group again
|
||||
if (in_array($parentGroupId, $processedGroupIds, true)) {
|
||||
continue;
|
||||
}
|
||||
$processedGroupIds[] = $parentGroupId;
|
||||
$validParentGroupIds[] = $parentGroupId;
|
||||
}
|
||||
|
||||
$grandParentGroups = $this->fetchParentGroupsRecursive($sourceTable, $validParentGroupIds, $processedGroupIds);
|
||||
return array_merge($validParentGroupIds, $grandParentGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all groups that have a FIND_IN_SET(subgroups, [$subgroupIds]) => the parent groups
|
||||
* via one SQL query.
|
||||
*/
|
||||
protected function fetchParentGroupsFromDatabase(string $sourceTable, array $subgroupIds): array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($sourceTable);
|
||||
$queryBuilder
|
||||
->select('*')
|
||||
->from($sourceTable);
|
||||
|
||||
$constraints = [];
|
||||
foreach ($subgroupIds as $subgroupId) {
|
||||
$constraints[] = $queryBuilder->expr()->inSet(self::RECURSIVE_SOURCE_FIELD, (string)$subgroupId);
|
||||
}
|
||||
|
||||
$result = $queryBuilder
|
||||
->where(
|
||||
$queryBuilder->expr()->or(...$constraints)
|
||||
)
|
||||
->executeQuery();
|
||||
|
||||
$groups = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$groups[(int)$row['uid']] = $row;
|
||||
}
|
||||
return $groups;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Handles the locking of sessions to IP addresses.
|
||||
*/
|
||||
class IpLocker
|
||||
{
|
||||
public const DISABLED_LOCK_VALUE = '[DISABLED]';
|
||||
|
||||
/**
|
||||
* If set to 4, the session will be locked to the user's IP address (all four numbers).
|
||||
* Reducing this to 1-3 means that only the given number of parts of the IP address is used.
|
||||
*/
|
||||
protected int $lockIPv4PartCount = 4;
|
||||
|
||||
/**
|
||||
* Same setting as lockIP but for IPv6 addresses.
|
||||
*/
|
||||
protected int $lockIPv6PartCount = 8;
|
||||
|
||||
public function __construct(int $lockIPv4PartCount, int $lockIPv6PartCount)
|
||||
{
|
||||
$this->lockIPv4PartCount = $lockIPv4PartCount;
|
||||
$this->lockIPv6PartCount = $lockIPv6PartCount;
|
||||
}
|
||||
|
||||
public function getSessionIpLock(string $ipAddress): string
|
||||
{
|
||||
if ($this->lockIPv4PartCount === 0 && $this->lockIPv6PartCount === 0) {
|
||||
return static::DISABLED_LOCK_VALUE;
|
||||
}
|
||||
|
||||
if ($this->isIpv6Address($ipAddress)) {
|
||||
return $this->getIpLockPartForIpv6Address($ipAddress);
|
||||
}
|
||||
return $this->getIpLockPartForIpv4Address($ipAddress);
|
||||
}
|
||||
|
||||
public function validateRemoteAddressAgainstSessionIpLock(string $ipAddress, string $sessionIpLock): bool
|
||||
{
|
||||
if ($sessionIpLock === static::DISABLED_LOCK_VALUE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$ipToCompare = $this->isIpv6Address($ipAddress)
|
||||
? $this->getIpLockPartForIpv6Address($ipAddress)
|
||||
: $this->getIpLockPartForIpv4Address($ipAddress);
|
||||
return $ipToCompare === $sessionIpLock;
|
||||
}
|
||||
|
||||
protected function getIpLockPart(string $ipAddress, int $numberOfParts, int $maxParts, string $delimiter): string
|
||||
{
|
||||
if ($numberOfParts >= $maxParts) {
|
||||
return $ipAddress;
|
||||
}
|
||||
|
||||
$numberOfParts = MathUtility::forceIntegerInRange($numberOfParts, 1, $maxParts);
|
||||
$ipParts = explode($delimiter, $ipAddress);
|
||||
|
||||
for ($a = $maxParts; $a > $numberOfParts; $a--) {
|
||||
$ipPartValue = $delimiter === '.' ? '0' : str_pad('', strlen($ipParts[$a - 1]), '0');
|
||||
$ipParts[$a - 1] = $ipPartValue;
|
||||
}
|
||||
|
||||
return implode($delimiter, $ipParts);
|
||||
}
|
||||
|
||||
protected function getIpLockPartForIpv4Address(string $ipAddress): string
|
||||
{
|
||||
if ($this->lockIPv4PartCount === 0) {
|
||||
return static::DISABLED_LOCK_VALUE;
|
||||
}
|
||||
|
||||
return $this->getIpLockPart($ipAddress, $this->lockIPv4PartCount, 4, '.');
|
||||
}
|
||||
|
||||
protected function getIpLockPartForIpv6Address(string $ipAddress): string
|
||||
{
|
||||
if ($this->lockIPv6PartCount === 0) {
|
||||
return static::DISABLED_LOCK_VALUE;
|
||||
}
|
||||
|
||||
// inet_pton also takes care of IPv4-mapped addresses (see https://en.wikipedia.org/wiki/IPv6_address#Representation)
|
||||
$unpacked = unpack('H*hex', (string)inet_pton($ipAddress)) ?: [];
|
||||
$expandedAddress = rtrim(chunk_split($unpacked['hex'] ?? '', 4, ':'), ':');
|
||||
return $this->getIpLockPart($expandedAddress, $this->lockIPv6PartCount, 8, ':');
|
||||
}
|
||||
|
||||
protected function isIpv6Address(string $ipAddress): bool
|
||||
{
|
||||
return str_contains($ipAddress, ':');
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Authentication;
|
||||
|
||||
use TYPO3\CMS\Core\Type\BitSet;
|
||||
|
||||
/**
|
||||
* Bitset for bitwise operations on javascript confirmation popups
|
||||
*
|
||||
* @see https://docs.typo3.org/m/typo3/reference-tsconfig/main/en-us/UserTsconfig/Options.html#alertpopups
|
||||
*/
|
||||
final class JsConfirmation extends BitSet
|
||||
{
|
||||
public const int TYPE_CHANGE = 0b00000001;
|
||||
public const int COPY_MOVE_PASTE = 0b00000010;
|
||||
public const int DELETE = 0b00000100;
|
||||
public const int FE_EDIT = 0b00001000;
|
||||
private const int UNUSED_16 = 0b00010000;
|
||||
private const int UNUSED_32 = 0b00100000;
|
||||
private const int UNUSED_64 = 0b01000000;
|
||||
public const int OTHER = 0b10000000;
|
||||
public const ALL = self::TYPE_CHANGE | self::COPY_MOVE_PASTE | self::DELETE | self::FE_EDIT | self::UNUSED_16 | self::UNUSED_32 | self::UNUSED_64 | self::OTHER;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
/**
|
||||
* Contains the different login types
|
||||
*/
|
||||
enum LoginType: string
|
||||
{
|
||||
case LOGIN = 'login';
|
||||
case LOGOUT = 'logout';
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* To be implemented by all MFA providers.
|
||||
*/
|
||||
interface MfaProviderInterface
|
||||
{
|
||||
/**
|
||||
* Check if the current request can be handled by this provider (e.g.
|
||||
* necessary query arguments are set).
|
||||
*/
|
||||
public function canProcess(ServerRequestInterface $request): bool;
|
||||
|
||||
/**
|
||||
* Check if provider is active for the user by e.g. checking the user
|
||||
* record for some provider specific active state.
|
||||
*/
|
||||
public function isActive(MfaProviderPropertyManager $propertyManager): bool;
|
||||
|
||||
/**
|
||||
* Check if provider is temporarily locked for the user, because
|
||||
* of e.g. to much false authentication attempts. This differs
|
||||
* from the "isActive" state on purpose, so please DO NOT use
|
||||
* the "isActive" state for such check internally. This will
|
||||
* allow attackers to easily circumvent MFA!
|
||||
*/
|
||||
public function isLocked(MfaProviderPropertyManager $propertyManager): bool;
|
||||
|
||||
/**
|
||||
* Verifies the MFA request
|
||||
*/
|
||||
public function verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool;
|
||||
|
||||
/**
|
||||
* Generate the provider specific response for the given view type.
|
||||
* Note: Currently the calling controller only evaluates the response
|
||||
* body and directly injects it into the corresponding view. It's however
|
||||
* planned to also take further information like headers into account.
|
||||
*
|
||||
* @see MfaViewType
|
||||
*/
|
||||
public function handleRequest(
|
||||
ServerRequestInterface $request,
|
||||
MfaProviderPropertyManager $propertyManager,
|
||||
MfaViewType $type
|
||||
): ResponseInterface;
|
||||
|
||||
/**
|
||||
* Activate / register this provider for the user
|
||||
*
|
||||
* @return bool TRUE in case operation was successful, FALSE otherwise
|
||||
*/
|
||||
public function activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool;
|
||||
|
||||
/**
|
||||
* Deactivate this provider for the user
|
||||
*
|
||||
* @return bool TRUE in case operation was successful, FALSE otherwise
|
||||
*/
|
||||
public function deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool;
|
||||
|
||||
/**
|
||||
* Unlock this provider for the user
|
||||
*
|
||||
* @return bool TRUE in case operation was successful, FALSE otherwise
|
||||
*/
|
||||
public function unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool;
|
||||
|
||||
/**
|
||||
* Handle changes of the provider by the user
|
||||
*
|
||||
* @return bool TRUE in case operation was successful, FALSE otherwise
|
||||
*/
|
||||
public function update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool;
|
||||
}
|
||||
@@ -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\Core\Authentication\Mfa;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Adapter for MFA providers
|
||||
*
|
||||
* @internal should only be used by the TYPO3 Core
|
||||
*/
|
||||
final class MfaProviderManifest implements MfaProviderManifestInterface
|
||||
{
|
||||
private ?MfaProviderInterface $instance = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $identifier,
|
||||
private readonly string $title,
|
||||
private readonly string $description,
|
||||
private readonly string $setupInstructions,
|
||||
private readonly string $iconIdentifier,
|
||||
private readonly bool $isDefaultProviderAllowed,
|
||||
private readonly string $serviceName,
|
||||
private readonly ContainerInterface $container
|
||||
) {}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getIconIdentifier(): string
|
||||
{
|
||||
return $this->iconIdentifier;
|
||||
}
|
||||
|
||||
public function getSetupInstructions(): string
|
||||
{
|
||||
return $this->setupInstructions;
|
||||
}
|
||||
|
||||
public function isDefaultProviderAllowed(): bool
|
||||
{
|
||||
return $this->isDefaultProviderAllowed;
|
||||
}
|
||||
|
||||
public function canProcess(ServerRequestInterface $request): bool
|
||||
{
|
||||
return $this->getInstance()->canProcess($request);
|
||||
}
|
||||
|
||||
public function isActive(MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return $this->getInstance()->isActive($propertyManager);
|
||||
}
|
||||
|
||||
public function isLocked(MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return $this->getInstance()->isLocked($propertyManager);
|
||||
}
|
||||
|
||||
public function verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return $this->getInstance()->verify($request, $propertyManager);
|
||||
}
|
||||
|
||||
public function handleRequest(
|
||||
ServerRequestInterface $request,
|
||||
MfaProviderPropertyManager $propertyManager,
|
||||
MfaViewType $type
|
||||
): ResponseInterface {
|
||||
return $this->getInstance()->handleRequest($request, $propertyManager, $type);
|
||||
}
|
||||
|
||||
public function activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return $this->getInstance()->activate($request, $propertyManager);
|
||||
}
|
||||
|
||||
public function deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return $this->getInstance()->deactivate($request, $propertyManager);
|
||||
}
|
||||
|
||||
public function unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return $this->getInstance()->unlock($request, $propertyManager);
|
||||
}
|
||||
|
||||
public function update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return $this->getInstance()->update($request, $propertyManager);
|
||||
}
|
||||
|
||||
private function getInstance(): MfaProviderInterface
|
||||
{
|
||||
return $this->instance ?? $this->createInstance();
|
||||
}
|
||||
|
||||
private function createInstance(): MfaProviderInterface
|
||||
{
|
||||
$this->instance = $this->container->get($this->serviceName);
|
||||
return $this->instance;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Authentication\Mfa;
|
||||
|
||||
/**
|
||||
* Annotated information about the MFA provider – used in various views
|
||||
*
|
||||
* @internal should only be used by the TYPO3 Core
|
||||
*/
|
||||
interface MfaProviderManifestInterface extends MfaProviderInterface
|
||||
{
|
||||
/**
|
||||
* Unique provider identifier
|
||||
*/
|
||||
public function getIdentifier(): string;
|
||||
|
||||
/**
|
||||
* The title of the provider
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* A short description about the provider
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
|
||||
/**
|
||||
* Instructions to be displayed in the setup view
|
||||
*/
|
||||
public function getSetupInstructions(): string;
|
||||
|
||||
/**
|
||||
* The icon identifier for this provider
|
||||
*/
|
||||
public function getIconIdentifier(): string;
|
||||
|
||||
/**
|
||||
* Whether the provider is allowed to be set as default
|
||||
*/
|
||||
public function isDefaultProviderAllowed(): bool;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Basic manager for MFA providers to access and update their
|
||||
* properties (information) from the mfa column in the user array.
|
||||
*/
|
||||
class MfaProviderPropertyManager implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
protected const DATABASE_FIELD_NAME = 'mfa';
|
||||
|
||||
protected array $mfa;
|
||||
protected array $providerProperties;
|
||||
|
||||
public function __construct(protected readonly AbstractUserAuthentication $user, protected readonly string $providerIdentifier)
|
||||
{
|
||||
$this->mfa = json_decode($user->user[self::DATABASE_FIELD_NAME] ?? '', true) ?? [];
|
||||
$this->providerProperties = $this->mfa[$this->providerIdentifier] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider entry exists for the current user
|
||||
*/
|
||||
public function hasProviderEntry(): bool
|
||||
{
|
||||
return isset($this->mfa[$this->providerIdentifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider property exists
|
||||
*/
|
||||
public function hasProperty(string $key): bool
|
||||
{
|
||||
return isset($this->providerProperties[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a provider specific property value or the defined
|
||||
* default value if the requested property was not found.
|
||||
*/
|
||||
public function getProperty(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->providerProperties[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider specific properties
|
||||
*/
|
||||
public function getProperties(): array
|
||||
{
|
||||
return $this->providerProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the provider properties
|
||||
* Note: If no entry exists yet, use createProviderEntry() instead.
|
||||
* This can be checked with hasProviderEntry().
|
||||
*/
|
||||
public function updateProperties(array $properties): bool
|
||||
{
|
||||
// This is to prevent provider data inconsistency
|
||||
if (!$this->hasProviderEntry()) {
|
||||
throw new \InvalidArgumentException(
|
||||
'No entry for provider ' . $this->providerIdentifier . ' exists yet. Use createProviderEntry() instead.',
|
||||
1613993188
|
||||
);
|
||||
}
|
||||
|
||||
if (!isset($properties['updated'])) {
|
||||
$properties['updated'] = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
|
||||
}
|
||||
|
||||
$this->providerProperties = array_replace($this->providerProperties, $properties);
|
||||
$this->mfa[$this->providerIdentifier] = $this->providerProperties;
|
||||
return $this->storeProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new provider entry for the current user
|
||||
* Note: If an entry already exists, use updateProperties() instead.
|
||||
* This can be checked with hasProviderEntry().
|
||||
*/
|
||||
public function createProviderEntry(array $properties): bool
|
||||
{
|
||||
// This is to prevent unintentional overwriting of provider entries
|
||||
if ($this->hasProviderEntry()) {
|
||||
throw new \InvalidArgumentException(
|
||||
'A entry for provider ' . $this->providerIdentifier . ' already exists. Use updateProperties() instead.',
|
||||
1612781782
|
||||
);
|
||||
}
|
||||
|
||||
if (!isset($properties['created'])) {
|
||||
$properties['created'] = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
|
||||
}
|
||||
|
||||
if (!isset($properties['updated'])) {
|
||||
$properties['updated'] = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
|
||||
}
|
||||
|
||||
$this->providerProperties = $properties;
|
||||
$this->mfa[$this->providerIdentifier] = $this->providerProperties;
|
||||
return $this->storeProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a provider entry for the current user
|
||||
*
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function deleteProviderEntry(): bool
|
||||
{
|
||||
$this->providerProperties = [];
|
||||
unset($this->mfa[$this->providerIdentifier]);
|
||||
return $this->storeProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the updated properties in the user array and the database
|
||||
*
|
||||
* @throws \JsonException
|
||||
*/
|
||||
protected function storeProperties(): bool
|
||||
{
|
||||
// encode the mfa properties to store them in the database and the user array
|
||||
$mfa = json_encode($this->mfa, JSON_THROW_ON_ERROR) ?: '';
|
||||
|
||||
// Write back the updated mfa properties to the user array
|
||||
$this->user->user[self::DATABASE_FIELD_NAME] = $mfa;
|
||||
|
||||
// Log MFA update
|
||||
$this->logger->debug('MFA properties updated', [
|
||||
'provider' => $this->providerIdentifier,
|
||||
'user' => [
|
||||
'uid' => $this->user->getUserId(),
|
||||
'username' => $this->user->getUserName(),
|
||||
],
|
||||
]);
|
||||
|
||||
// Store updated mfa properties in the database
|
||||
return (bool)GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user->user_table)->update(
|
||||
$this->user->user_table,
|
||||
[self::DATABASE_FIELD_NAME => $mfa],
|
||||
[$this->user->userid_column => (int)$this->user->getUserId()],
|
||||
[self::DATABASE_FIELD_NAME => Connection::PARAM_LOB]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current user
|
||||
*/
|
||||
public function getUser(): AbstractUserAuthentication
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current providers identifier
|
||||
*/
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->providerIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create property manager for the user with the given provider
|
||||
*/
|
||||
public static function create(MfaProviderManifestInterface $provider, AbstractUserAuthentication $user): self
|
||||
{
|
||||
return GeneralUtility::makeInstance(self::class, $user, $provider->getIdentifier());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
|
||||
/**
|
||||
* Registry for configuration providers which is called by the ConfigurationProviderPass
|
||||
*
|
||||
* @internal should only be used by the TYPO3 Core
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class MfaProviderRegistry
|
||||
{
|
||||
/**
|
||||
* @var MfaProviderManifestInterface[]
|
||||
*/
|
||||
protected array $providers = [];
|
||||
|
||||
public function registerProvider(MfaProviderManifestInterface $provider): void
|
||||
{
|
||||
$this->providers[$provider->getIdentifier()] = $provider;
|
||||
}
|
||||
|
||||
public function hasProvider(string $identifier): bool
|
||||
{
|
||||
return isset($this->providers[$identifier]);
|
||||
}
|
||||
|
||||
public function hasProviders(): bool
|
||||
{
|
||||
return $this->providers !== [];
|
||||
}
|
||||
|
||||
public function getProvider(string $identifier): MfaProviderManifestInterface
|
||||
{
|
||||
if (!$this->hasProvider($identifier)) {
|
||||
throw new \InvalidArgumentException('No MFA provider for identifier ' . $identifier . ' found.', 1610994735);
|
||||
}
|
||||
return $this->providers[$identifier];
|
||||
}
|
||||
|
||||
public function getProviders(): array
|
||||
{
|
||||
return $this->providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given user has active providers
|
||||
*/
|
||||
public function hasActiveProviders(AbstractUserAuthentication $user): bool
|
||||
{
|
||||
return $this->getActiveProviders($user) !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active providers for the given user
|
||||
*
|
||||
* @return MfaProviderManifestInterface[]
|
||||
*/
|
||||
public function getActiveProviders(AbstractUserAuthentication $user): array
|
||||
{
|
||||
return array_filter($this->providers, static function (MfaProviderManifestInterface $provider) use ($user): bool {
|
||||
return $provider->isActive(MfaProviderPropertyManager::create($provider, $user));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first provider for the user which can be used for authentication.
|
||||
* This is either the user specified default provider, or the first active
|
||||
* provider based on the providers configured ordering.
|
||||
*
|
||||
* @return MfaProviderManifestInterface
|
||||
*/
|
||||
public function getFirstAuthenticationAwareProvider(AbstractUserAuthentication $user): ?MfaProviderManifestInterface
|
||||
{
|
||||
$activeProviders = $this->getActiveProviders($user);
|
||||
// If the user did not activate any provider yet, authentication is not possible
|
||||
if ($activeProviders === []) {
|
||||
return null;
|
||||
}
|
||||
// Check if the user has chosen a default (preferred) provider, which is still active
|
||||
$defaultProvider = (string)($user->uc['mfa']['defaultProvider'] ?? '');
|
||||
if ($defaultProvider !== '' && isset($activeProviders[$defaultProvider])) {
|
||||
return $activeProviders[$defaultProvider];
|
||||
}
|
||||
// If no default provider exists or is not valid, return the first active provider
|
||||
return array_shift($activeProviders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given user has locked providers
|
||||
*/
|
||||
public function hasLockedProviders(AbstractUserAuthentication $user): bool
|
||||
{
|
||||
return $this->getLockedProviders($user) !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all locked providers for the given user
|
||||
*
|
||||
* @return MfaProviderManifestInterface[]
|
||||
*/
|
||||
public function getLockedProviders(AbstractUserAuthentication $user): array
|
||||
{
|
||||
return array_filter($this->providers, static function (MfaProviderManifestInterface $provider) use ($user): bool {
|
||||
return $provider->isLocked(MfaProviderPropertyManager::create($provider, $user));
|
||||
});
|
||||
}
|
||||
|
||||
public function allowedProvidersItemsProcFunc(array &$parameters): void
|
||||
{
|
||||
foreach ($this->providers as $provider) {
|
||||
$parameters['items'][] = [
|
||||
'label' => $provider->getTitle(),
|
||||
'value' => $provider->getIdentifier(),
|
||||
'icon' => $provider->getIconIdentifier(),
|
||||
'description' => $provider->getDescription(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* This exception is thrown during the authentication process
|
||||
* when a user has successfully passed his first authentication
|
||||
* method (e.g. via username+password), but is required to also
|
||||
* pass multi-factor authentication (e.g. one-time password).
|
||||
*/
|
||||
class MfaRequiredException extends Exception
|
||||
{
|
||||
public function __construct(private readonly MfaProviderManifestInterface $provider, $code = 0, $message = '', ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getProvider(): MfaProviderManifestInterface
|
||||
{
|
||||
return $this->provider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa;
|
||||
|
||||
/**
|
||||
* Enumeration of possible view types for MFA providers
|
||||
*/
|
||||
enum MfaViewType: string
|
||||
{
|
||||
case SETUP = 'setup';
|
||||
case EDIT = 'edit';
|
||||
case AUTH = 'auth';
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa\Provider;
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Implementation for generation and validation of recovery codes
|
||||
*
|
||||
* @internal should only be used by the TYPO3 Core
|
||||
*/
|
||||
class RecoveryCodes
|
||||
{
|
||||
private const int MIN_LENGTH = 8;
|
||||
|
||||
protected PasswordHashFactory $passwordHashFactory;
|
||||
|
||||
public function __construct(protected readonly string $mode)
|
||||
{
|
||||
$this->passwordHashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate plain and hashed recovery codes and return them as key/value
|
||||
*/
|
||||
public function generateRecoveryCodes(): array
|
||||
{
|
||||
$plainCodes = $this->generatePlainRecoveryCodes();
|
||||
return array_combine($plainCodes, $this->generatedHashedRecoveryCodes($plainCodes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate given amount of plain recovery codes with the given length
|
||||
*
|
||||
* @return list<non-empty-string>
|
||||
*/
|
||||
public function generatePlainRecoveryCodes(int $length = 8, int $quantity = 8): array
|
||||
{
|
||||
if ($length < self::MIN_LENGTH) {
|
||||
throw new \InvalidArgumentException(
|
||||
$length . ' is not allowed as length for recovery codes. Must be at least ' . self::MIN_LENGTH,
|
||||
1613666803
|
||||
);
|
||||
}
|
||||
|
||||
/** @var list<non-empty-string> $codes */
|
||||
$codes = [];
|
||||
while ($quantity >= 1 && count($codes) < $quantity) {
|
||||
$code = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$code .= (string)random_int(0, 9);
|
||||
}
|
||||
// Prevent duplicate codes which is however very unlikely to happen
|
||||
if (!in_array($code, $codes, true)) {
|
||||
$codes[] = $code;
|
||||
}
|
||||
}
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash the given plain recovery codes with the default hash instance and return them
|
||||
*/
|
||||
public function generatedHashedRecoveryCodes(array $codes): array
|
||||
{
|
||||
// Use the current default hash instance for hashing the recovery codes
|
||||
$hashInstance = $this->passwordHashFactory->getDefaultHashInstance($this->mode);
|
||||
|
||||
foreach ($codes as &$code) {
|
||||
$code = $hashInstance->getHashedPassword($code);
|
||||
}
|
||||
unset($code);
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare given recovery code against all hashed codes and
|
||||
* unset the corresponding code on success.
|
||||
*/
|
||||
public function verifyRecoveryCode(string $recoveryCode, array &$codes): bool
|
||||
{
|
||||
if ($codes === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the hash instance which was initially used to generate these codes.
|
||||
// This could differ from the current default hash instance. We however only need
|
||||
// to check the first code since recovery codes can not be generated individually.
|
||||
$hasInstance = $this->passwordHashFactory->get(reset($codes), $this->mode);
|
||||
|
||||
foreach ($codes as $key => $code) {
|
||||
// Compare hashed codes
|
||||
if ($hasInstance->checkPassword($recoveryCode, $code)) {
|
||||
// Unset the matching code
|
||||
unset($codes[$key]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa\Provider;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderInterface;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\PropagateResponseException;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
|
||||
/**
|
||||
* MFA provider for authentication with recovery codes
|
||||
*
|
||||
* @internal should only be used by the TYPO3 Core
|
||||
*/
|
||||
final readonly class RecoveryCodesProvider implements MfaProviderInterface
|
||||
{
|
||||
private const int MAX_ATTEMPTS = 3;
|
||||
public function __construct(
|
||||
private MfaProviderRegistry $mfaProviderRegistry,
|
||||
private Context $context,
|
||||
private UriBuilder $uriBuilder,
|
||||
private FlashMessageService $flashMessageService,
|
||||
private HashService $hashService,
|
||||
private ViewFactoryInterface $viewFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check if a recovery code is given in the current request
|
||||
*/
|
||||
public function canProcess(ServerRequestInterface $request): bool
|
||||
{
|
||||
return $this->getRecoveryCode($request) !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if the provider is activated by checking the
|
||||
* active state from the provider properties. This provider
|
||||
* furthermore has a mannerism that it only works if at least
|
||||
* one other MFA provider is activated for the user.
|
||||
*/
|
||||
public function isActive(MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return $propertyManager->getProperty('active')
|
||||
&& $this->activeProvidersExist($propertyManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if the provider is temporarily locked by checking
|
||||
* the current attempts state from the provider properties and
|
||||
* if there are still recovery codes left.
|
||||
*/
|
||||
public function isLocked(MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
$attempts = (int)$propertyManager->getProperty('attempts', 0);
|
||||
$codes = (array)$propertyManager->getProperty('codes', []);
|
||||
// Assume the provider is locked in case either the maximum attempts are exceeded or no codes
|
||||
// are available. A provider however can only be locked if set up - an entry exists in database.
|
||||
return $propertyManager->hasProviderEntry() && ($attempts >= self::MAX_ATTEMPTS || $codes === []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given recovery code and remove it from the
|
||||
* provider properties if valid.
|
||||
*/
|
||||
public function verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if (!$this->isActive($propertyManager) || $this->isLocked($propertyManager)) {
|
||||
// Can not verify an inactive or locked provider
|
||||
return false;
|
||||
}
|
||||
|
||||
$recoveryCode = $this->getRecoveryCode($request);
|
||||
$codes = $propertyManager->getProperty('codes', []);
|
||||
$recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager));
|
||||
if (!$recoveryCodes->verifyRecoveryCode($recoveryCode, $codes)) {
|
||||
$attempts = $propertyManager->getProperty('attempts', 0);
|
||||
$propertyManager->updateProperties(['attempts' => ++$attempts]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since the codes were passed by reference to the verify method, the matching code was
|
||||
// unset so we simply need to write the array back. However, if the update fails, we must
|
||||
// return FALSE even if the authentication was successful to prevent data inconsistency.
|
||||
return $propertyManager->updateProperties([
|
||||
'codes' => $codes,
|
||||
'attempts' => 0,
|
||||
'lastUsed' => $this->context->getPropertyFromAspect('date', 'timestamp'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the provider specific response for the given content type
|
||||
*
|
||||
* @throws PropagateResponseException
|
||||
*/
|
||||
public function handleRequest(
|
||||
ServerRequestInterface $request,
|
||||
MfaProviderPropertyManager $propertyManager,
|
||||
MfaViewType $type
|
||||
): ResponseInterface {
|
||||
$viewFactoryData = new ViewFactoryData(
|
||||
templateRootPaths: ['EXT:core/Resources/Private/Templates'],
|
||||
partialRootPaths: ['EXT:core/Resources/Private/Partials'],
|
||||
layoutRootPaths: ['EXT:core/Resources/Private/Layouts'],
|
||||
request: $request,
|
||||
);
|
||||
switch ($type) {
|
||||
case MfaViewType::SETUP:
|
||||
if (!$this->activeProvidersExist($propertyManager)) {
|
||||
// If no active providers are present for the current user, add a flash message and redirect
|
||||
$lang = $this->getLanguageService();
|
||||
$this->addFlashMessage(
|
||||
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.message'),
|
||||
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:setup.recoveryCodes.noActiveProviders.title'),
|
||||
ContextualFeedbackSeverity::WARNING
|
||||
);
|
||||
if (($normalizedParams = $request->getAttribute('normalizedParams'))) {
|
||||
$returnUrl = $normalizedParams->getHttpReferer();
|
||||
} else {
|
||||
// @todo this will not work for FE - make this more generic!
|
||||
$returnUrl = $this->uriBuilder->buildUriFromRoute('mfa');
|
||||
}
|
||||
throw new PropagateResponseException(new RedirectResponse($returnUrl, 303), 1612883326);
|
||||
}
|
||||
$codes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager))->generatePlainRecoveryCodes();
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
$view->assignMultiple([
|
||||
'providerIdentifier' => $propertyManager->getIdentifier(),
|
||||
'recoveryCodes' => implode(PHP_EOL, $codes),
|
||||
// Generate hmac of the recovery codes to prevent them from being changed in the setup from
|
||||
'checksum' => $this->hashService->hmac(json_encode($codes) ?: '', 'recovery-codes-setup', HashAlgo::SHA3_256),
|
||||
]);
|
||||
return new HtmlResponse($view->render('Authentication/MfaProvider/RecoveryCodes/Setup'));
|
||||
case MfaViewType::EDIT:
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
$view->assignMultiple([
|
||||
'providerIdentifier' => $propertyManager->getIdentifier(),
|
||||
'name' => $propertyManager->getProperty('name'),
|
||||
'amountOfCodesLeft' => count($propertyManager->getProperty('codes', [])),
|
||||
'lastUsed' => $this->getDateTime($propertyManager->getProperty('lastUsed', 0)),
|
||||
'updated' => $this->getDateTime($propertyManager->getProperty('updated', 0)),
|
||||
]);
|
||||
return new HtmlResponse($view->render('Authentication/MfaProvider/RecoveryCodes/Edit'));
|
||||
default: // MfaViewType::AUTH
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
$view->assignMultiple([
|
||||
'providerIdentifier' => $propertyManager->getIdentifier(),
|
||||
'isLocked' => $this->isLocked($propertyManager),
|
||||
]);
|
||||
return new HtmlResponse($view->render('Authentication/MfaProvider/RecoveryCodes/Auth'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the provider by hashing and storing the given recovery codes
|
||||
*/
|
||||
public function activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if ($this->isActive($propertyManager)) {
|
||||
// Can not activate an active provider
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->activeProvidersExist($propertyManager)) {
|
||||
// Can not activate since no other provider is activated yet
|
||||
return false;
|
||||
}
|
||||
|
||||
$recoveryCodes = GeneralUtility::trimExplode(PHP_EOL, (string)($request->getParsedBody()['recoveryCodes'] ?? ''));
|
||||
$checksum = (string)($request->getParsedBody()['checksum'] ?? '');
|
||||
if ($recoveryCodes === []
|
||||
|| !hash_equals($this->hashService->hmac(json_encode($recoveryCodes) ?: '', 'recovery-codes-setup', HashAlgo::SHA3_256), $checksum)
|
||||
) {
|
||||
// Return since the request does not contain the initially created recovery codes
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hash given plain recovery codes and prepare the properties array with active state and custom name
|
||||
$hashedCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager))->generatedHashedRecoveryCodes($recoveryCodes);
|
||||
$properties = ['codes' => $hashedCodes, 'active' => true];
|
||||
if (($name = (string)($request->getParsedBody()['name'] ?? '')) !== '') {
|
||||
$properties['name'] = $name;
|
||||
}
|
||||
|
||||
// Usually there should be no entry if the provider is not activated, but to prevent the
|
||||
// provider from being unable to activate again, we update the existing entry in such case.
|
||||
return $propertyManager->hasProviderEntry()
|
||||
? $propertyManager->updateProperties($properties)
|
||||
: $propertyManager->createProviderEntry($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the deactivate action by removing the provider entry
|
||||
*/
|
||||
public function deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
// Only check for the active property here to enable bulk deactivation,
|
||||
// e.g. in FormEngine. Otherwise, it would not be possible to deactivate
|
||||
// this provider if the last "fully" provider was deactivated before.
|
||||
if (!(bool)$propertyManager->getProperty('active')) {
|
||||
// Can not deactivate an inactive provider
|
||||
return false;
|
||||
}
|
||||
// Delete the provider entry
|
||||
return $propertyManager->deleteProviderEntry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the unlock action by resetting the attempts
|
||||
* provider property and issuing new codes.
|
||||
*/
|
||||
public function unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if (!$this->isActive($propertyManager) || !$this->isLocked($propertyManager)) {
|
||||
// Can not unlock an inactive or not locked provider
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset attempts
|
||||
if ((int)$propertyManager->getProperty('attempts', 0) !== 0
|
||||
&& !$propertyManager->updateProperties(['attempts' => 0])
|
||||
) {
|
||||
// Could not reset the attempts, so we can not unlock the provider
|
||||
return false;
|
||||
}
|
||||
|
||||
// Regenerate codes
|
||||
if ($propertyManager->getProperty('codes', []) === []) {
|
||||
// Generate new codes and store the hashed ones
|
||||
$recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager))->generateRecoveryCodes();
|
||||
if (!$propertyManager->updateProperties(['codes' => array_values($recoveryCodes)])) {
|
||||
// Codes could not be stored, so we can not unlock the provider
|
||||
return false;
|
||||
}
|
||||
// Add the newly generated codes to a flash message so the user can copy them
|
||||
$lang = $this->getLanguageService();
|
||||
$this->addFlashMessage(
|
||||
sprintf(
|
||||
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.message'),
|
||||
implode(' ', array_keys($recoveryCodes))
|
||||
),
|
||||
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:unlock.recoveryCodes.title'),
|
||||
ContextualFeedbackSeverity::WARNING
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if (!$this->isActive($propertyManager) || $this->isLocked($propertyManager)) {
|
||||
// Can not update an inactive or locked provider
|
||||
return false;
|
||||
}
|
||||
|
||||
$name = (string)($request->getParsedBody()['name'] ?? '');
|
||||
if ($name !== '' && !$propertyManager->updateProperties(['name' => $name])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((bool)($request->getParsedBody()['regenerateCodes'] ?? false)) {
|
||||
// Generate new codes and store the hashed ones
|
||||
$recoveryCodes = GeneralUtility::makeInstance(RecoveryCodes::class, $this->getMode($propertyManager))->generateRecoveryCodes();
|
||||
if (!$propertyManager->updateProperties(['codes' => array_values($recoveryCodes)])) {
|
||||
// Codes could not be stored, so we can not update the provider
|
||||
return false;
|
||||
}
|
||||
// Add the newly generated codes to a flash message so the user can copy them
|
||||
$lang = $this->getLanguageService();
|
||||
$this->addFlashMessage(
|
||||
sprintf(
|
||||
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.message'),
|
||||
implode(' ', array_keys($recoveryCodes))
|
||||
),
|
||||
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mfa_provider.xlf:update.recoveryCodes.title'),
|
||||
ContextualFeedbackSeverity::OK
|
||||
);
|
||||
}
|
||||
|
||||
// Provider properties successfully updated
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has other active providers
|
||||
*/
|
||||
private function activeProvidersExist(MfaProviderPropertyManager $currentPropertyManager): bool
|
||||
{
|
||||
$user = $currentPropertyManager->getUser();
|
||||
foreach ($this->mfaProviderRegistry->getProviders() as $identifier => $provider) {
|
||||
$propertyManager = MfaProviderPropertyManager::create($provider, $user);
|
||||
if ($identifier !== $currentPropertyManager->getIdentifier() && $provider->isActive($propertyManager)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper method for fetching the recovery code from the request
|
||||
*/
|
||||
private function getRecoveryCode(ServerRequestInterface $request): string
|
||||
{
|
||||
return trim((string)($request->getQueryParams()['rc'] ?? $request->getParsedBody()['rc'] ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the mode (used for the hash instance) based on the current users table
|
||||
*/
|
||||
private function getMode(MfaProviderPropertyManager $propertyManager): string
|
||||
{
|
||||
return $propertyManager->getUser()->loginType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom flash message for this provider
|
||||
* Note: The flash messages added by the main controller are still shown to the user.
|
||||
*/
|
||||
private function addFlashMessage(string $message, string $title = '', ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::INFO): void
|
||||
{
|
||||
$this->flashMessageService->getMessageQueueByIdentifier()->enqueue(
|
||||
new FlashMessage($message, $title, $severity, true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the timestamp as local time (date string) by applying the globally configured format
|
||||
*/
|
||||
private function getDateTime(int $timestamp): string
|
||||
{
|
||||
if ($timestamp === 0) {
|
||||
return '';
|
||||
}
|
||||
return date(
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
|
||||
$timestamp
|
||||
) ?: '';
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa\Provider;
|
||||
|
||||
use Base32\Base32;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Time-based one-time password (TOTP) implementation according to rfc6238
|
||||
*
|
||||
* @internal should only be used by the TYPO3 Core
|
||||
*/
|
||||
class Totp
|
||||
{
|
||||
private const array ALLOWED_ALGOS = ['sha1', 'sha256', 'sha512'];
|
||||
private const int MIN_LENGTH = 6;
|
||||
private const int MAX_LENGTH = 8;
|
||||
|
||||
public function __construct(
|
||||
protected readonly string $secret,
|
||||
protected readonly string $algo = 'sha1',
|
||||
protected readonly int $length = 6,
|
||||
protected readonly int $step = 30,
|
||||
protected readonly int $epoch = 0
|
||||
) {
|
||||
if (!in_array($this->algo, self::ALLOWED_ALGOS, true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
$this->algo . ' is not allowed. Allowed algos are: ' . implode(',', self::ALLOWED_ALGOS),
|
||||
1611748791
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->length < self::MIN_LENGTH || $this->length > self::MAX_LENGTH) {
|
||||
throw new \InvalidArgumentException(
|
||||
$this->length . ' is not allowed as TOTP length. Must be between ' . self::MIN_LENGTH . ' and ' . self::MAX_LENGTH,
|
||||
1611748792
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a time-based one-time password for the given counter according to rfc4226
|
||||
*
|
||||
* @param int $counter A timestamp (counter) according to rfc6238
|
||||
* @return string The generated TOTP
|
||||
*/
|
||||
public function generateTotp(int $counter): string
|
||||
{
|
||||
// Generate a 8-byte counter value (C) from the given counter input
|
||||
$binary = [];
|
||||
while ($counter !== 0) {
|
||||
$binary[] = pack('C*', $counter);
|
||||
$counter >>= 8;
|
||||
}
|
||||
// Implode and fill with NULL values
|
||||
$binary = str_pad(implode(array_reverse($binary)), 8, "\000", STR_PAD_LEFT);
|
||||
// Create a 20-byte hash string (HS) with given algo and decoded shared secret (K)
|
||||
$hash = hash_hmac($this->algo, $binary, $this->getDecodedSecret());
|
||||
// Convert hash into hex and generate an array with the decimal values of the hash
|
||||
$hmac = [];
|
||||
foreach (str_split($hash, 2) as $hex) {
|
||||
$hmac[] = hexdec($hex);
|
||||
}
|
||||
// Generate a 4-byte string with dynamic truncation (DT)
|
||||
$offset = $hmac[count($hmac) - 1] & 0xf;
|
||||
$bits = ((($hmac[$offset + 0] & 0x7f) << 24) | (($hmac[$offset + 1] & 0xff) << 16) | (($hmac[$offset + 2] & 0xff) << 8) | ($hmac[$offset + 3] & 0xff));
|
||||
// Compute the TOTP value by reducing the bits modulo 10^Digits and filling it with zeros '0'
|
||||
return str_pad((string)($bits % (10 ** $this->length)), $this->length, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given time-based one-time password
|
||||
*
|
||||
* @param string $totp The time-based one-time password to be verified
|
||||
* @param int|null $gracePeriod The grace period for the TOTP +- (mainly to circumvent transmission delays)
|
||||
*/
|
||||
public function verifyTotp(string $totp, ?int $gracePeriod = null): bool
|
||||
{
|
||||
$counter = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
|
||||
|
||||
// If no grace period is given, only check once
|
||||
if ($gracePeriod === null) {
|
||||
return $this->compare($totp, $this->getTimeCounter($counter));
|
||||
}
|
||||
|
||||
// Check the token within the given grace period till it can be verified or the grace period is exhausted
|
||||
for ($i = 0; $i < $gracePeriod; ++$i) {
|
||||
$next = $i * $this->step + $counter;
|
||||
$prev = $counter - $i * $this->step;
|
||||
if ($this->compare($totp, $this->getTimeCounter($next))
|
||||
|| $this->compare($totp, $this->getTimeCounter($prev))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and return the otpauth URL for TOTP
|
||||
*/
|
||||
public function getTotpAuthUrl(string $issuer, string $account = '', array $additionalParameters = []): string
|
||||
{
|
||||
$parameters = [
|
||||
'secret' => $this->secret,
|
||||
'issuer' => htmlspecialchars($issuer),
|
||||
];
|
||||
|
||||
// Common OTP applications expect the following parameters:
|
||||
// - algo: sha1
|
||||
// - period: 30 (in seconds)
|
||||
// - digits 6
|
||||
// - epoch: 0
|
||||
// Only if we differ from these assumption, the exact values must be provided.
|
||||
if ($this->algo !== 'sha1') {
|
||||
$parameters['algorithm'] = $this->algo;
|
||||
}
|
||||
if ($this->step !== 30) {
|
||||
$parameters['period'] = $this->step;
|
||||
}
|
||||
if ($this->length !== 6) {
|
||||
$parameters['digits'] = $this->length;
|
||||
}
|
||||
if ($this->epoch !== 0) {
|
||||
$parameters['epoch'] = $this->epoch;
|
||||
}
|
||||
|
||||
// Generate the otpauth URL by providing information like issuer and account
|
||||
return sprintf(
|
||||
'otpauth://totp/%s?%s',
|
||||
rawurlencode($issuer . ($account !== '' ? ':' . $account : '')),
|
||||
http_build_query(array_merge($parameters, $additionalParameters), '', '&', PHP_QUERY_RFC3986)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare given time-based one-time password with a time-based one-time
|
||||
* password generated from the known $counter (the moving factor).
|
||||
*
|
||||
* @param string $totp The time-based one-time password to verify
|
||||
* @param int $counter The counter value, the moving factor
|
||||
*/
|
||||
protected function compare(string $totp, int $counter): bool
|
||||
{
|
||||
return hash_equals($this->generateTotp($counter), $totp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the counter value (moving factor) from the given timestamp
|
||||
*/
|
||||
protected function getTimeCounter(int $timestamp): int
|
||||
{
|
||||
return (int)floor(($timestamp - $this->epoch) / $this->step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the shared secret (K) by using a random and applying
|
||||
* additional authentication factors like username or email address.
|
||||
*/
|
||||
public static function generateEncodedSecret(array $additionalAuthFactors = []): string
|
||||
{
|
||||
$secret = '';
|
||||
$payload = implode($additionalAuthFactors);
|
||||
// Prevent secrets with a trailing pad character since this will eventually break the QR-code feature
|
||||
while ($secret === '' || str_contains($secret, '=')) {
|
||||
// RFC 4226 (https://tools.ietf.org/html/rfc4226#section-4) suggests 160 bit TOTP secret keys
|
||||
// HMAC-SHA1 based on static factors and a 160 bit HMAC-key lead again to 160 bits (20 bytes)
|
||||
// base64-encoding (factor 1.6) 20 bytes lead to 32 uppercase characters
|
||||
$secret = Base32::encode(hash_hmac('sha1', $payload, random_bytes(20), true));
|
||||
}
|
||||
return $secret;
|
||||
}
|
||||
|
||||
protected function getDecodedSecret(): string
|
||||
{
|
||||
return Base32::decode($this->secret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication\Mfa\Provider;
|
||||
|
||||
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderInterface;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
|
||||
/**
|
||||
* MFA provider for time-based one-time password authentication
|
||||
*
|
||||
* @internal should only be used by the TYPO3 Core
|
||||
*/
|
||||
final readonly class TotpProvider implements MfaProviderInterface
|
||||
{
|
||||
private const int MAX_ATTEMPTS = 3;
|
||||
|
||||
public function __construct(
|
||||
private Context $context,
|
||||
private HashService $hashService,
|
||||
private ViewFactoryInterface $viewFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check if a TOTP is given in the current request
|
||||
*/
|
||||
public function canProcess(ServerRequestInterface $request): bool
|
||||
{
|
||||
return $this->getTotp($request) !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if the provider is activated by checking the
|
||||
* active state and the secret from the provider properties.
|
||||
*/
|
||||
public function isActive(MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
return (bool)$propertyManager->getProperty('active')
|
||||
&& $propertyManager->getProperty('secret', '') !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if the provider is temporarily locked by checking
|
||||
* the current attempts state from the provider properties.
|
||||
*/
|
||||
public function isLocked(MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
$attempts = (int)$propertyManager->getProperty('attempts', 0);
|
||||
// Assume the provider is locked in case the maximum attempts are exceeded.
|
||||
// A provider however can only be locked if set up - an entry exists in database.
|
||||
return $propertyManager->hasProviderEntry() && $attempts >= self::MAX_ATTEMPTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given TOTP and update the provider properties in case the TOTP is valid.
|
||||
*/
|
||||
public function verify(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if (!$this->isActive($propertyManager) || $this->isLocked($propertyManager)) {
|
||||
// Can not verify an inactive or locked provider
|
||||
return false;
|
||||
}
|
||||
|
||||
$totp = $this->getTotp($request);
|
||||
$secret = $propertyManager->getProperty('secret', '');
|
||||
$verified = GeneralUtility::makeInstance(Totp::class, $secret)->verifyTotp($totp, 2);
|
||||
if (!$verified) {
|
||||
$attempts = $propertyManager->getProperty('attempts', 0);
|
||||
$propertyManager->updateProperties(['attempts' => ++$attempts]);
|
||||
return false;
|
||||
}
|
||||
$propertyManager->updateProperties([
|
||||
'attempts' => 0,
|
||||
'lastUsed' => $this->context->getPropertyFromAspect('date', 'timestamp'),
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the provider by checking the necessary parameters,
|
||||
* verifying the TOTP and storing the provider properties.
|
||||
*/
|
||||
public function activate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if ($this->isActive($propertyManager)) {
|
||||
// Can not activate an active provider
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->canProcess($request)) {
|
||||
// Return since the request can not be processed by this provider
|
||||
return false;
|
||||
}
|
||||
|
||||
$secret = (string)($request->getParsedBody()['secret'] ?? '');
|
||||
$checksum = (string)($request->getParsedBody()['checksum'] ?? '');
|
||||
if ($secret === '' || !hash_equals($this->hashService->hmac($secret, 'totp-setup', HashAlgo::SHA3_256), $checksum)) {
|
||||
// Return since the request does not contain the initially created secret
|
||||
return false;
|
||||
}
|
||||
|
||||
$totpInstance = GeneralUtility::makeInstance(Totp::class, $secret);
|
||||
if (!$totpInstance->verifyTotp($this->getTotp($request), 2)) {
|
||||
// Return since the given TOTP could not be verified
|
||||
return false;
|
||||
}
|
||||
|
||||
// If valid, prepare the provider properties to be stored
|
||||
$properties = ['secret' => $secret, 'active' => true];
|
||||
if (($name = (string)($request->getParsedBody()['name'] ?? '')) !== '') {
|
||||
$properties['name'] = $name;
|
||||
}
|
||||
|
||||
// Usually there should be no entry if the provider is not activated, but to prevent the
|
||||
// provider from being unable to activate again, we update the existing entry in such case.
|
||||
return $propertyManager->hasProviderEntry()
|
||||
? $propertyManager->updateProperties($properties)
|
||||
: $propertyManager->createProviderEntry($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the save action by updating the provider properties
|
||||
*/
|
||||
public function update(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if (!$this->isActive($propertyManager) || $this->isLocked($propertyManager)) {
|
||||
// Can not update an inactive or locked provider
|
||||
return false;
|
||||
}
|
||||
$name = (string)($request->getParsedBody()['name'] ?? '');
|
||||
if ($name !== '') {
|
||||
return $propertyManager->updateProperties(['name' => $name]);
|
||||
}
|
||||
// Provider properties successfully updated
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the unlock action by resetting the attempts provider property
|
||||
*/
|
||||
public function unlock(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if (!$this->isActive($propertyManager) || !$this->isLocked($propertyManager)) {
|
||||
// Can not unlock an inactive or not locked provider
|
||||
return false;
|
||||
}
|
||||
// Reset the attempts
|
||||
return $propertyManager->updateProperties(['attempts' => 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the deactivate action. For security reasons, the provider entry
|
||||
* is completely deleted and setting up this provider again, will therefore
|
||||
* create a brand-new entry.
|
||||
*/
|
||||
public function deactivate(ServerRequestInterface $request, MfaProviderPropertyManager $propertyManager): bool
|
||||
{
|
||||
if (!$this->isActive($propertyManager)) {
|
||||
// Can not deactivate an inactive provider
|
||||
return false;
|
||||
}
|
||||
// Delete the provider entry
|
||||
return $propertyManager->deleteProviderEntry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize view and forward to the appropriate implementation
|
||||
* based on the view type to be returned.
|
||||
*/
|
||||
public function handleRequest(
|
||||
ServerRequestInterface $request,
|
||||
MfaProviderPropertyManager $propertyManager,
|
||||
MfaViewType $type
|
||||
): ResponseInterface {
|
||||
$viewFactoryData = new ViewFactoryData(
|
||||
templateRootPaths: ['EXT:core/Resources/Private/Templates'],
|
||||
partialRootPaths: ['EXT:core/Resources/Private/Partials'],
|
||||
layoutRootPaths: ['EXT:core/Resources/Private/Layouts'],
|
||||
request: $request,
|
||||
);
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
switch ($type) {
|
||||
case MfaViewType::SETUP:
|
||||
// Generate a new shared secret, generate the otpauth URL and create a qr-code for improved usability.
|
||||
$userData = $propertyManager->getUser()->user ?? [];
|
||||
$secret = Totp::generateEncodedSecret([(string)($userData['uid'] ?? ''), (string)($userData['username'] ?? '')]);
|
||||
$totpInstance = GeneralUtility::makeInstance(Totp::class, $secret);
|
||||
$totpAuthUrl = $totpInstance->getTotpAuthUrl(
|
||||
(string)($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? 'TYPO3'),
|
||||
(string)($userData['email'] ?? '') ?: (string)($userData['username'] ?? '')
|
||||
);
|
||||
$view->assignMultiple([
|
||||
'secret' => $secret,
|
||||
'totpAuthUrl' => $totpAuthUrl,
|
||||
'qrCode' => $this->getSvgQrCode($totpAuthUrl),
|
||||
// Generate hmac of the secret to prevent it from being changed in the setup from
|
||||
'checksum' => $this->hashService->hmac($secret, 'totp-setup', HashAlgo::SHA3_256),
|
||||
'providerIdentifier' => $propertyManager->getIdentifier(),
|
||||
]);
|
||||
return new HtmlResponse($view->render('Authentication/MfaProvider/Totp/Setup'));
|
||||
case MfaViewType::EDIT:
|
||||
$view->assignMultiple([
|
||||
'name' => $propertyManager->getProperty('name'),
|
||||
'lastUsed' => $this->getDateTime($propertyManager->getProperty('lastUsed', 0)),
|
||||
'updated' => $this->getDateTime($propertyManager->getProperty('updated', 0)),
|
||||
'providerIdentifier' => $propertyManager->getIdentifier(),
|
||||
]);
|
||||
return new HtmlResponse($view->render('Authentication/MfaProvider/Totp/Edit'));
|
||||
default: // MfaViewType::AUTH
|
||||
$view->assignMultiple([
|
||||
'isLocked' => $this->isLocked($propertyManager),
|
||||
'providerIdentifier' => $propertyManager->getIdentifier(),
|
||||
]);
|
||||
return new HtmlResponse($view->render('Authentication/MfaProvider/Totp/Auth'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper method for fetching the TOTP from the request
|
||||
*/
|
||||
private function getTotp(ServerRequestInterface $request): string
|
||||
{
|
||||
return trim((string)($request->getQueryParams()['totp'] ?? $request->getParsedBody()['totp'] ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper method for generating a svg QR-code for TOTP applications
|
||||
*/
|
||||
private function getSvgQrCode(string $content): string
|
||||
{
|
||||
$qrCodeRenderer = new ImageRenderer(new RendererStyle(225, 4), new SvgImageBackEnd());
|
||||
return (new Writer($qrCodeRenderer))->writeString($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the timestamp as local time (date string) by applying the globally configured format
|
||||
*/
|
||||
private function getDateTime(int $timestamp): string
|
||||
{
|
||||
if ($timestamp === 0) {
|
||||
return '';
|
||||
}
|
||||
return date(
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
|
||||
$timestamp
|
||||
) ?: '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
interface MimicServiceInterface
|
||||
{
|
||||
/**
|
||||
* Mimics user authentication for known invalid authentication requests. This method can be used
|
||||
* to mitigate timing discrepancies for invalid authentication attempts, which can be used for
|
||||
* user enumeration.
|
||||
*
|
||||
* Authentication services can implement this method to simulate(!) corresponding processes that
|
||||
* would be processed during valid requests - e.g. perform password hashing (timing) or call
|
||||
* remote services (network latency).
|
||||
*
|
||||
* @return bool whether other services shall continue
|
||||
* @link https://cwe.mitre.org/data/definitions/208.html: CWE-208: Observable Timing Discrepancy
|
||||
*/
|
||||
public function mimicAuthUser(): bool;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use TYPO3\CMS\Core\Authentication\Exception\UserSettingsNotFoundException;
|
||||
|
||||
readonly class UserSettings implements ContainerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private array $settings,
|
||||
) {}
|
||||
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return array_key_exists($id, $this->settings);
|
||||
}
|
||||
|
||||
public function get(string $id): mixed
|
||||
{
|
||||
if (array_key_exists($id, $this->settings)) {
|
||||
return $this->settings[$id];
|
||||
}
|
||||
throw new UserSettingsNotFoundException(
|
||||
'User setting "' . $id . '" is not available.',
|
||||
1738500000
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
public function isEmailMeAtLoginEnabled(): bool
|
||||
{
|
||||
return (bool)($this->settings['emailMeAtLogin'] ?? false);
|
||||
}
|
||||
|
||||
public function isUploadFieldsInTopOfEBEnabled(): bool
|
||||
{
|
||||
return (bool)($this->settings['edit_docModuleUpload'] ?? true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal This class is not part of the TYPO3 Core API yet.
|
||||
*/
|
||||
readonly class UserSettingsFactory
|
||||
{
|
||||
private UserSettingsSchema $schema;
|
||||
|
||||
public function __construct(
|
||||
?UserSettingsSchema $schema = null,
|
||||
) {
|
||||
$this->schema = $schema ?? GeneralUtility::makeInstance(UserSettingsSchema::class);
|
||||
}
|
||||
|
||||
public function createFromUserRecord(array $userRecord, array $uc = []): UserSettings
|
||||
{
|
||||
$dbColumnSettings = $this->extractDbColumnSettings($userRecord);
|
||||
$jsonFieldSettings = $this->extractJsonFieldSettings($userRecord, $uc);
|
||||
|
||||
return new UserSettings(array_merge($dbColumnSettings, $jsonFieldSettings));
|
||||
}
|
||||
|
||||
public function createFromUc(array $uc): UserSettings
|
||||
{
|
||||
$settings = [];
|
||||
foreach ($this->schema->getJsonFieldSettingKeys() as $key) {
|
||||
if (array_key_exists($key, $uc)) {
|
||||
$settings[$key] = $uc[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return new UserSettings($settings);
|
||||
}
|
||||
|
||||
private function extractJsonFieldSettings(array $userRecord, array $uc): array
|
||||
{
|
||||
$settings = [];
|
||||
|
||||
// Primary source: user_settings JSON field
|
||||
if (!empty($userRecord['user_settings'])) {
|
||||
$decoded = is_string($userRecord['user_settings'])
|
||||
? json_decode($userRecord['user_settings'], true)
|
||||
: $userRecord['user_settings'];
|
||||
if (is_array($decoded)) {
|
||||
$settings = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: fill missing values from uc (migration period)
|
||||
foreach ($this->schema->getJsonFieldSettingKeys() as $key) {
|
||||
if (!array_key_exists($key, $settings) && array_key_exists($key, $uc)) {
|
||||
$settings[$key] = $uc[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
private function extractDbColumnSettings(array $userRecord): array
|
||||
{
|
||||
$settings = [];
|
||||
|
||||
foreach ($this->schema->getDbColumnSettingKeys() as $key) {
|
||||
if (array_key_exists($key, $userRecord)) {
|
||||
$settings[$key] = $userRecord[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Provides unified access to backend user settings configuration.
|
||||
*
|
||||
* This class consolidates access to user settings configuration stored in
|
||||
* TCA at $GLOBALS['TCA']['be_users']['columns']['user_settings'].
|
||||
*
|
||||
* @internal This class is not part of the TYPO3 Core API yet.
|
||||
*/
|
||||
readonly class UserSettingsSchema
|
||||
{
|
||||
/**
|
||||
* Get all column configurations in legacy format.
|
||||
* This merges TCA-based config with legacy global, preferring TCA.
|
||||
*
|
||||
* @return array<string, array>
|
||||
*/
|
||||
public function getColumns(): array
|
||||
{
|
||||
$columns = [];
|
||||
|
||||
$tcaColumns = $GLOBALS['TCA']['be_users']['columns']['user_settings']['columns'] ?? [];
|
||||
foreach ($tcaColumns as $fieldName => $tcaConfig) {
|
||||
$columns[$fieldName] = $this->resolveTcaColumn($fieldName, $tcaConfig);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for a specific field in legacy format.
|
||||
*/
|
||||
public function getColumn(string $fieldName): ?array
|
||||
{
|
||||
$tcaConfig = $GLOBALS['TCA']['be_users']['columns']['user_settings']['columns'][$fieldName] ?? null;
|
||||
if ($tcaConfig !== null) {
|
||||
return $this->resolveTcaColumn($fieldName, $tcaConfig);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a "fake TCA" for the be_users_settings pseudo-table.
|
||||
*/
|
||||
public function getTca(): array
|
||||
{
|
||||
$columns = $GLOBALS['TCA']['be_users']['columns']['user_settings']['columns'] ?? [];
|
||||
foreach ($columns as $fieldName => $columnConfig) {
|
||||
$partitionedFieldName = $this->getTcaFieldName($fieldName);
|
||||
$columns[$partitionedFieldName] = $this->resolveInheritFromParent($fieldName, $columnConfig);
|
||||
}
|
||||
|
||||
return [
|
||||
'be_users_settings' => [
|
||||
'ctrl' => [
|
||||
'title' => 'backend.user_profile:user_settings',
|
||||
],
|
||||
'columns' => $columns,
|
||||
'types' => [
|
||||
'0' => [
|
||||
'showitem' => $this->getTcaShowitem(),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a partitioned field name for use in TCA.
|
||||
* - e.g. `be_users__password`, reflecting `be_users` values
|
||||
* - e.g. `user_settings__titleLen`, reflecting JSON values
|
||||
*/
|
||||
public function getTcaFieldName(string $fieldName): string
|
||||
{
|
||||
$configuration = $this->getColumn($fieldName);
|
||||
$partition = ($configuration['table'] ?? null) === 'be_users' ? 'be_users' : 'user_settings';
|
||||
return $partition . '__' . $fieldName;
|
||||
}
|
||||
|
||||
private function resolveTcaFieldName(string $fieldName, bool $strict = true): string
|
||||
{
|
||||
$configuration = $this->getColumn($fieldName);
|
||||
if ($configuration !== null) {
|
||||
$partition = ($configuration['table'] ?? null) === 'be_users' ? 'be_users' : 'user_settings';
|
||||
return $partition . '__' . $fieldName;
|
||||
}
|
||||
if (!$strict) {
|
||||
return $fieldName;
|
||||
}
|
||||
throw new \LogicException(
|
||||
sprintf(
|
||||
'Column "%s" not found in UserSettingsSchema',
|
||||
$fieldName
|
||||
),
|
||||
1776439141
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the partitioned showitem string to be used as virtual TCA.
|
||||
*/
|
||||
public function getTcaShowitem(): string
|
||||
{
|
||||
$items = GeneralUtility::trimExplode(',', $this->getRawShowitem(), true);
|
||||
$items = array_map(
|
||||
fn(string $fieldName): string => $this->resolveTcaFieldName($fieldName, false),
|
||||
$items
|
||||
);
|
||||
return implode(',', $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw showitem string.
|
||||
*/
|
||||
public function getRawShowitem(): string
|
||||
{
|
||||
return trim($GLOBALS['TCA']['be_users']['columns']['user_settings']['showitem'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getJsonFieldSettingKeys(): array
|
||||
{
|
||||
$keys = [];
|
||||
foreach ($this->getColumns() as $key => $config) {
|
||||
// Fields with 'table' => 'be_users' are stored in be_users columns directly
|
||||
// Also skip non-storable types like 'button' and 'mfa'
|
||||
$type = $config['type'] ?? 'text';
|
||||
if (($config['table'] ?? '') !== 'be_users'
|
||||
&& !in_array($type, ['button', 'mfa'], true)
|
||||
) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getDbColumnSettingKeys(): array
|
||||
{
|
||||
$keys = [];
|
||||
foreach ($this->getColumns() as $key => $config) {
|
||||
$type = $config['type'] ?? 'text';
|
||||
if (($config['table'] ?? '') === 'be_users'
|
||||
&& !in_array($type, ['button', 'mfa', 'password'], true)
|
||||
) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
public function isJsonFieldSetting(string $key): bool
|
||||
{
|
||||
return in_array($key, $this->getJsonFieldSettingKeys(), true);
|
||||
}
|
||||
|
||||
public function isDbColumnSetting(string $key): bool
|
||||
{
|
||||
return in_array($key, $this->getDbColumnSettingKeys(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns field names that should trigger a JS persistent storage update
|
||||
* when their value changes in User Settings, so JS components can react
|
||||
* immediately without a page reload.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPersistentUpdateFieldNames(): array
|
||||
{
|
||||
$keys = [];
|
||||
foreach ($this->getColumns() as $key => $config) {
|
||||
if (!empty($config['persistentUpdate'])) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
public function getDefault(string $key): mixed
|
||||
{
|
||||
$config = $this->getColumn($key);
|
||||
return $config['default'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves inheritFromParent for a TCA column by merging with the
|
||||
* parent be_users TCA column configuration. Returns TCA format
|
||||
* (without legacy conversion).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function resolveInheritFromParent(string $fieldName, array $tcaConfig): array
|
||||
{
|
||||
if (!empty($tcaConfig['inheritFromParent'])) {
|
||||
$parentConfig = $GLOBALS['TCA']['be_users']['columns'][$fieldName] ?? [];
|
||||
$tcaConfig = array_replace_recursive($parentConfig, $tcaConfig);
|
||||
unset($tcaConfig['inheritFromParent']);
|
||||
}
|
||||
return $tcaConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a TCA column configuration, handling inheritFromParent,
|
||||
* and converts to legacy format.
|
||||
*/
|
||||
private function resolveTcaColumn(string $fieldName, array $tcaConfig): array
|
||||
{
|
||||
return $this->convertTcaToLegacyFormat($fieldName, $this->resolveInheritFromParent($fieldName, $tcaConfig));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a TCA column configuration to legacy format.
|
||||
*/
|
||||
private function convertTcaToLegacyFormat(string $fieldName, array $tcaConfig): array
|
||||
{
|
||||
$legacyConfig = [
|
||||
'label' => $tcaConfig['label'] ?? '',
|
||||
];
|
||||
|
||||
$config = $tcaConfig['config'] ?? [];
|
||||
$tcaType = $config['type'] ?? 'input';
|
||||
$renderType = $config['renderType'] ?? '';
|
||||
|
||||
// Determine if this field is stored in be_users table
|
||||
// Fields with inheritFromParent that exist in be_users columns are table fields
|
||||
if (isset($GLOBALS['TCA']['be_users']['columns'][$fieldName])) {
|
||||
$legacyConfig['table'] = 'be_users';
|
||||
}
|
||||
|
||||
// Convert TCA type to legacy type
|
||||
switch ($tcaType) {
|
||||
case 'input':
|
||||
$legacyConfig['type'] = 'text';
|
||||
if (isset($config['max'])) {
|
||||
$legacyConfig['max'] = $config['max'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'email':
|
||||
$legacyConfig['type'] = 'email';
|
||||
if (isset($config['max'])) {
|
||||
$legacyConfig['max'] = $config['max'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
$legacyConfig['type'] = 'number';
|
||||
break;
|
||||
|
||||
case 'password':
|
||||
$legacyConfig['type'] = 'password';
|
||||
break;
|
||||
|
||||
case 'check':
|
||||
$legacyConfig['type'] = 'check';
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
$legacyConfig['type'] = 'select';
|
||||
if (isset($config['items'])) {
|
||||
$legacyConfig['items'] = $this->convertSelectItemsToLegacy($config['items']);
|
||||
}
|
||||
if (isset($config['itemsProcFunc'])) {
|
||||
$legacyConfig['itemsProcFunc'] = $config['itemsProcFunc'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'language':
|
||||
$legacyConfig['type'] = 'language';
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$legacyConfig['type'] = 'avatar';
|
||||
break;
|
||||
|
||||
case 'button':
|
||||
$legacyConfig['type'] = 'button';
|
||||
if (isset($config['buttonLabel'])) {
|
||||
$legacyConfig['buttonlabel'] = $config['buttonLabel'];
|
||||
}
|
||||
if (isset($config['confirm'])) {
|
||||
$legacyConfig['confirm'] = $config['confirm'];
|
||||
}
|
||||
if (isset($config['confirmData'])) {
|
||||
$legacyConfig['confirmData'] = $config['confirmData'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mfa':
|
||||
$legacyConfig['type'] = 'mfa';
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$legacyConfig['type'] = 'user';
|
||||
if (isset($config['renderType'])) {
|
||||
$legacyConfig['userFunc'] = $config['renderType'];
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$legacyConfig['type'] = 'text';
|
||||
}
|
||||
|
||||
// Copy additional properties
|
||||
if (isset($config['default'])) {
|
||||
$legacyConfig['default'] = $config['default'];
|
||||
}
|
||||
if (isset($tcaConfig['access'])) {
|
||||
$legacyConfig['access'] = $tcaConfig['access'];
|
||||
}
|
||||
if (!empty($tcaConfig['persistentUpdate'])) {
|
||||
$legacyConfig['persistentUpdate'] = true;
|
||||
}
|
||||
|
||||
return $legacyConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts TCA select items format to legacy format.
|
||||
* Handles both TCA format ([['label' => '...', 'value' => '...'], ...])
|
||||
* and legacy format (['value' => 'label', ...]).
|
||||
*/
|
||||
private function convertSelectItemsToLegacy(array $items): array
|
||||
{
|
||||
$legacyItems = [];
|
||||
foreach ($items as $key => $item) {
|
||||
if (is_array($item) && isset($item['value']) && isset($item['label'])) {
|
||||
// TCA format: [['label' => '...', 'value' => '...'], ...]
|
||||
$legacyItems[$item['value']] = $item['label'];
|
||||
} elseif (is_string($item)) {
|
||||
// Legacy format: ['value' => 'label', ...]
|
||||
$legacyItems[$key] = $item;
|
||||
}
|
||||
}
|
||||
return $legacyItems;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Log\LogManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* An abstract caching backend
|
||||
*/
|
||||
abstract class AbstractBackend implements BackendInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
protected string $cacheIdentifier;
|
||||
|
||||
/**
|
||||
* Default lifetime of a cache entry in seconds
|
||||
*/
|
||||
protected int $defaultLifetime = 3600;
|
||||
|
||||
/**
|
||||
* @param array $options Configuration options - depends on the actual backend
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
foreach ($options as $optionKey => $optionValue) {
|
||||
$methodName = 'set' . ucfirst($optionKey);
|
||||
if (method_exists($this, $methodName)) {
|
||||
$this->{$methodName}($optionValue);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid cache backend option "' . $optionKey . '" for backend of type "' . static::class . '"', 1231267498);
|
||||
}
|
||||
}
|
||||
// Init logger. This is forces, even if $options['logger'] has been set, which shouldn't.
|
||||
$this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
|
||||
}
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
$this->cacheIdentifier = $cache->getIdentifier();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default lifetime for this cache backend
|
||||
*
|
||||
* @param int $defaultLifetime Default lifetime of this cache backend in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @internal Misused for testing purposes.
|
||||
* @todo: Fix tests and protect or remove
|
||||
*/
|
||||
public function setDefaultLifetime(int $defaultLifetime): void
|
||||
{
|
||||
if ($defaultLifetime < 0) {
|
||||
throw new \InvalidArgumentException('The default lifetime must be given as a positive integer.', 1233072774);
|
||||
}
|
||||
$this->defaultLifetime = $defaultLifetime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using APCu.
|
||||
*
|
||||
* The APCu backend is not very good with tagging and scales O(2n) with the
|
||||
* number of tags. Do not use this backend if the data to be cached has many tags!
|
||||
*
|
||||
* This backend uses the following types of keys:
|
||||
* - tag_xxx
|
||||
* xxx is tag name, value is array of associated identifiers identifier. This
|
||||
* is "forward" tag index. It is mainly used for obtaining content by tag
|
||||
* (get identifier by tag -> get content by identifier)
|
||||
* - ident_xxx
|
||||
* xxx is identifier, value is array of associated tags. This is "reverse" tag
|
||||
* index. It provides quick access for all tags associated with this identifier
|
||||
* and used when removing the identifier
|
||||
*
|
||||
* Each key is prepended with a prefix. The prefix makes sure keys from the different
|
||||
* installations do not conflict. By default, prefix consists from two parts
|
||||
* separated by underscore character and ends in yet another underscore character:
|
||||
* - "TYPO3"
|
||||
* - Hash of path to TYPO3 and user running TYPO3
|
||||
*/
|
||||
final class ApcuBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
/**
|
||||
* A prefix to separate stored data from other data possible stored in the APC.
|
||||
*/
|
||||
private string $identifierPrefix = '';
|
||||
|
||||
/**
|
||||
* Constructs this backend
|
||||
*
|
||||
* @param array $options Configuration options - unused here
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!extension_loaded('apcu')) {
|
||||
throw new Exception('The PHP extension "apcu" must be installed and loaded in order to use the APCu backend.', 1232985914);
|
||||
}
|
||||
if (PHP_SAPI === 'cli' && ini_get('apc.enable_cli') == 0) {
|
||||
throw new Exception('The APCu backend cannot be used because apcu is disabled on CLI.', 1232985915);
|
||||
}
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$this->identifierPrefix = 'TYPO3_' . hash('xxh3', Environment::getProjectPath() . $cache->getIdentifier()) . '_';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data The data to be stored. mixed is allowed due to TransientBackendInterface
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
$lifetime ??= $this->defaultLifetime;
|
||||
$success = apcu_store($this->identifierPrefix . $entryIdentifier, $data, $lifetime);
|
||||
if ($success === true) {
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
$this->addIdentifierToTags($entryIdentifier, $tags);
|
||||
} else {
|
||||
$this->logger->alert('Error using APCu: Could not save data in the cache.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from the cache.
|
||||
*
|
||||
* @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
|
||||
*/
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
$success = false;
|
||||
$value = apcu_fetch($this->identifierPrefix . $entryIdentifier, $success);
|
||||
return $success ? $value : $success;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
$success = false;
|
||||
apcu_fetch($this->identifierPrefix . $entryIdentifier, $success);
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry but if - for what reason ever -
|
||||
* old entries for the identifier still exist, they are removed as well.
|
||||
*
|
||||
* @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
return apcu_delete($this->identifierPrefix . $entryIdentifier);
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
$success = false;
|
||||
$identifiers = apcu_fetch($this->identifierPrefix . 'tag_' . $tag, $success);
|
||||
if ($success === false) {
|
||||
return [];
|
||||
}
|
||||
return (array)$identifiers;
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
apcu_delete(new \APCUIterator('/^' . preg_quote($this->identifierPrefix, '/') . '/'));
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
// Noop, APCu has internal GC
|
||||
}
|
||||
|
||||
private function addIdentifierToTags(string $entryIdentifier, array $tags): void
|
||||
{
|
||||
// Get identifier-to-tag index to look for updates
|
||||
$existingTags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
$existingTagsUpdated = false;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
// Update tag-to-identifier index
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
if (!in_array($entryIdentifier, $identifiers, true)) {
|
||||
$identifiers[] = $entryIdentifier;
|
||||
apcu_store($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
}
|
||||
// Test if identifier-to-tag index needs update
|
||||
if (!in_array($tag, $existingTags, true)) {
|
||||
$existingTags[] = $tag;
|
||||
$existingTagsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update identifier-to-tag index if needed
|
||||
if ($existingTagsUpdated) {
|
||||
apcu_store($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
|
||||
}
|
||||
}
|
||||
|
||||
private function removeIdentifierFromAllTags(string $entryIdentifier): void
|
||||
{
|
||||
// Get tags for this identifier
|
||||
$tags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
// De-associate tags with this identifier
|
||||
foreach ($tags as $tag) {
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
// Formally array_search() below should never return FALSE due to
|
||||
// the behavior of findTagsByIdentifier(). But if reverse index is
|
||||
// corrupted, we still can get 'FALSE' from array_search(). This is
|
||||
// not a problem because we are removing this identifier from
|
||||
// anywhere.
|
||||
if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
|
||||
unset($identifiers[$key]);
|
||||
if (!empty($identifiers)) {
|
||||
apcu_store($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
} else {
|
||||
apcu_delete($this->identifierPrefix . 'tag_' . $tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear reverse tag index for this identifier
|
||||
apcu_delete($this->identifierPrefix . 'ident_' . $entryIdentifier);
|
||||
}
|
||||
|
||||
private function findTagsByIdentifier(string $identifier): array
|
||||
{
|
||||
$success = false;
|
||||
$tags = apcu_fetch($this->identifierPrefix . 'ident_' . $identifier, $success);
|
||||
return $success ? (array)$tags : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
|
||||
/**
|
||||
* A contract for a Cache Backend
|
||||
*/
|
||||
interface BackendInterface
|
||||
{
|
||||
/**
|
||||
* Sets a reference to the cache frontend which uses this backend
|
||||
*
|
||||
* @param FrontendInterface $cache The frontend for this backend
|
||||
*/
|
||||
public function setCache(FrontendInterface $cache): void;
|
||||
|
||||
/**
|
||||
* Saves data in the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier for this specific cache entry
|
||||
* @param string $data The data to be stored
|
||||
* @param array $tags Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored.
|
||||
* @param int|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
*/
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void;
|
||||
|
||||
/**
|
||||
* Loads data from the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
|
||||
*/
|
||||
public function get(string $entryIdentifier): mixed;
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier specifying the cache entry
|
||||
* @return bool TRUE if such an entry exists, FALSE if not
|
||||
*/
|
||||
public function has(string $entryIdentifier): bool;
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry but if - for what reason ever -
|
||||
* old entries for the identifier still exist, they are removed as well.
|
||||
*
|
||||
* @param string $entryIdentifier Specifies the cache entry to remove
|
||||
* @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool;
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush(): void;
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage(): void;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries in files
|
||||
*/
|
||||
class FileBackend extends SimpleFileBackend implements TaggableBackendInterface
|
||||
{
|
||||
protected const EXPIRYTIME_LENGTH = 14;
|
||||
protected const DATASIZE_DIGITS = 10;
|
||||
|
||||
/**
|
||||
* @throws Exception if the directory does not exist or is not writable or exceeds the maximum allowed path length, or if no cache frontend has been set.
|
||||
*/
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073032);
|
||||
}
|
||||
if ($entryIdentifier === '') {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1298114280);
|
||||
}
|
||||
$this->remove($entryIdentifier);
|
||||
$temporaryCacheEntryPathAndFilename = $this->cacheDirectory . StringUtility::getUniqueId() . '.temp';
|
||||
$lifetime ??= $this->defaultLifetime;
|
||||
$expiryTime = $lifetime === 0 ? 0 : (int)($GLOBALS['EXEC_TIME'] + $lifetime);
|
||||
$metaData = str_pad((string)$expiryTime, self::EXPIRYTIME_LENGTH) . implode(' ', $tags) . str_pad((string)strlen($data), self::DATASIZE_DIGITS);
|
||||
$result = GeneralUtility::writeFile($temporaryCacheEntryPathAndFilename, $data . $metaData, true);
|
||||
if ($result === false) {
|
||||
throw new Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1204026251);
|
||||
}
|
||||
$i = 0;
|
||||
$cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
while (($result = rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename)) === false && $i < 5) {
|
||||
$i++;
|
||||
}
|
||||
if ($result === false) {
|
||||
throw new Exception('The cache file "' . $cacheEntryPathAndFilename . '" could not be written.', 1222361632);
|
||||
}
|
||||
if ($this->cacheEntryFileExtension === '.php') {
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($cacheEntryPathAndFilename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string The cache entry's content as a string or FALSE if the cache entry could not be loaded
|
||||
*/
|
||||
public function get(string $entryIdentifier): false|string
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073033);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if ($this->isCacheFileExpired($pathAndFilename)) {
|
||||
return false;
|
||||
}
|
||||
$dataSize = (int)file_get_contents(
|
||||
$pathAndFilename,
|
||||
false,
|
||||
null,
|
||||
filesize($pathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
return file_get_contents($pathAndFilename, false, null, 0, $dataSize);
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073034);
|
||||
}
|
||||
return !$this->isCacheFileExpired($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
$entryIdentifiers = [];
|
||||
$now = $GLOBALS['EXEC_TIME'];
|
||||
$cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
|
||||
for ($directoryIterator = GeneralUtility::makeInstance(\DirectoryIterator::class, $this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
|
||||
if (!$directoryIterator->isFile()) {
|
||||
continue;
|
||||
}
|
||||
$cacheEntryPathAndFilename = $directoryIterator->getPathname();
|
||||
$index = (int)file_get_contents(
|
||||
$cacheEntryPathAndFilename,
|
||||
false,
|
||||
null,
|
||||
filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
$metaData = (string)file_get_contents($cacheEntryPathAndFilename, false, null, $index);
|
||||
$expiryTime = (int)substr($metaData, 0, self::EXPIRYTIME_LENGTH);
|
||||
if ($expiryTime !== 0 && $expiryTime < $now) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($tag, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
|
||||
if ($cacheEntryFileExtensionLength > 0) {
|
||||
$entryIdentifiers[] = substr((string)$directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength);
|
||||
} else {
|
||||
$entryIdentifiers[] = $directoryIterator->getFilename();
|
||||
}
|
||||
}
|
||||
}
|
||||
return $entryIdentifiers;
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $entryIdentifier) {
|
||||
$this->remove($entryIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given cache entry files are still valid or if their
|
||||
* lifetime has exceeded.
|
||||
*/
|
||||
protected function isCacheFileExpired(string $cacheEntryPathAndFilename): bool
|
||||
{
|
||||
if (file_exists($cacheEntryPathAndFilename) === false) {
|
||||
return true;
|
||||
}
|
||||
$index = (int)file_get_contents(
|
||||
$cacheEntryPathAndFilename,
|
||||
false,
|
||||
null,
|
||||
filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
$expiryTime = (int)file_get_contents($cacheEntryPathAndFilename, false, null, $index, self::EXPIRYTIME_LENGTH);
|
||||
return $expiryTime !== 0 && $expiryTime < $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
|
||||
if (!$directoryIterator->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if ($this->isCacheFileExpired($directoryIterator->getPathname())) {
|
||||
$cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
|
||||
if ($cacheEntryFileExtensionLength > 0) {
|
||||
$this->remove(substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength));
|
||||
} else {
|
||||
$this->remove($directoryIterator->getFilename());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function requireOnce(string $entryIdentifier): mixed
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073036);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
return $this->isCacheFileExpired($pathAndFilename) ? false : require_once $pathAndFilename;
|
||||
}
|
||||
|
||||
public function require(string $entryIdentifier): mixed
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1532528246);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
return $this->isCacheFileExpired($pathAndFilename) ? false : require $pathAndFilename;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using Memcached.
|
||||
*
|
||||
* This backend uses the following types of Memcache keys:
|
||||
* - tag_xxx
|
||||
* xxx is tag name, value is array of associated identifiers identifier. This
|
||||
* is "forward" tag index. It is mainly used for obtaining content by tag
|
||||
* (get identifier by tag -> get content by identifier)
|
||||
* - ident_xxx
|
||||
* xxx is identifier, value is array of associated tags. This is "reverse" tag
|
||||
* index. It provides quick access for all tags associated with this identifier
|
||||
* and used when removing the identifier
|
||||
*
|
||||
* Each key is prepended with a prefix. By default prefix consists from two parts
|
||||
* separated by underscore character and ends in yet another underscore character:
|
||||
* - "TYPO3"
|
||||
* - Current site path obtained from Environment::getProjectPath()
|
||||
* This prefix makes sure that keys from the different installations do not
|
||||
* conflict.
|
||||
*
|
||||
* Note: When using the Memcached backend to store values of more than ~1 MB,
|
||||
* the data will be split into chunks to make them fit into the memcached limits.
|
||||
*/
|
||||
class MemcachedBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
/**
|
||||
* Max bucket size, (1024*1024)-42 bytes
|
||||
*/
|
||||
protected const MAX_BUCKET_SIZE = 1048534;
|
||||
|
||||
/**
|
||||
* Instance of the PHP Memcache class
|
||||
*/
|
||||
protected \Memcache|\Memcached $memcache;
|
||||
|
||||
/**
|
||||
* Used PECL module for memcached
|
||||
*/
|
||||
protected string $usedPeclModule = '';
|
||||
|
||||
/**
|
||||
* Array of Memcache server configurations
|
||||
*/
|
||||
protected array $servers = [];
|
||||
|
||||
/**
|
||||
* Indicates whether the memcache uses compression or not (requires zlib),
|
||||
* either 0 or \Memcached::OPT_COMPRESSION / MEMCACHE_COMPRESSED
|
||||
*/
|
||||
protected int $flags = 0;
|
||||
|
||||
/**
|
||||
* A prefix to separate stored data from other data possibly stored in the memcache
|
||||
*/
|
||||
protected string $identifierPrefix;
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!extension_loaded('memcache') && !extension_loaded('memcached')) {
|
||||
throw new Exception('The PHP extension "memcache" or "memcached" must be installed and loaded in order to use the Memcached backend.', 1213987706);
|
||||
}
|
||||
if ($this->usedPeclModule === '') {
|
||||
if (extension_loaded('memcache')) {
|
||||
$this->usedPeclModule = 'memcache';
|
||||
} elseif (extension_loaded('memcached')) {
|
||||
$this->usedPeclModule = 'memcached';
|
||||
}
|
||||
}
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for servers to be used. Expects an array, the values are expected
|
||||
* to be formatted like "<host>[:<port>]" or "unix://<path>"
|
||||
*
|
||||
* @param array $servers An array of servers to add.
|
||||
*/
|
||||
protected function setServers(array $servers): void
|
||||
{
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for compression flags bit
|
||||
*/
|
||||
protected function setCompression(bool $useCompression): void
|
||||
{
|
||||
$compressionFlag = $this->usedPeclModule === 'memcache' ? MEMCACHE_COMPRESSED : \Memcached::OPT_COMPRESSION;
|
||||
if ($useCompression) {
|
||||
$this->flags ^= $compressionFlag;
|
||||
} else {
|
||||
$this->flags &= ~$compressionFlag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for compression flag
|
||||
*/
|
||||
protected function getCompression(): bool
|
||||
{
|
||||
return $this->flags !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the identifier prefix
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function initializeObject(): void
|
||||
{
|
||||
if (empty($this->servers)) {
|
||||
throw new Exception('No servers were given to Memcache', 1213115903);
|
||||
}
|
||||
$memcachedPlugin = '\\' . ucfirst($this->usedPeclModule);
|
||||
$this->memcache = new $memcachedPlugin();
|
||||
$defaultPort = $this->usedPeclModule === 'memcache' ? ini_get('memcache.default_port') : 11211;
|
||||
foreach ($this->servers as $server) {
|
||||
if (str_starts_with((string)$server, 'unix://')) {
|
||||
$host = $server;
|
||||
$port = 0;
|
||||
} else {
|
||||
if (str_starts_with((string)$server, 'tcp://')) {
|
||||
$server = substr((string)$server, 6);
|
||||
}
|
||||
if (str_contains((string)$server, ':')) {
|
||||
[$host, $port] = explode(':', (string)$server, 2);
|
||||
} else {
|
||||
$host = $server;
|
||||
$port = $defaultPort;
|
||||
}
|
||||
}
|
||||
$this->memcache->addserver($host, (int)$port);
|
||||
}
|
||||
if ($this->usedPeclModule === 'memcached') {
|
||||
$this->memcache->setOption(\Memcached::OPT_COMPRESSION, $this->getCompression());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred PECL module
|
||||
*/
|
||||
public function setPeclModule(string $peclModule): void
|
||||
{
|
||||
if ($peclModule !== 'memcache' && $peclModule !== 'memcached') {
|
||||
throw new Exception('PECL module must be either "memcache" or "memcached".', 1442239768);
|
||||
}
|
||||
|
||||
$this->usedPeclModule = $peclModule;
|
||||
}
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$identifierHash = substr(md5(Environment::getProjectPath() . $this->cacheIdentifier), 0, 12);
|
||||
$this->identifierPrefix = 'TYPO3_' . $identifierHash . '_';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data The data to be stored. mixed is allowed due to TransientBackendInterface
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
|
||||
throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
|
||||
}
|
||||
$tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
|
||||
$expiration = $lifetime ?? $this->defaultLifetime;
|
||||
|
||||
// Memcached considers values over 2592000 sec (30 days) as UNIX timestamp
|
||||
// thus $expiration should be converted from lifetime to UNIX timestamp
|
||||
if ($expiration > 2592000) {
|
||||
$expiration += $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
try {
|
||||
if (is_string($data) && strlen($data) > self::MAX_BUCKET_SIZE) {
|
||||
$data = str_split($data, 1024 * 1000);
|
||||
$success = true;
|
||||
$chunkNumber = 1;
|
||||
foreach ($data as $chunk) {
|
||||
$success = $success && $this->setInternal($entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $expiration);
|
||||
$chunkNumber++;
|
||||
}
|
||||
$success = $success && $this->setInternal($entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $expiration);
|
||||
} else {
|
||||
$success = $this->setInternal($entryIdentifier, $data, $expiration);
|
||||
}
|
||||
if ($success) {
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
$this->addIdentifierToTags($entryIdentifier, $tags);
|
||||
} else {
|
||||
throw new Exception('Could not set data to memcache server.', 1275830266);
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->alert('Memcache: could not set value.', ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
$value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
|
||||
if (is_string($value) && str_starts_with($value, 'TYPO3*chunked:')) {
|
||||
[, $chunkCount] = explode(':', $value);
|
||||
$value = '';
|
||||
for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
|
||||
$value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
if ($this->usedPeclModule === 'memcache') {
|
||||
return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
|
||||
}
|
||||
// pecl-memcached supports storing literal FALSE
|
||||
$this->memcache->get($this->identifierPrefix . $entryIdentifier);
|
||||
return $this->memcache->getResultCode() !== \Memcached::RES_NOTFOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry but if - for what reason ever -
|
||||
* old entries for the identifier still exist, they are removed as well.
|
||||
*
|
||||
* @param string $entryIdentifier Specifies the cache entry to remove
|
||||
* @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
$identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
|
||||
if ($identifiers !== false) {
|
||||
return (array)$identifiers;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing, as memcached does GC itself
|
||||
*/
|
||||
public function collectGarbage(): void {}
|
||||
|
||||
/**
|
||||
* Stores the actual data inside memcache/memcached
|
||||
*/
|
||||
protected function setInternal(string $entryIdentifier, mixed $data, int $expiration): bool
|
||||
{
|
||||
if ($this->usedPeclModule === 'memcache') {
|
||||
return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
|
||||
}
|
||||
return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the identifier with the given tags
|
||||
*/
|
||||
protected function addIdentifierToTags(string $entryIdentifier, array $tags): void
|
||||
{
|
||||
// Get identifier-to-tag index to look for updates
|
||||
$existingTags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
$existingTagsUpdated = false;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
// Update tag-to-identifier index
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
if (!in_array($entryIdentifier, $identifiers, true)) {
|
||||
$identifiers[] = $entryIdentifier;
|
||||
$this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
}
|
||||
// Test if identifier-to-tag index needs update
|
||||
if (!in_array($tag, $existingTags, true)) {
|
||||
$existingTags[] = $tag;
|
||||
$existingTagsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update identifier-to-tag index if needed
|
||||
if ($existingTagsUpdated) {
|
||||
$this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes association of the identifier with the given tags
|
||||
*/
|
||||
protected function removeIdentifierFromAllTags(string $entryIdentifier): void
|
||||
{
|
||||
// Get tags for this identifier
|
||||
$tags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
// De-associate tags with this identifier
|
||||
foreach ($tags as $tag) {
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
// Formally array_search() below should never return FALSE due to
|
||||
// the behavior of findTagsByIdentifier(). But if reverse index is
|
||||
// corrupted, we still can get 'FALSE' from array_search(). This is
|
||||
// not a problem because we are removing this identifier from
|
||||
// anywhere.
|
||||
if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
|
||||
unset($identifiers[$key]);
|
||||
if (!empty($identifiers)) {
|
||||
$this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
} else {
|
||||
$this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear reverse tag index for this identifier
|
||||
$this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all tags for the given identifier. This function uses reverse tag
|
||||
* index to search for tags.
|
||||
*
|
||||
* @param string $identifier Identifier to find tags by
|
||||
*/
|
||||
protected function findTagsByIdentifier(string $identifier): array
|
||||
{
|
||||
$tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
|
||||
return $tags === false ? [] : (array)$tags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
|
||||
/**
|
||||
* A caching backend which forgets everything immediately
|
||||
*/
|
||||
class NullBackend implements PhpCapableBackendInterface, TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void {}
|
||||
|
||||
public function get(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag($tag): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function flush(): void {}
|
||||
|
||||
public function flushByTag(string $tag): void {}
|
||||
|
||||
public function flushByTags(array $tags): void {}
|
||||
|
||||
public function setCache(FrontendInterface $cache): void {}
|
||||
|
||||
public function collectGarbage(): void {}
|
||||
|
||||
public function requireOnce(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function require(string $entryIdentifier): false
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Cache\Backend;
|
||||
|
||||
/**
|
||||
* A contract for a cache backend which is capable of storing, retrieving and
|
||||
* including PHP source code.
|
||||
*/
|
||||
interface PhpCapableBackendInterface extends BackendInterface
|
||||
{
|
||||
/**
|
||||
* Loads PHP code from the cache and require_once() it right away.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed Potential return value from the include operation
|
||||
*/
|
||||
public function requireOnce(string $entryIdentifier): mixed;
|
||||
|
||||
/**
|
||||
* Loads PHP code from the cache and require() it right away.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed Potential return value from the include operation
|
||||
*/
|
||||
public function require(string $entryIdentifier): mixed;
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using Redis with phpredis
|
||||
* PHP module. Redis is a noSQL database with very good scaling characteristics
|
||||
* in proportion to the amount of entries and data size.
|
||||
*
|
||||
* @see https://redis.io/
|
||||
* @see https://github.com/phpredis/phpredis
|
||||
*/
|
||||
class RedisBackend extends AbstractBackend implements TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* Faked unlimited lifetime = 31536000 (1 Year).
|
||||
* In redis an entry does not have a lifetime by default (it's not "volatile").
|
||||
* Entries can be made volatile either with EXPIRE after it has been SET,
|
||||
* or with SETEX, which is a combined SET and EXPIRE command.
|
||||
* But an entry can not be made "unvolatile" again. To set a volatile entry to
|
||||
* not volatile again, it must be DELeted and SET without a following EXPIRE.
|
||||
* To save these additional calls on every set(),
|
||||
* we just make every entry volatile and treat a high number as "unlimited"
|
||||
*
|
||||
* @see https://redis.io/commands/expire
|
||||
*/
|
||||
protected const FAKED_UNLIMITED_LIFETIME = 31536000;
|
||||
|
||||
/**
|
||||
* Key prefix for identifier->data entries
|
||||
*/
|
||||
protected const IDENTIFIER_DATA_PREFIX = 'identData:';
|
||||
|
||||
/**
|
||||
* Key prefix for identifier->tags sets
|
||||
*/
|
||||
protected const IDENTIFIER_TAGS_PREFIX = 'identTags:';
|
||||
|
||||
/**
|
||||
* Key prefix for tag->identifiers sets
|
||||
*/
|
||||
protected const TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
|
||||
|
||||
protected \Redis $redis;
|
||||
|
||||
/**
|
||||
* Indicates whether the server is connected
|
||||
*/
|
||||
protected bool $connected = false;
|
||||
|
||||
/**
|
||||
* Persistent connection
|
||||
*/
|
||||
protected bool $persistentConnection = false;
|
||||
|
||||
/**
|
||||
* Hostname / IP of the Redis server, defaults to 127.0.0.1.
|
||||
*/
|
||||
protected string $hostname = '127.0.0.1';
|
||||
|
||||
/**
|
||||
* Port of the Redis server, defaults to 6379
|
||||
*/
|
||||
protected int $port = 6379;
|
||||
|
||||
/**
|
||||
* Number of selected database, defaults to 0
|
||||
*/
|
||||
protected int $database = 0;
|
||||
|
||||
/**
|
||||
* Username for authentication
|
||||
*/
|
||||
protected ?string $username = null;
|
||||
|
||||
/**
|
||||
* Password for authentication
|
||||
*/
|
||||
protected ?string $password = null;
|
||||
|
||||
/**
|
||||
* Indicates whether data is compressed or not (requires php zlib)
|
||||
*/
|
||||
protected bool $compression = false;
|
||||
|
||||
/**
|
||||
* -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
|
||||
*/
|
||||
protected int $compressionLevel = -1;
|
||||
|
||||
/**
|
||||
* limit in seconds (default is 0 meaning unlimited)
|
||||
*/
|
||||
protected int $connectionTimeout = 0;
|
||||
|
||||
/**
|
||||
* Used as prefix for all Redis keys/identifiers
|
||||
*/
|
||||
protected string $keyPrefix = '';
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!extension_loaded('redis')) {
|
||||
throw new Exception('The PHP extension "redis" must be installed and loaded in order to use the redis backend.', 1279462933);
|
||||
}
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
public function initializeObject(): void
|
||||
{
|
||||
$this->redis = new \Redis();
|
||||
try {
|
||||
if ($this->persistentConnection) {
|
||||
$this->connected = $this->redis->pconnect($this->hostname, $this->port, $this->connectionTimeout, (string)$this->database);
|
||||
} else {
|
||||
$this->connected = $this->redis->connect($this->hostname, $this->port, $this->connectionTimeout);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->alert('Could not connect to redis server.', ['exception' => $e]);
|
||||
}
|
||||
if ($this->connected) {
|
||||
$authentication = $this->getAuthentication();
|
||||
if ($authentication !== null) {
|
||||
$success = $this->redis->auth($this->getAuthentication());
|
||||
if (!$success) {
|
||||
throw new Exception('Authentication to Redis failed”.', 1279765134);
|
||||
}
|
||||
}
|
||||
if ($this->database >= 0) {
|
||||
$success = $this->redis->select($this->database);
|
||||
if (!$success) {
|
||||
throw new Exception('The given database "' . $this->database . '" could not be selected.', 1279765144);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function setPersistentConnection(bool $persistentConnection): void
|
||||
{
|
||||
$this->persistentConnection = $persistentConnection;
|
||||
}
|
||||
|
||||
protected function setHostname(string $hostname): void
|
||||
{
|
||||
$this->hostname = $hostname;
|
||||
}
|
||||
|
||||
protected function setPort(int $port): void
|
||||
{
|
||||
$this->port = $port;
|
||||
}
|
||||
|
||||
protected function setDatabase(int $database): void
|
||||
{
|
||||
if ($database < 0) {
|
||||
throw new \InvalidArgumentException('The specified database "' . $database . '" must be greater or equal than zero.', 1279763534);
|
||||
}
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
protected function setUsername(string $username): void
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for authentication password
|
||||
*/
|
||||
protected function setPassword(#[\SensitiveParameter] string $password): void
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
protected function setCompression(bool $compression): void
|
||||
{
|
||||
$this->compression = $compression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data compression level.
|
||||
* If compression is enabled and this is not set,
|
||||
* gzcompress default level will be used.
|
||||
*
|
||||
* @param int $compressionLevel -1 to 9: Compression level
|
||||
*/
|
||||
protected function setCompressionLevel(int $compressionLevel): void
|
||||
{
|
||||
if ($compressionLevel >= -1 && $compressionLevel <= 9) {
|
||||
$this->compressionLevel = $compressionLevel;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('The specified compression level must be an integer between -1 and 9.', 1289679155);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set connection timeout.
|
||||
* This value in seconds is used as a maximum number
|
||||
* of seconds to wait if a connection can be established.
|
||||
*
|
||||
* @param int $connectionTimeout limit in seconds, a value greater or equal than 0
|
||||
*/
|
||||
protected function setConnectionTimeout(int $connectionTimeout): void
|
||||
{
|
||||
if ($connectionTimeout < 0) {
|
||||
throw new \InvalidArgumentException('The specified connection timeout "' . $connectionTimeout . '" must be greater or equal than zero.', 1487849326);
|
||||
}
|
||||
|
||||
$this->connectionTimeout = $connectionTimeout;
|
||||
}
|
||||
|
||||
protected function setKeyPrefix(string $keyPrefix): void
|
||||
{
|
||||
$this->keyPrefix = $keyPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data in the cache
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n) with number of tags
|
||||
*/
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
$lifetime ??= $this->defaultLifetime;
|
||||
if ($lifetime < 0) {
|
||||
throw new \InvalidArgumentException('The specified lifetime "' . $lifetime . '" must be greater or equal than zero.', 1279487573);
|
||||
}
|
||||
if ($this->connected) {
|
||||
$expiration = $lifetime === 0 ? self::FAKED_UNLIMITED_LIFETIME : $lifetime;
|
||||
if ($this->compression) {
|
||||
$data = gzcompress($data, $this->compressionLevel);
|
||||
}
|
||||
$this->redis->setex($this->getDataIdentifier($entryIdentifier), $expiration, $data);
|
||||
$addTags = $tags;
|
||||
$removeTags = [];
|
||||
$existingTags = $this->redis->sMembers($this->getTagsIdentifier($entryIdentifier));
|
||||
if (!empty($existingTags)) {
|
||||
$addTags = array_diff($tags, $existingTags);
|
||||
$removeTags = array_diff($existingTags, $tags);
|
||||
}
|
||||
if (!empty($removeTags) || !empty($addTags)) {
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
foreach ($removeTags as $tag) {
|
||||
$queue->sRem($this->getTagsIdentifier($entryIdentifier), $tag);
|
||||
$queue->sRem($this->getTagIdentifier($tag), $entryIdentifier);
|
||||
}
|
||||
foreach ($addTags as $tag) {
|
||||
$queue->sAdd($this->getTagsIdentifier($entryIdentifier), $tag);
|
||||
$queue->sAdd($this->getTagIdentifier($tag), $entryIdentifier);
|
||||
}
|
||||
$queue->exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from the cache.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
*/
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
$storedEntry = false;
|
||||
if ($this->connected) {
|
||||
$storedEntry = $this->redis->get($this->getDataIdentifier($entryIdentifier));
|
||||
}
|
||||
if ($this->compression && (string)$storedEntry !== '') {
|
||||
return gzuncompress((string)$storedEntry);
|
||||
}
|
||||
return $storedEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
*/
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
return $this->connected && $this->redis->exists($this->getDataIdentifier($entryIdentifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n) with number of tags
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
if (!$this->connected) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->redis->exists($this->getDataIdentifier($entryIdentifier))) {
|
||||
return false;
|
||||
}
|
||||
$assignedTags = $this->redis->sMembers($this->getTagsIdentifier($entryIdentifier));
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
foreach ($assignedTags as $tag) {
|
||||
$queue->sRem($this->getTagIdentifier($tag), $entryIdentifier);
|
||||
}
|
||||
$queue->del($this->getDataIdentifier($entryIdentifier), $this->getTagsIdentifier($entryIdentifier));
|
||||
$queue->exec();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entry identifiers which are tagged by the specified tag.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n) with number of tag entries
|
||||
*/
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
if (!$this->connected) {
|
||||
return [];
|
||||
}
|
||||
return $this->redis->sMembers($this->getTagIdentifier($tag));
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
if (!$this->connected) {
|
||||
return;
|
||||
}
|
||||
// unless we have a key prefix all data can be flushed
|
||||
if ($this->keyPrefix === '') {
|
||||
$this->redis->flushDB();
|
||||
return;
|
||||
}
|
||||
$keys = $this->redis->keys($this->keyPrefix . '*');
|
||||
$queue = $this->redis->multi();
|
||||
$queue->del($keys);
|
||||
$queue->exec();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged with the specified tag.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n^2) with number of tag entries
|
||||
*/
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
if (!$this->connected) {
|
||||
return;
|
||||
}
|
||||
$identifiers = $this->redis->sMembers($this->getTagIdentifier($tag));
|
||||
if (!empty($identifiers)) {
|
||||
$this->removeIdentifierEntriesAndRelations($identifiers, [$tag]);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* With the current internal structure, only the identifier to data entries
|
||||
* have a redis internal lifetime. If an entry expires, attached
|
||||
* identifier to tags and tag to identifiers entries will be left over.
|
||||
* This method finds those entries and cleans them up.
|
||||
*
|
||||
* Scales O(n*m) with number of cache entries (n) and number of tags (m)
|
||||
*/
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
$identifierToTagsKeys = $this->redis->keys($this->getTagsIdentifier('*'));
|
||||
foreach ($identifierToTagsKeys as $identifierToTagsKey) {
|
||||
[, $identifier] = explode(':', $identifierToTagsKey);
|
||||
// Check if the data entry still exists
|
||||
if (!$this->redis->exists($this->getDataIdentifier($identifier))) {
|
||||
$tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
$queue->del($identifierToTagsKey);
|
||||
foreach ($tagsToRemoveIdentifierFrom as $tag) {
|
||||
$queue->sRem($this->getTagIdentifier($tag), $identifier);
|
||||
}
|
||||
$queue->exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for flushByTag()
|
||||
* Gets list of identifiers and tags and removes all relations of those tags
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n^2) with number of tags
|
||||
*/
|
||||
protected function removeIdentifierEntriesAndRelations(array $identifiers, array $tags): void
|
||||
{
|
||||
// Set a temporary entry which holds all identifiers that need to be removed from
|
||||
// the tag to identifiers sets
|
||||
$uniqueTempKey = 'temp:' . StringUtility::getUniqueId();
|
||||
$prefixedKeysToDelete = [$uniqueTempKey];
|
||||
$prefixedIdentifierToTagsKeysToDelete = [];
|
||||
foreach ($identifiers as $identifier) {
|
||||
$prefixedKeysToDelete[] = $this->getDataIdentifier($identifier);
|
||||
$prefixedIdentifierToTagsKeysToDelete[] = $this->getTagsIdentifier($identifier);
|
||||
}
|
||||
foreach ($tags as $tag) {
|
||||
$prefixedKeysToDelete[] = $this->getTagIdentifier($tag);
|
||||
}
|
||||
$tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion(...$prefixedIdentifierToTagsKeysToDelete);
|
||||
// Remove the tag to identifier set of the given tags, they will be removed anyway
|
||||
$tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
|
||||
// Diff all identifiers that must be removed from tag to identifiers sets off from a
|
||||
// tag to identifiers set and store result in same tag to identifiers set again
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$queue->sAdd($uniqueTempKey, $identifier);
|
||||
}
|
||||
foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
|
||||
$queue->sDiffStore($this->getTagIdentifier($tagToIdentifiersSet), $this->getTagIdentifier($tagToIdentifiersSet), $uniqueTempKey);
|
||||
}
|
||||
$queue->del(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
|
||||
$queue->exec();
|
||||
}
|
||||
|
||||
protected function getDataIdentifier(string $identifier): string
|
||||
{
|
||||
return $this->keyPrefix . self::IDENTIFIER_DATA_PREFIX . $identifier;
|
||||
}
|
||||
|
||||
protected function getTagsIdentifier(string $identifier): string
|
||||
{
|
||||
return $this->keyPrefix . self::IDENTIFIER_TAGS_PREFIX . $identifier;
|
||||
}
|
||||
|
||||
protected function getTagIdentifier(string $tag): string
|
||||
{
|
||||
return $this->keyPrefix . self::TAG_IDENTIFIERS_PREFIX . $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the authentication value based on the configuration, returning an associative array
|
||||
* in case `username` and `password` has been configured, the `password` as string if only
|
||||
* password has been configured or `null` to indicate no-authentication configuration, which
|
||||
* is also possible to be used with `redis`.
|
||||
*/
|
||||
protected function getAuthentication(): array|string|null
|
||||
{
|
||||
return match (true) {
|
||||
// Username and password configured for authentication, build associative array
|
||||
// out of possible and supported array variants by `php-redis::auth()`.
|
||||
($this->username !== null && $this->password !== null) => [
|
||||
'user' => $this->username,
|
||||
'pass' => $this->password,
|
||||
],
|
||||
// Password-only authentication configured.
|
||||
($this->username === null && $this->password !== null) => $this->password,
|
||||
// No authentication configured.
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries in files, but does not support or
|
||||
* care about expiry times and tags.
|
||||
*/
|
||||
class SimpleFileBackend extends AbstractBackend implements PhpCapableBackendInterface
|
||||
{
|
||||
/**
|
||||
* Directory where the files are stored
|
||||
*/
|
||||
protected string $cacheDirectory = '';
|
||||
|
||||
/**
|
||||
* Temporary path to cache directory before setCache() was called. It is
|
||||
* set by setCacheDirectory() and used in setCache() method which calls
|
||||
* the directory creation if needed. The variable is not used afterwards,
|
||||
* the final cache directory path is stored in $this->cacheDirectory then.
|
||||
*/
|
||||
protected string $temporaryCacheDirectory = '';
|
||||
|
||||
/**
|
||||
* A file extension to use for each cache entry.
|
||||
*/
|
||||
protected string $cacheEntryFileExtension = '';
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
parent::setCache($cache);
|
||||
if (empty($this->temporaryCacheDirectory)) {
|
||||
// If no cache directory was given with cacheDirectory
|
||||
// configuration option, set it to a path below var/ folder
|
||||
$temporaryCacheDirectory = Environment::getVarPath() . '/';
|
||||
} else {
|
||||
$temporaryCacheDirectory = $this->temporaryCacheDirectory;
|
||||
}
|
||||
$codeOrData = $cache instanceof PhpFrontend ? 'code' : 'data';
|
||||
$finalCacheDirectory = $temporaryCacheDirectory . 'cache/' . $codeOrData . '/' . $this->cacheIdentifier . '/';
|
||||
$this->createFinalCacheDirectory($finalCacheDirectory);
|
||||
$this->temporaryCacheDirectory = '';
|
||||
$this->cacheDirectory = $finalCacheDirectory;
|
||||
$this->cacheEntryFileExtension = $cache instanceof PhpFrontend ? '.php' : '';
|
||||
if (strlen($this->cacheDirectory) + 23 > PHP_MAXPATHLEN) {
|
||||
throw new Exception('The length of the temporary cache file path "' . $this->cacheDirectory . '" exceeds the maximum path length of ' . (PHP_MAXPATHLEN - 23) . '. Please consider setting the temporaryDirectoryBase option to a shorter path.', 1248710426);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the directory where the cache files are stored. By default it is
|
||||
* assumed that the directory is below TYPO3's Project Path. However, an
|
||||
* absolute path can be selected, too.
|
||||
*
|
||||
* This method enables to use a cache path outside of TYPO3's Project Path. The final
|
||||
* cache path is checked and created in createFinalCacheDirectory(),
|
||||
* called by setCache() method, which is done _after_ the cacheDirectory
|
||||
* option was handled.
|
||||
*
|
||||
* @internal Misused in tests
|
||||
* @todo: Fix tests and protect
|
||||
*/
|
||||
public function setCacheDirectory(string $cacheDirectory): void
|
||||
{
|
||||
$documentRoot = Environment::getProjectPath() . '/';
|
||||
if ($open_basedir = ini_get('open_basedir')) {
|
||||
if (Environment::isWindows()) {
|
||||
$delimiter = ';';
|
||||
$cacheDirectory = str_replace('\\', '/', $cacheDirectory);
|
||||
if (!preg_match('/[A-Z]:/', substr($cacheDirectory, 0, 2))) {
|
||||
$cacheDirectory = Environment::getProjectPath() . $cacheDirectory;
|
||||
}
|
||||
} else {
|
||||
$delimiter = ':';
|
||||
if ($cacheDirectory[0] !== '/') {
|
||||
// relative path to cache directory.
|
||||
$cacheDirectory = Environment::getProjectPath() . $cacheDirectory;
|
||||
}
|
||||
}
|
||||
$basedirs = explode($delimiter, $open_basedir);
|
||||
$cacheDirectoryInBaseDir = false;
|
||||
foreach ($basedirs as $basedir) {
|
||||
if (Environment::isWindows()) {
|
||||
$basedir = str_replace('\\', '/', $basedir);
|
||||
}
|
||||
if ($basedir[strlen($basedir) - 1] !== '/') {
|
||||
$basedir .= '/';
|
||||
}
|
||||
if (str_starts_with($cacheDirectory, $basedir)) {
|
||||
$documentRoot = $basedir;
|
||||
$cacheDirectory = str_replace($basedir, '', $cacheDirectory);
|
||||
$cacheDirectoryInBaseDir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$cacheDirectoryInBaseDir) {
|
||||
throw new Exception(
|
||||
'Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.',
|
||||
1476045417
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ($cacheDirectory[0] === '/') {
|
||||
// Absolute path to cache directory.
|
||||
$documentRoot = '';
|
||||
}
|
||||
if (Environment::isWindows() && (!empty($documentRoot) && str_starts_with($cacheDirectory, $documentRoot))) {
|
||||
$documentRoot = '';
|
||||
}
|
||||
}
|
||||
// After this point all paths have '/' as directory separator
|
||||
if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
|
||||
$cacheDirectory .= '/';
|
||||
}
|
||||
$this->temporaryCacheDirectory = $documentRoot . $cacheDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the final cache directory if it does not exist.
|
||||
*/
|
||||
protected function createFinalCacheDirectory(string $finalCacheDirectory): void
|
||||
{
|
||||
if (!is_dir($finalCacheDirectory)) {
|
||||
try {
|
||||
GeneralUtility::mkdir_deep($finalCacheDirectory);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new Exception('The directory "' . $finalCacheDirectory . '" can not be created.', 1303669848, $e);
|
||||
}
|
||||
}
|
||||
if (!is_writable($finalCacheDirectory)) {
|
||||
throw new Exception('The directory "' . $finalCacheDirectory . '" is not writable.', 1203965200);
|
||||
}
|
||||
$tmpFilesCacheDirectory = $finalCacheDirectory . 'tmp/';
|
||||
if (!is_dir($tmpFilesCacheDirectory)) {
|
||||
try {
|
||||
GeneralUtility::mkdir_deep($tmpFilesCacheDirectory);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new Exception('The temporary cache directory "' . $tmpFilesCacheDirectory . '" can not be created.', 1727176780, $e);
|
||||
}
|
||||
}
|
||||
if (!is_writable($tmpFilesCacheDirectory)) {
|
||||
throw new Exception('The temporary cache directory "' . $tmpFilesCacheDirectory . '" is not writable.', 1727176781);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where the cache files are stored
|
||||
*
|
||||
* @return string Full path of the cache directory
|
||||
* @internal Misused in tests
|
||||
* @todo: Fix tests and protect
|
||||
*/
|
||||
public function getCacheDirectory(): string
|
||||
{
|
||||
return $this->cacheDirectory;
|
||||
}
|
||||
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756735);
|
||||
}
|
||||
if ($entryIdentifier === '') {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756736);
|
||||
}
|
||||
$temporaryCacheEntryPathAndFilename = $this->cacheDirectory . 'tmp/' . StringUtility::getUniqueId() . '.temp';
|
||||
$result = GeneralUtility::writeFile($temporaryCacheEntryPathAndFilename, $data, true);
|
||||
if ($result === false) {
|
||||
throw new Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1334756737);
|
||||
}
|
||||
$cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
$result = @rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename);
|
||||
if ($result === false) {
|
||||
throw new Exception('The cache file "' . $cacheEntryPathAndFilename . '" could not be written.', 1727178709);
|
||||
}
|
||||
if ($this->cacheEntryFileExtension === '.php') {
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($cacheEntryPathAndFilename);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): false|string
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756877);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if (!file_exists($pathAndFilename)) {
|
||||
return false;
|
||||
}
|
||||
return file_get_contents($pathAndFilename);
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756878);
|
||||
}
|
||||
return file_exists($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756960);
|
||||
}
|
||||
if ($entryIdentifier === '') {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756961);
|
||||
}
|
||||
$file = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
return @unlink($file);
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$directoryIterator = new \DirectoryIterator($this->cacheDirectory);
|
||||
foreach ($directoryIterator as $fileInfo) {
|
||||
if (!$fileInfo->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (@unlink($this->cacheDirectory . $fileInfo->getFilename())) {
|
||||
continue;
|
||||
}
|
||||
$this->logger->error('Failed to unlink cache entry: {filename}', [
|
||||
'filename' => $this->cacheDirectory . $fileInfo->getFilename(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isCacheFileExpired(string $cacheEntryPathAndFilename): bool
|
||||
{
|
||||
return file_exists($cacheEntryPathAndFilename) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op
|
||||
*/
|
||||
public function collectGarbage(): void {}
|
||||
|
||||
public function requireOnce(string $entryIdentifier): mixed
|
||||
{
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073037);
|
||||
}
|
||||
return file_exists($pathAndFilename) ? require_once $pathAndFilename : false;
|
||||
}
|
||||
|
||||
public function require(string $entryIdentifier): mixed
|
||||
{
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1532528267);
|
||||
}
|
||||
return file_exists($pathAndFilename) ? require $pathAndFilename : false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/**
|
||||
* A contract for a cache backend which supports tagging.
|
||||
*/
|
||||
interface TaggableBackendInterface extends BackendInterface
|
||||
{
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged by the specified tag.
|
||||
*
|
||||
* @param string $tag The tag the entries must have
|
||||
*/
|
||||
public function flushByTag(string $tag): void;
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged by any of the specified tags.
|
||||
*
|
||||
* @param string[] $tags List of tags
|
||||
*/
|
||||
public function flushByTags(array $tags): void;
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entry identifiers which are tagged by the
|
||||
* specified tag
|
||||
*
|
||||
* @param string $tag The tag to search for
|
||||
* @return array An array with identifiers of all matching entries. An empty array if no entries matched
|
||||
*/
|
||||
public function findIdentifiersByTag(string $tag): array;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
|
||||
/**
|
||||
* A contract for a cache backends which store variables in volatile
|
||||
* memory and as such support receiving any variable type to store.
|
||||
*
|
||||
* Note: respect for this contract is up to each individual frontend.
|
||||
* The contract can be respected for a small performance boost, but
|
||||
* the result is marginal except for cases with huge serialized
|
||||
* data sets.
|
||||
*
|
||||
* Respected by the VariableFrontend which checks if the backend
|
||||
* has this interface, in which case it allows the backend to store
|
||||
* the value directly without serializing it to a string, and does
|
||||
* not attempt to unserialize the string on every get() request.
|
||||
*/
|
||||
interface TransientBackendInterface extends BackendInterface
|
||||
{
|
||||
/**
|
||||
* Saves data in the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier for this specific cache entry
|
||||
* @param mixed $data The data to be stored
|
||||
* @param array $tags Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored.
|
||||
* @param int|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @throws Exception if no cache frontend has been set.
|
||||
* @throws InvalidDataException if the data is not a string
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries during one script run.
|
||||
*/
|
||||
class TransientMemoryBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
protected array $entries = [];
|
||||
|
||||
protected array $tagsAndEntries = [];
|
||||
|
||||
/**
|
||||
* @param mixed $data The data to be stored. mixed is allowed due to TransientBackendInterface
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], $lifetime = null): void
|
||||
{
|
||||
$this->entries[$entryIdentifier] = $data;
|
||||
foreach ($tags as $tag) {
|
||||
$this->tagsAndEntries[$tag][$entryIdentifier] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
return $this->entries[$entryIdentifier] ?? false;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
return isset($this->entries[$entryIdentifier]);
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
if (isset($this->entries[$entryIdentifier])) {
|
||||
unset($this->entries[$entryIdentifier]);
|
||||
foreach (array_keys($this->tagsAndEntries) as $tag) {
|
||||
if (isset($this->tagsAndEntries[$tag][$entryIdentifier])) {
|
||||
unset($this->tagsAndEntries[$tag][$entryIdentifier]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
if (isset($this->tagsAndEntries[$tag])) {
|
||||
return array_keys($this->tagsAndEntries[$tag]);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->entries = [];
|
||||
$this->tagsAndEntries = [];
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
array_walk($tags, $this->flushByTag(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op
|
||||
*/
|
||||
public function collectGarbage(): void {}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries in database tables
|
||||
*/
|
||||
class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* @var int Timestamp of 2038-01-01
|
||||
*/
|
||||
protected const FAKED_UNLIMITED_EXPIRE = 2145909600;
|
||||
/**
|
||||
* @var string Name of the cache data table
|
||||
*/
|
||||
protected string $cacheTable;
|
||||
|
||||
/**
|
||||
* @var string Name of the cache tags table
|
||||
*/
|
||||
protected string $tagsTable;
|
||||
|
||||
/**
|
||||
* @var bool Indicates whether data is compressed or not (requires php zlib)
|
||||
*/
|
||||
protected bool $compression = false;
|
||||
|
||||
/**
|
||||
* @var int -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
|
||||
*/
|
||||
protected int $compressionLevel = -1;
|
||||
|
||||
/**
|
||||
* @var int Maximum lifetime to stay with expire field below FAKED_UNLIMITED_LIFETIME
|
||||
*/
|
||||
protected int $maximumLifetime;
|
||||
|
||||
public function setCache(FrontendInterface $cache): void
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$this->cacheTable = 'cache_' . $this->cacheIdentifier;
|
||||
$this->tagsTable = 'cache_' . $this->cacheIdentifier . '_tags';
|
||||
$this->maximumLifetime = self::FAKED_UNLIMITED_EXPIRE - $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
|
||||
public function set(string $entryIdentifier, string $data, array $tags = [], $lifetime = null): void
|
||||
{
|
||||
if ($lifetime === null) {
|
||||
$lifetime = $this->defaultLifetime;
|
||||
}
|
||||
if ($lifetime === 0 || $lifetime > $this->maximumLifetime) {
|
||||
$lifetime = $this->maximumLifetime;
|
||||
}
|
||||
$expires = $GLOBALS['EXEC_TIME'] + $lifetime;
|
||||
$this->remove($entryIdentifier);
|
||||
if ($this->compression) {
|
||||
$data = gzcompress($data, $this->compressionLevel);
|
||||
}
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->cacheTable)
|
||||
->insert(
|
||||
$this->cacheTable,
|
||||
[
|
||||
'identifier' => $entryIdentifier,
|
||||
'expires' => $expires,
|
||||
'content' => $data,
|
||||
],
|
||||
[
|
||||
'content' => Connection::PARAM_LOB,
|
||||
]
|
||||
);
|
||||
if (!empty($tags)) {
|
||||
$tagRows = [];
|
||||
foreach ($tags as $tag) {
|
||||
$tagRows[] = [$entryIdentifier, $tag];
|
||||
}
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->tagsTable)
|
||||
->bulkInsert($this->tagsTable, $tagRows, ['identifier', 'tag'], ['identifier' => Connection::PARAM_STR, 'tag' => Connection::PARAM_STR]);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->cacheTable);
|
||||
$cacheRow = $queryBuilder->select('content')
|
||||
->from($this->cacheTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'identifier',
|
||||
$queryBuilder->createNamedParameter($entryIdentifier)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
$content = '';
|
||||
if (!empty($cacheRow)) {
|
||||
$content = $cacheRow['content'];
|
||||
}
|
||||
if ($this->compression && (string)$content !== '') {
|
||||
$content = gzuncompress($content);
|
||||
}
|
||||
return empty($cacheRow) ? false : $content;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->cacheTable);
|
||||
$count = $queryBuilder->count('*')
|
||||
->from($this->cacheTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'identifier',
|
||||
$queryBuilder->createNamedParameter($entryIdentifier)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
return (bool)$count;
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
$numberOfRowsRemoved = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->cacheTable)
|
||||
->delete(
|
||||
$this->cacheTable,
|
||||
['identifier' => $entryIdentifier],
|
||||
['identifier' => Connection::PARAM_STR]
|
||||
);
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->tagsTable)
|
||||
->delete(
|
||||
$this->tagsTable,
|
||||
['identifier' => $entryIdentifier],
|
||||
['identifier' => Connection::PARAM_STR]
|
||||
);
|
||||
return (bool)$numberOfRowsRemoved;
|
||||
}
|
||||
|
||||
public function findIdentifiersByTag(string $tag): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tagsTable);
|
||||
$result = $queryBuilder->select($this->cacheTable . '.identifier')
|
||||
->from($this->cacheTable)
|
||||
->from($this->tagsTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq($this->cacheTable . '.identifier', $queryBuilder->quoteIdentifier($this->tagsTable . '.identifier')),
|
||||
$queryBuilder->expr()->eq(
|
||||
$this->tagsTable . '.tag',
|
||||
$queryBuilder->createNamedParameter($tag)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
$this->cacheTable . '.expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->groupBy($this->cacheTable . '.identifier')
|
||||
->executeQuery();
|
||||
$identifiers = $result->fetchFirstColumn();
|
||||
return array_combine($identifiers, $identifiers);
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable)->truncate($this->cacheTable);
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->tagsTable)->truncate($this->tagsTable);
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
if (empty($tags)) {
|
||||
return;
|
||||
}
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
// A large set of tags was detected. Process it in chunks to guard against exceeding
|
||||
// maximum SQL query limits.
|
||||
if (count($tags) > 100) {
|
||||
$chunks = array_chunk($tags, 100);
|
||||
array_walk($chunks, $this->flushByTags(...));
|
||||
return;
|
||||
}
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->tagsTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->in('tag', $queryBuilder->quoteArrayBasedValueListToStringList($tags)),
|
||||
)
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->executeQuery();
|
||||
$cacheEntryIdentifiers = $result->fetchFirstColumn();
|
||||
$this->flushCacheByCacheEntryIdentifiers($cacheEntryIdentifiers);
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
if (empty($tag)) {
|
||||
return;
|
||||
}
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->tagsTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('tag', $queryBuilder->quote($tag)),
|
||||
)
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->executeQuery();
|
||||
$cacheEntryIdentifiers = $result->fetchFirstColumn();
|
||||
$this->flushCacheByCacheEntryIdentifiers($cacheEntryIdentifiers);
|
||||
}
|
||||
|
||||
private function flushCacheByCacheEntryIdentifiers(array $cacheEntryIdentifiers): void
|
||||
{
|
||||
if ($cacheEntryIdentifiers === []) {
|
||||
// Nothing to do, return early.
|
||||
return;
|
||||
}
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
$maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
|
||||
foreach (array_chunk($cacheEntryIdentifiers, $maxBindParameters) as $chunk) {
|
||||
// Don't reuse QueryBuilder instance, create new one.
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
// Using string-list here directly is okay and mitigates additional processing
|
||||
// for database driver without named placeholder support, which comes with a
|
||||
// performance penalty we can work around and also do it only once per chunk.
|
||||
$quotedIdentifiers = $queryBuilder->quoteArrayBasedValueListToStringList($chunk);
|
||||
$queryBuilder->delete($this->cacheTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->executeStatement();
|
||||
// Don't reuse QueryBuilder instance, create new one.
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$queryBuilder->delete($this->tagsTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->cacheTable)
|
||||
->where($queryBuilder->expr()->lt(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
))
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->executeQuery();
|
||||
|
||||
// Get identifiers of expired cache entries
|
||||
$cacheEntryIdentifiers = $result->fetchFirstColumn();
|
||||
if (!empty($cacheEntryIdentifiers)) {
|
||||
// Delete tag rows connected to expired cache entries
|
||||
$this->deleteTagsChunked($cacheEntryIdentifiers);
|
||||
}
|
||||
$queryBuilder->delete($this->cacheTable)
|
||||
->where($queryBuilder->expr()->lt(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
))
|
||||
->executeStatement();
|
||||
|
||||
// Find out which "orphaned" tags rows exists that have no cache row and delete those, too.
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('tags.identifier')
|
||||
->from($this->tagsTable, 'tags')
|
||||
->leftJoin(
|
||||
'tags',
|
||||
$this->cacheTable,
|
||||
'cache',
|
||||
$queryBuilder->expr()->eq('tags.identifier', $queryBuilder->quoteIdentifier('cache.identifier'))
|
||||
)
|
||||
->where($queryBuilder->expr()->isNull('cache.identifier'))
|
||||
->groupBy('tags.identifier')
|
||||
->executeQuery();
|
||||
$tagsEntryIdentifiers = $result->fetchFirstColumn();
|
||||
|
||||
if (!empty($tagsEntryIdentifiers)) {
|
||||
$this->deleteTagsChunked($tagsEntryIdentifiers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $items
|
||||
*/
|
||||
protected function deleteTagsChunked(array $items): void
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->tagsTable);
|
||||
$maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
|
||||
foreach (array_chunk($items, $maxBindParameters, true) as $itemsChunk) {
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->delete($this->tagsTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $queryBuilder->quoteArrayBasedValueListToStringList($itemsChunk)))
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
protected function setCompression(bool $compression): void
|
||||
{
|
||||
$this->compression = $compression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data compression level.
|
||||
* If compression is enabled and this is not set,
|
||||
* gzcompress default level will be used
|
||||
*
|
||||
* @param int $compressionLevel -1 to 9: Compression level
|
||||
*/
|
||||
protected function setCompressionLevel(int $compressionLevel): void
|
||||
{
|
||||
if ($compressionLevel >= -1 && $compressionLevel <= 9) {
|
||||
$this->compressionLevel = $compressionLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate needed table definitions for this cache.
|
||||
* This helper method is used by install tool and extension manager
|
||||
* and is not part of the public API!
|
||||
*
|
||||
* @return string SQL of table definitions
|
||||
*/
|
||||
public function getTableDefinitions(): string
|
||||
{
|
||||
$cacheTableSql = (string)file_get_contents(
|
||||
ExtensionManagementUtility::extPath('core')
|
||||
. 'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
|
||||
);
|
||||
$requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
|
||||
$tagsTableSql = (string)file_get_contents(
|
||||
ExtensionManagementUtility::extPath('core')
|
||||
. 'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
|
||||
);
|
||||
return $requiredTableStructures . (str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache;
|
||||
|
||||
final class CacheDataCollector implements CacheDataCollectorInterface
|
||||
{
|
||||
private ?string $pageCacheIdentifier = null;
|
||||
|
||||
/**
|
||||
* @var CacheTag[]
|
||||
*/
|
||||
private array $cacheTags = [];
|
||||
|
||||
private int $lifetime = PHP_INT_MAX;
|
||||
|
||||
/**
|
||||
* @var CacheEntry[]
|
||||
*/
|
||||
private array $cacheEntries = [];
|
||||
|
||||
public function setPageCacheIdentifier(string $identifier): void
|
||||
{
|
||||
$this->pageCacheIdentifier = $identifier;
|
||||
}
|
||||
|
||||
public function getPageCacheIdentifier(): string
|
||||
{
|
||||
if ($this->pageCacheIdentifier === null) {
|
||||
throw new \LogicException('Page cache identifier has not been set. Broken call chain.', 1761315963);
|
||||
}
|
||||
return $this->pageCacheIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CacheTag[]
|
||||
*/
|
||||
public function getCacheTags(): array
|
||||
{
|
||||
return array_values($this->cacheTags);
|
||||
}
|
||||
|
||||
public function addCacheTags(CacheTag ...$cacheTags): void
|
||||
{
|
||||
array_walk($cacheTags, fn(CacheTag $cacheTag) => $this->addCacheTag($cacheTag));
|
||||
}
|
||||
|
||||
public function removeCacheTags(CacheTag ...$cacheTags): void
|
||||
{
|
||||
array_walk($cacheTags, fn(CacheTag $cacheTag) => $this->removeCacheTag($cacheTag));
|
||||
}
|
||||
|
||||
public function restrictMaximumLifetime(int $lifetime): void
|
||||
{
|
||||
$this->lifetime = min($lifetime, $this->lifetime);
|
||||
}
|
||||
|
||||
public function resolveLifetime(): int
|
||||
{
|
||||
$lifetimes = array_unique(
|
||||
[$this->lifetime, ...array_map(fn(CacheTag $cacheTag) => $cacheTag->lifetime, $this->cacheTags)]
|
||||
);
|
||||
return min($lifetimes);
|
||||
}
|
||||
|
||||
public function enqueueCacheEntry(CacheEntry $deferredCacheItem): void
|
||||
{
|
||||
$this->cacheEntries[$deferredCacheItem->identifier] = $deferredCacheItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CacheEntry[]
|
||||
*/
|
||||
public function getCacheEntries(): array
|
||||
{
|
||||
return array_values($this->cacheEntries);
|
||||
}
|
||||
|
||||
private function addCacheTag(CacheTag $cacheTag): void
|
||||
{
|
||||
$this->cacheTags[$cacheTag->name] = $cacheTag;
|
||||
}
|
||||
|
||||
private function removeCacheTag(CacheTag $cacheTag): void
|
||||
{
|
||||
unset($this->cacheTags[$cacheTag->name]);
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Cache;
|
||||
|
||||
interface CacheDataCollectorInterface
|
||||
{
|
||||
/**
|
||||
* @return CacheTag[]
|
||||
*/
|
||||
public function getCacheTags(): array;
|
||||
|
||||
public function addCacheTags(CacheTag ...$tag): void;
|
||||
|
||||
public function removeCacheTags(CacheTag ...$tag): void;
|
||||
|
||||
public function resolveLifetime(): int;
|
||||
|
||||
public function restrictMaximumLifetime(int $lifetime): void;
|
||||
|
||||
public function enqueueCacheEntry(CacheEntry $deferredCacheItem): void;
|
||||
|
||||
/**
|
||||
* @return CacheEntry[]
|
||||
*/
|
||||
public function getCacheEntries(): array;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* A closure for lazy cache entry persistence, allowing cache lifetime
|
||||
* to be altered in the CacheDataCollector.
|
||||
*
|
||||
* Also allows a custom middleware to intercept a cache entry
|
||||
* from within a middleware.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class CacheEntry
|
||||
{
|
||||
public function __construct(
|
||||
public string $identifier,
|
||||
public mixed $content,
|
||||
private \Closure $persist,
|
||||
) {}
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): void
|
||||
{
|
||||
($this->persist)($request, $this->identifier, $this->content);
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Cache;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\BackendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Backend\NullBackend;
|
||||
use TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend;
|
||||
use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
|
||||
use TYPO3\CMS\Core\Cache\Event\CacheFlushEvent;
|
||||
use TYPO3\CMS\Core\Cache\Exception\DuplicateIdentifierException;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidBackendException;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidCacheException;
|
||||
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
|
||||
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheGroupException;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
|
||||
class CacheManager implements SingletonInterface
|
||||
{
|
||||
/**
|
||||
* @var FrontendInterface[]
|
||||
*/
|
||||
protected array $caches = [];
|
||||
|
||||
protected array $cacheConfigurations = [];
|
||||
|
||||
/**
|
||||
* Used to flush caches of a specific group
|
||||
* is an associative array containing the group identifier as key
|
||||
* and the identifier as an array within that group
|
||||
* groups are set via the cache configurations of each cache.
|
||||
*/
|
||||
protected array $cacheGroups = [];
|
||||
|
||||
protected array $defaultCacheConfiguration = [
|
||||
'frontend' => VariableFrontend::class,
|
||||
'backend' => Typo3DatabaseBackend::class,
|
||||
'options' => [],
|
||||
'groups' => ['all'],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected bool $disableCaching = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets configurations for caches. The key of each entry specifies the
|
||||
* cache identifier and the value is an array of configuration options.
|
||||
* Possible options are:
|
||||
*
|
||||
* frontend
|
||||
* backend
|
||||
* backendOptions
|
||||
*
|
||||
* If one of the options is not specified, the default value is assumed.
|
||||
* Existing cache configurations are preserved.
|
||||
*
|
||||
* @param array<string, array> $cacheConfigurations The cache configurations to set
|
||||
* @throws \InvalidArgumentException If $cacheConfigurations is not an array
|
||||
*/
|
||||
public function setCacheConfigurations(array $cacheConfigurations): void
|
||||
{
|
||||
$newConfiguration = [];
|
||||
foreach ($cacheConfigurations as $identifier => $configuration) {
|
||||
if (empty($identifier)) {
|
||||
throw new \InvalidArgumentException('A cache identifier was not set.', 1596980032);
|
||||
}
|
||||
if (!is_array($configuration)) {
|
||||
throw new \InvalidArgumentException('The cache configuration for cache "' . $identifier . '" was not an array as expected.', 1231259656);
|
||||
}
|
||||
$newConfiguration[$identifier] = $configuration;
|
||||
}
|
||||
$this->cacheConfigurations = $newConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a cache so it can be retrieved at a later point.
|
||||
*
|
||||
* @param array $groups Cache groups to be associated to the cache
|
||||
* @throws DuplicateIdentifierException if a cache with the given identifier has already been registered.
|
||||
*/
|
||||
public function registerCache(FrontendInterface $cache, array $groups = []): void
|
||||
{
|
||||
$identifier = $cache->getIdentifier();
|
||||
if (isset($this->caches[$identifier])) {
|
||||
throw new DuplicateIdentifierException('A cache with identifier "' . $identifier . '" has already been registered.', 1203698223);
|
||||
}
|
||||
$this->caches[$identifier] = $cache;
|
||||
foreach ($groups as $groupIdentifier) {
|
||||
$this->cacheGroups[$groupIdentifier][] = $identifier;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cache specified by $identifier
|
||||
*
|
||||
* @throws NoSuchCacheException
|
||||
*/
|
||||
public function getCache(string $identifier): FrontendInterface
|
||||
{
|
||||
if ($this->hasCache($identifier) === false) {
|
||||
throw new NoSuchCacheException('A cache with identifier "' . $identifier . '" does not exist.', 1203699034);
|
||||
}
|
||||
if (!isset($this->caches[$identifier])) {
|
||||
$this->createCache($identifier);
|
||||
}
|
||||
return $this->caches[$identifier];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified cache has been registered.
|
||||
*/
|
||||
public function hasCache(string $identifier): bool
|
||||
{
|
||||
return isset($this->caches[$identifier]) || isset($this->cacheConfigurations[$identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes all registered caches
|
||||
*/
|
||||
public function flushCaches(): void
|
||||
{
|
||||
$this->createAllCaches();
|
||||
foreach ($this->caches as $cache) {
|
||||
$cache->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes all registered caches of a specific group
|
||||
*
|
||||
* @throws NoSuchCacheGroupException
|
||||
*/
|
||||
public function flushCachesInGroup(string $groupIdentifier): void
|
||||
{
|
||||
$this->createAllCaches();
|
||||
if (!isset($this->cacheGroups[$groupIdentifier])) {
|
||||
throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390334120);
|
||||
}
|
||||
foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
|
||||
if (isset($this->caches[$cacheIdentifier])) {
|
||||
$this->caches[$cacheIdentifier]->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes entries tagged by the specified tag of all registered
|
||||
* caches of a specific group.
|
||||
*
|
||||
* @throws NoSuchCacheGroupException
|
||||
*/
|
||||
public function flushCachesInGroupByTag(string $groupIdentifier, string $tag): void
|
||||
{
|
||||
if (empty($tag)) {
|
||||
return;
|
||||
}
|
||||
$this->createAllCaches();
|
||||
if (!isset($this->cacheGroups[$groupIdentifier])) {
|
||||
throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390337129);
|
||||
}
|
||||
foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
|
||||
if (isset($this->caches[$cacheIdentifier])) {
|
||||
$this->caches[$cacheIdentifier]->flushByTag($tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes entries tagged by any of the specified tags in all registered
|
||||
* caches of a specific group.
|
||||
*
|
||||
* @throws NoSuchCacheGroupException
|
||||
*/
|
||||
public function flushCachesInGroupByTags(string $groupIdentifier, array $tags): void
|
||||
{
|
||||
if (empty($tags)) {
|
||||
return;
|
||||
}
|
||||
$this->createAllCaches();
|
||||
if (!isset($this->cacheGroups[$groupIdentifier])) {
|
||||
throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390337130);
|
||||
}
|
||||
foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
|
||||
if (isset($this->caches[$cacheIdentifier])) {
|
||||
$this->caches[$cacheIdentifier]->flushByTags($tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes entries tagged by the specified tag of all registered caches.
|
||||
*/
|
||||
public function flushCachesByTag(string $tag): void
|
||||
{
|
||||
$this->createAllCaches();
|
||||
foreach ($this->caches as $cache) {
|
||||
$cache->flushByTag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes entries tagged by any of the specified tags in all registered caches.
|
||||
*/
|
||||
public function flushCachesByTags(array $tags): void
|
||||
{
|
||||
$this->createAllCaches();
|
||||
foreach ($this->caches as $cache) {
|
||||
$cache->flushByTags($tags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @internal
|
||||
*/
|
||||
public function getCacheGroups(): array
|
||||
{
|
||||
$groups = array_keys($this->cacheGroups);
|
||||
foreach ($this->cacheConfigurations as $config) {
|
||||
foreach ($config['groups'] ?? [] as $group) {
|
||||
if (!in_array($group, $groups, true)) {
|
||||
$groups[] = $group;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function handleCacheFlushEvent(CacheFlushEvent $event): void
|
||||
{
|
||||
foreach ($event->getGroups() as $group) {
|
||||
$this->flushCachesInGroup($group);
|
||||
}
|
||||
}
|
||||
|
||||
protected function createAllCaches(): void
|
||||
{
|
||||
foreach ($this->cacheConfigurations as $identifier => $_) {
|
||||
if (!isset($this->caches[$identifier])) {
|
||||
$this->createCache($identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates the cache for $identifier.
|
||||
*
|
||||
* @throws DuplicateIdentifierException
|
||||
* @throws InvalidBackendException
|
||||
* @throws InvalidCacheException
|
||||
*/
|
||||
protected function createCache(string $identifier): void
|
||||
{
|
||||
if (isset($this->cacheConfigurations[$identifier]['frontend'])) {
|
||||
$frontend = $this->cacheConfigurations[$identifier]['frontend'];
|
||||
} else {
|
||||
$frontend = $this->defaultCacheConfiguration['frontend'];
|
||||
}
|
||||
if (isset($this->cacheConfigurations[$identifier]['backend'])) {
|
||||
$backend = $this->cacheConfigurations[$identifier]['backend'];
|
||||
} else {
|
||||
$backend = $this->defaultCacheConfiguration['backend'];
|
||||
}
|
||||
if (isset($this->cacheConfigurations[$identifier]['options'])) {
|
||||
$backendOptions = $this->cacheConfigurations[$identifier]['options'];
|
||||
} else {
|
||||
$backendOptions = $this->defaultCacheConfiguration['options'];
|
||||
}
|
||||
// Normalize legacy non-bool 'compression' values for strictly typed backend setters.
|
||||
if (isset($backendOptions['compression']) && !is_bool($backendOptions['compression'])) {
|
||||
$backendOptions['compression'] = (bool)$backendOptions['compression'];
|
||||
}
|
||||
|
||||
if ($this->disableCaching && $backend !== TransientMemoryBackend::class) {
|
||||
$backend = NullBackend::class;
|
||||
$backendOptions = [];
|
||||
}
|
||||
|
||||
// Add the cache identifier to the groups that it should be attached to, or use the default ones.
|
||||
if (isset($this->cacheConfigurations[$identifier]['groups']) && is_array($this->cacheConfigurations[$identifier]['groups'])) {
|
||||
$assignedGroups = $this->cacheConfigurations[$identifier]['groups'];
|
||||
} else {
|
||||
$assignedGroups = $this->defaultCacheConfiguration['groups'];
|
||||
}
|
||||
foreach ($assignedGroups as $groupIdentifier) {
|
||||
if (!isset($this->cacheGroups[$groupIdentifier])) {
|
||||
$this->cacheGroups[$groupIdentifier] = [];
|
||||
}
|
||||
$this->cacheGroups[$groupIdentifier][] = $identifier;
|
||||
}
|
||||
|
||||
// New operator used on purpose: This class is required early during
|
||||
// bootstrap before makeInstance() is properly set up
|
||||
$backend = '\\' . ltrim($backend, '\\');
|
||||
$backendInstance = new $backend($backendOptions);
|
||||
if (!$backendInstance instanceof BackendInterface) {
|
||||
throw new InvalidBackendException('"' . $backend . '" is not a valid cache backend object.', 1464550977);
|
||||
}
|
||||
if (is_callable([$backendInstance, 'initializeObject'])) {
|
||||
$backendInstance->initializeObject();
|
||||
}
|
||||
|
||||
// New used on purpose, see comment above
|
||||
$frontendInstance = new $frontend($identifier, $backendInstance);
|
||||
if (!$frontendInstance instanceof FrontendInterface) {
|
||||
throw new InvalidCacheException('"' . $frontend . '" is not a valid cache frontend object.', 1464550984);
|
||||
}
|
||||
if (is_callable([$frontendInstance, 'initializeObject'])) {
|
||||
$frontendInstance->initializeObject();
|
||||
}
|
||||
|
||||
$this->registerCache($frontendInstance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache;
|
||||
|
||||
final readonly class CacheTag
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public int $lifetime = PHP_INT_MAX,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent;
|
||||
|
||||
/**
|
||||
* This service provides the sql schema for the caching framework
|
||||
*/
|
||||
final class DatabaseSchemaService
|
||||
{
|
||||
/**
|
||||
* An event listener to inject the required caching framework database tables to the
|
||||
* tables definitions string
|
||||
*/
|
||||
#[AsEventListener('caching-framework')]
|
||||
public function addCachingFrameworkDatabaseSchema(AlterTableDefinitionStatementsEvent $event): void
|
||||
{
|
||||
$event->addSqlData($this->getCachingFrameworkRequiredDatabaseSchema());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema SQL of required cache framework tables.
|
||||
*
|
||||
* This method needs ext_localconf loaded!
|
||||
*
|
||||
* @return string Cache framework SQL
|
||||
*/
|
||||
private function getCachingFrameworkRequiredDatabaseSchema(): string
|
||||
{
|
||||
// Use new to circumvent the singleton pattern of CacheManager
|
||||
$cacheManager = new CacheManager();
|
||||
$cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
|
||||
$tableDefinitions = '';
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] as $cacheName => $_) {
|
||||
$backend = $cacheManager->getCache($cacheName)->getBackend();
|
||||
if (method_exists($backend, 'getTableDefinitions')) {
|
||||
$tableDefinitions .= LF . $backend->getTableDefinitions();
|
||||
}
|
||||
}
|
||||
return $tableDefinitions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\CacheTag;
|
||||
|
||||
/**
|
||||
* This event should only be used in code that has no access to the request attribute 'frontend.cache.collector'.
|
||||
* If you have access to the request, use the $request->getAttribute('frontend.cache.collector')->addCacheTags(...)
|
||||
* directly. It's really just there to allow passive cache-data signaling, without exactly knowing the
|
||||
* current context.
|
||||
*
|
||||
* @internal This event is a tribute to core places that need to set cache tags but do not have the
|
||||
* current request yet. The FE CacheDataCollectorAttribute listens on this event. It
|
||||
* may vanish later without further notice.
|
||||
*/
|
||||
final readonly class AddCacheTagEvent
|
||||
{
|
||||
public function __construct(
|
||||
public CacheTag $cacheTag,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Event;
|
||||
|
||||
/**
|
||||
* Event fired when caches are to be cleared
|
||||
*/
|
||||
final class CacheFlushEvent
|
||||
{
|
||||
private array $errors = [];
|
||||
|
||||
public function __construct(private readonly array $groups) {}
|
||||
|
||||
public function getGroups(): array
|
||||
{
|
||||
return $this->groups;
|
||||
}
|
||||
|
||||
public function hasGroup(string $group): bool
|
||||
{
|
||||
return in_array($group, $this->groups, true);
|
||||
}
|
||||
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function addError(string $error): void
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Event;
|
||||
|
||||
/**
|
||||
* Event fired when caches are to be warmed up
|
||||
*/
|
||||
final class CacheWarmupEvent
|
||||
{
|
||||
private array $errors = [];
|
||||
|
||||
public function __construct(private readonly array $groups) {}
|
||||
|
||||
public function getGroups(): array
|
||||
{
|
||||
return $this->groups;
|
||||
}
|
||||
|
||||
public function hasGroup(string $group): bool
|
||||
{
|
||||
return in_array($group, $this->groups, true);
|
||||
}
|
||||
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function addError(string $error): void
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Cache;
|
||||
|
||||
/**
|
||||
* A generic Cache exception
|
||||
*/
|
||||
class Exception extends \Exception {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
class DuplicateIdentifierException extends Exception {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
class InvalidBackendException extends Exception {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
class InvalidCacheException extends Exception {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
class InvalidDataException extends Exception {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
class NoSuchCacheException extends Exception {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
class NoSuchCacheGroupException extends Exception {}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\BackendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface;
|
||||
|
||||
abstract class AbstractFrontend implements FrontendInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected string $identifier,
|
||||
protected BackendInterface $backend
|
||||
) {
|
||||
if (preg_match(self::PATTERN_ENTRYIDENTIFIER, $identifier) !== 1) {
|
||||
throw new \InvalidArgumentException('"' . $identifier . '" is not a valid cache identifier.', 1203584729);
|
||||
}
|
||||
$this->identifier = $identifier;
|
||||
$this->backend->setCache($this);
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function getBackend(): BackendInterface
|
||||
{
|
||||
return $this->backend;
|
||||
}
|
||||
|
||||
public function has(string $entryIdentifier): bool
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058486);
|
||||
}
|
||||
return $this->backend->has($entryIdentifier);
|
||||
}
|
||||
|
||||
public function remove(string $entryIdentifier): bool
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058495);
|
||||
}
|
||||
return $this->backend->remove($entryIdentifier);
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->backend->flush();
|
||||
}
|
||||
|
||||
public function flushByTags(array $tags): void
|
||||
{
|
||||
if (!$this->backend instanceof TaggableBackendInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057360);
|
||||
}
|
||||
}
|
||||
|
||||
$this->backend->flushByTags($tags);
|
||||
}
|
||||
|
||||
public function flushByTag(string $tag): void
|
||||
{
|
||||
if (!$this->backend instanceof TaggableBackendInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057359);
|
||||
}
|
||||
|
||||
$this->backend->flushByTag($tag);
|
||||
}
|
||||
|
||||
public function collectGarbage(): void
|
||||
{
|
||||
$this->backend->collectGarbage();
|
||||
}
|
||||
|
||||
public function isValidEntryIdentifier(string $identifier): bool
|
||||
{
|
||||
return preg_match(self::PATTERN_ENTRYIDENTIFIER, $identifier) === 1;
|
||||
}
|
||||
|
||||
public function isValidTag(string $tag): bool
|
||||
{
|
||||
return preg_match(self::PATTERN_TAG, $tag) === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\BackendInterface;
|
||||
|
||||
/**
|
||||
* Contract for a Cache (frontend)
|
||||
*/
|
||||
interface FrontendInterface
|
||||
{
|
||||
/**
|
||||
* Pattern an entry identifier must match.
|
||||
*/
|
||||
public const PATTERN_ENTRYIDENTIFIER = '/^[a-zA-Z0-9_%\\-&]{1,250}$/';
|
||||
|
||||
/**
|
||||
* Pattern a tag must match.
|
||||
*/
|
||||
public const PATTERN_TAG = '/^[a-zA-Z0-9_%\\-&]{1,250}$/';
|
||||
|
||||
/**
|
||||
* Returns this cache's identifier
|
||||
*
|
||||
* @return string The identifier for this cache
|
||||
*/
|
||||
public function getIdentifier(): string;
|
||||
|
||||
/**
|
||||
* Returns the backend used by this cache
|
||||
*
|
||||
* @return BackendInterface The backend used by this cache
|
||||
*/
|
||||
public function getBackend(): BackendInterface;
|
||||
|
||||
/**
|
||||
* Saves data in the cache.
|
||||
*
|
||||
* @param string $entryIdentifier Something which identifies the data - depends on concrete cache
|
||||
* @param mixed $data The data to cache - also depends on the concrete cache implementation
|
||||
* @param array $tags Tags to associate with this cache entry
|
||||
* @param int|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void;
|
||||
|
||||
/**
|
||||
* Finds and returns data from the cache.
|
||||
*
|
||||
* @param string $entryIdentifier Something which identifies the cache entry - depends on concrete cache
|
||||
*/
|
||||
public function get(string $entryIdentifier): mixed;
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier specifying the cache entry
|
||||
* @return bool TRUE if such an entry exists, FALSE if not
|
||||
*/
|
||||
public function has(string $entryIdentifier): bool;
|
||||
|
||||
/**
|
||||
* Removes the given cache entry from the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier specifying the cache entry
|
||||
* @return bool TRUE if such an entry exists, FALSE if not
|
||||
*/
|
||||
public function remove(string $entryIdentifier): bool;
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush(): void;
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged by the specified tag.
|
||||
*
|
||||
* @param string $tag The tag the entries must have
|
||||
*/
|
||||
public function flushByTag(string $tag): void;
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged by any of the specified tags.
|
||||
*
|
||||
* @param string[] $tags List of tags
|
||||
*/
|
||||
public function flushByTags(array $tags): void;
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage(): void;
|
||||
|
||||
/**
|
||||
* Checks the validity of an entry identifier. Returns TRUE if it's valid.
|
||||
*
|
||||
* @param string $identifier An identifier to be checked for validity
|
||||
*/
|
||||
public function isValidEntryIdentifier(string $identifier): bool;
|
||||
|
||||
/**
|
||||
* Checks the validity of a tag. Returns TRUE if it's valid.
|
||||
*
|
||||
* @param string $tag A tag to be checked for validity
|
||||
*/
|
||||
public function isValidTag(string $tag): bool;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\NullBackend;
|
||||
|
||||
/**
|
||||
* This class only acts as shortcut to construct a cache frontend with a null backend.
|
||||
* It extends PhpFrontend to be sure it can also be used for all types of caches (also the one requiring a PhpFrontend like "core").
|
||||
*
|
||||
* @todo: Instead a factory class should be introduced that replaces this class and \TYPO3\CMS\Core\Core\Bootstrap::createCache
|
||||
*/
|
||||
class NullFrontend extends PhpFrontend
|
||||
{
|
||||
public function __construct(string $identifier)
|
||||
{
|
||||
$backend = new NullBackend();
|
||||
parent::__construct($identifier, $backend);
|
||||
}
|
||||
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\PhpCapableBackendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
|
||||
/**
|
||||
* A cache frontend tailored to PHP code.
|
||||
*/
|
||||
class PhpFrontend extends AbstractFrontend
|
||||
{
|
||||
public function __construct(string $identifier, PhpCapableBackendInterface $backend)
|
||||
{
|
||||
parent::__construct($identifier, $backend);
|
||||
}
|
||||
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1264023823);
|
||||
}
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException('The given source code is not a valid string.', 1264023824);
|
||||
}
|
||||
foreach ($tags as $tag) {
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1264023825);
|
||||
}
|
||||
}
|
||||
$sourceCode = '<?php' . LF . $data . LF . '#';
|
||||
$this->backend->set($entryIdentifier, $sourceCode, $tags, $lifetime);
|
||||
}
|
||||
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057753);
|
||||
}
|
||||
return $this->backend->get($entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PHP code from the cache and require_onces it right away.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed Potential return value from the include operation
|
||||
*/
|
||||
public function requireOnce(string $entryIdentifier): mixed
|
||||
{
|
||||
$backend = $this->getBackend();
|
||||
if (!($backend instanceof PhpCapableBackendInterface)) {
|
||||
throw new \RuntimeException('Can not require: Not a PhpCapableBackendInterface', 1763660480);
|
||||
}
|
||||
return $backend->requireOnce($entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PHP code from the cache and require() it right away. Note require()
|
||||
* in comparison to requireOnce() is only "safe" if the cache entry only contain stuff
|
||||
* that can be required multiple times during one request. For instance a class definition
|
||||
* would fail here.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed Potential return value from the include operation
|
||||
*/
|
||||
public function require(string $entryIdentifier): mixed
|
||||
{
|
||||
$backend = $this->getBackend();
|
||||
if (!($backend instanceof PhpCapableBackendInterface)) {
|
||||
throw new \RuntimeException('Can not require: Not a PhpCapableBackendInterface', 1763660481);
|
||||
}
|
||||
return $backend->require($entryIdentifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\TransientBackendInterface;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Serializer\AuthenticatedMessageDeserializer;
|
||||
use TYPO3\CMS\Core\Serializer\DeserializationService;
|
||||
use TYPO3\CMS\Core\Serializer\Exception\DeserializerException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A cache frontend for any kinds of PHP variables
|
||||
*/
|
||||
class VariableFrontend extends AbstractFrontend
|
||||
{
|
||||
/**
|
||||
* Saves the value of a PHP variable in the cache. Note that the variable
|
||||
* will be serialized if necessary.
|
||||
*
|
||||
* @param int|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
*/
|
||||
public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'"' . $entryIdentifier . '" is not a valid cache entry identifier.',
|
||||
1233058264
|
||||
);
|
||||
}
|
||||
foreach ($tags as $tag) {
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233058269);
|
||||
}
|
||||
}
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/cache/frontend/class.t3lib_cache_frontend_variablefrontend.php']['set'] ?? [] as $_funcRef) {
|
||||
$params = [
|
||||
'entryIdentifier' => &$entryIdentifier,
|
||||
'variable' => &$data,
|
||||
'tags' => &$tags,
|
||||
'lifetime' => &$lifetime,
|
||||
];
|
||||
GeneralUtility::callUserFunction($_funcRef, $params, $this);
|
||||
}
|
||||
if (!$this->backend instanceof TransientBackendInterface) {
|
||||
// No DI/GeneralUtility::makeInstance usage, since caching needs to operate prior to DI container setup.
|
||||
$deserializer = new AuthenticatedMessageDeserializer(new HashService(), new DeserializationService());
|
||||
$data = $deserializer->serialize($data, VariableFrontend::class);
|
||||
}
|
||||
$this->backend->set($entryIdentifier, $data, $tags, $lifetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns a variable value from the cache.
|
||||
*/
|
||||
public function get(string $entryIdentifier): mixed
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'"' . $entryIdentifier . '" is not a valid cache entry identifier.',
|
||||
1233058294
|
||||
);
|
||||
}
|
||||
$rawResult = $this->backend->get($entryIdentifier);
|
||||
if ($rawResult === false) {
|
||||
return false;
|
||||
}
|
||||
if ($this->backend instanceof TransientBackendInterface) {
|
||||
return $rawResult;
|
||||
}
|
||||
try {
|
||||
// No DI/GeneralUtility::makeInstance usage, since caching needs to operate prior to DI container setup.
|
||||
$deserializer = new AuthenticatedMessageDeserializer(new HashService(), new DeserializationService());
|
||||
return $deserializer->deserialize($rawResult, VariableFrontend::class);
|
||||
} catch (DeserializerException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Category\Collection;
|
||||
|
||||
use TYPO3\CMS\Core\Collection\AbstractRecordCollection;
|
||||
use TYPO3\CMS\Core\Collection\CollectionInterface;
|
||||
use TYPO3\CMS\Core\Collection\EditableCollectionInterface;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Category Collection to handle records attached to a category
|
||||
*/
|
||||
class CategoryCollection extends AbstractRecordCollection implements EditableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* The table name collections are stored to
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $storageTableName = 'sys_category';
|
||||
|
||||
/**
|
||||
* Name of the categories-relation field (used in the MM_match_fields/fieldname property of the TCA)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $relationFieldName = 'categories';
|
||||
|
||||
/**
|
||||
* Creates this object.
|
||||
*
|
||||
* @param string $tableName Name of the table to be working on
|
||||
* @param string $fieldName Name of the field where the categories relations are defined
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function __construct($tableName = null, $fieldName = null)
|
||||
{
|
||||
parent::__construct();
|
||||
if (!empty($tableName)) {
|
||||
$this->setItemTableName($tableName);
|
||||
} elseif (empty($this->itemTableName)) {
|
||||
throw new \RuntimeException(self::class . ' needs a valid itemTableName.', 1341826168);
|
||||
}
|
||||
if (!empty($fieldName)) {
|
||||
$this->setRelationFieldName($fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new collection objects and reconstitutes the
|
||||
* given database record to the new object.
|
||||
*
|
||||
* @param array $collectionRecord Database record
|
||||
* @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections
|
||||
* @return CategoryCollection
|
||||
*/
|
||||
public static function create(array $collectionRecord, $fillItems = false)
|
||||
{
|
||||
$collection = GeneralUtility::makeInstance(
|
||||
self::class,
|
||||
$collectionRecord['table_name'],
|
||||
$collectionRecord['field_name']
|
||||
);
|
||||
$collection->fromArray($collectionRecord);
|
||||
if ($fillItems) {
|
||||
$collection->loadContents();
|
||||
}
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the collections with the given id from persistence
|
||||
* For memory reasons, per default only f.e. title, database-table,
|
||||
* identifier (what ever static data is defined) is loaded.
|
||||
* Entries can be load on first access.
|
||||
*
|
||||
* @param int $id Id of database record to be loaded
|
||||
* @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections
|
||||
* @param string $tableName Name of table from which entries should be loaded
|
||||
* @param string $fieldName Name of the categories relation field
|
||||
* @return CategoryCollection
|
||||
*/
|
||||
public static function load($id, $fillItems = false, $tableName = '', $fieldName = '')
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable(static::$storageTableName);
|
||||
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
|
||||
$collectionRecord = $queryBuilder->select('*')
|
||||
->from(static::$storageTableName)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, Connection::PARAM_INT))
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
|
||||
if ($collectionRecord === false) {
|
||||
return GeneralUtility::makeInstance(
|
||||
self::class,
|
||||
$tableName,
|
||||
$fieldName
|
||||
);
|
||||
}
|
||||
|
||||
$collectionRecord['table_name'] = $tableName;
|
||||
$collectionRecord['field_name'] = $fieldName;
|
||||
|
||||
return self::create($collectionRecord, $fillItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the collected records in this collection, by
|
||||
* looking up the MM relations of this record to the
|
||||
* table name defined in the local field 'table_name'.
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function getCollectedRecordsQueryBuilder()
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable(static::$storageTableName);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
|
||||
$queryBuilder->select($this->getItemTableName() . '.*')
|
||||
->from(static::$storageTableName)
|
||||
->join(
|
||||
static::$storageTableName,
|
||||
'sys_category_record_mm',
|
||||
'sys_category_record_mm',
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_category_record_mm.uid_local',
|
||||
$queryBuilder->quoteIdentifier(static::$storageTableName . '.uid')
|
||||
)
|
||||
)
|
||||
->join(
|
||||
'sys_category_record_mm',
|
||||
$this->getItemTableName(),
|
||||
$this->getItemTableName(),
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_category_record_mm.uid_foreign',
|
||||
$queryBuilder->quoteIdentifier($this->getItemTableName() . '.uid')
|
||||
)
|
||||
)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
static::$storageTableName . '.uid',
|
||||
$queryBuilder->createNamedParameter($this->getIdentifier(), Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_category_record_mm.tablenames',
|
||||
$queryBuilder->createNamedParameter($this->getItemTableName())
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_category_record_mm.fieldname',
|
||||
$queryBuilder->createNamedParameter($this->getRelationFieldName())
|
||||
)
|
||||
)
|
||||
// Add required sorting field.
|
||||
->orderBy('sys_category_record_mm.sorting', 'ASC')
|
||||
// Add foreign uid field to ensure determistic sorting across dbms and dbms versions
|
||||
->addOrderBy('sys_category_record_mm.uid_foreign', 'ASC')
|
||||
;
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the collected records in this collection, by
|
||||
* using <getCollectedRecordsQueryBuilder>.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getCollectedRecords()
|
||||
{
|
||||
$relatedRecords = [];
|
||||
|
||||
$queryBuilder = $this->getCollectedRecordsQueryBuilder();
|
||||
$result = $queryBuilder->executeQuery();
|
||||
|
||||
while ($record = $result->fetchAssociative()) {
|
||||
$relatedRecords[] = $record;
|
||||
}
|
||||
|
||||
return $relatedRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the content-entries of the storage
|
||||
* Queries the underlying storage for entries of the collection
|
||||
* and adds them to the collection data.
|
||||
* If the content entries of the storage had not been loaded on creation
|
||||
* ($fillItems = false) this function is to be used for loading the contents
|
||||
* afterwards.
|
||||
*/
|
||||
public function loadContents()
|
||||
{
|
||||
$entries = $this->getCollectedRecords();
|
||||
$this->removeAll();
|
||||
foreach ($entries as $entry) {
|
||||
$this->add($entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of the persistable properties and contents
|
||||
* which are processable by DataHandler.
|
||||
* for internal usage in persist only.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPersistableDataArray()
|
||||
{
|
||||
return [
|
||||
'title' => $this->getTitle(),
|
||||
'description' => $this->getDescription(),
|
||||
'items' => $this->getItemUidList(true),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds on entry to the collection
|
||||
*
|
||||
* @param mixed $data
|
||||
*/
|
||||
public function add($data)
|
||||
{
|
||||
$this->storage->push($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of entries to the collection
|
||||
*/
|
||||
public function addAll(CollectionInterface $other)
|
||||
{
|
||||
foreach ($other as $value) {
|
||||
$this->add($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given entry from collection
|
||||
* Note: not the given "index"
|
||||
*
|
||||
* @param mixed $data
|
||||
*/
|
||||
public function remove($data)
|
||||
{
|
||||
$offset = 0;
|
||||
foreach ($this->storage as $value) {
|
||||
if ($value == $data) {
|
||||
break;
|
||||
}
|
||||
$offset++;
|
||||
}
|
||||
$this->storage->offsetUnset($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries from the collection
|
||||
* collection will be empty afterwards
|
||||
*/
|
||||
public function removeAll()
|
||||
{
|
||||
$this->storage = new \SplDoublyLinkedList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current available items.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getItems()
|
||||
{
|
||||
$itemArray = [];
|
||||
/** @var File $item */
|
||||
foreach ($this->storage as $item) {
|
||||
$itemArray[] = $item;
|
||||
}
|
||||
return $itemArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the categories relation field
|
||||
*
|
||||
* @param string $field
|
||||
*/
|
||||
public function setRelationFieldName($field)
|
||||
{
|
||||
$this->relationFieldName = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the categories relation field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationFieldName()
|
||||
{
|
||||
return $this->relationFieldName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the storage table name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStorageTableName()
|
||||
{
|
||||
return self::$storageTableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the storage items field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStorageItemsField()
|
||||
{
|
||||
return self::$storageItemsField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Charset;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
|
||||
/**
|
||||
* Class for conversion between charsets
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class CharsetConverter
|
||||
{
|
||||
/**
|
||||
* Fallback character for chars with no equivalent.
|
||||
*/
|
||||
protected const FALLBACK_CHAR = '?';
|
||||
|
||||
public function __construct(
|
||||
private CharsetProvider $charsetProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Converts all chars in the input UTF-8 string into integer numbers returned in an array.
|
||||
* All HTML entities (like & or £ or { or 㽝) will be detected as characters.
|
||||
* Also, instead of integer numbers the real UTF-8 char is returned.
|
||||
*
|
||||
* @param string $str Input string, UTF-8
|
||||
* @return array Output array with the char numbers
|
||||
*/
|
||||
public function utf8_to_numberarray(string $str): array
|
||||
{
|
||||
// Entities must be registered as well
|
||||
$str = html_entity_decode($str, ENT_COMPAT, 'utf-8');
|
||||
|
||||
// Do conversion:
|
||||
$strLen = strlen($str);
|
||||
$outArr = [];
|
||||
// Traverse each char in UTF-8 string.
|
||||
for ($a = 0; $a < $strLen; $a++) {
|
||||
$chr = $str[$a];
|
||||
$ord = ord($chr);
|
||||
// This means multibyte! (first byte!)
|
||||
if ($ord > 127) {
|
||||
// Since the first byte must have the 7th bit set we check that. Otherwise, we might be in the middle of a byte sequence.
|
||||
if ($ord & 64) {
|
||||
// Add first byte
|
||||
$buf = $chr;
|
||||
// For each byte in multibyte string...
|
||||
for ($b = 0; $b < 8; $b++) {
|
||||
// Shift it left and ...
|
||||
$ord <<= 1;
|
||||
// ... and with 8th bit - if that is set, then there are still bytes in sequence.
|
||||
if ($ord & 128) {
|
||||
$a++;
|
||||
// ... and add the next char.
|
||||
$buf .= $str[$a];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$outArr[] = $buf;
|
||||
} else {
|
||||
$outArr[] = self::FALLBACK_CHAR;
|
||||
}
|
||||
} else {
|
||||
$outArr[] = chr($ord);
|
||||
}
|
||||
}
|
||||
return $outArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UTF-8 Multibyte character to a UNICODE number
|
||||
* Unit-tested by Kasper
|
||||
*
|
||||
* @param string $str UTF-8 multibyte character string
|
||||
* @param bool $hex If set, then a hex. number is returned.
|
||||
* @return ($hex is false ? int : string) UNICODE integer
|
||||
*/
|
||||
public function utf8CharToUnumber(string $str, bool $hex = false): int|string
|
||||
{
|
||||
// First char
|
||||
$ord = ord($str[0]);
|
||||
// This verifies that it IS a multibyte string
|
||||
if (($ord & 192) === 192) {
|
||||
$binBuf = '';
|
||||
$b = 0;
|
||||
// For each byte in multibyte string...
|
||||
for (; $b < 8; $b++) {
|
||||
// Shift it left and ...
|
||||
$ord <<= 1;
|
||||
// ... and with 8th bit - if that is set, then there are still bytes in sequence.
|
||||
if ($ord & 128) {
|
||||
$binBuf .= substr('00000000' . decbin(ord($str[$b + 1])), -6);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$binBuf = substr('00000000' . decbin(ord($str[0])), -(6 - $b)) . $binBuf;
|
||||
$int = bindec($binBuf);
|
||||
} else {
|
||||
$int = $ord;
|
||||
}
|
||||
return $hex ? 'x' . dechex((int)$int) : $int;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps all characters of a UTF-8 string.
|
||||
*
|
||||
* @param string $str UTF-8 string
|
||||
*/
|
||||
public function utf8_char_mapping(string $str): string
|
||||
{
|
||||
$out = '';
|
||||
for ($i = 0; isset($str[$i]); $i++) {
|
||||
$c = ord($str[$i]);
|
||||
$mbc = '';
|
||||
// single-byte (0xxxxxx)
|
||||
if (!($c & 128)) {
|
||||
$mbc = $str[$i];
|
||||
} elseif (($c & 192) === 192) {
|
||||
$bc = 0;
|
||||
// multibyte starting byte (11xxxxxx)
|
||||
for (; $c & 128; $c <<= 1) {
|
||||
$bc++;
|
||||
}
|
||||
// calculate number of bytes
|
||||
$mbc = substr($str, $i, $bc);
|
||||
$i += $bc - 1;
|
||||
}
|
||||
if ($this->charsetProvider->hasMultibyteChar('utf-8', $mbc)) {
|
||||
$out .= $this->charsetProvider->getByMultibyteChar('utf-8', $mbc);
|
||||
} else {
|
||||
$out .= $mbc;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Abstract implementation of a RecordCollection
|
||||
*
|
||||
* RecordCollection is a collections of TCA-Records.
|
||||
* The collection is meant to be stored in TCA-table sys_file_collections and is manageable
|
||||
* via FormEngine.
|
||||
*
|
||||
* A RecordCollection might be used to group a set of records (e.g. news, images, contentElements)
|
||||
* for output in frontend
|
||||
*
|
||||
* The AbstractRecordCollection uses SplDoublyLinkedList for internal storage
|
||||
*
|
||||
* @template T
|
||||
* @implements RecordCollectionInterface<T>
|
||||
*/
|
||||
abstract class AbstractRecordCollection implements RecordCollectionInterface, PersistableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* The table name collections are stored to
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $storageItemsField = 'items';
|
||||
|
||||
/**
|
||||
* The table name collections are stored to, must be defined in the subclass
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $storageTableName = '';
|
||||
|
||||
/**
|
||||
* Uid of the storage
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $uid = 0;
|
||||
|
||||
/**
|
||||
* Collection title
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title;
|
||||
|
||||
/**
|
||||
* Collection description
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description;
|
||||
|
||||
/**
|
||||
* Table name of the records stored in this collection
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $itemTableName;
|
||||
|
||||
/**
|
||||
* The local storage
|
||||
*
|
||||
* @var \SplDoublyLinkedList
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Creates this object.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->storage = new \SplDoublyLinkedList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
*
|
||||
* @return T|null
|
||||
*/
|
||||
public function current(): mixed
|
||||
{
|
||||
return $this->storage->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
*/
|
||||
public function next(): void
|
||||
{
|
||||
$this->storage->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
*
|
||||
* @return int|string 0 on failure.
|
||||
*/
|
||||
public function key(): mixed
|
||||
{
|
||||
$currentRecord = $this->storage->current();
|
||||
return $currentRecord['uid'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
*
|
||||
* @return bool The return value will be cast to boolean and then evaluated.
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->storage->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->storage->rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns class state to be serialized.
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
'uid' => $this->getIdentifier(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load records with the given serialized information
|
||||
*/
|
||||
public function __unserialize(array $arrayRepresentation): void
|
||||
{
|
||||
self::load($arrayRepresentation['uid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count elements of an object
|
||||
*
|
||||
* @return int The custom count as an integer.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->storage->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the UID
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUid()
|
||||
{
|
||||
return $this->uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the title
|
||||
*
|
||||
* @param string $title
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the description
|
||||
*
|
||||
* @param string $desc
|
||||
*/
|
||||
public function setDescription($desc)
|
||||
{
|
||||
$this->description = $desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the name of the data-source table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getItemTableName()
|
||||
{
|
||||
return $this->itemTableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the name of the data-source table
|
||||
*
|
||||
* @param string $tableName
|
||||
*/
|
||||
public function setItemTableName($tableName)
|
||||
{
|
||||
$this->itemTableName = $tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uid of the collection
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIdentifier()
|
||||
{
|
||||
return $this->uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the identifier of the collection
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function setIdentifier($id)
|
||||
{
|
||||
$this->uid = (int)$id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the collections with the given id from persistence
|
||||
*
|
||||
* For memory reasons, per default only f.e. title, database-table,
|
||||
* identifier (what ever static data is defined) is loaded.
|
||||
* Entries can be load on first access.
|
||||
*
|
||||
* @param int $id Id of database record to be loaded
|
||||
* @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections
|
||||
* @return CollectionInterface
|
||||
*/
|
||||
public static function load($id, $fillItems = false)
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(static::getCollectionDatabaseTable());
|
||||
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$collectionRecord = $queryBuilder->select('*')
|
||||
->from(static::getCollectionDatabaseTable())
|
||||
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)))
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
return self::create($collectionRecord ?: [], $fillItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new collection objects and reconstitutes the
|
||||
* given database record to the new object.
|
||||
*
|
||||
* @param array $collectionRecord Database record
|
||||
* @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections
|
||||
* @return CollectionInterface
|
||||
*/
|
||||
public static function create(array $collectionRecord, $fillItems = false)
|
||||
{
|
||||
// [phpstan] Unsafe usage of new static()
|
||||
// todo: Either mark this class or its constructor final or use new self instead.
|
||||
$collection = new static();
|
||||
$collection->fromArray($collectionRecord);
|
||||
if ($fillItems) {
|
||||
$collection->loadContents();
|
||||
}
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists current collection state to underlying storage
|
||||
*/
|
||||
public function persist()
|
||||
{
|
||||
$uid = $this->getIdentifier() == 0 ? 'NEW' . random_int(100000, 999999) : $this->getIdentifier();
|
||||
$data = [
|
||||
trim(static::getCollectionDatabaseTable()) => [
|
||||
$uid => $this->getPersistableDataArray(),
|
||||
],
|
||||
];
|
||||
// New records always must have a pid
|
||||
if ($this->getIdentifier() == 0) {
|
||||
$data[trim(static::getCollectionDatabaseTable())][$uid]['pid'] = 0;
|
||||
}
|
||||
$tce = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$tce->start($data, []);
|
||||
$tce->process_datamap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of the persistable properties and contents
|
||||
* which are processable by DataHandler.
|
||||
*
|
||||
* For internal usage in persist only.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function getPersistableDataArray();
|
||||
|
||||
/**
|
||||
* Generates comma-separated list of entry uids for usage in DataHandler
|
||||
*
|
||||
* also allow to add table name, if it might be needed by DataHandler for
|
||||
* storing the relation
|
||||
*
|
||||
* @param bool $includeTableName
|
||||
* @return string
|
||||
*/
|
||||
protected function getItemUidList($includeTableName = true)
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->storage as $entry) {
|
||||
$list[] = ($includeTableName ? $this->getItemTableName() . '_' : '') . $entry['uid'];
|
||||
}
|
||||
return implode(',', $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array representation of this collection
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$itemArray = [];
|
||||
foreach ($this->storage as $item) {
|
||||
$itemArray[] = $item;
|
||||
}
|
||||
return [
|
||||
'uid' => $this->getIdentifier(),
|
||||
'title' => $this->getTitle(),
|
||||
'description' => $this->getDescription(),
|
||||
'table_name' => $this->getItemTableName(),
|
||||
'items' => $itemArray,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the properties of this collection from an array
|
||||
*/
|
||||
public function fromArray(array $array)
|
||||
{
|
||||
$this->uid = $array['uid'];
|
||||
$this->title = $array['title'];
|
||||
$this->description = $array['description'];
|
||||
$this->itemTableName = $array['table_name'];
|
||||
}
|
||||
|
||||
protected static function getCollectionDatabaseTable(): string
|
||||
{
|
||||
if (!empty(static::$storageTableName)) {
|
||||
return static::$storageTableName;
|
||||
}
|
||||
throw new \RuntimeException('No storage table name was defined the class "' . static::class . '".', 1592207959);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/**
|
||||
* Marker interface for collection classes
|
||||
*
|
||||
* Collections are containers-classes handling the storage
|
||||
* of data values (f.e. strings, records, relations) in a
|
||||
* common and generic way, while the class manages the storage
|
||||
* in an appropriate way itself
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
interface CollectionInterface extends \Iterator, \Countable {}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/**
|
||||
* Interface for collection classes which es enabled to be modified
|
||||
*/
|
||||
interface EditableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* Adds on entry to the collection
|
||||
*
|
||||
* @param mixed $data
|
||||
*/
|
||||
public function add($data);
|
||||
|
||||
/**
|
||||
* Adds a set of entries to the collection
|
||||
*/
|
||||
public function addAll(CollectionInterface $other);
|
||||
|
||||
/**
|
||||
* Remove the given entry from collection
|
||||
*
|
||||
* Note: not the given "index"
|
||||
*
|
||||
* @param mixed $data
|
||||
*/
|
||||
public function remove($data);
|
||||
|
||||
/**
|
||||
* Removes all entries from the collection
|
||||
*
|
||||
* collection will be empty afterwards
|
||||
*/
|
||||
public function removeAll();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
use TYPO3\CMS\Core\Domain\RecordInterface;
|
||||
|
||||
/**
|
||||
* When first accessed, this class will initialize itself and find the relations
|
||||
* for this record field.
|
||||
*
|
||||
* This class acts as a "Value holder", as it only fetches the related records
|
||||
* when needed.
|
||||
*
|
||||
* @todo: Evaluate if we should use a Ghost object instead.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
class LazyRecordCollection implements \IteratorAggregate, \ArrayAccess, \Countable
|
||||
{
|
||||
/**
|
||||
* @var RecordInterface[]|\Closure
|
||||
*/
|
||||
private array|\Closure $items;
|
||||
|
||||
public function __construct(
|
||||
private readonly mixed $fieldValue,
|
||||
\Closure $initialization
|
||||
) {
|
||||
$this->items = $initialization;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
$this->initialize();
|
||||
return count($this->items);
|
||||
}
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
if ($this->items instanceof \Closure) {
|
||||
$this->items = ($this->items)();
|
||||
}
|
||||
}
|
||||
|
||||
public function getIterator(): \Iterator
|
||||
{
|
||||
$this->initialize();
|
||||
return new \ArrayIterator($this->items);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string)$this->fieldValue;
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
$this->initialize();
|
||||
return isset($this->items[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
$this->initialize();
|
||||
return $this->items[$offset] ?? null;
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
if ($value instanceof RecordInterface === false) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Modifying the record collection is only allowed by setting a value of type RecordInterface.',
|
||||
1723188315
|
||||
);
|
||||
}
|
||||
$this->items[$offset] = $value;
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
throw new \RuntimeException('Removing items from the record collection is not implemented.', 1723188316);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/**
|
||||
* Marker interface for a collection class with title and description
|
||||
*
|
||||
* Collections might be used internally as well as being shown
|
||||
* with the nameable interface a title and a description are added
|
||||
* to a collection, allowing every collection implementing Nameable
|
||||
* being displayed by the same logic.
|
||||
*/
|
||||
interface NameableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* Setter for the title
|
||||
*
|
||||
* @param string $title
|
||||
*/
|
||||
public function setTitle($title);
|
||||
|
||||
/**
|
||||
* Setter for the description
|
||||
*
|
||||
* @param string $description
|
||||
*/
|
||||
public function setDescription($description);
|
||||
|
||||
/**
|
||||
* Getter for the title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle();
|
||||
|
||||
/**
|
||||
* Getter for the description
|
||||
*/
|
||||
public function getDescription();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/**
|
||||
* Interface for collection class being persistable
|
||||
*
|
||||
* Collections are containers-classes handling the storage
|
||||
* of data values (f.e. strings, records, relations) in a
|
||||
* common and generic way, while the class manages the storage
|
||||
* in an appropriate way itself
|
||||
*/
|
||||
interface PersistableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* Get the identifier of the collection
|
||||
*
|
||||
* For database stored collections, this will be an integer,
|
||||
* session stored, registry stored or other collections might
|
||||
* use a string as well
|
||||
*
|
||||
* @return int|string
|
||||
*/
|
||||
public function getIdentifier();
|
||||
|
||||
/**
|
||||
* Sets the identifier of the collection
|
||||
*
|
||||
* @param int|string $id
|
||||
*/
|
||||
public function setIdentifier($id);
|
||||
|
||||
/**
|
||||
* Loads the collections with the given id from persistence
|
||||
*
|
||||
* For memory reasons, per default only f.e. title, database-table,
|
||||
* identifier (what ever static data is defined) is loaded.
|
||||
* Entries can be load on first access.
|
||||
*
|
||||
* @param int|string $id
|
||||
* @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections
|
||||
* @return \TYPO3\CMS\Core\Collection\CollectionInterface
|
||||
*/
|
||||
public static function load($id, $fillItems = false);
|
||||
|
||||
/**
|
||||
* Persists current collection state to underlying storage
|
||||
*/
|
||||
public function persist();
|
||||
|
||||
/**
|
||||
* Populates the content-entries of the storage
|
||||
*
|
||||
* Queries the underlying storage for entries of the collection
|
||||
* and adds them to the collection data.
|
||||
*
|
||||
* If the content entries of the storage had not been loaded on creation
|
||||
* ($fillItems = false) this function is to be used for loading the contents
|
||||
* afterwards.
|
||||
*/
|
||||
public function loadContents();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/**
|
||||
* Collection for handling records from a single database-table.
|
||||
*
|
||||
* @template T
|
||||
* @extends CollectionInterface<T>
|
||||
*/
|
||||
interface RecordCollectionInterface extends CollectionInterface, NameableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* Setter for the name of the data-source table
|
||||
*
|
||||
* @param string $tableName
|
||||
*/
|
||||
public function setItemTableName($tableName);
|
||||
|
||||
/**
|
||||
* Setter for the name of the data-source table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getItemTableName();
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Command\Output\MessageRenderer;
|
||||
use TYPO3\CMS\Core\Core\BootService;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Install\Middleware\AssetPublishing;
|
||||
|
||||
class AssetPublishCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly BootService $bootService,
|
||||
protected readonly PackageManager $packageManager,
|
||||
protected readonly MessageRenderer $messageRenderer,
|
||||
) {
|
||||
parent::__construct('asset:publish');
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Publish public assets.');
|
||||
$this->setHelp(
|
||||
'Publishes public assets. '
|
||||
. 'Needs to be run after composer install.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$failsafeContainer = $this->bootService->getFailsafeContainer();
|
||||
$failsafeResourcePublisher = $failsafeContainer->has(AssetPublishing::class) ? $failsafeContainer->get(SystemResourcePublisherInterface::class) : null;
|
||||
try {
|
||||
$container = $this->bootService->loadExtLocalconfDatabase(false, false);
|
||||
} catch (\Throwable $e) {
|
||||
if ($output->isVerbose()) {
|
||||
throw $e;
|
||||
}
|
||||
$output->writeln('<error>Can not initialize dependency injection container. Increase verbosity to get the full error message.</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
$resourcePublisher = $container->get(SystemResourcePublisherInterface::class);
|
||||
|
||||
$output->getFormatter()->setStyle('bold', new OutputFormatterStyle(null, null, ['bold']));
|
||||
$output->writeln('<bold>Publishing assets from extensions…</bold>');
|
||||
|
||||
$exitCode = self::SUCCESS;
|
||||
foreach ($this->packageManager->getAvailablePackages() as $package) {
|
||||
$messages = $resourcePublisher->publishResources($package);
|
||||
if ($package->isPartOfMinimalUsableSystem()) {
|
||||
// Publish resources for install tool, if it is installed
|
||||
$failsafeResourcePublisher?->publishResources($package);
|
||||
}
|
||||
$exitCode = $this->determineExitCode($exitCode, $messages);
|
||||
$this->messageRenderer->renderAll($messages, $output);
|
||||
}
|
||||
|
||||
$output->writeln('<bold>done.</bold>');
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
private function determineExitCode(int $currentCode, FlashMessageQueue $queue): int
|
||||
{
|
||||
if ($currentCode === self::FAILURE) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
foreach ($queue->getAllMessages() as $message) {
|
||||
if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
return $currentCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Cache\Event\CacheFlushEvent;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Core\BootService;
|
||||
use TYPO3\CMS\Core\DependencyInjection\Cache\ContainerBackend;
|
||||
|
||||
class CacheFlushCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly BootService $bootService,
|
||||
protected readonly FrontendInterface $dependencyInjectionCache
|
||||
) {
|
||||
parent::__construct('cache:flush');
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Flush TYPO3 caches.');
|
||||
$this->setHelp(
|
||||
'Clears TYPO3 caches. '
|
||||
. 'Useful after code changes during development or after deployments. '
|
||||
. 'You can flush a specific cache group (system, pages, di) or all caches.'
|
||||
);
|
||||
$this->setDefinition([
|
||||
new InputOption('group', 'g', InputOption::VALUE_OPTIONAL, 'Cache group to flush (system, pages, di, or all).', 'all'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$group = $input->getOption('group') ?? 'all';
|
||||
|
||||
$this->flushDependencyInjectionCaches($group);
|
||||
if ($group === 'di') {
|
||||
if ($output->isVerbose()) {
|
||||
$io->success('Dependency Injection caches flushed.');
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$container = $this->bootService->getContainer(true);
|
||||
|
||||
$this->flushCoreCaches($group, $container);
|
||||
|
||||
$this->bootService->loadExtLocalconfDatabase(false, true);
|
||||
|
||||
$eventDispatcher = $container->get(EventDispatcherInterface::class);
|
||||
|
||||
$groups = $group === 'all' ? $container->get(CacheManager::class)->getCacheGroups() : [$group];
|
||||
$event = new CacheFlushEvent($groups);
|
||||
$eventDispatcher->dispatch($event);
|
||||
|
||||
if (count($event->getErrors()) > 0) {
|
||||
$io->error('Errors occurred while flushing caches.');
|
||||
foreach ($event->getErrors() as $error) {
|
||||
$io->error($error);
|
||||
}
|
||||
return Command::FAILURE;
|
||||
}
|
||||
if ($output->isVerbose()) {
|
||||
if ($group === 'all') {
|
||||
$io->success('All caches flushed.');
|
||||
} else {
|
||||
$io->success(sprintf('Caches for group "%s" flushed.', $group));
|
||||
}
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function flushDependencyInjectionCaches(string $group): void
|
||||
{
|
||||
if ($group !== 'di' && $group !== 'system' && $group !== 'all') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->dependencyInjectionCache->getBackend() instanceof ContainerBackend) {
|
||||
$diCacheBackend = $this->dependencyInjectionCache->getBackend();
|
||||
// We need to remove using the forceFlush method because the DI cache backend disables the flush method
|
||||
$diCacheBackend->forceFlush();
|
||||
}
|
||||
}
|
||||
|
||||
protected function flushCoreCaches(string $group, ContainerInterface $container): void
|
||||
{
|
||||
if ($group !== 'system' && $group !== 'all') {
|
||||
return;
|
||||
}
|
||||
|
||||
$container->get('cache.core')->flush();
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
#[AsCommand('cache:flushtags', 'Cache clearing caches with tags.')]
|
||||
class CacheFlushTagsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly CacheManager $cacheManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Flush TYPO3 caches with tags.');
|
||||
$this->setHelp('This command can be used to clear the caches with specific tags, for example after code updates in local development and after deployments.');
|
||||
$this->setDefinition([
|
||||
new InputArgument(
|
||||
'tags',
|
||||
InputArgument::REQUIRED,
|
||||
'Array of tags (specified as comma separated values) to flush.'
|
||||
),
|
||||
new InputOption(
|
||||
'groups',
|
||||
'g',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Array of groups (specified as comma separated values) for which to flush tags. If no group is specified, caches of all groups are flushed.',
|
||||
'all'
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$groups = GeneralUtility::trimExplode(',', $input->getOption('groups') ?? '', true);
|
||||
$tags = GeneralUtility::trimExplode(',', $input->getArgument('tags') ?? '', true);
|
||||
|
||||
foreach ($groups as $group) {
|
||||
if ($group === 'all') {
|
||||
$this->cacheManager->flushCachesByTags($tags);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->cacheManager->flushCachesInGroupByTags($group, $tags);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Configuration\Extension\ExtLocalconfFactory;
|
||||
use TYPO3\CMS\Core\Configuration\Tca\TcaFactory;
|
||||
use TYPO3\CMS\Core\Core\BootService;
|
||||
use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
|
||||
class CacheWarmupCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly ContainerBuilder $containerBuilder,
|
||||
protected readonly PackageManager $packageManager,
|
||||
protected readonly BootService $bootService,
|
||||
protected readonly FrontendInterface $dependencyInjectionCache
|
||||
) {
|
||||
parent::__construct('cache:warmup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Warmup TYPO3 caches.');
|
||||
$this->setHelp(
|
||||
<<<'EOF'
|
||||
This command is useful for deployments to warmup caches during release preparation.
|
||||
|
||||
<fg=yellow>
|
||||
Cache warming does not work if the PHP version used to execute the command differs from
|
||||
the PHP version used in the web context.
|
||||
|
||||
See: https://docs.typo3.org/permalink/changelog:important-107649-1760090777
|
||||
</>
|
||||
EOF
|
||||
);
|
||||
$this->setDefinition([
|
||||
new InputOption('group', 'g', InputOption::VALUE_OPTIONAL, 'The cache group to warmup (system, pages, di or all)', 'all'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$group = $input->getOption('group') ?? 'all';
|
||||
|
||||
if ($group === 'di' || $group === 'system' || $group === 'all') {
|
||||
$this->containerBuilder->warmupCache($this->packageManager, $this->dependencyInjectionCache);
|
||||
if ($group === 'di') {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$container = $this->bootService->getContainer();
|
||||
|
||||
$allowExtFileCaches = true;
|
||||
if ($group === 'system' || $group === 'all') {
|
||||
$allowExtFileCaches = false;
|
||||
$container->get(ExtLocalconfFactory::class)->createCacheEntry();
|
||||
}
|
||||
// Perform a full boot to load localconf (requirement for extensions and for TCA loading).
|
||||
$this->bootService->loadExtLocalconfDatabase(false, $allowExtFileCaches);
|
||||
if ($group === 'system' || $group === 'all') {
|
||||
$tcaFactory = $container->get(TcaFactory::class);
|
||||
$tcaFactory->createBaseTcaCacheFile($GLOBALS['TCA']);
|
||||
}
|
||||
|
||||
$eventDispatcher = $container->get(EventDispatcherInterface::class);
|
||||
|
||||
$groups = $group === 'all' ? $container->get(CacheManager::class)->getCacheGroups() : [$group];
|
||||
$event = new CacheWarmupEvent($groups);
|
||||
$eventDispatcher->dispatch($event);
|
||||
|
||||
if (count($event->getErrors()) > 0) {
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Logger\ConsoleLogger;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Messenger\EventListener\StopWorkerOnFailureLimitListener;
|
||||
use Symfony\Component\Messenger\EventListener\StopWorkerOnMemoryLimitListener;
|
||||
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
|
||||
use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Messenger\Transport\Sync\SyncTransport;
|
||||
use Symfony\Component\Messenger\Worker;
|
||||
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
|
||||
|
||||
/**
|
||||
* Almost full version of the symfony command with the same name.
|
||||
*/
|
||||
#[AsCommand(name: 'messenger:consume', description: 'Consume messages')]
|
||||
class ConsumeMessagesCommand extends Command
|
||||
{
|
||||
private const int DEFAULT_KEEPALIVE_INTERVAL = 5;
|
||||
|
||||
private ?Worker $worker = null;
|
||||
private ?LoggerInterface $logger = null;
|
||||
private array $receiverNames;
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(service: 'messenger.bus.default')]
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
#[AutowireLocator('messenger.receiver', indexAttribute: 'identifier')]
|
||||
private readonly ContainerInterface $receiverLocator,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly ListenerProvider $listenerProvider,
|
||||
private readonly ContainerInterface $container,
|
||||
#[AutowireIterator('messenger.receiver', indexAttribute: 'identifier')]
|
||||
iterable $receiverNamesIterator,
|
||||
private readonly array $busIds = [],
|
||||
#[AutowireLocator('messenger.rate_limiter', indexAttribute: 'identifier')]
|
||||
private readonly ?ContainerInterface $rateLimiterLocator = null,
|
||||
private readonly ?array $signals = null,
|
||||
) {
|
||||
$this->receiverNames = array_keys([...$receiverNamesIterator]);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$defaultReceiverName = count($this->receiverNames) === 1 ? current($this->receiverNames) : null;
|
||||
|
||||
$this
|
||||
->setDefinition(
|
||||
[
|
||||
new InputArgument(
|
||||
'receivers',
|
||||
InputArgument::IS_ARRAY,
|
||||
'Names of the receivers/transports to consume in order of priority',
|
||||
$defaultReceiverName ? [$defaultReceiverName] : []
|
||||
),
|
||||
new InputOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit the number of received messages'),
|
||||
new InputOption('failure-limit', 'f', InputOption::VALUE_REQUIRED, 'The number of failed messages the worker can consume'),
|
||||
new InputOption('memory-limit', 'm', InputOption::VALUE_REQUIRED, 'The memory limit the worker can consume'),
|
||||
new InputOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'The time limit in seconds the worker can handle new messages'),
|
||||
new InputOption('sleep', null, InputOption::VALUE_REQUIRED, 'Seconds to sleep before asking for new messages after no messages were found', 1),
|
||||
new InputOption('bus', 'b', InputOption::VALUE_REQUIRED, 'Name of the bus to which received messages should be dispatched (if not passed, bus is determined automatically)'),
|
||||
new InputOption('queues', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Limit receivers to only consume from the specified queues'),
|
||||
new InputOption('all', null, InputOption::VALUE_NONE, 'Consume messages from all receivers'),
|
||||
new InputOption('keepalive', null, InputOption::VALUE_OPTIONAL, 'Whether to use the transport\'s keepalive mechanism if implemented', self::DEFAULT_KEEPALIVE_INTERVAL),
|
||||
]
|
||||
)
|
||||
->setHelp(
|
||||
<<<'EOF'
|
||||
The <info>%command.name%</info> command consumes messages and dispatches them to the message bus.
|
||||
|
||||
<info>php %command.full_name% <receiver-name></info>
|
||||
|
||||
To receive from multiple transports, pass each name:
|
||||
|
||||
<info>php %command.full_name% receiver1 receiver2</info>
|
||||
|
||||
Use the --limit option to limit the number of messages received:
|
||||
|
||||
<info>php %command.full_name% <receiver-name> --limit=10</info>
|
||||
|
||||
Use the --failure-limit option to stop the worker when the given number of failed messages is reached:
|
||||
|
||||
<info>php %command.full_name% <receiver-name> --failure-limit=2</info>
|
||||
|
||||
Use the --memory-limit option to stop the worker if it exceeds a given memory usage limit. You can use shorthand byte values [K, M or G]:
|
||||
|
||||
<info>php %command.full_name% <receiver-name> --memory-limit=128M</info>
|
||||
|
||||
Use the --time-limit option to stop the worker when the given time limit (in seconds) is reached.
|
||||
If a message is being handled, the worker will stop after the processing is finished:
|
||||
|
||||
<info>php %command.full_name% <receiver-name> --time-limit=3600</info>
|
||||
|
||||
Use the --bus option to specify the message bus to dispatch received messages
|
||||
to instead of trying to determine it automatically. This is required if the
|
||||
messages didn't originate from Messenger:
|
||||
|
||||
<info>php %command.full_name% <receiver-name> --bus=event_bus</info>
|
||||
|
||||
Use the --queues option to limit a receiver to only certain queues (only supported by some receivers):
|
||||
|
||||
<info>php %command.full_name% <receiver-name> --queues=fasttrack</info>
|
||||
|
||||
Use the --all option to consume from all receivers:
|
||||
|
||||
<info>php %command.full_name% --all</info>
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
if ($input->hasParameterOption('--keepalive')) {
|
||||
$this->getApplication()->setAlarmInterval((int)($input->getOption('keepalive') ?? self::DEFAULT_KEEPALIVE_INTERVAL));
|
||||
}
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
|
||||
|
||||
if ($input->getOption('all')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->receiverNames && !$input->getArgument('receivers')) {
|
||||
if (count($this->receiverNames) === 1) {
|
||||
$input->setArgument('receivers', $this->receiverNames);
|
||||
return;
|
||||
}
|
||||
|
||||
$io->block('Which transports/receivers do you want to consume?', null, 'fg=white;bg=blue', ' ', true);
|
||||
|
||||
$io->writeln('Choose which receivers you want to consume messages from in order of priority.');
|
||||
$io->writeln(sprintf('Hint: to consume from multiple, use a list of their names, e.g. <comment>%s</comment>', implode(', ', $this->receiverNames)));
|
||||
|
||||
$question = new ChoiceQuestion('Select receivers to consume:', $this->receiverNames, 0);
|
||||
$question->setMultiselect(true);
|
||||
|
||||
$input->setArgument('receivers', $io->askQuestion($question));
|
||||
}
|
||||
|
||||
if (!$input->getArgument('receivers')) {
|
||||
throw new RuntimeException('Please pass at least one receiver.', 1605305001);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->logger = new ConsoleLogger($output);
|
||||
|
||||
$receivers = [];
|
||||
$rateLimiters = [];
|
||||
$receiverNames = $input->getOption('all') ? $this->receiverNames : $input->getArgument('receivers');
|
||||
foreach ($receiverNames as $receiverName) {
|
||||
if (!$this->receiverLocator->has($receiverName)) {
|
||||
$message = sprintf('The receiver "%s" does not exist.', $receiverName);
|
||||
if ($this->receiverNames) {
|
||||
$message .= sprintf(' Valid receivers are: %s.', implode(', ', $this->receiverNames));
|
||||
}
|
||||
throw new RuntimeException($message, 1605305002);
|
||||
}
|
||||
|
||||
$receiver = $this->receiverLocator->get($receiverName);
|
||||
if ($receiver instanceof SyncTransport) {
|
||||
$idx = array_search($receiverName, $receiverNames);
|
||||
unset($receiverNames[$idx]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$receivers[$receiverName] = $receiver;
|
||||
if ($this->rateLimiterLocator?->has($receiverName)) {
|
||||
$rateLimiters[$receiverName] = $this->rateLimiterLocator->get($receiverName);
|
||||
}
|
||||
}
|
||||
|
||||
$stopsWhen = [];
|
||||
if (null !== $limit = $input->getOption('limit')) {
|
||||
if (!is_numeric($limit) || $limit <= 0) {
|
||||
throw new InvalidOptionException(sprintf('Option "limit" must be a positive integer, "%s" passed.', $limit), 1605305003);
|
||||
}
|
||||
|
||||
$stopsWhen[] = "processed {$limit} messages";
|
||||
$this->addSubscriber(new StopWorkerOnMessageLimitListener((int)$limit, $this->logger));
|
||||
}
|
||||
|
||||
if ($failureLimit = $input->getOption('failure-limit')) {
|
||||
$stopsWhen[] = "reached {$failureLimit} failed messages";
|
||||
$this->addSubscriber(new StopWorkerOnFailureLimitListener((int)$failureLimit, $this->logger));
|
||||
}
|
||||
|
||||
if ($memoryLimit = $input->getOption('memory-limit')) {
|
||||
$stopsWhen[] = "exceeded {$memoryLimit} of memory";
|
||||
$this->addSubscriber(new StopWorkerOnMemoryLimitListener($this->convertToBytes($memoryLimit), $this->logger));
|
||||
}
|
||||
|
||||
if (null !== $timeLimit = $input->getOption('time-limit')) {
|
||||
if (!is_numeric($timeLimit) || $timeLimit <= 0) {
|
||||
throw new InvalidOptionException(sprintf('Option "time-limit" must be a positive integer, "%s" passed.', $timeLimit), 1605305004);
|
||||
}
|
||||
|
||||
$stopsWhen[] = "been running for {$timeLimit}s";
|
||||
$this->addSubscriber(new StopWorkerOnTimeLimitListener((int)$timeLimit, $this->logger));
|
||||
}
|
||||
|
||||
$stopsWhen[] = 'received a stop signal via the messenger:stop-workers command';
|
||||
|
||||
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
|
||||
$io->success(sprintf('Consuming messages from transport%s "%s".', count($receivers) > 1 ? 's' : '', implode(', ', $receiverNames)));
|
||||
|
||||
if ($stopsWhen) {
|
||||
$last = array_pop($stopsWhen);
|
||||
$stopsWhen = ($stopsWhen ? implode(', ', $stopsWhen) . ' or ' : '') . $last;
|
||||
$io->comment("The worker will automatically exit once it has {$stopsWhen}.");
|
||||
}
|
||||
|
||||
$io->comment('Quit the worker with CONTROL-C.');
|
||||
|
||||
if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$io->comment('Re-run the command with a -vv option to see logs about consumed messages.');
|
||||
}
|
||||
|
||||
$this->worker = new Worker($receivers, $this->messageBus, $this->eventDispatcher, $this->logger, $rateLimiters);
|
||||
$options = [
|
||||
'sleep' => $input->getOption('sleep') * 1000000,
|
||||
];
|
||||
if ($queues = $input->getOption('queues')) {
|
||||
$options['queues'] = $queues;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->worker->run($options);
|
||||
} finally {
|
||||
$this->worker = null;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestArgumentValuesFor('receivers')) {
|
||||
$suggestions->suggestValues(array_diff($this->receiverNames, array_diff($input->getArgument('receivers'), [$input->getCompletionValue()])));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('bus')) {
|
||||
$suggestions->suggestValues($this->busIds);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSubscribedSignals(): array
|
||||
{
|
||||
return $this->signals ?? (extension_loaded('pcntl') ? [SIGTERM, SIGINT, SIGQUIT, SIGALRM] : []);
|
||||
}
|
||||
|
||||
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
|
||||
{
|
||||
if (!$this->worker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (defined('SIGALRM') && $signal === SIGALRM) {
|
||||
$this->logger?->debug('Sending keepalive request.', ['transport_names' => $this->worker->getMetadata()->getTransportNames()]);
|
||||
|
||||
$this->worker->keepalive($this->getApplication()->getAlarmInterval());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->logger?->info('Received signal {signal}.', ['signal' => $signal, 'transport_names' => $this->worker->getMetadata()->getTransportNames()]);
|
||||
|
||||
$this->worker->stop();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function convertToBytes(string $memoryLimit): int
|
||||
{
|
||||
$memoryLimit = strtolower($memoryLimit);
|
||||
$max = ltrim($memoryLimit, '+');
|
||||
if (str_starts_with($max, '0x')) {
|
||||
$max = intval($max, 16);
|
||||
} elseif (str_starts_with($max, '0')) {
|
||||
$max = intval($max, 8);
|
||||
} else {
|
||||
$max = (float)$max;
|
||||
}
|
||||
|
||||
switch (substr(rtrim($memoryLimit, 'b'), -1)) {
|
||||
case 't': $max *= 1024;
|
||||
// no break
|
||||
case 'g': $max *= 1024;
|
||||
// no break
|
||||
case 'm': $max *= 1024;
|
||||
// no break
|
||||
case 'k': $max *= 1024;
|
||||
}
|
||||
|
||||
return (int)$max;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo: This method show be removed when we can add event subscribers dynamically.
|
||||
*/
|
||||
private function addSubscriber(EventSubscriberInterface $subscriber): void
|
||||
{
|
||||
$this->container->set($subscriber::class, $subscriber);
|
||||
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
|
||||
$this->listenerProvider->addListener(
|
||||
$eventName,
|
||||
$subscriber::class,
|
||||
is_string($params) ? $params : $params[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Descriptor\ApplicationDescription;
|
||||
use Symfony\Component\Console\Descriptor\TextDescriptor as SymfonyTextDescriptor;
|
||||
use Symfony\Component\Console\Helper\Helper;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use TYPO3\CMS\Core\Console\CommandRegistry;
|
||||
|
||||
/**
|
||||
* Text descriptor.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TextDescriptor extends SymfonyTextDescriptor
|
||||
{
|
||||
private CommandRegistry $commandRegistry;
|
||||
private bool $degraded;
|
||||
|
||||
public function __construct(CommandRegistry $commandRegistry, bool $degraded)
|
||||
{
|
||||
$this->commandRegistry = $commandRegistry;
|
||||
$this->degraded = $degraded;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function describeApplication(Application $application, array $options = []): void
|
||||
{
|
||||
$describedNamespace = $options['namespace'] ?? null;
|
||||
$rawOutput = $options['raw_text'] ?? false;
|
||||
|
||||
$commands = $this->commandRegistry->filter($describedNamespace);
|
||||
|
||||
if ($rawOutput) {
|
||||
$width = $this->getColumnWidth(['' => ['commands' => array_keys($commands)]]);
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$this->write(sprintf("%-{$width}s %s\n", $command['name'], strip_tags($command['description'] ?? '')), true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->degraded) {
|
||||
$this->write("<error>Failed to boot dependency injection, only lowlevel commands are available.</error>\n\n", true);
|
||||
}
|
||||
|
||||
$namespaces = $this->commandRegistry->getNamespaces();
|
||||
$help = $application->getHelp();
|
||||
if ($help !== '') {
|
||||
$this->write($help . "\n\n", true);
|
||||
}
|
||||
|
||||
$this->write("<comment>Usage:</comment>\n", true);
|
||||
$this->write(" command [options] [arguments]\n\n");
|
||||
|
||||
$this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()));
|
||||
|
||||
$this->write("\n\n");
|
||||
|
||||
if ($describedNamespace) {
|
||||
$this->write(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), true);
|
||||
$namespace = $namespaces[$describedNamespace] ?? [];
|
||||
$width = $this->getColumnWidth(['' => $namespace]);
|
||||
$this->describeNamespace($namespace, $commands, $width);
|
||||
} else {
|
||||
$this->write('<comment>Available commands:</comment>', true);
|
||||
// calculate max. width based on available commands per namespace
|
||||
$width = $this->getColumnWidth($namespaces);
|
||||
foreach ($namespaces as $namespace) {
|
||||
if ($namespace['id'] !== ApplicationDescription::GLOBAL_NAMESPACE) {
|
||||
$this->write("\n");
|
||||
$this->write(' <comment>' . $namespace['id'] . '</comment>', true);
|
||||
}
|
||||
$this->describeNamespace($namespace, $commands, $width);
|
||||
}
|
||||
}
|
||||
|
||||
$this->write("\n");
|
||||
|
||||
if ($this->degraded) {
|
||||
$this->write("\n<error>Failed to boot dependency injection, only lowlevel commands are available.</error>\n", true);
|
||||
}
|
||||
}
|
||||
|
||||
private function describeNamespace(array $namespace, array $commands, int $width): void
|
||||
{
|
||||
foreach ($namespace['commands'] as $name) {
|
||||
$this->write("\n");
|
||||
$spacingWidth = $width - Helper::length($name);
|
||||
$command = $commands[$name];
|
||||
|
||||
$aliases = count($command['aliases']) ? '[' . implode('|', $command['aliases']) . '] ' : '';
|
||||
$this->write(sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $aliases . $command['description']), true);
|
||||
}
|
||||
}
|
||||
|
||||
private function getColumnWidth(array $namespaces): int
|
||||
{
|
||||
$widths = [];
|
||||
foreach ($namespaces as $name => $namespace) {
|
||||
$widths[] = Helper::length($name);
|
||||
foreach ($namespace['commands'] as $commandName) {
|
||||
$widths[] = Helper::length($commandName);
|
||||
}
|
||||
}
|
||||
|
||||
return $widths ? max($widths) + 2 : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
|
||||
/**
|
||||
* Command for dumping the class-loading information.
|
||||
*/
|
||||
class DumpAutoloadCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('dumpautoload');
|
||||
$this->setDescription('Updates class loading information in non-composer mode.');
|
||||
$this->setHelp('This command is only needed during development. The extension manager takes care of creating or updating this info properly during extension (de-)activation.');
|
||||
$this->setAliases([
|
||||
'extensionmanager:extension:dumpclassloadinginformation',
|
||||
'extension:dumpclassloadinginformation',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This command is not needed in composer mode.
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return !Environment::isComposerMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the class loading information
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
ClassLoadingInformation::dumpClassLoadingInformation();
|
||||
$io->success('Class loading information has been updated.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception as Typo3InstallException;
|
||||
|
||||
/**
|
||||
* This exception is thrown in UpgradeWizardRunCommand, if a requested wizard exists but does not need to make any changes.
|
||||
*
|
||||
* @internal for use in UpgradeWizardRunCommand only and not part of public API.
|
||||
*/
|
||||
final class WizardDoesNotNeedToMakeChangesException extends Typo3InstallException {}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception as Typo3InstallException;
|
||||
|
||||
/**
|
||||
* This exception is thrown in the UpgradeWizardRunCommand, if a requested wizard is already marked as done.
|
||||
*
|
||||
* @internal for use in UpgradeWizardRunCommand only and not part of public API.
|
||||
*/
|
||||
final class WizardMarkedAsDoneException extends Typo3InstallException {}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception as Typo3InstallException;
|
||||
|
||||
/**
|
||||
* This exception is thrown in UpgradeWizardRunCommand, if a requested wizard could not be found.
|
||||
*
|
||||
* @internal for use in UpgradeWizardRunCommand only and not part of public API.
|
||||
*/
|
||||
final class WizardNotFoundException extends Typo3InstallException {}
|
||||
@@ -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\Core\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\FormatterHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Helper\TableCell;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
|
||||
/**
|
||||
* Command for listing all extensions known to the system.
|
||||
*
|
||||
* If the command is called with the verbose option, also shows the description of the package.
|
||||
*/
|
||||
#[AsCommand('extension:list', 'Shows the list of extensions available to the system.')]
|
||||
#[AsNonSchedulableCommand]
|
||||
class ExtensionListCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly PackageManager $packageManager)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'all',
|
||||
'a',
|
||||
InputOption::VALUE_NONE,
|
||||
'Also display currently inactive/uninstalled extensions.'
|
||||
)
|
||||
->addOption(
|
||||
'inactive',
|
||||
'i',
|
||||
InputOption::VALUE_NONE,
|
||||
'Only show inactive/uninstalled extensions available for installation.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the list of all extensions
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$onlyShowInactiveExtensions = $input->getOption('inactive');
|
||||
$showAlsoInactiveExtensions = $input->getOption('all');
|
||||
if ($onlyShowInactiveExtensions) {
|
||||
$packages = $this->packageManager->getAvailablePackages();
|
||||
$io->title('All inactive/currently uninstalled extensions');
|
||||
} elseif ($showAlsoInactiveExtensions) {
|
||||
$packages = $this->packageManager->getAvailablePackages();
|
||||
$io->title('All installed (= active) and available (= inactive/currently uninstalled) extensions');
|
||||
} else {
|
||||
$packages = $this->packageManager->getActivePackages();
|
||||
$io->title('All installed (= active) extensions');
|
||||
}
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([
|
||||
'Extension Key',
|
||||
'Version',
|
||||
'Type',
|
||||
'Status',
|
||||
]);
|
||||
$table->setColumnWidths([30, 10, 8, 6]);
|
||||
|
||||
/** @var FormatterHelper $formatter */
|
||||
$formatter = $this->getHelper('formatter');
|
||||
foreach ($packages as $package) {
|
||||
$isActivePackage = $this->packageManager->isPackageActive($package->getPackageKey());
|
||||
if (!$package->getPackageMetaData()->isExtensionType()) {
|
||||
continue;
|
||||
}
|
||||
// Do not show the package if it is active but we only want to see inactive packages
|
||||
if ($onlyShowInactiveExtensions && $isActivePackage) {
|
||||
continue;
|
||||
}
|
||||
$type = $package->getPackageMetaData()->isFrameworkType() ? 'System' : 'Local';
|
||||
// Ensure that the inactive extensions are shown as well
|
||||
if ($onlyShowInactiveExtensions || ($showAlsoInactiveExtensions && !$isActivePackage)) {
|
||||
$status = '<comment>inactive</comment>';
|
||||
} else {
|
||||
$status = '<info>active</info>';
|
||||
}
|
||||
|
||||
$table->addRow([$package->getPackageKey(), $package->getPackageMetaData()->getVersion(), $type, $status]);
|
||||
|
||||
// Also show the title of the extension, if verbose option is set
|
||||
if ($output->isVerbose()) {
|
||||
$title = (string)$package->getPackageMetaData()->getTitle();
|
||||
$table->addRow([new TableCell(' ' . $formatter->truncate($title, 80) . "\n\n", ['colspan' => 4])]);
|
||||
}
|
||||
}
|
||||
$table->render();
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Command;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Command\ListCommand as SymfonyListCommand;
|
||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Command\Descriptor\TextDescriptor;
|
||||
use TYPO3\CMS\Core\Console\CommandRegistry;
|
||||
use TYPO3\CMS\Core\Core\BootService;
|
||||
|
||||
/**
|
||||
* ListCommand displays the list of all available commands for the application.
|
||||
*/
|
||||
class ListCommand extends SymfonyListCommand
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly ContainerInterface $failsafeContainer,
|
||||
protected readonly BootService $bootService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$degraded = false;
|
||||
try {
|
||||
$container = $this->bootService->getContainer();
|
||||
} catch (\Throwable $e) {
|
||||
$container = $this->failsafeContainer;
|
||||
$degraded = true;
|
||||
}
|
||||
|
||||
$commandRegistry = $container->get(CommandRegistry::class);
|
||||
|
||||
$helper = new DescriptorHelper();
|
||||
$helper->register('txt', new TextDescriptor($commandRegistry, $degraded));
|
||||
$helper->describe($output, $this->getApplication(), [
|
||||
'format' => $input->getOption('format'),
|
||||
'raw_text' => $input->getOption('raw'),
|
||||
'namespace' => $input->getArgument('namespace'),
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Command\Output;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
|
||||
class MessageRenderer
|
||||
{
|
||||
public function renderAll(FlashMessageQueue $queue, OutputInterface $output): void
|
||||
{
|
||||
foreach ($queue->getAllMessages() as $message) {
|
||||
$this->renderOne($message, $output);
|
||||
}
|
||||
}
|
||||
|
||||
public function renderOne(FlashMessage $message, OutputInterface $output): void
|
||||
{
|
||||
[$style, $verbosity] = match ($message->getSeverity()) {
|
||||
ContextualFeedbackSeverity::INFO,
|
||||
ContextualFeedbackSeverity::NOTICE,
|
||||
ContextualFeedbackSeverity::OK => ['info', $output::VERBOSITY_VERBOSE],
|
||||
ContextualFeedbackSeverity::WARNING => ['comment', $output::VERBOSITY_NORMAL],
|
||||
ContextualFeedbackSeverity::ERROR => ['error', $output::VERBOSITY_NORMAL],
|
||||
};
|
||||
$formattedMessage = sprintf(
|
||||
"<%s><bold>%s</bold>\n%s</%s>\n",
|
||||
$style,
|
||||
$message->getTitle(),
|
||||
$message->getMessage(),
|
||||
$style,
|
||||
);
|
||||
$output->writeln($formattedMessage, $verbosity);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user