diff --git a/Classes/ContentObject/JsonContentContentObject.php b/Classes/ContentObject/JsonContentContentObject.php index 3f717984..5437deeb 100755 --- a/Classes/ContentObject/JsonContentContentObject.php +++ b/Classes/ContentObject/JsonContentContentObject.php @@ -14,7 +14,10 @@ use FriendsOfTYPO3\Headless\Json\JsonEncoder; use FriendsOfTYPO3\Headless\Json\JsonEncoderInterface; use FriendsOfTYPO3\Headless\Utility\HeadlessUserInt; +use Psr\EventDispatcher\EventDispatcherInterface; use TYPO3\CMS\Backend\View\BackendLayoutView; +use TYPO3\CMS\Core\Information\Typo3Version; +use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentContentObject; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -23,6 +26,7 @@ use function count; use function is_array; use function json_decode; +use function json_encode; use function str_contains; use function trim; @@ -102,11 +106,18 @@ class JsonContentContentObject extends ContentContentObject { private HeadlessUserInt $headlessUserInt; private JsonEncoderInterface $jsonEncoder; + /** + * @var mixed|object|\Psr\Log\LoggerAwareInterface|\TYPO3\CMS\Core\SingletonInterface|TimeTracker|(TimeTracker&\Psr\Log\LoggerAwareInterface)|(TimeTracker&\TYPO3\CMS\Core\SingletonInterface)|null + */ + private TimeTracker $timeTracker; + private EventDispatcherInterface $eventDispatcher; public function __construct() { $this->headlessUserInt = GeneralUtility::makeInstance(HeadlessUserInt::class); $this->jsonEncoder = GeneralUtility::makeInstance(JsonEncoder::class); + $this->timeTracker = GeneralUtility::makeInstance(TimeTracker::class); + $this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); } /** @@ -199,6 +210,8 @@ protected function groupContentElementsByColPos(array $contentElements, array $c */ private function prepareValue(array $conf): array { + $t3v13andAbove = (new Typo3Version())->getMajorVersion() >= 13; + $frontendController = $this->getTypoScriptFrontendController(); $theValue = []; $originalRec = $frontendController->currentRecord; @@ -233,20 +246,44 @@ private function prepareValue(array $conf): array $tmpValue = ''; do { - $records = $this->cObj->getRecords($conf['table'], $conf['select.']); + if ($t3v13andAbove) { + $modifyRecordsEvent = $this->eventDispatcher->dispatch( + new \TYPO3\CMS\Frontend\ContentObject\Event\ModifyRecordsAfterFetchingContentEvent( + $this->cObj->getRecords($conf['table'], $conf['select.']), + json_encode($theValue, JSON_THROW_ON_ERROR), + $slide, + $slideCollect, + $slideCollectReverse, + $slideCollectFuzzy, + $conf + ) + ); + + $records = $modifyRecordsEvent->getRecords(); + $theValue = json_decode($modifyRecordsEvent->getFinalContent(), true, 512, JSON_THROW_ON_ERROR); + $slide = $modifyRecordsEvent->getSlide(); + $slideCollect = $modifyRecordsEvent->getSlideCollect(); + $slideCollectReverse = $modifyRecordsEvent->getSlideCollectReverse(); + $slideCollectFuzzy = $modifyRecordsEvent->getSlideCollectFuzzy(); + $conf = $modifyRecordsEvent->getConfiguration(); + } else { + $records = $this->cObj->getRecords($conf['table'], $conf['select.']); + } $cobjValue = []; if (!empty($records)) { - $this->getTimeTracker()->setTSlogMessage('NUMROWS: ' . count($records)); + $this->timeTracker->setTSlogMessage('NUMROWS: ' . count($records)); $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class, $frontendController); $cObj->setParent($this->cObj->data, $this->cObj->currentRecord); $this->cObj->currentRecordNumber = 0; foreach ($records as $row) { - // Call hook for possible manipulation of database row for cObj->data - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content_content.php']['modifyDBRow'] ?? [] as $className) { - $_procObj = GeneralUtility::makeInstance($className); - $_procObj->modifyDBRow($row, $conf['table']); + if (!$t3v13andAbove) { + // Call hook for possible manipulation of database row for cObj->data + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content_content.php']['modifyDBRow'] ?? [] as $className) { + $_procObj = GeneralUtility::makeInstance($className); + $_procObj->modifyDBRow($row, $conf['table']); + } } $registerField = $conf['table'] . ':' . ($row['uid'] ?? 0); if (!($frontendController->recordRegister[$registerField] ?? false)) { @@ -282,7 +319,7 @@ private function prepareValue(array $conf): array } $again = (string)$conf['select.']['pidInList'] !== ''; } - } while ($again && $slide && ((string)$tmpValue === '' && $slideCollectFuzzy || $slideCollect)); + } while ($again && $slide && (((string)$tmpValue === '' && $slideCollectFuzzy) || $slideCollect)); $theValue = $this->groupContentElementsByColPos($theValue, $conf); // Restore diff --git a/Classes/Middleware/ShortcutAndMountPointRedirect.php b/Classes/Middleware/ShortcutAndMountPointRedirect.php index 8c644bfa..0c8b15b9 100644 --- a/Classes/Middleware/ShortcutAndMountPointRedirect.php +++ b/Classes/Middleware/ShortcutAndMountPointRedirect.php @@ -12,29 +12,16 @@ namespace FriendsOfTYPO3\Headless\Middleware; use FriendsOfTYPO3\Headless\Utility\HeadlessMode; +use FriendsOfTYPO3\Headless\Utility\UrlUtility; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; -use TYPO3\CMS\Core\Domain\Repository\PageRepository; -use TYPO3\CMS\Core\Http\ImmediateResponseException; use TYPO3\CMS\Core\Http\JsonResponse; use TYPO3\CMS\Core\Http\RedirectResponse; -use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Frontend\Controller\ErrorController; -use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; - -use function is_array; -use function parse_url; - -class ShortcutAndMountPointRedirect implements MiddlewareInterface, LoggerAwareInterface +class ShortcutAndMountPointRedirect extends \TYPO3\CMS\Frontend\Middleware\ShortcutAndMountPointRedirect { - use LoggerAwareTrait; - public function __construct(private readonly HeadlessMode $headlessMode) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface @@ -46,96 +33,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $handler->handle($request); } - $exposeInformation = $GLOBALS['TYPO3_CONF_VARS']['FE']['exposeRedirectInformation'] ?? false; - - // Check for shortcut page and mount point redirect - try { - $redirectToUri = $this->getRedirectUri($request); - } catch (ImmediateResponseException $e) { - return $e->getResponse(); - } - if ($redirectToUri !== null && $redirectToUri !== (string)$request->getUri()) { - /** @var PageArguments $pageArguments */ - $pageArguments = $request->getAttribute('routing', null); - $message = 'TYPO3 Shortcut/Mountpoint' . ($exposeInformation ? ' at page with ID ' . $pageArguments->getPageId() : ''); - - if ($this->isHeadlessEnabled($request)) { - $parsed = parse_url($redirectToUri); - if (is_array($parsed)) { - $path = $parsed['path'] ?? '/'; - return new JsonResponse(['redirectUrl' => $path, 'statusCode' => 307]); - } - } - - return new RedirectResponse( - $redirectToUri, - 307, - ['X-Redirect-By' => $message] - ); - } - - // See if the current page is of doktype "External URL", if so, do a redirect as well. - $controller = $request->getAttribute('frontend.controller'); - if ((int)$controller->page['doktype'] === PageRepository::DOKTYPE_LINK) { - $externalUrl = $this->prefixExternalPageUrl( - $controller->page['url'], - $request->getAttribute('normalizedParams')->getSiteUrl() - ); - $message = 'TYPO3 External URL' . ($exposeInformation ? ' at page with ID ' . $controller->page['uid'] : ''); - if (!empty($externalUrl)) { - if ($this->isHeadlessEnabled($request)) { - return new JsonResponse(['redirectUrl' => $externalUrl, 'statusCode' => 303]); - } + $coreResponse = parent::process($request, $handler); - return new RedirectResponse( - $externalUrl, - 303, - ['X-Redirect-By' => $message] - ); - } - $this->logger->error( - 'Page of type "External URL" could not be resolved properly', - [ - 'page' => $controller->page, - ] - ); - return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'Page of type "External URL" could not be resolved properly', - $controller->getPageAccessFailureReasons(PageAccessFailureReasons::INVALID_EXTERNAL_URL) - ); + if ($coreResponse instanceof RedirectResponse && $this->isHeadlessEnabled($request)) { + return new JsonResponse([ + 'redirectUrl' => GeneralUtility::makeInstance(UrlUtility::class) + ->withRequest($request) + ->prepareRelativeUrlIfPossible($coreResponse->getHeader('location')[0] ?? ''), + 'statusCode' => $coreResponse->getStatusCode(), + ]); } - return $handler->handle($request); - } - - protected function getRedirectUri(ServerRequestInterface $request): ?string - { - $controller = $request->getAttribute('frontend.controller'); - $redirectToUri = $controller->getRedirectUriForShortcut($request); - return $redirectToUri ?? $controller->getRedirectUriForMountPoint($request); - } - - /** - * Returns the redirect URL for the input page row IF the doktype is set to 3. - * - * @param string $redirectTo The page row to return URL type for - * @param string $sitePrefix if no protocol or relative path given, the site prefix is added - * @return string The URL from based on the external page URL given with a prefix. - */ - protected function prefixExternalPageUrl(string $redirectTo, string $sitePrefix): string - { - $uI = parse_url($redirectTo); - // If relative path, prefix Site URL - // If it's a valid email without protocol, add "mailto:" - if (!($uI['scheme'] ?? false)) { - if (GeneralUtility::validEmail($redirectTo)) { - $redirectTo = 'mailto:' . $redirectTo; - } elseif (!str_starts_with($redirectTo, '/')) { - $redirectTo = $sitePrefix . $redirectTo; - } - } - return $redirectTo; + return $coreResponse; } private function isHeadlessEnabled(ServerRequestInterface $request): bool diff --git a/Classes/Seo/CanonicalGenerator.php b/Classes/Seo/CanonicalGenerator.php index fafb93cc..e24f9cf6 100644 --- a/Classes/Seo/CanonicalGenerator.php +++ b/Classes/Seo/CanonicalGenerator.php @@ -12,73 +12,42 @@ namespace FriendsOfTYPO3\Headless\Seo; use FriendsOfTYPO3\Headless\Utility\HeadlessMode; -use Psr\EventDispatcher\EventDispatcherInterface; -use TYPO3\CMS\Core\Domain\Page; -use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; -use TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent; +use TYPO3\CMS\Seo\Canonical\CanonicalGenerator as CoreCanonicalGenerator; use function htmlspecialchars; use function json_encode; /** - * Overridden core version with headless implementation + * Decorate Core version with headless flavor * * @codeCoverageIgnore */ -class CanonicalGenerator extends \TYPO3\CMS\Seo\Canonical\CanonicalGenerator +class CanonicalGenerator { - protected TypoScriptFrontendController $typoScriptFrontendController; - protected PageRepository $pageRepository; - protected EventDispatcherInterface $eventDispatcher; - public function handle(array &$params): string { - if ($this->typoScriptFrontendController->config['config']['disableCanonical'] ?? false) { - return ''; - } + $canonical = GeneralUtility::makeInstance(CoreCanonicalGenerator::class)->generate($params); - $event = new ModifyUrlForCanonicalTagEvent('', $params['request'], new Page($params['page'])); - $event = $this->eventDispatcher->dispatch($event); - $href = $event->getUrl(); - - if (empty($href) && (int)$this->typoScriptFrontendController->page['no_index'] === 1) { + if ($canonical === '') { return ''; } - if (empty($href)) { - // 1) Check if page has canonical URL set - $href = $this->checkForCanonicalLink(); - } - if (empty($href)) { - // 2) Check if page show content from other page - $href = $this->checkContentFromPid(); - } - if (empty($href)) { - // 3) Fallback, create canonical URL - $href = $this->checkDefaultCanonical(); - } + if (GeneralUtility::makeInstance(HeadlessMode::class)->withRequest($params['request'])->isEnabled()) { + $canonical = [ + 'href' => $this->processCanonical($canonical), + 'rel' => 'canonical', + ]; - if (!empty($href)) { - if (GeneralUtility::makeInstance(HeadlessMode::class)->withRequest($params['request'])->isEnabled()) { - $canonical = [ - 'href' => htmlspecialchars($href), - 'rel' => 'canonical', - ]; + $params['_seoLinks'][] = $canonical; + $canonical = json_encode($canonical); + } - $params['_seoLinks'][] = $canonical; - $canonical = json_encode($canonical); - } else { - $canonical = ' 'canonical', - 'href' => $href, - ], true) . '/>' . LF; - $this->typoScriptFrontendController->additionalHeaderData[] = $canonical; - } + return $canonical; + } - return $canonical; - } - return ''; + protected function processCanonical(string $canonical): string + { + return htmlspecialchars(GeneralUtility::get_tag_attributes($canonical)['href'] ?? ''); } } diff --git a/Classes/Seo/MetaHandler.php b/Classes/Seo/MetaHandler.php index 7be45107..bf8a573c 100644 --- a/Classes/Seo/MetaHandler.php +++ b/Classes/Seo/MetaHandler.php @@ -35,7 +35,7 @@ public function process(ServerRequestInterface $request, TypoScriptFrontendContr GeneralUtility::callUserFunction($_funcRef, $_params, $_ref); } - $content['seo']['title'] = $controller->generatePageTitle(); + $content['seo']['title'] = $controller->generatePageTitle($request); $this->generateMetaTagsFromTyposcript( $controller->pSetup['meta.'] ?? [], diff --git a/Classes/XClass/Controller/FormFrontendController.php b/Classes/XClass/Controller/FormFrontendController.php index bc62fb04..64c13a8e 100644 --- a/Classes/XClass/Controller/FormFrontendController.php +++ b/Classes/XClass/Controller/FormFrontendController.php @@ -18,39 +18,34 @@ use FriendsOfTYPO3\Headless\Utility\HeadlessMode; use FriendsOfTYPO3\Headless\XClass\FormRuntime; use Psr\Http\Message\ResponseInterface; +use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface; use TYPO3\CMS\Extbase\Error\Error; -use TYPO3\CMS\Extbase\Security\Cryptography\HashService; +use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters; use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory; use TYPO3\CMS\Form\Domain\Model\FormDefinition; use function array_merge; use function array_pop; use function base64_encode; +use function class_exists; use function count; use function in_array; +use function is_array; use function json_decode; use function serialize; use function str_replace; /** - * The frontend controller + * Overridden form implementation with headless flavor * - * Scope: frontend * @internal * @codeCoverageIgnore */ class FormFrontendController extends \TYPO3\CMS\Form\Controller\FormFrontendController { - private Translator $jsonFormTranslator; - - public function __construct() - { - $this->hashService = GeneralUtility::makeInstance(HashService::class); - $this->jsonFormTranslator = GeneralUtility::makeInstance(Translator::class); - } - /** * Take the form which should be rendered from the plugin settings * and overlay the formDefinition with additional data from @@ -64,13 +59,28 @@ public function renderAction(): ResponseInterface { $headlessMode = GeneralUtility::makeInstance(HeadlessMode::class); - if (!$headlessMode->withRequest($GLOBALS['TYPO3_REQUEST'])->isEnabled()) { + if (!$headlessMode->withRequest($this->request)->isEnabled()) { return parent::renderAction(); } $formDefinition = []; if (!empty($this->settings['persistenceIdentifier'])) { - $formDefinition = $this->formPersistenceManager->load($this->settings['persistenceIdentifier']); + $formSettings = []; + $typoScriptSettings = []; + + if ((new Typo3Version())->getMajorVersion() >= 13) { + $typoScriptSettings = $this->configurationManager->getConfiguration( + ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, + 'form' + ); + $formSettings = $this->extFormConfigurationManager->getYamlConfiguration($typoScriptSettings, true); + } + + $formDefinition = $this->formPersistenceManager->load( + $this->settings['persistenceIdentifier'], + $formSettings, + $typoScriptSettings + ); $formDefinition['persistenceIdentifier'] = $this->settings['persistenceIdentifier']; $formDefinition = $this->overrideByFlexFormSettings($formDefinition); $formDefinition = ArrayUtility::setValueByPath( @@ -80,10 +90,7 @@ public function renderAction(): ResponseInterface '.' ); - $formId = ($this->configurationManager->getContentObject() !== null ? - ($this->configurationManager->getContentObject()->data['uid'] ?? 0) : 0); - - $formDefinition['identifier'] .= '-' . $formId; + $formDefinition['identifier'] .= '-' . ($this->request->getAttribute('currentContentObject')?->data['uid'] ?? ''); } $i18n = []; @@ -128,14 +135,21 @@ public function renderAction(): ResponseInterface $honeyPot = array_pop($elements); } - $stateHash = $this->hashService->appendHmac(base64_encode(serialize($formState))); + $stateHash = $this->getHashService()->appendHmac( + base64_encode(serialize($formState)), + class_exists(\TYPO3\CMS\Form\Security\HashScope::class) ? \TYPO3\CMS\Form\Security\HashScope::FormState->prefix() : '' + ); $currentPageIndex = $formRuntime->getCurrentPage() ? $formRuntime->getCurrentPage()->getIndex() : 0; $currentPageId = $currentPageIndex + 1; $formFields = $formDefinition['renderables'][$currentPageIndex]['renderables'] ?? []; // provides support for custom options providers (dynamic selects/radio/checkboxes) - $formFieldsNames = $this->generateFieldNamesAndReplaceCustomOptions($formFields, $formDefinition['identifier'], $formRuntime); + $formFieldsNames = $this->generateFieldNamesAndReplaceCustomOptions( + $formFields, + $formDefinition['identifier'], + $formRuntime + ); if ($honeyPot) { $formFields[] = [ @@ -188,7 +202,7 @@ public function renderAction(): ResponseInterface $formDefinition['renderables'][$currentPageIndex]['renderables'] = $formFields; $formDefinition['i18n'] = count($i18n) ? $i18n : null; - $formDefinition = $this->jsonFormTranslator->translate( + $formDefinition = $this->getFormTranslator()->translate( $formDefinition, $formRuntime->getFormDefinition()->getRenderingOptions(), $formRuntime->getFormState() ? $formRuntime->getFormState()->getFormValues() : [] @@ -260,8 +274,11 @@ private function getNextPage(\TYPO3\CMS\Form\Domain\Runtime\FormRuntime $formRun * @param array $formFields * @return array */ - private function generateFieldNamesAndReplaceCustomOptions(array &$formFields, string $identifier, FormRuntime $formRuntime): array - { + private function generateFieldNamesAndReplaceCustomOptions( + array &$formFields, + string $identifier, + FormRuntime $formRuntime + ): array { $formFieldsNames = []; foreach ($formFields as &$field) { @@ -274,7 +291,13 @@ private function generateFieldNamesAndReplaceCustomOptions(array &$formFields, s ); } else { if (!empty($field['properties']['customOptions'])) { - $customOptions = GeneralUtility::makeInstance($field['properties']['customOptions'], $field, $formFields, $identifier, $formRuntime); + $customOptions = GeneralUtility::makeInstance( + $field['properties']['customOptions'], + $field, + $formFields, + $identifier, + $formRuntime + ); if ($customOptions instanceof CustomOptionsInterface) { $field['properties']['options'] = $customOptions->get(); @@ -295,4 +318,18 @@ private function generateFieldNamesAndReplaceCustomOptions(array &$formFields, s return $formFieldsNames; } + + private function getHashService(): \TYPO3\CMS\Extbase\Security\Cryptography\HashService|\TYPO3\CMS\Core\Crypto\HashService + { + if ((new Typo3Version())->getMajorVersion() >= 13) { + return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Crypto\HashService::class); + } + + return GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Security\Cryptography\HashService::class); + } + + private function getFormTranslator(): Translator + { + return GeneralUtility::makeInstance(Translator::class); + } } diff --git a/Classes/XClass/ResourceLocalDriver.php b/Classes/XClass/ResourceLocalDriver.php index 285479af..a67207d5 100644 --- a/Classes/XClass/ResourceLocalDriver.php +++ b/Classes/XClass/ResourceLocalDriver.php @@ -16,6 +16,8 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Http\Uri; +use TYPO3\CMS\Core\Information\Typo3Version; +use TYPO3\CMS\Core\Resource\Capabilities; use TYPO3\CMS\Core\Resource\Driver\LocalDriver; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -41,7 +43,7 @@ protected function determineBaseUrl(): void return; } - if ($this->hasCapability(ResourceStorage::CAPABILITY_PUBLIC)) { + if ($this->hasCapability(((new Typo3Version())->getMajorVersion() < 13) ? ResourceStorage::CAPABILITY_PUBLIC : Capabilities::CAPABILITY_PUBLIC)) { $urlUtility = GeneralUtility::makeInstance(UrlUtility::class)->withRequest($request); $basePath = match (true) { diff --git a/Configuration/SiteConfiguration/Overrides/site.php b/Configuration/SiteConfiguration/Overrides/site.php index e3f14f98..03bfb80e 100644 --- a/Configuration/SiteConfiguration/Overrides/site.php +++ b/Configuration/SiteConfiguration/Overrides/site.php @@ -7,7 +7,6 @@ * LICENSE.md file that was distributed with this source code. */ -use FriendsOfTYPO3\Headless\Utility\Headless; use FriendsOfTYPO3\Headless\Utility\HeadlessMode; use TYPO3\CMS\Core\Configuration\Features; use TYPO3\CMS\Core\Utility\GeneralUtility; diff --git a/composer.json b/composer.json index 5d4f06ff..5be2f056 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,8 @@ ], "require": { "ext-json": "*", - "typo3/cms-core": "^12.4 || ^13 ", - "typo3/cms-install": "^12.4 || ^13" + "typo3/cms-core": "^12.4 || ^13.3", + "typo3/cms-install": "^12.4 || ^13.3" }, "require-dev": { "ergebnis/composer-normalize": "^2.15.0",