TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,253 @@
<?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\Backend\Bookmark\Security;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Backend\Bookmark\Traits\RouteParserTrait;
use TYPO3\CMS\Backend\Module\ModuleProvider;
use TYPO3\CMS\Backend\Routing\Router;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
/**
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
*/
#[Autoconfigure(public: true)]
final readonly class BookmarkVoter
{
use RouteParserTrait;
public const string READ = 'read';
public const string CREATE = 'create';
public const string EDIT = 'edit';
public const string DELETE = 'delete';
public const string NAVIGATE = 'navigate';
public function __construct(
protected ModuleProvider $moduleProvider,
protected Router $router,
protected LoggerInterface $logger,
protected StorageRepository $storageRepository,
) {}
protected function getRouter(): Router
{
return $this->router;
}
public function vote(string $attribute, array $bookmark, BackendUserAuthentication $user): bool
{
return match ($attribute) {
self::READ => $this->canRead($bookmark, $user),
self::CREATE => $this->canCreate($user),
self::EDIT => $this->canEdit($bookmark, $user),
self::DELETE => $this->canDelete($bookmark, $user),
self::NAVIGATE => $this->canNavigate($bookmark, $user),
default => false,
};
}
private function canCreate(BackendUserAuthentication $user): bool
{
return isset($user->user['uid']) && (int)$user->user['uid'] > 0;
}
private function canRead(array $bookmark, BackendUserAuthentication $user): bool
{
$userId = (int)$user->user['uid'];
// User's own bookmarks are always readable
if ((int)$bookmark['userid'] === $userId) {
return true;
}
// Global bookmarks are only readable if user can navigate to them
if ((int)$bookmark['sc_group'] < 0) {
return $this->canNavigate($bookmark, $user);
}
return false;
}
private function canEdit(array $bookmark, BackendUserAuthentication $user): bool
{
if (!$this->canRead($bookmark, $user)) {
return false;
}
$userId = (int)$user->user['uid'];
$isOwner = (int)$bookmark['userid'] === $userId;
$groupId = isset($bookmark['group_uuid']) && $bookmark['group_uuid'] !== '' ? $bookmark['group_uuid'] : (int)$bookmark['sc_group'];
$isGlobal = is_int($groupId) && $groupId < 0;
if ($isOwner && !$isGlobal) {
return true;
}
if ($user->isAdmin() && $isGlobal) {
return true;
}
return false;
}
private function canDelete(array $bookmark, BackendUserAuthentication $user): bool
{
$userId = (int)$user->user['uid'];
if ((int)$bookmark['userid'] === $userId) {
return true;
}
if ($user->isAdmin() && (int)$bookmark['sc_group'] < 0) {
return true;
}
return false;
}
private function canNavigate(array $bookmark, BackendUserAuthentication $user): bool
{
$routeIdentifier = $bookmark['route'] ?? '';
try {
$arguments = json_decode($bookmark['arguments'] ?? '', true, 64, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return false;
}
if (!is_array($arguments)) {
return false;
}
$moduleName = $this->getModuleNameFromRouteIdentifier($routeIdentifier);
if ($moduleName === '') {
return false;
}
if (!$this->canAccessModule($routeIdentifier, $moduleName, $user)) {
return false;
}
if (!$this->canAccessFile($moduleName, $arguments)) {
return false;
}
if (!$this->canAccessRecord($moduleName, $routeIdentifier, $arguments, $user)) {
return false;
}
return true;
}
private function canAccessModule(string $routeIdentifier, string $moduleName, BackendUserAuthentication $user): bool
{
// record_edit has its own access checks via canAccessRecord
if ($routeIdentifier === 'record_edit') {
return true;
}
return $this->moduleProvider->accessGranted($moduleName, $user);
}
private function canAccessFile(string $moduleName, array $arguments): bool
{
if ($moduleName !== 'file_FilelistList' && $moduleName !== 'media_management') {
return true;
}
$combinedIdentifier = (string)($arguments['id'] ?? '');
if ($combinedIdentifier === '') {
return true;
}
$storage = $this->storageRepository->findByCombinedIdentifier($combinedIdentifier);
if ($storage === null || $storage->isFallbackStorage()) {
return false;
}
$folderIdentifier = substr($combinedIdentifier, strpos($combinedIdentifier, ':') + 1);
try {
$storage->getFolder($folderIdentifier);
} catch (InsufficientFolderAccessPermissionsException) {
return false;
} catch (FolderDoesNotExistException) {
return true;
} catch (\Throwable $e) {
$this->logger->error('Failed to resolve folder identifier "{folder}" in backend user bookmark: {message}', [
'folder' => $folderIdentifier,
'message' => $e->getMessage(),
]);
return false;
}
return true;
}
private function canAccessRecord(
string $moduleName,
string $routeIdentifier,
array $arguments,
BackendUserAuthentication $user
): bool {
if ($moduleName === 'file_FilelistList' || $moduleName === 'media_management') {
return true;
}
$bookmarkData = $this->parseRecordEditData($routeIdentifier, $arguments);
$pageId = 0;
if ($moduleName === 'record_edit' && isset($bookmarkData['table'], $bookmarkData['recordid'])) {
if (!$user->check('tables_modify', $bookmarkData['table'])) {
return false;
}
$action = $bookmarkData['action'] ?? '';
$recordId = (int)$bookmarkData['recordid'];
if ($action === 'edit' || ($action === 'new' && $recordId < 0)) {
$record = BackendUtility::getRecord($bookmarkData['table'], abs($recordId));
if ($record === null || $record === []) {
return false;
}
$pageId = ($bookmarkData['table'] === 'pages' ? (int)($record['uid'] ?? 0) : (int)($record['pid'] ?? 0));
} elseif ($action === 'new' && $recordId > 0) {
$pageId = $recordId;
}
} else {
$pageId = (int)($arguments['id'] ?? 0);
}
if ($pageId > 0 && !$user->isAdmin()) {
if ($user->isInWebMount($pageId) === null) {
return false;
}
$pageRow = BackendUtility::getRecord('pages', $pageId);
if ($pageRow === null || !$user->doesUserHaveAccess($pageRow, Permission::PAGE_SHOW)) {
return false;
}
}
return true;
}
}