Leuchtfeuer/auth0-for-typo3

View on GitHub
Classes/Service/RedirectService.php

Summary

Maintainability
D
1 day
Test Coverage
<?php

declare(strict_types=1);

/*
 * This file is part of the "Auth0" extension for TYPO3 CMS.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * Florian Wessels <f.wessels@Leuchtfeuer.com>, Leuchtfeuer Digital Marketing
 */

namespace Leuchtfeuer\Auth0\Service;

use Leuchtfeuer\Auth0\Event\RedirectPreProcessingEvent;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Felogin\Controller\FrontendLoginController;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

/**
 * @see FrontendLoginController
 */
class RedirectService implements LoggerAwareInterface
{
    use LoggerAwareTrait;

    /**
     * @var array
     */
    protected array $settings = [];

    public function __construct(array $redirectSettings)
    {
        $this->settings = $redirectSettings;

        if (!$this->logger instanceof Logger) {
            $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
        }
    }

    public function handleRedirect(array $allowedMethods, array $additionalParameters = []): void
    {
        if ((bool)$this->settings['redirectDisable'] === false && !empty($this->settings['redirectMode'])) {
            $this->logger->notice('Try to redirect user.');
            $redirectUris = $this->getRedirectUri($allowedMethods);

            if (!empty($redirectUris)) {
                $redirectUri = $this->addAdditionalParamsToRedirectUri($this->getUri($redirectUris), $additionalParameters);
                $redirectUri = $this->getEventDispatcher()->dispatch(new RedirectPreProcessingEvent($redirectUri, $this))->getRedirectUri();

                $this->logger->notice(sprintf('Redirect to: %s', $redirectUri));
                header('Location: ' . $redirectUri, false, 307);
                die;
            }

            $this->logger->warning('Redirect failed.');
        }
    }

    public function forceRedirectByReferrer($additionalParameters = []): void
    {
        $this->setRedirectDisable(false);
        $this->setRedirectMode('referrer');
        $this->handleRedirect(['referrer'], $additionalParameters);
    }

    public function getRedirectUri(array $allowedRedirects): array
    {
        $redirect_url = [];

        if ($this->settings['redirectMode']) {
            $redirectMethods = GeneralUtility::trimExplode(',', $this->settings['redirectMode'], true);
            foreach ($redirectMethods as $redirMethod) {
                if (in_array($redirMethod, $allowedRedirects)) {
                    // Logintype is needed because the login-page wouldn't be accessible anymore after a login (would always redirect)
                    switch ($redirMethod) {
                        case 'groupLogin':
                            // taken from dkd_redirect_at_login written by Ingmar Schlecht; database-field changed
                            $groupData = $GLOBALS['TSFE']->fe_user->groupData;
                            if (!empty($groupData['uid'])) {
                                // take the first group with a redirect page
                                $userGroupTable = $GLOBALS['TSFE']->fe_user->usergroup_table;
                                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($userGroupTable);
                                $queryBuilder->getRestrictions()->removeAll();
                                $row = $queryBuilder
                                    ->select('felogin_redirectPid')
                                    ->from($userGroupTable)
                                    ->where(
                                        $queryBuilder->expr()->neq(
                                            'felogin_redirectPid',
                                            $queryBuilder->createNamedParameter('')
                                        ),
                                        $queryBuilder->expr()->in(
                                            'uid',
                                            $queryBuilder->createNamedParameter(
                                                $groupData['uid'],
                                                Connection::PARAM_INT_ARRAY
                                            )
                                        )
                                    )
                                    ->execute()
                                    ->fetchAssociative();
                                if ($row) {
                                    $redirect_url[] = $this->pi_getPageLink($row['felogin_redirectPid']);
                                }
                            }
                            break;

                        case 'userLogin':
                            $userTable = $GLOBALS['TSFE']->fe_user->user_table;
                            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($userTable);
                            $queryBuilder->getRestrictions()->removeAll();
                            $row = $queryBuilder
                                ->select('felogin_redirectPid')
                                ->from($userTable)
                                ->where(
                                    $queryBuilder->expr()->neq(
                                        'felogin_redirectPid',
                                        $queryBuilder->createNamedParameter('')
                                    ),
                                    $queryBuilder->expr()->eq(
                                        $GLOBALS['TSFE']->fe_user->userid_column,
                                        $queryBuilder->createNamedParameter(
                                            $GLOBALS['TSFE']->fe_user->user['uid'],
                                            \PDO::PARAM_INT
                                        )
                                    )
                                )
                                ->execute()
                                ->fetchAssociative();

                            if ($row) {
                                $redirect_url[] = $this->pi_getPageLink($row['felogin_redirectPid']);
                            }
                            break;

                        case 'login':
                            if (isset($this->settings['redirectPageLogin'])) {
                                $redirect_url[] = $this->pi_getPageLink((int)$this->settings['redirectPageLogin']);
                            }
                            break;

                        case 'referrer':
                            $redirect_url[] = $this->validateRedirectUrl(GeneralUtility::_GP('referrer'));
                            break;

                        case 'loginError':
                            if ($this->settings['redirectPageLoginError']) {
                                $redirect_url[] = $this->pi_getPageLink((int)$this->settings['redirectPageLoginError']);
                            }
                            break;

                        case 'logout':
                            if (isset($this->settings['redirectPageLogout'])) {
                                $redirect_url[] = $this->pi_getPageLink((int)$this->settings['redirectPageLogout']);
                            }
                            break;

                        case 'getpost':
                            $gpParameters = GeneralUtility::_GPmerged('tx_auth0_loginform');
                            if (isset($gpParameters['redirect']) && !(empty($gpParameters['redirect']))) {
                                $redirect_url[] = $gpParameters['redirect'];
                            }
                            break;
                    }
                }
            }
        }

        // Remove empty values, but keep "0" as value (that's why "strlen" is used as second parameter)
        if (!empty($redirect_url)) {
            return array_filter($redirect_url, 'strlen');
        }

        return [];
    }

    /**
     * @param array $redirectUris
     * @return string
     */
    public function getUri(array $redirectUris): string
    {
        return ((bool)$this->settings['redirectFirstMethod']) ? array_shift($redirectUris) : array_pop($redirectUris);
    }

    public function setRedirectDisable(bool $value): void
    {
        $this->settings['redirectDisable'] = $value;
    }

    public function setRedirectMode(string $value): void
    {
        $this->settings['redirectMode'] = $value;
    }

    protected function addAdditionalParamsToRedirectUri(string $uri, array $additionalParams): string
    {
        if (!empty($additionalParams)) {
            $uri .= '?';
        }

        foreach ($additionalParams as $key => $value) {
            $uri .= $key . '=' . $value . '&';
        }

        return rtrim($uri, '&');
    }

    /**
     * @param $id
     * @param string $target
     * @param array $urlParameters
     * @return string
     */
    protected function pi_getPageLink($id, string $target = '', array $urlParameters = [])
    {
        if ($GLOBALS['TSFE']->cObj instanceof ContentObjectRenderer) {
            return $GLOBALS['TSFE']->cObj->getTypoLink_URL($id, $urlParameters, $target);
        }

        return (string)GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($id)->getRouter()->generateUri($id);
    }

    /**
     * Returns a valid and XSS cleaned url for redirect, checked against configuration "allowedRedirectHosts"
     *
     * @param string $url
     * @return string cleaned referrer or empty string if not valid
     */
    protected function validateRedirectUrl($url): string
    {
        $url = (string)$url;
        if ($url === '') {
            return '';
        }
        // Validate the URL:
        if ($this->isRelativeUrl($url) || $this->isInCurrentDomain($url) || $this->isInLocalDomain($url)) {
            return $url;
        }
        // URL is not allowed
        $this->logger->warning('Url "' . $url . '"" for redirect was not accepted!');

        return '';
    }

    /**
     * Determines whether the URL is relative to the
     * current TYPO3 installation.
     *
     * @param string $url URL which needs to be checked
     * @return bool Whether the URL is considered to be relative
     */
    protected function isRelativeUrl($url)
    {
        $parsedUrl = @parse_url($url);
        if ($parsedUrl !== false && !isset($parsedUrl['scheme']) && !isset($parsedUrl['host'])) {
            // If the relative URL starts with a slash, we need to check if it's within the current site path
            return $parsedUrl['path'][0] !== '/' || \str_starts_with($parsedUrl['path'], GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
        }

        return false;
    }

    /**
     * Determines whether the URL is on the current host and belongs to the
     * current TYPO3 installation. The scheme part is ignored in the comparison.
     *
     * @param string $url URL to be checked
     * @return bool Whether the URL belongs to the current TYPO3 installation
     */
    protected function isInCurrentDomain($url)
    {
        $urlWithoutSchema = preg_replace('#^https?://#', '', $url);
        $siteUrlWithoutSchema = preg_replace('#^https?://#', '', GeneralUtility::getIndpEnv('TYPO3_SITE_URL'));

        return \str_starts_with($urlWithoutSchema . '/', GeneralUtility::getIndpEnv('HTTP_HOST') . '/')
            && \str_starts_with($urlWithoutSchema, $siteUrlWithoutSchema);
    }

    /**
     * Determines whether the URL matches a domain
     * in the sys_domain database table.
     *
     * @param string $url Absolute URL which needs to be checked
     * @return bool Whether the URL is considered to be local
     */
    protected function isInLocalDomain(string $url): bool
    {
        if (GeneralUtility::isValidUrl($url)) {
            $parsedUrl = parse_url($url);
            if ($parsedUrl['scheme'] === 'http' || $parsedUrl['scheme'] === 'https') {
                $host = $parsedUrl['host'];
                // Removes the last path segment and slash sequences like /// (if given):
                $path = preg_replace('#/+[^/]*$#', '', $parsedUrl['path']);

                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_domain');
                $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
                $localDomains = $queryBuilder->select('domainName')
                    ->from('sys_domain')
                    ->execute()
                    ->fetchAllAssociative();

                if (is_array($localDomains)) {
                    foreach ($localDomains as $localDomain) {
                        // strip trailing slashes (if given)
                        $domainName = rtrim($localDomain['domainName'], '/');
                        if (\str_starts_with($host . $path . '/', $domainName . '/')) {
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    }

    protected function getEventDispatcher(): EventDispatcherInterface
    {
        return GeneralUtility::getContainer()->get(EventDispatcherInterface::class);
    }
}