<?php

/*
 * 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\Form\FormDataProvider;

use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedEditInternalsException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedListenerException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedPageEditException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedPageNewException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedRootNodeException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedTableModifyException;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Type\Bitmask\Permission;

/**
 * Determine user permission for action and check them
 */
readonly class DatabaseUserPermissionCheck implements FormDataProviderInterface
{
    public function __construct(
        private EventDispatcherInterface $eventDispatcher,
    ) {}

    /**
     * Set userPermissionOnPage to result array and check access rights.
     *
     * A couple of different exceptions are thrown here:
     * * If something weird happens a top level SPL exception is thrown.
     *   This indicates a non-recoverable error.
     * * If user has no access to whatever should be done, an exception that
     *   extends from Form\Exception\AccessDeniedException is thrown. This
     *   can be caught by upper level controller code and can be translated
     *   to a specific error message that is shown to the user.
     *
     * @throws AccessDeniedException
     */
    public function addData(array $result): array
    {
        $backendUser = $this->getBackendUser();

        // Early return for admins
        if ($backendUser->isAdmin()) {
            $result['userPermissionOnPage'] = Permission::ALL;
            return $result;
        }

        if (!$backendUser->check('tables_modify', $result['tableName'])) {
            // If user has no modify rights on table, processing is stopped by throwing an
            // exception immediately. This case can not be circumvented by hooks.
            throw new AccessDeniedTableModifyException(
                'No table modify permission for user ' . $backendUser->user['uid'] . ' on table ' . $result['tableName'],
                1437683248
            );
        }

        $exception = null;
        $userPermissionOnPage = new Permission(Permission::NOTHING);
        $rootLevelCapability = $result['tcaSchemata']->get($result['tableName'])->getCapability(TcaSchemaCapability::RestrictionRootLevel);

        if ($result['command'] === 'new') {
            // A new record is created. Access rights of parent record are important here
            // @todo: In case of new inline child, parentPageRow should probably be the
            // @todo: "inlineFirstPid" page - Maybe effectivePid and parentPageRow should be calculated differently then?
            if (is_array($result['parentPageRow'])) {
                // Record is added below an existing page
                $userPermissionOnPage = new Permission($backendUser->calcPerms($result['parentPageRow']));
                if ($result['tableName'] === 'pages') {
                    // New page is created, user needs PAGE_NEW for this
                    if (!$userPermissionOnPage->createPagePermissionIsGranted()) {
                        $exception = new AccessDeniedPageNewException(
                            'No page new permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['databaseRow']['uid'],
                            1437745640
                        );
                    }
                } elseif (!$userPermissionOnPage->editContentPermissionIsGranted()) {
                    // A regular record is added, not a page. User needs CONTENT_EDIT permission
                    $exception = new AccessDeniedContentEditException(
                        'No content new permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['parentPageRow']['uid'],
                        1437745759
                    );
                }
            } elseif ($rootLevelCapability->shallIgnoreRootLevelRestriction()) {
                // Non admin is creating a record on root node for a table that is actively allowed
                $userPermissionOnPage->set(Permission::ALL);
            } else {
                // Non admin has no create permission on root node records
                $exception = new AccessDeniedRootNodeException(
                    'No record creation permission for user ' . $backendUser->user['uid'] . ' on page root node',
                    1437745221
                );
            }
        } else {
            // A page or a record on a page is edited
            if ($result['tableName'] === 'pages') {
                // A page record is edited, check edit rights of this record directly
                $userPermissionOnPage = new Permission($backendUser->calcPerms($result['defaultLanguagePageRow'] ?? $result['databaseRow']));
                if (!$userPermissionOnPage->editPagePermissionIsGranted()
                    || !$backendUser->check('pagetypes_select', $result['databaseRow'][$result['processedTca']['ctrl']['type']])
                ) {
                    $exception = new AccessDeniedPageEditException(
                        'No page edit permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['databaseRow']['uid'],
                        1437679336
                    );
                }
            } elseif (isset($result['parentPageRow']) && is_array($result['parentPageRow'])) {
                // A non page record is edited.
                // If there is a parent page row, check content edit right of user
                $userPermissionOnPage = new Permission($backendUser->calcPerms($result['parentPageRow']));
                if (!$userPermissionOnPage->editContentPermissionIsGranted()) {
                    $exception = new AccessDeniedContentEditException(
                        'No content edit permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['parentPageRow']['uid'],
                        1437679657
                    );
                }
            } elseif ($rootLevelCapability->shallIgnoreRootLevelRestriction()) {
                // Non admin is editing a record on root node for a table that is actively allowed
                $userPermissionOnPage->set(Permission::ALL);
            } else {
                // Non admin has no edit permission on root node records
                // @todo: This probably needs further handling, see http://review.typo3.org/40835
                $exception = new AccessDeniedRootNodeException(
                    'No content edit permission for user ' . $backendUser->user['uid'] . ' on page root node',
                    1437679856
                );
            }
            // If general access is allowed, check record edit access
            if ($exception === null
                && !($result['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)
            ) {
                $accessResult = $backendUser->checkRecordEditAccess($result['tableName'], $result['databaseRow']);
                if (!$accessResult->isAllowed) {
                    $exception = new AccessDeniedEditInternalsException($accessResult->errorMessage, 1437687404);
                }
            }
        }

        $userHasAccess = $this->eventDispatcher->dispatch(
            new ModifyEditFormUserAccessEvent(
                $exception,
                $result['tableName'],
                $result['command'],
                $result['databaseRow'],
            )
        )->doesUserHaveAccess();

        // Throw specific exception because a listener to the Event denied the previous positive user access decision
        if ($exception === null && !$userHasAccess) {
            $exception = new AccessDeniedListenerException(
                'Access to table ' . $result['tableName'] . ' for user ' . $backendUser->user['uid'] . ' was denied by a ModifyRecordEditUserAccessEvent listener',
                1662727149
            );
        }

        // Unset a previous exception because a listener to the Event allowed the previous negative user access decision
        if ($exception !== null && $userHasAccess) {
            $exception = null;
        }

        if ($exception) {
            throw $exception;
        }

        $result['userPermissionOnPage'] = $userPermissionOnPage->__toInt();

        return $result;
    }

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