eliashaeussler/typo3-warming

View on GitHub
Classes/Backend/ContextMenu/ItemProviders/CacheWarmupProvider.php

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS extension "warming".
 *
 * Copyright (C) 2021-2024 Elias Häußler <elias@haeussler.dev>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

namespace EliasHaeussler\Typo3Warming\Backend\ContextMenu\ItemProviders;

use EliasHaeussler\Typo3SitemapLocator;
use EliasHaeussler\Typo3Warming\Configuration;
use EliasHaeussler\Typo3Warming\Utility;
use Exception;
use TYPO3\CMS\Backend;
use TYPO3\CMS\Core;

/**
 * CacheWarmupProvider
 *
 * @author Elias Häußler <elias@haeussler.dev>
 * @license GPL-2.0-or-later
 */
final class CacheWarmupProvider extends Backend\ContextMenu\ItemProviders\PageProvider
{
    private const ITEM_MODE_PAGE = 'cacheWarmupPage';
    private const ITEM_MODE_SITE = 'cacheWarmupSite';

    /**
     * @var array<string, array{
     *     type: string
     * }|array{
     *     type: string,
     *     label: string,
     *     iconIdentifier: string,
     *     callbackAction: string,
     *     childItems?: array<string, array{
     *         label?: string,
     *         iconIdentifier?: string,
     *         callbackAction?: string
     *     }>
     * }>
     */
    protected $itemsConfiguration = [
        'cacheWarmupDivider' => [
            'type' => 'divider',
        ],
        self::ITEM_MODE_PAGE => [
            'type' => 'item',
            'label' => 'LLL:EXT:warming/Resources/Private/Language/locallang.xlf:contextMenu.item.cacheWarmup',
            'iconIdentifier' => 'cache-warmup-page',
            'callbackAction' => 'warmupPageCache',
        ],
        self::ITEM_MODE_SITE => [
            'type' => 'item',
            'label' => 'LLL:EXT:warming/Resources/Private/Language/locallang.xlf:contextMenu.item.cacheWarmupAll',
            'iconIdentifier' => 'cache-warmup-site',
            'callbackAction' => 'warmupSiteCache',
        ],
    ];

    public function __construct(
        private readonly Typo3SitemapLocator\Sitemap\SitemapLocator $sitemapLocator,
        private readonly Core\Site\SiteFinder $siteFinder,
        private readonly Configuration\Configuration $configuration,
    ) {
        parent::__construct();
    }

    protected function canRender(string $itemName, string $type): bool
    {
        // Early return if cache warmup from page tree is disabled globally
        if (!$this->configuration->isEnabledInPageTree()) {
            return false;
        }

        // Pseudo items (such as dividers) are always renderable
        if ($type !== 'item') {
            return true;
        }

        // Non-supported doktypes are never renderable
        $doktype = (int)($this->record['doktype'] ?? null);
        if ($doktype <= 0 || !\in_array($doktype, $this->configuration->getSupportedDoktypes(), true)) {
            return false;
        }

        // Language items in sub-menus are already filtered
        if (str_contains($itemName, '_lang_')) {
            return true;
        }

        if (\in_array($itemName, $this->disabledItems, true)) {
            return false;
        }

        // Root page cannot be used for cache warmup since it is not accessible in Frontend
        if ($this->isRoot()) {
            return false;
        }

        // Running cache warmup in "site" mode (= using XML sitemap) is only valid for root pages
        if ($itemName === self::ITEM_MODE_SITE) {
            return $this->canWarmupCachesOfSite();
        }

        return Utility\AccessUtility::canWarmupCacheOfPage((int)$this->identifier);
    }

    /**
     * @param array<string, array<string, mixed>> $items
     * @return array<string, array<string, mixed>>
     */
    public function addItems(array $items): array
    {
        $this->initialize();

        $localItems = $this->prepareItems($this->itemsConfiguration);
        $items += $localItems;

        return $items;
    }

    protected function initialize(): void
    {
        parent::initialize();
        $this->initSubMenus();
    }

    public function getPriority(): int
    {
        return 45;
    }

    private function initSubMenus(): void
    {
        $site = $this->getCurrentSite();

        // Early return if site cannot be resolved
        if ($site === null) {
            return;
        }

        foreach ($this->itemsConfiguration as $itemName => &$configuration) {
            // Skip pseudo types and non-renderable items
            $type = $configuration['type'];
            if ($type !== 'item' || !$this->canRender($itemName, $type)) {
                continue;
            }

            // Get all languages of current site that are available
            // for the current Backend user
            $languages = $site->getAvailableLanguages($this->backendUser);

            // Remove sites where no XML sitemap is available
            if ($itemName === self::ITEM_MODE_SITE) {
                $languages = array_filter(
                    $languages,
                    fn (Core\Site\Entity\SiteLanguage $siteLanguage): bool => $this->canWarmupCachesOfSite($siteLanguage)
                );
            } else {
                $languages = array_filter(
                    $languages,
                    fn (Core\Site\Entity\SiteLanguage $siteLanguage): bool => Utility\AccessUtility::canWarmupCacheOfPage(
                        (int)$this->identifier,
                        $siteLanguage->getLanguageId(),
                    )
                );
            }

            // Ignore item if no languages are available
            if ($languages === []) {
                $this->disabledItems[] = $itemName;
                continue;
            }

            // Treat current item as submenu
            $configuration['type'] = 'submenu';
            $configuration['childItems'] = [];

            // Add each site language as child element of the current item
            foreach ($languages as $language) {
                $configuration['childItems'][$itemName . '_lang_' . $language->getLanguageId()] = [
                    'label' => $language->getTitle(),
                    'iconIdentifier' => $language->getFlagIdentifier(),
                    'callbackAction' => $configuration['callbackAction'] ?? null,
                ];
            }

            // Callback action is not required on the parent item
            unset($configuration['callbackAction']);
        }
    }

    /**
     * @return array<string, mixed>
     */
    protected function getAdditionalAttributes(string $itemName): array
    {
        $attributes = [
            'data-callback-module' => '@eliashaeussler/typo3-warming/backend/context-menu-action',
        ];

        // Early return if current item is not part of a submenu
        // within the configured context menu items
        if (!str_contains($itemName, '_lang_')) {
            return $attributes;
        }

        [$parentItem, $languageId] = explode('_lang_', $itemName);

        // Add site identifier as data attribute
        if ($parentItem === self::ITEM_MODE_SITE) {
            $attributes['data-site-identifier'] = $this->getCurrentSite()?->getIdentifier();
        }

        // Add language ID as data attribute
        $attributes['data-language-id'] = (int)$languageId;

        return $attributes;
    }

    private function canWarmupCachesOfSite(Core\Site\Entity\SiteLanguage $siteLanguage = null): bool
    {
        $site = $this->getCurrentSite();
        $languageId = $siteLanguage?->getLanguageId();

        if ($site === null ||
            $site->getRootPageId() !== (int)$this->identifier ||
            !Utility\AccessUtility::canWarmupCacheOfSite($site, $languageId)
        ) {
            return false;
        }

        // Check if any sitemap exists
        try {
            foreach ($this->sitemapLocator->locateBySite($site, $siteLanguage) as $sitemap) {
                if ($this->sitemapLocator->isValidSitemap($sitemap)) {
                    return true;
                }
            }
        } catch (Exception) {
            // Unable to locate any sitemaps
        }

        return false;
    }

    private function getCurrentSite(): ?Core\Site\Entity\Site
    {
        try {
            return $this->siteFinder->getSiteByPageId((int)$this->identifier);
        } catch (Core\Exception\SiteNotFoundException) {
            return null;
        }
    }
}