<?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\Controller;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Event\AddUserSettingsJavaScriptModulesEvent;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\UserSettingsDataGroup;
use TYPO3\CMS\Backend\Form\FormResultFactory;
use TYPO3\CMS\Backend\Form\FormResultHandler;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Module\ModuleProvider;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\Enum\ModuleLayout;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry;
use TYPO3\CMS\Core\Authentication\UserSettingsSchema;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Localization\DateFormatter;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Schema\TcaSchemaBuilder;
use TYPO3\CMS\Core\SysLog\Action\Setting as SystemLogSettingAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Script class for the User Settings module
 *
 * @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
 */
#[AsController]
class SetupModuleController
{
    protected const DISALLOWED_FIELD_NAMES = ['password', 'password2', 'email', 'realName', 'admin', 'avatar'];

    protected const PASSWORD_NOT_UPDATED = 0;
    protected const PASSWORD_UPDATED = 1;
    protected const PASSWORD_NOT_THE_SAME = 2;
    // @todo: Can this constant be removed?
    protected const PASSWORD_OLD_WRONG = 3;
    protected const PASSWORD_POLICY_FAILED = 4;

    protected array $overrideConf = [];
    protected bool $languageUpdate = false;
    protected array $persistentUpdate = [];
    protected bool $pagetreeNeedsRefresh = false;
    protected bool $colorSchemeChanged = false;
    protected bool $themeChanged = false;
    protected bool $backendTitleFormatChanged = false;
    protected bool $dateTimeFirstDayOfWeekChanged = false;

    protected array $tsFieldConf = [];
    protected int $passwordIsUpdated = self::PASSWORD_NOT_UPDATED;
    protected bool $passwordIsSubmitted = false;
    protected bool $setupIsUpdated = false;
    protected bool $settingsAreResetToDefault = false;

    protected PasswordPolicyValidator $passwordPolicyValidator;

    public function __construct(
        protected readonly Typo3Information $typo3Information,
        protected readonly EventDispatcherInterface $eventDispatcher,
        protected readonly MfaProviderRegistry $mfaProviderRegistry,
        protected readonly IconFactory $iconFactory,
        protected readonly PageRenderer $pageRenderer,
        protected readonly ModuleTemplateFactory $moduleTemplateFactory,
        protected readonly LanguageServiceFactory $languageServiceFactory,
        protected readonly ModuleProvider $moduleProvider,
        protected readonly UriBuilder $uriBuilder,
        protected readonly FormProtectionFactory $formProtectionFactory,
        protected readonly Locales $locales,
        protected readonly ComponentFactory $componentFactory,
        protected readonly DateFormatter $dateFormatter,
        protected readonly FormDataCompiler $formDataCompiler,
        protected readonly NodeFactory $nodeFactory,
        protected readonly FormResultFactory $formResultFactory,
        protected readonly FormResultHandler $formResultHandler,
        protected readonly UserSettingsSchema $userSettingsSchema,
        protected readonly TcaSchemaBuilder $tcaSchemaBuilder,
        protected readonly ResourceFactory $resourceFactory,
    ) {
        $passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default';

        $action = PasswordPolicyAction::UPDATE_USER_PASSWORD;
        if ($this->getBackendUser()->getOriginalUserIdWhenInSwitchUserMode()) {
            $action = PasswordPolicyAction::UPDATE_USER_PASSWORD_SWITCH_USER_MODE;
        }

        $this->passwordPolicyValidator = GeneralUtility::makeInstance(
            PasswordPolicyValidator::class,
            $action,
            is_string($passwordPolicy) ? $passwordPolicy : ''
        );
    }

    /**
     * Injects the request object, checks if data should be saved, and prepares a HTML page
     */
    public function mainAction(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->initialize($request);
        $this->storeIncomingData($request);
        if ($this->pagetreeNeedsRefresh || $this->settingsAreResetToDefault) {
            BackendUtility::setUpdateSignal('updatePageTree');
        }
        if ($this->colorSchemeChanged || $this->settingsAreResetToDefault) {
            BackendUtility::setUpdateSignal('updateColorScheme', $this->getBackendUser()->uc['colorScheme'] ?? 'auto');
        }
        if ($this->themeChanged || $this->settingsAreResetToDefault) {
            BackendUtility::setUpdateSignal('updateTheme', $this->getBackendUser()->uc['theme'] ?? 'modern');
        }
        if ($this->backendTitleFormatChanged || $this->settingsAreResetToDefault) {
            BackendUtility::setUpdateSignal('updateTitleFormat', $this->getBackendUser()->uc['backendTitleFormat'] ?? 'titleFirst');
        }
        if ($this->dateTimeFirstDayOfWeekChanged || $this->settingsAreResetToDefault) {
            BackendUtility::setUpdateSignal('updateDateTimeFirstDayOfWeek', $this->getBackendUser()->uc['dateTimeFirstDayOfWeek'] ?? '');
        }
        if ($this->languageUpdate) {
            $this->getLanguageService()->init($this->getBackendUser()->user['lang'] ?? 'en');
            $locale = $this->getLanguageService()->getLocale();
            if ($locale !== null) {
                $parameters = [
                    'language' => $locale->getLanguageCode(),
                ];
                BackendUtility::setUpdateSignal('updateBackendLanguage', $parameters);
            }
        }
        if ($this->persistentUpdate !== []) {
            foreach ($this->persistentUpdate as $params) {
                BackendUtility::setUpdateSignal('updatePersistent', $params);
            }
        }

        // Use FormEngine to render the user settings form
        $formData = $this->compileFormData($request, $this->userSettingsSchema->getTca());
        $formData['renderType'] = 'fullRecordContainer';
        $formResultArray = $this->nodeFactory->create($formData)->render();
        $formResult = $this->formResultFactory->create($formResultArray);
        $this->formResultHandler->addAssets($formResult);

        $formProtection = $this->formProtectionFactory->createFromRequest($request);
        $this->addFlashMessages($view);
        $view->addButtonToButtonBar($this->componentFactory->createSaveButton('SetupModuleController')->setName('data[save]'));
        $this->registerResetButtonToButtonBar($view);
        // Set shortcut context - reload button is added automatically
        $view->getDocHeaderComponent()->setShortcutContext(
            'user_setup',
            $this->getLanguageService()->translate('short_description', 'backend.modules.user_settings')
        );
        $view->assignMultiple([
            'typo3Info' => $this->typo3Information,
            'isLanguageUpdate' => $this->languageUpdate,
            'formEngineHtml' => $formResult->html,
            'formToken' => $formProtection->generateToken('BE user setup', 'edit'),
        ]);
        return $view->renderResponse('Setup/Main');
    }

    /**
     * Compile form data for FormEngine rendering.
     */
    protected function compileFormData(ServerRequestInterface $request, array $userSettingsTca): array
    {
        $backendUser = $this->getBackendUser();
        $formDataCompilerInput = [
            'request' => $request,
            'tableName' => 'be_users_settings',
            'vanillaUid' => (int)$backendUser->user['uid'],
            'command' => 'edit',
            'returnUrl' => '',
            'tcaSchemata' => $this->tcaSchemaBuilder->buildFromStructure($userSettingsTca),
            'fullTca' => $userSettingsTca,
        ];
        return $this->formDataCompiler->compile(
            $formDataCompilerInput,
            GeneralUtility::makeInstance(UserSettingsDataGroup::class)
        );
    }

    /**
     * Initializes the module for display of the settings form.
     */
    protected function initialize(ServerRequestInterface $request): ModuleTemplate
    {
        $languageService = $this->getLanguageService();
        $backendUser = $this->getBackendUser();
        $view = $this->moduleTemplateFactory->create($request);
        $this->pageRenderer->loadJavaScriptModule('@typo3/backend/modal.js');
        $this->pageRenderer->loadJavaScriptModule('@typo3/backend/form-engine.js');
        $this->pageRenderer->loadJavaScriptModule('@typo3/backend/setup-module.js');
        $this->processAdditionalJavaScriptModules($request);
        $this->pageRenderer->addInlineSetting('FormEngine', 'formName', 'editform');
        $this->pageRenderer->addInlineLanguageLabelArray([
            'FormEngine.remainingCharacters' => $languageService->translate('labels.remainingCharacters', 'core.core'),
        ]);
        $view->setTitle($languageService->translate('user_settings', 'backend.user_profile'));
        $view->setLayout(ModuleLayout::NORMAL);
        // Getting the 'override' values as set might be set in user TSconfig
        $this->overrideConf = $backendUser->getTSConfig()['setup.']['override.'] ?? [];
        // Getting the disabled fields might be set in user TSconfig (eg setup.fields.password.disabled=1)
        $this->tsFieldConf = $backendUser->getTSConfig()['setup.']['fields.'] ?? [];
        // if password is disabled, disable repeat of password too (password2)
        if ($this->tsFieldConf['password.']['disabled'] ?? false) {
            $this->tsFieldConf['password2.']['disabled'] = 1;
        }
        return $view;
    }

    protected function processAdditionalJavaScriptModules(ServerRequestInterface $request): void
    {
        $event = new AddUserSettingsJavaScriptModulesEvent($request);
        $event = $this->eventDispatcher->dispatch($event);
        foreach ($event->getJavaScriptModules() as $specifier) {
            $this->pageRenderer->loadJavaScriptModule($specifier);
        }
    }

    /**
     * If settings are submitted via POST, store them
     */
    protected function storeIncomingData(ServerRequestInterface $request): void
    {
        $postData = $request->getParsedBody();
        if (!is_array($postData) || empty($postData)) {
            return;
        }

        $formProtection = $this->formProtectionFactory->createFromRequest($request);
        // Separate submitted data into corresponding partitions
        $backendUserId = (int)$this->getBackendUser()->user['uid'];
        $beUsersSubmission = $this->extractPartitionData($postData['data']['be_users_settings'][$backendUserId] ?? [], 'be_users');
        $userSettingsSubmission = $this->extractPartitionData($postData['data']['be_users_settings'][$backendUserId] ?? [], 'user_settings');
        $columns = $this->userSettingsSchema->getColumns();
        $backendUser = $this->getBackendUser();
        $beUserId = (int)$backendUser->user['uid'];
        $storeRec = [];
        $doSaveData = false;
        $fieldList = $this->getFieldsFromShowItem();

        if ($beUsersSubmission !== []
            && $userSettingsSubmission !== []
            && $formProtection->validateToken((string)($postData['formToken'] ?? ''), 'BE user setup', 'edit')
        ) {
            // UC hashed before applying changes
            $save_before = md5(serialize($backendUser->uc));
            // PUT SETTINGS into the ->uc array:
            // Reload left frame when switching BE language
            if (isset($beUsersSubmission['lang']) && $beUsersSubmission['lang'] !== $backendUser->user['lang']) {
                $this->languageUpdate = true;
            }
            // Reload pagetree if the title length is changed
            if (isset($userSettingsSubmission['titleLen']) && $userSettingsSubmission['titleLen'] !== $backendUser->uc['titleLen']) {
                $this->pagetreeNeedsRefresh = true;
            }
            if (isset($userSettingsSubmission['colorScheme']) && $userSettingsSubmission['colorScheme'] !== ($backendUser->uc['colorScheme'] ?? null)) {
                $this->colorSchemeChanged = true;
            }
            if (isset($userSettingsSubmission['theme']) && $userSettingsSubmission['theme'] !== ($backendUser->uc['theme'] ?? null)) {
                $this->themeChanged = true;
            }
            if (isset($userSettingsSubmission['backendTitleFormat']) && $userSettingsSubmission['backendTitleFormat'] !== ($backendUser->uc['backendTitleFormat'] ?? null)) {
                $this->backendTitleFormatChanged = true;
            }
            if (isset($userSettingsSubmission['dateTimeFirstDayOfWeek']) && $userSettingsSubmission['dateTimeFirstDayOfWeek'] !== ($backendUser->uc['dateTimeFirstDayOfWeek'] ?? null)) {
                $this->dateTimeFirstDayOfWeekChanged = true;
                $this->persistentUpdate[] = [
                    'fieldName' => 'dateTimeFirstDayOfWeek',
                    'value' => $userSettingsSubmission['dateTimeFirstDayOfWeek'],
                ];
            }
            // Options which should trigger direct JS persistent update, because
            // their new state needs to be available in JS components right away.
            foreach ($this->userSettingsSchema->getPersistentUpdateFieldNames() as $fieldName) {
                $fieldValue = ((int)($userSettingsSubmission[$fieldName] ?? 0)) ? 1 : 0;
                if ($fieldValue !== ($backendUser->uc[$fieldName] ?? null)) {
                    $this->persistentUpdate[] = [
                        'fieldName' => $fieldName,
                        'value' => $fieldValue ? '1' : '0',
                    ];
                }
            }

            if ($postData['data']['setValuesToDefault'] ?? false) {
                // If every value should be default
                $backendUser->resetUC();
                $this->settingsAreResetToDefault = true;
            } elseif ($postData['data']['save'] ?? false) {
                foreach ($columns as $field => $config) {
                    if (!in_array($field, $fieldList, true)) {
                        continue;
                    }
                    // Skip any disallowed field name, not matter if it's in be_users or user_settings partition
                    if (in_array($field, self::DISALLOWED_FIELD_NAMES, true)) {
                        continue;
                    }
                    $isBeUsersField = ($config['table'] ?? '') === 'be_users';
                    $fieldType = $config['type'] ?? 'text';
                    if ($isBeUsersField) {
                        $submittedValue = $beUsersSubmission[$field] ?? null;
                        if (!isset($config['access']) || ($this->checkAccess($config) && ($backendUser->user[$field] !== $submittedValue))) {
                            if ($fieldType === 'check') {
                                $fieldValue = (int)($submittedValue ?? 0);
                            } else {
                                $fieldValue = $submittedValue;
                            }
                            $storeRec['be_users'][$beUserId][$field] = $fieldValue;
                            $backendUser->user[$field] = $fieldValue;
                        }
                    } else {
                        if ($fieldType === 'check') {
                            $backendUser->uc[$field] = (int)($userSettingsSubmission[$field] ?? 0);
                        } else {
                            $backendUser->uc[$field] = htmlspecialchars($userSettingsSubmission[$field] ?? '');
                        }
                    }
                }
                // Personal data for the users be_user-record (email, name, password...)
                // If email and name is changed, set it in the users record:
                $be_user_data = $beUsersSubmission;
                // Temporarily hold `password2` for the hook to be able to adjust the password
                $be_user_data['password2'] = $userSettingsSubmission['password2'] ?? '';
                // Possibility to modify the transmitted values. Useful to do transformations, like RSA password decryption
                foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/setup/mod/index.php']['modifyUserDataBeforeSave'] ?? [] as $function) {
                    $params = ['be_user_data' => &$be_user_data];
                    GeneralUtility::callUserFunction($function, $params, $this);
                }
                $this->passwordIsSubmitted = (string)($be_user_data['password'] ?? '') !== '';
                $passwordIsConfirmed = $this->passwordIsSubmitted && $be_user_data['password'] === $be_user_data['password2'];
                unset($be_user_data['password2']);

                // Validate password against password policy
                $contextData = new ContextData(
                    loginMode: 'BE',
                    currentPasswordHash: $this->getBackendUser()->user['password'],
                    newUserFullName: $be_user_data['realName'] ?? $this->getBackendUser()->user['realName']
                );
                $contextData->setData('currentUsername', $this->getBackendUser()->user['username']);
                $event = $this->eventDispatcher->dispatch(
                    new EnrichPasswordValidationContextDataEvent(
                        $contextData,
                        $be_user_data,
                        self::class
                    )
                );
                $contextData = $event->getContextData();

                $passwordValid = true;
                if ($passwordIsConfirmed
                    && !$this->passwordPolicyValidator->isValidPassword($be_user_data['password'], $contextData)
                ) {
                    $passwordValid = false;
                    $this->passwordIsUpdated = self::PASSWORD_POLICY_FAILED;
                }

                // Update the real name:
                if (isset($be_user_data['realName']) && $be_user_data['realName'] !== $backendUser->user['realName']) {
                    $backendUser->user['realName'] = ($storeRec['be_users'][$beUserId]['realName'] = substr($be_user_data['realName'], 0, 80));
                }
                // Update the email address:
                if (isset($be_user_data['email']) && $be_user_data['email'] !== $backendUser->user['email']) {
                    $backendUser->user['email'] = ($storeRec['be_users'][$beUserId]['email'] = substr($be_user_data['email'], 0, 255));
                }
                // Update the password:
                if ($this->passwordIsSubmitted) {
                    if ($passwordIsConfirmed && $passwordValid) {
                        $this->passwordIsUpdated = self::PASSWORD_UPDATED;
                        $storeRec['be_users'][$beUserId]['password'] = $be_user_data['password'];
                    } elseif ($passwordIsConfirmed) {
                        $this->passwordIsUpdated = self::PASSWORD_POLICY_FAILED;
                    } else {
                        $this->passwordIsUpdated = self::PASSWORD_NOT_THE_SAME;
                    }
                }

                $this->setAvatarFileUid($beUserId, $be_user_data['avatar'] ?? null, $storeRec);

                $doSaveData = true;
            }
            // Explicitly unset disallowed field names
            foreach (self::DISALLOWED_FIELD_NAMES as $disallowedFieldName) {
                unset($backendUser->uc[$disallowedFieldName]);
            }
            // Inserts the overriding values.
            $backendUser->overrideUC();
            $save_after = md5(serialize($backendUser->uc));
            // If something in the uc-array of the user has changed, we save the array...
            if ($save_before != $save_after) {
                $backendUser->writeUC();
                $backendUser->writelog(SystemLogType::SETTING, SystemLogSettingAction::CHANGE, SystemLogErrorClassification::MESSAGE, null, 'Personal settings changed', []);
                $this->setupIsUpdated = true;
            }
            // Persist data if something has changed:
            if (!empty($storeRec) && $doSaveData) {
                // Set user to admin to circumvent DataHandler restrictions.
                // Not using isAdmin() to fetch the original value, just in case it has been boolean casted.
                $savedUserAdminState = $backendUser->user['admin'];
                $backendUser->user['admin'] = true;
                // Make dedicated instance of TCE for storing the changes.
                $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
                $dataHandler->start($storeRec, [], $backendUser);
                $dataHandler->process_datamap();
                $dataHandler->printLogErrorMessages();
                // reset the user record admin flag to previous value, just in case it gets used any further.
                $backendUser->user['admin'] = $savedUserAdminState;
                if ($this->passwordIsUpdated === self::PASSWORD_NOT_UPDATED || count($storeRec['be_users'][$beUserId]) > 1) {
                    $this->setupIsUpdated = true;
                }
                BackendUtility::setUpdateSignal('updateTopbar');
            }
        }
    }

    /**
     * Returns access check (currently only "admin" is supported)
     *
     * @param array $config Configuration of the field, access mode is defined in key 'access'
     * @return bool Whether it is allowed to modify the given field
     */
    protected function checkAccess(array $config)
    {
        $access = $config['access'];
        if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['setup']['accessLevelCheck'][$access])) {
            if (class_exists($access)) {
                $accessObject = GeneralUtility::makeInstance($access);
                if (method_exists($accessObject, 'accessLevelCheck')) {
                    // Initialize vars. If method fails, $set will be set to FALSE
                    return $accessObject->accessLevelCheck($config);
                }
            }
        } elseif ($access === 'admin') {
            return $this->getBackendUser()->isAdmin();
        }

        return false;
    }

    /**
     * Returns array with fields defined in TCA user settings showitem.
     * Remove fields which are disabled by user TSconfig
     *
     * @return list<string> Array with field names visible in form
     */
    protected function getFieldsFromShowItem(): array
    {
        // just keep field names, filter out control sequences
        $tcaFieldNames = array_keys($this->userSettingsSchema->getColumns());
        $allowedFields = GeneralUtility::trimExplode(',', $this->userSettingsSchema->getRawShowitem(), true);
        $allowedFields = array_map($this->extractShowitemFieldName(...), $allowedFields);
        $allowedFields = array_filter($allowedFields, static fn(string $field): bool => in_array($field, $tcaFieldNames, true));
        $backendUser = $this->getBackendUser();
        if ($backendUser->getOriginalUserIdWhenInSwitchUserMode() && $backendUser->isSystemMaintainer(true)) {
            // DataHandler denies changing the password of system maintainer users in switch user mode.
            // Do not show the password fields is this case.
            $key = array_search('password', $allowedFields);
            if ($key !== false) {
                unset($allowedFields[$key]);
            }
            $key = array_search('password2', $allowedFields);
            if ($key !== false) {
                unset($allowedFields[$key]);
            }
        }

        foreach ($this->tsFieldConf as $fieldName => $userTsFieldConfig) {
            if (!empty($userTsFieldConfig['disabled'])) {
                $fieldName = rtrim($fieldName, '.');
                $key = array_search($fieldName, $allowedFields);
                if ($key !== false) {
                    unset($allowedFields[$key]);
                }
            }
        }
        return $allowedFields;
    }

    /**
     * Extracts `field_name` from a showitem field `field_name;field_label`.
     */
    protected function extractShowitemFieldName(string $fieldName): string
    {
        $offset = strpos($fieldName, ';');
        return $offset !== false ? substr($fieldName, 0, $offset) : $fieldName;
    }

    /**
     * Get Avatar fileUid
     */
    protected function getAvatarFileUid(int $beUserId): int
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference');
        $file = $queryBuilder
            ->select('uid_local')
            ->from('sys_file_reference')
            ->where(
                $queryBuilder->expr()->eq(
                    'tablenames',
                    $queryBuilder->createNamedParameter('be_users')
                ),
                $queryBuilder->expr()->eq(
                    'fieldname',
                    $queryBuilder->createNamedParameter('avatar')
                ),
                $queryBuilder->expr()->eq(
                    'uid_foreign',
                    $queryBuilder->createNamedParameter($beUserId, Connection::PARAM_INT)
                )
            )
            ->executeQuery()
            ->fetchOne();
        return (int)$file;
    }

    /**
     * Set avatar fileUid for backend user
     *
     * @param numeric-string|''|'delete'|null $fileUid either null, a file UID, an empty string, or `delete`
     */
    protected function setAvatarFileUid(int $beUserId, ?string $fileUid, array &$storeRec): void
    {
        // Update is only needed when new fileUid is set
        if ((int)$fileUid === $this->getAvatarFileUid($beUserId)) {
            return;
        }

        // If user is not allowed to modify avatar $fileUid is empty - so don't overwrite existing avatar
        if (empty($fileUid)) {
            return;
        }

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference');
        $queryBuilder->getRestrictions()->removeAll();
        $queryBuilder
            ->delete('sys_file_reference')
            ->where(
                $queryBuilder->expr()->eq(
                    'tablenames',
                    $queryBuilder->createNamedParameter('be_users')
                ),
                $queryBuilder->expr()->eq(
                    'fieldname',
                    $queryBuilder->createNamedParameter('avatar')
                ),
                $queryBuilder->expr()->eq(
                    'uid_foreign',
                    $queryBuilder->createNamedParameter($beUserId, Connection::PARAM_INT)
                )
            )
            ->executeStatement();

        // If Avatar is marked for delete => set it to empty string so it will be updated properly
        if ($fileUid === 'delete') {
            $fileUid = '';
        }

        // Create new reference
        if ((int)$fileUid > 0) {
            // Get file object
            try {
                $file = $this->resourceFactory->getFileObject((int)$fileUid);
            } catch (FileDoesNotExistException $e) {
                $file = false;
            }

            // Check if user is allowed to use the image (only when not in simulation mode)
            if ($file && !$file->getStorage()->checkFileActionPermission('read', $file)) {
                $file = false;
            }

            // Check if extension is allowed
            if ($file && $file->isImage()) {
                // Create new file reference
                $storeRec['sys_file_reference']['NEW1234'] = [
                    'uid_local' => (int)$fileUid,
                    'uid_foreign' => (int)$beUserId,
                    'tablenames' => 'be_users',
                    'fieldname' => 'avatar',
                    'pid' => 0,
                ];
                $storeRec['be_users'][(int)$beUserId]['avatar'] = 'NEW1234';
            }
        }
    }

    /**
     * Register the reset configuration button to the button bar.
     */
    protected function registerResetButtonToButtonBar(ModuleTemplate $view): void
    {
        $languageService = $this->getLanguageService();
        $resetButton = $this->componentFactory->createGenericButton()
            ->setTag('button')
            ->setLabel($languageService->translate('reset_configuration_button', 'backend.user_profile'))
            ->setTitle($languageService->translate('reset_configuration', 'backend.user_profile'))
            ->setIcon($this->iconFactory->getIcon('actions-undo', IconSize::SMALL))
            ->setShowLabelText(true)
            ->setClasses('t3js-modal-trigger')
            ->setAttributes([
                'type' => 'button',
                'data-severity' => 'warning',
                'data-title' => $languageService->translate('reset_configuration', 'backend.user_profile'),
                'data-content' => $languageService->translate('set_to_standard_question', 'backend.user_profile'),
                'data-event' => 'confirm',
                'data-event-name' => 'setup:confirmation:response',
                'data-event-payload' => 'resetConfiguration',
            ]);
        $view->addButtonToButtonBar($resetButton, ButtonBar::BUTTON_POSITION_RIGHT);
    }

    /**
     * Add FlashMessages for various actions
     */
    protected function addFlashMessages(ModuleTemplate $view): void
    {
        $languageService = $this->getLanguageService();
        if ($this->setupIsUpdated && !$this->settingsAreResetToDefault) {
            $view->addFlashMessage($languageService->translate('setup_was_updated', 'backend.user_profile'), $languageService->translate('user_settings', 'backend.user_profile'));
        }
        if ($this->settingsAreResetToDefault) {
            $view->addFlashMessage($languageService->translate('settings_are_reset', 'backend.user_profile'), $languageService->translate('reset_configuration', 'backend.user_profile'));
        }
        if ($this->passwordIsSubmitted) {
            switch ($this->passwordIsUpdated) {
                case self::PASSWORD_NOT_THE_SAME:
                    $view->addFlashMessage($languageService->translate('new_password_failed', 'backend.user_profile'), $languageService->translate('new_password', 'backend.user_profile'), ContextualFeedbackSeverity::ERROR);
                    break;
                case self::PASSWORD_UPDATED:
                    $view->addFlashMessage($languageService->translate('new_password_ok', 'backend.user_profile'), $languageService->translate('new_password', 'backend.user_profile'));
                    break;
                case self::PASSWORD_POLICY_FAILED:
                    $view->addFlashMessage($languageService->translate('password_policy_failed', 'backend.user_profile'), $languageService->translate('new_password', 'backend.user_profile'), ContextualFeedbackSeverity::ERROR);
                    break;
            }
        }
    }

    /**
     * @param array<string, scalar> $data
     * @return array<string, scalar>
     */
    protected function extractPartitionData(array $data, string $partition): array
    {
        $partitionData = [];
        $prefix = $partition . '__';
        $length = strlen($prefix);
        foreach ($data as $key => $value) {
            if (!str_starts_with($key, $prefix)) {
                continue;
            }
            $normalizedKey = substr($key, $length);
            $partitionData[$normalizedKey] = $value;
        }
        return $partitionData;
    }

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

    protected function getLanguageService(): LanguageService
    {
        return $GLOBALS['LANG'];
    }
}
