<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;

use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Context menu item provider for pages table
 */
class PageProvider extends RecordProvider
{
    /**
     * @var string
     */
    protected $table = 'pages';

    /**
     * @var array
     */
    protected $itemsConfiguration = [
        'view' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view',
            'iconIdentifier' => 'actions-view-page',
            'callbackAction' => 'viewRecord',
        ],
        'edit' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit',
            'iconIdentifier' => 'actions-page-open',
            'callbackAction' => 'editRecord',
        ],
        'new' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.newSubpage',
            'iconIdentifier' => 'actions-page-new',
            'callbackAction' => 'newRecord',
        ],
        'info' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info',
            'iconIdentifier' => 'actions-document-info',
            'callbackAction' => 'openInfoPopUp',
        ],
        'qrcode' => [
            'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:showPageQrCode',
            'iconIdentifier' => 'actions-qrcode',
            'callbackAction' => 'showQrCode',
        ],
        'divider1' => [
            'type' => 'divider',
        ],
        'copy' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
            'iconIdentifier' => 'actions-edit-copy',
            'callbackAction' => 'copy',
        ],
        'copyRelease' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
            'iconIdentifier' => 'actions-edit-copy-release',
            'callbackAction' => 'clipboardRelease',
        ],
        'cut' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut',
            'iconIdentifier' => 'actions-edit-cut',
            'callbackAction' => 'cut',
        ],
        'cutRelease' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease',
            'iconIdentifier' => 'actions-edit-cut-release',
            'callbackAction' => 'clipboardRelease',
        ],
        'pasteAfter' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter',
            'iconIdentifier' => 'actions-document-paste-after',
            'callbackAction' => 'pasteAfter',
        ],
        'pasteInto' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteinto',
            'iconIdentifier' => 'actions-document-paste-into',
            'callbackAction' => 'pasteInto',
        ],
        'divider2' => [
            'type' => 'divider',
        ],
        'more' => [
            'type' => 'submenu',
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more',
            'iconIdentifier' => '',
            'callbackAction' => 'openSubmenu',
            'childItems' => [
                'pagesSort' => [
                    'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_sort.xlf:title',
                    'iconIdentifier' => 'actions-page-move',
                    'callbackAction' => 'pagesSort',
                ],
                'pagesNewMultiple' => [
                    'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_new.xlf:title',
                    'iconIdentifier' => 'apps-pagetree-drag-move-between',
                    'callbackAction' => 'pagesNewMultiple',
                ],
                'mountAsTreeRoot' => [
                    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint',
                    'iconIdentifier' => 'actions-pagetree-mountroot',
                    'callbackAction' => 'mountAsTreeRoot',
                ],
                'showInMenus' => [
                    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_showInMenus',
                    'iconIdentifier' => 'actions-view',
                    'callbackAction' => 'showInMenus',
                ],
                'hideInMenus' => [
                    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_hideInMenus',
                    'iconIdentifier' => 'actions-ban',
                    'callbackAction' => 'hideInMenus',
                ],
            ],
        ],
        'divider3' => [
            'type' => 'divider',
        ],
        'enable' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable',
            'iconIdentifier' => 'actions-edit-unhide',
            'callbackAction' => 'enableRecord',
        ],
        'disable' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable',
            'iconIdentifier' => 'actions-edit-hide',
            'callbackAction' => 'disableRecord',
        ],
        'delete' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete',
            'iconIdentifier' => 'actions-edit-delete',
            'callbackAction' => 'deleteRecord',
        ],
        'history' => [
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history',
            'iconIdentifier' => 'actions-document-history-open',
            'callbackAction' => 'openHistoryPopUp',
        ],
        'clearCache' => [
            'label' => 'core.cache:page.label',
            'iconIdentifier' => 'actions-system-cache-clear',
            'callbackAction' => 'clearCache',
        ],
    ];

    protected bool $languageAccess = false;

    /**
     * Checks if the provider can add items to the menu
     */
    public function canHandle(): bool
    {
        return $this->table === 'pages';
    }

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

    protected function canRender(string $itemName, string $type): bool
    {
        if (in_array($type, ['divider', 'submenu'], true)) {
            return true;
        }
        if (in_array($itemName, $this->disabledItems, true)) {
            return false;
        }
        $canRender = false;
        switch ($itemName) {
            case 'view':
            case 'qrcode':
                $canRender = $this->canBeViewed();
                break;
            case 'edit':
                $canRender = $this->canBeEdited();
                break;
            case 'new':
            case 'pagesNewMultiple':
                $canRender = $this->canBeCreated();
                break;
            case 'info':
                $canRender = $this->canShowInfo();
                break;
            case 'enable':
                $canRender = $this->canBeEnabled();
                break;
            case 'disable':
                $canRender = $this->canBeDisabled();
                break;
            case 'showInMenus':
                $canRender = $this->canBeToggled('nav_hide', 1);
                break;
            case 'hideInMenus':
                $canRender = $this->canBeToggled('nav_hide', 0);
                break;
            case 'delete':
                $canRender = $this->canBeDeleted();
                break;
            case 'history':
                $canRender = $this->canShowHistory();
                break;
            case 'pagesSort':
                $canRender = $this->canBeSorted();
                break;
            case 'mountAsTreeRoot':
                $canRender = !$this->isRoot();
                break;
            case 'copy':
                $canRender = $this->canBeCopied();
                break;
            case 'copyRelease':
                $canRender = $this->isRecordInClipboard('copy');
                break;
            case 'cut':
                $canRender = $this->canBeCut() && !$this->isRecordInClipboard('cut');
                break;
            case 'cutRelease':
                $canRender = $this->isRecordInClipboard('cut');
                break;
            case 'pasteAfter':
                $canRender = $this->canBePastedAfter();
                break;
            case 'pasteInto':
                $canRender = $this->canBePastedInto();
                break;
            case 'clearCache':
                $canRender = $this->canClearCache();
                break;
        }
        return $canRender;
    }

    /**
     * Saves calculated permissions for a page to speed things up
     */
    protected function initPermissions(): void
    {
        $this->pagePermissions = new Permission($this->backendUser->calcPerms($this->record));
        $this->languageAccess = $this->hasLanguageAccess();
    }

    /**
     * Checks if the user may create pages below the given page
     */
    protected function canBeCreated(): bool
    {
        if (!$this->backendUser->checkLanguageAccess(0)) {
            return false;
        }
        if ($this->getLanguageField() !== ''
            && !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1])
        ) {
            return false;
        }
        if (!$this->backendUser->check('tables_modify', $this->table)) {
            return false;
        }
        return $this->hasPagePermission(Permission::PAGE_NEW);
    }

    /**
     * Checks if the user has editing rights
     */
    protected function canBeEdited(): bool
    {
        if (!$this->languageAccess) {
            return false;
        }
        if ($this->isRoot()) {
            return false;
        }
        if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessReadOnly)) {
            return false;
        }
        if ($this->backendUser->isAdmin()) {
            return true;
        }
        if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessAdminOnly)) {
            return false;
        }
        if (!$this->backendUser->check('tables_modify', $this->table)) {
            return false;
        }
        return !$this->isRecordLocked() && $this->hasPagePermission(Permission::PAGE_EDIT);
    }

    /**
     * Check if a page is locked
     */
    protected function isRecordLocked(): bool
    {
        return (bool)$this->record[$this->getSchema()->getCapability(TcaSchemaCapability::EditLock)->getFieldName()];
    }

    /**
     * Checks if the page is allowed to can be cut
     */
    protected function canBeCut(): bool
    {
        if (!$this->languageAccess) {
            return false;
        }
        if ($this->getLanguageField() !== ''
            && !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1])
        ) {
            return false;
        }
        if (!$this->backendUser->check('tables_modify', $this->table)) {
            return false;
        }
        return !$this->isWebMount()
            && $this->canBeEdited()
            && !$this->isDeletePlaceholder();
    }

    /**
     * Checks if the page is allowed to be copied
     */
    protected function canBeCopied(): bool
    {
        if (!$this->languageAccess) {
            return false;
        }
        if ($this->getLanguageField() !== ''
            && !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1])
        ) {
            return false;
        }
        if (!$this->backendUser->check('tables_select', $this->table)) {
            return false;
        }
        return !$this->isRoot()
            && !$this->isWebMount()
            && !$this->isRecordInClipboard('copy')
            && $this->hasPagePermission(Permission::PAGE_SHOW)
            && !$this->isDeletePlaceholder();
    }

    /**
     * Checks if something can be pasted into the node
     */
    protected function canBePastedInto(): bool
    {
        if (!$this->languageAccess) {
            return false;
        }
        $clipboardElementCount = count($this->clipboard->elFromTable($this->table));

        return $clipboardElementCount
            && $this->canBeCreated()
            && !$this->isDeletePlaceholder();
    }

    /**
     * Checks if something can be pasted after the node
     */
    protected function canBePastedAfter(): bool
    {
        if (!$this->languageAccess) {
            return false;
        }
        $clipboardElementCount = count($this->clipboard->elFromTable($this->table));
        return $clipboardElementCount
            && $this->canBeCreated()
            && !$this->isDeletePlaceholder();
    }

    /**
     * Check if sub pages of given page can be sorted
     */
    protected function canBeSorted(): bool
    {
        if (!$this->languageAccess) {
            return false;
        }
        return $this->backendUser->check('tables_modify', $this->table)
            && $this->hasPagePermission(Permission::CONTENT_EDIT)
            && !$this->isDeletePlaceholder()
            && $this->backendUser->workspace === 0;
    }

    /**
     * Checks if the page is allowed to be removed
     */
    protected function canBeDeleted(): bool
    {
        if (!$this->languageAccess) {
            return false;
        }
        return !$this->isRoot()
            && !$this->isDeletePlaceholder()
            && !$this->isRecordLocked()
            && !$this->isDeletionDisabledInTS()
            && $this->hasPagePermission(Permission::PAGE_DELETE);
    }

    /**
     * Checks if the page is allowed to be viewed in frontend
     */
    protected function canBeViewed(): bool
    {
        return !$this->isRoot()
            && !$this->isDeleted()
            && $this->previewLinkCanBeBuild();
    }

    /**
     * Checks if the page is allowed to show info
     */
    protected function canShowInfo(): bool
    {
        return !$this->isRoot();
    }

    /**
     * Checks if the user has clear cache rights
     */
    protected function canClearCache(): bool
    {
        return !$this->isRoot()
            && ($this->backendUser->isAdmin() || ($this->backendUser->getTSConfig()['options.']['clearCache.']['pages'] ?? false));
    }

    /**
     * Determines whether this node is deleted.
     */
    protected function isDeleted(): bool
    {
        return !empty($this->record['deleted']) || $this->isDeletePlaceholder();
    }

    /**
     * Returns true if current record is a root page
     */
    protected function isRoot(): bool
    {
        return (int)$this->identifier === 0;
    }

    /**
     * Returns true if current record is a web mount
     */
    protected function isWebMount(): bool
    {
        return in_array($this->identifier, $this->backendUser->getWebmounts());
    }

    protected function getAdditionalAttributes(string $itemName): array
    {
        $attributes = [];
        if ($itemName === 'view' || $itemName === 'qrcode') {
            $attributes += $this->getViewAdditionalAttributes();
        }
        if ($itemName === 'enable' || $itemName === 'disable') {
            $attributes += $this->getEnableDisableAdditionalAttributes();
        }
        if ($itemName === 'delete') {
            $attributes += $this->getDeleteAdditionalAttributes();
        }
        if ($itemName === 'pasteInto') {
            $attributes += $this->getPasteAdditionalAttributes('into');
        }
        if ($itemName === 'pasteAfter') {
            $attributes += $this->getPasteAdditionalAttributes('after');
        }
        if ($itemName === 'pagesSort') {
            $attributes += [
                'data-pages-sort-url' => (string)$this->uriBuilder->buildUriFromRoute('pages_sort', ['id' => $this->record['uid'] ?? null]),
            ];
        }
        if ($itemName === 'pagesNewMultiple') {
            $attributes += [
                'data-pages-new-multiple-url' => (string)$this->uriBuilder->buildUriFromRoute('pages_new', ['id' => $this->record['uid'] ?? 0]),
            ];
        }

        if ($itemName === 'edit') {
            $attributes = [
                'data-pages-language-uid' => $this->record[$this->getLanguageField()] ?? null,
            ];
        }
        return $attributes;
    }

    protected function getPreviewPid(): int
    {
        return (int)($this->record[$this->getLanguageField()] ?? 0) === 0 ? (int)$this->record['uid'] : (int)$this->record['l10n_parent'];
    }

    /**
     * Returns the view link
     */
    protected function getViewLink(): string
    {
        return (string)PreviewUriBuilder::create($this->record)->buildUri();
    }

    /**
     * Checks if user has access to this column, the doktype
     * is not excluded and that it contains the given value.
     */
    protected function canBeToggled(string $fieldName, int $value): bool
    {
        if (!$this->languageAccess || $this->isRoot()) {
            return false;
        }
        $field = $this->getSchema()->getField($fieldName);
        if ($field->supportsAccessControl()
            && !$this->isExcludedDoktype()
            && $this->backendUser->check('non_exclude_fields', $this->table . ':' . $fieldName)
            && $this->backendUser->check('tables_modify', $this->table)
        ) {
            return (int)$this->record[$fieldName] === $value;
        }
        return false;
    }

    /**
     * Returns true if a current user has access to the language of the record
     *
     * @see BackendUserAuthentication::checkLanguageAccess()
     */
    protected function hasLanguageAccess(): bool
    {
        if ($this->backendUser->isAdmin()) {
            return true;
        }
        if (($languageField = $this->getLanguageField()) !== '' && isset($this->record[$languageField])) {
            return $this->backendUser->checkLanguageAccess((int)$this->record[$languageField]);
        }
        return true;
    }

    /**
     * Returns true if the page doktype is excluded
     */
    protected function isExcludedDoktype(): bool
    {
        $doktypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class);
        return !$doktypeRegistry->isPageTypeViewable((int)($this->record['doktype'] ?? 0));
    }
}
