first commit
This commit is contained in:
commit
1218dd1517
12716 files changed
+989233
No files matched your search
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
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\Service\AbstractService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Authentication services class
|
||||
*/
|
||||
class AbstractAuthenticationService extends AbstractService
|
||||
{
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
/**
|
||||
* Usergroups db table definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $db_groups = [];
|
||||
|
||||
/**
|
||||
* If the writelog() functions is called if a login-attempt has be tried without success
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $writeAttemptLog = false;
|
||||
|
||||
/**
|
||||
* 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->db_groups = $this->getServiceOption('db_groups', $authInfo['db_groups'] ?? [], false);
|
||||
$this->writeAttemptLog = $this->pObj->writeAttemptLog ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the login data with the user record data for builtin login methods
|
||||
*
|
||||
* @param array $user User data array
|
||||
* @param array $loginData Login data array
|
||||
* @param string $passwordCompareStrategy Password compare strategy
|
||||
* @return bool TRUE if login data matched
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
public function compareUident(array $user, array $loginData, $passwordCompareStrategy = '')
|
||||
{
|
||||
trigger_error('This method will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
|
||||
return $this->pObj->compareUident($user, $loginData, $passwordCompareStrategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 int $details_nr The message number. Specific for each $type and $action. in the future this will make it possible to translate error messages to other languages
|
||||
* @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, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
|
||||
* @param int|string $recuid Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
|
||||
* @param int|string $recpid Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
|
||||
*/
|
||||
public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '')
|
||||
{
|
||||
if ($this->writeAttemptLog) {
|
||||
$this->pObj->writelog($type, $action, $error, $details_nr, $details, $data, $tablename, $recuid, $recpid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 mixed 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['check_pid_clause']),
|
||||
QueryHelper::stripLogicalOperatorPrefix($dbUser['enable_clause']),
|
||||
QueryHelper::stripLogicalOperatorPrefix($extraWhere),
|
||||
]);
|
||||
if (!empty($username)) {
|
||||
array_unshift(
|
||||
$constraints,
|
||||
$query->expr()->eq(
|
||||
$dbUser['username_column'],
|
||||
$query->createNamedParameter($username, \PDO::PARAM_STR)
|
||||
)
|
||||
);
|
||||
}
|
||||
$user = $query->select('*')
|
||||
->from($dbUser['table'])
|
||||
->where(...$constraints)
|
||||
->execute()
|
||||
->fetch();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
||||
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Authentication services class
|
||||
*/
|
||||
class AuthenticationService extends AbstractAuthenticationService
|
||||
{
|
||||
/**
|
||||
* Process the submitted credentials.
|
||||
* In this case hash the clear text password if it has been submitted.
|
||||
*
|
||||
* @param array $loginData Credentials that are submitted and potentially modified by other services
|
||||
* @param string $passwordTransmissionStrategy Keyword of how the password has been hashed or encrypted before submission
|
||||
* @return bool
|
||||
*/
|
||||
public function processLoginData(array &$loginData, $passwordTransmissionStrategy)
|
||||
{
|
||||
$isProcessed = false;
|
||||
if ($passwordTransmissionStrategy === 'normal') {
|
||||
$loginData['uident_text'] = $loginData['uident'];
|
||||
$isProcessed = true;
|
||||
}
|
||||
return $isProcessed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user (eg. look up the user record in database when a login is sent)
|
||||
*
|
||||
* @return mixed User array or FALSE
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
if ($this->login['status'] !== LoginType::LOGIN) {
|
||||
return false;
|
||||
}
|
||||
if ((string)$this->login['uident_text'] === '') {
|
||||
// Failed Login attempt (no password given)
|
||||
$this->writelog(255, 3, 3, 2, 'Login-attempt from ###IP### for username \'%s\' with an empty password!', [
|
||||
$this->login['uname']
|
||||
]);
|
||||
$this->logger->warning(sprintf('Login-attempt from %s, for username \'%s\' with an empty password!', $this->authInfo['REMOTE_ADDR'], $this->login['uname']));
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->fetchUserRecord($this->login['uname']);
|
||||
if (!is_array($user)) {
|
||||
// Failed login attempt (no username found)
|
||||
$this->writelog(255, 3, 3, 2, 'Login-attempt from ###IP###, username \'%s\' not found!!', [$this->login['uname']]);
|
||||
$this->logger->info('Login-attempt from username \'' . $this->login['uname'] . '\' not found!', [
|
||||
'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,
|
||||
* check domain lock if configured.
|
||||
*
|
||||
* 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 $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'];
|
||||
$queriedDomain = $this->authInfo['HTTP_HOST'];
|
||||
$configuredDomainLock = $user['lockToDomain'];
|
||||
$userDatabaseTable = $this->db_user['table'];
|
||||
|
||||
$isSaltedPassword = false;
|
||||
$isValidPassword = false;
|
||||
$isReHashNeeded = false;
|
||||
$isDomainLockMet = false;
|
||||
|
||||
$saltFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
|
||||
|
||||
// Get a hashed password instance for the hash stored in db of this user
|
||||
$invalidPasswordHashException = null;
|
||||
try {
|
||||
$hashInstance = $saltFactory->get($passwordHashInDatabase, TYPO3_MODE);
|
||||
} catch (InvalidPasswordHashException $invalidPasswordHashException) {
|
||||
// This can be refactored if the 'else' part below is gone in TYPO3 v10.0: Log and return 100 here
|
||||
$hashInstance = null;
|
||||
}
|
||||
// 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(TYPO3_MODE);
|
||||
|
||||
if ($hashInstance instanceof PasswordHashInterface) {
|
||||
// We found a hash class that can handle this type of hash
|
||||
$isSaltedPassword = true;
|
||||
$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 (empty($configuredDomainLock)) {
|
||||
// No domain restriction set for user in db. This is ok.
|
||||
$isDomainLockMet = true;
|
||||
} elseif (!strcasecmp($configuredDomainLock, $queriedDomain)) {
|
||||
// Domain restriction set and it matches given host. Ok.
|
||||
$isDomainLockMet = true;
|
||||
}
|
||||
}
|
||||
} elseif (substr($user['password'], 0, 2) === 'M$') {
|
||||
// @todo @deprecated: The entire else should be removed in TYPO3 v10.0 as dedicated breaking patch
|
||||
// If the stored db password starts with M$, it may be a md5 password that has been
|
||||
// upgraded to a salted md5 using the old salted passwords scheduler task.
|
||||
// See if a salt instance is returned if we cut off the M, so Md5PasswordHash kicks in
|
||||
try {
|
||||
$hashInstance = $saltFactory->get(substr($passwordHashInDatabase, 1), TYPO3_MODE);
|
||||
$isSaltedPassword = true;
|
||||
$isValidPassword = $hashInstance->checkPassword(md5($submittedPassword), substr($passwordHashInDatabase, 1));
|
||||
if ($isValidPassword) {
|
||||
// Upgrade this password to a sane mechanism now
|
||||
$isReHashNeeded = true;
|
||||
if (empty($configuredDomainLock)) {
|
||||
// No domain restriction set for user in db. This is ok.
|
||||
$isDomainLockMet = true;
|
||||
} elseif (!strcasecmp($configuredDomainLock, $queriedDomain)) {
|
||||
// Domain restriction set and it matches given host. Ok.
|
||||
$isDomainLockMet = true;
|
||||
}
|
||||
}
|
||||
} catch (InvalidPasswordHashException $e) {
|
||||
// Still no instance found: $isSaltedPasswords is NOT set to true, logging and return done below
|
||||
}
|
||||
} else {
|
||||
// @todo: Simplify if elseif part is gone
|
||||
// Still no valid hash instance could be found. Probably the stored hash used a mechanism
|
||||
// that is not available on current system. We throw the previous exception again to be
|
||||
// handled on a higher level.
|
||||
if ($invalidPasswordHashException !== null) {
|
||||
throw $invalidPasswordHashException;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isSaltedPassword) {
|
||||
// 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(255, 3, 3, 1, $message, [$submittedUsername]);
|
||||
// Not responsible, check other services
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (!$isValidPassword) {
|
||||
// Failed login attempt - wrong password
|
||||
$message = 'Login-attempt from ###IP###, username \'%s\', password not accepted!';
|
||||
$this->writeLogMessage($message, $submittedUsername);
|
||||
$this->writelog(255, 3, 3, 1, $message, [$submittedUsername]);
|
||||
// Responsible, authentication failed, do NOT check other services
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!$isDomainLockMet) {
|
||||
// Password ok, but configured domain lock not met
|
||||
$errorMessage = 'Login-attempt from ###IP###, username \'%s\', locked domain \'%s\' did not match \'%s\'!';
|
||||
$this->writeLogMessage($errorMessage, $user[$this->db_user['username_column']], $configuredDomainLock, $queriedDomain);
|
||||
$this->writelog(255, 3, 3, 1, $errorMessage, [$user[$this->db_user['username_column']], $configuredDomainLock, $queriedDomain]);
|
||||
// Responsible, authentication ok, but domain lock not ok, 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, domain lock ok. Log successful login and return 'auth ok, do NOT check other services'
|
||||
$this->writeLogMessage(TYPO3_MODE . ' Authentication successful for username \'%s\'', $submittedUsername);
|
||||
return 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find usergroup records, currently only for frontend
|
||||
*
|
||||
* @param array $user Data of user.
|
||||
* @param array $knownGroups Group data array of already known groups. This is handy if you want select other related groups. Keys in this array are unique IDs of those groups.
|
||||
* @return mixed Groups array, keys = uid which must be unique
|
||||
*/
|
||||
public function getGroups($user, $knownGroups)
|
||||
{
|
||||
// Attention: $knownGroups is not used within this method, but other services can use it.
|
||||
// This parameter should not be removed!
|
||||
// The FrontendUserAuthentication call getGroups and handover the previous detected groups.
|
||||
$groupDataArr = [];
|
||||
if ($this->mode === 'getGroupsFE') {
|
||||
$groups = [];
|
||||
if ($user[$this->db_user['usergroup_column']] ?? false) {
|
||||
$groupList = $user[$this->db_user['usergroup_column']];
|
||||
$groups = [];
|
||||
$this->getSubGroups($groupList, '', $groups);
|
||||
}
|
||||
// ADD group-numbers if the IPmask matches.
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['FE']['IPmaskMountGroups'] ?? [] as $IPel) {
|
||||
if ($this->authInfo['REMOTE_ADDR'] && $IPel[0] && GeneralUtility::cmpIP($this->authInfo['REMOTE_ADDR'], $IPel[0])) {
|
||||
$groups[] = (int)$IPel[1];
|
||||
}
|
||||
}
|
||||
$groups = array_unique($groups);
|
||||
if (!empty($groups)) {
|
||||
$this->logger->debug('Get usergroups with id: ' . implode(',', $groups));
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($this->db_groups['table']);
|
||||
if (!empty($this->authInfo['showHiddenRecords'])) {
|
||||
$queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
|
||||
}
|
||||
|
||||
$res = $queryBuilder->select('*')
|
||||
->from($this->db_groups['table'])
|
||||
->where(
|
||||
$queryBuilder->expr()->in(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($groups, Connection::PARAM_INT_ARRAY)
|
||||
),
|
||||
$queryBuilder->expr()->orX(
|
||||
$queryBuilder->expr()->eq(
|
||||
'lockToDomain',
|
||||
$queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->isNull('lockToDomain'),
|
||||
$queryBuilder->expr()->eq(
|
||||
'lockToDomain',
|
||||
$queryBuilder->createNamedParameter($this->authInfo['HTTP_HOST'], \PDO::PARAM_STR)
|
||||
)
|
||||
)
|
||||
)
|
||||
->execute();
|
||||
|
||||
while ($row = $res->fetch()) {
|
||||
$groupDataArr[$row['uid']] = $row;
|
||||
}
|
||||
} else {
|
||||
$this->logger->debug('No usergroups found.');
|
||||
}
|
||||
}
|
||||
return $groupDataArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches subgroups of groups. Function is called recursively for each subgroup.
|
||||
* Function was previously copied from
|
||||
* \TYPO3\CMS\Core\Authentication\BackendUserAuthentication->fetchGroups and has been slightly modified.
|
||||
*
|
||||
* @param string $grList Commalist of fe_groups uid numbers
|
||||
* @param string $idList List of already processed fe_groups-uids so the function will not fall into an eternal recursion.
|
||||
* @param array $groups
|
||||
* @internal
|
||||
*/
|
||||
public function getSubGroups($grList, $idList, &$groups)
|
||||
{
|
||||
// Fetching records of the groups in $grList (which are not blocked by lockedToDomain either):
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_groups');
|
||||
if (!empty($this->authInfo['showHiddenRecords'])) {
|
||||
$queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
|
||||
}
|
||||
|
||||
$res = $queryBuilder
|
||||
->select('uid', 'subgroup')
|
||||
->from($this->db_groups['table'])
|
||||
->where(
|
||||
$queryBuilder->expr()->in(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter(
|
||||
GeneralUtility::intExplode(',', $grList, true),
|
||||
Connection::PARAM_INT_ARRAY
|
||||
)
|
||||
),
|
||||
$queryBuilder->expr()->orX(
|
||||
$queryBuilder->expr()->eq(
|
||||
'lockToDomain',
|
||||
$queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->isNull('lockToDomain'),
|
||||
$queryBuilder->expr()->eq(
|
||||
'lockToDomain',
|
||||
$queryBuilder->createNamedParameter($this->authInfo['HTTP_HOST'], \PDO::PARAM_STR)
|
||||
)
|
||||
)
|
||||
)
|
||||
->execute();
|
||||
|
||||
// Internal group record storage
|
||||
$groupRows = [];
|
||||
// The groups array is filled
|
||||
while ($row = $res->fetch()) {
|
||||
if (!in_array($row['uid'], $groups)) {
|
||||
$groups[] = $row['uid'];
|
||||
}
|
||||
$groupRows[$row['uid']] = $row;
|
||||
}
|
||||
// Traversing records in the correct order
|
||||
$include_staticArr = GeneralUtility::intExplode(',', $grList);
|
||||
// traversing list
|
||||
foreach ($include_staticArr as $uid) {
|
||||
// Get row:
|
||||
$row = $groupRows[$uid];
|
||||
// Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
|
||||
if (is_array($row) && !GeneralUtility::inList($idList, $uid) && trim($row['subgroup'])) {
|
||||
// Make integer list
|
||||
$theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
|
||||
// Call recursively, pass along list of already processed groups so they are not processed again.
|
||||
$this->getSubGroups($theList, $idList . ',' . $uid, $groups);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, mixed> $params
|
||||
*/
|
||||
protected function writeLogMessage(string $message, ...$params): void
|
||||
{
|
||||
if (!empty($params)) {
|
||||
$message = vsprintf($message, $params);
|
||||
}
|
||||
$message = str_replace('###IP###', (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'), $message);
|
||||
if (TYPO3_MODE === 'FE') {
|
||||
$timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
|
||||
$timeTracker->setTSlogMessage($message);
|
||||
}
|
||||
$this->logger->notice($message);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
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;
|
||||
|
||||
/**
|
||||
* TYPO3 backend user authentication on a CLI level
|
||||
* Auto-logs in, only allowed on CLI
|
||||
*/
|
||||
class CommandLineUserAuthentication extends BackendUserAuthentication
|
||||
{
|
||||
|
||||
/**
|
||||
* The username of the CLI user (there is only one)
|
||||
* @var string
|
||||
*/
|
||||
protected $username = '_cli_';
|
||||
|
||||
/**
|
||||
* 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 AbstactUserAuthentication::start()
|
||||
*
|
||||
* We do not need support for sessions, cookies, $_GET-modes, the postUserLookup hook or
|
||||
* a database connectiona during CLI Bootstrap
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
$this->logger->debug('## Beginning of auth logging.');
|
||||
// svConfig is unused, but we set it, as the property is public and might be used by extensions
|
||||
$this->svConfig = $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement for AbstactUserAuthentication::checkAuthentication()
|
||||
*
|
||||
* Not required in CLI mode, therefore empty.
|
||||
*/
|
||||
public function checkAuthentication()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($this->username);
|
||||
if (!$this->user['uid']) {
|
||||
// create a new BE user in the database
|
||||
if (!$this->checkIfCliUserExists()) {
|
||||
$this->createCliUser();
|
||||
} else {
|
||||
throw new \RuntimeException('No backend user named "_cli_" could be authenticated, maybe this user is "hidden"?', 1484050401);
|
||||
}
|
||||
$this->setBeUserByName($this->username);
|
||||
}
|
||||
if (!$this->user['uid']) {
|
||||
throw new \RuntimeException('No backend user named "_cli_" could be created.', 1476107195);
|
||||
}
|
||||
// The groups are fetched and ready for permission checking in this initialization.
|
||||
$this->fetchGroupData();
|
||||
$this->backendSetUC();
|
||||
// activate this functionality for DataHandler
|
||||
$this->uc['recursiveDelete'] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in the TYPO3 Backend user "_cli_"
|
||||
*
|
||||
* @param bool $proceedIfNoUserIsLoggedIn if this option is set, then there won't be a redirect to the login screen of the Backend - used for areas in the backend which do not need user rights like the login page.
|
||||
*/
|
||||
public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
|
||||
{
|
||||
$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
|
||||
*/
|
||||
protected function isUserAllowedToLogin()
|
||||
{
|
||||
return in_array((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'], [0, 2], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user with username "_cli_" exists. Deleted users are left out
|
||||
* but hidden and start / endtime restricted users are considered.
|
||||
*
|
||||
* @return bool true if the user exists
|
||||
*/
|
||||
protected function checkIfCliUserExists()
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$count = $queryBuilder
|
||||
->count('*')
|
||||
->from('be_users')
|
||||
->where($queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter('_cli_')))
|
||||
->execute()
|
||||
->fetchColumn(0);
|
||||
return (bool)$count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a record in the DB table be_users called "_cli_" with no other information
|
||||
*/
|
||||
protected function createCliUser()
|
||||
{
|
||||
$userFields = [
|
||||
'username' => $this->username,
|
||||
'password' => $this->generateHashedPassword(),
|
||||
'admin' => 1,
|
||||
'tstamp' => $GLOBALS['EXEC_TIME'],
|
||||
'crdate' => $GLOBALS['EXEC_TIME']
|
||||
];
|
||||
|
||||
$databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable('be_users');
|
||||
$databaseConnection->insert('be_users', $userFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function returns a salted hashed key.
|
||||
*
|
||||
* @return string a random password
|
||||
*/
|
||||
protected function generateHashedPassword()
|
||||
{
|
||||
$cryptoService = GeneralUtility::makeInstance(Random::class);
|
||||
$password = $cryptoService->generateRandomBytes(20);
|
||||
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
|
||||
return $hashInstance->getHashedPassword($password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Core\Authentication;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Type\Enumeration;
|
||||
|
||||
/**
|
||||
* Contains the different login types
|
||||
*/
|
||||
final class LoginType extends Enumeration
|
||||
{
|
||||
/**
|
||||
* When a login is in process
|
||||
*/
|
||||
public const LOGIN = 'login';
|
||||
|
||||
/**
|
||||
* When a logout is in process
|
||||
*/
|
||||
public const LOGOUT = 'logout';
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\Log\LogManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* An abstract caching backend
|
||||
*/
|
||||
abstract class AbstractBackend implements BackendInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
const DATETIME_EXPIRYTIME_UNLIMITED = '9999-12-31T23:59:59+0000';
|
||||
const UNLIMITED_LIFETIME = 0;
|
||||
/**
|
||||
* Reference to the cache which uses this backend
|
||||
*
|
||||
* @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $cacheIdentifier;
|
||||
|
||||
/**
|
||||
* The current application context
|
||||
*
|
||||
* This variable is currently unused and set to "production" always.
|
||||
* It is only kept to keep backwards compatibility.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* Default lifetime of a cache entry in seconds
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $defaultLifetime = 3600;
|
||||
|
||||
/**
|
||||
* Constructs this backend
|
||||
*
|
||||
* @param string $context Unused, for backward compatibility only
|
||||
* @param array $options Configuration options - depends on the actual backend
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct($context, array $options = [])
|
||||
{
|
||||
$this->context = $context;
|
||||
if (is_array($options) || $options instanceof \ArrayAccess) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->logger === null) {
|
||||
$this->setLogger(
|
||||
GeneralUtility::makeInstance(LogManager::class)
|
||||
->getLogger(static::class)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a reference to the cache frontend which uses this backend
|
||||
*
|
||||
* @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache The frontend for this backend
|
||||
*/
|
||||
public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
$this->cacheIdentifier = $this->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.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setDefaultLifetime($defaultLifetime)
|
||||
{
|
||||
if (!is_int($defaultLifetime) || $defaultLifetime < 0) {
|
||||
throw new \InvalidArgumentException('The default lifetime must be given as a positive integer.', 1233072774);
|
||||
}
|
||||
$this->defaultLifetime = $defaultLifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility safeguard since re-introducing flushByTags as API.
|
||||
* See https://review.typo3.org/#/c/50537/ comments for patch set 14.
|
||||
*
|
||||
* The method is here even though it is only required for TaggableBackendInterface.
|
||||
* We add it here to ensure third party cache backends do not fail but instead
|
||||
* delegate to a less efficient linear flushing behavior.
|
||||
*
|
||||
* @param string[] $tags
|
||||
*/
|
||||
public function flushByTags(array $tags)
|
||||
{
|
||||
array_walk($tags, [$this, 'flushByTag']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the expiry time by the given lifetime. If no lifetime is
|
||||
* specified, the default lifetime is used.
|
||||
*
|
||||
* @param int $lifetime The lifetime in seconds
|
||||
* @return \DateTime The expiry time
|
||||
*/
|
||||
protected function calculateExpiryTime($lifetime = null)
|
||||
{
|
||||
if ($lifetime === self::UNLIMITED_LIFETIME || $lifetime === null && $this->defaultLifetime === self::UNLIMITED_LIFETIME) {
|
||||
$expiryTime = new \DateTime(self::DATETIME_EXPIRYTIME_UNLIMITED, new \DateTimeZone('UTC'));
|
||||
} else {
|
||||
if ($lifetime === null) {
|
||||
$lifetime = $this->defaultLifetime;
|
||||
}
|
||||
$expiryTime = new \DateTime('now +' . $lifetime . ' seconds', new \DateTimeZone('UTC'));
|
||||
}
|
||||
return $expiryTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using APC.
|
||||
*
|
||||
* 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. By default prefix consists from two parts
|
||||
* separated by underscore character and ends in yet another underscore character:
|
||||
* - "TYPO3"
|
||||
* - MD5 of path to TYPO3 project folder and user running TYPO3
|
||||
* This prefix makes sure that keys from the different installations do not
|
||||
* conflict.
|
||||
*/
|
||||
class ApcBackend extends AbstractBackend implements TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* A prefix to separate stored data from other data possible stored in the APC
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $identifierPrefix;
|
||||
|
||||
/**
|
||||
* Set the cache identifier prefix.
|
||||
*
|
||||
* @param string $identifierPrefix
|
||||
*/
|
||||
protected function setIdentifierPrefix($identifierPrefix)
|
||||
{
|
||||
$this->identifierPrefix = $identifierPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cache identifier prefix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getIdentifierPrefix()
|
||||
{
|
||||
return $this->identifierPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs this backend
|
||||
*
|
||||
* @param string $context Unused, for backward compatibility only
|
||||
* @param array $options Configuration options - unused here
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct($context, array $options = [])
|
||||
{
|
||||
if (!extension_loaded('apc')) {
|
||||
throw new Exception('The PHP extension "apc" or "apcu" must be installed and loaded in order to use the APC backend.', 1232985414);
|
||||
}
|
||||
if (PHP_SAPI === 'cli' && ini_get('apc.enable_cli') == 0) {
|
||||
throw new Exception('The APC backend cannot be used because apc is disabled on CLI.', 1232985415);
|
||||
}
|
||||
parent::__construct($context, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the identifier prefix when setting the cache.
|
||||
*
|
||||
* @param FrontendInterface $cache
|
||||
*/
|
||||
public function setCache(FrontendInterface $cache)
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$processUser = $this->getCurrentUserData();
|
||||
$pathHash = GeneralUtility::shortMD5(Environment::getProjectPath() . $processUser['name'] . $this->context . $cache->getIdentifier(), 12);
|
||||
$this->setIdentifierPrefix('TYPO3_' . $pathHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current user data with posix_getpwuid or a default structure when
|
||||
* posix_getpwuid is not available.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getCurrentUserData()
|
||||
{
|
||||
return extension_loaded('posix') ? posix_getpwuid(posix_geteuid()) : ['name' => 'default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param int $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 $data is not a string
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('No cache frontend has been set yet via setCache().', 1232986818);
|
||||
}
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1232986825);
|
||||
}
|
||||
$tags[] = '%APCBE%' . $this->cacheIdentifier;
|
||||
$expiration = $lifetime ?? $this->defaultLifetime;
|
||||
$success = apc_store($this->getIdentifierPrefix() . $entryIdentifier, $data, $expiration);
|
||||
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.
|
||||
*
|
||||
* @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($entryIdentifier)
|
||||
{
|
||||
$success = false;
|
||||
$value = apc_fetch($this->getIdentifierPrefix() . $entryIdentifier, $success);
|
||||
return $success ? $value : $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
$success = false;
|
||||
apc_fetch($this->getIdentifierPrefix() . $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.
|
||||
*
|
||||
* @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($entryIdentifier)
|
||||
{
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
return apc_delete($this->getIdentifierPrefix() . $entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$success = false;
|
||||
$identifiers = apc_fetch($this->getIdentifierPrefix() . 'tag_' . $tag, $success);
|
||||
if ($success === false) {
|
||||
return [];
|
||||
}
|
||||
return (array)$identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return array Array with tags
|
||||
*/
|
||||
protected function findTagsByIdentifier($identifier)
|
||||
{
|
||||
$success = false;
|
||||
$tags = apc_fetch($this->getIdentifierPrefix() . 'ident_' . $identifier, $success);
|
||||
return $success ? (array)$tags : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('Yet no cache frontend has been set via setCache().', 1232986971);
|
||||
}
|
||||
$this->flushByTag('%APCBE%' . $this->cacheIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the identifier with the given tags
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
* @param array $tags
|
||||
*/
|
||||
protected function addIdentifierToTags($entryIdentifier, array $tags)
|
||||
{
|
||||
// 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;
|
||||
apc_store($this->getIdentifierPrefix() . '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) {
|
||||
apc_store($this->getIdentifierPrefix() . 'ident_' . $entryIdentifier, $existingTags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes association of the identifier with the given tags
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
*/
|
||||
protected function removeIdentifierFromAllTags($entryIdentifier)
|
||||
{
|
||||
// Get tags for this identifier
|
||||
$tags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
// Deassociate 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)) {
|
||||
apc_store($this->getIdentifierPrefix() . 'tag_' . $tag, $identifiers);
|
||||
} else {
|
||||
apc_delete($this->getIdentifierPrefix() . 'tag_' . $tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear reverse tag index for this identifier
|
||||
apc_delete($this->getIdentifierPrefix() . 'ident_' . $entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing, as APC does GC itself
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using APCu.
|
||||
*
|
||||
* 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. By default prefix consists from two parts
|
||||
* separated by underscore character and ends in yet another underscore character:
|
||||
* - "TYPO3"
|
||||
* - MD5 of path to TYPO3 and user running TYPO3
|
||||
* This prefix makes sure that keys from the different installations do not
|
||||
* conflict.
|
||||
*/
|
||||
class ApcuBackend extends AbstractBackend implements TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* A prefix to separate stored data from other data possible stored in the APC
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $identifierPrefix;
|
||||
|
||||
/**
|
||||
* Set the cache identifier prefix.
|
||||
*
|
||||
* @param string $identifierPrefix
|
||||
*/
|
||||
protected function setIdentifierPrefix($identifierPrefix)
|
||||
{
|
||||
$this->identifierPrefix = $identifierPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cache identifier prefix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getIdentifierPrefix()
|
||||
{
|
||||
return $this->identifierPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs this backend
|
||||
*
|
||||
* @param string $context Unused, for backward compatibility only
|
||||
* @param array $options Configuration options - unused here
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct($context, 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($context, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the identifier prefix when setting the cache.
|
||||
*
|
||||
* @param FrontendInterface $cache
|
||||
*/
|
||||
public function setCache(FrontendInterface $cache)
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$processUser = $this->getCurrentUserData();
|
||||
$pathHash = GeneralUtility::shortMD5(Environment::getProjectPath() . $processUser['name'] . $this->context . $cache->getIdentifier(), 12);
|
||||
$this->setIdentifierPrefix('TYPO3_' . $pathHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current user data with posix_getpwuid or a default structure when
|
||||
* posix_getpwuid is not available.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getCurrentUserData()
|
||||
{
|
||||
return extension_loaded('posix') ? posix_getpwuid(posix_geteuid()) : ['name' => 'default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param int $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 $data is not a string
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('No cache frontend has been set yet via setCache().', 1232986118);
|
||||
}
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1232986125);
|
||||
}
|
||||
$tags[] = '%APCBE%' . $this->cacheIdentifier;
|
||||
$expiration = $lifetime ?? $this->defaultLifetime;
|
||||
$success = apcu_store($this->getIdentifierPrefix() . $entryIdentifier, $data, $expiration);
|
||||
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.
|
||||
*
|
||||
* @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($entryIdentifier)
|
||||
{
|
||||
$success = false;
|
||||
$value = apcu_fetch($this->getIdentifierPrefix() . $entryIdentifier, $success);
|
||||
return $success ? $value : $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
$success = false;
|
||||
apcu_fetch($this->getIdentifierPrefix() . $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.
|
||||
*
|
||||
* @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($entryIdentifier)
|
||||
{
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
return apcu_delete($this->getIdentifierPrefix() . $entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$success = false;
|
||||
$identifiers = apcu_fetch($this->getIdentifierPrefix() . 'tag_' . $tag, $success);
|
||||
if ($success === false) {
|
||||
return [];
|
||||
}
|
||||
return (array)$identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return array Array with tags
|
||||
*/
|
||||
protected function findTagsByIdentifier($identifier)
|
||||
{
|
||||
$success = false;
|
||||
$tags = apcu_fetch($this->getIdentifierPrefix() . 'ident_' . $identifier, $success);
|
||||
return $success ? (array)$tags : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('Yet no cache frontend has been set via setCache().', 1232986571);
|
||||
}
|
||||
$this->flushByTag('%APCBE%' . $this->cacheIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the identifier with the given tags
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
* @param array $tags
|
||||
*/
|
||||
protected function addIdentifierToTags($entryIdentifier, array $tags)
|
||||
{
|
||||
// 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->getIdentifierPrefix() . '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->getIdentifierPrefix() . 'ident_' . $entryIdentifier, $existingTags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes association of the identifier with the given tags
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
*/
|
||||
protected function removeIdentifierFromAllTags($entryIdentifier)
|
||||
{
|
||||
// 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->getIdentifierPrefix() . 'tag_' . $tag, $identifiers);
|
||||
} else {
|
||||
apcu_delete($this->getIdentifierPrefix() . 'tag_' . $tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear reverse tag index for this identifier
|
||||
apcu_delete($this->getIdentifierPrefix() . 'ident_' . $entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing, as APCu does GC itself
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache;
|
||||
|
||||
/**
|
||||
* A contract for a Cache Backend
|
||||
*/
|
||||
interface BackendInterface
|
||||
{
|
||||
/**
|
||||
* Sets a reference to the cache frontend which uses this backend
|
||||
*
|
||||
* @param Cache\Frontend\FrontendInterface $cache The frontend for this backend
|
||||
*/
|
||||
public function setCache(Cache\Frontend\FrontendInterface $cache);
|
||||
|
||||
/**
|
||||
* 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 $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @throws \TYPO3\CMS\Core\Cache\Exception if no cache frontend has been set.
|
||||
* @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException if the data is not a string
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null);
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier);
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier);
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier);
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush();
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage();
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
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 FreezableBackendInterface, TaggableBackendInterface
|
||||
{
|
||||
const SEPARATOR = '^';
|
||||
const EXPIRYTIME_FORMAT = 'YmdHis';
|
||||
const EXPIRYTIME_LENGTH = 14;
|
||||
const DATASIZE_DIGITS = 10;
|
||||
/**
|
||||
* A file extension to use for each cache entry.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cacheEntryFileExtension = '';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $cacheEntryIdentifiers = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $frozen = false;
|
||||
|
||||
/**
|
||||
* Freezes this cache backend.
|
||||
*
|
||||
* All data in a frozen backend remains unchanged and methods which try to add
|
||||
* or modify data result in an exception thrown. Possible expiry times of
|
||||
* individual cache entries are ignored.
|
||||
*
|
||||
* On the positive side, a frozen cache backend is much faster on read access.
|
||||
* A frozen backend can only be thawed by calling the flush() method.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function freeze()
|
||||
{
|
||||
if ($this->frozen === true) {
|
||||
throw new \RuntimeException(sprintf('The cache "%s" is already frozen.', $this->cacheIdentifier), 1323353176);
|
||||
}
|
||||
$cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
|
||||
for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
|
||||
if ($directoryIterator->isDot()) {
|
||||
continue;
|
||||
}
|
||||
if ($cacheEntryFileExtensionLength > 0) {
|
||||
$entryIdentifier = substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength);
|
||||
} else {
|
||||
$entryIdentifier = $directoryIterator->getFilename();
|
||||
}
|
||||
$this->cacheEntryIdentifiers[$entryIdentifier] = true;
|
||||
file_put_contents($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension, $this->get($entryIdentifier));
|
||||
}
|
||||
file_put_contents($this->cacheDirectory . 'FrozenCache.data', serialize($this->cacheEntryIdentifiers));
|
||||
$this->frozen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this backend is frozen.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFrozen()
|
||||
{
|
||||
return $this->frozen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a reference to the cache frontend which uses this backend and
|
||||
* initializes the default cache directory.
|
||||
*
|
||||
* This method also detects if this backend is frozen and sets the internal
|
||||
* flag accordingly.
|
||||
*
|
||||
* @param FrontendInterface $cache The cache frontend
|
||||
*/
|
||||
public function setCache(FrontendInterface $cache)
|
||||
{
|
||||
parent::setCache($cache);
|
||||
if (file_exists($this->cacheDirectory . 'FrozenCache.data')) {
|
||||
$this->frozen = true;
|
||||
$this->cacheEntryIdentifiers = unserialize(file_get_contents($this->cacheDirectory . 'FrozenCache.data'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves data in a cache file.
|
||||
*
|
||||
* @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
|
||||
* @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @throws \RuntimeException
|
||||
* @throws InvalidDataException 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.
|
||||
* @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.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1204481674);
|
||||
}
|
||||
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);
|
||||
}
|
||||
if ($this->frozen === true) {
|
||||
throw new \RuntimeException(sprintf('Cannot add or modify cache entry because the backend of cache "%s" is frozen.', $this->cacheIdentifier), 1323344192);
|
||||
}
|
||||
$this->remove($entryIdentifier);
|
||||
$temporaryCacheEntryPathAndFilename = $this->cacheDirectory . StringUtility::getUniqueId() . '.temp';
|
||||
$lifetime = $lifetime ?? $this->defaultLifetime;
|
||||
$expiryTime = $lifetime === 0 ? 0 : $GLOBALS['EXEC_TIME'] + $lifetime;
|
||||
$metaData = str_pad($expiryTime, self::EXPIRYTIME_LENGTH) . implode(' ', $tags) . str_pad(strlen($data), self::DATASIZE_DIGITS);
|
||||
$result = file_put_contents($temporaryCacheEntryPathAndFilename, $data . $metaData);
|
||||
GeneralUtility::fixPermissions($temporaryCacheEntryPathAndFilename);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from a cache file.
|
||||
*
|
||||
* @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
|
||||
* @throws \InvalidArgumentException If identifier is invalid
|
||||
*/
|
||||
public function get($entryIdentifier)
|
||||
{
|
||||
if ($this->frozen === true) {
|
||||
return isset($this->cacheEntryIdentifiers[$entryIdentifier]) ? file_get_contents($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension) : false;
|
||||
}
|
||||
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,
|
||||
null,
|
||||
null,
|
||||
filesize($pathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
return file_get_contents($pathAndFilename, null, null, 0, $dataSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
* @return bool TRUE if such an entry exists, FALSE if not
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function has($entryIdentifier)
|
||||
{
|
||||
if ($this->frozen === true) {
|
||||
return isset($this->cacheEntryIdentifiers[$entryIdentifier]);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry.
|
||||
*
|
||||
* @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
|
||||
* @throws \RuntimeException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function remove($entryIdentifier)
|
||||
{
|
||||
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073035);
|
||||
}
|
||||
if ($entryIdentifier === '') {
|
||||
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1298114279);
|
||||
}
|
||||
if ($this->frozen === true) {
|
||||
throw new \RuntimeException(sprintf('Cannot remove cache entry because the backend of cache "%s" is frozen.', $this->cacheIdentifier), 1323344193);
|
||||
}
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
if (file_exists($pathAndFilename) === false) {
|
||||
return false;
|
||||
}
|
||||
if (@unlink($pathAndFilename) === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entry identifiers which are tagged by the
|
||||
* specified tag.
|
||||
*
|
||||
* @param string $searchedTag 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($searchedTag)
|
||||
{
|
||||
$entryIdentifiers = [];
|
||||
$now = $GLOBALS['EXEC_TIME'];
|
||||
$cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
|
||||
for ($directoryIterator = GeneralUtility::makeInstance(\DirectoryIterator::class, $this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
|
||||
if ($directoryIterator->isDot()) {
|
||||
continue;
|
||||
}
|
||||
$cacheEntryPathAndFilename = $directoryIterator->getPathname();
|
||||
$index = (int)file_get_contents(
|
||||
$cacheEntryPathAndFilename,
|
||||
null,
|
||||
null,
|
||||
filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
$metaData = file_get_contents($cacheEntryPathAndFilename, null, null, $index);
|
||||
$expiryTime = (int)substr($metaData, 0, self::EXPIRYTIME_LENGTH);
|
||||
if ($expiryTime !== 0 && $expiryTime < $now) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($searchedTag, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
|
||||
if ($cacheEntryFileExtensionLength > 0) {
|
||||
$entryIdentifiers[] = substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength);
|
||||
} else {
|
||||
$entryIdentifiers[] = $directoryIterator->getFilename();
|
||||
}
|
||||
}
|
||||
}
|
||||
return $entryIdentifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache and sets the frozen flag to FALSE.
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
parent::flush();
|
||||
if ($this->frozen === true) {
|
||||
$this->frozen = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
if (empty($identifiers)) {
|
||||
return;
|
||||
}
|
||||
foreach ($identifiers as $entryIdentifier) {
|
||||
$this->remove($entryIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given cache entry files are still valid or if their
|
||||
* lifetime has exceeded.
|
||||
*
|
||||
* @param string $cacheEntryPathAndFilename
|
||||
* @return bool
|
||||
*/
|
||||
protected function isCacheFileExpired($cacheEntryPathAndFilename)
|
||||
{
|
||||
if (file_exists($cacheEntryPathAndFilename) === false) {
|
||||
return true;
|
||||
}
|
||||
$index = (int)file_get_contents(
|
||||
$cacheEntryPathAndFilename,
|
||||
null,
|
||||
null,
|
||||
filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS,
|
||||
self::DATASIZE_DIGITS
|
||||
);
|
||||
$expiryTime = (int)file_get_contents($cacheEntryPathAndFilename, null, null, $index, self::EXPIRYTIME_LENGTH);
|
||||
return $expiryTime !== 0 && $expiryTime < $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
if ($this->frozen === true) {
|
||||
return;
|
||||
}
|
||||
for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
|
||||
if ($directoryIterator->isDot()) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the cache entry for the specified identifier.
|
||||
* Usually only one cache entry should be found - if more than one exist, this
|
||||
* is due to some error or crash.
|
||||
*
|
||||
* @param string $entryIdentifier The cache entry identifier
|
||||
* @return mixed The filenames (including path) as an array if one or more entries could be found, otherwise FALSE
|
||||
*/
|
||||
protected function findCacheFilesByIdentifier($entryIdentifier)
|
||||
{
|
||||
$pattern = $this->cacheDirectory . $entryIdentifier;
|
||||
$filesFound = glob($pattern);
|
||||
if ($filesFound === false || empty($filesFound)) {
|
||||
return false;
|
||||
}
|
||||
return $filesFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PHP code from the cache and require_onces it right away.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @throws \InvalidArgumentException
|
||||
* @return mixed Potential return value from the include operation
|
||||
*/
|
||||
public function requireOnce($entryIdentifier)
|
||||
{
|
||||
if ($this->frozen === true) {
|
||||
if (isset($this->cacheEntryIdentifiers[$entryIdentifier])) {
|
||||
return require_once $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PHP code from the cache and require it right away.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @throws \InvalidArgumentException
|
||||
* @return mixed Potential return value from the include operation
|
||||
*/
|
||||
public function require(string $entryIdentifier)
|
||||
{
|
||||
if ($this->frozen) {
|
||||
if (isset($this->cacheEntryIdentifiers[$entryIdentifier])) {
|
||||
return require $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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,40 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A contract for a cache backend which can be frozen.
|
||||
*/
|
||||
interface FreezableBackendInterface extends BackendInterface
|
||||
{
|
||||
/**
|
||||
* Freezes this cache backend.
|
||||
*
|
||||
* All data in a frozen backend remains unchanged and methods which try to add
|
||||
* or modify data result in an exception thrown. Possible expiry times of
|
||||
* individual cache entries are ignored.
|
||||
*
|
||||
* On the positive side, a frozen cache backend is much faster on read access.
|
||||
* A frozen backend can only be thawn by calling the flush() method.
|
||||
*/
|
||||
public function freeze();
|
||||
|
||||
/**
|
||||
* Tells if this backend is frozen.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFrozen();
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
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
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_BUCKET_SIZE = 1048534;
|
||||
|
||||
/**
|
||||
* Instance of the PHP Memcache class
|
||||
*
|
||||
* @var \Memcache|\Memcached
|
||||
*/
|
||||
protected $memcache;
|
||||
|
||||
/**
|
||||
* Used PECL module for memcached
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $usedPeclModule = '';
|
||||
|
||||
/**
|
||||
* Array of Memcache server configurations
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $servers = [];
|
||||
|
||||
/**
|
||||
* Indicates whether the memcache uses compression or not (requires zlib),
|
||||
* either 0 or \Memcached::OPT_COMPRESSION / MEMCACHE_COMPRESSED
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $flags;
|
||||
|
||||
/**
|
||||
* A prefix to separate stored data from other data possibly stored in the memcache
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $identifierPrefix;
|
||||
|
||||
/**
|
||||
* Constructs this backend
|
||||
*
|
||||
* @param string $context Unused, for backward compatibility only
|
||||
* @param array $options Configuration options - depends on the actual backend
|
||||
* @throws Exception if memcache is not installed
|
||||
*/
|
||||
public function __construct($context, 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);
|
||||
}
|
||||
|
||||
parent::__construct($context, $options);
|
||||
|
||||
if ($this->usedPeclModule === '') {
|
||||
if (extension_loaded('memcache')) {
|
||||
$this->usedPeclModule = 'memcache';
|
||||
} elseif (extension_loaded('memcached')) {
|
||||
$this->usedPeclModule = 'memcached';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$this->servers = $servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for compression flags bit
|
||||
*
|
||||
* @param bool $useCompression
|
||||
*/
|
||||
protected function setCompression($useCompression)
|
||||
{
|
||||
$compressionFlag = $this->usedPeclModule === 'memcache' ? MEMCACHE_COMPRESSED : \Memcached::OPT_COMPRESSION;
|
||||
if ($useCompression === true) {
|
||||
$this->flags ^= $compressionFlag;
|
||||
} else {
|
||||
$this->flags &= ~$compressionFlag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for compression flag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function getCompression()
|
||||
{
|
||||
return $this->flags !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the identifier prefix
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function initializeObject()
|
||||
{
|
||||
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 (strpos($server, 'unix://') === 0) {
|
||||
$host = $server;
|
||||
$port = 0;
|
||||
} else {
|
||||
if (strpos($server, 'tcp://') === 0) {
|
||||
$server = substr($server, 6);
|
||||
}
|
||||
if (strpos($server, ':') !== false) {
|
||||
list($host, $port) = explode(':', $server, 2);
|
||||
} else {
|
||||
$host = $server;
|
||||
$port = $defaultPort;
|
||||
}
|
||||
}
|
||||
$this->memcache->addserver($host, $port);
|
||||
}
|
||||
if ($this->usedPeclModule === 'memcached') {
|
||||
$this->memcache->setOption(\Memcached::OPT_COMPRESSION, $this->getCompression());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred PECL module
|
||||
*
|
||||
* @param string $peclModule
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setPeclModule($peclModule)
|
||||
{
|
||||
if ($peclModule !== 'memcache' && $peclModule !== 'memcached') {
|
||||
throw new Exception('PECL module must be either "memcache" or "memcached".', 1442239768);
|
||||
}
|
||||
|
||||
$this->usedPeclModule = $peclModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the identifier prefix when setting the cache.
|
||||
*
|
||||
* @param FrontendInterface $cache The frontend for this backend
|
||||
*/
|
||||
public function setCache(FrontendInterface $cache)
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$identifierHash = substr(md5(Environment::getProjectPath() . $this->context . $this->cacheIdentifier), 0, 12);
|
||||
$this->identifierPrefix = 'TYPO3_' . $identifierHash . '_';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param int $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 \InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
|
||||
throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
|
||||
}
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('No cache frontend has been set yet via setCache().', 1207149215);
|
||||
}
|
||||
$tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
|
||||
$expiration = $lifetime ?? $this->defaultLifetime;
|
||||
|
||||
// Memcached consideres 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 === true) {
|
||||
$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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the actual data inside memcache/memcached
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
* @param mixed $data
|
||||
* @param int $expiration
|
||||
* @return bool
|
||||
*/
|
||||
protected function setInternal($entryIdentifier, $data, $expiration)
|
||||
{
|
||||
if ($this->usedPeclModule === 'memcache') {
|
||||
return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
|
||||
}
|
||||
return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
$value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
|
||||
if (is_string($value) && strpos($value, 'TYPO3*chunked:') === 0) {
|
||||
list(, $chunkCount) = explode(':', $value);
|
||||
$value = '';
|
||||
for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
|
||||
$value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
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($entryIdentifier)
|
||||
{
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 of entries with all matching entries. An empty array if no entries matched
|
||||
*/
|
||||
public function findIdentifiersByTag($tag)
|
||||
{
|
||||
$identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
|
||||
if ($identifiers !== false) {
|
||||
return (array)$identifiers;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('No cache frontend has been set via setCache() yet.', 1204111376);
|
||||
}
|
||||
$this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the identifier with the given tags
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
* @param array $tags
|
||||
*/
|
||||
protected function addIdentifierToTags($entryIdentifier, array $tags)
|
||||
{
|
||||
// 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
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
*/
|
||||
protected function removeIdentifierFromAllTags($entryIdentifier)
|
||||
{
|
||||
// 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
|
||||
* @return array
|
||||
*/
|
||||
protected function findTagsByIdentifier($identifier)
|
||||
{
|
||||
$tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
|
||||
return $tags === false ? [] : (array)$tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing, as memcached does GC itself
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A caching backend which forgets everything immediately
|
||||
*/
|
||||
class NullBackend extends AbstractBackend implements PhpCapableBackendInterface, TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* Acts as if it would save data
|
||||
*
|
||||
* @param string $entryIdentifier ignored
|
||||
* @param string $data ignored
|
||||
* @param array $tags ignored
|
||||
* @param int $lifetime ignored
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts as if it would enable data compression
|
||||
*
|
||||
* @param bool $compression ignored
|
||||
*/
|
||||
public function setCompression($compression)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns False
|
||||
*
|
||||
* @param string $entryIdentifier ignored
|
||||
* @return bool FALSE
|
||||
*/
|
||||
public function get($entryIdentifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns False
|
||||
*
|
||||
* @param string $entryIdentifier ignored
|
||||
* @return bool FALSE
|
||||
*/
|
||||
public function has($entryIdentifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing
|
||||
*
|
||||
* @param string $entryIdentifier ignored
|
||||
* @return bool FALSE
|
||||
*/
|
||||
public function remove($entryIdentifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty array
|
||||
*
|
||||
* @param string $tag ignored
|
||||
* @return array An empty array
|
||||
*/
|
||||
public function findIdentifiersByTag($tag)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing
|
||||
*
|
||||
* @param string $tag ignored
|
||||
*/
|
||||
public function flushByTag($tag)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing
|
||||
*
|
||||
* @param string $identifier An identifier which describes the cache entry to load
|
||||
*/
|
||||
public function requireOnce($identifier)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing
|
||||
*
|
||||
* @param string $identifier An identifier which describes the cache entry to load
|
||||
*/
|
||||
public function require(string $identifier)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A PDO database cache backend
|
||||
*/
|
||||
class PdoBackend extends AbstractBackend implements TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $dataSourceName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $username;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $password;
|
||||
|
||||
/**
|
||||
* @var \PDO
|
||||
*/
|
||||
protected $databaseHandle;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $pdoDriver;
|
||||
|
||||
/**
|
||||
* Sets the DSN to use
|
||||
*
|
||||
* @param string $DSN The DSN to use for connecting to the DB
|
||||
*/
|
||||
public function setDataSourceName($DSN)
|
||||
{
|
||||
$this->dataSourceName = $DSN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the username to use
|
||||
*
|
||||
* @param string $username The username to use for connecting to the DB
|
||||
*/
|
||||
public function setUsername($username)
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the password to use
|
||||
*
|
||||
* @param string $password The password to use for connecting to the DB
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the cache backend.
|
||||
*/
|
||||
public function initializeObject()
|
||||
{
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param int $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 \InvalidArgumentException if the identifier is not valid
|
||||
* @throws InvalidDataException if $data is not a string
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('No cache frontend has been set yet via setCache().', 1259515600);
|
||||
}
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1259515601);
|
||||
}
|
||||
$this->remove($entryIdentifier);
|
||||
$lifetime = $lifetime ?? $this->defaultLifetime;
|
||||
$statementHandle = $this->databaseHandle->prepare('INSERT INTO "cache" ("identifier", "context", "cache", "created", "lifetime", "content") VALUES (?, ?, ?, ?, ?, ?)');
|
||||
$result = $statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier, $GLOBALS['EXEC_TIME'], $lifetime, $data]);
|
||||
if ($result === false) {
|
||||
throw new Exception('The cache entry "' . $entryIdentifier . '" could not be written.', 1259530791);
|
||||
}
|
||||
$statementHandle = $this->databaseHandle->prepare('INSERT INTO "tags" ("identifier", "context", "cache", "tag") VALUES (?, ?, ?, ?)');
|
||||
foreach ($tags as $tag) {
|
||||
$result = $statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier, $tag]);
|
||||
if ($result === false) {
|
||||
throw new Exception('The tag "' . $tag . ' for cache entry "' . $entryIdentifier . '" could not be written.', 1259530751);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
$statementHandle = $this->databaseHandle->prepare('SELECT "content" FROM "cache" WHERE "identifier"=? AND "context"=? AND "cache"=?' . $this->getNotExpiredStatement());
|
||||
$statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier]);
|
||||
return $statementHandle->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
$statementHandle = $this->databaseHandle->prepare('SELECT COUNT("identifier") FROM "cache" WHERE "identifier"=? AND "context"=? AND "cache"=?' . $this->getNotExpiredStatement());
|
||||
$statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier]);
|
||||
return $statementHandle->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
$statementHandle = $this->databaseHandle->prepare('DELETE FROM "tags" WHERE "identifier"=? AND "context"=? AND "cache"=?');
|
||||
$statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier]);
|
||||
$statementHandle = $this->databaseHandle->prepare('DELETE FROM "cache" WHERE "identifier"=? AND "context"=? AND "cache"=?');
|
||||
$statementHandle->execute([$entryIdentifier, $this->context, $this->cacheIdentifier]);
|
||||
return $statementHandle->rowCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
$statementHandle = $this->databaseHandle->prepare('DELETE FROM "tags" WHERE "context"=? AND "cache"=?');
|
||||
$statementHandle->execute([$this->context, $this->cacheIdentifier]);
|
||||
$statementHandle = $this->databaseHandle->prepare('DELETE FROM "cache" WHERE "context"=? AND "cache"=?');
|
||||
$statementHandle->execute([$this->context, $this->cacheIdentifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$statementHandle = $this->databaseHandle->prepare('DELETE FROM "cache" WHERE "context"=? AND "cache"=? AND "identifier" IN (SELECT "identifier" FROM "tags" WHERE "context"=? AND "cache"=? AND "tag"=?)');
|
||||
$statementHandle->execute([$this->context, $this->cacheIdentifier, $this->context, $this->cacheIdentifier, $tag]);
|
||||
$statementHandle = $this->databaseHandle->prepare('DELETE FROM "tags" WHERE "context"=? AND "cache"=? AND "tag"=?');
|
||||
$statementHandle->execute([$this->context, $this->cacheIdentifier, $tag]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$statementHandle = $this->databaseHandle->prepare('SELECT "identifier" FROM "tags" WHERE "context"=? AND "cache"=? AND "tag"=?');
|
||||
$statementHandle->execute([$this->context, $this->cacheIdentifier, $tag]);
|
||||
return $statementHandle->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
$statementHandle = $this->databaseHandle->prepare('DELETE FROM "tags" WHERE "context"=? AND "cache"=? AND "identifier" IN ' . '(SELECT "identifier" FROM "cache" WHERE "context"=? AND "cache"=? AND "lifetime" > 0 AND "created" + "lifetime" < ' . $GLOBALS['EXEC_TIME'] . ')');
|
||||
$statementHandle->execute([$this->context, $this->cacheIdentifier, $this->context, $this->cacheIdentifier]);
|
||||
$statementHandle = $this->databaseHandle->prepare('DELETE FROM "cache" WHERE "context"=? AND "cache"=? AND "lifetime" > 0 AND "created" + "lifetime" < ' . $GLOBALS['EXEC_TIME']);
|
||||
$statementHandle->execute([$this->context, $this->cacheIdentifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an SQL statement that evaluates to TRUE if the entry is not expired.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getNotExpiredStatement()
|
||||
{
|
||||
return ' AND ("lifetime" = 0 OR "created" + "lifetime" >= ' . $GLOBALS['EXEC_TIME'] . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database
|
||||
*
|
||||
* @throws \RuntimeException if something goes wrong
|
||||
*/
|
||||
protected function connect()
|
||||
{
|
||||
try {
|
||||
$splitdsn = explode(':', $this->dataSourceName, 2);
|
||||
$this->pdoDriver = $splitdsn[0];
|
||||
if ($this->pdoDriver === 'sqlite' && !file_exists($splitdsn[1])) {
|
||||
$this->databaseHandle = GeneralUtility::makeInstance(\PDO::class, $this->dataSourceName, $this->username, $this->password);
|
||||
$this->createCacheTables();
|
||||
} else {
|
||||
$this->databaseHandle = GeneralUtility::makeInstance(\PDO::class, $this->dataSourceName, $this->username, $this->password);
|
||||
}
|
||||
$this->databaseHandle->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
if (strpos($this->pdoDriver, 'mysql') === 0) {
|
||||
$this->databaseHandle->exec('SET SESSION sql_mode=\'ANSI\';');
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
throw new \RuntimeException('Could not connect to cache table with DSN "' . $this->dataSourceName . '". PDO error: ' . $e->getMessage(), 1334736164);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the tables needed for the cache backend.
|
||||
*
|
||||
* @throws \RuntimeException if something goes wrong
|
||||
*/
|
||||
protected function createCacheTables()
|
||||
{
|
||||
try {
|
||||
$this->importSql(
|
||||
$this->databaseHandle,
|
||||
$this->pdoDriver,
|
||||
ExtensionManagementUtility::extPath('core') .
|
||||
'Resources/Private/Sql/Cache/Backend/PdoBackendCacheAndTags.sql'
|
||||
);
|
||||
} catch (\PDOException $e) {
|
||||
throw new \RuntimeException('Could not create cache tables with DSN "' . $this->dataSourceName . '". PDO error: ' . $e->getMessage(), 1259576985);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pumps the SQL into the database. Use for DDL only.
|
||||
*
|
||||
* Important: key definitions with length specifiers (needed for MySQL) must
|
||||
* be given as "field"(xyz) - no space between double quote and parenthesis -
|
||||
* so they can be removed automatically.
|
||||
*
|
||||
* @param \PDO $databaseHandle
|
||||
* @param string $pdoDriver
|
||||
* @param string $pathAndFilename
|
||||
*/
|
||||
protected function importSql(\PDO $databaseHandle, string $pdoDriver, string $pathAndFilename): void
|
||||
{
|
||||
$sql = file($pathAndFilename, FILE_IGNORE_NEW_LINES & FILE_SKIP_EMPTY_LINES);
|
||||
// Remove MySQL style key length delimiters (yuck!) if we are not setting up a MySQL db
|
||||
if (strpos($pdoDriver, 'mysql') !== 0) {
|
||||
$sql = preg_replace('/"\\([0-9]+\\)/', '"', $sql);
|
||||
}
|
||||
$statement = '';
|
||||
foreach ($sql as $line) {
|
||||
$statement .= ' ' . trim($line);
|
||||
if (substr($statement, -1) === ';') {
|
||||
$databaseHandle->exec($statement);
|
||||
$statement = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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_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($entryIdentifier);
|
||||
|
||||
// @todo: Add require() as breaking patch in TYPO3 v10.0 to the interface
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
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
|
||||
* @var int Faked unlimited lifetime
|
||||
*/
|
||||
const FAKED_UNLIMITED_LIFETIME = 31536000;
|
||||
/**
|
||||
* Key prefix for identifier->data entries
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER_DATA_PREFIX = 'identData:';
|
||||
/**
|
||||
* Key prefix for identifier->tags sets
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER_TAGS_PREFIX = 'identTags:';
|
||||
/**
|
||||
* Key prefix for tag->identifiers sets
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
|
||||
/**
|
||||
* Instance of the PHP redis class
|
||||
*
|
||||
* @var \Redis
|
||||
*/
|
||||
protected $redis;
|
||||
|
||||
/**
|
||||
* Indicates whether the server is connected
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $connected = false;
|
||||
|
||||
/**
|
||||
* Persistent connection
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $persistentConnection = false;
|
||||
|
||||
/**
|
||||
* Hostname / IP of the Redis server, defaults to 127.0.0.1.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $hostname = '127.0.0.1';
|
||||
|
||||
/**
|
||||
* Port of the Redis server, defaults to 6379
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $port = 6379;
|
||||
|
||||
/**
|
||||
* Number of selected database, defaults to 0
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $database = 0;
|
||||
|
||||
/**
|
||||
* Password for redis authentication
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $password = '';
|
||||
|
||||
/**
|
||||
* Indicates whether data is compressed or not (requires php zlib)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $compression = false;
|
||||
|
||||
/**
|
||||
* -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $compressionLevel = -1;
|
||||
|
||||
/**
|
||||
* limit in seconds (default is 0 meaning unlimited)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $connectionTimeout = 0;
|
||||
|
||||
/**
|
||||
* Construct this backend
|
||||
*
|
||||
* @param string $context Unused, for backward compatibility only
|
||||
* @param array $options Configuration options
|
||||
* @throws Exception if php redis module is not loaded
|
||||
*/
|
||||
public function __construct($context, 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($context, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the redis backend
|
||||
*
|
||||
* @throws Exception if access to redis with password is denied or if database selection fails
|
||||
*/
|
||||
public function initializeObject()
|
||||
{
|
||||
$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) {
|
||||
if ($this->password !== '') {
|
||||
$success = $this->redis->auth($this->password);
|
||||
if (!$success) {
|
||||
throw new Exception('The given password was not accepted by the redis server.', 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for persistent connection
|
||||
*
|
||||
* @param bool $persistentConnection
|
||||
*/
|
||||
public function setPersistentConnection($persistentConnection)
|
||||
{
|
||||
$this->persistentConnection = $persistentConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for server hostname
|
||||
*
|
||||
* @param string $hostname Hostname
|
||||
*/
|
||||
public function setHostname($hostname)
|
||||
{
|
||||
$this->hostname = $hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for server port
|
||||
*
|
||||
* @param int $port Port
|
||||
*/
|
||||
public function setPort($port)
|
||||
{
|
||||
$this->port = $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for database number
|
||||
*
|
||||
* @param int $database Database
|
||||
* @throws \InvalidArgumentException if database number is not valid
|
||||
*/
|
||||
public function setDatabase($database)
|
||||
{
|
||||
if (!is_int($database)) {
|
||||
throw new \InvalidArgumentException('The specified database number is of type "' . gettype($database) . '" but an integer is expected.', 1279763057);
|
||||
}
|
||||
if ($database < 0) {
|
||||
throw new \InvalidArgumentException('The specified database "' . $database . '" must be greater or equal than zero.', 1279763534);
|
||||
}
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for authentication password
|
||||
*
|
||||
* @param string $password Password
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable data compression
|
||||
*
|
||||
* @param bool $compression TRUE to enable compression
|
||||
* @throws \InvalidArgumentException if compression parameter is not of type boolean
|
||||
*/
|
||||
public function setCompression($compression)
|
||||
{
|
||||
if (!is_bool($compression)) {
|
||||
throw new \InvalidArgumentException('The specified compression of type "' . gettype($compression) . '" but a boolean is expected.', 1289679153);
|
||||
}
|
||||
$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
|
||||
* @throws \InvalidArgumentException if compressionLevel parameter is not within allowed bounds
|
||||
*/
|
||||
public function setCompressionLevel($compressionLevel)
|
||||
{
|
||||
if (!is_int($compressionLevel)) {
|
||||
throw new \InvalidArgumentException('The specified compression of type "' . gettype($compressionLevel) . '" but an integer is expected.', 1289679154);
|
||||
}
|
||||
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
|
||||
* @throws \InvalidArgumentException if compressionLevel parameter is not within allowed bounds
|
||||
*/
|
||||
public function setConnectionTimeout($connectionTimeout)
|
||||
{
|
||||
if (!is_int($connectionTimeout)) {
|
||||
throw new \InvalidArgumentException('The specified connection timeout is of type "' . gettype($connectionTimeout) . '" but an integer is expected.', 1487849315);
|
||||
}
|
||||
|
||||
if ($connectionTimeout < 0) {
|
||||
throw new \InvalidArgumentException('The specified connection timeout "' . $connectionTimeout . '" must be greater or equal than zero.', 1487849326);
|
||||
}
|
||||
|
||||
$this->connectionTimeout = $connectionTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data in the cache
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n) with number of tags
|
||||
*
|
||||
* @param string $entryIdentifier Identifier for this specific cache entry
|
||||
* @param string $data Data to be stored
|
||||
* @param array $tags Tags to associate with this cache entry
|
||||
* @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, default lifetime is used. "0" means unlimited lifetime.
|
||||
* @throws \InvalidArgumentException if identifier is not valid
|
||||
* @throws InvalidDataException if data is not a string
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!$this->canBeUsedInStringContext($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006651);
|
||||
}
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1279469941);
|
||||
}
|
||||
$lifetime = $lifetime ?? $this->defaultLifetime;
|
||||
if (!is_int($lifetime)) {
|
||||
throw new \InvalidArgumentException('The specified lifetime is of type "' . gettype($lifetime) . '" but an integer or NULL is expected.', 1279488008);
|
||||
}
|
||||
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(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
|
||||
$addTags = $tags;
|
||||
$removeTags = [];
|
||||
$existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $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(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
|
||||
$queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
|
||||
}
|
||||
foreach ($addTags as $tag) {
|
||||
$queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
|
||||
$queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
|
||||
}
|
||||
$queue->exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from the cache.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
*
|
||||
* @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
|
||||
* @throws \InvalidArgumentException if identifier is not a string
|
||||
*/
|
||||
public function get($entryIdentifier)
|
||||
{
|
||||
if (!$this->canBeUsedInStringContext($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006652);
|
||||
}
|
||||
$storedEntry = false;
|
||||
if ($this->connected) {
|
||||
$storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
|
||||
}
|
||||
if ($this->compression && (string)$storedEntry !== '') {
|
||||
$storedEntry = gzuncompress($storedEntry);
|
||||
}
|
||||
return $storedEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
*
|
||||
* @param string $entryIdentifier Identifier specifying the cache entry
|
||||
* @return bool TRUE if such an entry exists, FALSE if not
|
||||
* @throws \InvalidArgumentException if identifier is not a string
|
||||
*/
|
||||
public function has($entryIdentifier)
|
||||
{
|
||||
if (!$this->canBeUsedInStringContext($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006653);
|
||||
}
|
||||
return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
* Scales O(n) with number of tags
|
||||
*
|
||||
* @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
|
||||
* @throws \InvalidArgumentException if identifier is not a string
|
||||
*/
|
||||
public function remove($entryIdentifier)
|
||||
{
|
||||
if (!$this->canBeUsedInStringContext($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006654);
|
||||
}
|
||||
$elementsDeleted = false;
|
||||
if ($this->connected) {
|
||||
if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
|
||||
$assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
foreach ($assignedTags as $tag) {
|
||||
$queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
|
||||
}
|
||||
$queue->del(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
|
||||
$queue->exec();
|
||||
$elementsDeleted = true;
|
||||
}
|
||||
}
|
||||
return $elementsDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param string $tag The tag to search for
|
||||
* @return array An array of entries with all matching entries. An empty array if no entries matched
|
||||
* @throws \InvalidArgumentException if tag is not a string
|
||||
*/
|
||||
public function findIdentifiersByTag($tag)
|
||||
{
|
||||
if (!$this->canBeUsedInStringContext($tag)) {
|
||||
throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006655);
|
||||
}
|
||||
$foundIdentifiers = [];
|
||||
if ($this->connected) {
|
||||
$foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
|
||||
}
|
||||
return $foundIdentifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*
|
||||
* Scales O(1) with number of cache entries
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
if ($this->connected) {
|
||||
$this->redis->flushDB();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param string $tag Tag the entries must have
|
||||
* @throws \InvalidArgumentException if identifier is not a string
|
||||
*/
|
||||
public function flushByTag($tag)
|
||||
{
|
||||
if (!$this->canBeUsedInStringContext($tag)) {
|
||||
throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006656);
|
||||
}
|
||||
if ($this->connected) {
|
||||
$identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
|
||||
if (!empty($identifiers)) {
|
||||
$this->removeIdentifierEntriesAndRelations($identifiers, [$tag]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 methods 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()
|
||||
{
|
||||
$identifierToTagsKeys = $this->redis->keys(self::IDENTIFIER_TAGS_PREFIX . '*');
|
||||
foreach ($identifierToTagsKeys as $identifierToTagsKey) {
|
||||
list(, $identifier) = explode(':', $identifierToTagsKey);
|
||||
// Check if the data entry still exists
|
||||
if (!$this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $identifier)) {
|
||||
$tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
|
||||
$queue = $this->redis->multi(\Redis::PIPELINE);
|
||||
$queue->del($identifierToTagsKey);
|
||||
foreach ($tagsToRemoveIdentifierFrom as $tag) {
|
||||
$queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $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
|
||||
*
|
||||
* @param array $identifiers List of identifiers to remove
|
||||
* @param array $tags List of tags to be handled
|
||||
*/
|
||||
protected function removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
|
||||
{
|
||||
// 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[] = self::IDENTIFIER_DATA_PREFIX . $identifier;
|
||||
$prefixedIdentifierToTagsKeysToDelete[] = self::IDENTIFIER_TAGS_PREFIX . $identifier;
|
||||
}
|
||||
foreach ($tags as $tag) {
|
||||
$prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $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(self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, $uniqueTempKey);
|
||||
}
|
||||
$queue->del(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
|
||||
$queue->exec();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to catch invalid identifiers and tags
|
||||
*
|
||||
* @param mixed $variable Variable to be checked
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeUsedInStringContext($variable)
|
||||
{
|
||||
return is_scalar($variable) || (is_object($variable) && method_exists($variable, '__toString'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
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
|
||||
{
|
||||
const SEPARATOR = '^';
|
||||
const EXPIRYTIME_FORMAT = 'YmdHis';
|
||||
const EXPIRYTIME_LENGTH = 14;
|
||||
const DATASIZE_DIGITS = 10;
|
||||
/**
|
||||
* Directory where the files are stored
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $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.
|
||||
*
|
||||
* @var string Temporary path to cache directory
|
||||
*/
|
||||
protected $temporaryCacheDirectory = '';
|
||||
|
||||
/**
|
||||
* A file extension to use for each cache entry.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cacheEntryFileExtension = '';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $cacheEntryIdentifiers = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $frozen = false;
|
||||
|
||||
/**
|
||||
* Sets a reference to the cache frontend which uses this backend and
|
||||
* initializes the default cache directory.
|
||||
*
|
||||
* @param FrontendInterface $cache The cache frontend
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setCache(FrontendInterface $cache)
|
||||
{
|
||||
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 . '/';
|
||||
if (!is_dir($finalCacheDirectory)) {
|
||||
$this->createFinalCacheDirectory($finalCacheDirectory);
|
||||
}
|
||||
unset($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.
|
||||
*
|
||||
* @param string $cacheDirectory The cache base directory. If a relative path
|
||||
* @throws Exception if the directory is not within allowed
|
||||
*/
|
||||
public function setCacheDirectory($cacheDirectory)
|
||||
{
|
||||
// Skip handling if directory is a stream ressource
|
||||
// This is used by unit tests with vfs:// directories
|
||||
if (strpos($cacheDirectory, '://')) {
|
||||
$this->temporaryCacheDirectory = $cacheDirectory;
|
||||
return;
|
||||
}
|
||||
$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 (GeneralUtility::isFirstPartOfStr($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()) {
|
||||
if (!empty($documentRoot) && strpos($cacheDirectory, $documentRoot) === 0) {
|
||||
$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.
|
||||
*
|
||||
* @param string $finalCacheDirectory Absolute path to final cache directory
|
||||
* @throws Exception If directory is not writable after creation
|
||||
*/
|
||||
protected function createFinalCacheDirectory($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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where the cache files are stored
|
||||
*
|
||||
* @return string Full path of the cache directory
|
||||
*/
|
||||
public function getCacheDirectory()
|
||||
{
|
||||
return $this->cacheDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves data in a cache file.
|
||||
*
|
||||
* @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
|
||||
* @param int $lifetime This cache backend does not support life times
|
||||
* @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.
|
||||
* @throws InvalidDataException if the data to bes stored is not a string.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1334756734);
|
||||
}
|
||||
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 . StringUtility::getUniqueId() . '.temp';
|
||||
$result = file_put_contents($temporaryCacheEntryPathAndFilename, $data);
|
||||
GeneralUtility::fixPermissions($temporaryCacheEntryPathAndFilename);
|
||||
if ($result === false) {
|
||||
throw new Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1334756737);
|
||||
}
|
||||
$cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename);
|
||||
if ($this->cacheEntryFileExtension === '.php') {
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($cacheEntryPathAndFilename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from a cache file.
|
||||
*
|
||||
* @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
|
||||
* @throws \InvalidArgumentException If identifier is invalid
|
||||
*/
|
||||
public function get($entryIdentifier)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
* @return bool TRUE if such an entry exists, FALSE if not
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function has($entryIdentifier)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry.
|
||||
*
|
||||
* @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
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function remove($entryIdentifier)
|
||||
{
|
||||
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);
|
||||
}
|
||||
try {
|
||||
unlink($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
GeneralUtility::flushDirectory($this->cacheDirectory, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given cache entry files are still valid or if their
|
||||
* lifetime has exceeded.
|
||||
*
|
||||
* @param string $cacheEntryPathAndFilename
|
||||
* @return bool
|
||||
*/
|
||||
protected function isCacheFileExpired($cacheEntryPathAndFilename)
|
||||
{
|
||||
return file_exists($cacheEntryPathAndFilename) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not necessary
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the cache entry for the specified identifier.
|
||||
*
|
||||
* @param string $entryIdentifier The cache entry identifier
|
||||
* @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
|
||||
*/
|
||||
protected function findCacheFilesByIdentifier($entryIdentifier)
|
||||
{
|
||||
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
||||
return file_exists($pathAndFilename) ? [$pathAndFilename] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function requireOnce($entryIdentifier)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function require(string $entryIdentifier)
|
||||
{
|
||||
$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,44 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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($tag);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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($tag);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries during one script run.
|
||||
*/
|
||||
class TransientMemoryBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $entries = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $tagsAndEntries = [];
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param int $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.
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('No cache frontend has been set yet via setCache().', 1238244992);
|
||||
}
|
||||
$this->entries[$entryIdentifier] = $data;
|
||||
foreach ($tags as $tag) {
|
||||
$this->tagsAndEntries[$tag][$entryIdentifier] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
return $this->entries[$entryIdentifier] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
return isset($this->entries[$entryIdentifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
*
|
||||
* @param string $entryIdentifier Specifies the cache entry to remove
|
||||
* @return bool TRUE if the entry could be removed or FALSE if no entry was found
|
||||
*/
|
||||
public function remove($entryIdentifier)
|
||||
{
|
||||
if (isset($this->entries[$entryIdentifier])) {
|
||||
unset($this->entries[$entryIdentifier]);
|
||||
foreach ($this->tagsAndEntries as $tag => $_) {
|
||||
if (isset($this->tagsAndEntries[$tag][$entryIdentifier])) {
|
||||
unset($this->tagsAndEntries[$tag][$entryIdentifier]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
if (isset($this->tagsAndEntries[$tag])) {
|
||||
return array_keys($this->tagsAndEntries[$tag]);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
$this->entries = [];
|
||||
$this->tagsAndEntries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Doctrine\DBAL\FetchMode;
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
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)
|
||||
*/
|
||||
const FAKED_UNLIMITED_EXPIRE = 2145909600;
|
||||
/**
|
||||
* @var string Name of the cache data table
|
||||
*/
|
||||
protected $cacheTable;
|
||||
|
||||
/**
|
||||
* @var string Name of the cache tags table
|
||||
*/
|
||||
protected $tagsTable;
|
||||
|
||||
/**
|
||||
* @var bool Indicates whether data is compressed or not (requires php zlib)
|
||||
*/
|
||||
protected $compression = false;
|
||||
|
||||
/**
|
||||
* @var int -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
|
||||
*/
|
||||
protected $compressionLevel = -1;
|
||||
|
||||
/**
|
||||
* @var int Maximum lifetime to stay with expire field below FAKED_UNLIMITED_LIFETIME
|
||||
*/
|
||||
protected $maximumLifetime;
|
||||
|
||||
/**
|
||||
* Set cache frontend instance and calculate data and tags table name
|
||||
*
|
||||
* @param FrontendInterface $cache The frontend for this backend
|
||||
*/
|
||||
public function setCache(FrontendInterface $cache)
|
||||
{
|
||||
parent::setCache($cache);
|
||||
$this->cacheTable = 'cf_' . $this->cacheIdentifier;
|
||||
$this->tagsTable = 'cf_' . $this->cacheIdentifier . '_tags';
|
||||
$this->maximumLifetime = self::FAKED_UNLIMITED_EXPIRE - $GLOBALS['EXEC_TIME'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves data in a cache file.
|
||||
*
|
||||
* @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
|
||||
* @param int $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 to be stored is not a string.
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException(
|
||||
'The specified data is of type "' . gettype($data) . '" but a string is expected.',
|
||||
1236518298
|
||||
);
|
||||
}
|
||||
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']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data from a cache file.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier which describes the cache entry to load
|
||||
* @return mixed The cache entry's data as a string or FALSE if the cache entry could not be loaded
|
||||
*/
|
||||
public function get($entryIdentifier)
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($this->cacheTable);
|
||||
$cacheRow = $queryBuilder->select('content')
|
||||
->from($this->cacheTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'identifier',
|
||||
$queryBuilder->createNamedParameter($entryIdentifier, \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->execute()
|
||||
->fetch();
|
||||
$content = '';
|
||||
if (!empty($cacheRow)) {
|
||||
$content = $cacheRow['content'];
|
||||
}
|
||||
if ($this->compression && (string)$content !== '') {
|
||||
$content = gzuncompress($content);
|
||||
}
|
||||
return !empty($cacheRow) ? $content : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cache entry with the specified identifier exists.
|
||||
*
|
||||
* @param string $entryIdentifier Specifies the identifier to check for existence
|
||||
* @return bool TRUE if such an entry exists, FALSE if not
|
||||
*/
|
||||
public function has($entryIdentifier)
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($this->cacheTable);
|
||||
$count = $queryBuilder->count('*')
|
||||
->from($this->cacheTable)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'identifier',
|
||||
$queryBuilder->createNamedParameter($entryIdentifier, \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->execute()
|
||||
->fetchColumn(0);
|
||||
return (bool)$count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries matching the specified identifier.
|
||||
* Usually this only affects one entry.
|
||||
*
|
||||
* @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($entryIdentifier)
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
$numberOfRowsRemoved = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->cacheTable)
|
||||
->delete(
|
||||
$this->cacheTable,
|
||||
['identifier' => $entryIdentifier]
|
||||
);
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->tagsTable)
|
||||
->delete(
|
||||
$this->tagsTable,
|
||||
['identifier' => $entryIdentifier]
|
||||
);
|
||||
return (bool)$numberOfRowsRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entries 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($tag)
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
$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, \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
$this->cacheTable . '.expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->groupBy($this->cacheTable . '.identifier')
|
||||
->execute();
|
||||
$identifiers = $result->fetchAll(FetchMode::COLUMN, 0);
|
||||
return array_combine($identifiers, $identifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->cacheTable)
|
||||
->truncate($this->cacheTable);
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->tagsTable)
|
||||
->truncate($this->tagsTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries tagged by any of the specified tags. Performs the SQL
|
||||
* operation as a bulk query for better performance.
|
||||
*
|
||||
* @param string[] $tags
|
||||
*/
|
||||
public function flushByTags(array $tags)
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
|
||||
if (empty($tags)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Connection $connection */
|
||||
$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) {
|
||||
array_walk(array_chunk($tags, 100), [$this, 'flushByTags']);
|
||||
return;
|
||||
}
|
||||
// VERY simple quoting of tags is sufficient here for performance. Tags are already
|
||||
// validated to not contain any bad characters, e.g. they are automatically generated
|
||||
// inside this class and suffixed with a pure integer enforced by DB.
|
||||
$quotedTagList = array_map(function ($value) {
|
||||
return '\'' . $value . '\'';
|
||||
}, $tags);
|
||||
|
||||
if ($this->isConnectionMysql($connection)) {
|
||||
// Use a optimized query on mysql ... don't use on your own
|
||||
// * ansi sql does not know about multi table delete
|
||||
// * doctrine query builder does not support join on delete()
|
||||
$connection->executeQuery(
|
||||
'DELETE tags2, cache1'
|
||||
. ' FROM ' . $this->tagsTable . ' AS tags1'
|
||||
. ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
|
||||
. ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
|
||||
. ' WHERE tags1.tag IN (' . implode(',', $quotedTagList) . ')'
|
||||
);
|
||||
} else {
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->tagsTable)
|
||||
->where('tag IN (' . implode(',', $quotedTagList) . ')')
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->execute();
|
||||
$cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
|
||||
$quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
|
||||
$queryBuilder->delete($this->cacheTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->execute();
|
||||
$queryBuilder->delete($this->tagsTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
|
||||
if (empty($tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Connection $connection */
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
|
||||
$quotedTag = '\'' . $tag . '\'';
|
||||
|
||||
if ($this->isConnectionMysql($connection)) {
|
||||
// Use a optimized query on mysql ... don't use on your own
|
||||
// * ansi sql does not know about multi table delete
|
||||
// * doctrine query builder does not support join on delete()
|
||||
$connection->executeQuery(
|
||||
'DELETE tags2, cache1'
|
||||
. ' FROM ' . $this->tagsTable . ' AS tags1'
|
||||
. ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier'
|
||||
. ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier'
|
||||
. ' WHERE tags1.tag = ' . $quotedTag
|
||||
);
|
||||
} else {
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->tagsTable)
|
||||
->where('tag = ' . $quotedTag)
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->execute();
|
||||
$cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
|
||||
$quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
|
||||
$queryBuilder->delete($this->cacheTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->execute();
|
||||
$queryBuilder->delete($this->tagsTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable);
|
||||
if ($this->isConnectionMysql($connection)) {
|
||||
// Use a optimized query on mysql ... don't use on your own
|
||||
// * ansi sql does not know about multi table delete
|
||||
// * doctrine query builder does not support join on delete()
|
||||
// First delete all expired rows from cache table and their connected tag rows
|
||||
$connection->executeQuery(
|
||||
'DELETE cache, tags'
|
||||
. ' FROM ' . $this->cacheTable . ' AS cache'
|
||||
. ' LEFT OUTER JOIN ' . $this->tagsTable . ' AS tags ON cache.identifier = tags.identifier'
|
||||
. ' WHERE cache.expires < ?',
|
||||
[(int)$GLOBALS['EXEC_TIME']]
|
||||
);
|
||||
// Then delete possible "orphaned" rows from tags table - tags that have no cache row for whatever reason
|
||||
$connection->executeQuery(
|
||||
'DELETE tags'
|
||||
. ' FROM ' . $this->tagsTable . ' AS tags'
|
||||
. ' LEFT OUTER JOIN ' . $this->cacheTable . ' as cache ON tags.identifier = cache.identifier'
|
||||
. ' WHERE cache.identifier IS NULL'
|
||||
);
|
||||
} else {
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$result = $queryBuilder->select('identifier')
|
||||
->from($this->cacheTable)
|
||||
->where($queryBuilder->expr()->lt(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
|
||||
))
|
||||
// group by is like DISTINCT and used here to suppress possible duplicate identifiers
|
||||
->groupBy('identifier')
|
||||
->execute();
|
||||
|
||||
// Get identifiers of expired cache entries
|
||||
$cacheEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
|
||||
if (!empty($cacheEntryIdentifiers)) {
|
||||
// Delete tag rows connected to expired cache entries
|
||||
$quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY);
|
||||
$queryBuilder->delete($this->tagsTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->execute();
|
||||
}
|
||||
$queryBuilder->delete($this->cacheTable)
|
||||
->where($queryBuilder->expr()->lt(
|
||||
'expires',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
|
||||
))
|
||||
->execute();
|
||||
|
||||
// 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')
|
||||
->execute();
|
||||
$tagsEntryIdentifiers = $result->fetchAll(FetchMode::COLUMN, 0);
|
||||
|
||||
if (!empty($tagsEntryIdentifiers)) {
|
||||
$quotedIdentifiers = $queryBuilder->createNamedParameter($tagsEntryIdentifiers, Connection::PARAM_STR_ARRAY);
|
||||
$queryBuilder->delete($this->tagsTable)
|
||||
->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table where the cache entries are stored.
|
||||
*
|
||||
* @return string The cache table.
|
||||
*/
|
||||
public function getCacheTable()
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
return $this->cacheTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table where cache tags are stored.
|
||||
*
|
||||
* @return string Name of the table storing tags
|
||||
*/
|
||||
public function getTagsTable()
|
||||
{
|
||||
$this->throwExceptionIfFrontendDoesNotExist();
|
||||
return $this->tagsTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable data compression
|
||||
*
|
||||
* @param bool $compression TRUE to enable compression
|
||||
*/
|
||||
public function setCompression($compression)
|
||||
{
|
||||
$this->compression = $compression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data compression level.
|
||||
* If compression is enabled and this is not set,
|
||||
* gzcompress default level will be used
|
||||
*
|
||||
* @param int -1 to 9: Compression level
|
||||
*/
|
||||
public function setCompressionLevel($compressionLevel)
|
||||
{
|
||||
if ($compressionLevel >= -1 && $compressionLevel <= 9) {
|
||||
$this->compressionLevel = $compressionLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This database backend uses some optimized queries for mysql
|
||||
* to get maximum performance.
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @return bool
|
||||
*/
|
||||
protected function isConnectionMysql(Connection $connection): bool
|
||||
{
|
||||
$serverVersion = $connection->getServerVersion();
|
||||
return (bool)(strpos($serverVersion, 'MySQL') === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if required frontend instance exists
|
||||
*
|
||||
* @throws Exception If there is no frontend instance in $this->cache
|
||||
*/
|
||||
protected function throwExceptionIfFrontendDoesNotExist()
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('No cache frontend has been set via setCache() yet.', 1236518288);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
$cacheTableSql = file_get_contents(
|
||||
ExtensionManagementUtility::extPath('core') .
|
||||
'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
|
||||
);
|
||||
$requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
|
||||
$tagsTableSql = file_get_contents(
|
||||
ExtensionManagementUtility::extPath('core') .
|
||||
'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
|
||||
);
|
||||
$requiredTableStructures .= str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF;
|
||||
return $requiredTableStructures;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Backend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Exception;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
|
||||
/**
|
||||
* A caching backend which stores cache entries by using wincache.
|
||||
*
|
||||
* 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. By default prefix consists from two parts
|
||||
* separated by underscore character and ends in yet another underscore character:
|
||||
* - "TYPO3"
|
||||
* - MD5 of script path and filename and SAPI name
|
||||
* This prefix makes sure that keys from the different installations do not
|
||||
* conflict.
|
||||
*/
|
||||
class WincacheBackend extends AbstractBackend implements TaggableBackendInterface
|
||||
{
|
||||
/**
|
||||
* A prefix to separate stored data from other data possible stored in the wincache
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $identifierPrefix;
|
||||
|
||||
/**
|
||||
* Constructs this backend
|
||||
*
|
||||
* @param string $context Unused, for backward compatibility only
|
||||
* @param array $options Configuration options
|
||||
* @throws Exception If wincache PHP extension is not loaded
|
||||
*/
|
||||
public function __construct($context, array $options = [])
|
||||
{
|
||||
if (!extension_loaded('wincache')) {
|
||||
throw new Exception('The PHP extension "wincache" must be installed and loaded in order to use the wincache backend.', 1343331520);
|
||||
}
|
||||
parent::__construct($context, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param int $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 \InvalidArgumentException if the identifier is not valid
|
||||
* @throws InvalidDataException if $data is not a string
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('No cache frontend has been set yet via setCache().', 1343331521);
|
||||
}
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1343331522);
|
||||
}
|
||||
$tags[] = '%WCBE%' . $this->cache->getIdentifier();
|
||||
$expiration = $lifetime ?? $this->defaultLifetime;
|
||||
$success = wincache_ucache_set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
|
||||
if ($success === true) {
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
$this->addIdentifierToTags($entryIdentifier, $tags);
|
||||
} else {
|
||||
throw new Exception('Could not set value.', 1343331523);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
$success = false;
|
||||
$value = wincache_ucache_get($this->identifierPrefix . $entryIdentifier, $success);
|
||||
return $success ? $value : $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
return wincache_ucache_exists($this->identifierPrefix . $entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
$this->removeIdentifierFromAllTags($entryIdentifier);
|
||||
return wincache_ucache_delete($this->identifierPrefix . $entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$success = false;
|
||||
$identifiers = wincache_ucache_get($this->identifierPrefix . 'tag_' . $tag, $success);
|
||||
if ($success === false) {
|
||||
return [];
|
||||
}
|
||||
return (array)$identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return array Array with tags
|
||||
*/
|
||||
protected function findTagsByIdentifier($identifier)
|
||||
{
|
||||
$success = false;
|
||||
$tags = wincache_ucache_get($this->identifierPrefix . 'ident_' . $identifier, $success);
|
||||
return $success ? (array)$tags : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
if (!$this->cache instanceof FrontendInterface) {
|
||||
throw new Exception('Yet no cache frontend has been set via setCache().', 1343331524);
|
||||
}
|
||||
$this->flushByTag('%WCBE%' . $this->cache->getIdentifier());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($tag)
|
||||
{
|
||||
$identifiers = $this->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$this->remove($identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the identifier with the given tags
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
* @param array $tags
|
||||
*/
|
||||
protected function addIdentifierToTags($entryIdentifier, array $tags)
|
||||
{
|
||||
// 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;
|
||||
wincache_ucache_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) {
|
||||
wincache_ucache_set($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes association of the identifier with the given tags
|
||||
*
|
||||
* @param string $entryIdentifier
|
||||
*/
|
||||
protected function removeIdentifierFromAllTags($entryIdentifier)
|
||||
{
|
||||
// Get tags for this identifier
|
||||
$tags = $this->findTagsByIdentifier($entryIdentifier);
|
||||
// Deassociate 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)) {
|
||||
wincache_ucache_set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
|
||||
} else {
|
||||
wincache_ucache_delete($this->identifierPrefix . 'tag_' . $tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear reverse tag index for this identifier
|
||||
wincache_ucache_delete($this->identifierPrefix . 'ident_' . $entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing, as wincache does GC itself
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
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\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;
|
||||
|
||||
/**
|
||||
* The Cache Manager
|
||||
*/
|
||||
class CacheManager implements SingletonInterface
|
||||
{
|
||||
/**
|
||||
* @var FrontendInterface[]
|
||||
*/
|
||||
protected $caches = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $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.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cacheGroups = [];
|
||||
|
||||
/**
|
||||
* @var array Default cache configuration as fallback
|
||||
*/
|
||||
protected $defaultCacheConfiguration = [
|
||||
'frontend' => VariableFrontend::class,
|
||||
'backend' => Typo3DatabaseBackend::class,
|
||||
'options' => [],
|
||||
'groups' => ['all']
|
||||
];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $disableCaching = false;
|
||||
|
||||
/**
|
||||
* Used by Bootstrap to define whether the configuration has been set finally.
|
||||
* Controls whether a deprecation warning is logged in getCache().
|
||||
* This property will be removed in TYPO3 v10.0.
|
||||
*
|
||||
* @var bool
|
||||
* @internal
|
||||
*/
|
||||
protected $limbo = false;
|
||||
|
||||
/**
|
||||
* @param bool $disableCaching
|
||||
*/
|
||||
public function __construct(bool $disableCaching = false)
|
||||
{
|
||||
$this->disableCaching = $disableCaching;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $cacheConfigurations The cache configurations to set
|
||||
* @throws \InvalidArgumentException If $cacheConfigurations is not an array
|
||||
*/
|
||||
public function setCacheConfigurations(array $cacheConfigurations)
|
||||
{
|
||||
foreach ($cacheConfigurations as $identifier => $configuration) {
|
||||
if (!is_array($configuration)) {
|
||||
throw new \InvalidArgumentException('The cache configuration for cache "' . $identifier . '" was not an array as expected.', 1231259656);
|
||||
}
|
||||
$this->cacheConfigurations[$identifier] = $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a cache so it can be retrieved at a later point.
|
||||
*
|
||||
* @param FrontendInterface $cache The cache frontend to be registered
|
||||
* @throws DuplicateIdentifierException if a cache with the given identifier has already been registered.
|
||||
*/
|
||||
public function registerCache(FrontendInterface $cache)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cache specified by $identifier
|
||||
*
|
||||
* @param string $identifier Identifies which cache to return
|
||||
* @return FrontendInterface The specified cache frontend
|
||||
* @throws NoSuchCacheException
|
||||
*/
|
||||
public function getCache($identifier)
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @param string $identifier The identifier of the cache
|
||||
* @return bool TRUE if a cache with the given identifier exists, otherwise FALSE
|
||||
*/
|
||||
public function hasCache($identifier)
|
||||
{
|
||||
return isset($this->caches[$identifier]) || isset($this->cacheConfigurations[$identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes all registered caches
|
||||
*/
|
||||
public function flushCaches()
|
||||
{
|
||||
$this->createAllCaches();
|
||||
foreach ($this->caches as $cache) {
|
||||
$cache->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes all registered caches of a specific group
|
||||
*
|
||||
* @param string $groupIdentifier
|
||||
* @throws NoSuchCacheGroupException
|
||||
*/
|
||||
public function flushCachesInGroup($groupIdentifier)
|
||||
{
|
||||
$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.
|
||||
*
|
||||
* @param string $groupIdentifier
|
||||
* @param string|array $tag Tag to search for
|
||||
* @throws NoSuchCacheGroupException
|
||||
*/
|
||||
public function flushCachesInGroupByTag($groupIdentifier, $tag)
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @param string $groupIdentifier
|
||||
* @param string[] $tags Tags to search for
|
||||
* @throws NoSuchCacheGroupException
|
||||
*/
|
||||
public function flushCachesInGroupByTags($groupIdentifier, array $tags)
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @param string $tag Tag to search for
|
||||
*/
|
||||
public function flushCachesByTag($tag)
|
||||
{
|
||||
$this->createAllCaches();
|
||||
foreach ($this->caches as $cache) {
|
||||
$cache->flushByTag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes entries tagged by any of the specified tags in all registered caches.
|
||||
*
|
||||
* @param string[] $tags Tags to search for
|
||||
*/
|
||||
public function flushCachesByTags(array $tags)
|
||||
{
|
||||
$this->createAllCaches();
|
||||
foreach ($this->caches as $cache) {
|
||||
$cache->flushByTags($tags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates all registered caches.
|
||||
*/
|
||||
protected function createAllCaches()
|
||||
{
|
||||
foreach ($this->cacheConfigurations as $identifier => $_) {
|
||||
if (!isset($this->caches[$identifier])) {
|
||||
$this->createCache($identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates the cache for $identifier.
|
||||
*
|
||||
* @param string $identifier
|
||||
* @throws DuplicateIdentifierException
|
||||
* @throws InvalidBackendException
|
||||
* @throws InvalidCacheException
|
||||
*/
|
||||
protected function createCache($identifier)
|
||||
{
|
||||
// @deprecated will be removed with TYPO3 v10.0
|
||||
if ($this->limbo) {
|
||||
trigger_error('Usage of ' . self::class . '->createCache(\'' . $identifier . '\') in ext_localconf.php will not be supported in TYPO3 v10.0.', E_USER_DEPRECATED);
|
||||
}
|
||||
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'];
|
||||
}
|
||||
|
||||
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('production', $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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the limbo state
|
||||
*
|
||||
* If limbo is enable, then getCache() will log a deprecation warning.
|
||||
* This method will be removed in TYPO3 v10.0.
|
||||
*
|
||||
* @param bool $limbo
|
||||
* @internal
|
||||
*/
|
||||
public function setLimbo(bool $limbo)
|
||||
{
|
||||
$this->limbo = $limbo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* This service provides the sql schema for the caching framework
|
||||
*/
|
||||
class DatabaseSchemaService
|
||||
{
|
||||
/**
|
||||
* Get schema SQL of required cache framework tables.
|
||||
*
|
||||
* This method needs ext_localconf and ext_tables loaded!
|
||||
*
|
||||
* @return string Cache framework SQL
|
||||
*/
|
||||
public function getCachingFrameworkRequiredDatabaseSchema()
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* A slot method to inject the required caching framework database tables to the
|
||||
* tables definitions string
|
||||
*
|
||||
* @param array $sqlString
|
||||
* @return array
|
||||
*/
|
||||
public function addCachingFrameworkRequiredDatabaseSchemaForSqlExpectedSchemaService(array $sqlString)
|
||||
{
|
||||
$sqlString[] = $this->getCachingFrameworkRequiredDatabaseSchema();
|
||||
return [$sqlString];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A generic Cache exception
|
||||
*/
|
||||
class Exception extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A "Duplicate Identifier" exception
|
||||
*/
|
||||
class DuplicateIdentifierException extends \TYPO3\CMS\Core\Cache\Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* An "Invalid Backend" exception
|
||||
*/
|
||||
class InvalidBackendException extends \TYPO3\CMS\Core\Cache\Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* An "Invalid Cache" exception
|
||||
*/
|
||||
class InvalidCacheException extends \TYPO3\CMS\Core\Cache\Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* An "Invalid Data" exception
|
||||
*/
|
||||
class InvalidDataException extends \TYPO3\CMS\Core\Cache\Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A "No Such Cache" exception
|
||||
*/
|
||||
class NoSuchCacheException extends \TYPO3\CMS\Core\Cache\Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A "No Such Cache Group" exception
|
||||
*/
|
||||
class NoSuchCacheGroupException extends \TYPO3\CMS\Core\Cache\Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\AbstractBackend;
|
||||
use TYPO3\CMS\Core\Cache\Backend\BackendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* An abstract cache
|
||||
*/
|
||||
abstract class AbstractFrontend implements FrontendInterface
|
||||
{
|
||||
/**
|
||||
* Identifies this cache
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $identifier;
|
||||
|
||||
/**
|
||||
* @var AbstractBackend|TaggableBackendInterface
|
||||
*/
|
||||
protected $backend;
|
||||
|
||||
/**
|
||||
* Constructs the cache
|
||||
*
|
||||
* @param string $identifier An identifier which describes this cache
|
||||
* @param BackendInterface $backend Backend to be used for this cache
|
||||
* @throws \InvalidArgumentException if the identifier doesn't match PATTERN_ENTRYIDENTIFIER
|
||||
*/
|
||||
public function __construct($identifier, 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 = $backend;
|
||||
$this->backend->setCache($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this cache's identifier
|
||||
*
|
||||
* @return string The identifier for this cache
|
||||
*/
|
||||
public function getIdentifier()
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the backend used by this cache
|
||||
*
|
||||
* @return BackendInterface The backend used by this cache
|
||||
*/
|
||||
public function getBackend()
|
||||
{
|
||||
return $this->backend;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws \InvalidArgumentException If $entryIdentifier is invalid
|
||||
*/
|
||||
public function has($entryIdentifier)
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058486);
|
||||
}
|
||||
return $this->backend->has($entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function remove($entryIdentifier)
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058495);
|
||||
}
|
||||
return $this->backend->remove($entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
$this->backend->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged by any of the specified tags.
|
||||
*
|
||||
* @param string[] $tags
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function flushByTags(array $tags)
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057360);
|
||||
}
|
||||
}
|
||||
if ($this->backend instanceof TaggableBackendInterface) {
|
||||
$this->backend->flushByTags($tags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache which are tagged by the specified tag.
|
||||
*
|
||||
* @param string $tag The tag the entries must have
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function flushByTag($tag)
|
||||
{
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057359);
|
||||
}
|
||||
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/cache/frontend/class.t3lib_cache_frontend_abstractfrontend.php']['flushByTag'] ?? [] as $_funcRef) {
|
||||
$params = ['tag' => $tag];
|
||||
GeneralUtility::callUserFunction($_funcRef, $params, $this);
|
||||
}
|
||||
|
||||
if ($this->backend instanceof TaggableBackendInterface) {
|
||||
$this->backend->flushByTag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage()
|
||||
{
|
||||
$this->backend->collectGarbage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the validity of an entry identifier. Returns TRUE if it's valid.
|
||||
*
|
||||
* @param string $identifier An identifier to be checked for validity
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidEntryIdentifier($identifier)
|
||||
{
|
||||
return preg_match(self::PATTERN_ENTRYIDENTIFIER, $identifier) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the validity of a tag. Returns TRUE if it's valid.
|
||||
*
|
||||
* @param string|array $tag An identifier to be checked for validity
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidTag($tag)
|
||||
{
|
||||
if (!is_array($tag)) {
|
||||
return preg_match(self::PATTERN_TAG, $tag) === 1;
|
||||
}
|
||||
foreach ($tag as $tagValue) {
|
||||
if (!$this->isValidTag($tagValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contract for a Cache (frontend)
|
||||
*/
|
||||
interface FrontendInterface
|
||||
{
|
||||
/**
|
||||
* "Magic" tag for class-related entries
|
||||
*/
|
||||
const TAG_CLASS = '%CLASS%';
|
||||
/**
|
||||
* "Magic" tag for package-related entries
|
||||
*/
|
||||
const TAG_PACKAGE = '%PACKAGE%';
|
||||
/**
|
||||
* Pattern an entry identifer must match.
|
||||
*/
|
||||
const PATTERN_ENTRYIDENTIFIER = '/^[a-zA-Z0-9_%\\-&]{1,250}$/';
|
||||
/**
|
||||
* Pattern a tag must match.
|
||||
*/
|
||||
const PATTERN_TAG = '/^[a-zA-Z0-9_%\\-&]{1,250}$/';
|
||||
/**
|
||||
* Returns this cache's identifier
|
||||
*
|
||||
* @return string The identifier for this cache
|
||||
*/
|
||||
public function getIdentifier();
|
||||
|
||||
/**
|
||||
* Returns the backend used by this cache
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Cache\Backend\BackendInterface The backend used by this cache
|
||||
*/
|
||||
public function getBackend();
|
||||
|
||||
/**
|
||||
* 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 $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
*/
|
||||
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null);
|
||||
|
||||
/**
|
||||
* Finds and returns data from the cache.
|
||||
*
|
||||
* @param string $entryIdentifier Something which identifies the cache entry - depends on concrete cache
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($entryIdentifier);
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier);
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier);
|
||||
|
||||
/**
|
||||
* Removes all cache entries of this cache.
|
||||
*/
|
||||
public function flush();
|
||||
|
||||
/**
|
||||
* 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($tag);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Does garbage collection
|
||||
*/
|
||||
public function collectGarbage();
|
||||
|
||||
/**
|
||||
* Checks the validity of an entry identifier. Returns TRUE if it's valid.
|
||||
*
|
||||
* @param string $identifier An identifier to be checked for validity
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidEntryIdentifier($identifier);
|
||||
|
||||
/**
|
||||
* Checks the validity of a tag. Returns TRUE if it's valid.
|
||||
*
|
||||
* @param string $tag A tag to be checked for validity
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidTag($tag);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
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
|
||||
{
|
||||
/**
|
||||
* Constructs the cache
|
||||
*
|
||||
* @param string $identifier An identifier which describes this cache
|
||||
* @param PhpCapableBackendInterface $backend Backend to be used for this cache
|
||||
*/
|
||||
public function __construct($identifier, PhpCapableBackendInterface $backend)
|
||||
{
|
||||
parent::__construct($identifier, $backend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the PHP source code in the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier used for this cache entry, for example the class name
|
||||
* @param string $sourceCode PHP source code
|
||||
* @param array $tags Tags to associate with this cache entry
|
||||
* @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @throws \InvalidArgumentException If $entryIdentifier or $tags is invalid
|
||||
* @throws InvalidDataException If $sourceCode is not a string
|
||||
*/
|
||||
public function set($entryIdentifier, $sourceCode, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1264023823);
|
||||
}
|
||||
if (!is_string($sourceCode)) {
|
||||
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 . $sourceCode . LF . '#';
|
||||
$this->backend->set($entryIdentifier, $sourceCode, $tags, $lifetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns a variable value from the cache.
|
||||
*
|
||||
* @param string $entryIdentifier Identifier of the cache entry to fetch
|
||||
* @return string The value
|
||||
* @throws \InvalidArgumentException if the cache identifier is not valid
|
||||
*/
|
||||
public function get($entryIdentifier)
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057753);
|
||||
}
|
||||
return $this->backend->get($entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entries which are tagged by the specified tag.
|
||||
*
|
||||
* @param string $tag The tag to search for
|
||||
* @return array An array with the content of all matching entries. An empty array if no entries matched
|
||||
* @throws \InvalidArgumentException if the tag is not valid
|
||||
* @deprecated since TYPO3 v9, Avoid using this method since it is not compliant to PSR-6
|
||||
*/
|
||||
public function getByTag($tag)
|
||||
{
|
||||
trigger_error('PhpFrontend->getByTag() will be removed in TYPO3 v10.0. Avoid using this method since it is not compliant to PSR-6.', E_USER_DEPRECATED);
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057773);
|
||||
}
|
||||
$entries = [];
|
||||
$identifiers = $this->backend->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$entries[] = $this->backend->get($identifier);
|
||||
}
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($entryIdentifier)
|
||||
{
|
||||
return $this->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)
|
||||
{
|
||||
return $this->backend->require($entryIdentifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\BackendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
|
||||
|
||||
/**
|
||||
* A cache frontend for strings. Nothing else.
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 - use VariableFrontend instead.
|
||||
*/
|
||||
class StringFrontend extends AbstractFrontend
|
||||
{
|
||||
/**
|
||||
* @param string $identifier
|
||||
* @param BackendInterface $backend
|
||||
*/
|
||||
public function __construct($identifier, BackendInterface $backend)
|
||||
{
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Usage of class %s will be removed in TYPO3 v10.0, use %s instead',
|
||||
static::class,
|
||||
VariableFrontend::class
|
||||
),
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
parent::__construct($identifier, $backend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the value of a PHP variable in the cache.
|
||||
*
|
||||
* @param string $entryIdentifier An identifier used for this cache entry
|
||||
* @param string $string The variable to cache
|
||||
* @param array $tags Tags to associate with this cache entry
|
||||
* @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @throws \InvalidArgumentException if the identifier or tag is not valid
|
||||
* @throws InvalidDataException if the variable to cache is not of type string
|
||||
*/
|
||||
public function set($entryIdentifier, $string, array $tags = [], $lifetime = null)
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057566);
|
||||
}
|
||||
if (!is_string($string)) {
|
||||
throw new InvalidDataException('Given data is of type "' . gettype($string) . '", but a string is expected for string cache.', 1222808333);
|
||||
}
|
||||
foreach ($tags as $tag) {
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057512);
|
||||
}
|
||||
}
|
||||
$this->backend->set($entryIdentifier, $string, $tags, $lifetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns a variable value from the cache.
|
||||
*
|
||||
* @param string $entryIdentifier Identifier of the cache entry to fetch
|
||||
* @return string The value
|
||||
* @throws \InvalidArgumentException if the cache identifier is not valid
|
||||
*/
|
||||
public function get($entryIdentifier)
|
||||
{
|
||||
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
|
||||
throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057752);
|
||||
}
|
||||
return $this->backend->get($entryIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entries which are tagged by the specified tag.
|
||||
*
|
||||
* @param string $tag The tag to search for
|
||||
* @return array An array with the content of all matching entries. An empty array if no entries matched
|
||||
* @throws \InvalidArgumentException if the tag is not valid
|
||||
* @deprecated since TYPO3 v9, Avoid using this method since it is not compliant to PSR-6
|
||||
*/
|
||||
public function getByTag($tag)
|
||||
{
|
||||
trigger_error('StringFrontend->getByTag() will be removed in TYPO3 v10.0. Avoid using this method since it is not compliant to PSR-6.', E_USER_DEPRECATED);
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057772);
|
||||
}
|
||||
$entries = [];
|
||||
$identifiers = $this->backend->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$entries[] = $this->backend->get($identifier);
|
||||
}
|
||||
return $entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Cache\Frontend;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\TransientBackendInterface;
|
||||
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 string $entryIdentifier An identifier used for this cache entry
|
||||
* @param mixed $variable The variable to cache
|
||||
* @param array $tags Tags to associate with this cache entry
|
||||
* @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
|
||||
* @throws \InvalidArgumentException if the identifier or tag is not valid
|
||||
*/
|
||||
public function set($entryIdentifier, $variable, array $tags = [], $lifetime = null)
|
||||
{
|
||||
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' => &$variable,
|
||||
'tags' => &$tags,
|
||||
'lifetime' => &$lifetime
|
||||
];
|
||||
GeneralUtility::callUserFunction($_funcRef, $params, $this);
|
||||
}
|
||||
if (!$this->backend instanceof TransientBackendInterface) {
|
||||
$variable = serialize($variable);
|
||||
}
|
||||
$this->backend->set($entryIdentifier, $variable, $tags, $lifetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns a variable value from the cache.
|
||||
*
|
||||
* @param string $entryIdentifier Identifier of the cache entry to fetch
|
||||
*
|
||||
* @return mixed The value
|
||||
* @throws \InvalidArgumentException if the identifier is not valid
|
||||
*/
|
||||
public function get($entryIdentifier)
|
||||
{
|
||||
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;
|
||||
}
|
||||
return $this->backend instanceof TransientBackendInterface ? $rawResult : unserialize($rawResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns all cache entries which are tagged by the specified tag.
|
||||
*
|
||||
* @param string $tag The tag to search for
|
||||
*
|
||||
* @return array An array with the content of all matching entries. An empty array if no entries matched
|
||||
* @throws \InvalidArgumentException if the tag is not valid
|
||||
* @deprecated since TYPO3 v9, Avoid using this method since it is not compliant to PSR-6
|
||||
*/
|
||||
public function getByTag($tag)
|
||||
{
|
||||
trigger_error('VariableFrontend->getByTag() will be removed in TYPO3 v10.0. Avoid using this method since it is not compliant to PSR-6.', E_USER_DEPRECATED);
|
||||
if (!$this->isValidTag($tag)) {
|
||||
throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233058312);
|
||||
}
|
||||
$entries = [];
|
||||
$identifiers = $this->backend->findIdentifiersByTag($tag);
|
||||
foreach ($identifiers as $identifier) {
|
||||
$rawResult = $this->backend->get($identifier);
|
||||
if ($rawResult !== false) {
|
||||
$entries[] = $this->backend instanceof TransientBackendInterface ? $rawResult : unserialize($rawResult);
|
||||
}
|
||||
}
|
||||
return $entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Category;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class to register category configurations.
|
||||
*/
|
||||
class CategoryRegistry implements SingletonInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $registry = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $extensions = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $addedCategoryTabs = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $template = '';
|
||||
|
||||
/**
|
||||
* Returns a class instance
|
||||
*
|
||||
* @return CategoryRegistry
|
||||
* @internal
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
return GeneralUtility::makeInstance(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates this object.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->template = str_repeat(PHP_EOL, 3) . 'CREATE TABLE %s (' . PHP_EOL
|
||||
. ' %s int(11) DEFAULT \'0\' NOT NULL' . PHP_EOL . ');' . str_repeat(PHP_EOL, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new category configuration to this registry.
|
||||
* TCA changes are directly applied
|
||||
*
|
||||
* @param string $extensionKey Extension key to be used
|
||||
* @param string $tableName Name of the table to be registered
|
||||
* @param string $fieldName Name of the field to be registered
|
||||
* @param array $options Additional configuration options
|
||||
* + fieldList: field configuration to be added to showitems
|
||||
* + typesList: list of types that shall visualize the categories field
|
||||
* + position: insert position of the categories field
|
||||
* + label: backend label of the categories field
|
||||
* + fieldConfiguration: TCA field config array to override defaults
|
||||
* @param bool $override If TRUE, any category configuration for the same table / field is removed before the new configuration is added
|
||||
* @return bool
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
* @internal
|
||||
*/
|
||||
public function add($extensionKey, $tableName, $fieldName = 'categories', array $options = [], $override = false)
|
||||
{
|
||||
$didRegister = false;
|
||||
if (empty($tableName) || !is_string($tableName)) {
|
||||
throw new \InvalidArgumentException('No or invalid table name "' . $tableName . '" given.', 1369122038);
|
||||
}
|
||||
if (empty($extensionKey) || !is_string($extensionKey)) {
|
||||
throw new \InvalidArgumentException('No or invalid extension key "' . $extensionKey . '" given.', 1397836158);
|
||||
}
|
||||
|
||||
if ($override) {
|
||||
$this->remove($tableName, $fieldName);
|
||||
}
|
||||
|
||||
if (!$this->isRegistered($tableName, $fieldName)) {
|
||||
$this->registry[$tableName][$fieldName] = $options;
|
||||
$this->extensions[$extensionKey][$tableName][$fieldName] = $fieldName;
|
||||
|
||||
if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
|
||||
$this->applyTcaForTableAndField($tableName, $fieldName);
|
||||
$didRegister = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $didRegister;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all extension keys that registered a category configuration.
|
||||
*
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
public function getExtensionKeys()
|
||||
{
|
||||
return array_keys($this->extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all categorized tables
|
||||
*
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
public function getCategorizedTables()
|
||||
{
|
||||
return array_keys($this->registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of category fields for a given table for populating selector "category_field"
|
||||
* in tt_content table (called as itemsProcFunc).
|
||||
*
|
||||
* @param array $configuration Current field configuration
|
||||
* @throws \UnexpectedValueException
|
||||
* @internal
|
||||
*/
|
||||
public function getCategoryFieldsForTable(array &$configuration)
|
||||
{
|
||||
$table = $configuration['config']['itemsProcConfig']['table'] ?? '';
|
||||
// Lookup table for legacy menu content element
|
||||
if (empty($table)) {
|
||||
$menuType = $configuration['row']['menu_type'][0] ?? '';
|
||||
// Define the table being looked up from the type of menu
|
||||
if ($menuType === 'categorized_pages') {
|
||||
$table = 'pages';
|
||||
} elseif ($menuType === 'categorized_content') {
|
||||
$table = 'tt_content';
|
||||
}
|
||||
}
|
||||
// Return early if no table is defined
|
||||
if (empty($table)) {
|
||||
throw new \UnexpectedValueException('The given menu_type is not supported.', 1381823570);
|
||||
}
|
||||
// Loop on all registries and find entries for the correct table
|
||||
foreach ($this->registry as $tableName => $fields) {
|
||||
if ($tableName === $table) {
|
||||
foreach ($fields as $fieldName => $options) {
|
||||
$fieldLabel = $this->getLanguageService()->sL($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['label']);
|
||||
$configuration['items'][] = [$fieldLabel, $fieldName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether a table has a category configuration in the registry.
|
||||
*
|
||||
* @param string $tableName Name of the table to be looked up
|
||||
* @param string $fieldName Name of the field to be looked up
|
||||
* @return bool
|
||||
* @internal
|
||||
*/
|
||||
public function isRegistered($tableName, $fieldName = 'categories')
|
||||
{
|
||||
return isset($this->registry[$tableName][$fieldName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates tables definitions for all registered tables.
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getDatabaseTableDefinitions()
|
||||
{
|
||||
$sql = '';
|
||||
foreach ($this->getExtensionKeys() as $extensionKey) {
|
||||
$sql .= $this->getDatabaseTableDefinition($extensionKey);
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates table definitions for registered tables by an extension.
|
||||
*
|
||||
* @param string $extensionKey Extension key to have the database definitions created for
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getDatabaseTableDefinition($extensionKey)
|
||||
{
|
||||
if (!isset($this->extensions[$extensionKey]) || !is_array($this->extensions[$extensionKey])) {
|
||||
return '';
|
||||
}
|
||||
$sql = '';
|
||||
|
||||
foreach ($this->extensions[$extensionKey] as $tableName => $fields) {
|
||||
foreach ($fields as $fieldName) {
|
||||
$sql .= sprintf($this->template, $tableName, $fieldName);
|
||||
}
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply TCA to all registered tables
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function applyTcaForPreRegisteredTables()
|
||||
{
|
||||
$this->registerDefaultCategorizedTables();
|
||||
foreach ($this->registry as $tableName => $fields) {
|
||||
foreach ($fields as $fieldName => $_) {
|
||||
$this->applyTcaForTableAndField($tableName, $fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the additions directly to the TCA
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param string $fieldName
|
||||
*/
|
||||
protected function applyTcaForTableAndField($tableName, $fieldName)
|
||||
{
|
||||
$this->addTcaColumn($tableName, $fieldName, $this->registry[$tableName][$fieldName]);
|
||||
$this->addToAllTCAtypes($tableName, $fieldName, $this->registry[$tableName][$fieldName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default categorized tables to the registry
|
||||
*/
|
||||
protected function registerDefaultCategorizedTables()
|
||||
{
|
||||
$defaultCategorizedTables = GeneralUtility::trimExplode(
|
||||
',',
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultCategorizedTables'],
|
||||
true
|
||||
);
|
||||
foreach ($defaultCategorizedTables as $defaultCategorizedTable) {
|
||||
if (!$this->isRegistered($defaultCategorizedTable)) {
|
||||
$this->add('core', $defaultCategorizedTable, 'categories');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new field into the TCA types -> showitem
|
||||
*
|
||||
* @param string $tableName Name of the table to be categorized
|
||||
* @param string $fieldName Name of the field to be used to store categories
|
||||
* @param array $options Additional configuration options
|
||||
* + fieldList: field configuration to be added to showitems
|
||||
* + typesList: list of types that shall visualize the categories field
|
||||
* + position: insert position of the categories field
|
||||
*/
|
||||
protected function addToAllTCAtypes($tableName, $fieldName, array $options)
|
||||
{
|
||||
|
||||
// Makes sure to add more TCA to an existing structure
|
||||
if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
|
||||
if (empty($options['fieldList'])) {
|
||||
$fieldList = $this->addCategoryTab($tableName, $fieldName);
|
||||
} else {
|
||||
$fieldList = $options['fieldList'];
|
||||
}
|
||||
|
||||
$typesList = '';
|
||||
if (isset($options['typesList']) && $options['typesList'] !== '') {
|
||||
$typesList = $options['typesList'];
|
||||
}
|
||||
|
||||
$position = '';
|
||||
if (!empty($options['position'])) {
|
||||
$position = $options['position'];
|
||||
}
|
||||
|
||||
// Makes the new "categories" field to be visible in TSFE.
|
||||
ExtensionManagementUtility::addToAllTCAtypes($tableName, $fieldList, $typesList, $position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the 'fieldList' string for $fieldName which includes a categories tab.
|
||||
* But only one categories tab is added per table.
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param string $fieldName
|
||||
* @return string
|
||||
*/
|
||||
protected function addCategoryTab($tableName, $fieldName)
|
||||
{
|
||||
$fieldList = '';
|
||||
if (!isset($this->addedCategoryTabs[$tableName])) {
|
||||
$fieldList .= '--div--;LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_category.tabs.category, ';
|
||||
$this->addedCategoryTabs[$tableName] = $tableName;
|
||||
}
|
||||
$fieldList .= $fieldName;
|
||||
return $fieldList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new TCA Column
|
||||
*
|
||||
* @param string $tableName Name of the table to be categorized
|
||||
* @param string $fieldName Name of the field to be used to store categories
|
||||
* @param array $options Additional configuration options
|
||||
* + fieldConfiguration: TCA field config array to override defaults
|
||||
* + label: backend label of the categories field
|
||||
* + interface: boolean if the category should be included in the "interface" section of the TCA table
|
||||
* + l10n_mode
|
||||
* + l10n_display
|
||||
*/
|
||||
protected function addTcaColumn($tableName, $fieldName, array $options)
|
||||
{
|
||||
// Makes sure to add more TCA to an existing structure
|
||||
if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
|
||||
// Take specific label into account
|
||||
$label = 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_category.categories';
|
||||
if (!empty($options['label'])) {
|
||||
$label = $options['label'];
|
||||
}
|
||||
|
||||
// Take specific value of exclude flag into account
|
||||
$exclude = true;
|
||||
if (isset($options['exclude'])) {
|
||||
$exclude = (bool)$options['exclude'];
|
||||
}
|
||||
|
||||
$fieldConfiguration = empty($options['fieldConfiguration']) ? [] : $options['fieldConfiguration'];
|
||||
|
||||
$columns = [
|
||||
$fieldName => [
|
||||
'exclude' => $exclude,
|
||||
'label' => $label,
|
||||
'config' => static::getTcaFieldConfiguration($tableName, $fieldName, $fieldConfiguration),
|
||||
],
|
||||
];
|
||||
|
||||
if (isset($options['l10n_mode'])) {
|
||||
$columns[$fieldName]['l10n_mode'] = $options['l10n_mode'];
|
||||
}
|
||||
if (isset($options['l10n_display'])) {
|
||||
$columns[$fieldName]['l10n_display'] = $options['l10n_display'];
|
||||
}
|
||||
if (isset($options['displayCond'])) {
|
||||
$columns[$fieldName]['displayCond'] = $options['displayCond'];
|
||||
}
|
||||
if (isset($options['onChange'])) {
|
||||
$columns[$fieldName]['onChange'] = $options['onChange'];
|
||||
}
|
||||
|
||||
// Register opposite references for the foreign side of a relation
|
||||
if (empty($GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName])) {
|
||||
$GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName] = [];
|
||||
}
|
||||
if (!in_array($fieldName, $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName])) {
|
||||
$GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName][] = $fieldName;
|
||||
}
|
||||
|
||||
// Add field to interface list per default (unless the 'interface' property is FALSE)
|
||||
if (
|
||||
(!isset($options['interface']) || $options['interface'])
|
||||
&& !empty($GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'])
|
||||
&& !GeneralUtility::inList($GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'], $fieldName)
|
||||
) {
|
||||
$GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'] .= ',' . $fieldName;
|
||||
}
|
||||
|
||||
// Adding fields to an existing table definition
|
||||
ExtensionManagementUtility::addTCAcolumns($tableName, $columns);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config array for given table and field.
|
||||
* This method does NOT take care of adding sql fields, adding the field to TCA types
|
||||
* nor does it set the MM_oppositeUsage in the sys_category TCA. This has to be taken care of manually!
|
||||
*
|
||||
* @param string $tableName The table name
|
||||
* @param string $fieldName The field name (default categories)
|
||||
* @param array $fieldConfigurationOverride Changes to the default configuration
|
||||
* @return array
|
||||
*/
|
||||
public static function getTcaFieldConfiguration($tableName, $fieldName = 'categories', array $fieldConfigurationOverride = [])
|
||||
{
|
||||
// Forges a new field, default name is "categories"
|
||||
$fieldConfiguration = [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectTree',
|
||||
'foreign_table' => 'sys_category',
|
||||
'foreign_table_where' => ' AND sys_category.sys_language_uid IN (-1, 0) ORDER BY sys_category.sorting ASC',
|
||||
'MM' => 'sys_category_record_mm',
|
||||
'MM_opposite_field' => 'items',
|
||||
'MM_match_fields' => [
|
||||
'tablenames' => $tableName,
|
||||
'fieldname' => $fieldName,
|
||||
],
|
||||
'size' => 20,
|
||||
'maxitems' => 9999,
|
||||
'treeConfig' => [
|
||||
'parentField' => 'parent',
|
||||
'appearance' => [
|
||||
'expandAll' => true,
|
||||
'showHeader' => true,
|
||||
'maxLevels' => 99,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Merge changes to TCA configuration
|
||||
if (!empty($fieldConfigurationOverride)) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$fieldConfiguration,
|
||||
$fieldConfigurationOverride
|
||||
);
|
||||
}
|
||||
|
||||
return $fieldConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* A slot method to inject the required category database fields to the
|
||||
* tables definition string
|
||||
*
|
||||
* @param array $sqlString
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
public function addCategoryDatabaseSchemaToTablesDefinition(array $sqlString)
|
||||
{
|
||||
$this->registerDefaultCategorizedTables();
|
||||
$sqlString[] = $this->getDatabaseTableDefinitions();
|
||||
return ['sqlString' => $sqlString];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given field in the given table from the registry if it is found.
|
||||
*
|
||||
* @param string $tableName The name of the table for which the registration should be removed.
|
||||
* @param string $fieldName The name of the field for which the registration should be removed.
|
||||
*/
|
||||
protected function remove($tableName, $fieldName)
|
||||
{
|
||||
if (!$this->isRegistered($tableName, $fieldName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->registry[$tableName][$fieldName]);
|
||||
|
||||
foreach ($this->extensions as $extensionKey => $tableFieldConfig) {
|
||||
foreach ($tableFieldConfig as $extTableName => $fieldNameArray) {
|
||||
if ($extTableName === $tableName && isset($fieldNameArray[$fieldName])) {
|
||||
unset($this->extensions[$extensionKey][$tableName][$fieldName]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no more fields are configured we unregister the categories tab.
|
||||
if (empty($this->registry[$tableName]) && isset($this->addedCategoryTabs[$tableName])) {
|
||||
unset($this->addedCategoryTabs[$tableName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Category\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Collection\AbstractRecordCollection;
|
||||
use TYPO3\CMS\Core\Collection\CollectionInterface;
|
||||
use TYPO3\CMS\Core\Collection\EditableCollectionInterface;
|
||||
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\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)
|
||||
{
|
||||
/** @var CategoryCollection $collection */
|
||||
$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 CollectionInterface
|
||||
*/
|
||||
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, \PDO::PARAM_INT))
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->execute()
|
||||
->fetch();
|
||||
|
||||
$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(), \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_category_record_mm.tablenames',
|
||||
$queryBuilder->createNamedParameter($this->getItemTableName(), \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_category_record_mm.fieldname',
|
||||
$queryBuilder->createNamedParameter($this->getRelationFieldName(), \PDO::PARAM_STR)
|
||||
)
|
||||
);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the collected records in this collection, by
|
||||
* using <getCollectedRecordsQueryBuilder>.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getCollectedRecords()
|
||||
{
|
||||
$relatedRecords = [];
|
||||
|
||||
$queryBuilder = $this->getCollectedRecordsQueryBuilder();
|
||||
$result = $queryBuilder->execute();
|
||||
|
||||
while ($record = $result->fetch()) {
|
||||
$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
|
||||
*
|
||||
* @param CollectionInterface $other
|
||||
*/
|
||||
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 \TYPO3\CMS\Core\Resource\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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Charset;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Exception for unknown charsets
|
||||
*/
|
||||
class UnknownCharsetException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
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_collections and is manageable
|
||||
* via TCEforms.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
abstract class AbstractRecordCollection implements RecordCollectionInterface, PersistableCollectionInterface, SortableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* The table name collections are stored to
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $storageTableName = 'sys_collection';
|
||||
|
||||
/**
|
||||
* The table name collections are stored to
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $storageItemsField = 'items';
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)
|
||||
* Return the current element
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
* @return mixed Can return any type.
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->storage->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)
|
||||
* Move forward to next element
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->storage->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)
|
||||
* Return the key of the current element
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
* @return int 0 on failure.
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
$currentRecord = $this->storage->current();
|
||||
return $currentRecord['uid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)
|
||||
* Checks if current position is valid
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
* @return bool The return value will be casted to boolean and then evaluated.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->storage->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)
|
||||
* Rewind the Iterator to the first element
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->storage->rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)
|
||||
* String representation of object
|
||||
*
|
||||
* @link http://php.net/manual/en/serializable.serialize.php
|
||||
* @return string the string representation of the object or &null;
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
$data = [
|
||||
'uid' => $this->getIdentifier()
|
||||
];
|
||||
return serialize($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)
|
||||
* Constructs the object
|
||||
*
|
||||
* @link http://php.net/manual/en/serializable.unserialize.php
|
||||
* @param string $serialized The string representation of the object
|
||||
* @return mixed the original value unserialized.
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
$data = unserialize($serialized);
|
||||
return self::load($data['uid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.1.0)
|
||||
* Count elements of an object
|
||||
*
|
||||
* @link http://php.net/manual/en/countable.count.php
|
||||
* @return int The custom count as an integer.
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts collection via given callBackFunction
|
||||
*
|
||||
* The comparison function given as must return an integer less than, equal to, or greater than
|
||||
* zero if the first argument is considered to be respectively less than, equal to, or greater than the second.
|
||||
*
|
||||
* @param $callbackFunction
|
||||
* @see http://www.php.net/manual/en/function.usort.php
|
||||
*/
|
||||
public function usort($callbackFunction)
|
||||
{
|
||||
// @todo Implement usort() method with TCEforms in mind
|
||||
throw new \RuntimeException('This method is not yet supported.', 1322545589);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the item within the collection
|
||||
*
|
||||
* the item at $currentPosition will be moved to
|
||||
* $newPosition. Omitting $newPosition will move to top.
|
||||
*
|
||||
* @param int $currentPosition
|
||||
* @param int $newPosition
|
||||
*/
|
||||
public function moveItemAt($currentPosition, $newPosition = 0)
|
||||
{
|
||||
// @todo Implement usort() method with TCEforms in mind
|
||||
throw new \RuntimeException('This method is not yet supported.', 1322545626);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 \TYPO3\CMS\Core\Collection\CollectionInterface
|
||||
*/
|
||||
public static function load($id, $fillItems = false)
|
||||
{
|
||||
$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, \PDO::PARAM_INT)))
|
||||
->execute()
|
||||
->fetch();
|
||||
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 \TYPO3\CMS\Core\Collection\CollectionInterface
|
||||
*/
|
||||
public static function create(array $collectionRecord, $fillItems = false)
|
||||
{
|
||||
$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' . rand(100000, 999999) : $this->getIdentifier();
|
||||
$data = [
|
||||
trim(static::$storageTableName) => [
|
||||
$uid => $this->getPersistableDataArray()
|
||||
]
|
||||
];
|
||||
// New records always must have a pid
|
||||
if ($this->getIdentifier() == 0) {
|
||||
$data[trim(static::$storageTableName)][$uid]['pid'] = 0;
|
||||
}
|
||||
/** @var \TYPO3\CMS\Core\DataHandling\DataHandler $tce */
|
||||
$tce = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\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
|
||||
*
|
||||
* @param array $array
|
||||
*/
|
||||
public function fromArray(array $array)
|
||||
{
|
||||
$this->uid = $array['uid'];
|
||||
$this->title = $array['title'];
|
||||
$this->description = $array['description'];
|
||||
$this->itemTableName = $array['table_name'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
interface CollectionInterface extends \Iterator, \Serializable, \Countable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param CollectionInterface $other
|
||||
*/
|
||||
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,52 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 an collection, allowing every collection implementing Nameable
|
||||
* being display 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,74 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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,35 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Collection for handling records from a single database-table.
|
||||
*/
|
||||
interface RecordCollectionInterface extends \TYPO3\CMS\Core\Collection\CollectionInterface, \TYPO3\CMS\Core\Collection\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,232 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Implements the repository for record collections.
|
||||
*/
|
||||
class RecordCollectionRepository
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
const TYPE_Static = 'static';
|
||||
|
||||
/**
|
||||
* Name of the table the collection records are stored to
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'sys_collection';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $typeField = 'type';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $tableField = 'table_name';
|
||||
|
||||
/**
|
||||
* Finds a record collection by uid.
|
||||
*
|
||||
* @param int $uid The uid to be looked up
|
||||
* @return \TYPO3\CMS\Core\Collection\AbstractRecordCollection|null
|
||||
*/
|
||||
public function findByUid($uid)
|
||||
{
|
||||
$result = null;
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
|
||||
|
||||
if ($this->getEnvironmentMode() === 'FE') {
|
||||
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
|
||||
} else {
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
}
|
||||
|
||||
$data = $queryBuilder->select('*')
|
||||
->from($this->table)
|
||||
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
|
||||
->execute()
|
||||
->fetch();
|
||||
if (is_array($data)) {
|
||||
$result = $this->createDomainObject($data);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all record collections.
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Collection\AbstractRecordCollection[]|null
|
||||
*/
|
||||
public function findAll()
|
||||
{
|
||||
return $this->queryMultipleRecords();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds record collections by table name.
|
||||
*
|
||||
* @param string $tableName Name of the table to be looked up
|
||||
* @return \TYPO3\CMS\Core\Collection\AbstractRecordCollection[]
|
||||
*/
|
||||
public function findByTableName($tableName)
|
||||
{
|
||||
$expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($tableName)
|
||||
->expr();
|
||||
|
||||
return $this->queryMultipleRecords([
|
||||
$expressionBuilder->eq($this->tableField, $expressionBuilder->literal($tableName))
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds record collection by type.
|
||||
*
|
||||
* @param string $type Type to be looked up
|
||||
* @return \TYPO3\CMS\Core\Collection\AbstractRecordCollection[]|null
|
||||
*/
|
||||
public function findByType($type)
|
||||
{
|
||||
$expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($this->table)
|
||||
->expr();
|
||||
|
||||
return $this->queryMultipleRecords([
|
||||
$expressionBuilder->eq($this->typeField, $expressionBuilder->literal($type))
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds record collections by type and table name.
|
||||
*
|
||||
* @param string $type Type to be looked up
|
||||
* @param string $tableName Name of the table to be looked up
|
||||
* @return \TYPO3\CMS\Core\Collection\AbstractRecordCollection[]|null
|
||||
*/
|
||||
public function findByTypeAndTableName($type, $tableName)
|
||||
{
|
||||
$expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($this->table)
|
||||
->expr();
|
||||
|
||||
return $this->queryMultipleRecords([
|
||||
$expressionBuilder->eq($this->typeField, $expressionBuilder->literal($type)),
|
||||
$expressionBuilder->eq($this->tableField, $expressionBuilder->literal($tableName))
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a record collection by uid.
|
||||
*
|
||||
* @param int $uid uid to be deleted
|
||||
*/
|
||||
public function deleteByUid($uid)
|
||||
{
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable($this->table)
|
||||
->update(
|
||||
$this->table,
|
||||
['deleted' => 1, 'tstamp' => (int)$GLOBALS['EXEC_TIME']],
|
||||
['uid' => (int)$uid]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries for multiple records for the given conditions.
|
||||
*
|
||||
* @param array $conditions Conditions concatenated with AND for query
|
||||
* @return \TYPO3\CMS\Core\Collection\AbstractRecordCollection[]|null
|
||||
*/
|
||||
protected function queryMultipleRecords(array $conditions = [])
|
||||
{
|
||||
$result = null;
|
||||
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
|
||||
$queryBuilder->select('*')
|
||||
->from($this->table);
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$queryBuilder->where(...$conditions);
|
||||
}
|
||||
|
||||
$data = $queryBuilder->execute()->fetchAll();
|
||||
if (!empty($data)) {
|
||||
$result = $this->createMultipleDomainObjects($data);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a record collection domain object.
|
||||
*
|
||||
* @param array $record Database record to be reconstituted
|
||||
* @return \TYPO3\CMS\Core\Collection\AbstractRecordCollection
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function createDomainObject(array $record)
|
||||
{
|
||||
switch ($record['type']) {
|
||||
case self::TYPE_Static:
|
||||
$collection = StaticRecordCollection::create($record);
|
||||
break;
|
||||
default:
|
||||
throw new \RuntimeException('Unknown record collection type "' . $record['type'], 1328646798);
|
||||
}
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates multiple record collection domain objects.
|
||||
*
|
||||
* @param array $data Array of multiple database records to be reconstituted
|
||||
* @return \TYPO3\CMS\Core\Collection\AbstractRecordCollection[]
|
||||
*/
|
||||
protected function createMultipleDomainObjects(array $data)
|
||||
{
|
||||
$collections = [];
|
||||
foreach ($data as $collection) {
|
||||
$collections[] = $this->createDomainObject($collection);
|
||||
}
|
||||
return $collections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to return the current TYPO3_MODE.
|
||||
* This function can be mocked in unit tests to be able to test frontend behaviour.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getEnvironmentMode()
|
||||
{
|
||||
return TYPO3_MODE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface for collection class being sortable
|
||||
*
|
||||
* This interface allows you to either define a callback implementing
|
||||
* your own sorting method and explicitly move an item from one position
|
||||
* to another.
|
||||
*
|
||||
* This assumes that entries are sortable and therefore an index can be assigned
|
||||
*/
|
||||
interface SortableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* Sorts collection via given callBackFunction
|
||||
*
|
||||
* The comparison function given as must return an integer less than, equal to, or greater than
|
||||
* zero if the first argument is considered to be respectively less than, equal to, or greater than the second.
|
||||
*
|
||||
* @param $callbackFunction
|
||||
* @see http://www.php.net/manual/en/function.usort.php
|
||||
*/
|
||||
public function usort($callbackFunction);
|
||||
|
||||
/**
|
||||
* Moves the item within the collection
|
||||
*
|
||||
* The item at $currentPosition will be moved to
|
||||
* $newPosition. Omitting $newPosition will move to top.
|
||||
*
|
||||
* @param int $currentPosition
|
||||
* @param int $newPosition
|
||||
*/
|
||||
public function moveItemAt($currentPosition, $newPosition = 0);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Collection;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Implementation of a RecordCollection for static TCA-Records
|
||||
*/
|
||||
class StaticRecordCollection extends AbstractRecordCollection implements EditableCollectionInterface
|
||||
{
|
||||
/**
|
||||
* 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 \TYPO3\CMS\Core\Collection\StaticRecordCollection
|
||||
*/
|
||||
public static function create(array $collectionRecord, $fillItems = false)
|
||||
{
|
||||
/** @var StaticRecordCollection $collection */
|
||||
$collection = GeneralUtility::makeInstance(
|
||||
self::class,
|
||||
$collectionRecord['table_name']
|
||||
);
|
||||
$collection->fromArray($collectionRecord);
|
||||
if ($fillItems) {
|
||||
$collection->loadContents();
|
||||
}
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates this object.
|
||||
*
|
||||
* @param string $tableName Name of the table to be working on
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function __construct($tableName = null)
|
||||
{
|
||||
parent::__construct();
|
||||
if (!empty($tableName)) {
|
||||
$this->setItemTableName($tableName);
|
||||
} elseif (empty($this->itemTableName)) {
|
||||
throw new \RuntimeException(\TYPO3\CMS\Core\Collection\StaticRecordCollection::class . ' needs a valid itemTableName.', 1330293778);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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),
|
||||
'type' => 'static',
|
||||
'table_name' => $this->getItemTableName()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param CollectionInterface $other
|
||||
*/
|
||||
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 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 array
|
||||
*/
|
||||
protected function getCollectedRecords()
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable(self::$storageTableName);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$statement = $queryBuilder->select($this->getItemTableName() . '.*')
|
||||
->from(self::$storageTableName)
|
||||
->join(
|
||||
self::$storageTableName,
|
||||
'sys_collection_entries',
|
||||
'sys_collection_entries',
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_collection_entries.uid_local',
|
||||
$queryBuilder->quoteIdentifier(self::$storageTableName . '.uid')
|
||||
)
|
||||
)
|
||||
->join(
|
||||
'sys_collection_entries',
|
||||
$this->getItemTableName(),
|
||||
$this->getItemTableName(),
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_collection_entries.uid_foreign',
|
||||
$queryBuilder->quoteIdentifier($this->getItemTableName() . '.uid')
|
||||
)
|
||||
)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
self::$storageTableName . '.uid',
|
||||
$queryBuilder->createNamedParameter($this->getIdentifier(), \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->execute();
|
||||
$relatedRecords = [];
|
||||
while ($record = $statement->fetch()) {
|
||||
$relatedRecords[] = $record;
|
||||
}
|
||||
return $relatedRecords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
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()
|
||||
{
|
||||
$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.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return !Environment::isComposerMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the class loading information
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
ClassLoadingInformation::dumpClassLoadingInformation();
|
||||
$io->success('Class loading information has been updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
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\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class ExtensionListCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDescription('Shows the list of extensions available to the system.')
|
||||
->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
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
|
||||
|
||||
$onlyShowInactiveExtensions = $input->getOption('inactive');
|
||||
$showAlsoInactiveExtensions = $input->getOption('all');
|
||||
if ($onlyShowInactiveExtensions) {
|
||||
$packages = $packageManager->getAvailablePackages();
|
||||
$io->title('All inactive/currently uninstalled extensions');
|
||||
} elseif ($showAlsoInactiveExtensions) {
|
||||
$packages = $packageManager->getAvailablePackages();
|
||||
$io->title('All installed (= active) and available (= inactive/currently uninstalled) extensions');
|
||||
} else {
|
||||
$packages = $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]);
|
||||
|
||||
$formatter = $this->getHelper('formatter');
|
||||
foreach ($packages as $package) {
|
||||
$isActivePackage = $packageManager->isPackageActive($package->getPackageKey());
|
||||
// Do not show the package if it is active but we only want to see inactive packages
|
||||
if ($onlyShowInactiveExtensions && $isActivePackage) {
|
||||
continue;
|
||||
}
|
||||
if ($package->getValueFromComposerManifest('type') === 'typo3-cms-framework') {
|
||||
$type = 'System';
|
||||
} else {
|
||||
$type = '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 description of the extension, if verbose option is set
|
||||
if ($output->isVerbose()) {
|
||||
$description = (string)$package->getValueFromComposerManifest('description');
|
||||
$table->addRow([new TableCell(' ' . $formatter->truncate($description, 80) . "\n\n", ['colspan' => 4])]);
|
||||
}
|
||||
}
|
||||
$table->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
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\Mail\Mailer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Command for sending spooled messages.
|
||||
*
|
||||
* Inspired and partially taken from symfony's swiftmailer package.
|
||||
*
|
||||
* @link https://github.com/symfony/swiftmailer-bundle/blob/master/Command/SendEmailCommand.php
|
||||
*/
|
||||
class SendEmailCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDescription('Sends emails from the spool')
|
||||
->addOption('message-limit', null, InputOption::VALUE_REQUIRED, 'The maximum number of messages to send.')
|
||||
->addOption('time-limit', null, InputOption::VALUE_REQUIRED, 'The time limit for sending messages (in seconds).')
|
||||
->addOption('recover-timeout', null, InputOption::VALUE_REQUIRED, 'The timeout for recovering messages that have taken too long to send (in seconds).');
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the mailer command
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @return int|void|null
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$mailer = $this->getMailer();
|
||||
|
||||
$transport = $mailer->getTransport();
|
||||
if ($transport instanceof \Swift_Transport_SpoolTransport) {
|
||||
$spool = $transport->getSpool();
|
||||
if ($spool instanceof \Swift_ConfigurableSpool) {
|
||||
$spool->setMessageLimit((int)$input->getOption('message-limit'));
|
||||
$spool->setTimeLimit((int)$input->getOption('time-limit'));
|
||||
}
|
||||
if ($spool instanceof \Swift_FileSpool) {
|
||||
$recoverTimeout = (int)$input->getOption('recover-timeout');
|
||||
if ($recoverTimeout) {
|
||||
$spool->recover($recoverTimeout);
|
||||
} else {
|
||||
$spool->recover();
|
||||
}
|
||||
}
|
||||
$sent = $spool->flushQueue($mailer->getRealTransport());
|
||||
$io->comment($sent . ' emails sent');
|
||||
} else {
|
||||
$io->error('The Mailer Transport is not set to "spool".');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TYPO3 mailer.
|
||||
*
|
||||
* @return Mailer
|
||||
*/
|
||||
protected function getMailer(): Mailer
|
||||
{
|
||||
return GeneralUtility::makeInstance(Mailer::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
|
||||
/**
|
||||
* Command for listing all configured sites
|
||||
*/
|
||||
class SiteListCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->setDescription('Shows the list of sites available to the system.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a table with all configured sites
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$siteFinder = new SiteFinder();
|
||||
$sites = $siteFinder->getAllSites();
|
||||
|
||||
if (empty($sites)) {
|
||||
$io->title('No sites configured');
|
||||
$io->note('Configure new sites in the "Sites" module.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$io->title('All configured sites');
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([
|
||||
'Identifier',
|
||||
'Root PID',
|
||||
'Base URL',
|
||||
'Language',
|
||||
'Locale',
|
||||
'Status',
|
||||
]);
|
||||
foreach ($sites as $site) {
|
||||
$baseUrls = [];
|
||||
$languages = [];
|
||||
$locales = [];
|
||||
$status = [];
|
||||
foreach ($site->getLanguages() as $language) {
|
||||
$baseUrls[] = (string)$language->getBase();
|
||||
$languages[] = sprintf(
|
||||
'%s (id:%d)',
|
||||
$language->getTitle(),
|
||||
$language->getLanguageId()
|
||||
);
|
||||
$locales[] = $language->getLocale();
|
||||
$status[] = $language->isEnabled()
|
||||
? '<fg=green>enabled</>'
|
||||
: '<fg=yellow>disabled</>';
|
||||
}
|
||||
$table->addRow(
|
||||
[
|
||||
'<options=bold>' . $site->getIdentifier() . '</>',
|
||||
$site->getRootPageId(),
|
||||
implode("\n", $baseUrls),
|
||||
implode("\n", $languages),
|
||||
implode("\n", $locales),
|
||||
implode("\n", $status),
|
||||
]
|
||||
);
|
||||
}
|
||||
$table->render();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Core\Command;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
|
||||
/**
|
||||
* Command for showing the configuration of a site
|
||||
*/
|
||||
class SiteShowCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Defines the allowed options for this command
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->setDescription('Shows the configuration of the specified site. Specify the identifier via "site:show <identifier>".')
|
||||
->addArgument(
|
||||
'identifier',
|
||||
InputArgument::REQUIRED,
|
||||
'The identifier of the site'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the configuration of a site
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$siteFinder = new SiteFinder();
|
||||
$site = $siteFinder->getSiteByIdentifier($input->getArgument('identifier'));
|
||||
$io->title('Site configuration for ' . $input->getArgument('identifier'));
|
||||
$io->block(Yaml::dump($site->getConfiguration(), 4));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Compatibility;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Package\PackageInterface;
|
||||
|
||||
/**
|
||||
* Class to simulate the "old" extension information array element
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LoadedExtensionArrayElement implements \IteratorAggregate, \ArrayAccess, \Serializable, \Countable
|
||||
{
|
||||
/**
|
||||
* @var PackageInterface Instance of package manager
|
||||
*/
|
||||
protected $package;
|
||||
|
||||
/**
|
||||
* @var array List of relevant extension files
|
||||
*/
|
||||
protected $extensionFilesToCheckFor = [
|
||||
'ext_localconf.php',
|
||||
'ext_tables.php',
|
||||
'ext_tables.sql',
|
||||
'ext_tables_static+adt.sql',
|
||||
'ext_typoscript_constants.typoscript',
|
||||
'ext_typoscript_setup.typoscript',
|
||||
'ext_typoscript_constants.txt',
|
||||
'ext_typoscript_setup.txt'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Final extension information
|
||||
*/
|
||||
protected $extensionInformation = [];
|
||||
|
||||
/**
|
||||
* Constructor builds compatibility API
|
||||
*
|
||||
* @param PackageInterface $package
|
||||
*/
|
||||
public function __construct(PackageInterface $package)
|
||||
{
|
||||
$this->package = $package;
|
||||
$this->initializeBasicExtensionInformation();
|
||||
$this->initializeExtensionFiles();
|
||||
$this->initializeExtensionIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create main information
|
||||
*/
|
||||
protected function initializeBasicExtensionInformation()
|
||||
{
|
||||
$pathSite = Environment::getPublicPath() . '/';
|
||||
$pathSiteLength = strlen($pathSite);
|
||||
$absolutePackagePath = $this->package->getPackagePath();
|
||||
if (strpos($absolutePackagePath, $pathSite) === 0) {
|
||||
$relativePackagePathToPathSite = substr($absolutePackagePath, $pathSiteLength);
|
||||
$relativePackagePathToPathSiteSegments = explode('/', $relativePackagePathToPathSite);
|
||||
$packageType = null;
|
||||
// Determine if extension is installed locally, globally or system (in this order)
|
||||
switch (implode('/', array_slice($relativePackagePathToPathSiteSegments, 0, 2))) {
|
||||
case 'typo3conf/ext':
|
||||
$packageType = 'L';
|
||||
break;
|
||||
case TYPO3_mainDir . 'ext':
|
||||
$packageType = 'G';
|
||||
break;
|
||||
case TYPO3_mainDir . 'sysext':
|
||||
$packageType = 'S';
|
||||
break;
|
||||
case 'typo3temp/var/tests/test_ext':
|
||||
$packageType = 'T';
|
||||
break;
|
||||
}
|
||||
if ($packageType !== null) {
|
||||
$this->extensionInformation['type'] = $packageType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize extension icon
|
||||
*/
|
||||
protected function initializeExtensionIcon()
|
||||
{
|
||||
$this->extensionInformation['ext_icon'] = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getExtensionIcon($this->package->getPackagePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register found files in extension array if extension was found
|
||||
*/
|
||||
protected function initializeExtensionFiles()
|
||||
{
|
||||
foreach ($this->extensionFilesToCheckFor as $fileName) {
|
||||
$absolutePathToFile = $this->package->getPackagePath() . $fileName;
|
||||
if (@file_exists($absolutePathToFile)) {
|
||||
$this->extensionInformation[$fileName] = $absolutePathToFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an external iterator
|
||||
*
|
||||
* @link http://php.net/manual/en/iteratoraggregate.getiterator.php
|
||||
* @return \Traversable An instance of an object implementing Iterator or Traversable
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new \ArrayIterator($this->extensionInformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an offset exists
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
|
||||
* @param mixed $offset An offset to check for.
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return isset($this->extensionInformation[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to retrieve
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetget.php
|
||||
* @param mixed $offset The offset to retrieve.
|
||||
* @return mixed Can return all value types.
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->extensionInformation[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to set
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetset.php
|
||||
* @param mixed $offset The offset to assign the value to.
|
||||
* @param mixed $value The value to set.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
throw new \InvalidArgumentException('The array $GLOBALS[\'TYPO3_LOADED_EXT\'] may not be modified.', 1361915115);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to unset
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
|
||||
* @param mixed $offset The offset to unset.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
throw new \InvalidArgumentException('The array $GLOBALS[\'TYPO3_LOADED_EXT\'] may not be modified.', 1361915206);
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation of object
|
||||
*
|
||||
* @link http://php.net/manual/en/serializable.serialize.php
|
||||
* @return string the string representation of the object or null
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
return serialize($this->extensionInformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the object
|
||||
*
|
||||
* @link http://php.net/manual/en/serializable.unserialize.php
|
||||
* @param string $serialized The string representation of the object.
|
||||
* @return mixed the original value unserialized.
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
$this->extensionInformation = unserialize($serialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count elements of an object
|
||||
*
|
||||
* @link http://php.net/manual/en/countable.count.php
|
||||
* @return int The custom count as an integer. The return value is cast to an integer.
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->extensionInformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return iterator_to_array($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Compatibility;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to simulate the "old" extension information array
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LoadedExtensionsArray implements \Iterator, \ArrayAccess, \Serializable, \Countable
|
||||
{
|
||||
/**
|
||||
* @var \TYPO3\CMS\Core\Package\PackageManager Instance of package manager
|
||||
*/
|
||||
protected $packageManager;
|
||||
|
||||
/**
|
||||
* @var array Loaded element cache
|
||||
*/
|
||||
protected $loadedExtensionArrayElementCache = [];
|
||||
|
||||
/**
|
||||
* @var string Pointer to current position
|
||||
*/
|
||||
protected $iteratorPosition;
|
||||
|
||||
/**
|
||||
* @param \TYPO3\CMS\Core\Package\PackageManager $packageManager
|
||||
*/
|
||||
public function __construct(\TYPO3\CMS\Core\Package\PackageManager $packageManager)
|
||||
{
|
||||
$this->packageManager = $packageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an offset exists
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
|
||||
* @param mixed $offset An offset to check for.
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return $this->packageManager->isPackageActive($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to retrieve
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetget.php
|
||||
* @param mixed $offset The offset to retrieve.
|
||||
* @return mixed Can return all value types.
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
// Pass it through the package manager, as it resolves package aliases
|
||||
$package = $this->packageManager->getPackage($offset);
|
||||
$packageKey = $package->getPackageKey();
|
||||
if (!isset($this->loadedExtensionArrayElementCache[$packageKey])) {
|
||||
$this->loadedExtensionArrayElementCache[$packageKey] = new LoadedExtensionArrayElement($package);
|
||||
}
|
||||
return $this->loadedExtensionArrayElementCache[$packageKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to set
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetset.php
|
||||
* @param mixed $offset The offset to assign the value to.
|
||||
* @param mixed $value The value to set.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
throw new \InvalidArgumentException('The array $GLOBALS[\'TYPO3_LOADED_EXT\'] may not be modified.', 1361915596);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to unset
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
|
||||
* @param mixed $offset The offset to unset.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
throw new \InvalidArgumentException('The array $GLOBALS[\'TYPO3_LOADED_EXT\'] may not be modified.', 1361915610);
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation of object
|
||||
*
|
||||
* @link http://php.net/manual/en/serializable.serialize.php
|
||||
* @return string the string representation of the object or null
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
return serialize($this->loadedExtensionArrayElementCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the object
|
||||
*
|
||||
* @link http://php.net/manual/en/serializable.unserialize.php
|
||||
* @param string $serialized The string representation of the object.
|
||||
* @return mixed the original value unserialized.
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
$this->loadedExtensionArrayElementCache = unserialize($serialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count elements of an object
|
||||
*
|
||||
* @link http://php.net/manual/en/countable.count.php
|
||||
* @return int The custom count as an integer.
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->packageManager->getActivePackages());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
* @return mixed Can return any type.
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->offsetGet($this->iteratorPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$packageKeys = array_keys($this->packageManager->getActivePackages());
|
||||
$position = array_search($this->iteratorPosition, $packageKeys);
|
||||
if (isset($packageKeys[$position + 1])) {
|
||||
$this->iteratorPosition = $packageKeys[$position + 1];
|
||||
} else {
|
||||
$this->iteratorPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
* @return mixed scalar on success, or null on failure.
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->iteratorPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
* @return bool The return value will be casted to boolean and then evaluated. Returns true on success or false on failure.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->offsetExists($this->iteratorPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$keys = array_keys($this->packageManager->getActivePackages());
|
||||
$this->iteratorPosition = array_shift($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->loadedExtensionArrayElementCache = [];
|
||||
$this->rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether package manager is set in class
|
||||
*
|
||||
* @return bool TRUE if package manager is set
|
||||
*/
|
||||
public function hasPackageManager()
|
||||
{
|
||||
return $this->packageManager !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return array_map(
|
||||
function ($loadedExtElement) {
|
||||
return $loadedExtElement->toArray();
|
||||
},
|
||||
iterator_to_array($this)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Compatibility;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Routing\SiteMatcher;
|
||||
use TYPO3\CMS\Core\Site\Entity\PseudoSite;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A display condition that returns true if the page we are dealing
|
||||
* with is in a page tree that is represented by a PseudoSite object.
|
||||
*
|
||||
* This is used to suppress the 'slug' field in pseudo site page trees
|
||||
* when editing page records and to show the alias field.
|
||||
*
|
||||
* Both "Pseudo sites" and "alias" db field will bite the dust in TYPO3 v10.0,
|
||||
* so this is a temporary display condition for v9 only and thus marked internal.
|
||||
*
|
||||
* @internal Implementation and class will probably vanish in TYPO3 v10.0 without further notice
|
||||
*/
|
||||
class PseudoSiteTcaDisplayCondition
|
||||
{
|
||||
/**
|
||||
* Takes the given page id of the record and verifies if the page has
|
||||
* a pseudo site object or a site object attached.
|
||||
*
|
||||
* @param array $parameters
|
||||
* @return bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function isInPseudoSite(array $parameters): bool
|
||||
{
|
||||
if (!isset($parameters['conditionParameters'][0])
|
||||
|| $parameters['conditionParameters'][0] !== 'pages'
|
||||
|| !isset($parameters['conditionParameters'][1])
|
||||
|| (!in_array($parameters['conditionParameters'][1], ['true', 'false'], true))
|
||||
|| empty($parameters['record']['uid'])
|
||||
) {
|
||||
// Validate arguments
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid arguments using isInPseudoSite display condition',
|
||||
1535055415
|
||||
);
|
||||
}
|
||||
|
||||
// uid is set if we're editing an existing page
|
||||
// This resolves to 0 if the page is 'new'
|
||||
$defaultLanguagePageId = (int)($parameters['record']['t3ver_oid'] ?: $parameters['record']['uid']);
|
||||
if (is_array($parameters['record']['l10n_parent'])
|
||||
&& (int)$parameters['record']['sys_language_uid'][0] > 0
|
||||
) {
|
||||
// But if the page is a localized page, we take the l10n_parent as uid for the sitematcher
|
||||
$defaultLanguagePageId = (int)$parameters['record']['l10n_parent'][0];
|
||||
}
|
||||
// If still 0, this is probably a 'new' page somewhere, so we take the pid
|
||||
// For additional fun, pid can be -1*real-pid, if the new page is created "after" an existing page
|
||||
// Also take care if a record is a localized one, so set the pid instead of the parent (language) record
|
||||
if ($defaultLanguagePageId === 0
|
||||
|| (int)$parameters['record']['sys_language_uid'][0] > 0
|
||||
) {
|
||||
$defaultLanguagePageId = abs((int)$parameters['record']['pid']);
|
||||
}
|
||||
// And if now still 0, this is a 'new' page below pid 0. This will resolve to a 'NullSite' object
|
||||
|
||||
// If not a Site or a NullSite object, it must be a PseudoSite. We show the slug for
|
||||
// NullSites (new pages below root) to simplify the editing workflow a bit.
|
||||
$site = GeneralUtility::makeInstance(SiteMatcher::class)->matchByPageId($defaultLanguagePageId);
|
||||
$isInPseudoSite = ($site instanceof PseudoSite);
|
||||
|
||||
if ($parameters['conditionParameters'][1] === 'false') {
|
||||
// Negate if requested
|
||||
return !$isInPseudoSite;
|
||||
}
|
||||
|
||||
return $isInPseudoSite;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Compatibility;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Trait to support the logging of deprecation of public INSTANCE methods.
|
||||
*
|
||||
* This is useful due to the long list of PHP4 methods that have been set to
|
||||
* public previously, which should be removed or moved to "protected" / "private".
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* 1. Use this trait for the class with the method to change the visibility status or to be removed.
|
||||
* 2. Add the class methods with deprecated public visibility to $deprecatedPublicMethods.
|
||||
* 3. Set these methods to protected or private.
|
||||
* 4. Add the phpDoc tag "@private" to the method so IDEs understand that.
|
||||
*
|
||||
* With the next major release remove the "@private" tag and remove the methods from
|
||||
* $deprecatedPublicMethods. Remove the trait use after removing the last deprecation.
|
||||
*
|
||||
* Note:
|
||||
*
|
||||
* - Only use this trait in classes only that do not define their own magic __call() method.
|
||||
* - Do not use this trait for static methods.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
*
|
||||
* class MyControllerClass {
|
||||
* use PublicMethodDeprecationTrait;
|
||||
*
|
||||
* /**
|
||||
* * List previously publicly accessible variables
|
||||
* * @var array
|
||||
* *...
|
||||
* private $deprecatedPublicMethods = [
|
||||
* 'myMethod' => 'Using MyControllerClass::myMethod() is deprecated and will not be possible anymore in TYPO3 v10.0. Use MyControllerClass:myOtherMethod() instead.'
|
||||
* ];
|
||||
*
|
||||
* /**
|
||||
* * This is my method.
|
||||
* *
|
||||
* * @deprecated (if deprecated)
|
||||
* * @private (if switched to private)
|
||||
* /
|
||||
* protected function myMethod($arg1, $arg2);
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* This trait has no public methods by default, ensure to add a $deprecatedPublicMethods property
|
||||
* to your class when using this trait.
|
||||
*/
|
||||
trait PublicMethodDeprecationTrait
|
||||
{
|
||||
/**
|
||||
* Checks if the method of the given name is available, calls it but throws a deprecation.
|
||||
* If the method does not exist, a fatal error is thrown.
|
||||
*
|
||||
* Unavailable protected methods must return in a fatal error as usual.
|
||||
* Marked methods are called and a deprecation entry is thrown.
|
||||
*
|
||||
* __call() is not called for public methods.
|
||||
*
|
||||
* @property array $deprecatedPublicMethods List of deprecated public methods
|
||||
* @param string $methodName
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $methodName, array $arguments)
|
||||
{
|
||||
if (method_exists($this, $methodName) && isset($this->deprecatedPublicMethods[$methodName])) {
|
||||
trigger_error($this->deprecatedPublicMethods[$methodName], E_USER_DEPRECATED);
|
||||
return $this->$methodName(...$arguments);
|
||||
}
|
||||
|
||||
// Do the same behaviour as calling $myObject->method();
|
||||
if (method_exists($this, $methodName)) {
|
||||
throw new \Error('Call to protected/private method ' . self::class . '::' . $methodName . '()');
|
||||
}
|
||||
|
||||
throw new \Error('Call to undefined method ' . self::class . '::' . $methodName . '()');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Compatibility;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Trait to support the logging of deprecation of public properties.
|
||||
*
|
||||
* This is useful due to the long list of PHP4 properties have been set to
|
||||
* public previously, which should be removed or moved to "protected" / "private".
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* - Use this trait for the class with the properties to change the visibility status or to be removed.
|
||||
* - Set internal class properties to protected.
|
||||
* - Add the phpDoc tag "private" to the property (so IDEs understand that).
|
||||
* - Remove this tag with the next major version.
|
||||
* - Remove trait after last deprecation.
|
||||
*
|
||||
* Note:
|
||||
*
|
||||
* Use this trait for classes only that do not make use of magic accessors otherwise.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
*
|
||||
* class MyControllerClass {
|
||||
* use PublicPropertyDeprecationTrait;
|
||||
*
|
||||
* /**
|
||||
* * List previously publically accessible variables
|
||||
* * @var array
|
||||
* *...
|
||||
* private $deprecatedPublicProperties = [
|
||||
* 'myProperty' => 'Using myProperty is deprecated and will not be possible anymore in TYPO3 v10.0. Use getMyProperty() instead.'
|
||||
* ];
|
||||
*
|
||||
* /**
|
||||
* * This is my property.
|
||||
* *
|
||||
* * @var bool
|
||||
* * @deprecated (if deprecated)
|
||||
* * @private (if switched to private)
|
||||
* /
|
||||
* protected $myProperty = true;
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* This trait has no public properties by default, ensure to add a $deprecatedPublicProperties to your class
|
||||
* when using this trait.
|
||||
*/
|
||||
trait PublicPropertyDeprecationTrait
|
||||
{
|
||||
/**
|
||||
* Checks if the property of the given name is set.
|
||||
*
|
||||
* Unmarked protected properties must return false as usual.
|
||||
* Marked properties are evaluated by isset().
|
||||
*
|
||||
* This method is not called for public properties.
|
||||
*
|
||||
* @property array $deprecatedPublicProperties List of deprecated public properties
|
||||
* @param string $propertyName
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset(string $propertyName)
|
||||
{
|
||||
if (isset($this->deprecatedPublicProperties[$propertyName])) {
|
||||
trigger_error($this->deprecatedPublicProperties[$propertyName], E_USER_DEPRECATED);
|
||||
return isset($this->$propertyName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the property of the given name if tagged.
|
||||
*
|
||||
* The evaluation is done in the assumption that this method is never
|
||||
* reached for a public property.
|
||||
*
|
||||
* @property array $deprecatedPublicProperties List of deprecated public properties
|
||||
* @param string $propertyName
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $propertyName)
|
||||
{
|
||||
if (isset($this->deprecatedPublicProperties[$propertyName])) {
|
||||
trigger_error($this->deprecatedPublicProperties[$propertyName], E_USER_DEPRECATED);
|
||||
}
|
||||
return $this->$propertyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the property of the given name if tagged.
|
||||
*
|
||||
* Additionally it's allowed to set unknown properties.
|
||||
*
|
||||
* The evaluation is done in the assumption that this method is never
|
||||
* reached for a public property.
|
||||
*
|
||||
* @property array $deprecatedPublicProperties List of deprecated public properties
|
||||
* @param string $propertyName
|
||||
* @param mixed $propertyValue
|
||||
*/
|
||||
public function __set(string $propertyName, $propertyValue)
|
||||
{
|
||||
// It's allowed to set an unknown property as public, the check is thus necessary
|
||||
if (property_exists($this, $propertyName) && isset($this->deprecatedPublicProperties[$propertyName])) {
|
||||
trigger_error($this->deprecatedPublicProperties[$propertyName], E_USER_DEPRECATED);
|
||||
}
|
||||
$this->$propertyName = $propertyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets the property of the given name if tagged.
|
||||
*
|
||||
* @property array $deprecatedPublicProperties List of deprecated public properties
|
||||
* @param string $propertyName
|
||||
*/
|
||||
public function __unset(string $propertyName)
|
||||
{
|
||||
if (isset($this->deprecatedPublicProperties[$propertyName])) {
|
||||
trigger_error($this->deprecatedPublicProperties[$propertyName], E_USER_DEPRECATED);
|
||||
}
|
||||
unset($this->$propertyName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Composer;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 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!
|
||||
*/
|
||||
|
||||
use Composer\Script\Event;
|
||||
use Composer\Util\Filesystem as FilesystemUtility;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use TYPO3\CMS\Composer\Plugin\Config;
|
||||
use TYPO3\CMS\Composer\Plugin\Core\InstallerScript;
|
||||
|
||||
class CliEntryPoint implements InstallerScript
|
||||
{
|
||||
/**
|
||||
* Absolute path to entry script source
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* The target file relative to the web directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $target;
|
||||
|
||||
public function __construct(string $source, string $target)
|
||||
{
|
||||
$this->source = $source;
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
public function run(Event $event): bool
|
||||
{
|
||||
$composer = $event->getComposer();
|
||||
$filesystemUtility = new FilesystemUtility();
|
||||
$filesystem = new Filesystem();
|
||||
$pluginConfig = Config::load($composer);
|
||||
|
||||
$entryPointContent = file_get_contents($this->source);
|
||||
$targetFile = $pluginConfig->get('root-dir') . '/' . $this->target;
|
||||
$autoloadFile = $composer->getConfig()->get('vendor-dir') . '/autoload.php';
|
||||
|
||||
$entryPointContent = preg_replace(
|
||||
'/__DIR__ . \'[^\']*\'/',
|
||||
$filesystemUtility->findShortestPathCode($targetFile, $autoloadFile),
|
||||
$entryPointContent
|
||||
);
|
||||
|
||||
$filesystemUtility->ensureDirectoryExists(dirname($targetFile));
|
||||
$filesystem->dumpFile($targetFile, $entryPointContent);
|
||||
$filesystem->chmod($targetFile, 0755);
|
||||
|
||||
return $filesystem->exists($targetFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Composer;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 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!
|
||||
*/
|
||||
|
||||
use Composer\Script\Event;
|
||||
use TYPO3\CMS\Composer\Plugin\Core\InstallerScriptsRegistration;
|
||||
use TYPO3\CMS\Composer\Plugin\Core\ScriptDispatcher;
|
||||
|
||||
/**
|
||||
* Hook into Composer build to generate TYPO3 cli tool entry script
|
||||
* @internal only used for TYPO3 internally for setting up the installation.
|
||||
*/
|
||||
class InstallerScripts implements InstallerScriptsRegistration
|
||||
{
|
||||
/**
|
||||
* @param Event $event
|
||||
* @param ScriptDispatcher $scriptDispatcher
|
||||
*/
|
||||
public static function register(Event $event, ScriptDispatcher $scriptDispatcher)
|
||||
{
|
||||
$source = dirname(__DIR__, 2) . '/Resources/Private/Php/cli.php';
|
||||
$target = 'typo3/sysext/core/bin/typo3';
|
||||
|
||||
$scriptDispatcher->addInstallerScript(new CliEntryPoint($source, $target));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle loading and writing of global and local (instance specific)
|
||||
* configuration.
|
||||
*
|
||||
* This class handles the access to the files
|
||||
* - EXT:core/Configuration/DefaultConfiguration.php (default TYPO3_CONF_VARS)
|
||||
* - typo3conf/LocalConfiguration.php (overrides of TYPO3_CONF_VARS)
|
||||
* - typo3conf/AdditionalConfiguration.php (optional additional local code blocks)
|
||||
*
|
||||
* IMPORTANT:
|
||||
* This class is intended for internal core use ONLY.
|
||||
* Extensions should usually use the resulting $GLOBALS['TYPO3_CONF_VARS'] array,
|
||||
* do not try to modify settings in LocalConfiguration.php with an extension.
|
||||
* @internal
|
||||
*/
|
||||
class ConfigurationManager
|
||||
{
|
||||
/**
|
||||
* @var string Path to default TYPO3_CONF_VARS file, relative to the public web folder
|
||||
*/
|
||||
protected $defaultConfigurationFile = 'core/Configuration/DefaultConfiguration.php';
|
||||
|
||||
/**
|
||||
* @var string Path to description file for TYPO3_CONF_VARS, relative to the public web folder
|
||||
*/
|
||||
protected $defaultConfigurationDescriptionFile = 'core/Configuration/DefaultConfigurationDescription.yaml';
|
||||
|
||||
/**
|
||||
* @var string Path to local overload TYPO3_CONF_VARS file, relative to the public web folder
|
||||
*/
|
||||
protected $localConfigurationFile = 'LocalConfiguration.php';
|
||||
|
||||
/**
|
||||
* @var string Path to additional local file, relative to the public web folder
|
||||
*/
|
||||
protected $additionalConfigurationFile = 'AdditionalConfiguration.php';
|
||||
|
||||
/**
|
||||
* @var string Path to factory configuration file used during installation as LocalConfiguration boilerplate
|
||||
*/
|
||||
protected $factoryConfigurationFile = 'core/Configuration/FactoryConfiguration.php';
|
||||
|
||||
/**
|
||||
* @var string Path to possible additional factory configuration file delivered by packages
|
||||
*/
|
||||
protected $additionalFactoryConfigurationFile = 'AdditionalFactoryConfiguration.php';
|
||||
|
||||
/**
|
||||
* Writing to these configuration paths is always allowed,
|
||||
* even if the requested sub path does not exist yet.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $whiteListedLocalConfigurationPaths = [
|
||||
'EXT/extConf',
|
||||
'EXTCONF',
|
||||
'DB',
|
||||
'SYS/caching/cacheConfigurations',
|
||||
'SYS/session',
|
||||
'EXTENSIONS',
|
||||
];
|
||||
|
||||
/**
|
||||
* Return default configuration array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultConfiguration()
|
||||
{
|
||||
return require $this->getDefaultConfigurationFileLocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of the default configuration file,
|
||||
* currently the path and filename.
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getDefaultConfigurationFileLocation()
|
||||
{
|
||||
return Environment::getFrameworkBasePath() . '/' . $this->defaultConfigurationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of the default configuration description file,
|
||||
* currently the path and filename.
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getDefaultConfigurationDescriptionFileLocation()
|
||||
{
|
||||
return Environment::getFrameworkBasePath() . '/' . $this->defaultConfigurationDescriptionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return local configuration array typo3conf/LocalConfiguration.php
|
||||
*
|
||||
* @return array Content array of local configuration file
|
||||
*/
|
||||
public function getLocalConfiguration()
|
||||
{
|
||||
return require $this->getLocalConfigurationFileLocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of the local configuration file,
|
||||
* currently the path and filename.
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getLocalConfigurationFileLocation()
|
||||
{
|
||||
return Environment::getLegacyConfigPath() . '/' . $this->localConfigurationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns local configuration array merged with default configuration
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMergedLocalConfiguration(): array
|
||||
{
|
||||
$localConfiguration = $this->getDefaultConfiguration();
|
||||
ArrayUtility::mergeRecursiveWithOverrule($localConfiguration, $this->getLocalConfiguration());
|
||||
return $localConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file location of the additional configuration file,
|
||||
* currently the path and filename.
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function getAdditionalConfigurationFileLocation()
|
||||
{
|
||||
return Environment::getLegacyConfigPath() . '/' . $this->additionalConfigurationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute file location of factory configuration file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFactoryConfigurationFileLocation()
|
||||
{
|
||||
return Environment::getFrameworkBasePath() . '/' . $this->factoryConfigurationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute file location of factory configuration file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAdditionalFactoryConfigurationFileLocation()
|
||||
{
|
||||
return Environment::getLegacyConfigPath() . '/' . $this->additionalFactoryConfigurationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override local configuration with new values.
|
||||
*
|
||||
* @param array $configurationToMerge Override configuration array
|
||||
*/
|
||||
public function updateLocalConfiguration(array $configurationToMerge)
|
||||
{
|
||||
$newLocalConfiguration = $this->getLocalConfiguration();
|
||||
ArrayUtility::mergeRecursiveWithOverrule($newLocalConfiguration, $configurationToMerge);
|
||||
$this->writeLocalConfiguration($newLocalConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value at given path from default configuration
|
||||
*
|
||||
* @param string $path Path to search for
|
||||
* @return mixed Value at path
|
||||
*/
|
||||
public function getDefaultConfigurationValueByPath($path)
|
||||
{
|
||||
return ArrayUtility::getValueByPath($this->getDefaultConfiguration(), $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value at given path from local configuration
|
||||
*
|
||||
* @param string $path Path to search for
|
||||
* @return mixed Value at path
|
||||
*/
|
||||
public function getLocalConfigurationValueByPath($path)
|
||||
{
|
||||
return ArrayUtility::getValueByPath($this->getLocalConfiguration(), $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from configuration, this is default configuration
|
||||
* merged with local configuration
|
||||
*
|
||||
* @param string $path Path to search for
|
||||
* @return mixed
|
||||
*/
|
||||
public function getConfigurationValueByPath($path)
|
||||
{
|
||||
$defaultConfiguration = $this->getDefaultConfiguration();
|
||||
ArrayUtility::mergeRecursiveWithOverrule($defaultConfiguration, $this->getLocalConfiguration());
|
||||
return ArrayUtility::getValueByPath($defaultConfiguration, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a given path in local configuration to a new value.
|
||||
*
|
||||
* @param string $path Path to update
|
||||
* @param mixed $value Value to set
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function setLocalConfigurationValueByPath($path, $value)
|
||||
{
|
||||
$result = false;
|
||||
if ($this->isValidLocalConfigurationPath($path)) {
|
||||
$localConfiguration = $this->getLocalConfiguration();
|
||||
$localConfiguration = ArrayUtility::setValueByPath($localConfiguration, $path, $value);
|
||||
$result = $this->writeLocalConfiguration($localConfiguration);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update / set a list of path and value pairs in local configuration file
|
||||
*
|
||||
* @param array $pairs Key is path, value is value to set
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function setLocalConfigurationValuesByPathValuePairs(array $pairs)
|
||||
{
|
||||
$localConfiguration = $this->getLocalConfiguration();
|
||||
foreach ($pairs as $path => $value) {
|
||||
if ($this->isValidLocalConfigurationPath($path)) {
|
||||
$localConfiguration = ArrayUtility::setValueByPath($localConfiguration, $path, $value);
|
||||
}
|
||||
}
|
||||
return $this->writeLocalConfiguration($localConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove keys from LocalConfiguration
|
||||
*
|
||||
* @param array $keys Array with key paths to remove from LocalConfiguration
|
||||
* @return bool TRUE if something was removed
|
||||
*/
|
||||
public function removeLocalConfigurationKeysByPath(array $keys)
|
||||
{
|
||||
$result = false;
|
||||
$localConfiguration = $this->getLocalConfiguration();
|
||||
foreach ($keys as $path) {
|
||||
// Remove key if path is within LocalConfiguration
|
||||
if (ArrayUtility::isValidPath($localConfiguration, $path)) {
|
||||
$result = true;
|
||||
$localConfiguration = ArrayUtility::removeByPath($localConfiguration, $path);
|
||||
}
|
||||
}
|
||||
if ($result) {
|
||||
$this->writeLocalConfiguration($localConfiguration);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a certain feature and writes the option to LocalConfiguration.php
|
||||
* Short-hand method
|
||||
*
|
||||
* @param string $featureName something like "InlineSvgImages"
|
||||
* @return bool true on successful writing the setting
|
||||
*/
|
||||
public function enableFeature(string $featureName): bool
|
||||
{
|
||||
return $this->setLocalConfigurationValueByPath('SYS/features/' . $featureName, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a feature and writes the option to LocalConfiguration.php
|
||||
* Short-hand method
|
||||
*
|
||||
* @param string $featureName something like "InlineSvgImages"
|
||||
* @return bool true on successful writing the setting
|
||||
*/
|
||||
public function disableFeature(string $featureName): bool
|
||||
{
|
||||
return $this->setLocalConfigurationValueByPath('SYS/features/' . $featureName, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the configuration can be written.
|
||||
*
|
||||
* @return bool
|
||||
* @internal
|
||||
*/
|
||||
public function canWriteConfiguration()
|
||||
{
|
||||
$fileLocation = $this->getLocalConfigurationFileLocation();
|
||||
return @is_writable(file_exists($fileLocation) ? $fileLocation : Environment::getLegacyConfigPath() . '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the configuration array and exports it to the global variable
|
||||
*
|
||||
* @internal
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function exportConfiguration()
|
||||
{
|
||||
if (@is_file($this->getLocalConfigurationFileLocation())) {
|
||||
$localConfiguration = $this->getLocalConfiguration();
|
||||
if (is_array($localConfiguration)) {
|
||||
$defaultConfiguration = $this->getDefaultConfiguration();
|
||||
ArrayUtility::mergeRecursiveWithOverrule($defaultConfiguration, $localConfiguration);
|
||||
$GLOBALS['TYPO3_CONF_VARS'] = $defaultConfiguration;
|
||||
} else {
|
||||
throw new \UnexpectedValueException('LocalConfiguration invalid.', 1349272276);
|
||||
}
|
||||
} else {
|
||||
// No LocalConfiguration (yet), load DefaultConfiguration
|
||||
$GLOBALS['TYPO3_CONF_VARS'] = $this->getDefaultConfiguration();
|
||||
}
|
||||
|
||||
// Load AdditionalConfiguration
|
||||
if (@is_file($this->getAdditionalConfigurationFileLocation())) {
|
||||
require $this->getAdditionalConfigurationFileLocation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write local configuration array to typo3conf/LocalConfiguration.php
|
||||
*
|
||||
* @param array $configuration The local configuration to be written
|
||||
* @throws \RuntimeException
|
||||
* @return bool TRUE on success
|
||||
* @internal
|
||||
*/
|
||||
public function writeLocalConfiguration(array $configuration)
|
||||
{
|
||||
$localConfigurationFile = $this->getLocalConfigurationFileLocation();
|
||||
if (!$this->canWriteConfiguration()) {
|
||||
throw new \RuntimeException(
|
||||
$localConfigurationFile . ' is not writable.',
|
||||
1346323822
|
||||
);
|
||||
}
|
||||
$configuration = ArrayUtility::sortByKeyRecursive($configuration);
|
||||
$result = GeneralUtility::writeFile(
|
||||
$localConfigurationFile,
|
||||
"<?php\n" .
|
||||
'return ' .
|
||||
ArrayUtility::arrayExport($configuration) .
|
||||
";\n",
|
||||
true
|
||||
);
|
||||
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($localConfigurationFile);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write additional configuration array to typo3conf/AdditionalConfiguration.php
|
||||
*
|
||||
* @param array $additionalConfigurationLines The configuration lines to be written
|
||||
* @throws \RuntimeException
|
||||
* @return bool TRUE on success
|
||||
* @internal
|
||||
*/
|
||||
public function writeAdditionalConfiguration(array $additionalConfigurationLines)
|
||||
{
|
||||
return GeneralUtility::writeFile(
|
||||
$this->getAdditionalConfigurationFileLocation(),
|
||||
"<?php\n" . implode("\n", $additionalConfigurationLines) . "\n"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses FactoryConfiguration file and a possible AdditionalFactoryConfiguration
|
||||
* file in typo3conf to create a basic LocalConfiguration.php. This is used
|
||||
* by the install tool in an early step.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* @internal
|
||||
*/
|
||||
public function createLocalConfigurationFromFactoryConfiguration()
|
||||
{
|
||||
if (file_exists($this->getLocalConfigurationFileLocation())) {
|
||||
throw new \RuntimeException(
|
||||
'LocalConfiguration.php exists already',
|
||||
1364836026
|
||||
);
|
||||
}
|
||||
$localConfigurationArray = require $this->getFactoryConfigurationFileLocation();
|
||||
$additionalFactoryConfigurationFileLocation = $this->getAdditionalFactoryConfigurationFileLocation();
|
||||
if (file_exists($additionalFactoryConfigurationFileLocation)) {
|
||||
$additionalFactoryConfigurationArray = require $additionalFactoryConfigurationFileLocation;
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$localConfigurationArray,
|
||||
$additionalFactoryConfigurationArray
|
||||
);
|
||||
}
|
||||
$randomKey = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
|
||||
$localConfigurationArray['SYS']['encryptionKey'] = $randomKey;
|
||||
|
||||
$this->writeLocalConfiguration($localConfigurationArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if access / write to given path in local configuration is allowed.
|
||||
*
|
||||
* @param string $path Path to search for
|
||||
* @return bool TRUE if access is allowed
|
||||
*/
|
||||
protected function isValidLocalConfigurationPath($path)
|
||||
{
|
||||
// Early return for white listed paths
|
||||
foreach ($this->whiteListedLocalConfigurationPaths as $whiteListedPath) {
|
||||
if (GeneralUtility::isFirstPartOfStr($path, $whiteListedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return ArrayUtility::isValidPath($this->getDefaultConfiguration(), $path);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* An exception thrown if ExtensionConfiguration->get() is called for
|
||||
* an extension that has no configuration.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ExtensionConfigurationExtensionNotConfiguredException extends Exception
|
||||
{
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* An exception thrown if ExtensionConfiguration->get() is called with
|
||||
* a path that does not exist within the extension configuration.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ExtensionConfigurationPathDoesNotExistException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Configuration;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
|
||||
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* API to get() instance specific extension configuration options.
|
||||
*
|
||||
* Extension authors are encouraged to use this API - it is currently a simple
|
||||
* wrapper to access TYPO3_CONF_VARS['EXTENSIONS'] but could later become something
|
||||
* different in case core decides to store extension configuration elsewhere.
|
||||
*
|
||||
* Extension authors must not access TYPO3_CONF_VARS['EXTENSIONS'] on their own.
|
||||
*
|
||||
* Extension configurations are often 'feature flags' currently defined by
|
||||
* ext_conf_template.txt files. The core (more specifically the install tool)
|
||||
* takes care default values and overridden values are properly prepared upon
|
||||
* loading or updating an extension.
|
||||
*
|
||||
* Note only ->get() is official API and other public methods are low level
|
||||
* core internal API that is usually only used by extension manager and install tool.
|
||||
*/
|
||||
class ExtensionConfiguration
|
||||
{
|
||||
/**
|
||||
* TypoScript hierarchy being build.
|
||||
* Used parsing ext_conf_template.txt
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $setup = [];
|
||||
|
||||
/**
|
||||
* Raw data, the input string exploded by LF.
|
||||
* Used parsing ext_conf_template.txt
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $raw;
|
||||
|
||||
/**
|
||||
* Pointer to entry in raw data array.
|
||||
* Used parsing ext_conf_template.txt
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $rawPointer = 0;
|
||||
|
||||
/**
|
||||
* Holding the value of the last comment
|
||||
* Used parsing ext_conf_template.txt
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $lastComment = '';
|
||||
|
||||
/**
|
||||
* Internal flag to create a multi-line comment (one of those like /* ... * /)
|
||||
* Used parsing ext_conf_template.txt
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $commentSet = false;
|
||||
|
||||
/**
|
||||
* Internally set, when in brace. Counter.
|
||||
* Used parsing ext_conf_template.txt
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $inBrace = 0;
|
||||
|
||||
/**
|
||||
* Get a single configuration value, a sub array or the whole configuration.
|
||||
*
|
||||
* Examples:
|
||||
* // Simple and typical usage: Get a single config value, or an array if the key is a "TypoScript"
|
||||
* // a-like sub-path in ext_conf_template.txt "foo.bar = defaultValue"
|
||||
* ->get('myExtension', 'aConfigKey');
|
||||
*
|
||||
* // Get all current configuration values, always an array
|
||||
* ->get('myExtension');
|
||||
*
|
||||
* // Get a nested config value if the path is a "TypoScript" a-like sub-path
|
||||
* // in ext_conf_template.txt "topLevelKey.subLevelKey = defaultValue"
|
||||
* ->get('myExtension', 'topLevelKey/subLevelKey')
|
||||
*
|
||||
* Notes:
|
||||
* - If a configuration or configuration path of an extension is not found, the
|
||||
* code tries to synchronize configuration with ext_conf_template.txt first, only
|
||||
* if still not found, it will throw exceptions.
|
||||
* - Return values are NOT type safe: A boolean false could be returned as string 0.
|
||||
* Cast accordingly.
|
||||
* - This API throws exceptions if the path does not exist or the extension
|
||||
* configuration is not available. The install tool takes care any new
|
||||
* ext_conf_template.txt values are available TYPO3_CONF_VARS['EXTENSIONS'],
|
||||
* a thrown exception indicates a programming error on developer side
|
||||
* and should not be caught.
|
||||
* - It is not checked if the extension in question is loaded at all,
|
||||
* it's just checked the extension configuration path exists.
|
||||
* - Extensions should typically not get configuration of a different extension.
|
||||
*
|
||||
* @param string $extension Extension name
|
||||
* @param string $path Configuration path - eg. "featureCategory/coolThingIsEnabled"
|
||||
* @return mixed The value. Can be a sub array or a single value.
|
||||
* @throws ExtensionConfigurationExtensionNotConfiguredException If ext configuration does no exist
|
||||
* @throws ExtensionConfigurationPathDoesNotExistException If a requested extension path does not exist
|
||||
*/
|
||||
public function get(string $extension, string $path = '')
|
||||
{
|
||||
$hasBeenSynchronized = false;
|
||||
if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
|
||||
// This if() should not be hit at "casual" runtime, but only in early setup phases
|
||||
$this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
|
||||
$hasBeenSynchronized = true;
|
||||
if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
|
||||
// If there is still no such entry, even after sync -> throw
|
||||
throw new ExtensionConfigurationExtensionNotConfiguredException(
|
||||
'No extension configuration for extension ' . $extension . ' found. Either this extension'
|
||||
. ' has no extension configuration or the configuration is not up to date. Execute the'
|
||||
. ' install tool to update configuration.',
|
||||
1509654728
|
||||
);
|
||||
}
|
||||
}
|
||||
if (empty($path)) {
|
||||
return $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension];
|
||||
}
|
||||
if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
|
||||
// This if() should not be hit at "casual" runtime, but only in early setup phases
|
||||
if (!$hasBeenSynchronized) {
|
||||
$this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
|
||||
}
|
||||
// If there is still no such entry, even after sync -> throw
|
||||
if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
|
||||
throw new ExtensionConfigurationPathDoesNotExistException(
|
||||
'Path ' . $path . ' does not exist in extension configuration',
|
||||
1509977699
|
||||
);
|
||||
}
|
||||
}
|
||||
return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new or overwrite an existing configuration value.
|
||||
*
|
||||
* This is typically used by core internal low level tasks like the install
|
||||
* tool but may become handy if an extension needs to update extension configuration
|
||||
* on the fly for whatever reason.
|
||||
*
|
||||
* Examples:
|
||||
* // Set a full extension configuration ($value could be a nested array, too)
|
||||
* ->set('myExtension', ['aFeature' => 'true', 'aCustomClass' => 'css-foo'])
|
||||
*
|
||||
* // Unset a whole extension configuration
|
||||
* ->set('myExtension')
|
||||
*
|
||||
* Notes:
|
||||
* - Do NOT call this at arbitrary places during runtime (eg. NOT in ext_localconf.php or
|
||||
* similar). ->set() is not supposed to be called each request since it writes LocalConfiguration
|
||||
* each time. This API is however OK to be called from extension manager hooks.
|
||||
* - Values are not type safe, if the install tool wrote them,
|
||||
* boolean true could become string 1 on ->get()
|
||||
* - It is not possible to store 'null' as value, giving $value=null
|
||||
* or no value at all will unset the path
|
||||
* - Setting a value and calling ->get() afterwards will still return the new value.
|
||||
* - Warning on AdditionalConfiguration.php: If this file overwrites settings, it spoils the
|
||||
* ->set() call and values may not end up as expected.
|
||||
*
|
||||
* @param string $extension Extension name
|
||||
* @param string $path Configuration path to set - eg. "featureCategory/coolThingIsEnabled"
|
||||
* @param null $value The value. If null, unset the path
|
||||
* @internal
|
||||
*/
|
||||
public function set(string $extension, string $path = '', $value = null)
|
||||
{
|
||||
if (empty($extension)) {
|
||||
throw new \RuntimeException('extension name must not be empty', 1509715852);
|
||||
}
|
||||
if (!empty($path)) {
|
||||
// @todo: this functionality can be removed once EXT:bootstrap_package is adapted to the new API.
|
||||
$extensionConfiguration = $this->get($extension);
|
||||
$value = ArrayUtility::setValueByPath($extensionConfiguration, $path, $value);
|
||||
}
|
||||
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||
if ($value === null) {
|
||||
// Remove whole extension config
|
||||
$configurationManager->removeLocalConfigurationKeysByPath(['EXTENSIONS/' . $extension]);
|
||||
if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
|
||||
unset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]);
|
||||
}
|
||||
} else {
|
||||
// Set full extension config
|
||||
$configurationManager->setLocalConfigurationValueByPath('EXTENSIONS/' . $extension, $value);
|
||||
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension] = $value;
|
||||
}
|
||||
|
||||
// After TYPO3_CONF_VARS['EXTENSIONS'] has been written, update legacy layer TYPO3_CONF_VARS['EXTENSIONS']['extConf']
|
||||
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 with removal of old serialized 'extConf' layer
|
||||
if (!empty($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'])) {
|
||||
$extConfArray = [];
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] as $extensionName => $extensionConfig) {
|
||||
$extConfArray[$extensionName] = serialize($this->addDotsToArrayKeysRecursiveForLegacyExtConf($extensionConfig));
|
||||
}
|
||||
$configurationManager->setLocalConfigurationValueByPath('EXT/extConf', $extConfArray);
|
||||
$GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'] = $extConfArray;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new configuration of all extensions and reload TYPO3_CONF_VARS.
|
||||
* This is a "do all" variant of set() for all extensions that prevents
|
||||
* writing and loading LocalConfiguration many times.
|
||||
*
|
||||
* @param array $configuration Configuration of all extensions
|
||||
* @internal
|
||||
*/
|
||||
public function setAll(array $configuration)
|
||||
{
|
||||
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||
$configurationManager->setLocalConfigurationValueByPath('EXTENSIONS', $configuration);
|
||||
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] = $configuration;
|
||||
|
||||
// After TYPO3_CONF_VARS['EXTENSIONS'] has been written, update legacy layer TYPO3_CONF_VARS['EXTENSIONS']['extConf']
|
||||
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 with removal of old serialized 'extConf' layer
|
||||
$extConfArray = [];
|
||||
foreach ($configuration as $extensionName => $extensionConfig) {
|
||||
$extConfArray[$extensionName] = serialize($this->addDotsToArrayKeysRecursiveForLegacyExtConf($extensionConfig));
|
||||
}
|
||||
$configurationManager->setLocalConfigurationValueByPath('EXT/extConf', $extConfArray);
|
||||
$GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'] = $extConfArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are new config settings in ext_conf_template of an extension,
|
||||
* they are found here and synchronized to LocalConfiguration['EXTENSIONS'].
|
||||
*
|
||||
* Used when entering the install tool, during installation and if calling ->get()
|
||||
* with an extension or path that is not yet found in LocalConfiguration
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions()
|
||||
{
|
||||
$activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
|
||||
$fullConfiguration = [];
|
||||
$currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] ?? [];
|
||||
foreach ($activePackages as $package) {
|
||||
if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
|
||||
continue;
|
||||
}
|
||||
$extensionKey = $package->getPackageKey();
|
||||
$currentExtensionConfig = $currentLocalConfiguration[$extensionKey] ?? [];
|
||||
$extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentExtensionConfig);
|
||||
if (!empty($extConfTemplateConfiguration)) {
|
||||
$fullConfiguration[$extensionKey] = $extConfTemplateConfiguration;
|
||||
}
|
||||
}
|
||||
// Write new config if changed. Loose array comparison to not write if only array key order is different
|
||||
if ($fullConfiguration != $currentLocalConfiguration) {
|
||||
$this->setAll($fullConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read values from ext_conf_template, verify if they are in LocalConfiguration.php
|
||||
* already and if not, add them.
|
||||
*
|
||||
* Used public by extension manager when updating extension
|
||||
*
|
||||
* @param string $extensionKey The extension to sync
|
||||
* @internal
|
||||
*/
|
||||
public function synchronizeExtConfTemplateWithLocalConfiguration(string $extensionKey)
|
||||
{
|
||||
$package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey);
|
||||
if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
|
||||
return;
|
||||
}
|
||||
$currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extensionKey] ?? [];
|
||||
$extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentLocalConfiguration);
|
||||
// Write new config if changed. Loose array comparison to not write if only array key order is different
|
||||
if ($extConfTemplateConfiguration != $currentLocalConfiguration) {
|
||||
$this->set($extensionKey, '', $extConfTemplateConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The old EXT/extConf layer had '.' (dots) at the end of all nested array keys. This is created here
|
||||
* to keep EXT/extConf format compatible with old not yet adapted extensions.
|
||||
* But extensions may rely on ending dots if using legacy unserialize() on their extensions, too.
|
||||
*
|
||||
* A EXTENSIONS array like:
|
||||
* TYPO3_CONF_VARS['EXTENSIONS']['someExtension'] => [
|
||||
* 'someKey' => [
|
||||
* 'someSubKey' => [
|
||||
* 'someSubSubKey' => 'someValue',
|
||||
* ],
|
||||
* ],
|
||||
* ]
|
||||
* becomes (serialized) in old EXT/extConf (mind the dots and end of array keys for sub arrays):
|
||||
* TYPO3_CONF_VARS['EXTENSIONS']['someExtension'] => [
|
||||
* 'someKey.' => [
|
||||
* 'someSubKey.' => [
|
||||
* 'someSubSubKey' => 'someValue',
|
||||
* ],
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* @param array $extensionConfig
|
||||
* @return array
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 with removal of old serialized 'extConf' layer
|
||||
*/
|
||||
private function addDotsToArrayKeysRecursiveForLegacyExtConf(array $extensionConfig): array
|
||||
{
|
||||
$newArray = [];
|
||||
foreach ($extensionConfig as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$newArray[$key . '.'] = $this->addDotsToArrayKeysRecursiveForLegacyExtConf($value);
|
||||
} else {
|
||||
$newArray[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $newArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* Poor man version of getDefaultConfigurationFromExtConfTemplateAsValuedArray() which ignores
|
||||
* comments and returns ext_conf_template as array where nested keys have no dots.
|
||||
*
|
||||
* @param string $extensionKey
|
||||
* @return array
|
||||
*/
|
||||
protected function getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots(string $extensionKey): array
|
||||
{
|
||||
$configuration = $this->getParsedExtConfTemplate($extensionKey);
|
||||
return $this->removeCommentsAndDotsRecursive($configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger main ext_conf_template.txt parsing logic.
|
||||
* Needs to be public as it is used by install tool ExtensionConfigurationService
|
||||
* which adds the comment parsing on top for display of options in install tool.
|
||||
*
|
||||
* @param string $extensionKey
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
public function getParsedExtConfTemplate(string $extensionKey): array
|
||||
{
|
||||
$rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
|
||||
$configuration = [];
|
||||
if ((string)$rawConfigurationString !== '') {
|
||||
$this->raw = explode(LF, $rawConfigurationString);
|
||||
$this->rawPointer = 0;
|
||||
$this->setup = [];
|
||||
$this->parseSub($this->setup);
|
||||
if ($this->inBrace) {
|
||||
throw new \RuntimeException(
|
||||
'Line ' . ($this->rawPointer - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)',
|
||||
1507645349
|
||||
);
|
||||
}
|
||||
$configuration = $this->setup;
|
||||
}
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* Return content of an extensions ext_conf_template.txt file if
|
||||
* the file exists, empty string if file does not exist.
|
||||
*
|
||||
* @param string $extensionKey Extension key
|
||||
* @return string
|
||||
*/
|
||||
protected function getDefaultConfigurationRawString(string $extensionKey): string
|
||||
{
|
||||
$rawString = '';
|
||||
$extConfTemplateFileLocation = GeneralUtility::getFileAbsFileName(
|
||||
'EXT:' . $extensionKey . '/ext_conf_template.txt'
|
||||
);
|
||||
if (file_exists($extConfTemplateFileLocation)) {
|
||||
$rawString = file_get_contents($extConfTemplateFileLocation);
|
||||
}
|
||||
return $rawString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* "Comments" from the "TypoScript" parser below are identified by two (!) dots at the end of array keys
|
||||
* and all array keys have a single dot at the end, if they have sub arrays. This is cleaned here.
|
||||
*
|
||||
* Incoming array:
|
||||
* [
|
||||
* 'automaticInstallation' => '1',
|
||||
* 'automaticInstallation..' => '# cat=basic/enabled; ...'
|
||||
* 'FE.' => [
|
||||
* 'enabled' = '1',
|
||||
* 'enabled..' => '# cat=basic/enabled; ...'
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* Output array:
|
||||
* [
|
||||
* 'automaticInstallation' => '1',
|
||||
* 'FE' => [
|
||||
* 'enabled' => '1',
|
||||
* ]
|
||||
*
|
||||
* @param array $config Incoming configuration
|
||||
* @return array
|
||||
*/
|
||||
protected function removeCommentsAndDotsRecursive(array $config): array
|
||||
{
|
||||
$cleanedConfig = [];
|
||||
foreach ($config as $key => $value) {
|
||||
if (substr($key, -2) === '..') {
|
||||
continue;
|
||||
}
|
||||
if (substr($key, -1) === '.') {
|
||||
$cleanedConfig[rtrim($key, '.')] = $this->removeCommentsAndDotsRecursive($value);
|
||||
} else {
|
||||
$cleanedConfig[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $cleanedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* Parsing the $this->raw TypoScript lines from pointer, $this->rawP
|
||||
*
|
||||
* @param array $setup Reference to the setup array in which to accumulate the values.
|
||||
*/
|
||||
protected function parseSub(array &$setup)
|
||||
{
|
||||
while (isset($this->raw[$this->rawPointer])) {
|
||||
$line = ltrim($this->raw[$this->rawPointer]);
|
||||
$this->rawPointer++;
|
||||
// Set comment flag?
|
||||
if (strpos($line, '/*') === 0) {
|
||||
$this->commentSet = 1;
|
||||
}
|
||||
if (!$this->commentSet && $line) {
|
||||
if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
|
||||
// If not brace-end or comment
|
||||
// Find object name string until we meet an operator
|
||||
$varL = strcspn($line, "\t" . ' {=<>(');
|
||||
// check for special ":=" operator
|
||||
if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
|
||||
--$varL;
|
||||
}
|
||||
// also remove tabs after the object string name
|
||||
$objStrName = substr($line, 0, $varL);
|
||||
if ($objStrName !== '') {
|
||||
$r = [];
|
||||
if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
|
||||
throw new \RuntimeException(
|
||||
'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."',
|
||||
1507645381
|
||||
);
|
||||
}
|
||||
$line = ltrim(substr($line, $varL));
|
||||
if ($line === '') {
|
||||
throw new \RuntimeException(
|
||||
'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
|
||||
1507645417
|
||||
);
|
||||
}
|
||||
switch ($line[0]) {
|
||||
case '=':
|
||||
if (strpos($objStrName, '.') !== false) {
|
||||
$value = [];
|
||||
$value[0] = trim(substr($line, 1));
|
||||
$this->setVal($objStrName, $setup, $value);
|
||||
} else {
|
||||
$setup[$objStrName] = trim(substr($line, 1));
|
||||
if ($this->lastComment) {
|
||||
// Setting comment..
|
||||
$setup[$objStrName . '..'] .= $this->lastComment;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '{':
|
||||
$this->inBrace++;
|
||||
if (strpos($objStrName, '.') !== false) {
|
||||
$this->rollParseSub($objStrName, $setup);
|
||||
} else {
|
||||
if (!isset($setup[$objStrName . '.'])) {
|
||||
$setup[$objStrName . '.'] = [];
|
||||
}
|
||||
$this->parseSub($setup[$objStrName . '.']);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new \RuntimeException(
|
||||
'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
|
||||
1507645445
|
||||
);
|
||||
}
|
||||
|
||||
$this->lastComment = '';
|
||||
}
|
||||
} elseif ($line[0] === '}') {
|
||||
$this->inBrace--;
|
||||
$this->lastComment = '';
|
||||
if ($this->inBrace < 0) {
|
||||
throw new \RuntimeException(
|
||||
'Line ' . ($this->rawPointer - 1) . ': An end brace is in excess.',
|
||||
1507645489
|
||||
);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
$this->lastComment .= rtrim($line) . LF;
|
||||
}
|
||||
}
|
||||
// Unset comment
|
||||
if ($this->commentSet) {
|
||||
if (strpos($line, '*/') === 0) {
|
||||
$this->commentSet = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
|
||||
* thus having to recursively call itself to get the value.
|
||||
*
|
||||
* @param string $string The object sub-path, eg "thisprop.another_prot"
|
||||
* @param array $setup The local setup array from the function calling this function
|
||||
*/
|
||||
protected function rollParseSub($string, array &$setup)
|
||||
{
|
||||
if ((string)$string === '') {
|
||||
return;
|
||||
}
|
||||
list($key, $remainingKey) = $this->parseNextKeySegment($string);
|
||||
$key .= '.';
|
||||
if (!isset($setup[$key])) {
|
||||
$setup[$key] = [];
|
||||
}
|
||||
$remainingKey === ''
|
||||
? $this->parseSub($setup[$key])
|
||||
: $this->rollParseSub($remainingKey, $setup[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* Setting a value/property of an object string in the setup array.
|
||||
*
|
||||
* @param string $string The object sub-path, eg "thisprop.another_prot
|
||||
* @param array $setup The local setup array from the function calling this function.
|
||||
* @param void
|
||||
*/
|
||||
protected function setVal($string, array &$setup, $value)
|
||||
{
|
||||
if ((string)$string === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
list($key, $remainingKey) = $this->parseNextKeySegment($string);
|
||||
$subKey = $key . '.';
|
||||
if ($remainingKey === '') {
|
||||
if (isset($value[0])) {
|
||||
$setup[$key] = $value[0];
|
||||
}
|
||||
if (isset($value[1])) {
|
||||
$setup[$subKey] = $value[1];
|
||||
}
|
||||
if ($this->lastComment) {
|
||||
$setup[$key . '..'] .= $this->lastComment;
|
||||
}
|
||||
} else {
|
||||
if (!isset($setup[$subKey])) {
|
||||
$setup[$subKey] = [];
|
||||
}
|
||||
$this->setVal($remainingKey, $setup[$subKey], $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method of ext_conf_template.txt parsing.
|
||||
*
|
||||
* Determines the first key segment of a TypoScript key by searching for the first
|
||||
* unescaped dot in the given key string.
|
||||
*
|
||||
* Since the escape characters are only needed to correctly determine the key
|
||||
* segment any escape characters before the first unescaped dot are
|
||||
* stripped from the key.
|
||||
*
|
||||
* @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
|
||||
* @return array Array with key segment and remaining part of $key
|
||||
*/
|
||||
protected function parseNextKeySegment($key): array
|
||||
{
|
||||
// if no dot is in the key, nothing to do
|
||||
$dotPosition = strpos($key, '.');
|
||||
if ($dotPosition === false) {
|
||||
return [$key, ''];
|
||||
}
|
||||
|
||||
if (strpos($key, '\\') !== false) {
|
||||
// backslashes are in the key, so we do further parsing
|
||||
while ($dotPosition !== false) {
|
||||
if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
|
||||
break;
|
||||
}
|
||||
// escaped dot found, continue
|
||||
$dotPosition = strpos($key, '.', $dotPosition + 1);
|
||||
}
|
||||
|
||||
if ($dotPosition === false) {
|
||||
// no regular dot found
|
||||
$keySegment = $key;
|
||||
$remainingKey = '';
|
||||
} else {
|
||||
if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
|
||||
$keySegment = substr($key, 0, $dotPosition - 1);
|
||||
} else {
|
||||
$keySegment = substr($key, 0, $dotPosition);
|
||||
}
|
||||
$remainingKey = substr($key, $dotPosition + 1);
|
||||
}
|
||||
|
||||
// fix key segment by removing escape sequences
|
||||
$keySegment = str_replace('\\.', '.', $keySegment);
|
||||
} else {
|
||||
// no backslash in the key, we're fine off
|
||||
list($keySegment, $remainingKey) = explode('.', $key, 2);
|
||||
}
|
||||
return [$keySegment, $remainingKey];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Configuration;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A lightweight API class to check if a feature is enabled.
|
||||
*
|
||||
* Features are simple options (true/false), and are stored in the
|
||||
* global configuration array $TYPO3_CONF_VARS[SYS][features].
|
||||
*
|
||||
* For disabling or enabling a feature the "ConfigurationManager"
|
||||
* should be used.
|
||||
*
|
||||
* -- Naming --
|
||||
*
|
||||
* Feature names should NEVER named "enable" or having a negation, or containing versions or years
|
||||
* "enableFeatureXyz"
|
||||
* "disableOverlays"
|
||||
* "schedulerRevamped"
|
||||
* "useDoctrineQueries"
|
||||
* "disablePreparedStatements"
|
||||
* "disableHooksInFE"
|
||||
*
|
||||
* Proper namings for features
|
||||
* "ExtendedRichtextFormat"
|
||||
* "NativeYamlParser"
|
||||
* "InlinePageTranslations"
|
||||
* "TypoScriptParserIncludesAsXml"
|
||||
* "NativeDoctrineQueries"
|
||||
*
|
||||
* Ideally, these feature switches are added via the Install Tool or via FactoryConfiguration
|
||||
* and can be used for Extensions as well.
|
||||
*
|
||||
* --- Usage ---
|
||||
*
|
||||
* if (GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('InlineSvg')) {
|
||||
* ... do stuff here ...
|
||||
* }
|
||||
*/
|
||||
class Features
|
||||
{
|
||||
/**
|
||||
* Checks if a feature is active
|
||||
*
|
||||
* @param string $featureName the name of the feature
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureEnabled(string $featureName): bool
|
||||
{
|
||||
return isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['features'][$featureName]) && $GLOBALS['TYPO3_CONF_VARS']['SYS']['features'][$featureName] === true;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Abstract exception thrown if data structures can not be resolved,
|
||||
* found or parsed.
|
||||
*/
|
||||
abstract class AbstractInvalidDataStructureException extends Exception
|
||||
{
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Thrown if a combined ds_pointerField can not be resolved.
|
||||
*/
|
||||
class InvalidCombinedPointerFieldException extends AbstractInvalidDataStructureException
|
||||
{
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Thrown if parseFlexFormDataStructureByIdentifier() is given an empty string
|
||||
*/
|
||||
class InvalidIdentifierException extends AbstractInvalidDataStructureException
|
||||
{
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exception thrown if lookup of a parent row in a tree does not return a valid result
|
||||
* during flex from structure lookup.
|
||||
*/
|
||||
class InvalidParentRowException extends AbstractInvalidDataStructureException
|
||||
{
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exception thrown if lookup of a parent row in a tree results in a loop.
|
||||
*/
|
||||
class InvalidParentRowLoopException extends AbstractInvalidDataStructureException
|
||||
{
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exception thrown if lookup of a parent row in a tree is root node and still nothing was found.
|
||||
*/
|
||||
class InvalidParentRowRootException extends AbstractInvalidDataStructureException
|
||||
{
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exception thrown if ds_pointerField does not point to a valid value.
|
||||
*/
|
||||
class InvalidPointerFieldValueException extends AbstractInvalidDataStructureException
|
||||
{
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Thrown if a single ds_pointerField can not be resolved.
|
||||
*/
|
||||
class InvalidSinglePointerFieldException extends AbstractInvalidDataStructureException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\FlexForm\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Thrown if TCA is invalid.
|
||||
* This may happen if a record is opened that points to a data structure of
|
||||
* a no longer loaded extension
|
||||
*/
|
||||
class InvalidTcaException extends AbstractInvalidDataStructureException
|
||||
{
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Core\Configuration\Loader\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
class YamlFileLoadingException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Core\Configuration\Loader\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
class YamlParseException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\Loader;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlFileLoadingException;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlParseException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A YAML file loader that allows to load YAML files, based on the Symfony/Yaml component
|
||||
*
|
||||
* In addition to just load a YAML file, it adds some special functionality.
|
||||
*
|
||||
* - A special "imports" key in the YAML file allows to include other YAML files recursively
|
||||
* where the actual YAML file gets loaded after the import statements, which are interpreted at the very beginning
|
||||
*
|
||||
* - Merging configuration options of import files when having simple "lists" will add items to the list instead
|
||||
* of overwriting them.
|
||||
*
|
||||
* - Special placeholder values set via %optionA.suboptionB% replace the value with the named path of the configuration
|
||||
* The placeholders will act as a full replacement of this value.
|
||||
*
|
||||
* - Environment placeholder values set via %env(option)% will be replaced by env variables of the same name
|
||||
*/
|
||||
class YamlFileLoader implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
public const PROCESS_PLACEHOLDERS = 1;
|
||||
public const PROCESS_IMPORTS = 2;
|
||||
|
||||
/**
|
||||
* Loads and parses a YAML file, and returns an array with the found data
|
||||
*
|
||||
* @param string $fileName either relative to TYPO3's base project folder or prefixed with EXT:...
|
||||
* @param int $flags Flags to configure behaviour of the loader: see public PROCESS_ constants above
|
||||
* @return array the configuration as array
|
||||
*/
|
||||
public function load(string $fileName, int $flags = self::PROCESS_PLACEHOLDERS | self::PROCESS_IMPORTS): array
|
||||
{
|
||||
$content = $this->getFileContents($fileName);
|
||||
$content = Yaml::parse($content);
|
||||
|
||||
if (!is_array($content)) {
|
||||
throw new YamlParseException(
|
||||
'YAML file "' . $fileName . '" could not be parsed into valid syntax, probably empty?',
|
||||
1497332874
|
||||
);
|
||||
}
|
||||
|
||||
if (($flags & self::PROCESS_IMPORTS) === self::PROCESS_IMPORTS) {
|
||||
$content = $this->processImports($content);
|
||||
}
|
||||
if (($flags & self::PROCESS_PLACEHOLDERS) === self::PROCESS_PLACEHOLDERS) {
|
||||
// Check for "%" placeholders
|
||||
$content = $this->processPlaceholders($content, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put into a separate method to ease the pains with unit tests
|
||||
*
|
||||
* @param string $fileName either relative to TYPO3's base project folder or prefixed with EXT:...
|
||||
*
|
||||
* @return string the contents of the file
|
||||
* @throws YamlFileLoadingException when the file was not accessible
|
||||
*/
|
||||
protected function getFileContents(string $fileName): string
|
||||
{
|
||||
$streamlinedFileName = GeneralUtility::getFileAbsFileName($fileName);
|
||||
if (!$streamlinedFileName) {
|
||||
throw new YamlFileLoadingException('YAML File "' . $fileName . '" could not be loaded', 1485784246);
|
||||
}
|
||||
return file_get_contents($streamlinedFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return value from environment variable
|
||||
*
|
||||
* Environment variables may only contain word characters and underscores (a-zA-Z0-9_)
|
||||
* to be compatible to shell environments.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
protected function getValueFromEnv(string $value): string
|
||||
{
|
||||
$matches = [];
|
||||
preg_match_all('/%env\([\'"]?(\w+)[\'"]?\)%/', $value, $matches);
|
||||
$envVars = array_combine($matches[0], $matches[1]);
|
||||
foreach ($envVars as $substring => $envVarName) {
|
||||
$envVar = getenv($envVarName);
|
||||
$value = $envVar ? str_replace($substring, $envVar, $value) : $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the special "imports" key on the main level of a file,
|
||||
* which calls "load" recursively.
|
||||
* @param array $content
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function processImports(array $content): array
|
||||
{
|
||||
if (isset($content['imports']) && is_array($content['imports'])) {
|
||||
foreach ($content['imports'] as $import) {
|
||||
try {
|
||||
$importedContent = $this->load($import['resource']);
|
||||
// override the imported content with the one from the current file
|
||||
$content = $this->merge($importedContent, $content);
|
||||
} catch (YamlParseException | YamlFileLoadingException $exception) {
|
||||
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
unset($content['imports']);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function that gets called recursively to check for %...% placeholders
|
||||
* inside the array
|
||||
*
|
||||
* @param array $content the current sub-level content array
|
||||
* @param array $referenceArray the global configuration array
|
||||
*
|
||||
* @return array the modified sub-level content array
|
||||
*/
|
||||
protected function processPlaceholders(array $content, array $referenceArray): array
|
||||
{
|
||||
foreach ($content as $k => $v) {
|
||||
if ($this->isEnvPlaceholder($v)) {
|
||||
$content[$k] = $this->getValueFromEnv($v);
|
||||
} elseif ($this->isPlaceholder($v)) {
|
||||
$content[$k] = $this->getValueFromReferenceArray($v, $referenceArray);
|
||||
} elseif (is_array($v)) {
|
||||
$content[$k] = $this->processPlaceholders($v, $referenceArray);
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for a placeholder as fetched from the referenceArray
|
||||
*
|
||||
* @param string $placeholder the string to search for
|
||||
* @param array $referenceArray the main configuration array where to look up the data
|
||||
*
|
||||
* @return array|mixed|string
|
||||
*/
|
||||
protected function getValueFromReferenceArray(string $placeholder, array $referenceArray)
|
||||
{
|
||||
$pointer = trim($placeholder, '%');
|
||||
$parts = explode('.', $pointer);
|
||||
$referenceData = $referenceArray;
|
||||
foreach ($parts as $part) {
|
||||
if (isset($referenceData[$part])) {
|
||||
$referenceData = $referenceData[$part];
|
||||
} else {
|
||||
// return unsubstituted placeholder
|
||||
return $placeholder;
|
||||
}
|
||||
}
|
||||
if ($this->isPlaceholder($referenceData)) {
|
||||
$referenceData = $this->getValueFromReferenceArray($referenceData, $referenceArray);
|
||||
}
|
||||
return $referenceData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is a string and begins and ends with %...%
|
||||
*
|
||||
* @param mixed $value the probe to check for
|
||||
* @return bool
|
||||
*/
|
||||
protected function isPlaceholder($value): bool
|
||||
{
|
||||
return is_string($value) && strpos($value, '%') === 0 && substr($value, -1) === '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is a string and contains an env placeholder
|
||||
*
|
||||
* @param mixed $value the probe to check for
|
||||
* @return bool
|
||||
*/
|
||||
protected function isEnvPlaceholder($value): bool
|
||||
{
|
||||
return is_string($value) && (strpos($value, '%env(') !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as array_replace_recursive except that when in simple arrays (= YAML lists), the entries are
|
||||
* appended (array_merge)
|
||||
*
|
||||
* @param array $val1
|
||||
* @param array $val2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function merge(array $val1, array $val2): array
|
||||
{
|
||||
// Simple lists get merged / added up
|
||||
if (count(array_filter(array_keys($val1), 'is_int')) === count($val1)) {
|
||||
return array_merge($val1, $val2);
|
||||
}
|
||||
foreach ($val1 as $k => $v) {
|
||||
// The key also exists in second array, if it is a simple value
|
||||
// then $val2 will override the value, where an array is calling merge() recursively.
|
||||
if (isset($val2[$k])) {
|
||||
if (is_array($v) && isset($val2[$k])) {
|
||||
if (is_array($val2[$k])) {
|
||||
$val1[$k] = $this->merge($v, $val2[$k]);
|
||||
} else {
|
||||
$val1[$k] = $val2[$k];
|
||||
}
|
||||
} else {
|
||||
$val1[$k] = $val2[$k];
|
||||
}
|
||||
unset($val2[$k]);
|
||||
}
|
||||
}
|
||||
// If there are properties in the second array left, they are added up
|
||||
if (!empty($val2)) {
|
||||
foreach ($val2 as $k => $v) {
|
||||
$val1[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $val1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Configuration;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
|
||||
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Prepare richtext configuration. Used in DataHandler and FormEngine
|
||||
*
|
||||
* @internal Internal class for the time being - may change / vanish any time
|
||||
* @todo When I grow up, I want to become a data provider
|
||||
*/
|
||||
class Richtext
|
||||
{
|
||||
/**
|
||||
* This is an intermediate class / method to retrieve RTE
|
||||
* configuration until all core places use data providers to do that.
|
||||
*
|
||||
* @param string $table The table the field is in
|
||||
* @param string $field Field name
|
||||
* @param int $pid Real page id
|
||||
* @param string $recordType Record type value
|
||||
* @param array $tcaFieldConf ['config'] section of TCA field
|
||||
* @return array
|
||||
*/
|
||||
public function getConfiguration(string $table, string $field, int $pid, string $recordType, array $tcaFieldConf): array
|
||||
{
|
||||
// create instance of NodeFactory, ask for "text" element
|
||||
//
|
||||
// As soon an the Data handler starts using FormDataProviders, this class can vanish again, and the hack to
|
||||
// test for specific rich text instances can be dropped: Split the "TcaText" data provider into multiple parts, each
|
||||
// RTE should register and own data provider that does the transformation / configuration providing. This way,
|
||||
// the explicit check for different RTE classes is removed from core and "hooked in" by the RTE's.
|
||||
|
||||
// The main problem here is that all parameters that the processing needs is handed over to as TSconfig
|
||||
// "dotted array" syntax. We convert at least the processing information available under "processing"
|
||||
// together with pageTS, this way it can be overridden and understood in RteHtmlParser.
|
||||
// However, all other parts of the core will depend on the non-dotted syntax (coming from YAML directly)
|
||||
|
||||
$pageTs = $this->getPageTsConfiguration($table, $field, $pid, $recordType);
|
||||
|
||||
// determine which preset to use
|
||||
$usePreset = $pageTs['preset'] ?? $tcaFieldConf['richtextConfiguration'] ?? 'default';
|
||||
|
||||
// load configuration from preset
|
||||
$configuration = $this->loadConfigurationFromPreset($usePreset);
|
||||
|
||||
// overlay preset configuration with pageTs
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$configuration,
|
||||
$this->addFlattenedPageTsConfig($pageTs)
|
||||
);
|
||||
|
||||
// Handle "mode" / "transformation" config when overridden
|
||||
if (!isset($configuration['proc.']['mode']) && !isset($configuration['proc.']['overruleMode'])) {
|
||||
$configuration['proc.']['overruleMode'] = 'default';
|
||||
}
|
||||
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a configuration preset from an external resource (currently only YAML is supported).
|
||||
* This is the default behaviour and can be overridden by pageTSconfig.
|
||||
*
|
||||
* @param string $presetName
|
||||
* @return array the parsed configuration
|
||||
*/
|
||||
protected function loadConfigurationFromPreset(string $presetName = ''): array
|
||||
{
|
||||
$configuration = [];
|
||||
if (!empty($presetName) && isset($GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets'][$presetName])) {
|
||||
$fileLoader = GeneralUtility::makeInstance(YamlFileLoader::class);
|
||||
$configuration = $fileLoader->load($GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets'][$presetName]);
|
||||
// For future versions, you should however rely on the "processing" key and not the "proc" key.
|
||||
if (is_array($configuration['processing'])) {
|
||||
$configuration['proc.'] = $this->convertPlainArrayToTypoScriptArray($configuration['processing']);
|
||||
}
|
||||
}
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return RTE section of page TS
|
||||
*
|
||||
* @param int $pid Page ts of given pid
|
||||
* @return array RTE section of pageTs of given pid
|
||||
*/
|
||||
protected function getRtePageTsConfigOfPid(int $pid): array
|
||||
{
|
||||
return BackendUtility::getPagesTSconfig($pid)['RTE.'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with Typoscript the old way (with dot)
|
||||
* Since the functionality in YAML is without the dots, but the new configuration is used without the dots
|
||||
* this functionality adds also an explicit = 1 to the arrays
|
||||
*
|
||||
* @param array $plainArray An array
|
||||
* @return array array with TypoScript as usual (with dot)
|
||||
*/
|
||||
protected function convertPlainArrayToTypoScriptArray(array $plainArray)
|
||||
{
|
||||
$typoScriptArray = [];
|
||||
foreach ($plainArray as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
if (!isset($typoScriptArray[$key])) {
|
||||
$typoScriptArray[$key] = 1;
|
||||
}
|
||||
$typoScriptArray[$key . '.'] = $this->convertPlainArrayToTypoScriptArray($value);
|
||||
} else {
|
||||
$typoScriptArray[$key] = $value === null ? '' : $value;
|
||||
}
|
||||
}
|
||||
return $typoScriptArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all PageTS.RTE options keys to configuration without dots
|
||||
*
|
||||
* We need to keep the dotted keys for backwards compatibility like ext:rtehtmlarea
|
||||
*
|
||||
* @param array $typoScriptArray TypoScriptArray
|
||||
* @return array array with config without dots added
|
||||
*/
|
||||
protected function addFlattenedPageTsConfig(array $typoScriptArray): array
|
||||
{
|
||||
foreach ($typoScriptArray as $key => $data) {
|
||||
if (substr($key, -1) !== '.') {
|
||||
continue;
|
||||
}
|
||||
/** @var TypoScriptService $typoScriptService */
|
||||
$typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
|
||||
$typoScriptArray[substr($key, 0, -1)] = $typoScriptService->convertTypoScriptArrayToPlainArray($typoScriptArray[$key]);
|
||||
}
|
||||
|
||||
return $typoScriptArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load PageTS configuration for the RTE
|
||||
*
|
||||
* Return RTE section of page TS, taking into account overloading via table, field and record type
|
||||
*
|
||||
* @param string $table The table the field is in
|
||||
* @param string $field Field name
|
||||
* @param int $pid Real page id
|
||||
* @param string $recordType Record type value
|
||||
* @return array
|
||||
*/
|
||||
protected function getPageTsConfiguration(string $table, string $field, int $pid, string $recordType): array
|
||||
{
|
||||
// Load PageTSconfig configuration
|
||||
$fullPageTsConfig = $this->getRtePageTsConfigOfPid($pid);
|
||||
$defaultPageTsConfigOverrides = $fullPageTsConfig['default.'] ?? null;
|
||||
$fieldSpecificPageTsConfigOverrides = $fullPageTsConfig['config.'][$table . '.'][$field . '.'] ?? null;
|
||||
unset($fullPageTsConfig['default.'], $fullPageTsConfig['config.']);
|
||||
|
||||
// First use RTE.*
|
||||
$rtePageTsConfiguration = $fullPageTsConfig;
|
||||
|
||||
// Then overload with RTE.default.*
|
||||
if (is_array($defaultPageTsConfigOverrides)) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($rtePageTsConfiguration, $defaultPageTsConfigOverrides);
|
||||
}
|
||||
|
||||
// Then overload with RTE.config.tt_content.bodytext
|
||||
if (is_array($fieldSpecificPageTsConfigOverrides)) {
|
||||
$fieldSpecificPageTsConfigOverridesWithoutType = $fieldSpecificPageTsConfigOverrides;
|
||||
unset($fieldSpecificPageTsConfigOverridesWithoutType['types.']);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($rtePageTsConfiguration, $fieldSpecificPageTsConfigOverridesWithoutType);
|
||||
|
||||
// Then overload with RTE.config.tt_content.bodytext.types.textmedia
|
||||
if (
|
||||
$recordType
|
||||
&& isset($fieldSpecificPageTsConfigOverrides['types.'][$recordType . '.'])
|
||||
&& is_array($fieldSpecificPageTsConfigOverrides['types.'][$recordType . '.'])
|
||||
) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$rtePageTsConfiguration,
|
||||
$fieldSpecificPageTsConfigOverrides['types.'][$recordType . '.']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $rtePageTsConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Core\Configuration;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Responsibility: Handles the format of the configuration (currently yaml), and the location of the file system folder
|
||||
*
|
||||
* Reads all available site configuration options, and puts them into Site objects.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SiteConfiguration implements SingletonInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $configPath;
|
||||
|
||||
/**
|
||||
* Config yaml file name.
|
||||
*
|
||||
* @internal
|
||||
* @var string
|
||||
*/
|
||||
protected $configFileName = 'config.yaml';
|
||||
|
||||
/**
|
||||
* Identifier to store all configuration data in cache_core cache.
|
||||
*
|
||||
* @internal
|
||||
* @var string
|
||||
*/
|
||||
protected $cacheIdentifier = 'site-configuration';
|
||||
|
||||
/**
|
||||
* Cache stores all configuration as Site objects, as long as they haven't been changed.
|
||||
* This drastically improves performance as SiteFinder utilizes SiteConfiguration heavily
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $firstLevelCache;
|
||||
|
||||
/**
|
||||
* @param string $configPath
|
||||
*/
|
||||
public function __construct(string $configPath)
|
||||
{
|
||||
$this->configPath = $configPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all site objects which have been found in the filesystem.
|
||||
*
|
||||
* @param bool $useCache
|
||||
* @return Site[]
|
||||
*/
|
||||
public function getAllExistingSites(bool $useCache = true): array
|
||||
{
|
||||
if ($useCache && $this->firstLevelCache !== null) {
|
||||
return $this->firstLevelCache;
|
||||
}
|
||||
return $this->resolveAllExistingSites($useCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all site objects which have been found in the filesystem.
|
||||
*
|
||||
* @return Site[]
|
||||
*/
|
||||
public function resolveAllExistingSites(): array
|
||||
{
|
||||
$sites = [];
|
||||
$siteConfiguration = $this->getAllSiteConfigurationFromFiles();
|
||||
foreach ($siteConfiguration as $identifier => $configuration) {
|
||||
$rootPageId = (int)($configuration['rootPageId'] ?? 0);
|
||||
if ($rootPageId > 0) {
|
||||
$sites[$identifier] = GeneralUtility::makeInstance(Site::class, $identifier, $rootPageId, $configuration);
|
||||
}
|
||||
}
|
||||
$this->firstLevelCache = $sites;
|
||||
return $sites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the site configuration from config files.
|
||||
*
|
||||
* @return array
|
||||
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
|
||||
*/
|
||||
protected function getAllSiteConfigurationFromFiles(): array
|
||||
{
|
||||
// Check if the data is already cached
|
||||
if ($siteConfiguration = $this->getCache()->get($this->cacheIdentifier)) {
|
||||
// Due to the nature of PhpFrontend, the `<?php` and `#` wraps have to be removed
|
||||
$siteConfiguration = preg_replace('/^<\?php\s*|\s*#$/', '', $siteConfiguration);
|
||||
$siteConfiguration = json_decode($siteConfiguration, true);
|
||||
}
|
||||
|
||||
// Nothing in the cache (or no site found)
|
||||
if (empty($siteConfiguration)) {
|
||||
$finder = new Finder();
|
||||
try {
|
||||
$finder->files()->depth(0)->name($this->configFileName)->in($this->configPath . '/*');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Directory $this->configPath does not exist yet
|
||||
$finder = [];
|
||||
}
|
||||
$loader = GeneralUtility::makeInstance(YamlFileLoader::class);
|
||||
$siteConfiguration = [];
|
||||
foreach ($finder as $fileInfo) {
|
||||
$configuration = $loader->load(GeneralUtility::fixWindowsFilePath((string)$fileInfo));
|
||||
$identifier = basename($fileInfo->getPath());
|
||||
if (isset($configuration['site'])) {
|
||||
trigger_error(
|
||||
'Site configuration with key \'site\' has been deprecated, remove indentation level and site key.',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
$configuration = $configuration['site'];
|
||||
}
|
||||
$siteConfiguration[$identifier] = $configuration;
|
||||
}
|
||||
$this->getCache()->set($this->cacheIdentifier, json_encode($siteConfiguration));
|
||||
}
|
||||
return $siteConfiguration ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plain configuration
|
||||
* This method should only be used in case the original configuration as it exists in the file should be loaded,
|
||||
* for example for writing / editing configuration.
|
||||
*
|
||||
* All read related actions should be performed on the site entity.
|
||||
*
|
||||
* @param string $siteIdentifier
|
||||
* @return array
|
||||
*/
|
||||
public function load(string $siteIdentifier): array
|
||||
{
|
||||
$fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->configFileName;
|
||||
$loader = GeneralUtility::makeInstance(YamlFileLoader::class);
|
||||
return $loader->load(GeneralUtility::fixWindowsFilePath($fileName), YamlFileLoader::PROCESS_IMPORTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a site configuration
|
||||
*
|
||||
* @param string $siteIdentifier
|
||||
* @param array $configuration
|
||||
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
|
||||
*/
|
||||
public function write(string $siteIdentifier, array $configuration): void
|
||||
{
|
||||
$folder = $this->configPath . '/' . $siteIdentifier;
|
||||
$fileName = $folder . '/' . $this->configFileName;
|
||||
$newConfiguration = $configuration;
|
||||
if (!file_exists($folder)) {
|
||||
GeneralUtility::mkdir_deep($folder);
|
||||
} elseif (file_exists($fileName)) {
|
||||
$loader = GeneralUtility::makeInstance(YamlFileLoader::class);
|
||||
// load without any processing to have the unprocessed base to modify
|
||||
$newConfiguration = $loader->load(GeneralUtility::fixWindowsFilePath($fileName), 0);
|
||||
// load the processed configuration to diff changed values
|
||||
$processed = $loader->load(GeneralUtility::fixWindowsFilePath($fileName));
|
||||
// find properties that were modified via GUI
|
||||
$newModified = array_replace_recursive(
|
||||
self::findRemoved($processed, $configuration),
|
||||
self::findModified($processed, $configuration)
|
||||
);
|
||||
// change _only_ the modified keys, leave the original non-changed areas alone
|
||||
ArrayUtility::mergeRecursiveWithOverrule($newConfiguration, $newModified);
|
||||
}
|
||||
$newConfiguration = $this->sortConfiguration($newConfiguration);
|
||||
$yamlFileContents = Yaml::dump($newConfiguration, 99, 2);
|
||||
GeneralUtility::writeFile($fileName, $yamlFileContents);
|
||||
$this->firstLevelCache = null;
|
||||
$this->getCache()->remove($this->cacheIdentifier);
|
||||
$this->getCache()->remove('pseudo-sites');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a site identifier (and moves the folder)
|
||||
*
|
||||
* @param string $currentIdentifier
|
||||
* @param string $newIdentifier
|
||||
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
|
||||
*/
|
||||
public function rename(string $currentIdentifier, string $newIdentifier): void
|
||||
{
|
||||
$result = rename($this->configPath . '/' . $currentIdentifier, $this->configPath . '/' . $newIdentifier);
|
||||
if (!$result) {
|
||||
throw new \RuntimeException('Unable to rename folder sites/' . $currentIdentifier, 1522491300);
|
||||
}
|
||||
$this->getCache()->remove($this->cacheIdentifier);
|
||||
$this->firstLevelCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the config.yaml file of a site configuration.
|
||||
* Also clears the cache.
|
||||
*
|
||||
* @param string $siteIdentifier
|
||||
* @throws SiteNotFoundException
|
||||
*/
|
||||
public function delete(string $siteIdentifier): void
|
||||
{
|
||||
$sites = $this->getAllExistingSites();
|
||||
if (!isset($sites[$siteIdentifier])) {
|
||||
throw new SiteNotFoundException('Site configuration named ' . $siteIdentifier . ' not found.', 1522866183);
|
||||
}
|
||||
$fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->configFileName;
|
||||
if (!file_exists($fileName)) {
|
||||
throw new SiteNotFoundException('Site configuration file ' . $this->configFileName . ' within the site ' . $siteIdentifier . ' not found.', 1522866184);
|
||||
}
|
||||
@unlink($fileName);
|
||||
$this->getCache()->remove($this->cacheIdentifier);
|
||||
$this->getCache()->remove('pseudo-sites');
|
||||
$this->firstLevelCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Short-hand function for the cache
|
||||
*
|
||||
* @return FrontendInterface
|
||||
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
|
||||
*/
|
||||
protected function getCache(): FrontendInterface
|
||||
{
|
||||
return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_core');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $newConfiguration
|
||||
* @return array
|
||||
*/
|
||||
protected function sortConfiguration(array $newConfiguration): array
|
||||
{
|
||||
ksort($newConfiguration);
|
||||
if (isset($newConfiguration['imports'])) {
|
||||
$imports = $newConfiguration['imports'];
|
||||
unset($newConfiguration['imports']);
|
||||
$newConfiguration['imports'] = $imports;
|
||||
}
|
||||
return $newConfiguration;
|
||||
}
|
||||
|
||||
protected static function findModified(array $currentConfiguration, array $newConfiguration): array
|
||||
{
|
||||
$differences = [];
|
||||
foreach ($newConfiguration as $key => $value) {
|
||||
if (!isset($currentConfiguration[$key]) || $currentConfiguration[$key] !== $newConfiguration[$key]) {
|
||||
if (!isset($newConfiguration[$key]) && isset($currentConfiguration[$key])) {
|
||||
$differences[$key] = '__UNSET';
|
||||
} elseif (isset($currentConfiguration[$key])
|
||||
&& is_array($newConfiguration[$key])
|
||||
&& is_array($currentConfiguration[$key])
|
||||
) {
|
||||
$differences[$key] = self::findModified($currentConfiguration[$key], $newConfiguration[$key]);
|
||||
} else {
|
||||
$differences[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $differences;
|
||||
}
|
||||
|
||||
protected static function findRemoved(array $currentConfiguration, array $newConfiguration): array
|
||||
{
|
||||
$removed = [];
|
||||
foreach ($currentConfiguration as $key => $value) {
|
||||
if (!isset($newConfiguration[$key])) {
|
||||
$removed[$key] = '__UNSET';
|
||||
} elseif (isset($currentConfiguration[$key]) && is_array($currentConfiguration[$key]) && is_array($newConfiguration[$key])) {
|
||||
$removedInRecursion = self::findRemoved($currentConfiguration[$key], $newConfiguration[$key]);
|
||||
if (!empty($removedInRecursion)) {
|
||||
$removed[$key] = $removedInRecursion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $removed;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract class to define own custom TypoScript conditions.
|
||||
*
|
||||
* Used with the TypoScript parser.
|
||||
*/
|
||||
abstract class AbstractCondition
|
||||
{
|
||||
/**
|
||||
* @var \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher
|
||||
*/
|
||||
protected $conditionMatcherInstance;
|
||||
|
||||
/**
|
||||
* @return \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher
|
||||
*/
|
||||
protected function getConditionMatcherInstance()
|
||||
{
|
||||
return $this->conditionMatcherInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher $conditionMatcherInstance
|
||||
*/
|
||||
public function setConditionMatcherInstance($conditionMatcherInstance)
|
||||
{
|
||||
$this->conditionMatcherInstance = $conditionMatcherInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method handling the evaluation.
|
||||
* Any given parameters given within the condition in form of
|
||||
* [ACME\MyPackageName\MyCondition = value1, = value2]
|
||||
* will be given as parameter in form of a numeric array, each entry
|
||||
* containing the strings that are split by the commas
|
||||
* e.g. array('= value1', '= value2')
|
||||
*
|
||||
* @param array $conditionParameters
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function matchCondition(array $conditionParameters);
|
||||
}
|
||||
+787
@@ -0,0 +1,787 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\ExpressionLanguage\SyntaxError;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Configuration\TypoScript\Exception\InvalidTypoScriptConditionException;
|
||||
use TYPO3\CMS\Core\Error\Exception;
|
||||
use TYPO3\CMS\Core\Exception\MissingTsfeException;
|
||||
use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
|
||||
use TYPO3\CMS\Core\Log\LogLevel;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
|
||||
|
||||
/**
|
||||
* Matching TypoScript conditions
|
||||
*
|
||||
* Used with the TypoScript parser.
|
||||
* Matches IPnumbers etc. for use with templates
|
||||
*/
|
||||
abstract class AbstractConditionMatcher implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* Id of the current page.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $pageId;
|
||||
|
||||
/**
|
||||
* The rootline for the current page.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rootline;
|
||||
|
||||
/**
|
||||
* Whether to simulate the behaviour and match all conditions
|
||||
* (used in TypoScript object browser).
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $simulateMatchResult = false;
|
||||
|
||||
/**
|
||||
* Whether to simulat the behaviour and match specific conditions
|
||||
* (used in TypoScript object browser).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $simulateMatchConditions = [];
|
||||
|
||||
/**
|
||||
* @var Resolver
|
||||
*/
|
||||
protected $expressionLanguageResolver;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $expressionLanguageResolverVariables = [];
|
||||
|
||||
protected function initializeExpressionLanguageResolver(): void
|
||||
{
|
||||
$this->updateExpressionLanguageVariables();
|
||||
$this->expressionLanguageResolver = GeneralUtility::makeInstance(
|
||||
Resolver::class,
|
||||
'typoscript',
|
||||
$this->expressionLanguageResolverVariables
|
||||
);
|
||||
}
|
||||
|
||||
protected function updateExpressionLanguageVariables(): void
|
||||
{
|
||||
// deliberately empty and not "abstract" due to backwards compatibility
|
||||
// implement this method in derived classes
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function strictSyntaxEnabled(): bool
|
||||
{
|
||||
return GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('TypoScript.strictSyntax');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id of the page to evaluate conditions for.
|
||||
*
|
||||
* @param int $pageId Id of the page (must be positive)
|
||||
*/
|
||||
public function setPageId($pageId)
|
||||
{
|
||||
if (is_int($pageId) && $pageId > 0) {
|
||||
$this->pageId = $pageId;
|
||||
}
|
||||
$this->initializeExpressionLanguageResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the id of the page to evaluate conditions for.
|
||||
*
|
||||
* @return int Id of the page
|
||||
*/
|
||||
public function getPageId()
|
||||
{
|
||||
return $this->pageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rootline.
|
||||
*
|
||||
* @param array $rootline The rootline to be used for matching (must have elements)
|
||||
*/
|
||||
public function setRootline(array $rootline)
|
||||
{
|
||||
if (!empty($rootline)) {
|
||||
$this->rootline = $rootline;
|
||||
}
|
||||
$this->initializeExpressionLanguageResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rootline.
|
||||
*
|
||||
* @return array The rootline to be used for matching
|
||||
*/
|
||||
public function getRootline()
|
||||
{
|
||||
return $this->rootline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to simulate the behaviour and match all conditions.
|
||||
*
|
||||
* @param bool $simulateMatchResult Whether to simulate positive matches
|
||||
*/
|
||||
public function setSimulateMatchResult($simulateMatchResult)
|
||||
{
|
||||
if (is_bool($simulateMatchResult)) {
|
||||
$this->simulateMatchResult = $simulateMatchResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to simulate the behaviour and match specific conditions.
|
||||
*
|
||||
* @param array $simulateMatchConditions Conditions to simulate a match for
|
||||
*/
|
||||
public function setSimulateMatchConditions(array $simulateMatchConditions)
|
||||
{
|
||||
$this->simulateMatchConditions = $simulateMatchConditions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an expression
|
||||
* + OR normalization: "...]OR[...", "...]||[...", "...][..." --> "...]||[..."
|
||||
* + AND normalization: "...]AND[...", "...]&&[..." --> "...]&&[..."
|
||||
*
|
||||
* @param string $expression The expression to be normalized (e.g. "[A] && [B] OR [C]")
|
||||
* @return string The normalized expression (e.g. "[A]&&[B]||[C]")
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function normalizeExpression($expression)
|
||||
{
|
||||
$removeSpaces = '/
|
||||
\\s*
|
||||
( # subroutine 1
|
||||
\\[
|
||||
(?:
|
||||
[^\\[\\]] # any character except []
|
||||
| (?1) # recursive subroutine 1 when brackets are around
|
||||
)*
|
||||
\\]
|
||||
)
|
||||
\\s*
|
||||
/xi';
|
||||
|
||||
$adjacentBrackets = '/
|
||||
( # subroutine 1
|
||||
\\[
|
||||
(?:
|
||||
[^\\[\\]] # any character except []
|
||||
| (?1) # recursive subroutine 1 when brackets are around
|
||||
)*
|
||||
\\]
|
||||
)
|
||||
(*SKIP) # avoid backtracking into completed bracket expressions
|
||||
\\[ # match the following [
|
||||
/xi';
|
||||
|
||||
return preg_replace(
|
||||
[
|
||||
$removeSpaces,
|
||||
'/\\]AND\\[/i',
|
||||
'/\\]OR\\[/i',
|
||||
$adjacentBrackets
|
||||
],
|
||||
[
|
||||
'\\1',
|
||||
']&&[',
|
||||
']||[',
|
||||
'\\1||['
|
||||
],
|
||||
trim($expression)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a TypoScript condition expression.
|
||||
*
|
||||
* @param string $expression The expression to match
|
||||
* @return bool Whether the expression matched
|
||||
*/
|
||||
public function match($expression)
|
||||
{
|
||||
// Return directly if result should be simulated:
|
||||
if ($this->simulateMatchResult) {
|
||||
return $this->simulateMatchResult;
|
||||
}
|
||||
// Return directly if matching for specific condition is simulated only:
|
||||
if (!empty($this->simulateMatchConditions)) {
|
||||
return in_array($expression, $this->simulateMatchConditions);
|
||||
}
|
||||
// Sets the current pageId if not defined yet:
|
||||
if (!isset($this->pageId)) {
|
||||
$this->pageId = $this->determinePageId();
|
||||
}
|
||||
// Sets the rootline if not defined yet:
|
||||
if (!isset($this->rootline)) {
|
||||
$this->rootline = $this->determineRootline();
|
||||
}
|
||||
$result = false;
|
||||
$normalizedExpression = $this->normalizeExpression($expression);
|
||||
// First and last character must be square brackets (e.g. "[A]&&[B]":
|
||||
if ($normalizedExpression[0] === '[' && substr($normalizedExpression, -1) === ']') {
|
||||
$innerExpression = substr($normalizedExpression, 1, -1);
|
||||
$orParts = explode(']||[', $innerExpression);
|
||||
if ($this->strictSyntaxEnabled() && count($orParts) > 1) {
|
||||
trigger_error('Multiple conditions blocks combined with AND, OR, && or || will be removed in TYPO3 v10.0, use the new expression language.', E_USER_DEPRECATED);
|
||||
}
|
||||
foreach ($orParts as $orPart) {
|
||||
$andParts = explode(']&&[', $orPart);
|
||||
if ($this->strictSyntaxEnabled() && count($andParts) > 1) {
|
||||
trigger_error('Multiple conditions blocks combined with AND, OR, && or || will be removed in TYPO3 v10.0, use the new expression language.', E_USER_DEPRECATED);
|
||||
}
|
||||
foreach ($andParts as $andPart) {
|
||||
$result = $this->evaluateExpression($andPart);
|
||||
if (!is_bool($result)) {
|
||||
$result = $this->evaluateCondition($andPart);
|
||||
}
|
||||
// If condition in AND context fails, the whole block is FALSE:
|
||||
if ($result === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If condition in OR context succeeds, the whole expression is TRUE:
|
||||
if ($result === true) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $expression
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function evaluateExpression(string $expression): ?bool
|
||||
{
|
||||
try {
|
||||
$result = $this->expressionLanguageResolver->evaluate($expression);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
} catch (MissingTsfeException $e) {
|
||||
// TSFE is not available in the current context (e.g. TSFE in BE context),
|
||||
// we set all conditions false for this case.
|
||||
return false;
|
||||
} catch (SyntaxError $exception) {
|
||||
// SyntaxException means no support, let's try the fallback
|
||||
$message = 'Expression could not be parsed, fallback kicks in.';
|
||||
if (strpos($exception->getMessage(), 'Unexpected character "="') !== false) {
|
||||
$message .= ' It looks like an old condition with only one equal sign.';
|
||||
}
|
||||
$this->logger->log(
|
||||
$this->strictSyntaxEnabled() ? LogLevel::WARNING : LogLevel::INFO,
|
||||
$message,
|
||||
['expression' => $expression]
|
||||
);
|
||||
} catch (\Throwable $exception) {
|
||||
// The following error handling is required to mitigate a missing type check
|
||||
// in the Symfony Expression Language handling. In case a condition
|
||||
// use "in" or "not in" check in combination with a non array a PHP Warning
|
||||
// is thrown. Example: [1 in "foo"] or ["bar" in "foo,baz"]
|
||||
// This conditions are wrong for sure, but they will break the complete installation
|
||||
// including the backend. To mitigate the problem we do the following:
|
||||
// 1) In FE an InvalidTypoScriptConditionException is thrown (if strictSyntax is enabled)
|
||||
// 2) In FE silent catch this error and log it (if strictSyntax is disabled)
|
||||
// 3) In BE silent catch this error and log it, but never break the backend.
|
||||
$this->logger->error($exception->getMessage(), [
|
||||
'expression' => $expression,
|
||||
'exception' => $exception
|
||||
]);
|
||||
if (TYPO3_MODE === 'FE'
|
||||
&& $exception instanceof Exception
|
||||
&& $this->strictSyntaxEnabled()
|
||||
&& strpos($exception->getMessage(), 'in_array() expects parameter 2 to be array') !== false
|
||||
) {
|
||||
throw new InvalidTypoScriptConditionException('Invalid expression in condition: [' . $expression . ']', 1536950931);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a TypoScript condition given as input, eg. "[applicationContext = Production][...(other condition)...]"
|
||||
*
|
||||
* @param string $key The condition to match against its criteria.
|
||||
* @param string $value
|
||||
* @return bool|null Result of the evaluation; NULL if condition could not be evaluated
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function evaluateConditionCommon($key, $value)
|
||||
{
|
||||
$keyParts = GeneralUtility::trimExplode('|', $key);
|
||||
switch ($keyParts[0]) {
|
||||
case 'applicationContext':
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
$currentApplicationContext = GeneralUtility::getApplicationContext();
|
||||
foreach ($values as $applicationContext) {
|
||||
if ($this->searchStringWildcard($currentApplicationContext, $applicationContext)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'language':
|
||||
if (GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE') === $value) {
|
||||
return true;
|
||||
}
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
foreach ($values as $test) {
|
||||
// matches a string with asterix in front and back. See https://docs.typo3.org/typo3cms/TyposcriptReference/Conditions/Reference.html#language for use case.
|
||||
if (preg_match('/^\\*.+\\*$/', $test)) {
|
||||
$allLanguages = preg_split('/[,;]/', GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE'));
|
||||
if (in_array(substr($test, 1, -1), $allLanguages)) {
|
||||
return true;
|
||||
}
|
||||
} elseif (GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE') == $test) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'IP':
|
||||
if ($value === 'devIP') {
|
||||
$value = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']);
|
||||
}
|
||||
|
||||
return (bool)GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $value);
|
||||
case 'hostname':
|
||||
return (bool)GeneralUtility::cmpFQDN(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $value);
|
||||
case 'hour':
|
||||
case 'minute':
|
||||
case 'month':
|
||||
case 'year':
|
||||
case 'dayofweek':
|
||||
case 'dayofmonth':
|
||||
case 'dayofyear':
|
||||
// In order to simulate time properly in templates.
|
||||
$theEvalTime = $GLOBALS['SIM_EXEC_TIME'];
|
||||
switch ($key) {
|
||||
case 'hour':
|
||||
$theTestValue = date('H', $theEvalTime);
|
||||
break;
|
||||
case 'minute':
|
||||
$theTestValue = date('i', $theEvalTime);
|
||||
break;
|
||||
case 'month':
|
||||
$theTestValue = date('m', $theEvalTime);
|
||||
break;
|
||||
case 'year':
|
||||
$theTestValue = date('Y', $theEvalTime);
|
||||
break;
|
||||
case 'dayofweek':
|
||||
$theTestValue = date('w', $theEvalTime);
|
||||
break;
|
||||
case 'dayofmonth':
|
||||
$theTestValue = date('d', $theEvalTime);
|
||||
break;
|
||||
case 'dayofyear':
|
||||
$theTestValue = date('z', $theEvalTime);
|
||||
break;
|
||||
default:
|
||||
$theTestValue = 0;
|
||||
break;
|
||||
}
|
||||
$theTestValue = (int)$theTestValue;
|
||||
// comp
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
foreach ($values as $test) {
|
||||
if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($test)) {
|
||||
$test = '=' . $test;
|
||||
}
|
||||
if ($this->compareNumber($test, $theTestValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'compatVersion':
|
||||
return VersionNumberUtility::convertVersionNumberToInteger(TYPO3_branch) >= VersionNumberUtility::convertVersionNumberToInteger($value);
|
||||
case 'loginUser':
|
||||
if ($this->isUserLoggedIn()) {
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
foreach ($values as $test) {
|
||||
if ($test === '*' || (string)$this->getUserId() === (string)$test) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} elseif ($value === '') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case 'page':
|
||||
if ($keyParts[1]) {
|
||||
$page = $this->getPage();
|
||||
$property = $keyParts[1];
|
||||
if (!empty($page) && isset($page[$property]) && (string)$page[$property] === (string)$value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'globalVar':
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
foreach ($values as $test) {
|
||||
$point = strcspn($test, '!=<>');
|
||||
$theVarName = substr($test, 0, $point);
|
||||
$nv = $this->getVariable(trim($theVarName));
|
||||
$testValue = substr($test, $point);
|
||||
if ($this->compareNumber($testValue, $nv)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'globalString':
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
foreach ($values as $test) {
|
||||
$point = strcspn($test, '=');
|
||||
$theVarName = substr($test, 0, $point);
|
||||
$nv = (string)$this->getVariable(trim($theVarName));
|
||||
$testValue = substr($test, $point + 1);
|
||||
if ($this->searchStringWildcard($nv, trim($testValue))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'userFunc':
|
||||
$matches = [];
|
||||
preg_match_all('/^\s*([^\(\s]+)\s*(?:\((.*)\))?\s*$/', $value, $matches);
|
||||
$funcName = $matches[1][0];
|
||||
$funcValues = trim($matches[2][0]) !== '' ? $this->parseUserFuncArguments($matches[2][0]) : [];
|
||||
if (is_callable($funcName) && call_user_func_array($funcName, $funcValues)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a TypoScript condition given as input with a custom class name,
|
||||
* e.g. "[MyCompany\MyPackage\ConditionMatcher\MyOwnConditionMatcher = myvalue]"
|
||||
*
|
||||
* @param string $condition The condition to match
|
||||
* @return bool|null Result of the evaluation; NULL if condition could not be evaluated
|
||||
* @throws \TYPO3\CMS\Core\Configuration\TypoScript\Exception\InvalidTypoScriptConditionException
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function evaluateCustomDefinedCondition($condition)
|
||||
{
|
||||
$conditionResult = null;
|
||||
|
||||
list($conditionClassName, $conditionParameters) = GeneralUtility::trimExplode(' ', $condition, false, 2);
|
||||
|
||||
// Check if the condition class name is a valid class
|
||||
// This is necessary to not stop here for the conditions ELSE and GLOBAL
|
||||
if (class_exists($conditionClassName)) {
|
||||
// Use like this: [MyCompany\MyPackage\ConditionMatcher\MyOwnConditionMatcher = myvalue]
|
||||
/** @var \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractCondition $conditionObject */
|
||||
$conditionObject = GeneralUtility::makeInstance($conditionClassName);
|
||||
if (($conditionObject instanceof \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractCondition) === false) {
|
||||
throw new \TYPO3\CMS\Core\Configuration\TypoScript\Exception\InvalidTypoScriptConditionException(
|
||||
'"' . $conditionClassName . '" is not a valid TypoScript Condition object.',
|
||||
1410286153
|
||||
);
|
||||
}
|
||||
|
||||
$conditionParameters = $this->parseUserFuncArguments($conditionParameters);
|
||||
$conditionObject->setConditionMatcherInstance($this);
|
||||
$conditionResult = $conditionObject->matchCondition($conditionParameters);
|
||||
}
|
||||
|
||||
return $conditionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses arguments to the userFunc.
|
||||
*
|
||||
* @param string $arguments
|
||||
* @return array
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function parseUserFuncArguments($arguments)
|
||||
{
|
||||
$result = [];
|
||||
$arguments = trim($arguments);
|
||||
while ($arguments !== '') {
|
||||
if ($arguments[0] === ',') {
|
||||
$result[] = '';
|
||||
$arguments = substr($arguments, 1);
|
||||
} else {
|
||||
$pos = strcspn($arguments, ',\'"');
|
||||
if ($pos == 0) {
|
||||
// We hit a quote of some kind
|
||||
$quote = $arguments[0];
|
||||
$segment = preg_replace('/^(.*?[^\\\])' . $quote . '.*$/', '\1', substr($arguments, 1));
|
||||
$segment = str_replace('\\' . $quote, $quote, $segment);
|
||||
$result[] = $segment;
|
||||
// shorten $arguments
|
||||
$arguments = substr($arguments, strlen($segment) + 2);
|
||||
$offset = strpos($arguments, ',');
|
||||
if ($offset === false) {
|
||||
$offset = strlen($arguments);
|
||||
}
|
||||
$arguments = substr($arguments, $offset + 1);
|
||||
} else {
|
||||
$result[] = trim(substr($arguments, 0, $pos));
|
||||
$arguments = substr($arguments, $pos + 1);
|
||||
}
|
||||
}
|
||||
$arguments = trim($arguments);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get variable common
|
||||
*
|
||||
* @param array $vars
|
||||
* @return mixed Whatever value. If none, then NULL.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function getVariableCommon(array $vars)
|
||||
{
|
||||
$value = null;
|
||||
$namespace = trim($vars[0]);
|
||||
if (count($vars) === 1) {
|
||||
$value = $this->getGlobal($vars[0]);
|
||||
} elseif ($namespace === 'LIT') {
|
||||
$value = trim($vars[1]);
|
||||
} else {
|
||||
$splitAgain = explode('|', $vars[1], 2);
|
||||
$k = trim($splitAgain[0]);
|
||||
if ($k) {
|
||||
switch ($namespace) {
|
||||
case 'GP':
|
||||
$value = GeneralUtility::_GP($k);
|
||||
break;
|
||||
case 'GPmerged':
|
||||
$value = GeneralUtility::_GPmerged($k);
|
||||
break;
|
||||
case 'ENV':
|
||||
$value = getenv($k);
|
||||
break;
|
||||
case 'IENV':
|
||||
$value = GeneralUtility::getIndpEnv($k);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
// If array:
|
||||
if (count($splitAgain) > 1) {
|
||||
if (is_array($value) && trim($splitAgain[1]) !== '') {
|
||||
$value = $this->getGlobal($splitAgain[1], $value);
|
||||
} else {
|
||||
$value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a $leftValue based on an operator: "<", ">", "<=", ">=", "!=" or "="
|
||||
*
|
||||
* @param string $test The value to compare with on the form [operator][number]. Eg. "< 123
|
||||
* @param float $leftValue The value on the left side
|
||||
* @return bool If $value is "50" and $test is "< 123" then it will return TRUE.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function compareNumber($test, $leftValue)
|
||||
{
|
||||
if (preg_match('/^(!?=+|<=?|>=?)\\s*([^\\s]*)\\s*$/', $test, $matches)) {
|
||||
$operator = $matches[1];
|
||||
$rightValue = $matches[2];
|
||||
switch ($operator) {
|
||||
case '>=':
|
||||
return $leftValue >= (float)$rightValue;
|
||||
break;
|
||||
case '<=':
|
||||
return $leftValue <= (float)$rightValue;
|
||||
break;
|
||||
case '!=':
|
||||
// multiple values may be split with '|'
|
||||
// see if none matches ("not in list")
|
||||
$found = false;
|
||||
$rightValueParts = GeneralUtility::trimExplode('|', $rightValue);
|
||||
foreach ($rightValueParts as $rightValueSingle) {
|
||||
if ($leftValue == (float)$rightValueSingle) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $found === false;
|
||||
break;
|
||||
case '<':
|
||||
return $leftValue < (float)$rightValue;
|
||||
break;
|
||||
case '>':
|
||||
return $leftValue > (float)$rightValue;
|
||||
break;
|
||||
default:
|
||||
// nothing valid found except '=', use '='
|
||||
// multiple values may be split with '|'
|
||||
// see if one matches ("in list")
|
||||
$found = false;
|
||||
$rightValueParts = GeneralUtility::trimExplode('|', $rightValue);
|
||||
foreach ($rightValueParts as $rightValueSingle) {
|
||||
if ($leftValue == $rightValueSingle) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matching two strings against each other, supporting a "*" wildcard or (if wrapped in "/") PCRE regular expressions
|
||||
*
|
||||
* @param string $haystack The string in which to find $needle.
|
||||
* @param string $needle The string to find in $haystack
|
||||
* @return bool Returns TRUE if $needle matches or is found in (according to wildcards) in $haystack. Eg. if $haystack is "Netscape 6.5" and $needle is "Net*" or "Net*ape" then it returns TRUE.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function searchStringWildcard($haystack, $needle)
|
||||
{
|
||||
return StringUtility::searchStringWildcard($haystack, $needle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return global variable where the input string $var defines array keys separated by "|"
|
||||
* Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value
|
||||
*
|
||||
* @param string $var Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back.
|
||||
* @param array $source Alternative array than $GLOBAL to get variables from.
|
||||
* @return mixed Whatever value. If none, then blank string.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function getGlobal($var, $source = null)
|
||||
{
|
||||
$vars = explode('|', $var);
|
||||
$c = count($vars);
|
||||
$k = trim($vars[0]);
|
||||
$theVar = isset($source) ? ($source[$k] ?? null) : ($GLOBALS[$k] ?? null);
|
||||
for ($a = 1; $a < $c; $a++) {
|
||||
if (!isset($theVar)) {
|
||||
break;
|
||||
}
|
||||
$key = trim($vars[$a]);
|
||||
if (is_object($theVar)) {
|
||||
$theVar = $theVar->{$key};
|
||||
} elseif (is_array($theVar)) {
|
||||
$theVar = $theVar[$key];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
if (!is_array($theVar) && !is_object($theVar)) {
|
||||
return $theVar;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a TypoScript condition given as input, eg. "[browser=net][...(other conditions)...]"
|
||||
*
|
||||
* @param string $string The condition to match against its criteria.
|
||||
* @return bool Whether the condition matched
|
||||
* @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::parse()
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
abstract protected function evaluateCondition($string);
|
||||
|
||||
/**
|
||||
* Gets the value of a variable.
|
||||
*
|
||||
* Examples of names:
|
||||
* + TSFE:id
|
||||
* + GP:firstLevel|secondLevel
|
||||
* + _GET|firstLevel|secondLevel
|
||||
* + LIT:someLiteralValue
|
||||
*
|
||||
* @param string $name The name of the variable to fetch the value from
|
||||
* @return mixed The value of the given variable (string) or NULL if variable did not exist
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
abstract protected function getVariable($name);
|
||||
|
||||
/**
|
||||
* Gets the usergroup list of the current user.
|
||||
*
|
||||
* @return string The usergroup list of the current user
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
abstract protected function getGroupList();
|
||||
|
||||
/**
|
||||
* Determines the current page Id.
|
||||
*
|
||||
* @return int The current page Id
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
abstract protected function determinePageId();
|
||||
|
||||
/**
|
||||
* Gets the properties for the current page.
|
||||
*
|
||||
* @return array The properties for the current page.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
abstract protected function getPage();
|
||||
|
||||
/**
|
||||
* Determines the rootline for the current page.
|
||||
*
|
||||
* @return array The rootline for the current page.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
abstract protected function determineRootline();
|
||||
|
||||
/**
|
||||
* Gets the id of the current user.
|
||||
*
|
||||
* @return int The id of the current user
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
abstract protected function getUserId();
|
||||
|
||||
/**
|
||||
* Determines if a user is logged in.
|
||||
*
|
||||
* @return bool Determines if a user is logged in
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
abstract protected function isUserLoggedIn();
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Configuration\TypoScript\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A "Your TypoScript condition is invalid" exception
|
||||
* used when a TypoScript condition is called but not extending
|
||||
* from the AbstractCondition class.
|
||||
*/
|
||||
class InvalidTypoScriptConditionException extends \TYPO3\CMS\Core\Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\DateTimeAspect;
|
||||
use TYPO3\CMS\Core\Context\UserAspect;
|
||||
use TYPO3\CMS\Core\Context\VisibilityAspect;
|
||||
use TYPO3\CMS\Core\Context\WorkspaceAspect;
|
||||
use TYPO3\CMS\Core\Core\ApplicationInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Entry point for the TYPO3 Command Line for Commands
|
||||
* Does not run the RequestHandler as this already runs an Application inside an Application which
|
||||
* is just way too much logic around simple CLI calls
|
||||
*/
|
||||
class CommandApplication implements ApplicationInterface
|
||||
{
|
||||
/**
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->checkEnvironmentOrDie();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the Symfony Console application in this TYPO3 application
|
||||
*
|
||||
* @param callable $execute
|
||||
*/
|
||||
public function run(callable $execute = null)
|
||||
{
|
||||
$this->initializeContext();
|
||||
$handler = GeneralUtility::makeInstance(CommandRequestHandler::class);
|
||||
$handler->handleRequest(new ArgvInput());
|
||||
|
||||
if ($execute !== null) {
|
||||
call_user_func($execute);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the script is called from a cli environment.
|
||||
*/
|
||||
protected function checkEnvironmentOrDie()
|
||||
{
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
die('Not called from a command line interface (e.g. a shell or scheduler).' . LF);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Context used for accessing data and finding out the current state of the application
|
||||
* Will be moved to a DI-like concept once introduced, for now, this is a singleton
|
||||
*/
|
||||
protected function initializeContext()
|
||||
{
|
||||
GeneralUtility::makeInstance(Context::class, [
|
||||
'date' => new DateTimeAspect(new \DateTimeImmutable('@' . $GLOBALS['EXEC_TIME'])),
|
||||
'visibility' => new VisibilityAspect(true, true),
|
||||
'workspace' => new WorkspaceAspect(0),
|
||||
'backend.user' => new UserAspect(null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a command is registered with a name
|
||||
* that is already taken
|
||||
*/
|
||||
class CommandNameAlreadyInUseException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Registry for Symfony commands, populated from extensions
|
||||
*/
|
||||
class CommandRegistry implements \IteratorAggregate, SingletonInterface
|
||||
{
|
||||
/**
|
||||
* @var PackageManager
|
||||
*/
|
||||
protected $packageManager;
|
||||
|
||||
/**
|
||||
* Map of commands
|
||||
*
|
||||
* @var Command[]
|
||||
*/
|
||||
protected $commands = [];
|
||||
|
||||
/**
|
||||
* Map of command configurations with the command name as key
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
protected $commandConfigurations = [];
|
||||
|
||||
/**
|
||||
* @param PackageManager $packageManager
|
||||
*/
|
||||
public function __construct(PackageManager $packageManager = null)
|
||||
{
|
||||
$this->packageManager = $packageManager ?: GeneralUtility::makeInstance(PackageManager::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator
|
||||
*/
|
||||
public function getIterator(): \Generator
|
||||
{
|
||||
$this->populateCommandsFromPackages();
|
||||
foreach ($this->commands as $commandName => $command) {
|
||||
yield $commandName => $command;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all commands which are allowed for scheduling recurring commands.
|
||||
*
|
||||
* @return \Generator
|
||||
*/
|
||||
public function getSchedulableCommands(): \Generator
|
||||
{
|
||||
$this->populateCommandsFromPackages();
|
||||
foreach ($this->commands as $commandName => $command) {
|
||||
if ($this->commandConfigurations[$commandName]['schedulable'] ?? true) {
|
||||
yield $commandName => $command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $identifier
|
||||
* @throws CommandNameAlreadyInUseException
|
||||
* @throws UnknownCommandException
|
||||
* @return Command|null
|
||||
*/
|
||||
public function getCommandByIdentifier(string $identifier): ?Command
|
||||
{
|
||||
$this->populateCommandsFromPackages();
|
||||
|
||||
if (!isset($this->commands[$identifier])) {
|
||||
throw new UnknownCommandException(
|
||||
sprintf('Command "%s" has not been registered.', $identifier),
|
||||
1510906768
|
||||
);
|
||||
}
|
||||
|
||||
return $this->commands[$identifier] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all Configuration/Commands.php files of extensions and create a registry from it.
|
||||
* The file should return an array with a command key as key and the command description
|
||||
* as value. The command description must be an array and have a class key that defines
|
||||
* the class name of the command. Example:
|
||||
*
|
||||
* <?php
|
||||
* return [
|
||||
* 'backend:lock' => [
|
||||
* 'class' => \TYPO3\CMS\Backend\Command\LockBackendCommand::class
|
||||
* ],
|
||||
* ];
|
||||
*
|
||||
* @throws CommandNameAlreadyInUseException
|
||||
*/
|
||||
protected function populateCommandsFromPackages()
|
||||
{
|
||||
if ($this->commands) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->packageManager->getActivePackages() as $package) {
|
||||
$commandsOfExtension = $package->getPackagePath() . 'Configuration/Commands.php';
|
||||
if (@is_file($commandsOfExtension)) {
|
||||
/*
|
||||
* We use require instead of require_once here because it eases the testability as require_once returns
|
||||
* a boolean from the second execution on. As this class is a singleton, this require is only called
|
||||
* once per request anyway.
|
||||
*/
|
||||
$commands = require $commandsOfExtension;
|
||||
if (is_array($commands)) {
|
||||
foreach ($commands as $commandName => $commandConfig) {
|
||||
if (array_key_exists($commandName, $this->commands)) {
|
||||
throw new CommandNameAlreadyInUseException(
|
||||
'Command "' . $commandName . '" registered by "' . $package->getPackageKey() . '" is already in use',
|
||||
1484486383
|
||||
);
|
||||
}
|
||||
$this->commands[$commandName] = GeneralUtility::makeInstance($commandConfig['class'], $commandName);
|
||||
$this->commandConfigurations[$commandName] = $commandConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Command Line Interface Request Handler dealing with registered commands.
|
||||
*/
|
||||
class CommandRequestHandler implements RequestHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Instance of the symfony application
|
||||
* @var Application
|
||||
*/
|
||||
protected $application;
|
||||
|
||||
/**
|
||||
* Constructor initializing the symfony application
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->application = new Application('TYPO3 CMS', TYPO3_version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles any commandline request
|
||||
*
|
||||
* @param InputInterface $input
|
||||
*/
|
||||
public function handleRequest(InputInterface $input)
|
||||
{
|
||||
$output = new ConsoleOutput();
|
||||
|
||||
Bootstrap::loadExtTables();
|
||||
// create the BE_USER object (not logged in yet)
|
||||
Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
|
||||
Bootstrap::initializeLanguageObject();
|
||||
// Make sure output is not buffered, so command-line output and interaction can take place
|
||||
ob_clean();
|
||||
|
||||
$this->populateAvailableCommands();
|
||||
|
||||
$exitCode = $this->application->run($input, $output);
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* This request handler can handle any CLI request
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @return bool Always TRUE
|
||||
*/
|
||||
public function canHandleRequest(InputInterface $input): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the priority - how eager the handler is to actually handle the request.
|
||||
*
|
||||
* @return int The priority of the request handler.
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put all available commands inside the application
|
||||
* @throws \TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException
|
||||
*/
|
||||
protected function populateAvailableCommands()
|
||||
{
|
||||
$commands = GeneralUtility::makeInstance(CommandRegistry::class);
|
||||
|
||||
foreach ($commands as $commandName => $command) {
|
||||
/** @var Command $command */
|
||||
$this->application->add($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* The interface for a request handler for a console-based application
|
||||
*/
|
||||
interface RequestHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Handles a CLI request
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @return OutputInterface|null
|
||||
*/
|
||||
public function handleRequest(InputInterface $input);
|
||||
|
||||
/**
|
||||
* Checks if the request handler can handle the given request.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @return bool TRUE if it can handle the request, otherwise FALSE
|
||||
*/
|
||||
public function canHandleRequest(InputInterface $input);
|
||||
|
||||
/**
|
||||
* Returns the priority - how eager the handler is to actually handle the
|
||||
* request. An integer > 0 means "I want to handle this request" where
|
||||
* "100" is default. "0" means "I am a fallback solution".
|
||||
*
|
||||
* @return int The priority of the request handler
|
||||
* @internal will get removed from the interface soon as this is not needed in TYPO3 v10.0 anymore.
|
||||
*/
|
||||
public function getPriority();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when an unregistered command is asked for
|
||||
*/
|
||||
class UnknownCommandException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Context\Exception\AspectPropertyNotFoundException;
|
||||
|
||||
/**
|
||||
* Interface AspectInterface
|
||||
* Think of an aspect like a property bag
|
||||
*/
|
||||
interface AspectInterface
|
||||
{
|
||||
/**
|
||||
* Get a property from an aspect
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
* @throws AspectPropertyNotFoundException
|
||||
*/
|
||||
public function get(string $name);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
|
||||
use TYPO3\CMS\Core\Context\Exception\AspectPropertyNotFoundException;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
|
||||
/**
|
||||
* Contains the state of a current page request, to be used when reading
|
||||
* information of the current request in which configuration/context
|
||||
* it is used.
|
||||
*
|
||||
* Typically, the current main context is initialized very early within each entry-point application,
|
||||
* and is then modified overridden in e.g. PSR-15 middlewares (e.g. authentication, preview settings etc).
|
||||
*
|
||||
* For most use-cases, the current main context is fetched via GeneralUtility::makeInstance(Context::class),
|
||||
* however, if custom settings for a single use-case is necessary, it is recommended to clone the base context:
|
||||
*
|
||||
* $mainContext = GeneralUtility::makeInstance(Context::class);
|
||||
* $customContext = clone $mainContext;
|
||||
* $customContext->setAspect(GeneralUtility::makeInstance(VisibilityAspect::class, true, true, false))
|
||||
*
|
||||
* ... which in turn can be injected in the various places where TYPO3 uses contexts.
|
||||
*
|
||||
*
|
||||
* Classic aspect names to be used are:
|
||||
* - date (DateTimeAspect)
|
||||
* - workspace
|
||||
* - visibility
|
||||
* - frontend.user
|
||||
* - backend.user
|
||||
* - language
|
||||
*/
|
||||
class Context implements SingletonInterface
|
||||
{
|
||||
/**
|
||||
* @var AspectInterface[]
|
||||
*/
|
||||
protected $aspects = [];
|
||||
|
||||
/**
|
||||
* Sets up the context with pre-defined aspects
|
||||
*
|
||||
* @param array|null $defaultAspects
|
||||
*/
|
||||
public function __construct(array $defaultAspects = [])
|
||||
{
|
||||
foreach ($defaultAspects as $name => $defaultAspect) {
|
||||
if ($defaultAspect instanceof AspectInterface) {
|
||||
$this->aspects[$name] = $defaultAspect;
|
||||
}
|
||||
}
|
||||
// Ensure the default aspects are set, this is mostly necessary for tests to not set up everything
|
||||
if (!$this->hasAspect('date')) {
|
||||
$time = $GLOBALS['EXEC_TIME'] ?? time();
|
||||
$this->setAspect('date', new DateTimeAspect(new \DateTimeImmutable('@' . $time)));
|
||||
}
|
||||
if (!$this->hasAspect('visibility')) {
|
||||
$this->setAspect('visibility', new VisibilityAspect());
|
||||
}
|
||||
if (!$this->hasAspect('backend.user')) {
|
||||
$this->setAspect('backend.user', new UserAspect());
|
||||
}
|
||||
if (!$this->hasAspect('frontend.user')) {
|
||||
$this->setAspect('frontend.user', new UserAspect());
|
||||
}
|
||||
if (!$this->hasAspect('workspace')) {
|
||||
$this->setAspect('workspace', new WorkspaceAspect());
|
||||
}
|
||||
if (!$this->hasAspect('language')) {
|
||||
$this->setAspect('language', new LanguageAspect());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an aspect exists in the context
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAspect(string $name): bool
|
||||
{
|
||||
return isset($this->aspects[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an aspect, if it is set
|
||||
*
|
||||
* @param string $name
|
||||
* @return AspectInterface
|
||||
* @throws AspectNotFoundException
|
||||
*/
|
||||
public function getAspect(string $name): AspectInterface
|
||||
{
|
||||
if (!$this->hasAspect($name)) {
|
||||
throw new AspectNotFoundException('No aspect named "' . $name . '" found.', 1527777641);
|
||||
}
|
||||
return $this->aspects[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a property from the aspect, but only if the property is found.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $property
|
||||
* @param mixed $default
|
||||
* @return mixed|null
|
||||
* @throws AspectNotFoundException
|
||||
*/
|
||||
public function getPropertyFromAspect(string $name, string $property, $default = null)
|
||||
{
|
||||
if (!$this->hasAspect($name)) {
|
||||
throw new AspectNotFoundException('No aspect named "' . $name . '" found.', 1527777868);
|
||||
}
|
||||
try {
|
||||
return $this->aspects[$name]->get($property);
|
||||
} catch (AspectPropertyNotFoundException $e) {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an aspect, or overrides an existing aspect if an aspect is already set
|
||||
*
|
||||
* @param string $name
|
||||
* @param AspectInterface $aspect
|
||||
*/
|
||||
public function setAspect(string $name, AspectInterface $aspect): void
|
||||
{
|
||||
$this->aspects[$name] = $aspect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
interface ContextAwareInterface
|
||||
{
|
||||
public function setContext(Context $context): void;
|
||||
|
||||
public function getContext(): Context;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
trait ContextAwareTrait
|
||||
{
|
||||
/**
|
||||
* @var Context
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
public function setContext(Context $context): void
|
||||
{
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
public function getContext(): Context
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Context\Exception\AspectPropertyNotFoundException;
|
||||
|
||||
/**
|
||||
* The Aspect is usually available as "date.*" properties in the Context.
|
||||
*
|
||||
* Contains the current time + date + timezone,
|
||||
* and needs a DateTimeImmutable object
|
||||
*
|
||||
* Allowed properties:
|
||||
* - timestamp - unix timestamp number
|
||||
* - timezone - America/Los_Angeles
|
||||
* - iso - datetime as string in ISO 8601 format, e.g. `2004-02-12T15:19:21+00:00`
|
||||
* - full - the DateTimeImmutable object
|
||||
*/
|
||||
class DateTimeAspect implements AspectInterface
|
||||
{
|
||||
/**
|
||||
* @var \DateTimeImmutable
|
||||
*/
|
||||
protected $dateTimeObject;
|
||||
|
||||
/**
|
||||
* @param \DateTimeImmutable $dateTimeImmutable
|
||||
*/
|
||||
public function __construct(\DateTimeImmutable $dateTimeImmutable)
|
||||
{
|
||||
$this->dateTimeObject = $dateTimeImmutable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a property of the date time object or the object itself ("full").
|
||||
*
|
||||
* @param string $name
|
||||
* @return \DateTimeImmutable|string
|
||||
* @throws AspectPropertyNotFoundException
|
||||
*/
|
||||
public function get(string $name)
|
||||
{
|
||||
switch ($name) {
|
||||
case 'timestamp':
|
||||
return $this->dateTimeObject->format('U');
|
||||
case 'iso':
|
||||
return $this->dateTimeObject->format('c');
|
||||
case 'timezone':
|
||||
return $this->dateTimeObject->format('e');
|
||||
case 'full':
|
||||
return $this->dateTimeObject;
|
||||
case 'accessTime':
|
||||
return $this->dateTimeObject->format('U') - ($this->dateTimeObject->format('U') % 60);
|
||||
}
|
||||
throw new AspectPropertyNotFoundException('Property "' . $name . '" not found in Aspect "' . __CLASS__ . '".', 1527778767);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full date time object
|
||||
*
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getDateTime(): \DateTimeImmutable
|
||||
{
|
||||
return $this->dateTimeObject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Used when an aspect is requested but not found.
|
||||
*/
|
||||
class AspectNotFoundException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Used when an aspect property is requested but not found.
|
||||
*/
|
||||
class AspectPropertyNotFoundException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Context\Exception\AspectPropertyNotFoundException;
|
||||
|
||||
/**
|
||||
* The Aspect is usually available as "language" property, and
|
||||
* can be used to find out the "overlay"/data retrieval strategy.
|
||||
*
|
||||
*
|
||||
* "id" (languageId, int)
|
||||
* - formally known as $TSFE->sys_language_uid
|
||||
* - the requested language of the current page (frontend)
|
||||
* - used in menus and links to generate "links in language with this ID"
|
||||
*
|
||||
* "contentId" (int)
|
||||
* - formally known as $TSFE->sys_language_content
|
||||
* - the language of records to be fetched
|
||||
* - if empty, "languageId" is used.
|
||||
*
|
||||
* "fallbackChain"
|
||||
* - when "fallback" go with
|
||||
* - depends what "contentId" value should be set
|
||||
* - defined in config.sys_language_mode (strict/content_fallback:4,5,stop/ignore?)
|
||||
* - previously known as $TSFE->sys_language_mode
|
||||
* - defines "contentId" based on "if the current page is available in this language"
|
||||
* - "strict"
|
||||
* - "fallback" if current page is not available, check the fallbackChain"
|
||||
* - "fallbackAndIgnore"
|
||||
*
|
||||
* "overlayType"
|
||||
* - defines which way the records should be fetched from ($TSFE->sys_language_contentOL and config.sys_language_overlay)
|
||||
* - usually you fetch language 0 and -1, then take the "contentId" and "overlay" them
|
||||
* - here you have two choices
|
||||
* 1. "on" if there is no overlay, do not render the default language records ("hideNonTranslated")
|
||||
* 2. "mixed" - if there is no overlay, just keep the default language, possibility to have mixed languages - config.sys_language_overlay = 1
|
||||
* 3. "off" - do not do overlay, only fetch records available in the current "contentId" (see above), and do not care about overlays or fallbacks - fallbacks could be an option here, actually that is placed on top
|
||||
* 4. "includeFloating" - on + includeRecordsWithoutDefaultTranslation
|
||||
*/
|
||||
class LanguageAspect implements AspectInterface
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $id = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $contentId = 0;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fallbackChain = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $overlayType;
|
||||
|
||||
public const OVERLAYS_OFF = 'off'; // config.sys_language_overlay = 0
|
||||
public const OVERLAYS_MIXED = 'mixed'; // config.sys_language_overlay = 1 (keep the ones that are only available in default language)
|
||||
public const OVERLAYS_ON = 'on'; // "hideNonTranslated"
|
||||
public const OVERLAYS_ON_WITH_FLOATING = 'includeFloating'; // "hideNonTranslated" + records that are only available in polish
|
||||
|
||||
/**
|
||||
* Create the default language
|
||||
*
|
||||
* @param int $id
|
||||
* @param int|null $contentId
|
||||
* @param string $overlayType
|
||||
* @param array $fallbackChain
|
||||
*/
|
||||
public function __construct(int $id = 0, int $contentId = null, string $overlayType = self::OVERLAYS_ON_WITH_FLOATING, array $fallbackChain = [])
|
||||
{
|
||||
$this->overlayType = $overlayType;
|
||||
$this->id = $id;
|
||||
$this->contentId = $contentId === null ? $this->id : $contentId;
|
||||
$this->fallbackChain = $fallbackChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used language overlay
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOverlayType(): string
|
||||
{
|
||||
return $this->overlayType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language ID the current page was requested,
|
||||
* this is relevant when building menus or links to other pages.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the language UID of the content records that should be overlaid to would be fetched.
|
||||
* This is especially useful when a page requested with language=4 should fall back to showing
|
||||
* content of language=2 (see fallbackChain)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getContentId(): int
|
||||
{
|
||||
return $this->contentId;
|
||||
}
|
||||
|
||||
public function getFallbackChain(): array
|
||||
{
|
||||
return $this->fallbackChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether overlays should be done
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function doOverlays(): bool
|
||||
{
|
||||
return $this->contentId > 0 && $this->overlayType !== self::OVERLAYS_OFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Previously known as TSFE->sys_language_mode, here for compatibility reasons
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLegacyLanguageMode(): string
|
||||
{
|
||||
if ($this->fallbackChain === ['off']) {
|
||||
return '';
|
||||
}
|
||||
if (empty($this->fallbackChain)) {
|
||||
return 'strict';
|
||||
}
|
||||
if ($this->fallbackChain === [-1]) {
|
||||
return 'ignore';
|
||||
}
|
||||
return 'content_fallback';
|
||||
}
|
||||
|
||||
/**
|
||||
* Previously known as TSFE->sys_language_contentOL, here for compatibility reasons
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLegacyOverlayType(): string
|
||||
{
|
||||
switch ($this->overlayType) {
|
||||
case self::OVERLAYS_ON_WITH_FLOATING:
|
||||
case self::OVERLAYS_ON:
|
||||
return 'hideNonTranslated';
|
||||
case self::OVERLAYS_MIXED:
|
||||
return '1';
|
||||
case self::OVERLAYS_OFF:
|
||||
default:
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a property.
|
||||
*
|
||||
* @param string $name
|
||||
* @return int|string|array
|
||||
* @throws AspectPropertyNotFoundException
|
||||
*/
|
||||
public function get(string $name)
|
||||
{
|
||||
switch ($name) {
|
||||
case 'id':
|
||||
return $this->id;
|
||||
case 'contentId':
|
||||
return $this->contentId;
|
||||
case 'fallbackChain':
|
||||
return $this->fallbackChain;
|
||||
case 'overlayType':
|
||||
return $this->overlayType;
|
||||
case 'legacyLanguageMode':
|
||||
return $this->getLegacyLanguageMode();
|
||||
case 'legacyOverlayType':
|
||||
return $this->getLegacyOverlayType();
|
||||
}
|
||||
throw new AspectPropertyNotFoundException('Property "' . $name . '" not found in Aspect "' . __CLASS__ . '".', 1530448504);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Core\Context;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A simple factory to create a language aspect.
|
||||
*/
|
||||
class LanguageAspectFactory
|
||||
{
|
||||
/**
|
||||
* Create a language aspect based on TypoScript settings, which we know:
|
||||
*
|
||||
* config.sys_language_uid
|
||||
* config.sys_language_mode
|
||||
* config.sys_language_overlay
|
||||
*
|
||||
* @param array $config
|
||||
* @return LanguageAspect
|
||||
*/
|
||||
public static function createFromTypoScript(array $config): LanguageAspect
|
||||
{
|
||||
// Get values from TypoScript, if not set before
|
||||
$languageId = (int)($config['sys_language_uid'] ?? 0);
|
||||
$fallbacks = GeneralUtility::trimExplode(';', $config['sys_language_mode'] ?? '');
|
||||
$fallbackMode = null;
|
||||
if (isset($fallbacks[0])) {
|
||||
$fallbackMode = $fallbacks[0];
|
||||
}
|
||||
$fallbackOrder = null;
|
||||
if (isset($fallbacks[1])) {
|
||||
$fallbackOrder = $fallbacks[1];
|
||||
}
|
||||
|
||||
// Page resolving
|
||||
switch ($fallbackMode) {
|
||||
case 'strict':
|
||||
$fallBackOrder = [];
|
||||
break;
|
||||
// Ignore anything if a page cannot be found, and resolve pageId=0 instead.
|
||||
case 'ignore':
|
||||
$fallBackOrder = [-1];
|
||||
break;
|
||||
case 'fallback':
|
||||
case 'content_fallback':
|
||||
if (!empty($fallbackOrder)) {
|
||||
$fallBackOrder = GeneralUtility::trimExplode(',', $fallbackOrder);
|
||||
// no strict typing explictly done here
|
||||
if (!in_array(0, $fallBackOrder) && !in_array('pageNotFound', $fallBackOrder)) {
|
||||
$fallBackOrder[] = 'pageNotFound';
|
||||
}
|
||||
} else {
|
||||
$fallBackOrder = [0];
|
||||
}
|
||||
break;
|
||||
case '':
|
||||
$fallBackOrder = ['off'];
|
||||
break;
|
||||
default:
|
||||
$fallBackOrder = [0];
|
||||
}
|
||||
|
||||
// Content fetching
|
||||
switch ((string)($config['sys_language_overlay'] ?? '')) {
|
||||
case '1':
|
||||
$overlayType = LanguageAspect::OVERLAYS_MIXED;
|
||||
break;
|
||||
case '0':
|
||||
$overlayType = LanguageAspect::OVERLAYS_OFF;
|
||||
break;
|
||||
case 'hideNonTranslated':
|
||||
$overlayType = LanguageAspect::OVERLAYS_ON;
|
||||
break;
|
||||
default:
|
||||
$overlayType = LanguageAspect::OVERLAYS_ON_WITH_FLOATING;
|
||||
}
|
||||
|
||||
return GeneralUtility::makeInstance(LanguageAspect::class, $languageId, $languageId, $overlayType, $fallBackOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Site Languages always run with overlays + floating records.
|
||||
*
|
||||
* @param SiteLanguage $language
|
||||
* @return LanguageAspect
|
||||
*/
|
||||
public static function createFromSiteLanguage(SiteLanguage $language): LanguageAspect
|
||||
{
|
||||
$languageId = $language->getLanguageId();
|
||||
$fallbackType = $language->getFallbackType();
|
||||
$fallbackOrder = $language->getFallbackLanguageIds();
|
||||
$fallbackOrder[] = 'pageNotFound';
|
||||
switch ($fallbackType) {
|
||||
// Fall back to other language, if the page does not exist in the requested language
|
||||
// But always fetch only records of this specific (available) language
|
||||
case 'free':
|
||||
$overlayType = LanguageAspect::OVERLAYS_OFF;
|
||||
break;
|
||||
|
||||
// Fall back to other language, if the page does not exist in the requested language
|
||||
// Do overlays, and keep the ones that are not translated
|
||||
case 'fallback':
|
||||
$overlayType = LanguageAspect::OVERLAYS_MIXED;
|
||||
break;
|
||||
|
||||
// Same as "fallback" but remove the records that are not translated
|
||||
case 'strict':
|
||||
$overlayType = LanguageAspect::OVERLAYS_ON_WITH_FLOATING;
|
||||
break;
|
||||
|
||||
// Ignore, fallback to default language
|
||||
default:
|
||||
$fallbackOrder = [0];
|
||||
$overlayType = LanguageAspect::OVERLAYS_OFF;
|
||||
}
|
||||
|
||||
return GeneralUtility::makeInstance(LanguageAspect::class, $languageId, $languageId, $overlayType, $fallbackOrder);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 12716 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user