TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:27 +02:00
commit 44c503b2d1
233 changed files with 31556 additions and 0 deletions
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Aspect;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Resource\Event\EnrichFileMetaDataEvent;
/**
* This class deals with metadata translation as an event listener which reacts on an event MetadataRepository.
*
* The listener injects user permissions and mount points into the storage
* based on user or group configuration.
*
* @internal this is a concrete TYPO3 Event Listener and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
final readonly class FileMetadataOverlayAspect
{
public function __construct(
private PageRepository $pageRepository,
) {}
/**
* Do translation and workspace overlay
*/
#[AsEventListener('typo3-frontend/overlay')]
public function languageAndWorkspaceOverlay(EnrichFileMetaDataEvent $event): void
{
// Should only be in Frontend, but not in eID context
if (!($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
|| !ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
|| isset($_REQUEST['eID'])
) {
return;
}
$overlaidMetaData = $event->getRecord();
$this->pageRepository->versionOL('sys_file_metadata', $overlaidMetaData);
// getLanguageOverlay also calls versionOL() on the language overlaid record
$overlaidMetaData = $this->pageRepository->getLanguageOverlay('sys_file_metadata', $overlaidMetaData);
if ($overlaidMetaData !== null) {
$event->setRecord($overlaidMetaData);
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Aspect;
use TYPO3\CMS\Core\Context\AspectInterface;
use TYPO3\CMS\Core\Context\Exception\AspectPropertyNotFoundException;
/**
* The aspect contains information on whether TYPO3 is currently in preview mode
*
* Allowed properties:
* - isPreview
*/
final readonly class PreviewAspect implements AspectInterface
{
public function __construct(
private bool $isPreview = false
) {}
public function isPreview(): bool
{
return $this->isPreview;
}
/**
* Get a property from aspect
*
* @throws AspectPropertyNotFoundException
*/
public function get(string $name): bool
{
if ($name == 'isPreview') {
return $this->isPreview;
}
throw new AspectPropertyNotFoundException('Property "' . $name . '" not found in Aspect "' . __CLASS__ . '".', 1563375558);
}
}
@@ -0,0 +1,96 @@
<?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\Frontend\Authentication;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* TYPO3 backend user authentication in the Frontend rendering.
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class FrontendBackendUserAuthentication extends BackendUserAuthentication
{
/**
* Form field with login name.
*
* @var string
* @internal
*/
protected $formfield_uname = '';
/**
* Form field with password.
*
* @var string
* @internal
*/
protected $formfield_uident = '';
/**
* Formfield_status should be set to "". The value this->formfield_status is set to empty in order to
* disable login-attempts to the backend account through this script
*
* @var string
* @internal
*/
protected $formfield_status = '';
/**
* Decides if the writelog() function is called at login and logout.
*
* @var bool
*/
public $writeStdLog = false;
/**
* If the writelog() functions is called if a login-attempt has be tried without success.
*
* @var bool
*/
public $writeAttemptLog = false;
/**
* Implementing the access checks that the TYPO3 CMS bootstrap script does before a user is ever logged in.
* Used in the frontend.
*
* @return bool Returns TRUE if access is OK
*/
public function backendCheckLogin(?ServerRequestInterface $request = null)
{
if (empty($this->user['uid'])) {
return false;
}
// Check Hardcoded lock on BE
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'] < 0) {
return false;
}
return $this->isUserAllowedToLogin();
}
/**
* If a user is in a workspace, but previews the live workspace (GET keyword "LIVE") even if the user
* has no editing permissions for this, it should still be visible, even though "be_users.workspace_perms" is set to "0".
* If this ain't true, users without the live permission cannot see the live page, only the preview of the workspace of the user.
*/
protected function hasEditAccessToLiveWorkspace(): bool
{
return true;
}
}
@@ -0,0 +1,502 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Authentication;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
use TYPO3\CMS\Core\Authentication\GroupResolver;
use TYPO3\CMS\Core\Authentication\LoginType;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\SetCookieService;
use TYPO3\CMS\Core\Session\UserSession;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Extension class for Front End User Authentication.
*/
class FrontendUserAuthentication extends AbstractUserAuthentication
{
/**
* Login type, used for services.
* @var string
*/
public $loginType = 'FE';
/**
* Form field with login-name
* @var string
* @internal
*/
protected $formfield_uname = 'user';
/**
* Form field with password
* @var string
* @internal
*/
protected $formfield_uident = 'pass';
/**
* Form field with status: *'login', 'logout'. If empty login is not verified.
* @var string
* @internal
*/
protected $formfield_status = 'logintype';
/**
* form field with 0 or 1
* 1 = permanent login enabled
* 0 = session is valid for a browser session only
* @var string
* @internal
*/
protected $formfield_permanent = 'permalogin';
/**
* Table in database with user data
* @var string
*/
public $user_table = 'fe_users';
/**
* Column for login-name
* @var string
*/
public $username_column = 'username';
/**
* Column for password
* @var string
*/
public $userident_column = 'password';
/**
* Column for user-id
* @var string
*/
public $userid_column = 'uid';
/**
* Column name for last login timestamp
* @var string
* @internal
*/
protected $lastLogin_column = 'lastlogin';
/**
* @var string
*/
public $usergroup_column = 'usergroup';
/**
* @var string
*/
public $usergroup_table = 'fe_groups';
/**
* Enable field columns of user table
* @var array
*/
public $enablecolumns = [
'deleted' => 'deleted',
'disabled' => 'disable',
'starttime' => 'starttime',
'endtime' => 'endtime',
];
/**
* @var array
*/
public $groupData = [
'title' => [],
'uid' => [],
'pid' => [],
];
/**
* @var bool
*/
protected $userData_change = false;
/**
* @var bool
* @internal
*/
protected $is_permanent = false;
/**
* Will force the session cookie to be set every time (lifetime must be 0).
* @var bool
*/
protected $forceSetCookie = false;
/**
* Will prevent the setting of the session cookie (takes precedence over forceSetCookie)
* Disable cookie by default, will be activated if saveSessionData() is called,
* a user is logging-in or an existing session is found
* @var bool
* @internal
*/
protected $dontSetCookie = true;
public function __construct()
{
$this->name = self::getCookieName();
parent::__construct();
$this->checkPid = (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'] ?? true);
}
/**
* Returns the configured cookie name
*/
public static function getCookieName(): string
{
$configuredCookieName = trim((string)($GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName'] ?? ''));
return $configuredCookieName !== '' ? $configuredCookieName : 'fe_typo_user';
}
/**
* Determine whether a session cookie needs to be set (lifetime=0)
*
* @return bool
* @internal
*/
protected function isSetSessionCookie()
{
return SetCookieService::create($this->name, $this->loginType)->isSetSessionCookie($this->userSession, $this->forceSetCookie);
}
/**
* Determine whether a non-session cookie needs to be set (lifetime>0)
*
* @return bool
* @internal
*/
protected function isRefreshTimeBasedCookie()
{
return SetCookieService::create($this->name, $this->loginType)->isRefreshTimeBasedCookie($this->userSession);
}
/**
* Returns an info array with Login/Logout data submitted by a form or params
*
* @return array
* @see AbstractUserAuthentication::getLoginFormData()
*/
public function getLoginFormData(ServerRequestInterface $request)
{
$loginData = parent::getLoginFormData($request);
// Needed in order to fetch users which are already logged-in due to fetching from session
if (LoginType::tryFrom($loginData['status'] ?? '') !== LoginType::LOGIN) {
$this->checkPid_value = null;
}
if ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 0 || $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 1) {
$isPermanent = $request->getParsedBody()[$this->formfield_permanent] ?? '';
if (strlen((string)$isPermanent) != 1) {
$isPermanent = $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'];
} elseif (!$isPermanent) {
// To make sure the user gets a session cookie and doesn't keep a possibly existing time based cookie,
// we need to force setting the session cookie here
$this->forceSetCookie = true;
}
$isPermanent = (bool)$isPermanent;
} elseif ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 2) {
$isPermanent = true;
} else {
$isPermanent = false;
}
$loginData['permanent'] = $isPermanent;
$this->is_permanent = $isPermanent;
return $loginData;
}
/**
* Creates a user session record and returns its values.
* However, as the FE user cookie is normally not set, this has to be done
* before the parent class is doing the rest.
*
* @param array $tempuser User data array
* @return UserSession The session data for the newly created session.
*/
public function createUserSession(array $tempuser): UserSession
{
// At this point we do not know if we need to set a session or a permanent cookie
// So we force the cookie to be set after authentication took place, which will
// then call setSessionCookie(), which will set a cookie with correct settings.
$this->dontSetCookie = false;
$tempUserId = (int)($tempuser[$this->userid_column] ?? 0);
$session = $this->userSessionManager->elevateToFixatedUserSession(
$this->userSession,
$tempUserId,
(bool)$this->is_permanent
);
// Updating lastLogin_column carrying information about last login.
$this->updateLoginTimestamp($tempUserId);
return $session;
}
/**
* Will select all fe_groups records that the current fe_user is member of.
*
* @param ServerRequestInterface $request
*/
public function fetchGroupData(ServerRequestInterface $request)
{
$this->userGroups = [];
$this->groupData = [
'title' => [],
'uid' => [],
'pid' => [],
];
$groupDataArr = [];
if (is_array($this->user)) {
$this->logger->debug('Get usergroups for user', [
$this->userid_column => $this->getUserId(),
$this->username_column => $this->getUserName(),
]);
$groupDataArr = GeneralUtility::makeInstance(GroupResolver::class)->resolveGroupsForUser($this->user, $this->usergroup_table);
}
// Fire an event for any kind of user (even when no specific user is here, using hideLogin feature)
$dispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
$event = $dispatcher->dispatch(new ModifyResolvedFrontendGroupsEvent($this, $groupDataArr, $request));
$groupDataArr = $event->getGroups();
if (empty($groupDataArr)) {
$this->logger->debug('No usergroups found');
} else {
$this->logger->debug('{count} usergroup records found', ['count' => count($groupDataArr)]);
}
foreach ($groupDataArr as $groupData) {
$groupId = (int)$groupData['uid'];
$this->groupData['title'][$groupId] = $groupData['title'] ?? '';
$this->groupData['uid'][$groupId] = $groupData['uid'] ?? 0;
$this->groupData['pid'][$groupId] = $groupData['pid'] ?? 0;
$this->userGroups[$groupId] = $groupData;
}
// Sort information
ksort($this->groupData['title']);
ksort($this->groupData['uid']);
ksort($this->groupData['pid']);
}
/**
* Initializes the front-end user groups for the context API,
* based on the user groups and the logged-in state.
*
* @param bool $respectUserGroups used to disable the inclusion of the users' groups
*/
public function createUserAspect(bool $respectUserGroups = true): UserAspect
{
$userGroups = [0];
$isUserAndGroupSet = is_array($this->user) && !empty($this->userGroups);
if ($isUserAndGroupSet) {
// group -2 is not an existing group, but denotes a 'default' group when a user IS logged in.
// This is used to let elements be shown for all logged in users!
$userGroups[] = -2;
$groupsFromUserRecord = array_keys($this->userGroups);
} else {
// group -1 is not an existing group, but denotes a 'default' group when not logged in.
// This is used to let elements be hidden, when a user is logged in!
$userGroups[] = -1;
if ($respectUserGroups) {
// For cases where logins are not banned from a branch usergroups can be set based on IP masks so we should add the usergroups uids.
$groupsFromUserRecord = array_keys($this->userGroups);
} else {
// Set to blank since we will NOT risk any groups being set when no logins are allowed!
$groupsFromUserRecord = [];
}
}
// Make unique and sort the groups
$groupsFromUserRecord = array_unique($groupsFromUserRecord);
if ($respectUserGroups && !empty($groupsFromUserRecord)) {
sort($groupsFromUserRecord);
$userGroups = array_merge($userGroups, $groupsFromUserRecord);
}
// For every 60 seconds the is_online timestamp for a logged-in user is updated
if ($isUserAndGroupSet) {
$this->updateOnlineTimestamp();
}
$this->logger->debug('Valid frontend usergroups: {groups}', ['groups' => implode(',', $userGroups)]);
return new UserAspect($this, $userGroups);
}
/*****************************************
*
* Session data management functions
*
****************************************/
/**
* Will write UC and session data.
* If the flag $this->userData_change has been set, the function ->writeUC is called (which will save persistent user session data)
*
* @see getKey()
* @see setKey()
*/
public function storeSessionData()
{
// Saves UC and SesData if changed.
if ($this->userData_change) {
$this->writeUC();
}
if ($this->userSession->dataWasUpdated()) {
if (!$this->userSession->hasData()) {
// Remove session-data
$this->removeSessionData();
// Remove cookie if not logged in as the session data is removed as well
if (empty($this->user['uid']) && $this->isCookieSet()) {
$this->removeCookie();
}
} elseif (!$this->userSessionManager->isSessionPersisted($this->userSession)) {
// Create a new session entry in the backend
$this->userSession = $this->userSessionManager->fixateAnonymousSession($this->userSession, (bool)$this->is_permanent);
// Now set the cookie (= fix the session)
$this->setSessionCookie();
} else {
// Update session data of an already fixated session
$this->userSession = $this->userSessionManager->updateSession($this->userSession);
}
}
}
/**
* Removes data of the current session.
*/
public function removeSessionData()
{
$this->userSession->overrideData([]);
if ($this->userSessionManager->isSessionPersisted($this->userSession)) {
// Remove session record if $this->user is empty or in case the session is anonymous
if (empty($this->user) || $this->userSession->isAnonymous()) {
$this->userSessionManager->removeSession($this->userSession);
} else {
$this->userSession = $this->userSessionManager->updateSession($this->userSession);
}
}
}
/**
* Regenerate the session ID and transfer the session to new ID
* Call this method whenever a user proceeds to a higher authorization level
* e.g. when an anonymous session is now authenticated.
* Forces cookie to be set
*/
protected function regenerateSessionId()
{
parent::regenerateSessionId();
// We force the cookie to be set later in the authentication process
$this->dontSetCookie = false;
}
/**
* Returns session data for the fe_user; Either persistent data following the fe_users uid/profile (requires login)
* or current-session based (not available when browse is closed, but does not require login)
*
* @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
* @param string $key Key from the data array to return; The session data (in either case) is an array ($this->uc / $this->sessionData) and this value determines which key to return the value for.
* @return mixed Returns whatever value there was in the array for the key, $key
* @see setKey()
*/
public function getKey($type, $key)
{
if (!$key) {
return null;
}
$value = null;
switch ($type) {
case 'user':
$value = $this->uc[$key] ?? null;
break;
case 'ses':
$value = $this->getSessionData($key);
break;
}
return $value;
}
/**
* Saves session data, either persistent or bound to current session cookie. Please see getKey() for more details.
* When a value is set the flag $this->userData_change will be set so that the final call to ->storeSessionData() will know if a change has occurred and needs to be saved to the database.
* Notice: Simply calling this function will not save the data to the database! The actual saving is done in storeSessionData() which is called as some of the last things in \TYPO3\CMS\Frontend\Http\RequestHandler.
*
* @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
* @param string $key Key from the data array to store incoming data in; The session data (in either case) is an array ($this->uc / $this->sessionData) and this value determines in which key the $data value will be stored.
* @param mixed $data The data value to store in $key
* @see setKey()
* @see storeSessionData()
*/
public function setKey($type, $key, $data)
{
if (!$key) {
return;
}
switch ($type) {
case 'user':
if ($this->user['uid'] ?? 0) {
if ($data === null) {
unset($this->uc[$key]);
} else {
$this->uc[$key] = $data;
}
$this->userData_change = true;
}
break;
case 'ses':
$this->setSessionData($key, $data);
break;
}
}
/**
* Saves the tokens so that they can be used by a later incarnation of this class.
*
* @param string $key
* @param mixed $data
*/
public function setAndSaveSessionData($key, $data)
{
$this->setSessionData($key, $data);
$this->storeSessionData();
}
/**
* Update the field "is_online" every 60 seconds of a logged-in user
*
* @internal
*/
public function updateOnlineTimestamp()
{
if (!is_array($this->user)
|| !($this->user['uid'] ?? 0)
|| $this->user['uid'] === PHP_INT_MAX // Simulated preview user (flagged with PHP_INT_MAX uid)
|| ($this->user['is_online'] ?? 0) >= $GLOBALS['EXEC_TIME'] - 60) {
return;
}
$dbConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table);
$dbConnection->update(
$this->user_table,
['is_online' => $GLOBALS['EXEC_TIME']],
['uid' => (int)$this->user['uid']]
);
$this->user['is_online'] = $GLOBALS['EXEC_TIME'];
}
}
@@ -0,0 +1,53 @@
<?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\Frontend\Authentication;
use Psr\Http\Message\ServerRequestInterface;
/**
* Event listener to allow to add custom Frontend Groups to a (frontend) request
* regardless if a user is logged in or not.
*/
final class ModifyResolvedFrontendGroupsEvent
{
public function __construct(
private readonly FrontendUserAuthentication $user,
private array $groups,
private readonly ServerRequestInterface $request
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getUser(): FrontendUserAuthentication
{
return $this->user;
}
public function getGroups(): array
{
return $this->groups;
}
public function setGroups(array $groups): void
{
$this->groups = $groups;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Cache;
/**
* This class contains cache details and is created or updated in middlewares of the
* Frontend rendering chain and added as Request attribute "frontend.cache.instruction".
*
* Its main goal is to *disable* the Frontend cache mechanisms in various scenarios, for
* instance when the admin panel is used to simulate access times, or when security
* mechanisms like cHash evaluation do not match.
*/
final class CacheInstruction
{
private bool $allowCaching = true;
private array $disabledCacheReasons = [];
/**
* Instruct the core Frontend rendering to disable Frontend caching. Extensions with
* custom middlewares may set this.
*
* Note multiple cache layers are involved during Frontend rendering: For instance multiple
* TypoScript layers, the page cache and potentially others. Those caches are read from and
* written to within various middlewares. Depending on the position of a call to this method
* within the middleware stack, it can happen that some or all caches have already been
* read of written.
*
* Extensions that use this method should keep an eye on their middleware positions in the
* stack to estimate the performance impact of this call. It's of course best to not use
* the 'disable cache' mechanic at all, but to handle caching properly in extensions.
*/
public function disableCache(string $reason): void
{
if (empty($reason)) {
throw new \RuntimeException(
'A non-empty reason must be given to disable cache. At least mention the extension name that triggers it.',
1701528694
);
}
$this->allowCaching = false;
$this->disabledCacheReasons[] = $reason;
}
public function isCachingAllowed(): bool
{
return $this->allowCaching;
}
/**
* @internal Typically only consumed by extensions like EXT:adminpanel
*/
public function getDisabledCacheReasons(): array
{
return $this->disabledCacheReasons;
}
}
+272
View File
@@ -0,0 +1,272 @@
<?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\Frontend\Cache;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
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\EndTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent;
use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForRowEvent;
/**
* Calculates the max lifetime the given page should be stored in TYPO3's page cache.
*
* The "lifetime" is the number of seconds from the current time, it is not a full time/timestamp
* Example: If the lifetime is "3600" (=1h), the page will be cached for 1h.
*
* @internal This class is not part of the TYPO3 Core API
*/
#[Autoconfigure(public: true)]
class CacheLifetimeCalculator
{
protected const defaultCacheTimeout = 365 * 86400; // 1 year
public function __construct(
#[Autowire(service: 'cache.runtime')]
protected readonly FrontendInterface $runtimeCache,
protected readonly EventDispatcherInterface $eventDispatcher,
protected readonly ConnectionPool $connectionPool,
protected readonly TcaSchemaFactory $tcaSchemaFactory,
) {}
/**
* Get the cache lifetime in seconds for the given record.
*/
public function calculateLifetimeForRow(string $tableName, array $record, int $defaultCacheTimeoutInSeconds = 0): int
{
$cachedCacheLifetimeIdentifier = sprintf('calculateLifetimeForRow_%s_%d', $tableName, ($record['uid'] ?? 0));
$cachedCacheLifetime = $this->runtimeCache->get($cachedCacheLifetimeIdentifier);
if ($cachedCacheLifetime !== false) {
return (int)$cachedCacheLifetime;
}
$cacheTimeout = $defaultCacheTimeoutInSeconds ?: self::defaultCacheTimeout;
if ($this->tcaSchemaFactory->has($tableName)) {
$schema = $this->tcaSchemaFactory->get($tableName);
// If the record has a starttime or endtime, we have to adjust the cache timeout
foreach ([TcaSchemaCapability::RestrictionStartTime, TcaSchemaCapability::RestrictionEndTime] as $capability) {
if (!$schema->hasCapability($capability)) {
continue;
}
$timeField = $schema->getCapability($capability)->getFieldName();
if (array_key_exists($timeField, $record) && $record[$timeField] > 0 && ((int)$record[$timeField] - $GLOBALS['ACCESS_TIME']) > 0) {
$cacheTimeout = min($cacheTimeout, (int)$record[$timeField] - $GLOBALS['ACCESS_TIME']);
}
}
}
// Get the time, rounded to the minute (do not pollute MySQL cache!)
// It is ok that we do not take seconds into account here because this
// value will be subtracted later. So we never get the time "before"
// the cache change.
$currentTimestamp = (int)$GLOBALS['ACCESS_TIME'];
$cacheTimeout = min($currentTimestamp, $cacheTimeout);
$event = new ModifyCacheLifetimeForRowEvent(
$cacheTimeout,
$tableName,
$record
);
$event = $this->eventDispatcher->dispatch($event);
$cacheTimeout = $event->cacheLifetime;
$this->runtimeCache->set($cachedCacheLifetimeIdentifier, (string)$cacheTimeout);
return $cacheTimeout;
}
/**
* Get the cache lifetime in seconds for the given page.
*/
public function calculateLifetimeForPage(int $pageId, array $pageRecord, array $renderingInstructions, Context $context): int
{
$cachedCacheLifetimeIdentifier = 'cacheLifeTimeForPage_' . $pageId;
$cachedCacheLifetime = $this->runtimeCache->get($cachedCacheLifetimeIdentifier);
if ($cachedCacheLifetime !== false) {
return (int)$cachedCacheLifetime;
}
if ($pageRecord['cache_timeout'] ?? false) {
// Cache period was set for the page:
$cacheTimeout = (int)$pageRecord['cache_timeout'];
} else {
// Cache period was set via TypoScript "config.cache_period", otherwise it's the default of 24 hours
$cacheTimeout = (int)($renderingInstructions['cache_period'] ?? self::defaultCacheTimeout);
}
$cacheTimeout = $this->calculateLifetimeForRow('pages', $pageRecord, $cacheTimeout);
// Calculate the timeout time for records on the page and adjust cache timeout if necessary
// Get the configuration
$tablesToConsider = $this->getCurrentPageCacheConfiguration($pageId, $renderingInstructions);
// Get the time, rounded to the minute (do not pollute MySQL cache!)
// It is ok that we do not take seconds into account here because this
// value will be subtracted later. So we never get the time "before"
// the cache change.
$currentTimestamp = (int)$GLOBALS['ACCESS_TIME'];
$cacheTimeout = min($this->calculatePageCacheLifetime($tablesToConsider, $currentTimestamp), $cacheTimeout);
$event = new ModifyCacheLifetimeForPageEvent(
$cacheTimeout,
$pageId,
$pageRecord,
$renderingInstructions,
$context
);
$event = $this->eventDispatcher->dispatch($event);
$cacheTimeout = $event->getCacheLifetime();
$this->runtimeCache->set($cachedCacheLifetimeIdentifier, (string)$cacheTimeout);
return $cacheTimeout;
}
/**
* Calculates page cache timeout according to the records with starttime/endtime on the page.
*
* @return int Page cache timeout or PHP_INT_MAX if the timeout cannot be determined
*/
protected function calculatePageCacheLifetime(array $tablesToConsider, int $currentTimestamp): int
{
$result = PHP_INT_MAX;
// Find timeout by checking every table
foreach ($tablesToConsider as $tableDef) {
$result = min($result, $this->getFirstTimeValueForRecord($tableDef, $currentTimestamp));
}
// We return + 1 second just to ensure that cache is definitely regenerated
return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $currentTimestamp + 1;
}
/**
* Obtains a list of table/pid pairs to consider for page caching.
*
* TS configuration looks like this:
*
* The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account:
* config.cache.all = tt_news:14
*
* The cache lifetime of the current page allows to take records (e.g. fe_users) into account:
* config.cache.all = fe_users:current
*
* The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account:
* config.cache.42 = tt_news:15,tt_address:16
*
* @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array
* @see calculatePageCacheLifetime()
*/
protected function getCurrentPageCacheConfiguration(int $currentPageId, array $renderingInstructions): array
{
$result = ['tt_content:' . $currentPageId];
if (isset($renderingInstructions['cache.'][$currentPageId])) {
$result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $currentPageId, $renderingInstructions['cache.'][$currentPageId])));
}
if (isset($renderingInstructions['cache.']['all'])) {
$result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $currentPageId, $renderingInstructions['cache.']['all'])));
}
return array_unique($result);
}
/**
* Find the minimum starttime or endtime value in the table and pid that is greater than the current time.
*
* @param string $tableDef Table definition (format tablename:pid)
* @param int $currentTimestamp the UNIX timestamp of the current time
* @throws \InvalidArgumentException
* @return int Value of the next start/stop time or PHP_INT_MAX if not found
* @see calculatePageCacheLifetime()
*/
protected function getFirstTimeValueForRecord(string $tableDef, int $currentTimestamp): int
{
$result = PHP_INT_MAX;
[$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef);
if (empty($tableName) || !isset($pid)) {
throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365);
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()
->removeByType(StartTimeRestriction::class)
->removeByType(EndTimeRestriction::class);
$timeFields = [];
$timeConditions = $queryBuilder->expr()->or();
if ($this->tcaSchemaFactory->has($tableName)) {
$schema = $this->tcaSchemaFactory->get($tableName);
// If the record has a starttime or endtime, we have to adjust the cache timeout
foreach ([TcaSchemaCapability::RestrictionStartTime, TcaSchemaCapability::RestrictionEndTime] as $capability) {
if (!$schema->hasCapability($capability)) {
continue;
}
$timeField = $schema->getCapability($capability)->getFieldName();
$queryBuilder->addSelectLiteral(
'MIN('
. 'CASE WHEN '
. $queryBuilder->expr()->lte(
$timeField,
$queryBuilder->createNamedParameter($currentTimestamp, Connection::PARAM_INT)
)
. ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeField) . ' END'
. ') AS ' . $queryBuilder->quoteIdentifier($timeField)
);
$timeConditions = $timeConditions->with(
$queryBuilder->expr()->gt(
$timeField,
$queryBuilder->createNamedParameter($currentTimestamp, Connection::PARAM_INT)
)
);
$timeFields[] = $timeField;
}
}
// if starttime or endtime are defined, evaluate them
if ($timeFields !== []) {
// find the timestamp, when the current page's content changes the next time
$row = $queryBuilder
->from($tableName)
->where(
$queryBuilder->expr()->eq(
'pid',
$queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)
),
$timeConditions
)
->executeQuery()
->fetchAssociative();
if ($row) {
foreach ($timeFields as $timeField) {
// if a MIN value is found, take it into account for the
// cache lifetime we have to filter out start/endtimes < $currentTimestamp,
// as the SQL query also returns rows with starttime < $currentTimestamp
// and endtime > $currentTimestamp (and using a starttime from the past
// would be wrong)
if ($row[$timeField] !== null && (int)$row[$timeField] > $currentTimestamp) {
$result = min($result, (int)$row[$timeField]);
}
}
}
}
return $result;
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Cache;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ModelService;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyRegistry;
/**
* Meta-data class handling cacheable states for generic frontend functionality.
*
* @internal
*/
#[Autoconfigure(public: true)]
readonly class MetaDataState
{
public function __construct(
private ModelService $modelService,
private PolicyRegistry $policyRegistry,
private DirectiveHashCollection $directiveHashCollection,
) {}
public function getState(): array
{
return [
'PolicyRegistry::$mutationCollections' => json_encode($this->policyRegistry->getMutationCollections()),
'HashCollection' => json_encode($this->directiveHashCollection),
];
}
public function updateState(array $state): void
{
foreach ($state as $name => $value) {
switch ($name) {
case 'PolicyRegistry::$mutationCollections':
$this->updatePolicyRegistryMutationCollections($value);
break;
case 'HashCollection':
$this->updateHashCollection($value);
break;
}
}
}
private function updatePolicyRegistryMutationCollections(mixed $value): void
{
$array = $this->decodeJsonString($value);
if (is_array($array)) {
$this->policyRegistry->setMutationsCollections(
...array_map($this->modelService->buildMutationCollectionFromArray(...), $array)
);
}
}
private function updateHashCollection(mixed $value): void
{
$array = $this->decodeJsonString($value);
if (is_array($array)) {
$this->directiveHashCollection->updateFromJson($array);
}
}
private function decodeJsonString(mixed $value): ?array
{
if (!is_string($value) || $value === '') {
return null;
}
try {
$array = json_decode($value, true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return null;
}
return is_array($array) ? $array : null;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Cache;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
/**
* Substitutes a cached nonce value with the actual nonce value that
* is valid for the current request, and which is issued as HTTP CSP
* header during the frontend rendering process.
*/
class NonceValueSubstitution
{
/**
* @param array{content: string, nonce: string} $context
*/
public function substituteNonce(array $context): ?string
{
$currentNonce = $GLOBALS['TYPO3_REQUEST']?->getAttribute('nonce');
if (!$currentNonce instanceof ConsumableNonce
|| empty($context['content'])
|| empty($context['nonce'])
|| $currentNonce->value === $context['nonce']
|| !str_contains($context['content'], $context['nonce'])
) {
return null;
}
return str_replace($context['nonce'], $currentNonce->consumeInline(self::class), $context['content']);
}
}
@@ -0,0 +1,191 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Category\Collection;
use TYPO3\CMS\Core\Category\Collection\CategoryCollection as CoreCategoryCollection;
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\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Extend category collection for the frontend, to collect related records
* while respecting language, enable fields, etc.
*
* @internal this is a concrete TYPO3 hook implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class CategoryCollection extends CoreCategoryCollection
{
/**
* Creates a new collection objects and reconstitutes the
* given database record to the new object.
*
* Overrides the parent method to create a *frontend* category collection.
*
* @param array $collectionRecord Database record
* @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections
* @return CategoryCollection
*/
public static function create(array $collectionRecord, $fillItems = false)
{
$collection = GeneralUtility::makeInstance(
__CLASS__,
$collectionRecord['table_name'],
$collectionRecord['field_name']
);
$collection->fromArray($collectionRecord);
if ($fillItems) {
$collection->loadContents();
}
return $collection;
}
/**
* Loads the collection with the given id from persistence
* For memory reasons, only data for the collection itself is loaded by default.
* Entries can be loaded on first access or straightaway using the $fillItems flag.
*
* Overrides the parent method because of the call to "self::create()" which otherwise calls up
* \TYPO3\CMS\Core\Category\Collection\CategoryCollection
*
* @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 the table name
* @param string $fieldName Name of the categories relation field
* @return CategoryCollection
*/
public static function load($id, $fillItems = false, $tableName = '', $fieldName = '')
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable(static::$storageTableName);
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
$collectionRecord = $queryBuilder
->select('*')
->from(static::$storageTableName)
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
)
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if ($collectionRecord === false) {
return GeneralUtility::makeInstance(
self::class,
$tableName,
$fieldName
);
}
$collectionRecord['table_name'] = $tableName;
$collectionRecord['field_name'] = $fieldName;
return self::create($collectionRecord, $fillItems);
}
/**
* 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'.
*
* Overrides its parent method to implement usage of language,
* enable fields, etc. Also performs overlays.
*
* @return array
*/
protected function getCollectedRecords()
{
$relatedRecords = [];
$queryBuilder = $this->getCollectedRecordsQueryBuilder();
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
$context = GeneralUtility::makeInstance(Context::class);
$pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
$languageId = $context->getPropertyFromAspect('language', 'contentId', 0);
$table = $this->getItemTableName();
// If language handling is defined for item table, add language condition
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
if ($schemaFactory->has($table) && $schemaFactory->get($table)->isLanguageAware()) {
$schema = $schemaFactory->get($table);
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
// Consider default or "all" language
$languageField = sprintf(
'%s.%s',
$table,
$languageCapability->getLanguageField()->getName()
);
$languageConstraint = $queryBuilder->expr()->in(
$languageField,
$queryBuilder->createNamedParameter([0, -1], Connection::PARAM_INT_ARRAY)
);
// If not in default language, also consider items in current language with no original
if ($languageId > 0) {
$transOrigPointerField = sprintf(
'%s.%s',
$table,
$languageCapability->getTranslationOriginPointerField()->getName()
);
$languageConstraint = $queryBuilder->expr()->or(
$languageConstraint,
$queryBuilder->expr()->and(
$queryBuilder->expr()->eq(
$languageField,
$queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
$transOrigPointerField,
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
)
)
);
}
$queryBuilder->andWhere($languageConstraint);
}
// Get the related records from the database
$result = $queryBuilder->executeQuery();
while ($record = $result->fetchAssociative()) {
// Overlay the record for workspaces
$pageRepository->versionOL($table, $record);
// Overlay the record for translations
if (is_array($record)) {
$record = $pageRepository->getLanguageOverlay($table, $record);
}
// Record may have been unset during the overlay process
if (is_array($record)) {
$relatedRecords[] = $record;
}
}
return $relatedRecords;
}
}
+107
View File
@@ -0,0 +1,107 @@
<?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\Frontend\Content;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Page\ContentArea;
use TYPO3\CMS\Core\Page\ContentAreaClosure;
use TYPO3\CMS\Core\Page\ContentSlideMode;
use TYPO3\CMS\Core\Page\ResolveContentAreasEvent;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
final readonly class ContentAreaResolver
{
public function __construct(private RecordCollector $recordCollector) {}
#[AsEventListener]
public function __invoke(ResolveContentAreasEvent $event): void
{
$layout = $event->getBackendLayout();
$fullStructure = $layout->getStructure()['__config'];
$contentAreas = $this->collectContentAreasRecursive($fullStructure, $layout);
$event->setContentAreas($contentAreas);
}
/**
* Find all arrays recursively from where one of the columns within the array is called "colPos"
*
* @param array<mixed>|array{
* colPos: string|int,
* name?: string|int,
* slideMode?: string,
* identifier?: string|int,
* allowedContentTypes?: string,
* disallowedContentTypes?: string,
* } $structure
* @param array<string, ContentAreaClosure> $contentAreas
* @return array<string, ContentAreaClosure>
*/
private function collectContentAreasRecursive(array $structure, BackendLayout $layout, array $contentAreas = []): array
{
if (isset($structure['colPos'])) {
$name = (string)($structure['name'] ?? '');
$colPos = (int)$structure['colPos'];
$slideMode = ContentSlideMode::tryFrom($structure['slideMode'] ?? null);
$allowedContentTypes = GeneralUtility::trimExplode(',', $structure['allowedContentTypes'] ?? '', true);
$disallowedContentTypes = GeneralUtility::trimExplode(',', $structure['disallowedContentTypes'] ?? '', true);
$identifier = (string)($structure['identifier'] ?? '');
if ($identifier === '') {
throw new \RuntimeException(
'No identifier given for column with colPos "' . $colPos . '" in page layout "' . $layout->getIdentifier() . '". Setting an identifier is mandatory.',
1780173420
);
}
$contentAreas[$identifier] = new ContentAreaClosure(
function (ServerRequestInterface $request) use ($identifier, $name, $colPos, $slideMode, $allowedContentTypes, $disallowedContentTypes, $structure): ContentArea {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$records = $this->recordCollector->collect(
'tt_content',
[
'where' => '{#colPos}=' . $colPos,
'orderBy' => 'sorting',
],
$slideMode,
$cObj
);
return new ContentArea(
identifier: $identifier,
name: $name,
colPos: $colPos,
slideMode: $slideMode,
allowedContentTypes: $allowedContentTypes,
disallowedContentTypes: $disallowedContentTypes,
configuration: $structure,
records: $records
);
}
);
// Content Areas cannot be nested. Bubble up and find further areas next to this.
return $contentAreas;
}
foreach ($structure as $value) {
if (is_array($value)) {
$contentAreas = $this->collectContentAreasRecursive($value, $layout, $contentAreas);
}
}
return $contentAreas;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?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\Frontend\Content;
use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Page\ContentSlideMode;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Executes a SQL query, and retrieves TCA-based records for Frontend rendering.
*/
readonly class RecordCollector
{
public function __construct(
protected RecordFactory $recordFactory
) {}
public function collect(
string $table,
array $select,
ContentSlideMode $slideMode,
ContentObjectRenderer $contentObjectRenderer,
?RecordIdentityMap $recordIdentityMap = null,
): array {
$slideCollectReverse = false;
$collect = false;
switch ($slideMode) {
case ContentSlideMode::Slide:
$slide = true;
break;
case ContentSlideMode::Collect:
$slide = true;
$collect = true;
break;
case ContentSlideMode::CollectReverse:
$slide = true;
$collect = true;
$slideCollectReverse = true;
break;
default:
$slide = false;
}
$again = false;
$totalRecords = [];
do {
$recordsOnPid = $contentObjectRenderer->getRecords($table, $select);
$recordsOnPid = array_map(
fn(array $record): RecordInterface => $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $record, null, $recordIdentityMap),
$recordsOnPid
);
if ($slideCollectReverse) {
$totalRecords = array_merge($totalRecords, $recordsOnPid);
} else {
$totalRecords = array_merge($recordsOnPid, $totalRecords);
}
if ($slide) {
$select['pidInList'] = $contentObjectRenderer->getSlidePids($select['pidInList'] ?? '', $select['pidInList.'] ?? []);
if (isset($select['pidInList.'])) {
unset($select['pidInList.']);
}
$again = $select['pidInList'] !== '';
}
} while ($again && $slide && ($recordsOnPid === [] || $collect));
foreach ($totalRecords as $record) {
$contentObjectRenderer->lastChanged($record);
}
return $totalRecords;
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
/**
* Contains an abstract class for all tslib content class implementations.
*/
abstract class AbstractContentObject
{
/**
* Always set via setRequest() by ContentObjectFactory after instantiation
*/
protected ServerRequestInterface $request;
protected ?ContentObjectRenderer $cObj = null;
/**
* Renders the content object.
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string
* @throws ContentRenderingException
* @throws \Exception
*/
abstract public function render($conf = []);
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->cObj;
}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
// Provide the ContentObjectRenderer to the request as well, for code
// that only passes the request to more underlying layers, like Extbase does.
// Also makes sure the request in a Fluid RenderingContext also has the current
// content object available.
$this->request = $this->request->withAttribute('currentContentObject', $cObj);
}
protected function getPageRepository(): PageRepository
{
return GeneralUtility::makeInstance(PageRepository::class);
}
}
@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
/**
* Contains CASE class object.
*/
class CaseContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, CASE
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Output
*/
public function render($conf = []): string
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$setCurrent = $this->cObj->stdWrapValue('setCurrent', $conf);
if ($setCurrent) {
$this->cObj->data[$this->cObj->currentValKey] = $setCurrent;
}
$key = $this->cObj->stdWrapValue('key', $conf, null);
$key = isset($conf[$key]) && (string)$conf[$key] !== '' ? $key : 'default';
// If no "default" property is available, then an empty string is returned
if ($key === 'default' && !isset($conf['default'])) {
$theValue = '';
} else {
$theValue = $this->cObj->cObjGetSingle($conf[$key], $conf[$key . '.'] ?? [], $key);
}
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
}
@@ -0,0 +1,135 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Event\ModifyRecordsAfterFetchingContentEvent;
/**
* Contains CONTENT class object.
*/
class ContentContentObject extends AbstractContentObject
{
public function __construct(
private readonly TimeTracker $timeTracker,
private readonly EventDispatcherInterface $eventDispatcher,
) {}
/**
* Rendering the cObject, CONTENT
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$theValue = '';
$conf['table'] = trim((string)$this->cObj->stdWrapValue('table', $conf));
$conf['select.'] = !empty($conf['select.']) ? $conf['select.'] : [];
$renderObjName = ($conf['renderObj'] ?? false) ? $conf['renderObj'] : '<' . $conf['table'];
$renderObjKey = ($conf['renderObj'] ?? false) ? 'renderObj' : '';
$renderObjConf = $conf['renderObj.'] ?? [];
$slide = (int)$this->cObj->stdWrapValue('slide', $conf);
if (!$slide) {
$slide = 0;
}
$slideCollect = (int)$this->cObj->stdWrapValue('collect', $conf['slide.'] ?? []);
if (!$slideCollect) {
$slideCollect = 0;
}
$slideCollectReverse = (bool)$this->cObj->stdWrapValue('collectReverse', $conf['slide.'] ?? []);
$slideCollectFuzzy = (bool)$this->cObj->stdWrapValue('collectFuzzy', $conf['slide.'] ?? []);
if (!$slideCollect) {
$slideCollectFuzzy = true;
}
$again = false;
$tmpValue = '';
do {
$cobjValue = '';
$modifyRecordsEvent = $this->eventDispatcher->dispatch(
new ModifyRecordsAfterFetchingContentEvent(
$this->cObj->getRecords($conf['table'], $conf['select.']),
$theValue,
$slide,
$slideCollect,
$slideCollectReverse,
$slideCollectFuzzy,
$conf
)
);
$records = $modifyRecordsEvent->getRecords();
$theValue = $modifyRecordsEvent->getFinalContent();
$slide = $modifyRecordsEvent->getSlide();
$slideCollect = $modifyRecordsEvent->getSlideCollect();
$slideCollectReverse = $modifyRecordsEvent->getSlideCollectReverse();
$slideCollectFuzzy = $modifyRecordsEvent->getSlideCollectFuzzy();
$conf = $modifyRecordsEvent->getConfiguration();
if ($records !== []) {
$this->timeTracker->setTSlogMessage('NUMROWS: ' . count($records));
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setParent($this->cObj->data, $this->cObj->currentRecord);
foreach ($records as $row) {
$this->cObj->lastChanged($row['tstamp'] ?? 0);
$cObj->setRequest($this->request);
$cObj->start($row, $conf['table']);
$tmpValue = $cObj->cObjGetSingle($renderObjName, $renderObjConf, $renderObjKey);
$cobjValue .= $tmpValue;
}
}
if ($slideCollectReverse) {
$theValue = $cobjValue . $theValue;
} else {
$theValue .= $cobjValue;
}
if ($slideCollect > 0) {
$slideCollect--;
}
if ($slide) {
if ($slide > 0) {
$slide--;
}
$conf['select.']['pidInList'] = $this->cObj->getSlidePids(
$conf['select.']['pidInList'] ?? '',
$conf['select.']['pidInList.'] ?? [],
);
if (isset($conf['select.']['pidInList.'])) {
unset($conf['select.']['pidInList.']);
}
$again = (string)$conf['select.']['pidInList'] !== '';
}
} while ($again && $slide && ((string)$tmpValue === '' && $slideCollectFuzzy || $slideCollect));
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ($wrap) {
$theValue = $this->cObj->wrap($theValue, $wrap);
}
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
}
@@ -0,0 +1,101 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\DataProcessing\DataProcessorRegistry;
/**
* A class that contains methods that can be used to use the dataProcessing functionality
*/
#[Autoconfigure(public: true)]
readonly class ContentDataProcessor
{
public function __construct(
private ContainerInterface $container,
private DataProcessorRegistry $dataProcessorRegistry,
) {}
/**
* Check for the availability of processors, defined in TypoScript, and use them for data processing
*
* @param array $configuration Configuration array
* @param array $variables the variables to be processed
* @return array the processed data and variables as key/value store
* @throws \UnexpectedValueException If a processor class does not exist
*/
public function process(ContentObjectRenderer $cObject, array $configuration, array $variables)
{
if (
!empty($configuration['dataProcessing.'])
&& is_array($configuration['dataProcessing.'])
) {
$processors = $configuration['dataProcessing.'];
$processorKeys = ArrayUtility::filterAndSortByNumericKeys($processors);
foreach ($processorKeys as $key) {
$dataProcessor = $this->dataProcessorRegistry->getDataProcessor($processors[$key])
?? $this->getDataProcessor($processors[$key]);
$processorConfiguration = $processors[$key . '.'] ?? [];
$variables = $dataProcessor->process(
$cObject,
$configuration,
$processorConfiguration,
$variables
);
}
}
return $variables;
}
private function getDataProcessor(string $serviceName): DataProcessorInterface
{
if (!$this->container->has($serviceName)) {
// assume serviceName is the class name if it is not available in the container
return $this->instantiateDataProcessor($serviceName);
}
$dataProcessor = $this->container->get($serviceName);
if (!$dataProcessor instanceof DataProcessorInterface) {
throw new \UnexpectedValueException(
'Processor with service name "' . $serviceName . '" '
. 'must implement interface "' . DataProcessorInterface::class . '"',
1635927108
);
}
return $dataProcessor;
}
private function instantiateDataProcessor(string $className): DataProcessorInterface
{
if (!class_exists($className)) {
throw new \UnexpectedValueException('Processor class or service name "' . $className . '" does not exist!', 1427455378);
}
if (!in_array(DataProcessorInterface::class, class_implements($className) ?: [], true)) {
throw new \UnexpectedValueException(
'Processor with class name "' . $className . '" '
. 'must implement interface "' . DataProcessorInterface::class . '"',
1427455377
);
}
return GeneralUtility::makeInstance($className);
}
}
@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Contains COA class object.
*/
class ContentObjectArrayContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, COBJ_ARRAY / COA
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (empty($conf)) {
$this->getTimeTracker()->setTSlogMessage('No elements in this content object array (COBJ_ARRAY, COA).', LogLevel::WARNING);
return '';
}
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$content = $this->cObj->cObjGet($conf);
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ($wrap) {
$content = $this->cObj->wrap($content, $wrap);
}
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
/**
* @return TimeTracker
*/
protected function getTimeTracker()
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
}
@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Contains COA_INT class object.
*/
class ContentObjectArrayInternalContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, COA_INT
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (empty($conf)) {
$this->getTimeTracker()->setTSlogMessage('No elements in this content object array (COA_INT).', LogLevel::WARNING);
return '';
}
$substKey = 'INT_SCRIPT.' . md5(StringUtility::getUniqueId());
$pageParts = $this->request->getAttribute('frontend.page.parts');
$pageParts->addNotCachedContentElement([
'substKey' => $substKey,
'conf' => $conf,
'cObjData' => serialize($this->cObj->getState()),
'type' => 'COA',
]);
return '<!--' . $substKey . '-->';
}
protected function getTimeTracker(): TimeTracker
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
/**
* Registry to create cObjects (e.g. TEXT)
* @internal
*/
class ContentObjectFactory
{
public function __construct(private ContainerInterface $contentObjectLocator) {}
public function getContentObject(string $name, ServerRequestInterface $request, ContentObjectRenderer $contentObjectRenderer): ?AbstractContentObject
{
if (!$this->contentObjectLocator->has($name)) {
return null;
}
$contentObject = $this->contentObjectLocator->get($name);
if (!($contentObject instanceof AbstractContentObject)) {
throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', get_class($contentObject)), 1422564295);
}
$contentObject->setRequest($request);
$contentObject->setContentObjectRenderer($contentObjectRenderer);
return $contentObject;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use TYPO3\CMS\Core\Resource\File;
/**
* Interface for hooks to fetch the public URL of files
*/
interface ContentObjectGetPublicUrlForFileHookInterface
{
/**
* Post-processes a public URL.
*
* @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $parent The current content object (context)
* @param array $configuration TypoScript configuration
* @param File $file The file object to be used
* @param string $pubicUrl Reference to the public URL
*/
public function postProcess(ContentObjectRenderer $parent, array $configuration, File $file, &$pubicUrl);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
/**
* Interface for data processor classes processing data from
* ContentObjectRenderer, used e.g. with the FLUIDTEMPLATE content object
*/
interface DataProcessorInterface
{
/**
* Process content object data
*
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
);
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners are able to modify the initialized ContentObjectRenderer instance
*/
final readonly class AfterContentObjectRendererInitializedEvent
{
public function __construct(
private ContentObjectRenderer $contentObjectRenderer
) {}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,58 @@
<?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\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners are able to modify the resolved ContentObjectRenderer->getData() result
*/
final class AfterGetDataResolvedEvent
{
public function __construct(
private readonly string $parameterString,
private readonly array $alternativeFieldArray,
private mixed $result,
private readonly ContentObjectRenderer $contentObjectRenderer
) {}
public function getResult(): mixed
{
return $this->result;
}
public function setResult(mixed $result): void
{
$this->result = $result;
}
public function getParameterString(): string
{
return $this->parameterString;
}
public function getAlternativeFieldArray(): array
{
return $this->alternativeFieldArray;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Event;
use TYPO3\CMS\Core\Imaging\ImageResource;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileReference;
/**
* Listeners are able to modify the resolved ContentObjectRenderer->getImgResource() result
*/
final class AfterImageResourceResolvedEvent
{
public function __construct(
private readonly string|File|FileReference $file,
private readonly array $fileArray,
private ?ImageResource $imageResource
) {}
public function getFile(): string|File|FileReference
{
return $this->file;
}
public function getFileArray(): array
{
return $this->fileArray;
}
public function getImageResource(): ?ImageResource
{
return $this->imageResource;
}
public function setImageResource(?ImageResource $imageResource): void
{
$this->imageResource = $imageResource;
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Event;
/**
* Event is called after the content has been modified by the rest of the stdWrap functions
*/
final class AfterStdWrapFunctionsExecutedEvent extends EnhanceStdWrapEvent {}
@@ -0,0 +1,24 @@
<?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\Frontend\ContentObject\Event;
/**
* Event is dispatched after stdWrap functions have been initialized,
* but before any content gets modified or replaced.
*/
final class AfterStdWrapFunctionsInitializedEvent extends EnhanceStdWrapEvent {}
@@ -0,0 +1,89 @@
<?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\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners to this Event are able to modify the final stdWrap content
* and corresponding cache tags, before being stored in cache.
*
* Additionally, listeners are also able to change the cache key to be used
* as well as the lifetime. Therefore, the whole configuration is available.
*/
final class BeforeStdWrapContentStoredInCacheEvent
{
public function __construct(
private ?string $content,
private array $tags,
private string $key,
private ?int $lifetime,
private readonly array $configuration,
private readonly ContentObjectRenderer $contentObjectRenderer
) {}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
public function getTags(): array
{
return $this->tags;
}
public function setTags(array $tags): void
{
$this->tags = $tags;
}
public function getKey(): string
{
return $this->key;
}
public function setKey(string $key): void
{
$this->key = $key;
}
public function getLifetime(): ?int
{
return $this->lifetime;
}
public function setLifetime(?int $lifetime): void
{
$this->lifetime = $lifetime;
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Event;
/**
* Event is called directly after the recursive stdWrap function call but still before the content gets modified
*/
final class BeforeStdWrapFunctionsExecutedEvent extends EnhanceStdWrapEvent {}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Event;
/**
* Event is dispatched before any stdWrap function is initialized / called
*/
final class BeforeStdWrapFunctionsInitializedEvent extends EnhanceStdWrapEvent {}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners to this Event are able to modify the stdWrap processing, enhancing the functionality and
* manipulating the final result / content. This is the parent Event, which allows the corresponding
* listeners to be called on each step, see child Events:
*
* @see BeforeStdWrapFunctionsInitializedEvent
* @see AfterStdWrapFunctionsInitializedEvent
* @see BeforeStdWrapFunctionsExecutedEvent
* @see AfterStdWrapFunctionsExecutedEvent
*
* Note: The class is declared abstract to prevent it from being dispatched directly.
* Only child classes are to be dispatched to prevent duplicate executions.
*/
abstract class EnhanceStdWrapEvent
{
public function __construct(
private ?string $content,
private readonly array $configuration,
private readonly ContentObjectRenderer $contentObjectRenderer
) {}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners are able to enrich the final source collection result
*/
final class ModifyImageSourceCollectionEvent
{
public function __construct(
private string $sourceCollection,
private readonly string $fullSourceCollection,
private readonly array $sourceConfiguration,
private readonly array $sourceRenderConfiguration,
private readonly ContentObjectRenderer $contentObjectRenderer
) {}
public function setSourceCollection(string $sourceCollection): void
{
$this->sourceCollection = $sourceCollection;
}
public function getSourceCollection(): string
{
return $this->sourceCollection;
}
public function getFullSourceCollection(): string
{
return $this->fullSourceCollection;
}
public function getSourceConfiguration(): array
{
return $this->sourceConfiguration;
}
public function getSourceRenderConfiguration(): array
{
return $this->sourceRenderConfiguration;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,110 @@
<?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\Frontend\ContentObject\Event;
/**
* Event which is fired after ContentContentObject has pulled records from database.
*
* Therefore, allows listeners to completely manipulate the fetched
* records, prior to being further processed by the content object.
*
* Additionally, the event also allows to manipulate the configuration
* and options, such as the "value" or "slide".
*/
final class ModifyRecordsAfterFetchingContentEvent
{
public function __construct(
private array $records,
private string $finalContent,
private int $slide,
private int $slideCollect,
private bool $slideCollectReverse,
private bool $slideCollectFuzzy,
private array $configuration,
) {}
public function getRecords(): array
{
return $this->records;
}
public function setRecords(array $records): void
{
$this->records = $records;
}
public function getFinalContent(): string
{
return $this->finalContent;
}
public function setFinalContent(string $finalContent): void
{
$this->finalContent = $finalContent;
}
public function getSlide(): int
{
return $this->slide;
}
public function setSlide(int $slide): void
{
$this->slide = $slide;
}
public function getSlideCollect(): int
{
return $this->slideCollect;
}
public function setSlideCollect(int $slideCollect): void
{
$this->slideCollect = $slideCollect;
}
public function getSlideCollectReverse(): bool
{
return $this->slideCollectReverse;
}
public function setSlideCollectReverse(bool $slideCollectReverse): void
{
$this->slideCollectReverse = $slideCollectReverse;
}
public function getSlideCollectFuzzy(): bool
{
return $this->slideCollectFuzzy;
}
public function setSlideCollectFuzzy(bool $slideCollectFuzzy): void
{
$this->slideCollectFuzzy = $slideCollectFuzzy;
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function setConfiguration(array $configuration): void
{
$this->configuration = $configuration;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Exception;
use TYPO3\CMS\Core\Error\Exception;
/**
* @internal this is a concrete TYPO3 implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class ContentRenderingException extends Exception {}
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Exception;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
/**
* Interface ExceptionHandlerInterface
*/
interface ExceptionHandlerInterface
{
/**
* Handles exceptions thrown during rendering of content objects
* The handler can decide whether to re-throw the exception or
* return a nice error message for production context.
*
* @param array $contentObjectConfiguration
* @return string
*/
public function handle(\Exception $exception, ?AbstractContentObject $contentObject = null, $contentObjectConfiguration = []);
/**
* Used to pass the TypoScript configuration to the exception handler
*/
public function setConfiguration(array $configuration): void;
}
@@ -0,0 +1,86 @@
<?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\Frontend\ContentObject\Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Error\AbstractExceptionHandler;
use TYPO3\CMS\Core\Http\ImmediateResponseException;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
/**
* Exception handler class for content object rendering
* @internal this is a concrete TYPO3 implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
#[Autoconfigure(public: true, shared: false)]
class ProductionExceptionHandler implements ExceptionHandlerInterface
{
protected array $configuration = [];
public function __construct(
protected Context $context,
protected Random $random,
protected LoggerInterface $logger,
protected RequestId $requestId
) {}
public function setConfiguration(array $configuration): void
{
$this->configuration = $configuration;
}
/**
* Handles exceptions thrown during rendering of content objects
* The handler can decide whether to re-throw the exception or
* return a nice error message for production context.
*
* @param array $contentObjectConfiguration
* @throws \Exception
*/
public function handle(\Exception $exception, ?AbstractContentObject $contentObject = null, $contentObjectConfiguration = []): string
{
// ImmediateResponseException (and the derived PropagateResponseException) should work similar to
// exit / die and must therefore not be handled by this ExceptionHandler.
if ($exception instanceof ImmediateResponseException) {
throw $exception;
}
if (!empty($this->configuration['ignoreCodes.'])
&& in_array($exception->getCode(), array_map('intval', $this->configuration['ignoreCodes.']), true)
) {
throw $exception;
}
$errorMessage = $this->configuration['errorMessage'] ?? 'Oops, an error occurred! Request: {requestId}';
// $code and it's placeholder %s for b/w compatibility
$code = $this->context->getAspect('date')->getDateTime()->format('YmdHis') . $this->random->generateRandomHexString(8);
$errorMessage = str_replace('%s', '{code}', $errorMessage);
// Log exception except HMAC validation exceptions caused by potentially forged requests
if (!in_array($exception->getCode(), AbstractExceptionHandler::IGNORED_HMAC_EXCEPTION_CODES, true)) {
$this->logger->alert($errorMessage, ['exception' => $exception, 'code' => $code, 'requestId' => $this->requestId]);
}
// Return interpolated error message
return str_replace(['{code}', '{requestId}'], [$code, (string)$this->requestId], $errorMessage);
}
}
@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use TYPO3\CMS\Core\Resource\File;
/**
* Interface for classes which hook into \TYPO3\CMS\Frontend\ContentObject and do additional getImgResource processing
*/
interface FileLinkHookInterface
{
/**
* Finds alternative previewImage for given File.
*
* @return File
*/
public function getPreviewImage(File $file);
}
@@ -0,0 +1,185 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Frontend\Resource\FileCollector;
/**
* Contains FILES content object
*/
class FilesContentObject extends AbstractContentObject
{
/**
* Rendering the cObject FILES
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$register = $this->request->getAttribute('frontend.register.stack')->current();
// Store the original "currentFile" within a variable so it can be re-applied later-on
$originalFileInContentObject = $this->cObj->getCurrentFile();
$fileCollector = $this->findAndSortFiles($conf);
$fileObjects = $fileCollector->getFiles();
$availableFileObjectCount = count($fileObjects);
// optionSplit applied to conf to allow different settings per file
$splitConf = GeneralUtility::makeInstance(TypoScriptService::class)
->explodeConfigurationForOptionSplit($conf, $availableFileObjectCount);
$start = (int)$this->cObj->stdWrapValue('begin', $conf, 0);
$start = MathUtility::forceIntegerInRange($start, 0, $availableFileObjectCount);
$limit = (int)$this->cObj->stdWrapValue('maxItems', $conf, $availableFileObjectCount);
$end = MathUtility::forceIntegerInRange($start + $limit, $start, $availableFileObjectCount);
$register->set('FILES_COUNT', min($limit, $availableFileObjectCount));
$fileObjectCounter = 0;
$keys = array_keys($fileObjects);
$content = '';
for ($i = $start; $i < $end; $i++) {
$key = $keys[$i];
$fileObject = $fileObjects[$key];
$register->set('FILE_NUM_CURRENT', $fileObjectCounter);
$this->cObj->setCurrentFile($fileObject);
$content .= $this->cObj->cObjGetSingle($splitConf[$key]['renderObj'], $splitConf[$key]['renderObj.'], 'renderObj');
$fileObjectCounter++;
}
// Reset current file within cObj to the original file after rendering output of FILES
// so e.g. stdWrap is not working on the last current file applied, thus avoiding side-effects
$this->cObj->setCurrentFile($originalFileInContentObject);
return $this->cObj->stdWrap($content, $conf['stdWrap.'] ?? []);
}
/**
* Function to check for references, collections, folders and
* accumulates into one etc.
*/
protected function findAndSortFiles(array $conf): FileCollector
{
$fileCollector = $this->getFileCollector();
// Getting the files
if ((isset($conf['references']) && $conf['references']) || (isset($conf['references.']) && $conf['references.'])) {
/*
The TypoScript could look like this:
# all items related to the page.media field:
references {
table = pages
uid.data = page:uid
fieldName = media
}
# or: sys_file_references with uid 27:
references = 27
*/
$referencesUidList = (string)$this->cObj->stdWrapValue('references', $conf);
$referencesUids = GeneralUtility::intExplode(',', $referencesUidList, true);
$fileCollector->addFileReferences($referencesUids);
if (!empty($conf['references.'])) {
$this->addFileReferences($conf, (array)$this->cObj->data, $fileCollector);
}
}
if ((isset($conf['files']) && $conf['files']) || (isset($conf['files.']) && $conf['files.'])) {
/*
The TypoScript could look like this:
# with sys_file UIDs:
files = 12,14,15# using stdWrap:
files.field = some_field
*/
$fileUids = GeneralUtility::intExplode(',', (string)$this->cObj->stdWrapValue('files', $conf), true);
$fileCollector->addFiles($fileUids);
}
if ((isset($conf['collections']) && $conf['collections']) || (isset($conf['collections.']) && $conf['collections.'])) {
$collectionUids = GeneralUtility::intExplode(',', (string)$this->cObj->stdWrapValue('collections', $conf), true);
$fileCollector->addFilesFromFileCollections($collectionUids);
}
if ((isset($conf['folders']) && $conf['folders']) || (isset($conf['folders.']) && $conf['folders.'])) {
$folderIdentifiers = GeneralUtility::trimExplode(',', (string)$this->cObj->stdWrapValue('folders', $conf));
$fileCollector->addFilesFromFolders($folderIdentifiers, !empty($conf['folders.']['recursive']));
}
// Enable sorting for multiple fileObjects
$sortingProperty = (string)$this->cObj->stdWrapValue('sorting', $conf);
if ($sortingProperty !== '') {
$sortingDirection = $this->cObj->stdWrapValue('direction', $conf['sorting.'] ?? []);
$fileCollector->sort($sortingProperty, $sortingDirection);
}
return $fileCollector;
}
/**
* Handles and resolves file references.
*
* @param array $configuration TypoScript configuration
* @param array $element The parent element referencing to files
*/
protected function addFileReferences(array $configuration, array $element, FileCollector $fileCollector): void
{
// It's important that this always stays "fieldName" and not be renamed to "field" as it would otherwise collide with the stdWrap key of that name
$referencesFieldName = $this->cObj->stdWrapValue('fieldName', $configuration['references.'] ?? []);
// If no reference fieldName is set, there's nothing to do
if (empty($referencesFieldName)) {
return;
}
$currentId = !empty($element['uid']) ? $element['uid'] : 0;
$tableName = $this->cObj->getCurrentTable();
// Fetch the references of the default element
$referencesForeignTable = (string)$this->cObj->stdWrapValue('table', $configuration['references.'], $tableName);
$referencesForeignUid = (int)$this->cObj->stdWrapValue('uid', $configuration['references.'], $currentId);
$pageRepository = $this->getPageRepository();
// Fetch element if definition has been modified via TypoScript
if (
($referencesForeignTable !== '' && $referencesForeignTable !== $tableName)
|| ($referencesForeignUid !== 0 && $referencesForeignUid !== $currentId)
) {
$element = $pageRepository->getRawRecord($referencesForeignTable, $referencesForeignUid);
// Do versionOL() again and unset move pointers
$pageRepository->versionOL($referencesForeignTable, $element, true);
if (is_array($element)) {
$element = $pageRepository->getLanguageOverlay($referencesForeignTable, $element);
}
}
if (is_array($element)) {
$fileCollector->addFilesFromRelation($referencesForeignTable ?: $tableName, $referencesFieldName, $element);
}
}
protected function getFileCollector(): FileCollector
{
return GeneralUtility::makeInstance(FileCollector::class);
}
}
@@ -0,0 +1,273 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
use TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder;
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
use TYPO3Fluid\Fluid\View\Exception\InvalidLayoutException;
use TYPO3Fluid\Fluid\View\Exception\InvalidPartialException;
use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;
/**
* Contains FLUIDTEMPLATE class object
*/
class FluidTemplateContentObject extends AbstractContentObject
{
public function __construct(
private readonly ContentDataProcessor $contentDataProcessor,
private readonly TypoScriptService $typoScriptService,
private readonly ViewFactoryInterface $viewFactory,
) {}
/**
* Rendering the cObject, FLUIDTEMPLATE
*
* Configuration properties:
* - file string+stdWrap The FLUID template file
* - layoutRootPaths array of filepath+stdWrap Root paths to layouts (fallback)
* - partialRootPaths array of filepath+stdWrap Root paths to partials (fallback)
* - variable array of cObjects, the keys are the variable names in fluid
* - dataProcessing array of data processors which are classes to manipulate $data
* - extbase.pluginName
* - extbase.controllerExtensionName
* - extbase.controllerName
* - extbase.controllerActionName
*
* Example:
* 10 = FLUIDTEMPLATE
* 10.templateName = MyTemplate
* 10.templateRootPaths.10 = EXT:site_configuration/Resources/Private/Templates/
* 10.partialRootPaths.10 = EXT:site_configuration/Resources/Private/Partials/
* 10.layoutRootPaths.10 = EXT:site_configuration/Resources/Private/Layouts/
* 10.variables {
* mylabel = TEXT
* mylabel.value = Label from TypoScript coming
* }
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
*/
public function render($conf = []): string
{
if (!is_array($conf)) {
$conf = [];
}
$request = $this->buildExtbaseRequestIfNeeded($this->request, $conf);
$templateFilename = '';
$templateSource = null;
if ((!empty($conf['templateName']) || !empty($conf['templateName.']))
&& !empty($conf['templateRootPaths.']) && is_array($conf['templateRootPaths.'])
) {
// This is the most preferred way to render fluid: set up paths, then call render('My/Template')
$viewFactoryData = new ViewFactoryData(
templateRootPaths: $this->applyStandardWrapToFluidPaths($conf['templateRootPaths.']),
partialRootPaths: $this->getPartialRootPaths($conf),
layoutRootPaths: $this->getLayoutRootPaths($conf),
request: $request,
format: $this->cObj->stdWrapValue('format', $conf, null),
);
$templateFilename = $this->cObj->stdWrapValue('templateName', $conf);
} elseif (!empty($conf['template']) && !empty($conf['template.'])) {
// Fetch the Fluid template by template cObject "template = TEXT, template.value = <f:foo ..."
$templateSource = $this->cObj->cObjGetSingle($conf['template'], $conf['template.'], 'template');
if ($templateSource === '') {
throw new ContentRenderingException(
'Could not find template source for ' . $conf['template'],
1437420865
);
}
$viewFactoryData = new ViewFactoryData(
partialRootPaths: $this->getPartialRootPaths($conf),
layoutRootPaths: $this->getLayoutRootPaths($conf),
request: $request,
format: $this->cObj->stdWrapValue('format', $conf, null),
);
} else {
// Fetch the Fluid template by file stdWrap "file = EXT:myExt/.../Foo.html"
$file = (string)$this->cObj->stdWrapValue('file', $conf);
// Get the absolute file name
$templatePathAndFilename = GeneralUtility::getFileAbsFileName($file);
$viewFactoryData = new ViewFactoryData(
partialRootPaths: $this->getPartialRootPaths($conf),
layoutRootPaths: $this->getLayoutRootPaths($conf),
templatePathAndFilename: $templatePathAndFilename,
request: $request,
format: $this->cObj->stdWrapValue('format', $conf, null),
);
}
$view = $this->viewFactory->create($viewFactoryData);
if (!$view instanceof FluidViewAdapter) {
throw new ContentRenderingException(
'The FLUIDTEMPLATE content object only works with FluidViewAdapter view. Use a different'
. ' content object to render some other view',
1724680477
);
}
if ($templateSource) {
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($templateSource);
}
if (isset($conf['settings.'])) {
$settings = $this->typoScriptService->convertTypoScriptArrayToPlainArray($conf['settings.']);
$view->assign('settings', $settings);
}
$variables = $this->getContentObjectVariables($conf);
$variables = $this->contentDataProcessor->process($this->cObj, $conf, $variables);
$view->assignMultiple($variables);
try {
// View needs to be rendered before the following asset rendering because it
// sets the template (paths) internally.
$content = $view->render($templateFilename);
} catch (InvalidTemplateResourceException $e) {
// Only add a FLUIDTEMPLATE specific message in case the exception has been thrown for the given template
if ($e instanceof InvalidPartialException || $e instanceof InvalidLayoutException || $templateFilename === '' || $e->templateName !== 'Default/' . $templateFilename) {
throw $e;
}
throw new InvalidTemplateResourceException(
sprintf(
'FLUIDTEMPLATE TypoScript object: Failed to resolve a template file for templateName "%s". See also: %s. The following paths were checked: "%s"',
$templateFilename,
Typo3Information::getDocsLink('t3tsref:cobj-template'),
implode('", "', $e->evaluatedTemplatePaths),
),
1772572794,
$e,
$e->templateName,
$e->evaluatedTemplatePaths,
);
}
if (isset($conf['stdWrap.'])) {
return $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
protected function getLayoutRootPaths(array $conf): ?array
{
$layoutPaths = [];
$layoutRootPath = (string)$this->cObj->stdWrapValue('layoutRootPath', $conf);
if ($layoutRootPath !== '') {
$layoutPaths[] = GeneralUtility::getFileAbsFileName($layoutRootPath);
}
if (isset($conf['layoutRootPaths.'])) {
$layoutPaths = array_replace($layoutPaths, $this->applyStandardWrapToFluidPaths($conf['layoutRootPaths.']));
}
return !empty($layoutPaths) ? $layoutPaths : null;
}
protected function getPartialRootPaths(array $conf): ?array
{
$partialPaths = [];
$partialRootPath = (string)$this->cObj->stdWrapValue('partialRootPath', $conf);
if ($partialRootPath !== '') {
$partialPaths[] = GeneralUtility::getFileAbsFileName($partialRootPath);
}
if (isset($conf['partialRootPaths.'])) {
$partialPaths = array_replace($partialPaths, $this->applyStandardWrapToFluidPaths($conf['partialRootPaths.']));
}
return !empty($partialPaths) ? $partialPaths : null;
}
/**
* @todo: This magic has to fall one way or the other. It has been introduced for ext:form to
* mimic extbase, see https://forge.typo3.org/issues/78842. This is actively used when
* rendering forms using the formvh:render strategy, see the documentation.
*/
protected function buildExtbaseRequestIfNeeded(ServerRequestInterface $request, array $conf): ServerRequestInterface
{
$requestPluginName = (string)$this->cObj->stdWrapValue('pluginName', $conf['extbase.'] ?? []);
$requestControllerExtensionName = (string)$this->cObj->stdWrapValue('controllerExtensionName', $conf['extbase.'] ?? []);
$requestControllerName = (string)$this->cObj->stdWrapValue('controllerName', $conf['extbase.'] ?? []);
$requestControllerActionName = (string)$this->cObj->stdWrapValue('controllerActionName', $conf['extbase.'] ?? []);
if ($requestPluginName && $requestControllerExtensionName && $requestControllerName && $requestControllerActionName) {
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configurationManager->setConfiguration([
'extensionName' => $requestControllerExtensionName,
'pluginName' => $requestPluginName,
]);
if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$requestControllerExtensionName]['plugins'][$requestPluginName]['controllers'])) {
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$requestControllerExtensionName]['plugins'][$requestPluginName]['controllers'] = [
$requestControllerName => [
'actions' => [
$requestControllerActionName,
],
],
];
}
$requestBuilder = GeneralUtility::makeInstance(RequestBuilder::class);
$request = $requestBuilder->build($request);
}
return $request;
}
/**
* Compile rendered content objects in variables array ready to assign to the view.
*/
protected function getContentObjectVariables(array $conf): array
{
$variables = [];
$reservedVariables = ['data', 'current'];
// Accumulate the variables to be process and loop them through cObjGetSingle
$variablesToProcess = (array)($conf['variables.'] ?? []);
foreach ($variablesToProcess as $variableName => $cObjType) {
if (is_array($cObjType)) {
continue;
}
if (!in_array($variableName, $reservedVariables)) {
$cObjConf = $variablesToProcess[$variableName . '.'] ?? [];
$variables[$variableName] = $this->cObj->cObjGetSingle($cObjType, $cObjConf, 'variables.' . $variableName);
} else {
throw new \InvalidArgumentException(
'Cannot use reserved name "' . $variableName . '" as variable name in FLUIDTEMPLATE.',
1288095720
);
}
}
$variables['data'] = $this->cObj->data;
$variables['current'] = $this->cObj->data[$this->cObj->currentValKey] ?? null;
return $variables;
}
protected function applyStandardWrapToFluidPaths(array $paths): array
{
$finalPaths = [];
foreach ($paths as $key => $path) {
if (str_ends_with((string)$key, '.')) {
if (isset($paths[substr($key, 0, -1)])) {
continue;
}
$path = $this->cObj->stdWrap('', $path);
} elseif (isset($paths[$key . '.'])) {
$path = $this->cObj->stdWrap($path, $paths[$key . '.']);
}
$finalPaths[$key] = GeneralUtility::getFileAbsFileName($path);
}
return $finalPaths;
}
}
@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Menu\Exception\NoSuchMenuTypeException;
use TYPO3\CMS\Frontend\ContentObject\Menu\MenuContentObjectFactory;
/**
* Contains HMENU class object.
*/
class HierarchicalMenuContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, HMENU
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$theValue = '';
$menuType = $conf[1] ?? '';
try {
$register = $this->request->getAttribute('frontend.register.stack')->current();
$menuObjectFactory = GeneralUtility::makeInstance(MenuContentObjectFactory::class);
$menu = $menuObjectFactory->getMenuObjectByType($menuType);
$countHMENU = (int)$register->get('count_HMENU', 0);
$countHMENU++;
$register->set('count_HMENU', $countHMENU);
$register->set('count_HMENU_MENUOBJ', 0);
$register->set('count_MENUOBJ', 0);
$menu->parent_cObj = $this->getContentObjectRenderer();
$menu->start(null, $this->getPageRepository(), '', $conf, 1, '', $this->request);
$menu->makeMenu();
$theValue .= $menu->writeMenu();
} catch (NoSuchMenuTypeException) {
}
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ($wrap) {
$theValue = $this->cObj->wrap($theValue, $wrap);
}
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
}
@@ -0,0 +1,303 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Type\DocType;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Event\ModifyImageSourceCollectionEvent;
use TYPO3\CMS\Frontend\Page\FrontendUrlPrefix;
/**
* Contains IMAGE class object.
*/
class ImageContentObject extends AbstractContentObject
{
public function __construct(
protected readonly MarkerBasedTemplateService $markerTemplateService,
protected readonly TimeTracker $timeTracker,
protected readonly LoggerInterface $logger,
) {}
/**
* Rendering the cObject, IMAGE
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Output
*/
public function render($conf = []): string
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$theValue = $this->cImage($conf['file'] ?? '', is_array($conf) ? $conf : []);
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
/**
* Returns a <img> tag with the image file defined by $file and processed according to the properties in the TypoScript array.
* Mostly this function is a sub-function to the IMAGE function which renders the IMAGE cObject in TypoScript.
*
* @param string|File|FileReference|null $file File TypoScript resource
* @param array $conf TypoScript configuration properties
* @return string HTML <img> tag, (possibly wrapped in links and other HTML) if any image found.
*/
protected function cImage($file, array $conf): string
{
$imageResource = $this->cObj->getImgResource($file, $conf['file.'] ?? []);
if ($imageResource === null) {
return '';
}
// $info['originalFile'] will be set, when the file is processed by FAL.
// In that case the URL is final and we must not add a prefix
if ($imageResource->getOriginalFile() === null && is_file($imageResource->getFullPath())) {
$absRefPrefix = GeneralUtility::makeInstance(FrontendUrlPrefix::class)->getUrlPrefix($this->request);
$source = $absRefPrefix . str_replace('%2F', '/', rawurlencode($imageResource->getPublicUrl()));
} else {
$source = $imageResource->getPublicUrl();
}
// A file whose physical resource is gone (sys_file.missing=1) resolves to
// an image resource without public URL. Render nothing in this case, just
// like for an image resource that could not be resolved at all.
if ($source === null) {
$identifier = $imageResource->getOriginalFile()?->getIdentifier() ?: $imageResource->getFullPath();
$this->logger->warning('The image "{file}" has no public URL, the file is probably missing, and won\'t be included in frontend output', [
'file' => $identifier,
]);
$this->timeTracker->setTSlogMessage(
'The image "' . $identifier . '" has no public URL, the file is probably missing. It is not rendered.',
LogLevel::WARNING
);
return '';
}
GeneralUtility::makeInstance(AssetCollector::class)->addMedia(
$source,
$imageResource->getLegacyImageResourceInformation()
);
$layoutKey = (string)$this->cObj->stdWrapValue('layoutKey', $conf);
$imageTagTemplate = $this->getImageTagTemplate($layoutKey, $conf);
$sourceCollection = $this->getImageSourceCollection($layoutKey, $conf, $file);
$altParam = $this->getAltParam($conf);
$params = $this->cObj->stdWrapValue('params', $conf);
if ($params !== '' && $params[0] !== ' ') {
$params = ' ' . $params;
}
$imageTagValues = [
'width' => $imageResource->getWidth(),
'height' => $imageResource->getHeight(),
'src' => htmlspecialchars($source),
'params' => $params,
'altParams' => $altParam,
'sourceCollection' => $sourceCollection,
'selfClosingTagSlash' => DocType::createFromRequest($this->request)->isXmlCompliant() ? ' /' : '',
];
$theValue = $this->markerTemplateService->substituteMarkerArray($imageTagTemplate, $imageTagValues, '###|###', true, true);
$linkWrap = (string)$this->cObj->stdWrapValue('linkWrap', $conf);
if ($linkWrap !== '') {
$theValue = $this->linkWrap($theValue, $linkWrap);
} elseif ($conf['imageLinkWrap'] ?? false) {
$originalFile = urldecode($imageResource->getFullPath());
$theValue = $this->cObj->imageLinkWrap($theValue, $originalFile, $conf['imageLinkWrap.']);
}
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ((string)$wrap !== '') {
$theValue = $this->cObj->wrap($theValue, $conf['wrap']);
}
return $theValue;
}
/**
* Returns the html-template for rendering the image-Tag if no template is defined via typoscript the
* default <img> tag template is returned
*
* @param string $layoutKey rendering key
* @param array $conf TypoScript configuration properties
*/
protected function getImageTagTemplate($layoutKey, $conf): string
{
if ($layoutKey && isset($conf['layout.']) && isset($conf['layout.'][$layoutKey . '.'])) {
return $this->cObj->stdWrapValue('element', $conf['layout.'][$layoutKey . '.']);
}
return '<img src="###SRC###" width="###WIDTH###" height="###HEIGHT###" ###PARAMS### ###ALTPARAMS### ###SELFCLOSINGTAGSLASH###>';
}
/**
* Render alternate sources for the image tag. If no source collection is given an empty string is returned.
*
* @param string $layoutKey rendering key
* @param array $conf TypoScript configuration properties
* @param string|File|FileReference|null $file
* @return string
*/
protected function getImageSourceCollection(string $layoutKey, array $conf, $file)
{
$sourceCollection = '';
if ($layoutKey
&& isset($conf['sourceCollection.']) && $conf['sourceCollection.']
&& (
isset($conf['layout.'][$layoutKey . '.']['source']) && $conf['layout.'][$layoutKey . '.']['source']
|| isset($conf['layout.'][$layoutKey . '.']['source.']) && $conf['layout.'][$layoutKey . '.']['source.']
)
) {
// find active sourceCollection
$activeSourceCollections = [];
foreach ($conf['sourceCollection.'] as $sourceCollectionKey => $sourceCollectionConfiguration) {
if (str_ends_with($sourceCollectionKey, '.')) {
if (empty($sourceCollectionConfiguration['if.']) || $this->cObj->checkIf($sourceCollectionConfiguration['if.'])) {
$activeSourceCollections[] = $sourceCollectionConfiguration;
}
}
}
// apply option split to configurations
$typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
$srcLayoutOptionSplitted = $typoScriptService->explodeConfigurationForOptionSplit((array)$conf['layout.'][$layoutKey . '.'], count($activeSourceCollections));
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
$isXmlCompliant = DocType::createFromRequest($this->request)->isXmlCompliant();
// render sources
foreach ($activeSourceCollections as $key => $sourceConfiguration) {
$sourceLayout = $this->cObj->stdWrapValue('source', $srcLayoutOptionSplitted[$key] ?? []);
$sourceRenderConfiguration = [
'file' => $file,
'file.' => $conf['file.'] ?? null,
];
$imageQuality = $this->cObj->stdWrapValue('quality', $sourceConfiguration ?? []);
if ($imageQuality) {
$sourceRenderConfiguration['file.']['params'] = '-quality ' . (int)$imageQuality;
}
$pixelDensity = (int)$this->cObj->stdWrapValue('pixelDensity', $sourceConfiguration, 1);
$dimensionKeys = ['width', 'height', 'maxW', 'minW', 'maxH', 'minH', 'maxWidth', 'maxHeight', 'XY'];
foreach ($dimensionKeys as $dimensionKey) {
$dimension = (string)$this->cObj->stdWrapValue($dimensionKey, $sourceConfiguration);
if ($dimension === '') {
$dimension = (string)$this->cObj->stdWrapValue($dimensionKey, $conf['file.'] ?? []);
}
if ($dimension !== '') {
if (str_contains($dimension, 'c') && ($dimensionKey === 'width' || $dimensionKey === 'height')) {
$dimensionParts = explode('c', $dimension, 2);
$dimension = ((int)$dimensionParts[0] * $pixelDensity) . 'c';
if ($dimensionParts[1]) {
$dimension .= $dimensionParts[1];
}
} elseif ($dimensionKey === 'XY') {
$dimensionParts = GeneralUtility::intExplode(',', $dimension);
$dimension = $dimensionParts[0] * $pixelDensity;
if ($dimensionParts[1]) {
$dimension .= ',' . $dimensionParts[1] * $pixelDensity;
}
} else {
$dimension = (int)$dimension * $pixelDensity;
}
$sourceRenderConfiguration['file.'][$dimensionKey] = $dimension;
// Remove the stdWrap properties for dimension as they have been processed already above.
unset($sourceRenderConfiguration['file.'][$dimensionKey . '.']);
}
}
$imageResource = $this->cObj->getImgResource($sourceRenderConfiguration['file'], $sourceRenderConfiguration['file.']);
if ($imageResource !== null) {
$sourceConfiguration['width'] = $imageResource->getWidth();
$sourceConfiguration['height'] = $imageResource->getHeight();
$urlPrefix = '';
// Prepend 'absRefPrefix' to file path only if file was not processed by FAL, e.g. GIFBUILDER
if ($imageResource->getOriginalFile() === null && is_file($imageResource->getFullPath())) {
$urlPrefix = GeneralUtility::makeInstance(FrontendUrlPrefix::class)->getUrlPrefix($this->request);
}
$sourceConfiguration['src'] = htmlspecialchars($urlPrefix . $imageResource->getPublicUrl());
$sourceConfiguration['selfClosingTagSlash'] = $isXmlCompliant ? ' /' : '';
$oneSourceCollection = $this->markerTemplateService->substituteMarkerArray($sourceLayout, $sourceConfiguration, '###|###', true, true);
$sourceCollection .= $eventDispatcher->dispatch(
new ModifyImageSourceCollectionEvent($oneSourceCollection, $sourceCollection, (array)$sourceConfiguration, $sourceRenderConfiguration, $this->cObj)
)->getSourceCollection();
}
}
}
return $sourceCollection;
}
/**
* Wraps the input string by the $wrap value and implements the "linkWrap" data type as well.
*
* The "linkWrap" data type means that this function will find any integer encapsulated
* in {} (curly braces) in the first wrap part and substitute it with the corresponding page
* uid from the rootline where the found integer is pointing to the key in the rootline.
*
* @param string $content Input string
* @param string $wrap A string where the first two parts separated by "|" (vertical line) will be wrapped around the input string
*/
protected function linkWrap(string $content, string $wrap): string
{
$wrapArr = explode('|', $wrap);
if (preg_match('/\\{([0-9]*)\\}/', $wrapArr[0], $reg)) {
$localRootLine = $this->request->getAttribute('frontend.page.information')->getLocalRootLine();
$uid = $localRootLine[$reg[1]]['uid'] ?? null;
if ($uid) {
$wrapArr[0] = str_replace($reg[0], $uid, $wrapArr[0]);
}
}
return trim($wrapArr[0]) . $content . trim($wrapArr[1] ?? '');
}
/**
* An abstraction method which creates an alt or title parameter for an HTML img, applet, area or input element and the FILE content element.
* From the $conf array it implements the properties "altText" and "titleText"
*
* @param array $conf TypoScript configuration properties
* @return string Parameter string containing alt and title parameters (if any)
*/
protected function getAltParam(array $conf): string
{
$altText = trim((string)$this->cObj->stdWrapValue('altText', $conf));
$titleText = trim((string)$this->cObj->stdWrapValue('titleText', $conf));
// "alt":
$altParam = ' alt="' . htmlspecialchars($altText) . '"';
// "title":
$emptyTitleHandling = $this->cObj->stdWrapValue('emptyTitleHandling', $conf);
// Choices: 'keepEmpty' | 'useAlt' | 'removeAttr'
if ($titleText || $emptyTitleHandling === 'keepEmpty') {
$altParam .= ' title="' . htmlspecialchars($titleText) . '"';
} elseif ($emptyTitleHandling === 'useAlt') {
$altParam .= ' title="' . htmlspecialchars($altText) . '"';
}
return $altParam;
}
}
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
/**
* Contains IMG_RESOURCE class object.
*/
class ImageResourceContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, IMG_RESOURCE
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
$imageResource = $this->cObj->getImgResource($conf['file'] ?? '', $conf['file.'] ?? []);
if ($imageResource === null) {
return '';
}
return isset($conf['stdWrap.'])
? $this->cObj->stdWrap($imageResource->getPublicUrl(), $conf['stdWrap.'])
: $imageResource->getPublicUrl();
}
}
@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
/**
* Implement cObj "LOAD_REGISTER":
* Get latest Register, clone it, set a key/value, push as new latest RegisterStack entry.
*
* Note the naming "LOAD_REGISTER" is kinda misleading since it rather "loads into" or
* sets a new value and pushes as key/value to the stack. "RESTORE_REGISTER" is the
* counterpart cObj to get rid of that state again.
*/
class LoadRegisterContentObject extends AbstractContentObject
{
/**
* Does not return any content, it just sets internal data based on the TypoScript properties.
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Empty string
*/
public function render($conf = []): string
{
$registerStack = $this->request->getAttribute('frontend.register.stack');
$clonedRegister = clone $registerStack->current();
if (is_array($conf)) {
$isExecuted = [];
foreach ($conf as $key => $value) {
$key = rtrim($key, '.');
if (!isset($isExecuted[$key])) {
$registerProperties = $key . '.';
if (isset($conf[$key]) && isset($conf[$registerProperties])) {
$value = $this->cObj->stdWrap($conf[$key], $conf[$registerProperties]);
} elseif (isset($conf[$registerProperties])) {
$value = $this->cObj->stdWrap('', $conf[$registerProperties]);
}
$clonedRegister->set($key, $value);
$isExecuted[$key] = true;
}
}
}
$registerStack->push($clonedRegister);
return '';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Category\Collection\CategoryCollection;
/**
* Utility class for menus based on category collections of pages.
*
* Returns all the relevant pages for rendering with a menu content object.
* @internal this is only used for internal purposes and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class CategoryMenuUtility
{
/**
* @var string Name of the field used for sorting the pages
*/
protected static $sortingField;
/**
* Collects all pages for the selected categories, sorted according to configuration.
*
* @param string $selectedCategories Comma-separated list of system categories primary keys
* @param array|null $configuration TypoScript configuration for the "special." keyword
* @param AbstractMenuContentObject $parentObject Back-reference to the calling object
* @return array List of selected pages
*/
public function collectPages($selectedCategories, $configuration, $parentObject)
{
$selectedPages = [];
$categoriesPerPage = [];
// Determine the name of the relation field
$relationField = (string)$parentObject->getParentContentObject()->stdWrapValue('relation', $configuration ?? []);
// Get the pages for each selected category
$selectedCategories = GeneralUtility::intExplode(',', $selectedCategories, true);
foreach ($selectedCategories as $aCategory) {
$collection = CategoryCollection::load(
$aCategory,
true,
'pages',
$relationField
);
$categoryUid = $collection->getUid();
// Loop on the results, overlay each page record found
foreach ($collection as $pageItem) {
$parentObject->getSysPage()->versionOL('pages', $pageItem, true);
if (is_array($pageItem)) {
$selectedPages[$pageItem['uid']] = $parentObject->getSysPage()->getLanguageOverlay('pages', $pageItem);
// Keep a list of the categories each page belongs to
if (!isset($categoriesPerPage[$pageItem['uid']])) {
$categoriesPerPage[$pageItem['uid']] = [];
}
$categoriesPerPage[$pageItem['uid']][] = $categoryUid;
}
}
}
// Loop on the selected pages to add the categories they belong to, as comma-separated list of category uid's)
// (this makes them available for rendering, if needed)
foreach ($selectedPages as $uid => $pageRecord) {
$selectedPages[$uid]['_categories'] = implode(',', $categoriesPerPage[$uid]);
}
// Sort the pages according to the sorting property
self::$sortingField = (string)$parentObject->getParentContentObject()->stdWrapValue('sorting', $configuration ?? []);
$order = (string)$parentObject->getParentContentObject()->stdWrapValue('order', $configuration ?? []);
$selectedPages = $this->sortPages($selectedPages, $order);
return $selectedPages;
}
/**
* Sorts the selected pages
*
* If the sorting field is not defined or does not corresponding to an existing field
* of the "pages" tables, the list of pages will remain unchanged.
*
* @param array $pages List of selected pages
* @param string $order Order for sorting (should "asc" or "desc")
* @return array Sorted list of pages
*/
protected function sortPages($pages, $order)
{
// Perform the sorting only if a criterion was actually defined
if (!empty(self::$sortingField)) {
// Check that the sorting field exists (checking the first record is enough)
$firstPage = current($pages);
if (isset($firstPage[self::$sortingField])) {
// Make sure the order property is either "asc" or "desc" (default is "asc")
if (!empty($order)) {
$order = strtolower($order);
if ($order !== 'desc') {
$order = 'asc';
}
}
$sortMultiplier = $order === 'asc' ? 1 : -1;
uasort($pages, static function (array $pageA, array $pageB) use ($sortMultiplier): int {
return strnatcasecmp($pageA[self::$sortingField], $pageB[self::$sortingField]) * $sortMultiplier;
});
}
}
return $pages;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Menu\Exception;
use TYPO3\CMS\Frontend\Exception;
/**
* No such menu type exception
*/
class NoSuchMenuTypeException extends Exception {}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Menu\Exception\NoSuchMenuTypeException;
/**
* Factory for menu content objects. Allows overriding the default
* types like 'TMENU' with an own implementation (only one possible)
* and new types can be registered.
* @internal this is only used for internal purposes and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class MenuContentObjectFactory implements SingletonInterface
{
/**
* Register of TypoScript keys to according render class
*/
protected array $menuTypeToClassMapping = [
'TMENU' => TextMenuContentObject::class,
];
/**
* Gets a typo script string like 'TMENU' and returns an object of this type
*
* @throws Exception\NoSuchMenuTypeException
*/
public function getMenuObjectByType(string $type = ''): AbstractMenuContentObject
{
$upperCasedClassName = strtoupper($type);
if (array_key_exists($upperCasedClassName, $this->menuTypeToClassMapping)) {
/** @var AbstractMenuContentObject $object */
$object = GeneralUtility::makeInstance($this->menuTypeToClassMapping[$upperCasedClassName]);
return $object;
}
throw new NoSuchMenuTypeException(
'Menu type ' . (string)$type . ' has no implementing class.',
1363278130
);
}
/**
* Register new menu type or override existing type
*
* @param string $type Menu type to be used in TypoScript
* @param string $className Class rendering the menu
*/
public function registerMenuType(string $type, string $className)
{
$this->menuTypeToClassMapping[strtoupper($type)] = $className;
}
}
@@ -0,0 +1,166 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Extension class creating text based menus
*/
class TextMenuContentObject extends AbstractMenuContentObject
{
/**
* Traverses the ->result array of menu items configuration (made by ->generate()) and renders each item.
* An instance of ContentObjectRenderer is also made and for each menu item rendered it is loaded with
* the record for that page so that any stdWrap properties that applies will have the current menu items record available.
*
* @return string The HTML for the menu including submenus
*/
public function writeMenu()
{
if (empty($this->result)) {
return '';
}
$register = $this->request->getAttribute('frontend.register.stack')->current();
$cObjectForCurrentMenu = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$menuContent = [];
$typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
$subMenuObjSuffixes = $typoScriptService->explodeConfigurationForOptionSplit(['sOSuffix' => $this->mconf['submenuObjSuffixes'] ?? null], count($this->result));
$explicitSpacerRenderingEnabled = ($this->mconf['SPC'] ?? false);
foreach ($this->result as $key => $val) {
$register->set('count_HMENU_MENUOBJ', (int)$register->get('count_HMENU_MENUOBJ', 0) + 1);
$register->set('count_MENUOBJ', (int)$register->get('count_MENUOBJ', 0) + 1);
// Initialize the cObj with the page record of the menu item
$cObjectForCurrentMenu->setRequest($this->request);
$cObjectForCurrentMenu->start($this->menuArr[$key], 'pages');
$this->I = [];
$this->I['key'] = $key;
$this->I['val'] = $val;
$this->I['title'] = $this->getPageTitle($this->menuArr[$key]['title'] ?? '', $this->menuArr[$key]['nav_title'] ?? '');
$this->I['title.'] = $this->I['val']['stdWrap.'] ?? [];
$this->I['title'] = $cObjectForCurrentMenu->stdWrapValue('title', $this->I);
$this->I['uid'] = $this->menuArr[$key]['uid'] ?? 0;
$this->I['mount_pid'] = $this->menuArr[$key]['mount_pid'] ?? 0;
$this->I['pid'] = $this->menuArr[$key]['pid'] ?? 0;
$this->I['spacer'] = $this->menuArr[$key]['isSpacer'] ?? false;
// Make link tag
$this->I['val']['additionalParams'] = $cObjectForCurrentMenu->stdWrapValue('additionalParams', $this->I['val']);
$linkResult = $this->link((int)$key, (string)($this->I['val']['altTarget'] ?? ''), ($this->mconf['forceTypeValue'] ?? ''));
if ($linkResult === null) {
$this->I['val']['doNotLinkIt'] = 1;
}
// Title attribute of links
$titleAttrValue = $cObjectForCurrentMenu->stdWrapValue('ATagTitle', $this->I['val']);
if ($linkResult && $titleAttrValue !== '') {
$linkResult = $linkResult->withAttribute('title', $titleAttrValue);
}
$this->I['linkHREF'] = $linkResult;
$this->I['val']['doNotLinkIt'] = (bool)$cObjectForCurrentMenu->stdWrapValue('doNotLinkIt', $this->I['val']);
// Compile link tag
if (!$this->I['spacer'] && !$this->I['val']['doNotLinkIt']) {
$this->setATagParts($linkResult);
} else {
$this->I['A1'] = '';
$this->I['A2'] = '';
}
// ATagBeforeWrap processing:
if ($this->I['val']['ATagBeforeWrap'] ?? false) {
$wrapPartsBefore = explode('|', $this->I['val']['linkWrap'] ?? '');
$wrapPartsAfter = ['', ''];
} else {
$wrapPartsBefore = ['', ''];
$wrapPartsAfter = explode('|', $this->I['val']['linkWrap'] ?? '');
}
if (($this->I['val']['stdWrap2'] ?? false) || isset($this->I['val']['stdWrap2.'])) {
$stdWrap2 = (string)(isset($this->I['val']['stdWrap2.']) ? $cObjectForCurrentMenu->stdWrap('|', $this->I['val']['stdWrap2.']) : '|');
$stdWrap2Value = (string)($this->I['val']['stdWrap2'] ?? '|');
$stdWrap2Value = $stdWrap2Value !== '' ? $stdWrap2Value : '|';
$wrapPartsStdWrap = explode($stdWrap2Value, $stdWrap2);
} else {
$wrapPartsStdWrap = ['', ''];
}
// Make before, middle and after parts
$this->I['parts'] = [];
$this->I['parts']['before'] = $this->getBeforeAfter('before', $cObjectForCurrentMenu);
$this->I['parts']['stdWrap2_begin'] = $wrapPartsStdWrap[0];
// stdWrap for doNotShowLink
$this->I['val']['doNotShowLink'] = $cObjectForCurrentMenu->stdWrapValue('doNotShowLink', $this->I['val']);
if (!$this->I['val']['doNotShowLink']) {
$this->I['parts']['notATagBeforeWrap_begin'] = $wrapPartsAfter[0];
$this->I['parts']['ATag_begin'] = $this->I['A1'];
$this->I['parts']['ATagBeforeWrap_begin'] = $wrapPartsBefore[0];
$this->I['parts']['title'] = $this->I['title'];
$this->I['parts']['ATagBeforeWrap_end'] = $wrapPartsBefore[1] ?? '';
$this->I['parts']['ATag_end'] = $this->I['A2'];
$this->I['parts']['notATagBeforeWrap_end'] = $wrapPartsAfter[1] ?? '';
}
$this->I['parts']['stdWrap2_end'] = $wrapPartsStdWrap[1] ?? '';
$this->I['parts']['after'] = $this->getBeforeAfter('after', $cObjectForCurrentMenu);
// Passing I to a user function
if ($this->mconf['IProcFunc'] ?? false) {
$this->I = $this->userProcess('IProcFunc', $this->I);
}
// Merge parts + beforeAllWrap
$this->I['theItem'] = implode('', $this->I['parts']);
$allWrap = $cObjectForCurrentMenu->stdWrapValue('allWrap', $this->I['val']);
$this->I['theItem'] = $cObjectForCurrentMenu->wrap($this->I['theItem'], $allWrap);
if ($this->I['val']['subst_elementUid'] ?? false) {
$this->I['theItem'] = str_replace('{elementUid}', (string)$this->I['uid'], $this->I['theItem']);
}
if (is_array($this->I['val']['allStdWrap.'] ?? null)) {
$this->I['theItem'] = $cObjectForCurrentMenu->stdWrap($this->I['theItem'], $this->I['val']['allStdWrap.']);
}
$isSpacerPage = $this->I['spacer'] ?? false;
// If rendering of SPACERs is enabled, also allow rendering submenus with Spacers
if (!$isSpacerPage || $explicitSpacerRenderingEnabled) {
// Add part to the accumulated result + fetch submenus
$this->I['theItem'] .= $this->subMenu($this->I['uid'], $subMenuObjSuffixes[$key]['sOSuffix'] ?? '', $key);
}
$part = $cObjectForCurrentMenu->stdWrapValue('wrapItemAndSub', $this->I['val']);
$menuContent[] = $part ? $cObjectForCurrentMenu->wrap($this->I['theItem'], $part) : $this->I['theItem'];
}
$menuContent = implode('', $menuContent);
if (is_array($this->mconf['stdWrap.'] ?? null)) {
$menuContent = (string)$cObjectForCurrentMenu->stdWrap($menuContent, $this->mconf['stdWrap.']);
}
return $cObjectForCurrentMenu->wrap($menuContent, $this->mconf['wrap'] ?? '');
}
/**
* Generates the before* and after* stdWrap for TMENUs
* Evaluates:
* - before.stdWrap*
* - beforeWrap
* - after.stdWrap*
* - afterWrap
*
* @param string $pref Can be "before" or "after" and determines which kind of stdWrap to process (basically this is the prefix of the TypoScript properties that are read from the ->I['val'] array
* @return string The resulting HTML
*/
protected function getBeforeAfter(string $pref, ContentObjectRenderer $cObjectForCurrentMenu): string
{
$processedPref = $cObjectForCurrentMenu->stdWrapValue($pref, $this->I['val']);
if (isset($this->I['val'][$pref . 'Wrap'])) {
return $cObjectForCurrentMenu->wrap($processedPref, $this->I['val'][$pref . 'Wrap']);
}
return $processedPref;
}
}
@@ -0,0 +1,175 @@
<?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\Frontend\ContentObject;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Page\PageLayoutResolver;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
use TYPO3Fluid\Fluid\View\Exception\InvalidLayoutException;
use TYPO3Fluid\Fluid\View\Exception\InvalidPartialException;
use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;
/**
* PAGEVIEW Content Object.
*
* Built to render a full page with Fluid, and does the following
* - uses the template from the given Page Layout / Backend Layout of the current page in a folder "Pages/Mylayout.html"
* - paths are resolved from "paths." configuration
* - automatically adds templateRootPaths to the layoutRootPaths and partialRootPaths
* - injects pageInformation, site and siteLanguage (= language) as variables by default
* - adds all page settings (= TypoScript constants) into the settings variable of the View
*
* In contrast to FLUIDTEMPLATE, by design this cObject
* - does not handle custom layoutRootPaths and partialRootPaths
* - does not handle Extbase specialities
* - does not handle "templateName.", "template." and "file." resolving from cObject
*/
final class PageViewContentObject extends AbstractContentObject
{
private const array reservedVariables = ['site', 'language', 'page'];
public function __construct(
private readonly ContentDataProcessor $contentDataProcessor,
private readonly TypoScriptService $typoScriptService,
private readonly PageLayoutResolver $pageLayoutResolver,
private readonly ViewFactoryInterface $viewFactory,
) {}
/**
* Rendering the cObject, PAGEVIEW
*
* Configuration properties:
* - paths array to template files
* - variables array of cObjects, the keys are the variable names in fluid
* - dataProcessing array of data processors which are classes to manipulate $data
*
* Example:
* page.10 = PAGEVIEW
* page.10.paths.10 = EXT:site_configuration/Resources/Private/Templates/
* page.10.variables {
* mylabel = TEXT
* mylabel.value = Label from TypoScript
* }
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string The HTML output
* @throws ContentRenderingException
*/
public function render($conf = []): string
{
if (!is_array($conf)) {
$conf = [];
}
if (!is_array($conf['paths.'] ?? false) || $conf['paths.'] === []) {
throw new ContentRenderingException(
'PAGEVIEW content object needs a "paths." TypoScript array',
1724601907
);
}
$paths = array_map(PathUtility::sanitizeTrailingSeparator(...), $conf['paths.']);
$viewFactoryData = new ViewFactoryData(
// @todo: Do discuss: Rename 'paths.' to 'templateRootPaths.' again?
templateRootPaths: array_map(static fn(string $path): string => $path . 'Pages/', $paths),
// @todo: We should *still* allow setting both partialRootPaths and layoutRootPaths, and only fall back to
// [templateRootPaths]/Partials and [templateRootPaths]/Layouts if not set. And the fallback should be
// advertised as best practice.
partialRootPaths: array_map(static fn(string $path): string => $path . 'Partials/', $paths),
layoutRootPaths: array_map(static fn(string $path): string => $path . 'Layouts/', $paths),
request: $this->request,
);
$view = $this->viewFactory->create($viewFactoryData);
$pageSettings = $this->request->getAttribute('frontend.typoscript')->getSettingsTree()->toArray();
$view->assign('settings', $this->typoScriptService->convertTypoScriptArrayToPlainArray($pageSettings));
$variables = $this->getContentObjectVariables($conf);
$variables = $this->contentDataProcessor->process($this->cObj, $conf, $variables);
$view->assignMultiple($variables);
// Fetch the Fluid template by the name of the Page Layout and underneath "Pages"
$pageInformationObject = $this->request->getAttribute('frontend.page.information');
$pageLayoutName = $this->pageLayoutResolver->getLayoutIdentifierForPageWithoutPrefix(
$pageInformationObject->getPageRecord(),
$pageInformationObject->getRootLine()
);
try {
return $view->render($pageLayoutName);
} catch (InvalidTemplateResourceException $e) {
// Only add a PAGEVIEW specific message in case the exception has been thrown for the given template.
if ($e instanceof InvalidPartialException || $e instanceof InvalidLayoutException || $e->templateName !== 'Default/' . $pageLayoutName) {
throw $e;
}
throw new InvalidTemplateResourceException(
sprintf(
'PAGEVIEW TypoScript object: Failed to resolve a template file for page layout "%s". See also: %s. The following paths were checked: "%s"',
$pageLayoutName,
Typo3Information::getDocsLink('t3tsref:cobj-pageview'),
implode('", "', $e->evaluatedTemplatePaths),
),
1742058289,
$e,
$e->templateName,
$e->evaluatedTemplatePaths,
);
}
}
/**
* Compile rendered content objects in variables array ready to assign to the view
*
* @param array $conf Configuration array
* @return array the variables to be assigned
*/
private function getContentObjectVariables(array $conf): array
{
$pageInformation = $this->request->getAttribute('frontend.page.information');
$variables = [
'site' => $this->request->getAttribute('site'),
'language' => $this->request->getAttribute('language'),
'page' => $pageInformation,
];
// Accumulate the variables to be process and loop them through cObjGetSingle
if (is_array($conf['variables.'] ?? false) && $conf['variables.'] !== []) {
foreach ($conf['variables.'] as $variableName => $cObjType) {
if (!is_string($cObjType)) {
continue;
}
if (in_array($variableName, self::reservedVariables, true)) {
throw new \InvalidArgumentException(
'Cannot use reserved name "' . $variableName . '" as variable name in PAGEVIEW.',
1711748615
);
}
$cObjConf = $conf['variables.'][$variableName . '.'] ?? [];
$variables[$variableName] = $this->cObj->cObjGetSingle($cObjType, $cObjConf, 'variables.' . $variableName);
}
}
if (!($conf['contentAs'] ?? false) && isset($variables['content'])) {
throw new \InvalidArgumentException(
'No variable name ("contentAs" option) for the content areas has been defined in PAGEVIEW, and the fallback name "content" is not available because it has been manually set.',
1726475574
);
}
$variables[$conf['contentAs'] ?? 'content'] = $pageInformation->getPageLayout()?->getContentAreas()->withRequest($this->request);
return $variables;
}
}
@@ -0,0 +1,224 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Category\Collection\CategoryCollection;
/**
* Contains RECORDS class object.
*/
class RecordsContentObject extends AbstractContentObject
{
/**
* List of all items with table and uid information
*
* @var array
*/
protected $itemArray = [];
/**
* List of all selected records with full data, arranged per table
*
* @var array
*/
protected $data = [];
public function __construct(protected readonly TimeTracker $timeTracker) {}
/**
* Rendering the cObject, RECORDS
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Output
*/
public function render($conf = [])
{
// Reset items and data
$this->itemArray = [];
$this->data = [];
$theValue = '';
$tables = (string)$this->cObj->stdWrapValue('tables', $conf ?? []);
if ($tables !== '') {
$tablesArray = array_unique(GeneralUtility::trimExplode(',', $tables, true));
// Add tables which have a configuration (note that this may create duplicate entries)
if (is_array($conf['conf.'] ?? false)) {
foreach ($conf['conf.'] as $key => $value) {
if (!str_ends_with($key, '.') && !in_array($key, $tablesArray)) {
$tablesArray[] = $key;
}
}
}
// Get the data, depending on collection method.
// Property "source" is considered more precise and thus takes precedence over "categories"
$source = (string)$this->cObj->stdWrapValue('source', $conf ?? []);
$categories = (string)$this->cObj->stdWrapValue('categories', $conf ?? []);
if ($source !== '') {
$this->collectRecordsFromSource($source, $tablesArray);
} elseif ($categories !== '') {
$relationField = (string)$this->cObj->stdWrapValue('relation', $conf['categories.'] ?? []);
$this->collectRecordsFromCategories($categories, $tablesArray, $relationField);
}
if (!empty($this->itemArray)) {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setParent($this->cObj->data, $this->cObj->currentRecord);
$pageRepository = $this->getPageRepository();
foreach ($this->itemArray as $val) {
$row = $this->data[$val['table']][$val['id']] ?? null;
if (!is_array($row)) {
continue;
}
// Perform overlays if necessary (records coming from category collections are already overlaid)
if ($source !== '') {
// Versioning preview
$pageRepository->versionOL($val['table'], $row);
// Language overlay
if (is_array($row)) {
$row = $pageRepository->getLanguageOverlay($val['table'], $row);
}
}
// Might be unset during the overlay process
if (!is_array($row)) {
continue;
}
if ($this->isRecordsPageAccessible($val['table'], $row, $conf)) {
$renderObjName = ($conf['conf.'][$val['table']] ?? false) ? $conf['conf.'][$val['table']] : '<' . $val['table'];
$renderObjKey = ($conf['conf.'][$val['table']] ?? false) ? 'conf.' . $val['table'] : '';
$renderObjConf = ($conf['conf.'][$val['table'] . '.'] ?? false) ? $conf['conf.'][$val['table'] . '.'] : [];
$this->cObj->lastChanged($row['tstamp'] ?? 0);
$cObj->setRequest($this->request);
$cObj->start($row, $val['table']);
$tmpValue = $cObj->cObjGetSingle($renderObjName, $renderObjConf, $renderObjKey);
$theValue .= $tmpValue;
}
}
}
}
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ($wrap) {
$theValue = $this->cObj->wrap($theValue, $wrap);
}
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
/**
* Checks if the records page is accessible
*/
protected function isRecordsPageAccessible(string $table, array $row, array $conf): bool
{
$pageId = (int)($table === 'pages' ? $row['uid'] : $row['pid']);
if ($pageId === $this->request->getAttribute('frontend.page.information')->getId()) {
// Access to current page has already been checked before rendering this content object.
return true;
}
if ($this->cObj->stdWrapValue('dontCheckPid', $conf)) {
return true;
}
$validPageId = $this->getPageRepository()->filterAccessiblePageIds([$pageId]);
return $validPageId !== [];
}
/**
* Collects records according to the configured source
*
* @param string $source Source of records
* @param array $tables List of tables
*/
protected function collectRecordsFromSource($source, array $tables)
{
$loadDB = GeneralUtility::makeInstance(RelationHandler::class);
$loadDB->start($source, implode(',', $tables));
foreach ($loadDB->tableArray as $table => $v) {
$constraints = $this->getPageRepository()->getDefaultConstraints($table);
if ($constraints !== []) {
$loadDB->additionalWhere[$table] = implode(' AND ', $constraints);
}
}
$this->data = $loadDB->getFromDB();
reset($loadDB->itemArray);
$this->itemArray = $loadDB->itemArray;
}
/**
* Collects records for all selected tables and categories.
*
* @param string $selectedCategories Comma-separated list of categories
* @param array $tables List of tables
* @param string $relationField Name of the field containing the categories relation
*/
protected function collectRecordsFromCategories($selectedCategories, array $tables, $relationField)
{
$selectedCategories = array_unique(GeneralUtility::intExplode(',', $selectedCategories, true));
// Loop on all selected tables
foreach ($tables as $table) {
// Get the records for each selected category
$tableRecords = [];
$categoriesPerRecord = [];
foreach ($selectedCategories as $aCategory) {
try {
$collection = CategoryCollection::load(
$aCategory,
true,
$table,
$relationField
);
if ($collection->count() > 0) {
// Add items to the collection of records for the current table
foreach ($collection as $item) {
$tableRecords[$item['uid']] = $item;
// Keep track of all categories a given item belongs to
if (!isset($categoriesPerRecord[$item['uid']])) {
$categoriesPerRecord[$item['uid']] = [];
}
$categoriesPerRecord[$item['uid']][] = $aCategory;
}
}
} catch (\Exception $e) {
$message = sprintf(
'Could not get records for category id %d. Error: %s (%d)',
$aCategory,
$e->getMessage(),
$e->getCode()
);
$this->timeTracker->setTSlogMessage($message, LogLevel::WARNING);
}
}
// Store the resulting records into the itemArray and data results array
if (!empty($tableRecords)) {
$this->data[$table] = [];
foreach ($tableRecords as $record) {
$this->itemArray[] = [
'id' => $record['uid'],
'table' => $table,
];
// Add to the record the categories it belongs to
$record['_categories'] = implode(',', $categoriesPerRecord[$record['uid']]);
$this->data[$table][$record['uid']] = $record;
}
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
/**
* A simple key/value store. See class RegisterStack for more information.
*
* @internal This class is not part of the TYPO3 Core API
*/
final class Register
{
private array $keyValues = [];
/**
* Unable to deal with objects, accepts simple types only.
* This is done by intention to not run into issues if this
* state needs to be serialized (cached).
*/
public function set(string $key, string|int|bool|float $value): void
{
$this->keyValues[$key] = $value;
}
public function get(string $key, string|int|bool|float|null $default = null): string|int|bool|float|null
{
return $this->keyValues[$key] ?? $default;
}
}
+86
View File
@@ -0,0 +1,86 @@
<?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\Frontend\ContentObject;
/**
* Contains TypoScript "Register" state together with class Register.
*
* An instance of this class is created during frontend rendering and registered
* as Request attribute "frontend.register.stack".
*
* The TypoScript "register" is a key-value store (implemented by class Register)
* combined with a stack. This has probably been invented for menu rendering back
* then: content objects in general and TypoScript based menu rendering specifically
* can be nested when multiple menu depths/layers are rendered. Each of those layers
* may need own values within their scope.
*
* The two main usages are TypoScript data/getText to access register entries along
* with content objects LOAD_REGISTER and RESTORE_REGISTER which push and pop register
* objects from the stack and store key/value entries.
*
* There is one quirk in comparison to a "classic" stack: New Register instances are
* typically clones of the underlying Register instance plus new key/values, see class
* LoadRegisterContentObject for an implementation: Values can be set on a lower
* level register and is still "seen/part of" a register on top. key/values bubble up,
* but not down.
*
* @internal This class is not part of the TYPO3 Core API
*/
final class RegisterStack
{
/**
* @var Register[]
*/
private array $registerStack = [];
public function __construct()
{
$this->push(new Register());
}
/**
* Peek current Register. Does not change stack.
*/
public function current(): Register
{
return array_last($this->registerStack);
}
/**
* Add a new Register instance to top of stack
*/
public function push(Register $register): void
{
$this->registerStack[] = $register;
}
/**
* Remove top of stack Register and return it.
* Re-inits with an empty register if empty to avoid exception or nullable handling
* in consumers if there is for example a "RESTORE_REGISTER" cObj too much. As
* drawback, consumers never know if there is a leftover pop().
*/
public function pop(): Register
{
$register = array_pop($this->registerStack);
if (empty($this->registerStack)) {
$this->push(new Register());
}
return $register;
}
}
@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
/**
* Implement cObj "RESTORE_REGISTER":
* As the counterpart of "LOAD_REGISTER", "RESTORE_REGISTER" removes any state
* added by latest "LOAD_REGISTER" again.
*/
class RestoreRegisterContentObject extends AbstractContentObject
{
/**
* Does not return any content, it just sets internal data based on the TypoScript properties.
*
* @param array $conf Array of TypoScript properties
* @return string Empty string
*/
public function render($conf = []): string
{
$this->request->getAttribute('frontend.register.stack')->pop();
return '';
}
}
@@ -0,0 +1,150 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use TYPO3\CMS\Core\Imaging\Exception\InvalidSvgException;
use TYPO3\CMS\Core\Imaging\Svg\SvgDocumentFactory;
use TYPO3\CMS\Core\Imaging\Svg\SvgDocumentService;
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceDoesNotExistException;
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceException;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Type\PublicResourceInterface;
use TYPO3\CMS\Core\SystemResource\Type\SystemResourceInterface;
/**
* Contains SVG content object.
*/
class ScalableVectorGraphicsContentObject extends AbstractContentObject
{
public function __construct(
protected readonly SystemResourceFactory $resourceFactory,
protected readonly SystemResourcePublisherInterface $resourcePublisher,
protected readonly SvgDocumentFactory $svgDocumentFactory,
protected readonly SvgDocumentService $svgDocumentService,
) {}
/**
* Rendering the cObject, SVG
*
* @param array $conf Array of TypoScript properties
*/
public function render($conf = []): string
{
$renderMode = $this->cObj->stdWrapValue('renderMode', $conf);
if ($renderMode === 'inline') {
return $this->renderInline($conf);
}
return $this->renderObject($conf);
}
protected function renderInline(array $conf): string
{
$resource = $this->resolveResource($conf);
[$width, $height, $isDefaultWidth, $isDefaultHeight] = $this->getDimensions($conf);
$content = $svgContent = '';
if ($resource instanceof SystemResourceInterface) {
try {
$svgContent = $resource->getContents();
} catch (SystemResourceDoesNotExistException) {
}
}
if ($svgContent !== '') {
try {
$document = $this->svgDocumentFactory->fromStringAndSanitize($svgContent);
if (!$isDefaultWidth) {
$document->documentElement->setAttribute('width', (string)$width);
}
if (!$isDefaultHeight) {
$document->documentElement->setAttribute('height', (string)$height);
}
$content = $this->svgDocumentService->toInlineMarkup($document);
} catch (InvalidSvgException) {
$content = '';
}
} else {
$value = $this->cObj->stdWrapValue('value', $conf);
if (!empty($value)) {
$content = [];
$content[] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' . (int)$width . '" height="' . (int)$height . '">';
$content[] = $value;
$content[] = '</svg>';
$content = implode(LF, $content);
}
}
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
/**
* Render the SVG as <object> tag
*/
protected function renderObject(array $conf): string
{
$resource = $this->resolveResource($conf);
[$width, $height] = $this->getDimensions($conf);
$content = [];
if ($resource !== null) {
$uri = $this->resourcePublisher->generateUri($resource, $this->request);
$content[] = '<!--[if IE]>';
$content[] = ' <object src="' . htmlspecialchars($uri) . '" classid="image/svg+xml" width="' . (int)$width . '" height="' . (int)$height . '">';
$content[] = '<![endif]-->';
$content[] = '<!--[if !IE]>-->';
$content[] = ' <object data="' . htmlspecialchars($uri) . '" type="image/svg+xml" width="' . (int)$width . '" height="' . (int)$height . '">';
$content[] = '<!--<![endif]-->';
$content[] = '</object>';
}
$content = implode(LF, $content);
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
protected function resolveResource(array $conf): ?PublicResourceInterface
{
try {
$resourceIdentifier = (string)$this->cObj->stdWrapValue('src', $conf);
return $this->resourceFactory->createPublicResource($resourceIdentifier);
} catch (SystemResourceException) {
return null;
}
}
protected function getDimensions(array $conf): array
{
$isDefaultWidth = false;
$isDefaultHeight = false;
$width = $this->cObj->stdWrapValue('width', $conf);
$height = $this->cObj->stdWrapValue('height', $conf);
if (empty($width)) {
$isDefaultWidth = true;
$width = 600;
}
if (empty($height)) {
$isDefaultHeight = true;
$height = 400;
}
return [$width, $height, $isDefaultWidth, $isDefaultHeight];
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
/**
* Contains TEXT class object.
*/
class TextContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, TEXT
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Output
*/
public function render($conf = [])
{
if (!is_array($conf)) {
return '';
}
$content = '';
if (isset($conf['value'])) {
$content = $conf['value'];
unset($conf['value']);
}
if (isset($conf['value.'])) {
$content = $this->cObj->stdWrap($content, $conf['value.']);
unset($conf['value.']);
}
if (!empty($conf)) {
$content = $this->cObj->stdWrap($content, $conf);
}
return $content;
}
}
@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Contains USER class object.
*/
class UserContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, USER
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (empty($conf)) {
$this->getTimeTracker()->setTSlogMessage('USER without configuration.', LogLevel::WARNING);
return '';
}
$content = '';
if ($this->cObj->getUserObjectType() === false) {
// Render this if we are a delayed non cached object
$this->cObj->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER);
}
$tempContent = $this->cObj->callUserFunction($conf['userFunc'] ?? '', $conf, '');
if ($this->cObj->doConvertToUserIntObject) {
$this->cObj->doConvertToUserIntObject = false;
$content = $this->cObj->cObjGetSingle('USER_INT', $conf);
} else {
$content .= $tempContent;
// Only executed when the element is not converted to USER_INT
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
}
$this->cObj->setUserObjectType(false);
return $content;
}
/**
* @return TimeTracker
*/
protected function getTimeTracker()
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
}
@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Contains USER_INT class object.
*/
class UserInternalContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, USER_INT
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
$this->cObj->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER_INT);
$substKey = 'INT_SCRIPT.' . md5(StringUtility::getUniqueId());
$pageParts = $this->request->getAttribute('frontend.page.parts');
$pageParts->addNotCachedContentElement([
'substKey' => $substKey,
'conf' => $conf,
'cObjData' => serialize($this->cObj->getState()),
'type' => 'FUNC',
]);
$this->cObj->setUserObjectType(false);
return '<!--' . $substKey . '-->';
}
}
+227
View File
@@ -0,0 +1,227 @@
<?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\Frontend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Controller\ErrorPageController;
use TYPO3\CMS\Core\Error\Http\InternalServerErrorException;
use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerNotConfiguredException;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Middleware\ContentSecurityPolicyHeaders;
/**
* This controller provides actions for common HTTP error scenarios (404, 403, 500, 503) and supports custom error
* handling through site-specific error handlers. If no custom error handler is configured, it falls back to
* rendering a standard TYPO3 error page with appropriate status code and message.
*/
#[Autoconfigure(public: true)]
readonly class ErrorController
{
public function __construct(
private ContentSecurityPolicyHeaders $contentSecurityPolicyHeaders,
) {}
/**
* Used for creating a 500 response ("Internal Server Error"), usually due to some misconfiguration.
* If a page unavailable handler is configured, a RedirectResponse could be returned as well.
*
* @throws InternalServerErrorException
*/
public function internalErrorAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
if ($this->isRequestFromDevIp($request)) {
throw new InternalServerErrorException($message, 1607585445);
}
$errorHandler = $this->getErrorHandlerFromSite($request, 500);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
$response = $this->handleError(
$request,
500,
'Internal Server Error',
'An error occurred while processing your request. Please try again later.',
$message
);
return $this->contentSecurityPolicyHeaders->applyToResponse($request, $response);
}
/**
* Used for creating a 503 response ("Service Unavailable"), to be used for maintenance mode
* or when the server is overloaded, a RedirectResponse could be returned as well.
*
* @throws ServiceUnavailableException
*/
public function unavailableAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
if ($this->isRequestFromDevIp($request)) {
throw new ServiceUnavailableException($message, 1518472181);
}
$errorHandler = $this->getErrorHandlerFromSite($request, 503);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
$response = $this->handleError(
$request,
503,
'Service Unavailable',
'The application is currently down for maintenance. Please check back shortly.',
$message
);
return $this->contentSecurityPolicyHeaders->applyToResponse($request, $response);
}
/**
* Used for creating a 404 response ("Page Not Found"), but if configured, a RedirectResponse could be returned
* as well.
*
* @throws PageNotFoundException
*/
public function pageNotFoundAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
$errorHandler = $this->getErrorHandlerFromSite($request, 404);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
try {
$response = $this->handleError(
$request,
404,
'Page Not Found',
'The page did not exist or was inaccessible.',
$message
);
return $this->contentSecurityPolicyHeaders->applyToResponse($request, $response);
} catch (\RuntimeException) {
throw new PageNotFoundException($message, 1518472189);
}
}
/**
* Used for creating a 403 response ("Access denied"), but if configured, a RedirectResponse could be returned
* as well.
*
* @throws PageNotFoundException
*/
public function accessDeniedAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
$errorHandler = $this->getErrorHandlerFromSite($request, 403);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
try {
$response = $this->handleError(
$request,
403,
'Access Denied',
'You do not have the necessary permissions to access this resource.',
$message
);
return $this->contentSecurityPolicyHeaders->applyToResponse($request, $response);
} catch (\RuntimeException) {
throw new PageNotFoundException($message, 1518472195);
}
}
/**
* Used for creating an error with a custom status code, but if configured, a RedirectResponse could be
* returned as well.
*
* @param array<string, mixed> $reasons An array of reasons for evaluation in a possible resolved the error handler
*
* @throws PageNotFoundException
*/
public function customErrorAction(
ServerRequestInterface $request,
int $statusCode,
string $title,
string $message,
string $technicalReason = '',
array $reasons = [],
int $errorCode = 0
): ResponseInterface {
$errorHandler = $this->getErrorHandlerFromSite($request, $statusCode);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
try {
return $this->handleError($request, $statusCode, $title, $message, $technicalReason, $errorCode);
} catch (\RuntimeException) {
throw new PageNotFoundException($message, 1770466857);
}
}
/**
* Checks whether the devIPMask matches the current visitor's IP address.
*
* @return bool False if the server error handler should be used.
*/
protected function isRequestFromDevIp(ServerRequestInterface $request): bool
{
$normalizedParams = $request->getAttribute('normalizedParams');
return GeneralUtility::cmpIP($normalizedParams->getRemoteAddress(), $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']);
}
/**
* Checks if a site is configured, and an error handler is configured for this specific status code.
*/
protected function getErrorHandlerFromSite(ServerRequestInterface $request, int $statusCode): ?PageErrorHandlerInterface
{
$site = $request->getAttribute('site');
if ($site instanceof Site) {
try {
return $site->getErrorHandler($statusCode);
} catch (PageErrorHandlerNotConfiguredException $e) {
// No error handler found, so fallback back to the generic TYPO3 error handler.
}
}
return null;
}
/**
* Handles the error by creating a response object. Acts as a fallback when no error handler is configured.
*/
protected function handleError(
ServerRequestInterface $request,
int $statusCode,
string $title,
string $message,
string $technicalReason = '',
int $errorCode = 0
): ResponseInterface {
if (str_contains($request->getHeaderLine('Accept'), 'application/json')) {
return new JsonResponse(['reason' => $technicalReason], $statusCode);
}
$content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
$title,
$message . ($technicalReason ? ' Reason: ' . $technicalReason : ''),
$errorCode,
$statusCode
);
return new HtmlResponse($content, $statusCode);
}
}
+245
View File
@@ -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\Frontend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Crypto\HashAlgo;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Exception;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* eID-Script "tx_cms_showpic"
*
* Shows a picture from FAL in enlarged format in a separate window.
* Picture file and settings is supplied by GET-parameters:
*
* - file = fileUid or Combined Identifier
* - encoded in a parameter Array (with weird format - see ContentObjectRenderer about ll. 1500)
* - width, height = usual width an height, m/c supported
* - frame
* - bodyTag
* - title
*
* @internal this is a concrete TYPO3 implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
#[Autoconfigure(public: true)]
class ShowImageController
{
protected const ALLOWED_PARAMETER_NAMES = ['width', 'height', 'crop', 'bodyTag', 'title'];
/**
* @var \Psr\Http\Message\ServerRequestInterface
*/
protected $request;
/**
* @var File|Folder|null
*/
protected $file;
/**
* @var string
*/
protected $width;
/**
* @var int
*/
protected $height;
/**
* @var string
*/
protected $crop;
/**
* @var int|null
*/
protected $frame;
/**
* @var string
*/
protected $bodyTag = '<body>';
/**
* @var string
*/
protected $title = 'Image';
/**
* @var string
*/
protected $content = <<<EOF
<!DOCTYPE html>
<html>
<head>
<title>###TITLE###</title>
<meta name="robots" content="noindex,follow" />
</head>
###BODY###
###IMAGE###
</body>
</html>
EOF;
public function __construct(
protected readonly Features $features,
private readonly FileNameValidator $fileNameValidator,
private readonly ResourceFactory $resourceFactory,
) {}
/**
* Init function, setting the input vars in the global space.
*
* @throws \InvalidArgumentException
* @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException
*/
public function initialize()
{
$fileUid = $this->request->getQueryParams()['file'] ?? null;
$parametersArray = $this->request->getQueryParams()['parameters'] ?? null;
// If no file-param or parameters are given, we must exit
if (!$fileUid || !isset($parametersArray) || !is_array($parametersArray)) {
throw new \InvalidArgumentException('No valid fileUid given', 1476048455);
}
// rebuild the parameter array and check if the HMAC is correct
$parametersEncoded = implode('', $parametersArray);
/* For backwards compatibility the HMAC is transported within the md5 param */
$hmacParameter = $this->request->getQueryParams()['md5'] ?? null;
$hashService = GeneralUtility::makeInstance(HashService::class);
$hmac = $hashService->hmac(implode('|', [$fileUid, $parametersEncoded]), 'tx_cms_showpic', HashAlgo::SHA3_256);
if (!is_string($hmacParameter) || !hash_equals($hmac, $hmacParameter)) {
throw new \InvalidArgumentException('hash does not match', 1476048456);
}
// decode the parameters Array - `bodyTag` contains HTML if set and would lead
// to a false-positive XSS-detection, that's why parameters are base64-encoded
$parameters = json_decode(base64_decode($parametersEncoded), true) ?? [];
foreach ($parameters as $parameterName => $parameterValue) {
if (in_array($parameterName, static::ALLOWED_PARAMETER_NAMES, true)) {
$this->{$parameterName} = $parameterValue;
}
}
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
$this->file = $this->resourceFactory->getFileObject((int)$fileUid);
} else {
$this->file = $this->resourceFactory->retrieveFileOrFolderObject($fileUid);
}
if (!($this->file instanceof FileInterface && $this->isFileValid($this->file))) {
throw new Exception('File processing for local storage is denied', 1594043425);
}
if ($this->features->isFeatureEnabled('security.frontend.allowInsecureFrameOptionInShowImageController')) {
$frameValue = $this->request->getQueryParams()['frame'] ?? null;
if ($frameValue !== null && MathUtility::canBeInterpretedAsInteger($frameValue)) {
$this->frame = (int)$frameValue;
}
}
}
/**
* Main function which creates the image if needed and outputs the HTML code for the page displaying the image.
* Accumulates the content in $this->content
*/
public function main()
{
$processedImage = $this->processImage();
$imageAttributes = [
'src' => $processedImage->getPublicUrl() ?? '',
'alt' => $this->file->getProperty('alternative') ?: $this->title,
'title' => $this->file->getProperty('title') ?: $this->title,
'width' => (string)$processedImage->getProperty('width'),
'height' => (string)$processedImage->getProperty('height'),
];
$markerArray = [
'###TITLE###' => htmlspecialchars($this->file->getProperty('title') ?: $this->title),
'###IMAGE###' => sprintf('<img %s>', GeneralUtility::implodeAttributes($imageAttributes, true)),
'###BODY###' => $this->bodyTag,
];
$this->content = str_replace(array_keys($markerArray), array_values($markerArray), $this->content);
}
/**
* Does the actual image processing
*
* @return \TYPO3\CMS\Core\Resource\ProcessedFile
*/
protected function processImage()
{
$max = str_contains($this->width . $this->height, 'm') ? 'm' : '';
$this->height = MathUtility::forceIntegerInRange($this->height, 0);
$this->width = MathUtility::forceIntegerInRange((int)$this->width, 0) . $max;
$processingConfiguration = [
'width' => $this->width,
'height' => $this->height,
'frame' => $this->frame,
'crop' => $this->crop,
];
return $this->file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
}
/**
* Fetches the content and builds a content file out of it
*
* @param ServerRequestInterface $request the current request object
* @return ResponseInterface the modified response
*/
public function processRequest(ServerRequestInterface $request): ResponseInterface
{
$this->request = $request;
try {
$this->initialize();
$this->main();
$response = new Response();
$response->getBody()->write($this->content);
return $response;
} catch (\InvalidArgumentException $e) {
// add a 410 "gone" if invalid parameters given
return (new Response())->withStatus(410);
} catch (Exception $e) {
return (new Response())->withStatus(404);
}
}
protected function isFileValid(FileInterface $file): bool
{
return $file->getStorage()->getDriverType() !== 'Local'
|| $this->fileNameValidator->isValid(basename($file->getIdentifier()));
}
}
@@ -0,0 +1,103 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Core\Utility\CsvUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* This data processor will take field data formatted as a string, where each line, separated by line feed,
* represents a row. By default columns are separated by the delimiter character "comma ,",
* and can be enclosed by the character 'quotation mark "', like the default in a regular CSV file.
*
* An example of such a field is "bodytext" in the CType "table".
*
* The table data is transformed to a multi dimensional array, taking the delimiter and enclosure into account,
* before it is passed to the view.
*
* Example field data:
*
* This is row 1 column 1|This is row 1 column 2|This is row 1 column 3
* This is row 2 column 1|This is row 2 column 2|This is row 2 column 3
* This is row 3 column 1|This is row 3 column 2|This is row 3 column 3
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\CommaSeparatedValueProcessor
* 10 {
* if.isTrue.field = bodytext
* fieldName = bodytext
* fieldDelimiter = |
* fieldEnclosure = '
* maximumColumns = 2
* as = table
* }
*
* whereas "table" can be used as a variable {table} inside Fluid for iteration.
*
* Using maximumColumns limits the amount of columns in the multi dimensional array.
* In the example, field data of the last column will be stripped off.
*
* Multi line cells are taken into account.
*/
class CommaSeparatedValueProcessor implements DataProcessorInterface
{
/**
* Process CSV field data to split into a multi dimensional array
*
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
{
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
// The field name to process
$fieldName = $cObj->stdWrapValue('fieldName', $processorConfiguration);
if (empty($fieldName)) {
return $processedData;
}
$originalValue = (string)$cObj->data[$fieldName];
// Set the target variable
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, $fieldName);
// Set the maximum amount of columns
$maximumColumns = $cObj->stdWrapValue('maximumColumns', $processorConfiguration, 0);
// Set the field delimiter which is "," by default
$fieldDelimiter = (string)$cObj->stdWrapValue('fieldDelimiter', $processorConfiguration, ',');
// Set the field enclosure which is " by default
$fieldEnclosure = (string)$cObj->stdWrapValue('fieldEnclosure', $processorConfiguration, '"');
$processedData[$targetVariableName] = CsvUtility::csvToArray(
$originalValue,
$fieldDelimiter,
$fieldEnclosure,
(int)$maximumColumns
);
return $processedData;
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use Symfony\Component\DependencyInjection\ServiceLocator;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* Registry for data processors, tagged with "data.processor"
* @internal
*/
readonly class DataProcessorRegistry
{
public function __construct(private ServiceLocator $dataProcessorLocator) {}
public function getDataProcessor(string $identifer): ?DataProcessorInterface
{
if (!$this->dataProcessorLocator->has($identifer)) {
return null;
}
$dataProcessor = $this->dataProcessorLocator->get($identifer);
if (!($dataProcessor instanceof DataProcessorInterface)) {
throw new \UnexpectedValueException(
'Processor with alias / identifier "' . $identifer . '" '
. 'must implement interface "' . DataProcessorInterface::class . '"',
1666131903
);
}
return $dataProcessor;
}
}
@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* Fetch records from the database, using the default .select syntax from TypoScript.
*
* This way, e.g. a FLUIDTEMPLATE cObject can iterate over the array of records.
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor
* 10 {
* table = tt_address
* pidInList = 123
* where = company="Acme" AND first_name="Ralph"
* orderBy = sorting DESC
* as = addresses
* dataProcessing {
* 10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
* 10 {
* references.fieldName = image
* }
* }
* }
*
* where "as" means the variable to be containing the result-set from the DB query.
*/
readonly class DatabaseQueryProcessor implements DataProcessorInterface
{
public function __construct(protected ContentDataProcessor $contentDataProcessor) {}
/**
* Fetches records from the database as an array
*
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
*
* @return array the processed data as key/value store
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
{
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
// the table to query, if none given, exit
$tableName = $cObj->stdWrapValue('table', $processorConfiguration);
if (empty($tableName)) {
return $processedData;
}
if (isset($processorConfiguration['table.'])) {
unset($processorConfiguration['table.']);
}
if (isset($processorConfiguration['table'])) {
unset($processorConfiguration['table']);
}
// The variable to be used within the result
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'records');
// Execute a SQL statement to fetch the records
$records = $cObj->getRecords($tableName, $processorConfiguration);
$request = $cObj->getRequest();
$processedRecordVariables = [];
foreach ($records as $key => $record) {
$recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$recordContentObjectRenderer->setRequest($request);
$recordContentObjectRenderer->start($record, $tableName);
$processedRecordVariables[$key] = ['data' => $record];
$processedRecordVariables[$key] = $this->contentDataProcessor->process($recordContentObjectRenderer, $processorConfiguration, $processedRecordVariables[$key]);
}
$processedData[$targetVariableName] = $processedRecordVariables;
return $processedData;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\Resource\FileCollector;
/**
* This data processor can be used for processing data for record which contain
* relations to sys_file records (e.g. sys_file_reference records) or for fetching
* files directly from UIDs or from folders or collections.
*
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
* 10 {
* references.fieldName = image
* collections = 13,15
* as = myfiles
* }
*
* whereas "myfiles" can further be used as a variable {myfiles} inside a Fluid template for iteration.
*/
class FilesProcessor implements DataProcessorInterface
{
/**
* Process data of a record to resolve File objects to the view
*
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
{
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
// gather data
$fileCollector = GeneralUtility::makeInstance(FileCollector::class);
// references / relations
if (
(isset($processorConfiguration['references']) && $processorConfiguration['references'])
|| (isset($processorConfiguration['references.']) && $processorConfiguration['references.'])
) {
$referencesUidList = (string)$cObj->stdWrapValue('references', $processorConfiguration);
$referencesUids = GeneralUtility::intExplode(',', $referencesUidList, true);
$fileCollector->addFileReferences($referencesUids);
if (!empty($processorConfiguration['references.'])) {
$referenceConfiguration = $processorConfiguration['references.'];
$relationField = $cObj->stdWrapValue('fieldName', $referenceConfiguration);
// If no reference fieldName is set, there's nothing to do
if (!empty($relationField)) {
// Fetch the references of the default element
$relationTable = $cObj->stdWrapValue('table', $referenceConfiguration, $cObj->getCurrentTable());
if (!empty($relationTable)) {
$fileCollector->addFilesFromRelation($relationTable, $relationField, $cObj->data);
}
}
}
}
// files
$files = $cObj->stdWrapValue('files', $processorConfiguration);
if ($files) {
$files = GeneralUtility::intExplode(',', (string)$files, true);
$fileCollector->addFiles($files);
}
// collections
$collections = $cObj->stdWrapValue('collections', $processorConfiguration);
if (!empty($collections)) {
$collections = GeneralUtility::intExplode(',', (string)$collections, true);
$fileCollector->addFilesFromFileCollections($collections);
}
// folders
$folders = $cObj->stdWrapValue('folders', $processorConfiguration);
if (!empty($folders)) {
$folders = GeneralUtility::trimExplode(',', (string)$folders, true);
$fileCollector->addFilesFromFolders($folders, (bool)$cObj->stdWrapValue('recursive', $processorConfiguration['folders.'] ?? [], false));
}
// make sure to sort the files
$sortingProperty = $cObj->stdWrapValue('sorting', $processorConfiguration);
if ($sortingProperty) {
$sortingDirection = $cObj->stdWrapValue(
'direction',
$processorConfiguration['sorting.'] ?? [],
'ascending'
);
$fileCollector->sort($sortingProperty, $sortingDirection);
}
// set the files into a variable, default "files"
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'files');
$processedData[$targetVariableName] = $fileCollector->getFiles();
return $processedData;
}
}
@@ -0,0 +1,152 @@
<?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\Frontend\DataProcessing;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\Resource\FileCollector;
/**
* This data processor converts the XML structure of a given FlexForm field
* into a fluid readable array.
*
* Options:
* fieldName - The name of the field containing the FlexForm to be converted
* references - A key / value list for fields with file references to process
* dataProcessing - Additional sub DataProcessors to process
* as - The variable, the generated array should be assigned to
*
* Example of a minimal TypoScript configuration, which processes the field
* `pi_flexform` and assigns the array to the `flexFormData` variable:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
*
* Example of an advanced TypoScript configuration, which processes the field
* `my_flexform_field`, resolves its FAL references and assigns the array to the
* `myOutputVariable` variable:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
* 10 {
* fieldName = my_flexform_field
* references {
* my_flex_form_group.my_flex_form_field = my_field_reference
* }
* dataProcessing {
* 10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
* 10 {
* references.fieldName = media
* }
* }
* as = myOutputVariable
* }
*/
#[Autoconfigure(public: true)]
readonly class FlexFormProcessor implements DataProcessorInterface
{
public function __construct(
private FlexFormTools $flexFormTools,
) {}
/**
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
): array {
// The field name to process
$fieldName = $cObj->stdWrapValue('fieldName', $processorConfiguration, 'pi_flexform');
if (!isset($processedData['data'][$fieldName])) {
return $processedData;
}
// Process FlexForm
$originalValue = $processedData['data'][$fieldName];
if (!is_string($originalValue)) {
return $processedData;
}
$flexFormData = $this->flexFormTools->convertFlexFormContentToArray($originalValue);
// Process FAL references
if (isset($processorConfiguration['references.']) && is_array($processorConfiguration['references.'])) {
$this->processFileReferences($cObj, $flexFormData, $processorConfiguration['references.']);
}
// Process additional DataProcessors
if (isset($processorConfiguration['dataProcessing.']) && is_array($processorConfiguration['dataProcessing.'])) {
// @todo: It looks as if data processors should retrieve the current request from the outside,
// this would avoid $cObj->getRequest() here.
$flexFormData = $this->processAdditionalDataProcessors($flexFormData, $processorConfiguration, $cObj->getRequest());
}
// Set the target variable
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'flexFormData');
$processedData[$targetVariableName] = $flexFormData;
return $processedData;
}
/**
* Recursively process FAL references and replace them by FAL objects.
*/
protected function processFileReferences(ContentObjectRenderer $cObj, array &$data, array $fields): void
{
foreach ($fields as $key => $field) {
$key = rtrim($key, '.');
if (!isset($data[$key])) {
continue;
}
if (is_array($field)) {
$this->processFileReferences($cObj, $data[$key], $field);
} else {
$fileCollector = GeneralUtility::makeInstance(FileCollector::class);
$fileCollector->addFilesFromRelation($cObj->getCurrentTable(), $field, $cObj->data);
$data[$key] = $fileCollector->getFiles();
}
}
}
/**
* Recursively process sub processors of a data processor
*/
protected function processAdditionalDataProcessors(array $data, array $processorConfiguration, ServerRequestInterface $request): array
{
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObjectRenderer->setRequest($request);
$contentObjectRenderer->start([$data], '');
return GeneralUtility::makeInstance(ContentDataProcessor::class)->process(
$contentObjectRenderer,
$processorConfiguration,
$data
);
}
}
+510
View File
@@ -0,0 +1,510 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
/**
* This data processor will calculate rows, columns and dimensions for a gallery
* based on several settings and can be used for f.i. the CType "textmedia"
*
* The output will be an array which contains the rows and columns,
* including the file references and the calculated width and height for each media element,
* but also some more information of the gallery, like position, width and counters
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\GalleryProcessor
* 10 {
* filesProcessedDataKey = files
* mediaOrientation.field = imageorient
* numberOfColumns.field = imagecols
* equalMediaHeight.field = imageheight
* equalMediaWidth.field = imagewidth
* columnSpacing = 0
* borderEnabled.field = imageborder
* borderPadding = 0
* borderWidth = 0
* maxGalleryWidth = {$styles.content.mediatext.maxW}
* maxGalleryWidthInText = {$styles.content.mediatext.maxWInText}
* as = gallery
* }
*
* Output example:
*
* gallery {
* position {
* horizontal = center
* vertical = above
* noWrap = FALSE
* }
* width = 600
* count {
* files = 2
* columns = 1
* rows = 2
* }
* rows {
* 1 {
* columns {
* 1 {
* media = TYPO3\CMS\Core\Resource\FileReference
* dimensions {
* width = 600
* height = 400
* }
* }
* }
* }
* 2 {
* columns {
* 1 {
* media = TYPO3\CMS\Core\Resource\FileReference
* dimensions {
* width = 600
* height = 400
* }
* }
* }
* }
* }
* columnSpacing = 0
* border {
* enabled = FALSE
* width = 0
* padding = 0
* }
* }
*/
class GalleryProcessor implements DataProcessorInterface
{
/**
* The content object renderer
*
* @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
*/
protected $contentObjectRenderer;
/**
* The processor configuration
*
* @var array
*/
protected $processorConfiguration;
/**
* Matching the tt_content field towards the imageOrient option
*
* @var array
*/
protected $availableGalleryPositions = [
'horizontal' => [
'center' => [0, 8],
'right' => [1, 9, 17, 25],
'left' => [2, 10, 18, 26],
],
'vertical' => [
'above' => [0, 1, 2],
'intext' => [17, 18, 25, 26],
'below' => [8, 9, 10],
],
];
/**
* Storage for processed data
*
* @var array
*/
protected $galleryData = [
'position' => [
'horizontal' => '',
'vertical' => '',
'noWrap' => false,
],
'width' => 0,
'count' => [
'files' => 0,
'columns' => 0,
'rows' => 0,
],
'columnSpacing' => 0,
'border' => [
'enabled' => false,
'width' => 0,
'padding' => 0,
],
'rows' => [],
];
/**
* @var int
*/
protected $numberOfColumns;
/**
* @var int
*/
protected $mediaOrientation;
/**
* @var int
*/
protected $maxGalleryWidth;
/**
* @var int
*/
protected $maxGalleryWidthInText;
/**
* @var int
*/
protected $equalMediaHeight;
/**
* @var int
*/
protected $equalMediaWidth;
/**
* @var int
*/
protected $columnSpacing;
/**
* @var bool
*/
protected $borderEnabled;
/**
* @var int
*/
protected $borderWidth;
/**
* @var int
*/
protected $borderPadding;
/**
* @var string
*/
protected $cropVariant = 'default';
/**
* The (filtered) media files to be used in the gallery
*
* @var FileInterface[]
*/
protected $fileObjects = [];
/**
* The calculated dimensions for each media element
*
* @var array
*/
protected $mediaDimensions = [];
/**
* Process data for a gallery, for instance the CType "textmedia"
*
* @param ContentObjectRenderer $cObj The content object renderer, which contains data of the content element
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
* @throws ContentRenderingException
*/
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
) {
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
$this->contentObjectRenderer = $cObj;
$this->processorConfiguration = $processorConfiguration;
$filesProcessedDataKey = (string)$cObj->stdWrapValue(
'filesProcessedDataKey',
$processorConfiguration,
'files'
);
if (isset($processedData[$filesProcessedDataKey]) && is_array($processedData[$filesProcessedDataKey])) {
$this->fileObjects = $processedData[$filesProcessedDataKey];
$this->galleryData['count']['files'] = count($this->fileObjects);
} else {
throw new ContentRenderingException('No files found for key ' . $filesProcessedDataKey . ' in $processedData.', 1436809789);
}
$this->numberOfColumns = (int)$this->getConfigurationValue('numberOfColumns', 'imagecols');
$this->mediaOrientation = (int)$this->getConfigurationValue('mediaOrientation', 'imageorient');
$this->maxGalleryWidth = (int)$this->getConfigurationValue('maxGalleryWidth') ?: 600;
$this->maxGalleryWidthInText = (int)$this->getConfigurationValue('maxGalleryWidthInText') ?: 300;
$this->equalMediaHeight = (int)$this->getConfigurationValue('equalMediaHeight', 'imageheight');
$this->equalMediaWidth = (int)$this->getConfigurationValue('equalMediaWidth', 'imagewidth');
$this->columnSpacing = (int)$this->getConfigurationValue('columnSpacing');
$this->borderEnabled = (bool)$this->getConfigurationValue('borderEnabled', 'imageborder');
$this->borderWidth = (int)$this->getConfigurationValue('borderWidth');
$this->borderPadding = (int)$this->getConfigurationValue('borderPadding');
$this->cropVariant = $this->getConfigurationValue('cropVariant') ?: 'default';
$this->determineGalleryPosition();
$this->determineMaximumGalleryWidth();
$this->calculateRowsAndColumns();
$this->calculateMediaWidthsAndHeights();
$this->prepareGalleryData();
$targetFieldName = (string)$cObj->stdWrapValue(
'as',
$processorConfiguration,
'gallery'
);
$processedData[$targetFieldName] = $this->galleryData;
return $processedData;
}
/**
* Get configuration value from processorConfiguration
* with when $dataArrayKey fallback to value from cObj->data array
*
* @param string $key
* @param string|null $dataArrayKey
* @return string
*/
protected function getConfigurationValue($key, $dataArrayKey = null)
{
$defaultValue = '';
if ($dataArrayKey && isset($this->contentObjectRenderer->data[$dataArrayKey])) {
$defaultValue = $this->contentObjectRenderer->data[$dataArrayKey];
}
return $this->contentObjectRenderer->stdWrapValue(
$key,
$this->processorConfiguration,
$defaultValue
);
}
/**
* Define the gallery position
*
* Gallery has a horizontal and a vertical position towards the text
* and a possible wrapping of the text around the gallery.
*/
protected function determineGalleryPosition()
{
foreach ($this->availableGalleryPositions as $positionDirectionKey => $positionDirectionValue) {
foreach ($positionDirectionValue as $positionKey => $positionArray) {
if (in_array($this->mediaOrientation, $positionArray, true)) {
$this->galleryData['position'][$positionDirectionKey] = $positionKey;
}
}
}
if ($this->mediaOrientation === 25 || $this->mediaOrientation === 26) {
$this->galleryData['position']['noWrap'] = true;
}
}
/**
* Get the gallery width based on vertical position
*/
protected function determineMaximumGalleryWidth()
{
if ($this->galleryData['position']['vertical'] === 'intext') {
$this->galleryData['width'] = $this->maxGalleryWidthInText;
} else {
$this->galleryData['width'] = $this->maxGalleryWidth;
}
}
/**
* Calculate the amount of rows and columns
*/
protected function calculateRowsAndColumns()
{
// If no columns defined, set it to 1
$columns = max((int)$this->numberOfColumns, 1);
// When more columns than media elements, set the columns to the amount of media elements
if ($columns > $this->galleryData['count']['files']) {
$columns = $this->galleryData['count']['files'];
}
if ($columns === 0) {
$columns = 1;
}
// Calculate the rows from the amount of files and the columns
$rows = ceil($this->galleryData['count']['files'] / $columns);
$this->galleryData['count']['columns'] = $columns;
$this->galleryData['count']['rows'] = (int)$rows;
}
/**
* Calculate the width/height of the media elements
*
* Based on the width of the gallery, defined equal width or height by a user, the spacing between columns and
* the use of a border, defined by user, where the border width and padding are taken into account
*
* File objects MUST already be filtered. They need a height and width to be shown in the gallery
*/
protected function calculateMediaWidthsAndHeights()
{
$columnSpacingTotal = ($this->galleryData['count']['columns'] - 1) * $this->columnSpacing;
$galleryWidthMinusBorderAndSpacing = max($this->galleryData['width'] - $columnSpacingTotal, 1);
if ($this->borderEnabled) {
$borderPaddingTotal = ($this->galleryData['count']['columns'] * 2) * $this->borderPadding;
$borderWidthTotal = ($this->galleryData['count']['columns'] * 2) * $this->borderWidth;
$galleryWidthMinusBorderAndSpacing = $galleryWidthMinusBorderAndSpacing - $borderPaddingTotal - $borderWidthTotal;
}
// User entered a predefined height
if ($this->equalMediaHeight) {
$mediaScalingCorrection = 1;
$maximumRowWidth = 0;
// Calculate the scaling correction when the total of media elements is wider than the gallery width
for ($row = 1; $row <= $this->galleryData['count']['rows']; $row++) {
$totalRowWidth = 0;
for ($column = 1; $column <= $this->galleryData['count']['columns']; $column++) {
$fileKey = (($row - 1) * $this->galleryData['count']['columns']) + $column - 1;
if ($fileKey > $this->galleryData['count']['files'] - 1) {
break 2;
}
$currentMediaScaling = $this->equalMediaHeight / max($this->getCroppedDimensionalProperty($this->fileObjects[$fileKey], 'height'), 1);
$totalRowWidth += $this->getCroppedDimensionalProperty($this->fileObjects[$fileKey], 'width') * $currentMediaScaling;
}
$maximumRowWidth = max($totalRowWidth, $maximumRowWidth);
$mediaInRowScaling = $totalRowWidth / $galleryWidthMinusBorderAndSpacing;
$mediaScalingCorrection = max($mediaInRowScaling, $mediaScalingCorrection);
}
// Set the corrected dimensions for each media element
foreach ($this->fileObjects as $key => $fileObject) {
$mediaHeight = floor($this->equalMediaHeight / $mediaScalingCorrection);
$mediaWidth = floor(
$this->getCroppedDimensionalProperty($fileObject, 'width') * ($mediaHeight / max($this->getCroppedDimensionalProperty($fileObject, 'height'), 1))
);
$this->mediaDimensions[$key] = [
'width' => $mediaWidth,
'height' => $mediaHeight,
];
}
// Recalculate gallery width
$this->galleryData['width'] = floor($maximumRowWidth / $mediaScalingCorrection);
// User entered a predefined width
} elseif ($this->equalMediaWidth) {
$mediaScalingCorrection = 1;
// Calculate the scaling correction when the total of media elements is wider than the gallery width
$totalRowWidth = $this->galleryData['count']['columns'] * $this->equalMediaWidth;
$mediaInRowScaling = $totalRowWidth / $galleryWidthMinusBorderAndSpacing;
$mediaScalingCorrection = max($mediaInRowScaling, $mediaScalingCorrection);
// Set the corrected dimensions for each media element
foreach ($this->fileObjects as $key => $fileObject) {
$mediaWidth = floor($this->equalMediaWidth / $mediaScalingCorrection);
$mediaHeight = floor(
$this->getCroppedDimensionalProperty($fileObject, 'height') * ($mediaWidth / max($this->getCroppedDimensionalProperty($fileObject, 'width'), 1))
);
$this->mediaDimensions[$key] = [
'width' => $mediaWidth,
'height' => $mediaHeight,
];
}
// Recalculate gallery width
$this->galleryData['width'] = floor($totalRowWidth / $mediaScalingCorrection);
// Automatic setting of width and height
} else {
$maxMediaWidth = (int)($galleryWidthMinusBorderAndSpacing / $this->galleryData['count']['columns']);
foreach ($this->fileObjects as $key => $fileObject) {
$croppedWidth = $this->getCroppedDimensionalProperty($fileObject, 'width');
$mediaWidth = $croppedWidth > 0 ? min($maxMediaWidth, $croppedWidth) : $maxMediaWidth;
$mediaHeight = floor(
$this->getCroppedDimensionalProperty($fileObject, 'height') * ($mediaWidth / max($this->getCroppedDimensionalProperty($fileObject, 'width'), 1))
);
$this->mediaDimensions[$key] = [
'width' => $mediaWidth,
'height' => $mediaHeight,
];
}
}
}
/**
* When retrieving the height or width for a media file
* a possible cropping needs to be taken into account.
*
* @param string $dimensionalProperty 'width' or 'height'
* @return int
*/
protected function getCroppedDimensionalProperty(FileInterface $fileObject, $dimensionalProperty)
{
if (!$fileObject->hasProperty('crop') || empty($fileObject->getProperty('crop'))) {
return $fileObject->getProperty($dimensionalProperty);
}
$croppingConfiguration = $fileObject->getProperty('crop');
$cropVariantCollection = CropVariantCollection::create((string)$croppingConfiguration);
return (int)$cropVariantCollection->getCropArea($this->cropVariant)->makeAbsoluteBasedOnFile($fileObject)->asArray()[$dimensionalProperty];
}
/**
* Prepare the gallery data
*
* Make an array for rows, columns and configuration
*/
protected function prepareGalleryData()
{
for ($row = 1; $row <= $this->galleryData['count']['rows']; $row++) {
for ($column = 1; $column <= $this->galleryData['count']['columns']; $column++) {
$fileKey = (($row - 1) * $this->galleryData['count']['columns']) + $column - 1;
$this->galleryData['rows'][$row]['columns'][$column] = [
'media' => $this->fileObjects[$fileKey] ?? null,
'dimensions' => [
'width' => $this->mediaDimensions[$fileKey]['width'] ?? null,
'height' => $this->mediaDimensions[$fileKey]['height'] ?? null,
],
];
}
}
$this->galleryData['columnSpacing'] = $this->columnSpacing;
$this->galleryData['border']['enabled'] = $this->borderEnabled;
$this->galleryData['border']['width'] = $this->borderWidth;
$this->galleryData['border']['padding'] = $this->borderPadding;
}
}
@@ -0,0 +1,239 @@
<?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\Frontend\DataProcessing;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\ContentObject\Menu\MenuContentObjectFactory;
use TYPO3\CMS\Frontend\Utility\CanonicalizationUtility;
/**
* This menu processor generates a language menu array that will be
* assigned to FLUIDTEMPLATE as variable.
*
* Options:
* if - TypoScript if condition
* languages - A list of languages id's (e.g. 0,1,2) to use for the menu
* creation or 'auto' to load from system or site languages
* as - The variable to be used within the result
*
* Example TypoScript configuration:
* 10 = TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor
* 10 {
* as = languagenavigation
* }
*/
class LanguageMenuProcessor implements DataProcessorInterface
{
protected ContentObjectRenderer $cObj;
protected array $processorConfiguration;
/**
* Allowed configuration keys for menu generation, other keys
* will throw an exception to prevent configuration errors.
*/
protected array $allowedConfigurationKeys = [
'if',
'if.',
'languages',
'languages.',
'as',
'addQueryString',
'addQueryString.',
];
/**
* Remove keys from configuration that should not be passed
* to the menu to prevent configuration errors
*/
protected array $removeConfigurationKeysForHmenu = [
'languages',
'languages.',
'as',
];
protected array $menuConfig = [
'special' => 'language',
'addQueryString' => 1,
];
protected array $menuDefaults = [
'as' => 'languagemenu',
];
public function __construct(
protected readonly MenuContentObjectFactory $menuContentObjectFactory,
protected readonly PageRepository $pageRepository,
) {}
/**
* Get configuration value from processorConfiguration
*/
protected function getConfigurationValue(string $key): string
{
return $this->cObj->stdWrapValue($key, $this->processorConfiguration, $this->menuDefaults[$key] ?? '');
}
protected function getRequest(): ServerRequestInterface
{
return $this->cObj->getRequest();
}
/**
* Returns the currently configured "site" if a site is configured (= resolved) in the current request.
*/
protected function getCurrentSite(): Site
{
return $this->getRequest()->getAttribute('site');
}
/**
* @throws \InvalidArgumentException
*/
protected function validateConfiguration(): void
{
$invalidArguments = [];
foreach ($this->processorConfiguration as $key => $value) {
if (!in_array($key, $this->allowedConfigurationKeys)) {
$invalidArguments[str_replace('.', '', $key)] = $key;
}
}
if (!empty($invalidArguments)) {
throw new \InvalidArgumentException('LanguageMenuProcessor configuration contains invalid arguments: ' . implode(', ', $invalidArguments), 1522959188);
}
}
/**
* Process languages and filter the configuration
*/
protected function prepareConfiguration(): void
{
$this->menuConfig = array_merge($this->menuConfig, $this->processorConfiguration);
// Process languages
$this->menuConfig['special.']['value'] = $this->cObj->stdWrapValue('languages', $this->menuConfig, 'auto');
// Filter configuration
foreach ($this->menuConfig as $key => $value) {
if (in_array($key, $this->removeConfigurationKeysForHmenu, true)) {
unset($this->menuConfig[$key]);
}
}
$paramsToExclude = CanonicalizationUtility::getParamsToExcludeForCanonicalizedUrl(
$this->getRequest()->getAttribute('frontend.page.information')->getId(),
(array)$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters'],
$this->cObj->getRequest()
);
$this->menuConfig['addQueryString.']['exclude'] = implode(
',',
array_merge(
GeneralUtility::trimExplode(',', $this->menuConfig['addQueryString.']['exclude'] ?? '', true),
$paramsToExclude
)
);
}
/**
* Build the menu configuration so it can be treated by TMENU
*/
protected function buildConfiguration(): void
{
$this->menuConfig['1'] = 'TMENU';
$this->menuConfig['1.']['NO'] = '1';
}
/**
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData): array
{
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
$this->cObj = $cObj;
$this->processorConfiguration = $processorConfiguration;
// Validate Configuration
$this->validateConfiguration();
// Build Configuration
$this->prepareConfiguration();
$this->buildConfiguration();
// Create menu object and get menu items directly
$request = $cObj->getRequest();
$site = $this->getCurrentSite();
$menu = $this->menuContentObjectFactory->getMenuObjectByType('TMENU');
$menu->parent_cObj = $cObj;
if (!$menu->start(null, $this->pageRepository, '', $this->menuConfig, 1, '', $request)) {
return $processedData;
}
$menu->makeMenu();
$menuItems = $menu->getMenuItems();
if ($menuItems === []) {
return $processedData;
}
// Enrich with language-specific fields
$processedMenu = [];
foreach ($menuItems as $key => $item) {
$languageId = (int)($item['data']['_REQUESTED_OVERLAY_LANGUAGE'] ?? 0);
try {
$languageObject = $site->getLanguageById($languageId);
} catch (\InvalidArgumentException) {
// Language not found in site config
continue;
}
$item['languageId'] = $languageId;
$item['locale'] = $languageObject->getLocale()->getName();
// Override title with language title (not page title)
$item['title'] = $languageObject->getTitle();
$item['navigationTitle'] = $languageObject->getNavigationTitle();
$item['twoLetterIsoCode'] = $languageObject->getLocale()->getLanguageCode();
$item['hreflang'] = $languageObject->getHreflang();
$item['direction'] = $languageObject->getLocale()->isRightToLeftLanguageDirection() ? 'rtl' : 'ltr';
$item['flag'] = $languageObject->getFlagIdentifier();
// Determine state from ITEM_STATE set by the menu system
$itemState = $item['data']['ITEM_STATE'] ?? '';
// active = 1 if state is ACT, ACTIFSUB, USERDEF2 (active states)
$item['active'] = in_array($itemState, ['ACT', 'ACTIFSUB', 'USERDEF2'], true) ? 1 : 0;
// current = 1 if state is CUR, CURIFSUB (current language)
$item['current'] = in_array($itemState, ['CUR', 'CURIFSUB'], true) ? 1 : 0;
// available = 1 unless USERDEF1/USERDEF2 state (language not available)
$item['available'] = !in_array($itemState, ['USERDEF1', 'USERDEF2'], true) ? 1 : 0;
$processedMenu[$key] = $item;
}
// Return processed data
$processedData[$this->getConfigurationValue('as')] = $processedMenu;
return $processedData;
}
}
+298
View File
@@ -0,0 +1,298 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\ContentObject\Menu\MenuContentObjectFactory;
/**
* This menu processor generates a menu array that will be assigned to
* FLUIDTEMPLATE as variable. Additional DataProcessing is supported and
* will be applied to each record.
*
* Options:
* as - The variable to be used within the result
* levels - Number of levels of the menu
* expandAll = If false, submenus will only render if the parent page is active
* includeSpacer = If true, pagetype spacer will be included in the menu
* titleField = Field that should be used for the title
*
* See HMENU docs for more options.
* https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Hmenu/Index.html
*
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
* 10 {
* special = list
* special.value.field = pages
* levels = 7
* as = menu
* expandAll = 1
* includeSpacer = 1
* titleField = nav_title // title
* dataProcessing {
* 10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
* 10 {
* references.fieldName = media
* }
* }
* }
*/
class MenuProcessor implements DataProcessorInterface
{
/**
* The content object renderer
*/
protected ?ContentObjectRenderer $cObj = null;
/**
* The processor configuration
*/
protected array $processorConfiguration;
/**
* Allowed configuration keys for menu generation, other keys
* will throw an exception to prevent configuration errors.
*/
public array $allowedConfigurationKeys = [
'cache',
'cache.',
'cache_period',
'entryLevel',
'entryLevel.',
'special',
'special.',
'minItems',
'minItems.',
'maxItems',
'maxItems.',
'begin',
'begin.',
'alternativeSortingField',
'alternativeSortingField.',
'showAccessRestrictedPages',
'showAccessRestrictedPages.',
'excludeUidList',
'excludeUidList.',
'excludeDoktypes',
'includeNotInMenu',
'includeNotInMenu.',
'alwaysActivePIDlist',
'alwaysActivePIDlist.',
'protectLvar',
'addQueryString',
'addQueryString.',
'if',
'if.',
'levels',
'levels.',
'expandAll',
'expandAll.',
'includeSpacer',
'includeSpacer.',
'as',
'titleField',
'titleField.',
'dataProcessing',
'dataProcessing.',
];
/**
* Remove keys from configuration that should not be passed
* to HMENU to prevent configuration errors
*/
public array $removeConfigurationKeysForHmenu = [
'levels',
'levels.',
'expandAll',
'expandAll.',
'includeSpacer',
'includeSpacer.',
'as',
'titleField',
'titleField.',
'dataProcessing',
'dataProcessing.',
];
protected array $menuConfig = [];
public array $menuDefaults = [
'levels' => 1,
'expandAll' => 1,
'includeSpacer' => 0,
'as' => 'menu',
'titleField' => 'nav_title // title',
];
protected int $menuLevels;
protected int $menuExpandAll;
protected int $menuIncludeSpacer;
protected string $menuTitleField;
protected string $menuAlternativeSortingField;
protected string $menuTargetVariableName;
public function __construct(
protected ContentDataProcessor $contentDataProcessor,
protected MenuContentObjectFactory $menuContentObjectFactory,
protected PageRepository $pageRepository,
) {}
/**
* Get configuration value from processorConfiguration
*/
protected function getConfigurationValue(string $key): string
{
return $this->cObj->stdWrapValue($key, $this->processorConfiguration, $this->menuDefaults[$key] ?? '');
}
/**
* @throws \InvalidArgumentException
*/
public function validateConfiguration(): void
{
$invalidArguments = [];
foreach ($this->processorConfiguration as $key => $value) {
if (!in_array($key, $this->allowedConfigurationKeys)) {
$invalidArguments[str_replace('.', '', $key)] = $key;
}
}
if (!empty($invalidArguments)) {
throw new \InvalidArgumentException('MenuProcessor Configuration contains invalid Arguments: ' . implode(', ', $invalidArguments), 1478806566);
}
}
public function prepareConfiguration(): void
{
$this->menuConfig = $this->processorConfiguration;
// Filter configuration
foreach ($this->menuConfig as $key => $value) {
if (in_array($key, $this->removeConfigurationKeysForHmenu)) {
unset($this->menuConfig[$key]);
}
}
// Process special value
if (isset($this->menuConfig['special.']['value.'])) {
$this->menuConfig['special.']['value'] = $this->cObj->stdWrapValue('value', $this->menuConfig['special.']);
unset($this->menuConfig['special.']['value.']);
}
}
/**
* Build the menu configuration so it can be treated by TMENU
*/
public function buildConfiguration(): void
{
for ($i = 1; $i <= $this->menuLevels; $i++) {
$this->menuConfig[$i] = 'TMENU';
if (array_key_exists('showAccessRestrictedPages', $this->menuConfig)) {
$this->menuConfig[$i . '.']['showAccessRestrictedPages'] = $this->menuConfig['showAccessRestrictedPages'];
if (array_key_exists('showAccessRestrictedPages.', $this->menuConfig)
&& is_array($this->menuConfig['showAccessRestrictedPages.'])) {
$this->menuConfig[$i . '.']['showAccessRestrictedPages.'] = $this->menuConfig['showAccessRestrictedPages.'];
}
}
$this->menuConfig[$i . '.']['expAll'] = $this->menuExpandAll;
$this->menuConfig[$i . '.']['alternativeSortingField'] = $this->menuAlternativeSortingField;
$this->menuConfig[$i . '.']['NO'] = '1';
if ($this->menuIncludeSpacer) {
$this->menuConfig[$i . '.']['SPC'] = '1';
}
}
}
/**
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
{
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
$this->cObj = $cObj;
$this->processorConfiguration = $processorConfiguration;
// Get Configuration
$this->menuLevels = (int)$this->getConfigurationValue('levels') ?: 1;
$this->menuExpandAll = (int)$this->getConfigurationValue('expandAll');
$this->menuIncludeSpacer = (int)$this->getConfigurationValue('includeSpacer');
$this->menuTargetVariableName = $this->getConfigurationValue('as');
$this->menuTitleField = $this->getConfigurationValue('titleField');
$this->menuAlternativeSortingField = $this->getConfigurationValue('alternativeSortingField');
// Validate Configuration
$this->validateConfiguration();
// Build Configuration
$this->prepareConfiguration();
$this->buildConfiguration();
// Create menu object and get menu items directly
$request = $cObj->getRequest();
$menu = $this->menuContentObjectFactory->getMenuObjectByType('TMENU');
$menu->parent_cObj = $cObj;
if (!$menu->start(null, $this->pageRepository, '', $this->menuConfig, 1, '', $request)) {
return $processedData;
}
$menu->makeMenu();
$menuItems = $menu->getMenuItems();
if ($menuItems === []) {
return $processedData;
}
// Process additional data processors
$processedMenu = [];
foreach ($menuItems as $key => $page) {
$processedMenu[$key] = $this->processAdditionalDataProcessors($page, $processorConfiguration);
}
// Return processed data
$processedData[$this->menuTargetVariableName] = $processedMenu;
return $processedData;
}
/**
* Process additional data processors
*/
protected function processAdditionalDataProcessors(array $page, array $processorConfiguration): array
{
if (is_array($page['children'] ?? false)) {
foreach ($page['children'] as $key => $item) {
$page['children'][$key] = $this->processAdditionalDataProcessors($item, $processorConfiguration);
}
}
$request = $this->cObj->getRequest();
$recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$recordContentObjectRenderer->setRequest($request);
$recordContentObjectRenderer->start($page['data'] ?? [], 'pages');
$page['title'] = (string)$recordContentObjectRenderer->stdWrap('', ['field' => $this->menuTitleField]);
return $this->contentDataProcessor->process($recordContentObjectRenderer, $processorConfiguration, $page);
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Frontend\Content\RecordCollector;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\Event\AfterContentHasBeenFetchedEvent;
/**
* All-in-one data processor that loads all tt_content records from the current page
* layout into the template with a given identifier for each colPos, also respecting
* slideMode or collect options based on the page layouts content columns.
*
* Use "as" for the target variable where the fetched content elements will be provided.
* If empty, "content" is used.
*
* Example TypoScript configuration:
*
* page = PAGE
* page {
* 10 = PAGEVIEW
* 10 {
* paths.10 = EXT:my_site_package/Resources/Private/Templates/
* dataProcessing {
* 10 = page-content
* 10.as = myContent
* }
* }
* }
*
* which fetches all content elements for the current page and provides them as "myContent".
*/
readonly class PageContentFetchingProcessor implements DataProcessorInterface
{
public function __construct(
protected RecordCollector $recordCollector,
protected EventDispatcherInterface $eventDispatcher,
) {}
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
): array {
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
$request = $cObj->getRequest();
$pageInformation = $request->getAttribute('frontend.page.information');
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'content');
$contentAreas = $pageInformation->getPageLayout()?->getContentAreas();
$groupedContent = $this->eventDispatcher->dispatch(
new AfterContentHasBeenFetchedEvent($contentAreas->getGroupedRecords($request), $request)
)->groupedContent;
$processedData[$targetVariableName] = $contentAreas->withUpdatedRecords($groupedContent);
return $processedData;
}
}
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* Creates Record objects out of full data sets (= DB entries).
* This is typically useful in conjunction with the DatabaseQueryProcessor.
* Can also be used to transform the current data array of FLUIDTEMPLATE.
*
* The variable that contains the record(s) from a previous data processor,
* or from a FLUIDTEMPLATE view. Default is `data`.
*
* variableName = items
*
* The name of the database table of the records. Leave empty to auto-resolve
* the table from current ContentObjectRenderer.
*
* table = tt_content
*
* The target variable where the resolved record objects are contained.
* Can be set to `data` to override the input data array of FLUIDTEMPLATE.
* If empty, "record" or "records" (if multiple records are given) is used.
*
* as = myRecords
*
* Example TypoScript configuration:
*
* page = PAGE
* page {
* 10 = PAGEVIEW
* 10 {
* paths.10 = EXT:my_site_package/Resources/Private/Templates/
* dataProcessing {
* 10 = database-query
* 10 {
* as = mainContent
* table = tt_content
* select.where = colPos=0
* dataProcessing {
* 10 = record-transformation
* 10 {
* as = myContent
* }
* }
* }
* }
* }
* }
*
* which transforms all content elements fetched by the DatabaseQueryProcessor an provides them as "myContent".
*/
readonly class RecordTransformationProcessor implements DataProcessorInterface
{
public function __construct(
protected RecordFactory $recordFactory,
) {}
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
): array {
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
// `data` is the default variable name for the FLUIDTEMPLATE record
// and processed records of the DatabaseQueryProcessor.
$defaultVariableName = 'data';
$variableName = $cObj->stdWrapValue('variableName', $processorConfiguration, $defaultVariableName);
$input = $processedData[$variableName] ?? $processedData;
// We can only deal with arrays here.
if (!is_array($input)) {
return $processedData;
}
$table = $cObj->stdWrapValue('table', $processorConfiguration, $cObj->getCurrentTable());
$output = [];
if (array_is_list($input)) {
foreach ($input as $record) {
$output[] = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $record);
}
$defaultTargetVariableName = 'records';
} else {
$output = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $input);
$defaultTargetVariableName = 'record';
}
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, $defaultTargetVariableName);
// @todo Should we make sure that $output is actually a Record object?
$processedData[$targetVariableName] = $output;
return $processedData;
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* Fetch the SiteLanguage object containing all information about the current language
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\SiteLanguageProcessor
* 10 {
* as = siteLanguage
* }
*
* where "as" names the variable containing the SiteLanguage properties
*/
class SiteLanguageProcessor implements DataProcessorInterface
{
/**
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData): array
{
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'siteLanguage');
$processedData[$targetVariableName] = $cObj->getRequest()->getAttribute('language')?->toArray();
return $processedData;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* Fetch the site object containing all information about the current site
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\SiteProcessor
* 10 {
* as = site
* }
*
* where "as" names the variable containing the site object
*/
class SiteProcessor implements DataProcessorInterface
{
/**
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData): array
{
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'site');
$processedData[$targetVariableName] = $cObj->getRequest()->getAttribute('site');
return $processedData;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\DataProcessing;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* This data processor can be used for processing data for the content elements which have split contents in one field
* like e.g. "bullets". It will split the field data in an array ready to be iterated over in Fluid.
*
* Example field data:
*
* This is bullet 1, This is bullet 2, This is bullet 3
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\SplitProcessor
* 10 {
* if.isTrue.field = bodytext
* delimiter = ,
* fieldName = bodytext
* removeEmptyEntries = 1
* filterIntegers = 1
* filterUnique = 1
* as = bullets
* }
*
* whereas "bullets" can be used as a variable {bullets} inside Fluid for iteration.
*/
class SplitProcessor implements DataProcessorInterface
{
/**
* Process field data to split in an array
*
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
{
if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
return $processedData;
}
// The field name to process
$fieldName = $cObj->stdWrapValue('fieldName', $processorConfiguration);
if (empty($fieldName)) {
return $processedData;
}
$originalValue = (string)($cObj->data[$fieldName] ?? '');
// Set the target variable
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, $fieldName);
// Set the delimiter which is "LF" by default
$delimiter = (string)$cObj->stdWrapValue('delimiter', $processorConfiguration, LF);
// Filter integers
$filterIntegers = (bool)$cObj->stdWrapValue('filterIntegers', $processorConfiguration, false);
// Filter unique
$filterUnique = (bool)$cObj->stdWrapValue('filterUnique', $processorConfiguration, false);
// Remove empty entries
$removeEmptyEntries = (bool)$cObj->stdWrapValue('removeEmptyEntries', $processorConfiguration, false);
if ($filterIntegers === true) {
$processedData[$targetVariableName] = GeneralUtility::intExplode($delimiter, $originalValue, $removeEmptyEntries);
} else {
$processedData[$targetVariableName] = GeneralUtility::trimExplode($delimiter, $originalValue, $removeEmptyEntries);
}
if ($filterUnique === true) {
$processedData[$targetVariableName] = array_unique($processedData[$targetVariableName]);
}
return $processedData;
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
/**
* Event that allows to enhance or change content (also depending on enabled caching).
* Depending on disable or enabling caching, the cache is then not stored in the pageCache.
*
* Until TYPO3 v13, the flag "isCachingEnabled" was available in $TSFE->no_cache.
*/
final class AfterCacheableContentIsGeneratedEvent
{
public function __construct(
private readonly ServerRequestInterface $request,
private string $content,
private readonly string $cacheIdentifier,
private bool $usePageCache
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getContent(): string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
public function isCachingEnabled(): bool
{
return $this->usePageCache;
}
public function disableCaching(): void
{
$this->usePageCache = false;
}
public function enableCaching(): void
{
$this->usePageCache = true;
}
public function getCacheIdentifier(): string
{
return $this->cacheIdentifier;
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
/**
* Event that is used directly after all cached content is stored in the page cache.
*
* NOT fired, if:
* * A page is called from the cache
* * Caching is disabled using 'frontend.cache.instruction' request attribute, which can
* be set by various middlewares or AfterCacheableContentIsGeneratedEvent
*/
final readonly class AfterCachedPageIsPersistedEvent
{
public function __construct(
private ServerRequestInterface $request,
private string $cacheIdentifier,
private array $cacheData,
private int $cacheLifetime
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getCacheIdentifier(): string
{
return $this->cacheIdentifier;
}
public function getCacheData(): array
{
return $this->cacheData;
}
/**
* The amount of seconds until the cache entry is invalid.
*/
public function getCacheLifetime(): int
{
return $this->cacheLifetime;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
/**
* Event listeners are able to manipulate fetched page content, which is already grouped by column
* @todo Consider deprecation due to introduction of ResolveContentAreasEvent
*/
final class AfterContentHasBeenFetchedEvent
{
public function __construct(
public array $groupedContent,
public readonly ServerRequestInterface $request,
) {}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
/**
* Generic event to modify any kind of link generation with typolink(). This is processed by all
* frontend-related links.
*
* If a link could not be generated, a "UnableToLinkException" could be thrown by an Event Listener.
*/
final class AfterLinkIsGeneratedEvent
{
public function __construct(
private LinkResultInterface $linkResult,
private readonly ContentObjectRenderer $contentObjectRenderer,
private readonly array $linkInstructions,
) {}
/**
* Update a link when a part was modified by an Event Listener.
*/
public function setLinkResult(LinkResultInterface $linkResult): void
{
$this->linkResult = $linkResult;
}
public function getLinkResult(): LinkResultInterface
{
return $this->linkResult;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
/**
* Returns the original instructions / $linkConfiguration that were used to build the link
*/
public function getLinkInstructions(): array
{
return $this->linkInstructions;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Page\PageInformation;
/**
* A PSR-14 event fired in the frontend process after a given page has been resolved including
* its language.
*
* This event is intended to e.g. modify TYPO3's language resolving logic by custom additions.
* This event also allows to send a custom Response via Event Listeners (e.g. a custom 403 response)
*/
final class AfterPageAndLanguageIsResolvedEvent
{
private ?ResponseInterface $response = null;
public function __construct(
private readonly ServerRequestInterface $request,
private PageInformation $pageInformation,
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getPageInformation(): PageInformation
{
return $this->pageInformation;
}
public function setPageInformation(PageInformation $pageInformation): void
{
$this->pageInformation = $pageInformation;
}
public function getResponse(): ?ResponseInterface
{
return $this->response;
}
public function setResponse(ResponseInterface $response): void
{
$this->response = $response;
}
}
@@ -0,0 +1,63 @@
<?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\Frontend\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Page\PageInformation;
/**
* A PSR-14 event fired in the frontend process after a given page has been resolved with permissions, rootline etc.
* This is useful to modify the page + rootline (but before the language is resolved)
* to direct or load content from a different page, or modify the page response if additional
* permissions should be checked.
*/
final class AfterPageWithRootLineIsResolvedEvent
{
private ?ResponseInterface $response = null;
public function __construct(
private readonly ServerRequestInterface $request,
private PageInformation $pageInformation,
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function setResponse(ResponseInterface $response): void
{
$this->response = $response;
}
public function getResponse(): ?ResponseInterface
{
return $this->response;
}
public function getPageInformation(): PageInformation
{
return $this->pageInformation;
}
public function setPageInformation(PageInformation $pageInformation): void
{
$this->pageInformation = $pageInformation;
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use TYPO3\CMS\Core\TypoScript\FrontendTypoScript;
/**
* This event is dispatched after the FrontendTypoScript object has been calculated,
* just before it is attached to the request.
*
* The event is designed to enable listeners to act on specific TypoScript conditions.
* Listeners *must not* modify TypoScript at this point, the core will try to actively
* prevent this.
*
* This event is especially useful when "upper" middlewares that do not have the
* determined TypoScript need to behave differently depending on TypoScript 'config' that
* is only created after them.
* The core uses this in the TimeTrackInitialization and the WorkspacePreview middlewares,
* to determine debugging and preview details.
*
* Note both 'settings' ("constants") and 'config' are *always* set within the
* FrontendTypoScript at this point, even in 'fully cached page' scenarios. 'setup'
* and (@internal) 'page' may not be set.
*/
final readonly class AfterTypoScriptDeterminedEvent
{
public function __construct(
private FrontendTypoScript $frontendTypoScript,
) {}
public function getFrontendTypoScript(): FrontendTypoScript
{
return $this->frontendTypoScript;
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\EventDispatcher\StoppableEventInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Event dispatched before the DatabaseRecordLinkBuilder resolves a database record.
*
* This event is stoppable: If an event listener sets a record, the event propagation
* will be stopped and the default record retrieval logic will be skipped.
*/
final class BeforeDatabaseRecordLinkResolvedEvent implements StoppableEventInterface
{
public function __construct(
public readonly array $linkDetails,
public readonly string $databaseTable,
public readonly array $typoscriptConfiguration,
public readonly array $tsConfig,
public readonly ServerRequestInterface $request,
public ?array $record = null
) {}
public function isPropagationStopped(): bool
{
return $this->record !== null;
}
}
@@ -0,0 +1,57 @@
<?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\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
/**
* This event is dispatched just before the final page cache identifier is created,
* that is used to get() - and later set(), if needed and allowed - the page cache row.
*
* The event retrieves all current arguments that will be part of the identifier
* calculation and allows to add further arguments in case page caches need
* to be more specific.
*
* This event can be helpful in various scenarios, for example to implement
* proper page caching in A/B testing.
*
* Note this event is *always* dispatched, even in fully cached page scenarios,
* if an outer middleware did not return early (for instance due to permission issues).
*/
final class BeforePageCacheIdentifierIsHashedEvent
{
public function __construct(
private readonly ServerRequestInterface $request,
private array $pageCacheIdentifierParameters,
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getPageCacheIdentifierParameters(): array
{
return $this->pageCacheIdentifierParameters;
}
public function setPageCacheIdentifierParameters(array $pageCacheIdentifierParameters): void
{
$this->pageCacheIdentifierParameters = $pageCacheIdentifierParameters;
}
}
@@ -0,0 +1,51 @@
<?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\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Page\PageInformation;
/**
* A PSR-14 event fired before the frontend process is trying to fully resolve a given page
* by its page ID and the request.
*
* Event Listeners can modify incoming parameters (such as $controller->id) or modify the context
* for resolving a page.
*/
final class BeforePageIsResolvedEvent
{
public function __construct(
private readonly ServerRequestInterface $request,
private PageInformation $pageInformation,
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getPageInformation(): PageInformation
{
return $this->pageInformation;
}
public function setPageInformation(PageInformation $pageInformation): void
{
$this->pageInformation = $pageInformation;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?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\Frontend\Event;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Site\Entity\Site;
/**
* Listeners to this Event will be able to modify items for a menu generated with HMENU
*/
final class FilterMenuItemsEvent
{
public function __construct(
private readonly array $allMenuItems,
private array $filteredMenuItems,
private readonly array $menuConfiguration,
private readonly array $itemConfiguration,
private readonly array $bannedMenuItems,
private readonly array $excludedDoktypes,
private readonly Site $site,
private readonly Context $context,
private readonly array $currentPage
) {}
public function getAllMenuItems(): array
{
return $this->allMenuItems;
}
public function getFilteredMenuItems(): array
{
return $this->filteredMenuItems;
}
public function setFilteredMenuItems(array $filteredMenuItems): void
{
$this->filteredMenuItems = $filteredMenuItems;
}
public function getMenuConfiguration(): array
{
return $this->menuConfiguration;
}
public function getItemConfiguration(): array
{
return $this->itemConfiguration;
}
public function getBannedMenuItems(): array
{
return $this->bannedMenuItems;
}
public function getExcludedDoktypes(): array
{
return $this->excludedDoktypes;
}
public function getSite(): Site
{
return $this->site;
}
public function getContext(): Context
{
return $this->context;
}
public function getCurrentPage(): array
{
return $this->currentPage;
}
}
@@ -0,0 +1,65 @@
<?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\Frontend\Event;
use TYPO3\CMS\Core\Context\Context;
/**
* Event to allow listeners to modify the amount of seconds that a generated frontend page
* should be cached in the "pages" cache when initially generated.
*/
final class ModifyCacheLifetimeForPageEvent
{
public function __construct(
private int $cacheLifetime,
private readonly int $pageId,
private readonly array $pageRecord,
private readonly array $renderingInstructions,
private readonly Context $context
) {}
public function setCacheLifetime(int $cacheLifetime): void
{
$this->cacheLifetime = $cacheLifetime;
}
public function getCacheLifetime(): int
{
return $this->cacheLifetime;
}
public function getPageId(): int
{
return $this->pageId;
}
public function getPageRecord(): array
{
return $this->pageRecord;
}
public function getRenderingInstructions(): array
{
return $this->renderingInstructions;
}
public function getContext(): Context
{
return $this->context;
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
/**
* Event to allow listeners to modify the amount of seconds that a page
* containing this record should be cached.
*/
class ModifyCacheLifetimeForRowEvent
{
public function __construct(
public int $cacheLifetime,
public readonly string $tableName,
public readonly array $pageRecord
) {}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
/**
* Listeners to this event will be able to modify the hreflang tags that will be generated. You can use this when you
* have an edge case language scenario and need to alter the default hreflang tags.
*/
final class ModifyHrefLangTagsEvent
{
private array $hrefLangs = [];
public function __construct(private readonly ServerRequestInterface $request) {}
public function getHrefLangs(): array
{
return $this->hrefLangs;
}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
/**
* Set the hreflangs. This should be an array in format:
*
* ```
* [
* 'en-US' => 'https://example.com',
* 'nl-NL' => 'https://example.com/nl'
* ]
* ```
*
* @param array $hrefLangs
*/
public function setHrefLangs(array $hrefLangs): void
{
$this->hrefLangs = $hrefLangs;
}
/**
* Add a hreflang tag to the current list of hreflang tags
*
* @param string $languageCode The language of the hreflang tag you would like to add. For example: nl-NL
* @param string $url The URL of the translation. For example: https://example.com/nl
*/
public function addHrefLang(string $languageCode, string $url): void
{
$this->hrefLangs[$languageCode] = $url;
}
}
@@ -0,0 +1,96 @@
<?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\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
/**
* A generic PSR 14 Event to allow modifying the incoming (and resolved) page when building a "page link".
*
* This event allows Event Listener to change the page to be linked to, or add/remove possible query
* parameters / fragments to be generated.
*/
final class ModifyPageLinkConfigurationEvent
{
private bool $pageWasModified = false;
public function __construct(
private array $configuration,
private readonly array $linkDetails,
private array $page,
private array $queryParameters,
private string $fragment,
private readonly ServerRequestInterface $request,
) {}
public function getConfiguration(): array
{
return $this->configuration;
}
public function setConfiguration(array $configuration): void
{
$this->configuration = $configuration;
}
public function getLinkDetails(): array
{
return $this->linkDetails;
}
public function getPage(): array
{
return $this->page;
}
public function setPage(array $page): void
{
$this->page = $page;
$this->pageWasModified = true;
}
public function getQueryParameters(): array
{
return $this->queryParameters;
}
public function setQueryParameters(array $queryParameters): void
{
$this->queryParameters = $queryParameters;
}
public function getFragment(): string
{
return $this->fragment;
}
public function setFragment(string $fragment): void
{
$this->fragment = $fragment;
}
public function pageWasModified(): bool
{
return $this->pageWasModified;
}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
/**
* This event allows listeners to adjust and react on TypoScript 'config'.
*
* This event is dispatched *before* final TypoScript 'config' is written to cache, and
* *not* when a page can be successfully retrieved from cache, which is typically
* the case in 'page is fully cached' scenarios.
*
* This incoming $configTree has already been merged with the determined
* PAGE "page.config" TypoScript of the requested 'type' / 'typeNum' and the global
* TypoScript setup 'config'.
*
* The result of this event is available as Request attribute:
* $request->getAttribute('frontend.typoscript')->getConfigTree(),
* and its array variant $request->getAttribute('frontend.typoscript')->getConfigArray().
*
* Registered listener can *set* a modified setup config AST. Note the TypoScript AST
* structure is still marked @internal within v13 core and may change later,
* using the event to *write* different 'config' data is thus still a bit risky.
*/
final class ModifyTypoScriptConfigEvent
{
public function __construct(
private readonly ServerRequestInterface $request,
private readonly RootNode $setupTree,
private RootNode $configTree,
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getSetupTree(): RootNode
{
return $this->setupTree;
}
public function getConfigTree(): RootNode
{
return $this->configTree;
}
public function setConfigTree(RootNode $configTree): void
{
$this->configTree = $configTree;
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
/**
* This event will provide listeners with the opportunity to adjust constants according to the users' requirements
*
* @internal This event did not stabilize yet and may change. Use at your own risk.
* Note the TypoScript AST is also marked @internal at the moment and may change as well.
*/
final class ModifyTypoScriptConstantsEvent
{
public function __construct(
private RootNode $constantsAst,
) {}
public function getConstantsAst(): RootNode
{
return $this->constantsAst;
}
public function setConstantsAst(RootNode $constantsAst): void
{
$this->constantsAst = $constantsAst;
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
/**
* Event to allow listeners to disable the loading of cached page data when a page is requested.
* Does not have any effect if caching is disabled, or if there is no cached version of a page.
*/
final class ShouldUseCachedPageDataIfAvailableEvent
{
public function __construct(
private readonly ServerRequestInterface $request,
private bool $shouldUseCachedPageData
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function shouldUseCachedPageData(): bool
{
return $this->shouldUseCachedPageData;
}
public function setShouldUseCachedPageData(bool $shouldUseCachedPageData): void
{
$this->shouldUseCachedPageData = $shouldUseCachedPageData;
}
}
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\EventListener;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyPreparedEvent;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\PolicyBag;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Fluid\ViewHelpers\Security\NonceViewHelper;
use TYPO3\CMS\Frontend\Cache\CacheInstruction;
use TYPO3\CMS\Frontend\Page\PageParts;
#[AsEventListener('typo3/cms-frontend/content-security-policy/avoid-nonce')]
final class AvoidContentSecurityPolicyNonceEventListener
{
public function __invoke(PolicyPreparedEvent $event): void
{
// skip, since the behavior already has been modified
if ($event->policyBag->behavior->useNonce !== null) {
return;
}
// skip, since the frontend request is not supposed to be fully cacheable
if (!$this->isCacheableFrontendRequest($event->request)) {
return;
}
if ($event->policyBag->nonce->count() === 0
|| $this->isEmptyResponse($event->response)
|| $this->nonceProxyIsAvoidable($event->policyBag)
) {
$event->policyBag->behavior->useNonce = false;
}
}
private function isCacheableFrontendRequest(ServerRequestInterface $request): bool
{
if (!ApplicationType::fromRequest($request)->isFrontend()) {
return false;
}
$cacheInstruction = $request->getAttribute('frontend.cache.instruction');
$pageParts = $request->getAttribute('frontend.page.parts');
return $pageParts instanceof PageParts
&& $cacheInstruction instanceof CacheInstruction
&& $cacheInstruction->isCachingAllowed()
&& !$pageParts->hasNotCachedContentElements();
}
private function isEmptyResponse(string|ResponseInterface|null $response): bool
{
// skip, since it cannot be determined
if ($response === null) {
return false;
}
if (is_string($response)) {
return $response === '';
}
return $response->getBody()->isReadable()
&& $response->getBody()->getSize() === 0;
}
private function nonceProxyIsAvoidable(PolicyBag $policyBag): bool
{
$nonce = $policyBag->nonce;
foreach ($policyBag->dispositionMap->keys() as $disposition) {
$policy = $policyBag->getPolicy($disposition);
if ($policy->isEmpty()) {
continue;
}
foreach (SourceKeyword::nonceProxy->getApplicableDirectives() as $directive) {
$collection = $policy->get($directive);
// directive is not present or does not contain `nonce-proxy`
if ($collection === null || !$collection->contains(SourceKeyword::nonceProxy)) {
continue;
}
// stop in case this directive contains `strict-dynamic`, or the nonce for that directive was consumed
// for inline assets, and there is no alternative (weak) `'unsafe-inline'` source in the policy
if ($collection->contains(SourceKeyword::strictDynamic)
|| (!$collection->contains(SourceKeyword::unsafeInline)
&& $this->familyConsumedInlineNonce($directive, $nonce))
) {
return false;
}
}
}
return true;
}
private function familyConsumedInlineNonce(Directive $directive, ConsumableNonce $nonce): bool
{
// in case the directive was not specified for the `{f:security.nonce()}`
// view-helper, the nonce usage is considered active for all directives
if ($nonce->countInline(NonceViewHelper::class) > 0) {
return true;
}
foreach ($directive->getFamily() as $member) {
if ($nonce->countInline($member) > 0) {
return true;
}
}
return false;
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend;
use TYPO3\CMS\Core\Exception as CoreException;
/**
* A generic Frontend exception
*/
class Exception extends CoreException {}
+196
View File
@@ -0,0 +1,196 @@
<?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\Frontend\Html;
use Masterminds\HTML5;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Typolink\LinkFactory;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
/**
* @internal API still might change
*/
#[Autoconfigure(public: true)]
class HtmlWorker
{
/**
* Removes corresponding tag in case there's a failure
* e.g. `<a href="t3://!!INVALID!!">value</a>` --> ``
*/
public const REMOVE_TAG_ON_FAILURE = 1;
/**
* Removes corresponding attribute in case there's a failure
* e.g. `<a href="t3://!!INVALID!!">value</a>` --> `<a>value</a>`
*/
public const REMOVE_ATTR_ON_FAILURE = 2;
/**
* Removes corresponding enclosure in case there's a failure
* e.g. `<a href="t3://!!INVALID!!">value</a>` --> `value`
*/
public const REMOVE_ENCLOSURE_ON_FAILURE = 4;
protected ?\DOMNode $mount = null;
protected ?\DOMDocument $document = null;
public function __construct(
protected readonly LinkFactory $linkFactory,
protected readonly HTML5 $parser
) {}
public function __toString(): string
{
if (!$this->mount instanceof \DOMNode || !$this->document instanceof \DOMDocument) {
return '';
}
return $this->parser->saveHTML($this->mount->childNodes);
}
public function parse(string $html): self
{
// use document fragment to separate markup from default structure (html, body, ...)
$fragment = $this->parser->parseFragment($html);
// mount fragment to make it accessible in current document
$this->mount = $this->mountFragment($fragment);
$this->document = $this->mount->ownerDocument;
return $this;
}
/**
* @param string|ConsumableNonce $nonce none value to be added
* @param string ...$nodeNames element node names to be processed (e.g. `style`)
*/
public function addNonceAttribute(string|ConsumableNonce $nonce, string ...$nodeNames): self
{
if ($nodeNames === []) {
return $this;
}
$xpath = new \DOMXPath($this->document);
foreach ($nodeNames as $nodeName) {
$expression = sprintf('//%s[not(@*)]', $nodeName);
/** @var \DOMElement $element */
foreach ($xpath->query($expression, $this->mount) as $element) {
$element->setAttribute('nonce', (string)$nonce);
}
}
return $this;
}
public function transformUri(string $selector, int $flags = 0): self
{
if (!$this->mount instanceof \DOMNode || !$this->document instanceof \DOMDocument) {
return $this;
}
$subjects = $this->parseSelector($selector);
// use xpath to traverse potential candidates having "links"
$xpath = new \DOMXPath($this->document);
foreach ($subjects as $subject) {
$attrName = $subject['attr'];
$expression = sprintf('//%s[@%s]', $subject['node'], $attrName);
/** @var \DOMElement $element */
foreach ($xpath->query($expression, $this->mount) as $element) {
$elementAttrValue = $element->getAttribute($attrName);
$scheme = parse_url($elementAttrValue, PHP_URL_SCHEME);
// skip values not having a URI-scheme
if (empty($scheme)) {
continue;
}
try {
$linkResult = $this->linkFactory->createUri($elementAttrValue);
} catch (UnableToLinkException $exception) {
$this->onTransformUriFailure($element, $subject, $flags);
continue;
}
$linkResultAttrValues = array_filter($linkResult->getAttributes());
// usually link results contain `href` attr value, which needs to be assigned
// to a different value in case selector (e.g. `img.src` instead f `a.href`)
if (isset($linkResultAttrValues['href']) && $attrName !== 'href') {
$element->setAttribute($attrName, $linkResultAttrValues['href']);
unset($linkResultAttrValues['href']);
}
foreach ($linkResultAttrValues as $name => $value) {
$element->setAttribute($name, (string)$value);
}
}
}
return $this;
}
/**
* @param \DOMElement $element current element encountered failure
* @param array{node: string, attr: string} $subject node-attr combination
*/
protected function onTransformUriFailure(\DOMElement $element, array $subject, int $flags): void
{
if (($flags & self::REMOVE_TAG_ON_FAILURE) === self::REMOVE_TAG_ON_FAILURE) {
$element->parentNode->removeChild($element);
} elseif (($flags & self::REMOVE_ATTR_ON_FAILURE) === self::REMOVE_ATTR_ON_FAILURE) {
$attrName = $subject['attr'];
$element->removeAttribute($attrName);
} elseif (($flags & self::REMOVE_ENCLOSURE_ON_FAILURE) === self::REMOVE_ENCLOSURE_ON_FAILURE) {
// moves children out of element's enclosure, then removes (empty) element
// eg `<ELEMENT><a><b><c></ELEMENT><NEXT>`
// 1) `<ELEMENT><b><c></ELEMENT><a><NEXT>`
// 2) `<ELEMENT><c></ELEMENT><a><b><NEXT>`
// 3) `<ELEMENT></ELEMENT><a><b><c><NEXT>`
// rm `<a><b><c><NEXT>`
$parentNode = $element->parentNode;
foreach ($element->childNodes as $child) {
$cloned = $child->cloneNode(true);
$parentNode->insertBefore($cloned, $element);
}
$parentNode->removeChild($element);
}
}
/**
* @return array{node: string, attr: string}[]
*/
protected function parseSelector(string $selector): array
{
$items = GeneralUtility::trimExplode(',', $selector, true);
$items = array_map(
static function (string $item): ?array {
$parts = explode('.', $item);
if (count($parts) !== 2) {
return null;
}
return [
'node' => $parts[0] ?: '*',
'attr' => $parts[1],
];
},
$items
);
return array_filter($items);
}
protected function mountFragment(\DOMDocumentFragment $fragment): \DOMNode
{
$document = $fragment->ownerDocument;
$mount = $document->createElement('div');
$document->appendChild($mount);
if ($fragment->hasChildNodes()) {
$mount->appendChild($fragment);
}
return $mount;
}
}
+73
View File
@@ -0,0 +1,73 @@
<?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\Frontend\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
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\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Domain\DateTimeFactory;
use TYPO3\CMS\Core\Http\AbstractApplication;
use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
/**
* Entry point for the TYPO3 Frontend
*/
class Application extends AbstractApplication
{
public function __construct(
#[AutowireInline(
class: MiddlewareDispatcher::class,
arguments: [
'$kernel' => '@' . RequestHandler::class,
'$middlewares' => '@frontend.middlewares',
],
)]
RequestHandlerInterface $requestHandler,
protected readonly Context $context,
) {
$this->requestHandler = $requestHandler;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Create new request object having applicationType "I am a frontend request" attribute.
$request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE);
$this->initializeContext();
return parent::handle($request);
}
/**
* Initializes the Context used for accessing data and finding out the current state of the application
*/
protected function initializeContext(): void
{
$this->context->setAspect('date', new DateTimeAspect(DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME'])));
$this->context->setAspect('visibility', new VisibilityAspect());
$this->context->setAspect('workspace', new WorkspaceAspect(0));
$this->context->setAspect('backend.user', new UserAspect(null));
$this->context->setAspect('frontend.user', new UserAspect(null, [0, -1]));
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,131 @@
<?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\Frontend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\Mfa\MfaRequiredException;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Authentication\FrontendBackendUserAuthentication;
use TYPO3\CMS\Frontend\Cache\CacheInstruction;
/**
* This middleware authenticates a Backend User (be_user) (pre)-viewing a frontend page.
*
* This middleware also ensures that $GLOBALS['LANG'] is available, however it is possible that
* a different middleware later-on might unset the BE_USER as he/she is not allowed to preview a certain
* page due to rights management. As this can only happen once the page ID is resolved, this will happen
* after the routing middleware.
*/
class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator
{
public function __construct(
Context $context,
protected readonly LanguageServiceFactory $languageServiceFactory
) {
parent::__construct($context);
}
/**
* Creates a backend user authentication object, tries to authenticate a user
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Initializing a possible logged-in Backend User
// If the backend cookie is set,
// we proceed and check if a backend user is logged in.
$backendUserObject = null;
if (isset($request->getCookieParams()[BackendUserAuthentication::getCookieName()])) {
$backendUserObject = $this->initializeBackendUser($request);
}
$GLOBALS['BE_USER'] = $backendUserObject;
// Load specific dependencies which are necessary for a valid Backend User
// like $GLOBALS['LANG'] for labels in the language of the BE User (so AdminPanel works)
if ($backendUserObject !== null) {
$GLOBALS['LANG'] = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
$this->setBackendUserAspect($GLOBALS['BE_USER']);
if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)
&& (strtolower($request->getServerParams()['HTTP_CACHE_CONTROL'] ?? '') === 'no-cache'
|| strtolower($request->getServerParams()['HTTP_PRAGMA'] ?? '') === 'no-cache')
) {
// Detecting if shift-reload has been clicked to disable caching if so.
// This is only done if a backend user is logged in to prevent DoS-attacks for "casual" requests.
$cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
$cacheInstruction->disableCache('EXT:frontend: Logged in backend user forced reload disabled cache.');
$request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
}
}
$response = $handler->handle($request);
// If, when building the response, the user is still available, then ensure that the headers are sent properly
if ($this->context->getAspect('backend.user')->isLoggedIn()) {
return $this->applyHeadersToResponse($response);
}
return $response;
}
/**
* Creates the backend user object and returns it if a valid backend user is found.
*/
protected function initializeBackendUser(ServerRequestInterface $request): ?FrontendBackendUserAuthentication
{
// New backend user object
$backendUserObject = GeneralUtility::makeInstance(FrontendBackendUserAuthentication::class);
try {
$backendUserObject->start($request);
} catch (MfaRequiredException $e) {
// Do nothing, as the user is not fully authenticated - has not
// passed required multi-factor authentication - via the backend.
return null;
}
if (!empty($backendUserObject->user['uid'])) {
$this->setBackendUserAspect($backendUserObject, (int)$backendUserObject->user['workspace_id']);
$backendUserObject->fetchGroupData();
}
// Unset the user initialization if any setting / restriction applies
if (!$this->isAuthenticated($backendUserObject, $request, $request->getAttribute('normalizedParams'))) {
$backendUserObject = null;
$this->setBackendUserAspect(null);
}
return $backendUserObject;
}
/**
* Implementing the access checks that the TYPO3 CMS bootstrap script does before a user is ever logged in.
* Returns TRUE if access is OK
*/
protected function isAuthenticated(FrontendBackendUserAuthentication $user, ServerRequestInterface $request, NormalizedParams $normalizedParams): bool
{
// Check IP
$ipMask = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList'] ?? '');
if ($ipMask && !GeneralUtility::cmpIP($normalizedParams->getRemoteAddress(), $ipMask)) {
return false;
}
// Check SSL (https)
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL'] && !$normalizedParams->isHttps()) {
return false;
}
return $user->backendCheckLogin($request);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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\Frontend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Handle cache timeout that is set in typoscript.
*
* @internal
*/
class CacheTimeout implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
$config = $request->getAttribute('frontend.typoscript')?->getConfigArray() ?? [];
if ($config['cache_clearAtMidnight'] ?? false) {
// @todo: We should probably decide to deprecate or remove cache_clearAtMidnight
// altogether since it is a flawed concept based on server timezone
// "when is midnight?".
$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$timeOutTime = min($GLOBALS['EXEC_TIME'] + $cacheDataCollector->resolveLifetime(), PHP_INT_MAX);
$midnightTime = mktime(0, 0, 0, (int)date('m', $timeOutTime), (int)date('d', $timeOutTime), (int)date('Y', $timeOutTime));
// If the midnight time of the expire-day is greater than the current time,
// we may set the timeOutTime to the new midnighttime.
if ($midnightTime > $GLOBALS['EXEC_TIME']) {
$cacheDataCollector->restrictMaximumLifetime($midnightTime - $GLOBALS['EXEC_TIME']);
}
}
return $response;
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Context\Context;
/**
* Add content-length HTTP header to the response.
*
* Notice that all Content outside the length of the content-length header will be cut off!
* Therefore, content of unknown length from later-on middlewares and if admin users are logged
* in (admin panel might show...), we disable it!
*
* @internal
*/
final readonly class ContentLengthResponseHeader implements MiddlewareInterface
{
public function __construct(private Context $context) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
$typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray();
if (
(!isset($typoScriptConfigArray['enableContentLengthHeader']) || $typoScriptConfigArray['enableContentLengthHeader'])
&& !$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)
&& !$this->context->getPropertyFromAspect('workspace', 'isOffline', false)
) {
$response = $response->withHeader('Content-Length', (string)$response->getBody()->getSize());
}
return $response;
}
}
@@ -0,0 +1,113 @@
<?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\Frontend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\CspConfigurationFactory;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\PolicyBag;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\ResponseService;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Site\Entity\Site;
/**
* Adds Content-Security-Policy headers to response.
*
* @internal
*/
final readonly class ContentSecurityPolicyHeaders implements MiddlewareInterface
{
public function __construct(
private RequestId $requestId,
private LoggerInterface $logger,
#[Autowire(service: 'cache.assets')]
private FrontendInterface $cache,
private PolicyProvider $policyProvider,
private CspConfigurationFactory $cspConfigurationFactory,
private ResponseService $responseService,
private DirectiveHashCollection $directiveHashCollection,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
return $this->applyContentSecurityPolicy($request, $handler);
}
/**
* Apply Content-Security-Policy headers to an error response that bypassed
* the normal middleware stack (e.g. responses from ErrorController).
*/
public function applyToResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
return $this->applyContentSecurityPolicy($request, $response);
}
private function applyContentSecurityPolicy(ServerRequestInterface $request, ResponseInterface|RequestHandlerInterface $subject): ResponseInterface
{
$site = $request->getAttribute('site');
$cspConfiguration = $site instanceof Site ? ($site->getConfiguration()['contentSecurityPolicies'] ?? []) : [];
$dispositionMap = $this->cspConfigurationFactory->buildDispositionMap($cspConfiguration);
$behavior = $this->cspConfigurationFactory->buildBehavior($cspConfiguration);
// return early in case CSP shall not be used
if ($dispositionMap->keys() === []) {
return $subject instanceof RequestHandlerInterface ? $subject->handle($request) : $subject;
}
$scope = Scope::frontendSite($site);
$nonce = $this->requestId->nonce;
$policyBag = new PolicyBag($scope, $dispositionMap, $behavior, $nonce, $this->directiveHashCollection);
// make sure, the nonce value is set before processing the remaining components
$request = $request
->withAttribute('nonce', $nonce)
->withAttribute('csp.policyBag', $policyBag);
$response = $subject instanceof RequestHandlerInterface ? $subject->handle($request) : $subject;
if ($response->hasHeader('Content-Security-Policy') || $response->hasHeader('Content-Security-Policy-Report-Only')) {
if ($subject instanceof RequestHandlerInterface) {
$this->logger->info('Content-Security-Policy not enforced due to existence of custom header', [
'scope' => (string)$scope,
'uri' => (string)$request->getUri(),
]);
}
return $response;
}
$processedEarlier = $policyBag->hasPolicies();
$this->policyProvider->prepare($policyBag, $request, $response);
foreach ($dispositionMap as $disposition => $dispositionConfiguration) {
$policy = $policyBag->getPolicy($disposition);
if ($policy->isEmpty()) {
continue;
}
$response = $response->withHeader(
$disposition->getHttpHeaderName(),
$policy->compile($policyBag, $this->cache)
);
}
if (!$processedEarlier && $policyBag->behavior->useNonce === false) {
$response = $this->responseService->dropNonceFromHtmlResponse($response, $nonce);
}
return $response;
}
}

Some files were not shown because too many files have changed in this diff Show More