• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eliashaeussler / typo3-warming / 14429819143

13 Apr 2025 01:13PM UTC coverage: 90.562% (-0.9%) from 91.486%
14429819143

Pull #793

github

eliashaeussler
[!!!][FEATURE] Allow to exclude sites and languages from warming

Resolves: #777
Pull Request #793: [!!!][FEATURE] Allow to exclude sites and languages from warming

116 of 135 new or added lines in 10 files covered. (85.93%)

7 existing lines in 3 files now uncovered.

1161 of 1282 relevant lines covered (90.56%)

8.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.51
/Classes/Backend/ContextMenu/ItemProviders/CacheWarmupProvider.php
1
<?php
2

3
declare(strict_types=1);
4

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

24
namespace EliasHaeussler\Typo3Warming\Backend\ContextMenu\ItemProviders;
25

26
use EliasHaeussler\Typo3SitemapLocator;
27
use EliasHaeussler\Typo3Warming\Configuration;
28
use EliasHaeussler\Typo3Warming\Domain;
29
use EliasHaeussler\Typo3Warming\Utility;
30
use TYPO3\CMS\Backend;
31
use TYPO3\CMS\Core;
32

33
/**
34
 * CacheWarmupProvider
35
 *
36
 * @author Elias Häußler <elias@haeussler.dev>
37
 * @license GPL-2.0-or-later
38
 */
39
final class CacheWarmupProvider extends Backend\ContextMenu\ItemProviders\PageProvider
40
{
41
    private const ITEM_MODE_PAGE = 'cacheWarmupPage';
42
    private const ITEM_MODE_SITE = 'cacheWarmupSite';
43

44
    /**
45
     * @var array<string, array{
46
     *     type: string
47
     * }|array{
48
     *     type: string,
49
     *     label: string,
50
     *     iconIdentifier: string,
51
     *     callbackAction: string,
52
     *     childItems?: array<string, array{
53
     *         label?: string,
54
     *         iconIdentifier?: string,
55
     *         callbackAction?: string
56
     *     }>
57
     * }>
58
     */
59
    protected $itemsConfiguration = [
60
        'cacheWarmupDivider' => [
61
            'type' => 'divider',
62
        ],
63
        self::ITEM_MODE_PAGE => [
64
            'type' => 'item',
65
            'label' => 'LLL:EXT:warming/Resources/Private/Language/locallang.xlf:contextMenu.item.cacheWarmup',
66
            'iconIdentifier' => 'cache-warmup-page',
67
            'callbackAction' => 'warmupPageCache',
68
        ],
69
        self::ITEM_MODE_SITE => [
70
            'type' => 'item',
71
            'label' => 'LLL:EXT:warming/Resources/Private/Language/locallang.xlf:contextMenu.item.cacheWarmupAll',
72
            'iconIdentifier' => 'cache-warmup-site',
73
            'callbackAction' => 'warmupSiteCache',
74
        ],
75
    ];
76

77
    public function __construct(
11✔
78
        private readonly Typo3SitemapLocator\Sitemap\SitemapLocator $sitemapLocator,
79
        private readonly Domain\Repository\SiteRepository $siteRepository,
80
        private readonly Domain\Repository\SiteLanguageRepository $siteLanguageRepository,
81
        private readonly Configuration\Configuration $configuration,
82
    ) {
83
        parent::__construct();
11✔
84
    }
85

86
    protected function canRender(string $itemName, string $type): bool
11✔
87
    {
88
        // Early return if cache warmup from page tree is disabled globally
89
        if (!$this->configuration->isEnabledInPageTree()) {
11✔
90
            return false;
1✔
91
        }
92

93
        // Pseudo items (such as dividers) are always renderable
94
        if ($type !== 'item') {
10✔
95
            return true;
10✔
96
        }
97

98
        // Non-supported doktypes are never renderable
99
        $doktype = (int)($this->record['doktype'] ?? null);
10✔
100
        if ($doktype <= 0 || !\in_array($doktype, $this->configuration->getSupportedDoktypes(), true)) {
10✔
UNCOV
101
            return false;
×
102
        }
103

104
        // Language items in sub-menus are already filtered
105
        if (str_contains($itemName, '_lang_')) {
10✔
106
            return true;
7✔
107
        }
108

109
        if (\in_array($itemName, $this->disabledItems, true)) {
10✔
110
            return false;
1✔
111
        }
112

113
        // Root page cannot be used for cache warmup since it is not accessible in Frontend
114
        if ($this->isRoot()) {
10✔
UNCOV
115
            return false;
×
116
        }
117

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

123
        return Utility\AccessUtility::canWarmupCacheOfPage((int)$this->identifier);
10✔
124
    }
125

126
    /**
127
     * @param array<string, array<string, mixed>> $items
128
     * @return array<string, array<string, mixed>>
129
     */
130
    public function addItems(array $items): array
11✔
131
    {
132
        $this->initialize();
11✔
133

134
        $localItems = $this->prepareItems($this->itemsConfiguration);
11✔
135
        $items += $localItems;
11✔
136

137
        return $items;
11✔
138
    }
139

140
    protected function initialize(): void
11✔
141
    {
142
        parent::initialize();
11✔
143
        $this->initSubMenus();
11✔
144
    }
145

146
    public function getPriority(): int
11✔
147
    {
148
        return 45;
11✔
149
    }
150

151
    private function initSubMenus(): void
11✔
152
    {
153
        $site = $this->getCurrentSite();
11✔
154

155
        // Early return if site cannot be resolved
156
        if ($site === null) {
11✔
157
            return;
2✔
158
        }
159

160
        foreach ($this->itemsConfiguration as $itemName => &$configuration) {
9✔
161
            // Skip pseudo types and non-renderable items
162
            $type = $configuration['type'];
9✔
163
            if ($type !== 'item' || !$this->canRender($itemName, $type)) {
9✔
164
                continue;
165
            }
166

167
            // Get all languages of current site that are available for the current backend user
168
            $languages = $this->siteLanguageRepository->findAll($site, $this->backendUser);
8✔
169

170
            // Remove sites where no XML sitemap is available
171
            if ($itemName === self::ITEM_MODE_SITE) {
8✔
172
                $languages = array_filter(
6✔
173
                    $languages,
6✔
174
                    fn(Core\Site\Entity\SiteLanguage $siteLanguage): bool => $this->canWarmupCachesOfSite($siteLanguage),
6✔
175
                );
6✔
176
            } else {
177
                $languages = array_filter(
8✔
178
                    $languages,
8✔
179
                    fn(Core\Site\Entity\SiteLanguage $siteLanguage): bool => Utility\AccessUtility::canWarmupCacheOfPage(
8✔
180
                        (int)$this->identifier,
8✔
181
                        $siteLanguage->getLanguageId(),
8✔
182
                    )
8✔
183
                );
8✔
184
            }
185

186
            // Ignore item if no languages are available
187
            if ($languages === []) {
8✔
188
                $this->disabledItems[] = $itemName;
1✔
189
                continue;
1✔
190
            }
191

192
            // Treat current item as submenu
193
            $configuration['type'] = 'submenu';
7✔
194
            $configuration['childItems'] = [];
7✔
195

196
            // Add each site language as child element of the current item
197
            foreach ($languages as $language) {
7✔
198
                $configuration['childItems'][$itemName . '_lang_' . $language->getLanguageId()] = [
7✔
199
                    'label' => $language->getTitle(),
7✔
200
                    'iconIdentifier' => $language->getFlagIdentifier(),
7✔
201
                    'callbackAction' => $configuration['callbackAction'] ?? null,
7✔
202
                ];
7✔
203
            }
204

205
            // Callback action is not required on the parent item
206
            unset($configuration['callbackAction']);
7✔
207
        }
208
    }
209

210
    /**
211
     * @return array<string, mixed>
212
     */
213
    protected function getAdditionalAttributes(string $itemName): array
10✔
214
    {
215
        $attributes = [
10✔
216
            'data-callback-module' => '@eliashaeussler/typo3-warming/backend/context-menu-action',
10✔
217
        ];
10✔
218

219
        // Early return if current item is not part of a submenu
220
        // within the configured context menu items
221
        if (!str_contains($itemName, '_lang_')) {
10✔
222
            return $attributes;
10✔
223
        }
224

225
        [$parentItem, $languageId] = explode('_lang_', $itemName);
7✔
226

227
        // Add site identifier as data attribute
228
        if ($parentItem === self::ITEM_MODE_SITE) {
7✔
229
            $attributes['data-site-identifier'] = $this->getCurrentSite()?->getIdentifier();
5✔
230
        }
231

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

235
        return $attributes;
7✔
236
    }
237

238
    private function canWarmupCachesOfSite(?Core\Site\Entity\SiteLanguage $siteLanguage = null): bool
10✔
239
    {
240
        $site = $this->getCurrentSite();
10✔
241

242
        // Skip item if we're not in site context or resolved site is unexpected
243
        if ($site === null || $site->getRootPageId() !== (int)$this->identifier) {
10✔
244
            return false;
3✔
245
        }
246

247
        // Check if any sitemap exists
248
        try {
249
            foreach ($this->sitemapLocator->locateBySite($site, $siteLanguage) as $sitemap) {
7✔
250
                if ($this->sitemapLocator->isValidSitemap($sitemap)) {
7✔
251
                    return true;
6✔
252
                }
253
            }
UNCOV
254
        } catch (\Exception) {
×
255
            // Unable to locate any sitemaps
256
        }
257

258
        return false;
1✔
259
    }
260

261
    private function getCurrentSite(): ?Core\Site\Entity\Site
11✔
262
    {
263
        /** @var positive-int $pageId */
264
        $pageId = (int)$this->identifier;
11✔
265

266
        try {
267
            return $this->siteRepository->findOneByPageId($pageId);
11✔
268
        } catch (Core\Exception\SiteNotFoundException) {
1✔
269
            return null;
1✔
270
        }
271
    }
272
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc