Your IP : 216.73.217.95


Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-backend/Classes/Routing/
Upload File :
Current File : /var/www/surf/TYPO3/vendor/typo3/cms-backend/Classes/Routing/PreviewUriBuilder.php

<?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\Backend\Routing;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent;
use TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\DateTimeAspect;
use TYPO3\CMS\Core\Context\VisibilityAspect;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Routing\RouterInterface;
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Substitution for `BackendUtility::getPreviewUrl` for generating links to Frontend URLs
 * with a modified scope.
 */
class PreviewUriBuilder
{
    public const OPTION_SWITCH_FOCUS = 'switchFocus';
    public const OPTION_WINDOW_NAME = 'windowName';
    public const OPTION_WINDOW_FEATURES = 'windowFeatures';
    public const OPTION_WINDOW_SCOPE = 'windowScope';

    public const OPTION_WINDOW_SCOPE_LOCAL = 'local';
    public const OPTION_WINDOW_SCOPE_GLOBAL = 'global';

    protected int $pageId;
    protected int $languageId = 0;
    protected array $rootLine = [];
    protected string $section = '';
    protected array $additionalQueryParameters = [];
    protected bool $moduleLoading = true;
    protected Context $context;

    /**
     * @param int $pageId Page ID to be previewed
     * @return static
     */
    public static function create(int $pageId): self
    {
        return GeneralUtility::makeInstance(static::class, $pageId);
    }

    /**
     * @param int $pageId Page ID to be previewed
     */
    public function __construct(int $pageId)
    {
        $this->pageId = $pageId;
        $this->context = clone GeneralUtility::makeInstance(Context::class);
        $this->context->setAspect(
            'visibility',
            GeneralUtility::makeInstance(VisibilityAspect::class, true, false, false, true)
        );
    }

    /**
     * @param bool $moduleLoading whether to enable JavaScript module loading
     * @return static
     */
    public function withModuleLoading(bool $moduleLoading): self
    {
        if ($this->moduleLoading === $moduleLoading) {
            return $this;
        }
        $target = clone $this;
        $target->moduleLoading = $moduleLoading;
        return $target;
    }

    /**
     * @param array $rootLine (alternative) root-line of pages
     * @return static
     */
    public function withRootLine(array $rootLine): self
    {
        if ($this->rootLine === $rootLine) {
            return $this;
        }
        $target = clone $this;
        $target->rootLine = $rootLine;
        return $this;
    }

    /**
     * @param int $language particular language
     * @return static
     */
    public function withLanguage(int $language): self
    {
        if ($this->languageId === $language) {
            return $this;
        }
        $target = clone $this;
        $target->languageId = $language;
        return $target;
    }

    /**
     * @param string $section particular section (anchor element)
     * @return static
     */
    public function withSection(string $section): self
    {
        if ($this->section === $section) {
            return $this;
        }
        $target = clone $this;
        $target->section = $section;
        return $target;
    }

    /**
     * @param string|array $additionalQueryParameters additional URI query parameters
     * @return static
     */
    public function withAdditionalQueryParameters(array|string $additionalQueryParameters): self
    {
        if (is_array($additionalQueryParameters)) {
            $additionalQueryParams = $additionalQueryParameters;
        } else {
            $additionalQueryParams = [];
            parse_str($additionalQueryParameters, $additionalQueryParams);
        }
        $languageId = $this->languageId;
        if (isset($additionalQueryParams['_language'])) {
            $languageId = (int)$additionalQueryParams['_language'];
            unset($additionalQueryParams['_language']);
        }
        // No change
        if ($this->languageId === $languageId && $additionalQueryParams === $this->additionalQueryParameters) {
            return $this;
        }

        $target = clone $this;
        $target->additionalQueryParameters = $additionalQueryParams;
        $target->languageId = $languageId;
        return $target;
    }

    /**
     * Builds preview URI.
     */
    public function buildUri(array $options = null, Context $context = null): ?UriInterface
    {
        $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
        try {
            $event = new BeforePagePreviewUriGeneratedEvent(
                $this->pageId,
                $this->languageId,
                $this->rootLine,
                $this->section,
                $this->additionalQueryParameters,
                $context ?? $this->context,
                $this->enrichOptions($options)
            );
            $eventDispatcher->dispatch($event);

            // If there hasn't been a custom preview URI set by an event listener, generate it.
            if ($event->getPreviewUri() === null) {
                $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
                $pageInfo = BackendUtility::readPageAccess($event->getPageId(), $permissionClause) ?: [];
                // Check if the page (= its rootline) has a site attached, otherwise just keep the URI as is
                if ($event->getRootline() === []) {
                    $event->setRootline(BackendUtility::BEgetRootLine($event->getPageId()));
                }
                // prepare custom context for link generation (to allow for example time based previews)
                $event->setAdditionalQueryParameters(
                    array_replace_recursive(
                        $event->getAdditionalQueryParameters(),
                        $this->getAdditionalQueryParametersForAccessRestrictedPages($pageInfo, $event->getContext(), $event->getRootline())
                    )
                );

                // Build the URI with a site as prefix, if configured
                $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
                try {
                    $site = $siteFinder->getSiteByPageId($event->getPageId(), $event->getRootline());
                } catch (SiteNotFoundException $e) {
                    throw new UnableToLinkToPageException('The page ' . $event->getPageId() . ' had no proper connection to a site, no link could be built.', 1651499353);
                }
                try {
                    $previewRouteParameters = $event->getAdditionalQueryParameters();
                    // Reassemble encapsulated language id into route parameters to get proper localized page preview
                    // uri for non-default languages.
                    if ($event->getLanguageId() > 0) {
                        $previewRouteParameters['_language'] = $site->getLanguageById($event->getLanguageId());
                    }
                    $event->setPreviewUri(
                        $site->getRouter($event->getContext())->generateUri(
                            $event->getPageId(),
                            $previewRouteParameters,
                            $event->getSection(),
                            RouterInterface::ABSOLUTE_URL
                        )
                    );
                } catch (\InvalidArgumentException | InvalidRouteArgumentsException $e) {
                    throw new UnableToLinkToPageException(sprintf('The link to the page with ID "%d" could not be generated: %s', $event->getPageId(), $e->getMessage()), 1651499354, $e);
                }
            }

            $event = new AfterPagePreviewUriGeneratedEvent(
                $event->getPreviewUri(),
                $event->getPageId(),
                $event->getLanguageId(),
                $event->getRootline(),
                $event->getSection(),
                $event->getAdditionalQueryParameters(),
                $event->getContext(),
                $event->getOptions(),
            );
            $eventDispatcher->dispatch($event);

            return $event->getPreviewUri();
        } catch (UnableToLinkToPageException $e) {
            return null;
        }
    }

    /**
     * Builds attributes array (e.g. `['dispatch-action' => ...]`).
     * CAVE: Attributes are NOT XSS-protected and need to be put through `htmlspecialchars`
     *
     * @param array|null $options
     */
    public function buildDispatcherDataAttributes(array $options = null): ?array
    {
        if (null === ($attributes = $this->buildAttributes($options))) {
            return null;
        }
        $this->loadActionDispatcher();
        return $this->prefixAttributeNames('dispatch-', $attributes);
    }

    /**
     * Builds attributes array (e.g. `['data-dispatch-action' => ...]`).
     * CAVE: Attributes are NOT XSS-protected and need to be put through `htmlspecialchars`
     *
     * @param array|null $options
     */
    public function buildDispatcherAttributes(array $options = null): ?array
    {
        if (null === ($attributes = $this->buildAttributes($options))) {
            return null;
        }
        $this->loadActionDispatcher();
        return $this->prefixAttributeNames('data-dispatch-', $attributes);
    }

    /**
     * Serialized attributes are processed with `htmlspecialchars` and ready to be used.
     *
     * @param array|null $options
     */
    public function serializeDispatcherAttributes(array $options = null): ?string
    {
        if (null === ($attributes = $this->buildDispatcherAttributes($options))) {
            return null;
        }
        return ' ' . GeneralUtility::implodeAttributes($attributes, true);
    }

    /**
     * `<typo3-immediate-action>` does not have a specific meaning and is used to
     * expose `data` attributes, see custom element in `ImmediateActionElement.ts`.
     *
     * @param array|null $options
     */
    public function buildImmediateActionElement(array $options = null): ?string
    {
        if (null === ($attributes = $this->buildAttributes($options))) {
            return null;
        }
        $this->loadImmediateActionElement();
        return sprintf(
            // `<typo3-immediate-action action="TYPO3.WindowManager.localOpen" args="[...]">`
            '<typo3-immediate-action %s></typo3-immediate-action>',
            GeneralUtility::implodeAttributes($attributes, true)
        );
    }

    protected function buildAttributes(array $options = null): ?array
    {
        $options = $this->enrichOptions($options);
        if (null === ($uri = $this->buildUri($options))) {
            return null;
        }
        $args = [
            // target URI
            (string)$uri,
            // whether to switch focus to that window
            $options[self::OPTION_SWITCH_FOCUS],
            // name of the window instance for JavaScript references
            $options[self::OPTION_WINDOW_NAME],
        ];
        if (isset($options[self::OPTION_WINDOW_FEATURES])) {
            // optional window features (e.g. 'width=500,height=300')
            $args[] = $options[self::OPTION_WINDOW_FEATURES];
        }
        return [
            'action' => $options[self::OPTION_WINDOW_SCOPE] === self::OPTION_WINDOW_SCOPE_GLOBAL
                ? 'TYPO3.WindowManager.globalOpen'
                : 'TYPO3.WindowManager.localOpen',
            'args' => json_encode($args),
        ];
    }

    /**
     * Handles options to used for opening preview URI in a new window/tab.
     * + `switchFocus` (bool): whether to focus new window in browser
     * + `windowName` (string): name of window for internal reference
     * + `windowScope` (string): `local` (current document) `global` (whole backend)
     *
     * @param array|null $options
     */
    protected function enrichOptions(array $options = null): array
    {
        return array_merge(
            [
                self::OPTION_SWITCH_FOCUS => null,
                // 'newTYPO3frontendWindow' was used in BackendUtility::viewOnClick
                self::OPTION_WINDOW_NAME => 'newTYPO3frontendWindow',
                self::OPTION_WINDOW_SCOPE => self::OPTION_WINDOW_SCOPE_LOCAL,
            ],
            $options ?? []
        );
    }

    protected function loadActionDispatcher(): void
    {
        if (!$this->moduleLoading) {
            return;
        }
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
        $pageRenderer->loadJavaScriptModule('@typo3/backend/action-dispatcher.js');
    }

    protected function loadImmediateActionElement(): void
    {
        if (!$this->moduleLoading) {
            return;
        }
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
        $pageRenderer->loadJavaScriptModule('@typo3/backend/element/immediate-action-element.js');
    }

    protected function prefixAttributeNames(string $prefix, array $attributes): array
    {
        $attributeNames = array_map(
            static function (string $name) use ($prefix): string {
                return $prefix . $name;
            },
            array_keys($attributes)
        );
        return array_combine(
            $attributeNames,
            array_values($attributes)
        );
    }

    /**
     * Creates ADMCMD parameters for the "viewpage" extension / frontend
     */
    protected function getAdditionalQueryParametersForAccessRestrictedPages(array $pageInfo, Context $context, array $rootLine): array
    {
        if ($pageInfo === []) {
            return [];
        }
        // Initialize access restriction values from current page
        $access = [
            'fe_group' => (string)($pageInfo['fe_group'] ?? ''),
            'starttime' => (int)($pageInfo['starttime'] ?? 0),
            'endtime' => (int)($pageInfo['endtime'] ?? 0),
        ];
        // Only check rootline if the current page has not set extendToSubpages itself
        if (!(bool)($pageInfo['extendToSubpages'] ?? false)) {
            // remove the current page from the rootline
            array_shift($rootLine);
            foreach ($rootLine as $page) {
                // Skip root node and pages which do not define extendToSubpages
                if ((int)($page['uid'] ?? 0) === 0 || !(bool)($page['extendToSubpages'] ?? false)) {
                    continue;
                }
                $access['fe_group'] = (string)($page['fe_group'] ?? '');
                $access['starttime'] = (int)($page['starttime'] ?? 0);
                $access['endtime'] = (int)($page['endtime'] ?? 0);
                // Stop as soon as a page in the rootline has extendToSubpages set
                break;
            }
        }
        $additionalQueryParameters = [];
        if ((int)$access['fe_group'] === -2) {
            // -2 means "show at any login". We simulate first available fe_group.
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                ->getQueryBuilderForTable('fe_groups');
            $queryBuilder->getRestrictions()
                ->removeAll()
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class));

            $activeFeGroupId = $queryBuilder->select('uid')
                ->from('fe_groups')
                ->executeQuery()
                ->fetchOne();

            if ($activeFeGroupId) {
                $additionalQueryParameters['ADMCMD_simUser'] = $activeFeGroupId;
            }
        } elseif (!empty($access['fe_group'])) {
            $additionalQueryParameters['ADMCMD_simUser'] = $access['fe_group'];
        }
        if ($access['starttime'] > $GLOBALS['EXEC_TIME']) {
            // simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
            // a URL for it
            $dateAspect = GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $access['starttime']));
            $context->setAspect('date', $dateAspect);
            $additionalQueryParameters['ADMCMD_simTime'] = $access['starttime'];
        }
        if ($access['endtime'] < $GLOBALS['EXEC_TIME'] && $access['endtime'] !== 0) {
            // Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and
            // in turn PageRouter will generate a URL for it
            $dateAspect = GeneralUtility::makeInstance(
                DateTimeAspect::class,
                new \DateTimeImmutable('@' . ($access['endtime'] - 1))
            );
            $context->setAspect('date', $dateAspect);
            $additionalQueryParameters['ADMCMD_simTime'] = ($access['endtime'] - 1);
        }
        return $additionalQueryParameters;
    }
}