TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Service;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Attribute\Authorize;
|
||||
use TYPO3\CMS\Extbase\Authorization\AuthorizationFailureReason;
|
||||
use TYPO3\CMS\Extbase\Authorization\AuthorizationResult;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
readonly class ActionAuthorizationService
|
||||
{
|
||||
public function __construct(protected Context $context) {}
|
||||
|
||||
/**
|
||||
* Checks all authorize attributes
|
||||
*
|
||||
* @param array<Authorize> $authorizeAttributes
|
||||
*/
|
||||
public function checkAuthorization(
|
||||
ActionController $controller,
|
||||
array $authorizeAttributes,
|
||||
array $preparedArguments
|
||||
): AuthorizationResult {
|
||||
if ($authorizeAttributes === []) {
|
||||
return AuthorizationResult::allowed();
|
||||
}
|
||||
|
||||
foreach ($authorizeAttributes as $authorize) {
|
||||
$result = $this->evaluateAuthorizeAttribute($authorize, $controller, $preparedArguments);
|
||||
if ($result->isDenied()) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return AuthorizationResult::allowed();
|
||||
}
|
||||
|
||||
protected function evaluateAuthorizeAttribute(
|
||||
Authorize $authorize,
|
||||
ActionController $controller,
|
||||
array $preparedArguments
|
||||
): AuthorizationResult {
|
||||
$userAspect = $this->context->getAspect('frontend.user');
|
||||
|
||||
if ($authorize->requireLogin && !$userAspect->isLoggedIn()) {
|
||||
return AuthorizationResult::denied(AuthorizationFailureReason::NOT_LOGGED_IN, $authorize);
|
||||
}
|
||||
|
||||
if (!$this->checkGroupAccess($authorize, $userAspect)) {
|
||||
return AuthorizationResult::denied(AuthorizationFailureReason::MISSING_GROUP, $authorize);
|
||||
}
|
||||
|
||||
if ($authorize->callback !== null && !$this->executeCallback($authorize, $controller, $preparedArguments)) {
|
||||
return AuthorizationResult::denied(AuthorizationFailureReason::CALLBACK_DENIED, $authorize);
|
||||
}
|
||||
|
||||
return AuthorizationResult::allowed();
|
||||
}
|
||||
|
||||
protected function checkGroupAccess(Authorize $authorize, object $userAspect): bool
|
||||
{
|
||||
if (empty($authorize->requireGroups)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$userGroupIds = $userAspect->getGroupIds();
|
||||
$userGroupNames = $userAspect->getGroupNames();
|
||||
|
||||
foreach ($authorize->requireGroups as $requiredGroup) {
|
||||
if (is_numeric($requiredGroup) && in_array((int)$requiredGroup, $userGroupIds, true)) {
|
||||
return true;
|
||||
}
|
||||
if (!is_numeric($requiredGroup) && in_array($requiredGroup, $userGroupNames, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function executeCallback(
|
||||
Authorize $authorize,
|
||||
ActionController $controller,
|
||||
array $preparedArguments
|
||||
): bool {
|
||||
if (is_array($authorize->callback)) {
|
||||
return $this->executeClassCallback($authorize->callback, $preparedArguments);
|
||||
}
|
||||
return $this->executeControllerCallback($controller, $authorize->callback, $preparedArguments);
|
||||
}
|
||||
|
||||
protected function executeClassCallback(array $callback, array $arguments): bool
|
||||
{
|
||||
[$className, $methodName] = $callback;
|
||||
|
||||
$instance = $this->getCallbackInstance($className);
|
||||
$this->validateCallbackMethod($instance, $methodName, $className);
|
||||
|
||||
return (bool)$instance->$methodName(...$arguments);
|
||||
}
|
||||
|
||||
protected function executeControllerCallback(ActionController $controller, string $methodName, array $arguments): bool
|
||||
{
|
||||
$this->validateCallbackMethod($controller, $methodName, $controller::class);
|
||||
return (bool)$controller->$methodName(...$arguments);
|
||||
}
|
||||
|
||||
protected function getCallbackInstance(string $className): object
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('Authorization callback class "%s" does not exist', $className),
|
||||
1761287267
|
||||
);
|
||||
}
|
||||
return GeneralUtility::makeInstance($className);
|
||||
}
|
||||
|
||||
protected function validateCallbackMethod(object $instance, string $methodName, string $className): void
|
||||
{
|
||||
if (!method_exists($instance, $methodName)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('Authorization callback method "%s::%s" does not exist', $className, $methodName),
|
||||
1761287268
|
||||
);
|
||||
}
|
||||
|
||||
$reflectionMethod = new \ReflectionMethod($instance, $methodName);
|
||||
if (!$reflectionMethod->isPublic()) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('Authorization callback method "%s::%s" must be public', $className, $methodName),
|
||||
1761287269
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Service;
|
||||
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
|
||||
/**
|
||||
* Cache clearing helper functions
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class CacheService
|
||||
{
|
||||
protected array $clearCacheForTables = [];
|
||||
protected \SplStack $cacheTagStack;
|
||||
|
||||
public function __construct(
|
||||
private readonly ConfigurationManagerInterface $configurationManager,
|
||||
private readonly CacheManager $cacheManager,
|
||||
private readonly ConnectionPool $connectionPool,
|
||||
) {
|
||||
$this->cacheTagStack = new \SplStack();
|
||||
}
|
||||
|
||||
public function getCacheTagStack(): \SplStack
|
||||
{
|
||||
return $this->cacheTagStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the page cache
|
||||
*
|
||||
* @param int|int[]|string $pageIdsToClear single or multiple pageIds to clear the cache for
|
||||
* @todo This method should be hardened to only accept integers or an array of integers
|
||||
*/
|
||||
public function clearPageCache($pageIdsToClear = null): void
|
||||
{
|
||||
if ($pageIdsToClear === null) {
|
||||
$this->cacheManager->flushCachesInGroup('pages');
|
||||
} else {
|
||||
if (!is_array($pageIdsToClear)) {
|
||||
$pageIdsToClear = [(int)$pageIdsToClear];
|
||||
}
|
||||
$tags = array_map(static fn(int $item): string => 'pageId_' . $item, $pageIdsToClear);
|
||||
$this->cacheManager->flushCachesInGroupByTags('pages', $tags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First, this method checks, if any records are registered (usually via Database Backend)
|
||||
* to be analyzed for a page record, if so, adds additional page IDs to the pageIdStack.
|
||||
*
|
||||
* Walks through the pageIdStack, collects all pageIds
|
||||
* as array and passes them on to clearPageCache.
|
||||
*/
|
||||
public function clearCachesOfRegisteredPageIds(): void
|
||||
{
|
||||
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
if (!empty($frameworkConfiguration['persistence']['enableAutomaticCacheClearing'] ?? false)) {
|
||||
foreach ($this->clearCacheForTables as $table => $ids) {
|
||||
foreach ($ids as $id) {
|
||||
$this->clearPageCacheForGivenRecord($table, $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$this->cacheTagStack->isEmpty()) {
|
||||
$cacheTags = [];
|
||||
while (!$this->cacheTagStack->isEmpty()) {
|
||||
$cacheTagValue = $this->cacheTagStack->pop();
|
||||
// Add fallback to old behavior. Pushing pageIds directly to the stack is possible. So we need to handle int values as well.
|
||||
$cacheTags[] = is_int($cacheTagValue) ? sprintf('pageId_%s', $cacheTagValue) : (string)$cacheTagValue;
|
||||
}
|
||||
$cacheTags = array_values(array_unique($cacheTags));
|
||||
$this->cacheManager->flushCachesInGroupByTags('pages', $cacheTags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a record into the stack to resolve the page IDs later-on to clear the caches on these pages
|
||||
* then.
|
||||
*
|
||||
* Make sure to call clearCachesOfRegisteredPageIds() afterwards.
|
||||
*
|
||||
* @param string $table
|
||||
* @param int $uid
|
||||
*/
|
||||
public function clearCacheForRecord(string $table, int $uid): void
|
||||
{
|
||||
if (!is_array($this->clearCacheForTables[$table] ?? null)) {
|
||||
$this->clearCacheForTables[$table] = [];
|
||||
}
|
||||
$this->clearCacheForTables[$table][] = $uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the right PID(s) of a given record and loads the TYPO3 page cache for the given record.
|
||||
* If the record lies on a page, then we clear the cache of this page.
|
||||
* If the record has no PID column, we clear the cache of the current page as best-effort.
|
||||
*
|
||||
* Much of this functionality is taken from DataHandler::clear_cache() which unfortunately only works with logged-in BE user.
|
||||
*
|
||||
* @param string $tableName Table name of the record
|
||||
* @param int $uid UID of the record
|
||||
*/
|
||||
protected function clearPageCacheForGivenRecord(string $tableName, int $uid): void
|
||||
{
|
||||
$pageIdsToClear = [];
|
||||
$storagePage = null;
|
||||
|
||||
$this->getCacheTagStack()->push($tableName);
|
||||
$this->getCacheTagStack()->push(sprintf('%s_%s', $tableName, $uid));
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$result = $queryBuilder
|
||||
->select('pid')
|
||||
->from($tableName)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery();
|
||||
if ($row = $result->fetchAssociative()) {
|
||||
$storagePage = $row['pid'];
|
||||
$pageIdsToClear[] = $storagePage;
|
||||
}
|
||||
if ($storagePage === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pageTS = BackendUtility::getPagesTSconfig($storagePage);
|
||||
if (isset($pageTS['TCEMAIN.']['clearCacheCmd'])) {
|
||||
$clearCacheCommands = GeneralUtility::trimExplode(',', strtolower((string)$pageTS['TCEMAIN.']['clearCacheCmd']), true);
|
||||
$clearCacheCommands = array_unique($clearCacheCommands);
|
||||
foreach ($clearCacheCommands as $clearCacheCommand) {
|
||||
if (MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
|
||||
$pageIdsToClear[] = $clearCacheCommand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pageIdsToClear as $pageIdToClear) {
|
||||
$this->getCacheTagStack()->push('pageId_' . $pageIdToClear);
|
||||
$this->getCacheTagStack()->push(sprintf('%s_pid_%s', $tableName, $pageIdToClear));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Service;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Exception;
|
||||
|
||||
/**
|
||||
* Service for determining basic extension params
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class ExtensionService implements SingletonInterface
|
||||
{
|
||||
protected ConfigurationManagerInterface $configurationManager;
|
||||
|
||||
/**
|
||||
* Cache of result for getTargetPidByPlugin()
|
||||
* @var array
|
||||
* @todo: Fishy. This should be at least a runtime cache or something
|
||||
* if it can't be refactored away.
|
||||
*/
|
||||
protected $targetPidPluginCache = [];
|
||||
|
||||
public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
|
||||
{
|
||||
$this->configurationManager = $configurationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the plugin namespace of the specified plugin (defaults to "tx_[extensionname]_[pluginname]")
|
||||
* If plugin.tx_$pluginSignature.view.pluginNamespace is set, this value is returned
|
||||
* If pluginNamespace is not specified "tx_[extensionname]_[pluginname]" is returned.
|
||||
*
|
||||
* @param string|null $extensionName name of the extension to retrieve the namespace for
|
||||
* @param string|null $pluginName name of the plugin to retrieve the namespace for
|
||||
* @return string plugin namespace
|
||||
*/
|
||||
public function getPluginNamespace(?string $extensionName, ?string $pluginName): string
|
||||
{
|
||||
// todo: with $extensionName and $pluginName being null, tx__ will be returned here which is questionable.
|
||||
// find out, if and why this case could happen and maybe avoid this methods being called with null
|
||||
// arguments afterwards.
|
||||
$pluginSignature = strtolower($extensionName . '_' . $pluginName);
|
||||
$defaultPluginNamespace = 'tx_' . $pluginSignature;
|
||||
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, $extensionName, $pluginName);
|
||||
if (!isset($frameworkConfiguration['view']['pluginNamespace']) || empty($frameworkConfiguration['view']['pluginNamespace'])) {
|
||||
return $defaultPluginNamespace;
|
||||
}
|
||||
return $frameworkConfiguration['view']['pluginNamespace'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through the global TypoScript configuration and returns the name of the plugin
|
||||
* that matches specified extensionName, controllerName and actionName.
|
||||
* If no matching plugin was found, NULL is returned.
|
||||
* If more than one plugin matches and the current plugin is not configured to handle the action,
|
||||
* an Exception will be thrown
|
||||
*
|
||||
* @param string $extensionName name of the target extension (UpperCamelCase)
|
||||
* @param string $controllerName name of the target controller (UpperCamelCase)
|
||||
* @param string|null $actionName name of the target action (lowerCamelCase)
|
||||
* @return string|null name of the target plugin (UpperCamelCase) or NULL if no matching plugin configuration was found
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getPluginNameByAction(string $extensionName, string $controllerName, ?string $actionName): ?string
|
||||
{
|
||||
// check, whether the current plugin is configured to handle the action
|
||||
if (($pluginName = $this->getPluginNameFromFrameworkConfiguration($extensionName, $controllerName, $actionName)) !== null) {
|
||||
return $pluginName;
|
||||
}
|
||||
|
||||
$plugins = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'] ?? false;
|
||||
if (!$plugins) {
|
||||
return null;
|
||||
}
|
||||
$pluginNames = [];
|
||||
foreach ($plugins as $pluginName => $pluginConfiguration) {
|
||||
$controllers = $pluginConfiguration['controllers'] ?? [];
|
||||
$controllerAliases = array_column($controllers, 'actions', 'alias');
|
||||
|
||||
foreach ($controllerAliases as $pluginControllerName => $pluginControllerActions) {
|
||||
if (strtolower($pluginControllerName) !== strtolower($controllerName)) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($actionName, $pluginControllerActions, true)) {
|
||||
$pluginNames[] = $pluginName;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count($pluginNames) > 1) {
|
||||
throw new Exception('There is more than one plugin that can handle this request (Extension: "' . $extensionName . '", Controller: "' . $controllerName . '", action: "' . $actionName . '"). Please specify "pluginName" argument', 1280825466);
|
||||
}
|
||||
return !empty($pluginNames) ? $pluginNames[0] : null;
|
||||
}
|
||||
|
||||
private function getPluginNameFromFrameworkConfiguration(string $extensionName, string $controllerAlias, ?string $actionName): ?string
|
||||
{
|
||||
if ($actionName === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
|
||||
if (!is_string($pluginName = ($frameworkConfiguration['pluginName'] ?? null))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$configuredExtensionName = $frameworkConfiguration['extensionName'] ?? '';
|
||||
$configuredExtensionName = is_string($configuredExtensionName) ? $configuredExtensionName : '';
|
||||
|
||||
if ($configuredExtensionName === '' || $configuredExtensionName !== $extensionName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$configuredControllers = $frameworkConfiguration['controllerConfiguration'] ?? [];
|
||||
$configuredControllers = is_array($configuredControllers) ? $configuredControllers : [];
|
||||
|
||||
$configuredActionsByControllerAliases = array_column($configuredControllers, 'actions', 'alias');
|
||||
|
||||
$actions = $configuredActionsByControllerAliases[$controllerAlias] ?? [];
|
||||
$actions = is_array($actions) ? $actions : [];
|
||||
|
||||
return in_array($actionName, $actions, true) ? $pluginName : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the target page of the specified plugin.
|
||||
* If plugin.tx_$pluginSignature.view.defaultPid is set, this value is used as target page id
|
||||
* If defaultPid is set to "auto", the target pid is determined by loading the tt_content record that contains this plugin
|
||||
* If the page could not be determined, NULL is returned
|
||||
* If defaultPid is "auto" and more than one page contains the specified plugin, an Exception is thrown
|
||||
*
|
||||
* @param string $extensionName name of the extension to retrieve the target PID for
|
||||
* @param string $pluginName name of the plugin to retrieve the target PID for
|
||||
* @return int|null uid of the target page or NULL if target page could not be determined
|
||||
*@throws Exception
|
||||
*/
|
||||
public function getTargetPidByPlugin(string $extensionName, string $pluginName): ?int
|
||||
{
|
||||
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, $extensionName, $pluginName);
|
||||
if (!isset($frameworkConfiguration['view']['defaultPid']) || empty($frameworkConfiguration['view']['defaultPid'])) {
|
||||
return null;
|
||||
}
|
||||
$pluginSignature = strtolower($extensionName . '_' . $pluginName);
|
||||
if ($frameworkConfiguration['view']['defaultPid'] === 'auto') {
|
||||
if (!array_key_exists($pluginSignature, $this->targetPidPluginCache)) {
|
||||
$languageId = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('language', 'id', 0);
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tt_content');
|
||||
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
|
||||
|
||||
$pages = $queryBuilder
|
||||
->select('pid')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'CType',
|
||||
$queryBuilder->createNamedParameter($pluginSignature)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->setMaxResults(2)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
if (count($pages) > 1) {
|
||||
throw new Exception('There is more than one "' . $pluginSignature . '" plugin in the current page tree. Please remove one plugin or set the TypoScript configuration "plugin.tx_' . $pluginSignature . '.view.defaultPid" to a fixed page id', 1280773643);
|
||||
}
|
||||
$this->targetPidPluginCache[$pluginSignature] = !empty($pages) ? (int)$pages[0]['pid'] : null;
|
||||
}
|
||||
return $this->targetPidPluginCache[$pluginSignature];
|
||||
}
|
||||
return (int)$frameworkConfiguration['view']['defaultPid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the name of the first controller of the given plugin.
|
||||
*
|
||||
* @param string $extensionName name of the extension to retrieve the target PID for
|
||||
* @param string $pluginName name of the plugin to retrieve the target PID for
|
||||
*/
|
||||
public function getDefaultControllerNameByPlugin(string $extensionName, string $pluginName): ?string
|
||||
{
|
||||
$controllers = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'] ?? [];
|
||||
$controllerAliases = array_column($controllers, 'alias');
|
||||
$defaultControllerName = (string)($controllerAliases[0] ?? '');
|
||||
return $defaultControllerName !== '' ? $defaultControllerName : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the name of the first action of the given plugin controller.
|
||||
*
|
||||
* @param string $extensionName name of the extension to retrieve the target PID for
|
||||
* @param string $pluginName name of the plugin to retrieve the target PID for
|
||||
* @param string $controllerName name of the controller to retrieve default action for
|
||||
*/
|
||||
public function getDefaultActionNameByPluginAndController(string $extensionName, string $pluginName, string $controllerName): ?string
|
||||
{
|
||||
$controllers = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'] ?? [];
|
||||
$controllerActionsByAlias = array_column($controllers, 'actions', 'alias');
|
||||
$actions = $controllerActionsByAlias[$controllerName] ?? [];
|
||||
$defaultActionName = (string)($actions[0] ?? '');
|
||||
return $defaultActionName !== '' ? $defaultActionName : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the page type number to use for building a link for a specific format
|
||||
*
|
||||
* @param string|null $extensionName name of the extension that has defined the target page type
|
||||
* @param string $format The format for which to look up the page type
|
||||
* @return int Page type number for target page
|
||||
*/
|
||||
public function getTargetPageTypeByFormat(?string $extensionName, string $format): int
|
||||
{
|
||||
// Default behaviour
|
||||
$settings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, $extensionName);
|
||||
$formatToPageTypeMapping = $settings['view']['formatToPageTypeMapping'] ?? [];
|
||||
$formatToPageTypeMapping = is_array($formatToPageTypeMapping) ? $formatToPageTypeMapping : [];
|
||||
return (int)($formatToPageTypeMapping[$format] ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Service;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Http\UploadedFile;
|
||||
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Resource\ResourceInstructionTrait;
|
||||
use TYPO3\CMS\Core\Resource\StorageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||
use TYPO3\CMS\Extbase\Event\Service\ModifyUploadedFileTargetFilenameEvent;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\Argument;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\Arguments;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\FileUploadConfiguration;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\FileUploadDeletionConfiguration;
|
||||
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
|
||||
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
|
||||
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
|
||||
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
|
||||
|
||||
/**
|
||||
* @internal Only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class FileHandlingService
|
||||
{
|
||||
use ResourceInstructionTrait;
|
||||
|
||||
public const DELETE_IDENTIFIER = '@delete';
|
||||
|
||||
public function __construct(
|
||||
protected ReflectionService $reflectionService,
|
||||
protected ResourceFactory $resourceFactory,
|
||||
protected StorageRepository $storageRepository,
|
||||
protected DataMapFactory $dataMapFactory,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected HashService $hashService,
|
||||
protected ExtensionService $extensionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initializes file upload configurations for all FileUpload properties of the argument. Note, that this is only
|
||||
* applied for the HTTP method POST.
|
||||
*/
|
||||
public function initializeFileUploadConfigurationsFromRequest(
|
||||
RequestInterface $request,
|
||||
Arguments $arguments
|
||||
): void {
|
||||
if ($request->getMethod() !== 'POST' || $arguments->count() === 0) {
|
||||
return;
|
||||
}
|
||||
/** @var Argument $argument */
|
||||
foreach ($arguments as $argument) {
|
||||
if (!$argument->getValidator()
|
||||
|| !class_exists($argument->getDataType())
|
||||
) {
|
||||
// Either argument has no validator (IgnoreValidation) or the datatype of the argument is not a class.
|
||||
continue;
|
||||
}
|
||||
|
||||
$dataType = GeneralUtility::getClassName($argument->getDataType());
|
||||
$classSchema = $this->reflectionService->getClassSchema($dataType);
|
||||
foreach ($classSchema->getProperties() as $property) {
|
||||
$this->addUploadConfigurationForProperty($argument, $property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new upload configuration for the given property to the given argument.
|
||||
*/
|
||||
private function addUploadConfigurationForProperty(
|
||||
Argument $argument,
|
||||
Property $property
|
||||
): void {
|
||||
$primaryType = $property->getPrimaryType();
|
||||
if (!$primaryType) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf(
|
||||
'There is no @var annotation or type declaration for file upload property "%s" in class "%s".',
|
||||
$property->getName(),
|
||||
$argument->getDataType()
|
||||
),
|
||||
1712309171
|
||||
);
|
||||
}
|
||||
|
||||
$propertyTargetClassName = $primaryType->getClassName() ?? $primaryType->getBuiltinType();
|
||||
if ($propertyTargetClassName !== FileReference::class
|
||||
&& !TypeHandlingUtility::isSimpleType($propertyTargetClassName)
|
||||
) {
|
||||
$primaryCollectionValueType = $property->getPrimaryCollectionValueType();
|
||||
if ($propertyTargetClassName === ObjectStorage::class
|
||||
&& $primaryCollectionValueType
|
||||
&& $primaryType->isCollection()
|
||||
) {
|
||||
$propertyTargetClassName = $primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType();
|
||||
}
|
||||
}
|
||||
|
||||
// Skip unsupported classes for #[FileUpload] attribute or properties with empty FileUpload configuration
|
||||
if ($propertyTargetClassName !== FileReference::class || $property->getFileUpload() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fileUploadConfiguration = $property->getFileUpload();
|
||||
$configurationPropertyName = $property->getName();
|
||||
|
||||
$configuration = (new FileUploadConfiguration($configurationPropertyName))
|
||||
->initializeWithConfiguration($fileUploadConfiguration);
|
||||
$configuration->ensureValidConfiguration($propertyTargetClassName);
|
||||
|
||||
$argument->getFileHandlingServiceConfiguration()->addFileUploadConfiguration($configuration);
|
||||
|
||||
// If FileUpload is configured, the property mapping must be skipped
|
||||
$argument->getPropertyMappingConfiguration()->skipProperties($configurationPropertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes file deletion configurations for properties of the given argument.
|
||||
*/
|
||||
public function initializeFileUploadDeletionConfigurationsFromRequest(
|
||||
RequestInterface $request,
|
||||
Arguments $arguments
|
||||
): void {
|
||||
if ($request->getMethod() !== 'POST' || $arguments->count() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pluginNamespace = $this->extensionService->getPluginNamespace(
|
||||
$request->getControllerExtensionName(),
|
||||
$request->getPluginName()
|
||||
);
|
||||
$fileDeletions = $request->getParsedBody()[$pluginNamespace][self::DELETE_IDENTIFIER] ?? [];
|
||||
|
||||
// In case of validation errors, file deletions must not be processed
|
||||
if ($fileDeletions === [] || $this->hasMappingErrorOccurred($request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Argument $argument */
|
||||
foreach ($arguments as $argument) {
|
||||
if (isset($fileDeletions[$argument->getName()]) && is_array($fileDeletions[$argument->getName()])) {
|
||||
$this->addDeletionConfigurationsToArgument($argument, $fileDeletions[$argument->getName()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps and persists (if required) uploaded files for the given argument.
|
||||
*/
|
||||
public function mapUploadedFilesToArgument(Argument $argument): void
|
||||
{
|
||||
foreach ($argument->getFileHandlingServiceConfiguration()->getFileUploadConfigurations() as $configuration) {
|
||||
$this->mapUploadedFilesToArgumentForConfiguration($argument, $configuration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps uploaded files to the argument for configuration.
|
||||
*
|
||||
* Maps uploaded files to the specified property of the argument, if property is allowed in current
|
||||
* PropertyMappingConfiguration
|
||||
*/
|
||||
private function mapUploadedFilesToArgumentForConfiguration(
|
||||
Argument $argument,
|
||||
FileUploadConfiguration $configuration
|
||||
): void {
|
||||
$propertyName = $configuration->getPropertyName();
|
||||
|
||||
if ($this->shouldMapProperty($argument, $propertyName)) {
|
||||
$argumentValue = $argument->getValue();
|
||||
$uploadedFiles = $argument->getUploadedFilesForProperty($propertyName);
|
||||
$this->mapUploadedFilesToArgumentForProperty(
|
||||
$argumentValue,
|
||||
$propertyName,
|
||||
$uploadedFiles,
|
||||
$configuration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps uploaded files to the specified property of the object, based on the provided configuration.
|
||||
*/
|
||||
private function mapUploadedFilesToArgumentForProperty(
|
||||
mixed $argumentValue,
|
||||
string $propertyName,
|
||||
array $uploadedFiles,
|
||||
FileUploadConfiguration $configuration
|
||||
): void {
|
||||
if ($uploadedFiles === []
|
||||
|| !ObjectAccess::isPropertyGettable($argumentValue, $propertyName)
|
||||
|| !ObjectAccess::isPropertySettable($argumentValue, $propertyName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$classSchema = $this->reflectionService->getClassSchema($argumentValue);
|
||||
$property = $classSchema->getProperty($propertyName);
|
||||
if (!$property->getPrimaryType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isObjectStorage = $property->isObjectStorageType();
|
||||
$targetType = $isObjectStorage ? $property->getPrimaryCollectionValueType()->getClassName() : $property->getPrimaryType()->getClassName();
|
||||
|
||||
if ($targetType === FileReference::class) {
|
||||
$configuration->ensureValidConfiguration($targetType);
|
||||
$this->persistUploadedFilesAndMapAsFileReferencesToProperty(
|
||||
$argumentValue,
|
||||
$propertyName,
|
||||
$isObjectStorage,
|
||||
$configuration,
|
||||
$uploadedFiles
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves PSR-7 uploaded files to the target storage defined in the given file upload configuration.
|
||||
*
|
||||
* For target property type FileReference, either a new FileReference object is created or a possible existing
|
||||
* FileReference object is reused and the uploaded file is set.
|
||||
*
|
||||
* For target property type ObjectStorage<FileReference>, new FileReference objects are created and attached
|
||||
* to the property.
|
||||
*/
|
||||
private function persistUploadedFilesAndMapAsFileReferencesToProperty(
|
||||
mixed $argumentValue,
|
||||
string $propertyName,
|
||||
bool $isObjectStorage,
|
||||
FileUploadConfiguration $configuration,
|
||||
array $uploadedFiles,
|
||||
): void {
|
||||
$uploadFolder = $this->provideUploadFolder($configuration);
|
||||
$storage = $uploadFolder->getStorage();
|
||||
|
||||
if ($isObjectStorage) {
|
||||
/** @var ObjectStorage $currentPropertyValue */
|
||||
$currentPropertyValue = ObjectAccess::getProperty($argumentValue, $propertyName);
|
||||
|
||||
foreach ($uploadedFiles as $uploadedFile) {
|
||||
$targetFilename = $this->getTargetFilename($uploadedFile->getClientFilename(), $configuration);
|
||||
$this->skipResourceConsistencyCheckForUploads($storage, $uploadedFile, $targetFilename);
|
||||
$file = $storage->addUploadedFile($uploadedFile, $uploadFolder, $targetFilename, $configuration->getDuplicationBehavior());
|
||||
$coreFileReference = $this->createCoreFileReference($file);
|
||||
$fileReference = $this->createExtbaseFileReference($coreFileReference);
|
||||
$currentPropertyValue->attach($fileReference);
|
||||
}
|
||||
} else {
|
||||
/** @var UploadedFile $uploadedFile */
|
||||
$uploadedFile = $uploadedFiles[0];
|
||||
$targetFilename = $this->getTargetFilename($uploadedFile->getClientFilename(), $configuration);
|
||||
$this->skipResourceConsistencyCheckForUploads($storage, $uploadedFile, $targetFilename);
|
||||
$file = $storage->addUploadedFile($uploadedFile, $uploadFolder, $targetFilename, $configuration->getDuplicationBehavior());
|
||||
$coreFileReference = $this->createCoreFileReference($file);
|
||||
|
||||
/** @var FileReference|null $currentPropertyValue */
|
||||
$currentPropertyValue = ObjectAccess::getProperty($argumentValue, $propertyName);
|
||||
|
||||
if ($currentPropertyValue) {
|
||||
$currentPropertyValue->setOriginalResource($coreFileReference);
|
||||
} else {
|
||||
$currentPropertyValue = $this->createExtbaseFileReference($coreFileReference);
|
||||
}
|
||||
}
|
||||
|
||||
ObjectAccess::setProperty($argumentValue, $propertyName, $currentPropertyValue);
|
||||
}
|
||||
|
||||
private function addDeletionConfigurationsToArgument(Argument $argument, array $fileDeletions): void
|
||||
{
|
||||
foreach ($fileDeletions as $signedDeletionData) {
|
||||
$deletionData = $this->hashService->validateAndStripHmac(
|
||||
$signedDeletionData,
|
||||
self::DELETE_IDENTIFIER
|
||||
);
|
||||
$deletionData = json_decode($deletionData, true, 512, JSON_THROW_ON_ERROR);
|
||||
$fileReferenceUid = (int)$deletionData['fileReference'];
|
||||
$property = $deletionData['property'];
|
||||
|
||||
$argumentValue = $argument->getValue();
|
||||
$propertyValue = ObjectAccess::getPropertyPath($argumentValue, $property);
|
||||
|
||||
if ($propertyValue instanceof FileReference) {
|
||||
if ($propertyValue->getUid() === $fileReferenceUid) {
|
||||
$argument->getFileHandlingServiceConfiguration()
|
||||
->registerFileDeletion($property, $fileReferenceUid);
|
||||
}
|
||||
} elseif ($propertyValue instanceof ObjectStorage) {
|
||||
foreach ($propertyValue as $fileReference) {
|
||||
if ($fileReference instanceof FileReference && $fileReference->getUid() === $fileReferenceUid) {
|
||||
$argument->getFileHandlingServiceConfiguration()
|
||||
->registerFileDeletion($property, $fileReferenceUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function applyDeletionsToArgument(Argument $argument): void
|
||||
{
|
||||
$fileUploadDeletionConfigurations = $argument->getFileHandlingServiceConfiguration()
|
||||
->getFileUploadDeletionConfigurations();
|
||||
|
||||
/** @var FileUploadDeletionConfiguration $fileUploadDeletionConfiguration */
|
||||
foreach ($fileUploadDeletionConfigurations as $fileUploadDeletionConfiguration) {
|
||||
$property = $fileUploadDeletionConfiguration->getPropertyName();
|
||||
foreach ($fileUploadDeletionConfiguration->getFileReferenceUids() as $fileReferenceUid) {
|
||||
$argumentValue = $argument->getValue();
|
||||
$propertyValue = ObjectAccess::getPropertyPath($argumentValue, $property);
|
||||
|
||||
if ($propertyValue instanceof FileReference) {
|
||||
if ($propertyValue->getUid() === $fileReferenceUid) {
|
||||
$propertyValue->getOriginalResource()->getOriginalFile()->delete();
|
||||
$propertyValue->getOriginalResource()->delete();
|
||||
ObjectAccess::setProperty($argumentValue, $property, null);
|
||||
}
|
||||
} elseif ($propertyValue instanceof ObjectStorage) {
|
||||
foreach ($propertyValue as $fileReference) {
|
||||
if ($fileReference instanceof FileReference && $fileReference->getUid() === $fileReferenceUid) {
|
||||
$propertyValue->detach($fileReference);
|
||||
$fileReference->getOriginalResource()->getOriginalFile()->delete();
|
||||
$fileReference->getOriginalResource()->delete();
|
||||
}
|
||||
}
|
||||
ObjectAccess::setProperty($argumentValue, $property, $propertyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a property mapping error has occurred in given request.
|
||||
*/
|
||||
private function hasMappingErrorOccurred(RequestInterface $request): bool
|
||||
{
|
||||
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = $request->getAttribute('extbase');
|
||||
return $extbaseRequestParameters->getOriginalRequest() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a property should be mapped for the given argument and property name.
|
||||
*/
|
||||
private function shouldMapProperty(Argument $argument, string $propertyName): bool
|
||||
{
|
||||
if ($propertyName === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $argument->getPropertyMappingConfiguration()->shouldMap($propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target filename to use for the given client filename provided by the file upload.
|
||||
*/
|
||||
private function getTargetFilename(string $clientFilename, FileUploadConfiguration $configuration): string
|
||||
{
|
||||
$targetFilename = $clientFilename;
|
||||
|
||||
if ($configuration->isAddRandomSuffix()) {
|
||||
$pathInfo = pathinfo($targetFilename);
|
||||
$name = $pathInfo['filename'];
|
||||
$extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
|
||||
$randomSuffix = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(16);
|
||||
$targetFilename = $name . '-' . $randomSuffix . $extension;
|
||||
}
|
||||
|
||||
$event = new ModifyUploadedFileTargetFilenameEvent(
|
||||
targetFilename: $targetFilename,
|
||||
configuration: $configuration
|
||||
);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
return $event->getTargetFilename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that upload folder exists, creates it if it does not and if automatic folder creation is defined
|
||||
* @throws FolderDoesNotExistException
|
||||
*/
|
||||
private function provideUploadFolder(FileUploadConfiguration $configuration): Folder
|
||||
{
|
||||
$uploadFolderIdentifier = $configuration->getUploadFolder();
|
||||
try {
|
||||
return $this->resourceFactory->getFolderObjectFromCombinedIdentifier($uploadFolderIdentifier);
|
||||
} catch (FolderDoesNotExistException $exception) {
|
||||
if (!$configuration->isCreateUploadFolderIfNotExist()) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
[$storageId, $storagePath] = explode(':', $uploadFolderIdentifier, 2);
|
||||
$storage = $this->storageRepository->getStorageObject((int)$storageId);
|
||||
|
||||
if (!$storage->hasFolder($storagePath)) {
|
||||
$folder = $storage->createFolder($storagePath);
|
||||
} else {
|
||||
$folder = $storage->getFolder($storagePath);
|
||||
}
|
||||
|
||||
return $folder;
|
||||
}
|
||||
}
|
||||
|
||||
private function createCoreFileReference(FileInterface $file): CoreFileReference
|
||||
{
|
||||
if (!$file instanceof File) {
|
||||
throw new \RuntimeException('Given file must be a TYPO3\\CMS\\Core\\Resource.', 1712062607);
|
||||
}
|
||||
|
||||
return $this->resourceFactory->createFileReferenceObject(
|
||||
[
|
||||
'uid_local' => $file->getUid(),
|
||||
'uid_foreign' => StringUtility::getUniqueId('NEW_'),
|
||||
'uid' => StringUtility::getUniqueId('NEW_'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function createExtbaseFileReference(
|
||||
CoreFileReference $falFileReference
|
||||
): FileReference {
|
||||
$fileReference = GeneralUtility::makeInstance(FileReference::class);
|
||||
$fileReference->setOriginalResource($falFileReference);
|
||||
return $fileReference;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Service;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Http\ApplicationType;
|
||||
use TYPO3\CMS\Core\Imaging\ImageResource;
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
use TYPO3\CMS\Core\Page\AssetCollector;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Resource\FileReference;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Service for processing images
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class ImageService
|
||||
{
|
||||
public function __construct(
|
||||
protected ResourceFactory $resourceFactory,
|
||||
protected LinkService $linkService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a processed file
|
||||
*
|
||||
* @param FileInterface|FileReference $image
|
||||
*/
|
||||
public function applyProcessingInstructions($image, array $processingInstructions): ProcessedFile
|
||||
{
|
||||
/*
|
||||
* todo: this method should be split to be able to have a proper method signature.
|
||||
* todo: actually, this method only really works with objects of type \TYPO3\CMS\Core\Resource\File, as this
|
||||
* todo: is the only implementation that supports the support method.
|
||||
*/
|
||||
if (is_callable([$image, 'getOriginalFile'])) {
|
||||
// Get the original file from the file reference
|
||||
$image = $image->getOriginalFile();
|
||||
}
|
||||
|
||||
$processedImage = $image->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingInstructions);
|
||||
$this->setCompatibilityValues($processedImage);
|
||||
|
||||
return $processedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public url of image depending on the environment
|
||||
*
|
||||
* @param bool|false $absolute Force absolute URL
|
||||
*/
|
||||
public function getImageUri(FileInterface $image, bool $absolute = false): string
|
||||
{
|
||||
$imageUrl = $image->getPublicUrl();
|
||||
if (!$absolute || $imageUrl === null) {
|
||||
return (string)$imageUrl;
|
||||
}
|
||||
// @todo: Change method signature in >=v15 to receive $request, probably as first or second argument.
|
||||
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||
return GeneralUtility::locationHeaderUrl($imageUrl, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get File or FileReference object
|
||||
*
|
||||
* This method is a factory and compatibility method that does not belong to
|
||||
* this service, but is put here for pragmatic reasons for the time being.
|
||||
* It should be removed once we do not support string sources for images anymore.
|
||||
*
|
||||
* @param string $src
|
||||
* @param FileInterface|\TYPO3\CMS\Extbase\Domain\Model\FileReference|null $image
|
||||
* @param bool $treatIdAsReference
|
||||
* @throws \UnexpectedValueException
|
||||
* @internal
|
||||
*/
|
||||
public function getImage(string $src, $image, bool $treatIdAsReference): FileInterface
|
||||
{
|
||||
if ($image instanceof File || $image instanceof FileReference) {
|
||||
// We already received a valid file and therefore just return it
|
||||
return $image;
|
||||
}
|
||||
|
||||
if (is_callable([$image, 'getOriginalResource'])) {
|
||||
// We have a domain model, so we need to fetch the FAL resource object from there
|
||||
$originalResource = $image->getOriginalResource();
|
||||
if (!($originalResource instanceof File || $originalResource instanceof FileReference)) {
|
||||
throw new \UnexpectedValueException('No original resource could be resolved for supplied file ' . get_class($image), 1625838481);
|
||||
}
|
||||
return $originalResource;
|
||||
}
|
||||
|
||||
if ($image !== null) {
|
||||
// Some value is given for $image, but it's not a valid type
|
||||
throw new \UnexpectedValueException(
|
||||
'Supplied file must be File or FileReference, ' . get_debug_type($image) . ' given.',
|
||||
1625585157
|
||||
);
|
||||
}
|
||||
|
||||
// Since image is not given, try to resolve an image from the source string
|
||||
$resolvedImage = $this->getImageFromSourceString($src, $treatIdAsReference);
|
||||
|
||||
if ($resolvedImage instanceof File || $resolvedImage instanceof FileReference) {
|
||||
return $resolvedImage;
|
||||
}
|
||||
|
||||
if ($resolvedImage === null) {
|
||||
// No image could be resolved using the given source string
|
||||
throw new \UnexpectedValueException('Supplied ' . $src . ' could not be resolved to a File or FileReference.', 1625585158);
|
||||
}
|
||||
|
||||
// A FileInterface was found, however only File and FileReference are valid
|
||||
throw new \UnexpectedValueException(
|
||||
'Resolved file object type ' . get_class($resolvedImage) . ' for ' . $src . ' must be File or FileReference.',
|
||||
1382687163
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get File or FileReference object by src
|
||||
*/
|
||||
protected function getImageFromSourceString(string $src, bool $treatIdAsReference): ?FileInterface
|
||||
{
|
||||
if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
|
||||
&& ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()
|
||||
&& str_starts_with($src, '../')
|
||||
) {
|
||||
$src = substr($src, 3);
|
||||
}
|
||||
if (MathUtility::canBeInterpretedAsInteger($src)) {
|
||||
if ($treatIdAsReference) {
|
||||
$image = $this->resourceFactory->getFileReferenceObject((int)$src);
|
||||
} else {
|
||||
$image = $this->resourceFactory->getFileObject($src);
|
||||
}
|
||||
} elseif (str_starts_with($src, 't3://file')) {
|
||||
// We have a t3://file link to a file in FAL
|
||||
$data = $this->linkService->resolveByStringRepresentation($src);
|
||||
$image = $data['file'];
|
||||
} else {
|
||||
// We have a combined identifier or legacy (storage 0) path
|
||||
$image = $this->resourceFactory->retrieveFileOrFolderObject($src);
|
||||
}
|
||||
|
||||
// Check the resolved image as this could also be a FolderInterface
|
||||
return $image instanceof FileInterface ? $image : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set compatibility values in case we are in frontend environment.
|
||||
*/
|
||||
protected function setCompatibilityValues(ProcessedFile $processedImage): void
|
||||
{
|
||||
$imageResource = ImageResource::createFromProcessedFile($processedImage);
|
||||
if ($imageResource->getPublicUrl() !== null) {
|
||||
// only add the processed image to AssetCollector if the public url is not NULL
|
||||
GeneralUtility::makeInstance(AssetCollector::class)->addMedia(
|
||||
$imageResource->getPublicUrl(),
|
||||
$imageResource->getLegacyImageResourceInformation()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user