TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/vendor/
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 . '-->';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user