Leuchtfeuer/typo3-secure-downloads

View on GitHub
Classes/Factory/SecureLinkFactory.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

declare(strict_types=1);

/*
 * This file is part of the "Secure Downloads" Extension for TYPO3 CMS.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * (c) Dev <dev@Leuchtfeuer.com>, Leuchtfeuer Digital Marketing
 */

namespace Leuchtfeuer\SecureDownloads\Factory;

use Leuchtfeuer\SecureDownloads\Cache\EncodeCache;
use Leuchtfeuer\SecureDownloads\Domain\Transfer\ExtensionConfiguration;
use Leuchtfeuer\SecureDownloads\Domain\Transfer\Token\AbstractToken;
use Leuchtfeuer\SecureDownloads\Exception\InvalidClassException;
use Leuchtfeuer\SecureDownloads\Factory\Event\EnrichPayloadEvent;
use Leuchtfeuer\SecureDownloads\Registry\TokenRegistry;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

class SecureLinkFactory implements SingletonInterface
{
    public const DEFAULT_CACHE_LIFETIME = 86400;

    private AbstractToken $token;

    /**
     * @throws ContentRenderingException
     * @throws InvalidClassException
     */
    public function __construct(private EventDispatcher $eventDispatcher, private ExtensionConfiguration $extensionConfiguration)
    {
        $this->token = TokenRegistry::getToken();
        $this->initializeToken();
    }

    /**
     * Initialize the token.
     * @throws ContentRenderingException
     */
    protected function initializeToken(): void
    {
        $this->token->setExp($this->calculateLinkLifetime());
        if (!Environment::isCli()) {
            $request = $this->getRequest();
            if (ApplicationType::fromRequest($request)->isFrontend()) {
                $pageArguments = $request->getAttribute('routing');
                $pageId = $pageArguments?->getPageId();
            } elseif (ApplicationType::fromRequest($request)->isBackend()) {
                $site = $request->getAttribute('site');
                $pageId = $site->getRootPageId();
            }
        }
        $this->token->setPage($pageId ?? 0);

        try {
            /** @var UserAspect $userAspect */
            $userAspect = GeneralUtility::makeInstance(Context::class)->getAspect('frontend.user');
            $this->token->setUser($userAspect->get('id'));
            $this->token->setGroups($userAspect->getGroupIds());
        } catch (\Exception) {
            // Do nothing.
        }
    }

    private function getRequest(): ServerRequest
    {
        return $GLOBALS['TYPO3_REQUEST'];
    }

    /**
     * Adds the configured additional cache time and the cache lifetime of the current site to the actual time.
     *
     * @return int The link lifetime
     * @throws AspectNotFoundException
     * @throws AspectNotFoundException
     */
    protected function calculateLinkLifetime(): int
    {
        $cacheTimeout = (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController && !empty($GLOBALS['TSFE']->page)) ? $GLOBALS['TSFE']->get_cache_timeout() : self::DEFAULT_CACHE_LIFETIME;

        return $cacheTimeout + GeneralUtility::makeInstance(\TYPO3\CMS\Core\Context\Context::class)->getPropertyFromAspect('date', 'timestamp') + $this->extensionConfiguration->getCacheTimeAdd();
    }

    /**
     * Builds a URI which uses a PHP Script to access the resource by taking several parameters into account.
     */
    public function getUrl(): string
    {
        $hash = $this->token->getHash();

        // Retrieve URL from JWT cache
        if (EncodeCache::hasCache($hash)) {
            return EncodeCache::getCache($hash);
        }

        $url = sprintf(
            '%s/%s%s/%s',
            $this->extensionConfiguration->getLinkPrefix(),
            $this->extensionConfiguration->getTokenPrefix(),
            $this->getJsonWebToken(),
            pathinfo($this->token->getFile(), PATHINFO_BASENAME)
        );

        // Store URL in JWT cache
        EncodeCache::addCache($hash, $url);

        return $url;
    }

    /**
     * @param int $expires The timestamp at which the link becomes invalid
     *
     * @return $this
     */
    public function withLinkTimeout(int $expires): self
    {
        $clonedObject = clone $this;
        $clonedObject->token->setExp($expires);

        return $clonedObject;
    }

    /**
     * @param int $page The page ID for which the link should be generated for
     *
     * @return $this
     */
    public function withPage(int $page): self
    {
        $clonedObject = clone $this;
        $clonedObject->token->setPage($page);

        return $clonedObject;
    }

    /**
     * @param int $user The user for which the link should be valid for
     *
     * @return $this
     */
    public function withUser(int $user): self
    {
        $clonedObject = clone $this;
        $this->token->setUser($user);

        return $clonedObject;
    }

    /**
     * @param array<int> $groups An array of user groups for whom the link should be valid for
     *
     * @return $this
     */
    public function withGroups(array $groups): self
    {
        $clonedObject = clone $this;
        $clonedObject->token->setGroups($groups);

        return $clonedObject;
    }

    /**
     * @param string $resourceUri The actual path to the file that should be secured
     *
     * @return $this
     */
    public function withResourceUri(string $resourceUri): self
    {
        $clonedObject = clone $this;
        $clonedObject->token->setFile($resourceUri);

        return $clonedObject;
    }

    /**
     * @return string The generated JSON web token
     */
    protected function getJsonWebToken(): string
    {
        $payload = $this->token->getPayload();
        $this->dispatchEnrichPayloadEvent($payload);

        return $this->token->encode($payload);
    }

    /**
     * Dispatches the EnrichPayloadEvent event.
     *
     * @param array<string, mixed> $payload The payload of the token
     */
    protected function dispatchEnrichPayloadEvent(array &$payload): void
    {
        $event = new EnrichPayloadEvent($payload, $this->token);
        $event = $this->eventDispatcher->dispatch($event);
        $payload = $event->getPayload();
    }
}