Leuchtfeuer/typo3-secure-downloads

View on GitHub
Classes/Controller/LogController.php

Summary

Maintainability
A
1 hr
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\Controller;

use Doctrine\DBAL\Exception;
use Leuchtfeuer\SecureDownloads\Domain\Repository\LogRepository;
use Leuchtfeuer\SecureDownloads\Domain\Transfer\Filter;
use Leuchtfeuer\SecureDownloads\Domain\Transfer\Statistic;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class LogController extends ActionController
{
    public function __construct(
        protected ModuleTemplateFactory $moduleTemplateFactory,
        protected LogRepository $logRepository,
    ) {}

    public function initializeAction(): ResponseInterface
    {
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
        $pageRenderer->addCssFile('EXT:secure_downloads/Resources/Public/Styles/Styles.css');
        return $this->htmlResponse('');
    }

    /**
     * @param Filter|null $filter The filter object
     * @return ResponseInterface
     * @throws Exception
     * @throws ExtensionConfigurationExtensionNotConfiguredException
     * @throws ExtensionConfigurationPathDoesNotExistException
     */
    public function listAction(?Filter $filter = null): ResponseInterface
    {
        if ($this->request->hasArgument('reset') && (bool)$this->request->getArgument('reset')) {
            $filter = new Filter();
        } elseif (!$filter instanceof Filter) {
            $filter = $this->getFilterFromBeUserData();
        }

        $extensionConfigurationLogging = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('secure_downloads', 'log') ?? 0;

        $pageId = (int)(array_key_exists('id', $this->request->getQueryParams()) ? $this->request->getQueryParams()['id'] : 0);
        $filter->setPageId($pageId);

        $this->persistFilterInBeUserData($filter);
        $this->resetFilterOnMemoryExhaustionError();

        $itemsPerPage = 20;
        $currentPage = (int)(array_key_exists('currentPage', $this->request->getQueryParams()) && $this->request->getQueryParams()['currentPage'] > 0 ? $this->request->getQueryParams()['currentPage'] : 1);
        $logEntries = $this->logRepository->findByFilter($filter, $currentPage, $itemsPerPage);

        $totalResultsCount = $this->logRepository->countByFilter($filter);
        $totalPages = (int)(ceil($totalResultsCount / $itemsPerPage));

        $statistic = new Statistic();
        $statistic->calc($filter, $this->logRepository);

        $moduleTemplate = $this->moduleTemplateFactory->create($this->request);
        $moduleTemplate->assignMultiple([
            'loggingEnabled' => $extensionConfigurationLogging,
            'logs' => $logEntries,
            'page' => BackendUtility::getRecord('pages', $pageId),
            'users' => $this->getUsers(),
            'fileTypes' => $this->getFileTypes(),
            'filter' => $filter,
            'statistic' => $statistic,
            'pagination' => [
                'totalPages' => $totalPages,
                'currentPage' => $currentPage,
                'previousPage' => max($currentPage - 1, 0),
                'nextPage' => $totalPages > $currentPage ? $currentPage + 1 : 0,
            ],
            'totalResultCount' => $totalResultsCount,
            'isRoot' => $pageId == 0,
        ]);
        return $moduleTemplate->renderResponse('List');
    }

    /**
     * @return list<array<string,mixed>> Array containing all users that have downloaded files
     * @throws Exception
     */
    private function getUsers(): array
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_securedownloads_domain_model_log');

        return $queryBuilder
            ->select('users.uid as uid', 'users.username as username')
            ->from('tx_securedownloads_domain_model_log', 'log')
            ->join('log', 'fe_users', 'users', $queryBuilder->expr()->eq('users.uid', 'log.user'))
            ->where($queryBuilder->expr()->neq('user', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)))
            ->groupBy('users.uid')
            ->executeQuery()
            ->fetchAllAssociative();
    }

    /**
     * @return list<array<string,mixed>> Array containing all used file types
     * @throws Exception
     */
    private function getFileTypes(): array
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_securedownloads_domain_model_log');

        return $queryBuilder
            ->select('media_type')
            ->from('tx_securedownloads_domain_model_log')
            ->groupBy('media_type')->orderBy('media_type', 'ASC')
            ->executeQuery()
            ->fetchAllAssociative();
    }

    /**
     * Get module states (the filter object) from user data
     */
    protected function getFilterFromBeUserData(): Filter
    {
        $serializedConstraint = $this->request->getAttribute('moduleData')->get('filter');
        $filter = null;
        if (is_string($serializedConstraint) && !empty($serializedConstraint)) {
            $filter = @unserialize($serializedConstraint, ['allowed_classes' => [Filter::class, \DateTime::class]]);
        }
        return $filter ?: GeneralUtility::makeInstance(Filter::class);
    }

    /**
     * Save current filter object in be user settings (uC)
     */
    protected function persistFilterInBeUserData(Filter $filter): void
    {
        $moduleData = $this->request->getAttribute('moduleData');
        $moduleData->set('filter', serialize($filter));
        $this->getBackendUser()->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray());
    }

    /**
     * In case the script execution fails, because the user requested too many results
     * (memory exhaustion in php), reset the filters in be user settings, so
     * the belog can be accessed again in the next call.
     */
    protected function resetFilterOnMemoryExhaustionError(): void
    {
        $reservedMemory = new \SplFixedArray(187500); // 3M
        register_shutdown_function(function () use (&$reservedMemory): void {
            $reservedMemory = null; // free the reserved memory
            $error = error_get_last();
            if (str_contains($error['message'] ?? '', 'Allowed memory size of')) {
                $filter = GeneralUtility::makeInstance(Filter::class);
                $this->persistFilterInBeUserData($filter);
            }
        });
    }

    protected function getBackendUser(): BackendUserAuthentication
    {
        return $GLOBALS['BE_USER'];
    }
}