Commit 0ef9df90 authored by Susanne Moog's avatar Susanne Moog Committed by Frank Nägler
Browse files

[TASK] Streamline expressionLanguage usage in core

* provide same functions across contexts / methods
* provide same way to extend expressionLanguage everywhere
* provide way to load context specific variables and functions
* prepare compile step

Resolves: #86196
Related: #86243
Releases: master
Change-Id: I86cc04ec7051293c195879f823d90d894d160ff0
Reviewed-on: https://review.typo3.org/58232


Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
parent 2b4c6f3d
......@@ -18,7 +18,7 @@ use TYPO3\CMS\Backend\Controller\EditDocumentController;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\ExpressionLanguage\TypoScriptConditionProvider;
use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
......@@ -55,12 +55,15 @@ class ConditionMatcher extends AbstractConditionMatcher
$backend->user->userId = $backendUserAspect->get('id') ?? 0;
$backend->user->userGroupList = implode(',', $backendUserAspect->get('groupIds'));
$typoScriptConditionProvider = GeneralUtility::makeInstance(TypoScriptConditionProvider::class, [
'tree' => $tree,
'backend' => $backend,
'page' => $this->getPage(),
]);
parent::__construct($typoScriptConditionProvider);
$this->expressionLanguageResolver = GeneralUtility::makeInstance(
Resolver::class,
'typoscript',
[
'tree' => $tree,
'backend' => $backend,
'page' => $this->getPage(),
]
);
}
/**
......
......@@ -14,12 +14,17 @@ namespace TYPO3\CMS\Backend\Tests\Unit\Configuration\TypoScript\ConditionMatchin
* The TYPO3 project - inspiring people to share!
*/
use Prophecy\Argument;
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Package\PackageInterface;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
......@@ -58,6 +63,21 @@ class ConditionMatcherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCas
protected function setUp()
{
$GLOBALS['TYPO3_REQUEST'] = new ServerRequest();
$cacheFrontendProphecy = $this->prophesize(FrontendInterface::class);
$cacheFrontendProphecy->has(Argument::any())->willReturn(false);
$cacheFrontendProphecy->set(Argument::any(), Argument::any())->willReturn(null);
$cacheManagerProphecy = $this->prophesize(CacheManager::class);
$cacheManagerProphecy->getCache('cache_core')->willReturn($cacheFrontendProphecy->reveal());
GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
$packageManagerProphecy = $this->prophesize(PackageManager::class);
$corePackageProphecy = $this->prophesize(PackageInterface::class);
$corePackageProphecy->getPackagePath()->willReturn(__DIR__ . '/../../../../../../../sysext/core/');
$packageManagerProphecy->getActivePackages()->willReturn([
$corePackageProphecy->reveal()
]);
GeneralUtility::setSingletonInstance(PackageManager::class, $packageManagerProphecy->reveal());
$this->testTableName = 'conditionMatcherTestTable';
$this->testGlobalNamespace = $this->getUniqueId('TEST');
$GLOBALS['TCA'][$this->testTableName] = ['ctrl' => []];
......
......@@ -21,7 +21,7 @@ use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Configuration\TypoScript\Exception\InvalidTypoScriptConditionException;
use TYPO3\CMS\Core\Error\Exception;
use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
use TYPO3\CMS\Core\ExpressionLanguage\TypoScriptConditionProvider;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
......@@ -71,11 +71,6 @@ abstract class AbstractConditionMatcher implements LoggerAwareInterface
*/
protected $expressionLanguageResolver;
public function __construct(TypoScriptConditionProvider $typoScriptConditionProvider)
{
$this->expressionLanguageResolver = GeneralUtility::makeInstance(Resolver::class, $typoScriptConditionProvider);
}
/**
* @return bool
*/
......@@ -245,10 +240,11 @@ abstract class AbstractConditionMatcher implements LoggerAwareInterface
if (strpos($exception->getMessage(), 'Unexpected character "="') !== false) {
$message .= ' It looks like an old condition with only one equal sign.';
}
$this->logger->warning($message, [
'expression' => $expression,
'exception' => $exception
]);
$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
......
......@@ -19,7 +19,6 @@ use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
/**
* Class AbstractProvider
* @internal
*/
abstract class AbstractProvider implements ProviderInterface
{
......
......@@ -15,10 +15,16 @@ namespace TYPO3\CMS\Core\ExpressionLanguage;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\ExpressionLanguage\FunctionsProvider\DefaultFunctionsProvider;
/**
* Class DefaultProvider
* @internal
*/
class DefaultProvider extends AbstractProvider
{
public function __construct()
{
$this->expressionLanguageProviders[] = DefaultFunctionsProvider::class;
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\ExpressionLanguage;
namespace TYPO3\CMS\Core\ExpressionLanguage\FunctionsProvider;
/*
* This file is part of the TYPO3 CMS project.
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\ExpressionLanguage;
namespace TYPO3\CMS\Core\ExpressionLanguage\FunctionsProvider;
/*
* This file is part of the TYPO3 CMS project.
......@@ -17,6 +17,9 @@ namespace TYPO3\CMS\Core\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use TYPO3\CMS\Core\ExpressionLanguage\RequestWrapper;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
......@@ -24,7 +27,7 @@ use TYPO3\CMS\Core\Utility\VersionNumberUtility;
* Class TypoScriptConditionProvider
* @internal
*/
class TypoScriptConditionFunctionsProvider implements ExpressionFunctionProviderInterface
class Typo3ConditionFunctionsProvider implements ExpressionFunctionProviderInterface
{
/**
* @return ExpressionFunction[] An array of Function instances
......@@ -37,6 +40,9 @@ class TypoScriptConditionFunctionsProvider implements ExpressionFunctionProvider
$this->getLoginUserFunction(),
$this->getTSFEFunction(),
$this->getUsergroupFunction(),
$this->getSessionFunction(),
$this->getSiteFunction(),
$this->getSiteLanguageFunction(),
];
}
......@@ -105,4 +111,78 @@ class TypoScriptConditionFunctionsProvider implements ExpressionFunctionProvider
return false;
});
}
protected function getSessionFunction(): ExpressionFunction
{
return new ExpressionFunction(
'session',
function ($str) {
// Not implemented, we only use the evaluator
},
function ($arguments, $str) {
$retVal = null;
$keyParts = explode('|', $str);
$sessionKey = array_shift($keyParts);
// @todo fetch data from be session if available
$tsfe = $GLOBALS['TSFE'] ?? null;
if ($tsfe && is_object($tsfe->fe_user)) {
$retVal = $tsfe->fe_user->getSessionData($sessionKey);
foreach ($keyParts as $keyPart) {
if (is_object($retVal)) {
$retVal = $retVal->{$keyPart};
} elseif (is_array($retVal)) {
$retVal = $retVal[$keyPart];
} else {
break;
}
}
}
return $retVal;
}
);
}
protected function getSiteFunction(): ExpressionFunction
{
return new ExpressionFunction(
'site',
function ($str) {
// Not implemented, we only use the evaluator
},
function ($arguments, $str) {
/** @var RequestWrapper $requestWrapper */
$requestWrapper = $arguments['request'];
$site = $requestWrapper->getSite();
if ($site instanceof Site) {
$methodName = 'get' . ucfirst(trim($str));
if (method_exists($site, $methodName)) {
return $site->$methodName();
}
}
return null;
}
);
}
protected function getSiteLanguageFunction(): ExpressionFunction
{
return new ExpressionFunction(
'siteLanguage',
function ($str) {
// Not implemented, we only use the evaluator
},
function ($arguments, $str) {
/** @var RequestWrapper $requestWrapper */
$requestWrapper = $arguments['request'];
$siteLanguage = $requestWrapper->getSiteLanguage();
if ($siteLanguage instanceof SiteLanguage) {
$methodName = 'get' . ucfirst(trim($str));
if (method_exists($siteLanguage, $methodName)) {
return $siteLanguage->$methodName();
}
}
return null;
}
);
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\ExpressionLanguage;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class ProviderConfigurationLoader
* This class resolves the expression language provider configuration and store in a cache.
*/
class ProviderConfigurationLoader
{
protected $cacheIdentifier = 'expressionLanguageProviders';
/**
* @return array
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
*/
public function getExpressionLanguageProviders(): array
{
$packageManager = GeneralUtility::makeInstance(
PackageManager::class,
GeneralUtility::makeInstance(DependencyOrderingService::class)
);
$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_core');
if ($cache->has($this->cacheIdentifier)) {
/** @noinspection PhpUndefinedMethodInspection the method require() will be added to the interface in v10 */
return $cache->require($this->cacheIdentifier);
}
$packages = $packageManager->getActivePackages();
$providers = [];
foreach ($packages as $package) {
$packageConfiguration = $package->getPackagePath() . 'Configuration/ExpressionLanguage.php';
if (file_exists($packageConfiguration)) {
$providersInPackage = require $packageConfiguration;
if (is_array($providersInPackage)) {
$providers[] = $providersInPackage;
}
}
}
$providers = count($providers) > 0 ? array_merge_recursive(...$providers) : $providers;
$cache->set($this->cacheIdentifier, 'return ' . var_export($providers, true) . ';');
return $providers ?? [];
}
}
......@@ -19,7 +19,6 @@ use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
/**
* Interface ProviderInterface
* @internal
*/
interface ProviderInterface
{
......
......@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Core\ExpressionLanguage;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
......@@ -25,6 +26,9 @@ use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
* This class provides access to some methods of the ServerRequest object.
* To prevent access to all methods of the ServerRequest object within conditions,
* this class was introduced to control which methods are exposed.
*
* Additionally this class can be used to simulate a request for condition matching in case the condition matcher calls
* should be simulated (for example simulating parsing of TypoScript on CLI)
* @internal
*/
class RequestWrapper
......@@ -34,9 +38,9 @@ class RequestWrapper
*/
protected $request;
public function __construct()
public function __construct(?ServerRequestInterface $request)
{
$this->request = $GLOBALS['TYPO3_REQUEST'];
$this->request = $request ?? new ServerRequest();
}
public function getQueryParams(): array
......
......@@ -16,10 +16,10 @@ namespace TYPO3\CMS\Core\ExpressionLanguage;
*/
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class Resolver
* @internal
*/
class Resolver
{
......@@ -39,13 +39,31 @@ class Resolver
public $expressionLanguageVariables = [];
/**
* @param ProviderInterface $provider
* @param string $context
* @param array $variables
*/
public function __construct(ProviderInterface $provider)
public function __construct(string $context, array $variables)
{
$this->provider = $provider;
$this->expressionLanguage = new ExpressionLanguage(null, $provider->getExpressionLanguageProviders());
$this->expressionLanguageVariables = $provider->getExpressionLanguageVariables();
$functionProviderInstances = [];
$providers = GeneralUtility::makeInstance(ProviderConfigurationLoader::class)->getExpressionLanguageProviders()[$context] ?? [];
// Always add default provider
array_unshift($providers, DefaultProvider::class);
$providers = array_unique($providers);
$functionProviders = [];
$generalVariables = [];
foreach ($providers as $provider) {
/** @var ProviderInterface $providerInstance */
$providerInstance = GeneralUtility::makeInstance($provider);
$functionProviders[] = $providerInstance->getExpressionLanguageProviders();
$generalVariables[] = $providerInstance->getExpressionLanguageVariables();
}
$functionProviders = array_merge(...$functionProviders);
$generalVariables = array_replace_recursive(...$generalVariables);
$this->expressionLanguageVariables = array_replace_recursive($generalVariables, $variables);
foreach ($functionProviders as $functionProvider) {
$functionProviderInstances[] = GeneralUtility::makeInstance($functionProvider);
}
$this->expressionLanguage = new ExpressionLanguage(null, $functionProviderInstances);
}
/**
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\ExpressionLanguage;
/*
......@@ -15,41 +16,29 @@ namespace TYPO3\CMS\Core\ExpressionLanguage;
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use TYPO3\CMS\Core\ExpressionLanguage\FunctionsProvider\Typo3ConditionFunctionsProvider;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class TypoScriptConditionProvider
*
* @internal
*/
class TypoScriptConditionProvider extends AbstractProvider
{
public function __construct(array $expressionLanguageVariables = [], array $expressionLanguageProviders = [])
public function __construct()
{
$typo3 = new \stdClass();
$typo3->version = TYPO3_version;
$typo3->branch = TYPO3_branch;
$typo3->devIpMask = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']);
$this->expressionLanguageVariables = array_merge([
'request' => GeneralUtility::makeInstance(RequestWrapper::class),
$this->expressionLanguageVariables = [
'request' => GeneralUtility::makeInstance(RequestWrapper::class, $GLOBALS['TYPO3_REQUEST'] ?? null),
'applicationContext' => (string)GeneralUtility::getApplicationContext(),
'typo3' => $typo3,
], $expressionLanguageVariables);
$this->expressionLanguageProviders = $expressionLanguageProviders;
$this->initFunctions();
}
protected function initFunctions(): void
{
$this->expressionLanguageProviders[] = GeneralUtility::makeInstance(DefaultFunctionsProvider::class);
$this->expressionLanguageProviders[] = GeneralUtility::makeInstance(TypoScriptConditionFunctionsProvider::class);
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][__CLASS__]['additionalExpressionLanguageProvider'] ?? [] as $className) {
$expressionLanguageProvider = GeneralUtility::makeInstance($className);
if ($expressionLanguageProvider instanceof ExpressionFunctionProviderInterface) {
$this->expressionLanguageProviders[] = $expressionLanguageProvider;
}
}
];
$this->expressionLanguageProviders = [
Typo3ConditionFunctionsProvider::class
];
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\ExpressionLanguage;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full 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\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
/**
* Class TypoScriptFrontendConditionFunctionsProvider
* @internal
*/
class TypoScriptFrontendConditionFunctionsProvider implements ExpressionFunctionProviderInterface
{
/**
* @return ExpressionFunction[] An array of Function instances
*/
public function getFunctions()
{
$functions = [
$this->getSessionFunction(),
$this->getSiteFunction(),
$this->getSiteLanguageFunction(),
];
return $functions;
}
protected function getSessionFunction(): ExpressionFunction
{
return new ExpressionFunction('session', function ($str) {
// Not implemented, we only use the evaluator
}, function ($arguments, $str) {
$retVal = null;
$keyParts = explode('|', $str);
$sessionKey = array_shift($keyParts);
$tsfe = $GLOBALS['TSFE'];
if ($tsfe && is_object($tsfe->fe_user)) {
$retVal = $tsfe->fe_user->getSessionData($sessionKey);
foreach ($keyParts as $keyPart) {
if (is_object($retVal)) {
$retVal = $retVal->{$keyPart};
} elseif (is_array($retVal)) {
$retVal = $retVal[$keyPart];
} else {
break;
}
}
}
return $retVal;
});
}
protected function getSiteFunction(): ExpressionFunction
{
return new ExpressionFunction('site', function ($str) {
// Not implemented, we only use the evaluator
}, function ($arguments, $str) {
/** @var RequestWrapper $requestWrapper */
$requestWrapper = $arguments['request'];
$site = $requestWrapper->getSite();
if ($site instanceof Site) {
$methodName = 'get' . ucfirst(trim($str));
if (method_exists($site, $methodName)) {
return $site->$methodName();
}
}
return null;
});
}
protected function getSiteLanguageFunction(): ExpressionFunction
{
return new ExpressionFunction('siteLanguage', function ($str) {
// Not implemented, we only use the evaluator
}, function ($arguments, $str) {
/** @var RequestWrapper $requestWrapper */
$requestWrapper = $arguments['request'];
$siteLanguage = $requestWrapper->getSiteLanguage();
if ($siteLanguage instanceof SiteLanguage) {
$methodName = 'get' . ucfirst(trim($str));
if (method_exists($siteLanguage, $methodName)) {
return $siteLanguage->$methodName();
}
}
return null;
});
}
}
<?php
return [
'default' => [
// The DefaultProvider is loaded every time
// \TYPO3\CMS\Core\ExpressionLanguage\DefaultProvider::class,
],
'typoscript' => [
\TYPO3\CMS\Core\ExpressionLanguage\TypoScriptConditionProvider::class,
]
];