1<?php
2declare(strict_types = 1);
3namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18use TYPO3\CMS\Backend\Routing\UriBuilder;
19use TYPO3\CMS\Backend\Utility\BackendUtility;
20use TYPO3\CMS\Core\Type\Bitmask\Permission;
21use TYPO3\CMS\Core\Utility\GeneralUtility;
22
23/**
24 * Context menu item provider for pages table
25 */
26class PageProvider extends RecordProvider
27{
28    /**
29     * @var string
30     */
31    protected $table = 'pages';
32
33    /**
34     * @var array
35     */
36    protected $itemsConfiguration = [
37        'view' => [
38            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view',
39            'iconIdentifier' => 'actions-view-page',
40            'callbackAction' => 'viewRecord'
41        ],
42        'edit' => [
43            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit',
44            'iconIdentifier' => 'actions-page-open',
45            'callbackAction' => 'editRecord'
46        ],
47        'new' => [
48            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.new',
49            'iconIdentifier' => 'actions-page-new',
50            'callbackAction' => 'newRecord'
51        ],
52        'info' => [
53            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info',
54            'iconIdentifier' => 'actions-document-info',
55            'callbackAction' => 'openInfoPopUp'
56        ],
57        'divider1' => [
58            'type' => 'divider'
59        ],
60        'copy' => [
61            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
62            'iconIdentifier' => 'actions-edit-copy',
63            'callbackAction' => 'copy'
64        ],
65        'copyRelease' => [
66            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
67            'iconIdentifier' => 'actions-edit-copy-release',
68            'callbackAction' => 'clipboardRelease'
69        ],
70        'cut' => [
71            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut',
72            'iconIdentifier' => 'actions-edit-cut',
73            'callbackAction' => 'cut'
74        ],
75        'cutRelease' => [
76            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease',
77            'iconIdentifier' => 'actions-edit-cut-release',
78            'callbackAction' => 'clipboardRelease'
79        ],
80        'pasteAfter' => [
81            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter',
82            'iconIdentifier' => 'actions-document-paste-after',
83            'callbackAction' => 'pasteAfter'
84        ],
85        'pasteInto' => [
86            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteinto',
87            'iconIdentifier' => 'actions-document-paste-into',
88            'callbackAction' => 'pasteInto'
89        ],
90        'divider2' => [
91            'type' => 'divider'
92        ],
93        'more' => [
94            'type' => 'submenu',
95            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more',
96            'iconIdentifier' => '',
97            'callbackAction' => 'openSubmenu',
98            'childItems' => [
99                'newWizard' => [
100                    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_newWizard',
101                    'iconIdentifier' => 'actions-page-new',
102                    'callbackAction' => 'newPageWizard',
103                ],
104                'pagesSort' => [
105                    'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_sort.xlf:title',
106                    'iconIdentifier' => 'actions-page-move',
107                    'callbackAction' => 'pagesSort',
108                ],
109                'pagesNewMultiple' => [
110                    'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_new.xlf:title',
111                    'iconIdentifier' => 'apps-pagetree-drag-move-between',
112                    'callbackAction' => 'pagesNewMultiple',
113                ],
114                'openListModule' => [
115                    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_db_list',
116                    'iconIdentifier' => 'actions-system-list-open',
117                    'callbackAction' => 'openListModule',
118                ],
119                'mountAsTreeRoot' => [
120                    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint',
121                    'iconIdentifier' => 'actions-pagetree-mountroot',
122                    'callbackAction' => 'mountAsTreeRoot',
123                ],
124            ],
125        ],
126        'divider3' => [
127            'type' => 'divider'
128        ],
129        'enable' => [
130            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable',
131            'iconIdentifier' => 'actions-edit-unhide',
132            'callbackAction' => 'enableRecord',
133        ],
134        'disable' => [
135            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable',
136            'iconIdentifier' => 'actions-edit-hide',
137            'callbackAction' => 'disableRecord',
138        ],
139        'delete' => [
140            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete',
141            'iconIdentifier' => 'actions-edit-delete',
142            'callbackAction' => 'deleteRecord',
143        ],
144        'history' => [
145            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history',
146            'iconIdentifier' => 'actions-document-history-open',
147            'callbackAction' => 'openHistoryPopUp',
148        ],
149        'clearCache' => [
150            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache',
151            'iconIdentifier' => 'actions-system-cache-clear',
152            'callbackAction' => 'clearCache',
153        ],
154    ];
155
156    /**
157     * @var bool
158     */
159    protected $languageAccess = false;
160
161    /**
162     * Checks if the provider can add items to the menu
163     *
164     * @return bool
165     */
166    public function canHandle(): bool
167    {
168        return $this->table === 'pages';
169    }
170
171    /**
172     * @return int
173     */
174    public function getPriority(): int
175    {
176        return 100;
177    }
178
179    /**
180     * @param string $itemName
181     * @param string $type
182     * @return bool
183     */
184    protected function canRender(string $itemName, string $type): bool
185    {
186        if (in_array($type, ['divider', 'submenu'], true)) {
187            return true;
188        }
189        if (in_array($itemName, $this->disabledItems, true)) {
190            return false;
191        }
192        $canRender = false;
193        switch ($itemName) {
194            case 'view':
195                $canRender = $this->canBeViewed();
196                break;
197            case 'edit':
198                $canRender = $this->canBeEdited();
199                break;
200            case 'new':
201            case 'newWizard':
202            case 'pagesNewMultiple':
203                $canRender = $this->canBeCreated();
204                break;
205            case 'info':
206                $canRender = $this->canShowInfo();
207                break;
208            case 'enable':
209                $canRender = $this->canBeEnabled();
210                break;
211            case 'disable':
212                $canRender = $this->canBeDisabled();
213                break;
214            case 'delete':
215                $canRender = $this->canBeDeleted();
216                break;
217            case 'history':
218                $canRender = $this->canShowHistory();
219                break;
220            case 'openListModule':
221                $canRender = $this->canOpenListModule();
222                break;
223            case 'pagesSort':
224                $canRender = $this->canBeSorted();
225                break;
226            case 'mountAsTreeRoot':
227                $canRender = !$this->isRoot();
228                break;
229            case 'copy':
230                $canRender = $this->canBeCopied();
231                break;
232            case 'copyRelease':
233                $canRender = $this->isRecordInClipboard('copy');
234                break;
235            case 'cut':
236                $canRender = $this->canBeCut() && !$this->isRecordInClipboard('cut');
237                break;
238            case 'cutRelease':
239                $canRender = $this->isRecordInClipboard('cut');
240                break;
241            case 'pasteAfter':
242                $canRender = $this->canBePastedAfter();
243                break;
244            case 'pasteInto':
245                $canRender = $this->canBePastedInto();
246                break;
247            case 'clearCache':
248                $canRender = $this->canClearCache();
249                break;
250        }
251        return $canRender;
252    }
253
254    /**
255     * Saves calculated permissions for a page to speed things up
256     */
257    protected function initPermissions()
258    {
259        $this->pagePermissions = $this->backendUser->calcPerms($this->record);
260        $this->languageAccess = $this->hasLanguageAccess();
261    }
262
263    /**
264     * Checks if the user may create pages below the given page
265     *
266     * @return bool
267     */
268    protected function canBeCreated(): bool
269    {
270        if (!$this->backendUser->checkLanguageAccess(0)) {
271            return false;
272        }
273        return $this->hasPagePermission(Permission::PAGE_NEW);
274    }
275
276    /**
277     * Checks if the user has editing rights
278     *
279     * @return bool
280     */
281    protected function canBeEdited(): bool
282    {
283        if (!$this->languageAccess) {
284            return false;
285        }
286        if ($this->isRoot()) {
287            return false;
288        }
289        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) {
290            return false;
291        }
292        if ($this->backendUser->isAdmin()) {
293            return true;
294        }
295        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) {
296            return false;
297        }
298        return !$this->isRecordLocked() && $this->hasPagePermission(Permission::PAGE_EDIT);
299    }
300
301    /**
302     * Check if a page is locked
303     *
304     * @return bool
305     */
306    protected function isRecordLocked(): bool
307    {
308        return (bool)$this->record['editlock'];
309    }
310
311    /**
312     * Checks if the page is allowed to can be cut
313     *
314     * @return bool
315     */
316    protected function canBeCut(): bool
317    {
318        if (!$this->languageAccess) {
319            return false;
320        }
321        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['languageField'])
322            && !in_array($this->record[$GLOBALS['TCA'][$this->table]['ctrl']['languageField']], [0, -1])
323        ) {
324            return false;
325        }
326        return !$this->isWebMount()
327            && $this->canBeEdited()
328            && !$this->isDeletePlaceholder();
329    }
330
331    /**
332     * Checks if the page is allowed to be copied
333     *
334     * @return bool
335     */
336    protected function canBeCopied(): bool
337    {
338        if (!$this->languageAccess) {
339            return false;
340        }
341        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['languageField'])
342            && !in_array($this->record[$GLOBALS['TCA'][$this->table]['ctrl']['languageField']], [0, -1])
343        ) {
344            return false;
345        }
346        return !$this->isRoot()
347            && !$this->isWebMount()
348            && !$this->isRecordInClipboard('copy')
349            && $this->hasPagePermission(Permission::PAGE_SHOW)
350            && !$this->isDeletePlaceholder();
351    }
352
353    /**
354     * Checks if something can be pasted into the node
355     *
356     * @return bool
357     */
358    protected function canBePastedInto(): bool
359    {
360        if (!$this->languageAccess) {
361            return false;
362        }
363        $clipboardElementCount = count($this->clipboard->elFromTable($this->table));
364
365        return $clipboardElementCount
366            && $this->canBeCreated()
367            && !$this->isDeletePlaceholder();
368    }
369
370    /**
371     * Checks if something can be pasted after the node
372     *
373     * @return bool
374     */
375    protected function canBePastedAfter(): bool
376    {
377        if (!$this->languageAccess) {
378            return false;
379        }
380        $clipboardElementCount = count($this->clipboard->elFromTable($this->table));
381        return $clipboardElementCount
382            && $this->canBeCreated()
383            && !$this->isDeletePlaceholder();
384    }
385
386    /**
387     * Check if sub pages of given page can be sorted
388     *
389     * @return bool
390     */
391    protected function canBeSorted(): bool
392    {
393        if (!$this->languageAccess) {
394            return false;
395        }
396        return $this->backendUser->check('tables_modify', $this->table)
397            && $this->hasPagePermission(Permission::CONTENT_EDIT)
398            && !$this->isDeletePlaceholder()
399            && $this->backendUser->workspace === 0;
400    }
401
402    /**
403     * Checks if the page is allowed to be removed
404     *
405     * @return bool
406     */
407    protected function canBeDeleted(): bool
408    {
409        if (!$this->languageAccess) {
410            return false;
411        }
412        return !$this->isDeletePlaceholder()
413            && !$this->isRecordLocked()
414            && !$this->isDeletionDisabledInTS()
415            && $this->hasPagePermission(Permission::PAGE_DELETE);
416    }
417
418    /**
419     * Checks if the page is allowed to be viewed in frontend
420     *
421     * @return bool
422     */
423    protected function canBeViewed(): bool
424    {
425        return !$this->isRoot() && !$this->isDeleted();
426    }
427
428    /**
429     * Checks if the page is allowed to show info
430     *
431     * @return bool
432     */
433    protected function canShowInfo(): bool
434    {
435        return !$this->isRoot();
436    }
437
438    /**
439     * Checks if the user has clear cache rights
440     *
441     * @return bool
442     */
443    protected function canClearCache(): bool
444    {
445        return !$this->isRoot()
446            && ($this->backendUser->isAdmin() || $this->backendUser->getTSConfig()['options.']['clearCache.']['pages'] ?? false);
447    }
448
449    /**
450     * Determines whether this node is deleted.
451     *
452     * @return bool
453     */
454    protected function isDeleted(): bool
455    {
456        return !empty($this->record['deleted']) || $this->isDeletePlaceholder();
457    }
458
459    /**
460     * Returns true if current record is a root page
461     *
462     * @return bool
463     */
464    protected function isRoot()
465    {
466        return (int)$this->identifier === 0;
467    }
468
469    /**
470     * Returns true if current record is a web mount
471     *
472     * @return bool
473     */
474    protected function isWebMount()
475    {
476        return in_array($this->identifier, $this->backendUser->returnWebmounts());
477    }
478
479    /**
480     * @param string $itemName
481     * @return array
482     */
483    protected function getAdditionalAttributes(string $itemName): array
484    {
485        $attributes = [];
486        if ($itemName === 'view') {
487            $attributes += $this->getViewAdditionalAttributes();
488        }
489        if ($itemName === 'enable' || $itemName === 'disable') {
490            $attributes += $this->getEnableDisableAdditionalAttributes();
491        }
492        if ($itemName === 'delete') {
493            $attributes += $this->getDeleteAdditionalAttributes();
494        }
495        if ($itemName === 'pasteInto') {
496            $attributes += $this->getPasteAdditionalAttributes('into');
497        }
498        if ($itemName === 'pasteAfter') {
499            $attributes += $this->getPasteAdditionalAttributes('after');
500        }
501        if ($itemName === 'pagesSort') {
502            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
503            $attributes += [
504                'data-pages-sort-url' => (string)$uriBuilder->buildUriFromRoute('pages_sort', ['id' => $this->record['uid']]),
505            ];
506        }
507        if ($itemName === 'pagesNewMultiple') {
508            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
509            $attributes += [
510                'data-pages-new-multiple-url' => (string)$uriBuilder->buildUriFromRoute('pages_new', ['id' => $this->record['uid']]),
511            ];
512        }
513        if ($itemName === 'edit') {
514            $attributes = [
515                'data-pages-language-uid' => $this->record['sys_language_uid']
516            ];
517        }
518        return $attributes;
519    }
520
521    /**
522     * @return int
523     */
524    protected function getPreviewPid(): int
525    {
526        return (int)$this->record['sys_language_uid'] === 0 ? (int)$this->record['uid'] : (int)$this->record['l10n_parent'];
527    }
528
529    /**
530     * Returns the view link
531     *
532     * @return string
533     */
534    protected function getViewLink(): string
535    {
536        $language = (int)$this->record['sys_language_uid'];
537        $additionalParams = ($language > 0) ? '&L=' . $language : '';
538        return BackendUtility::getPreviewUrl(
539            $this->getPreviewPid(),
540            '',
541            null,
542            '',
543            '',
544            $additionalParams
545        );
546    }
547
548    /**
549     * Returns true if a current user has access to the language of the record
550     *
551     * @see BackendUserAuthentication::checkLanguageAccess()
552     * @return bool
553     */
554    protected function hasLanguageAccess(): bool
555    {
556        if ($this->backendUser->isAdmin()) {
557            return true;
558        }
559        $languageField = $GLOBALS['TCA'][$this->table]['ctrl']['languageField'] ?? '';
560        if ($languageField !== '' && isset($this->record[$languageField])) {
561            return $this->backendUser->checkLanguageAccess((int)$this->record[$languageField]);
562        }
563        return true;
564    }
565}
566