TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
<?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\Avatar;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Service\DependencyOrderingService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* Main class to render an avatar image of a certain Backend user, resolving any avatar provider
|
||||
* that takes care of fetching the image.
|
||||
*
|
||||
* See render() and getImgTag() as main entry points
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class Avatar
|
||||
{
|
||||
/**
|
||||
* @param list<AvatarProviderInterface> $avatarProviders
|
||||
*/
|
||||
public function __construct(
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
protected FrontendInterface $cache,
|
||||
protected DependencyOrderingService $dependencyOrderingService,
|
||||
protected IconFactory $iconFactory,
|
||||
protected array $avatarProviders = [],
|
||||
) {
|
||||
$this->validateAvatarProviders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an avatar based on a Fluid template which contains some base wrapper css classes.
|
||||
* Has a simple caching functionality. Used in Avatar ViewHelper for instance.
|
||||
* Renders avatar of a given backend user record, or of current logged-in backend user.
|
||||
*/
|
||||
public function render(?array $backendUser = null, int $size = 32, bool $showIcon = false): string
|
||||
{
|
||||
if (!is_array($backendUser)) {
|
||||
/** @var array $backendUser */
|
||||
$backendUser = $this->getBackendUser()->user;
|
||||
}
|
||||
$cacheId = 'avatar_' . sha1($backendUser['uid'] . $size . $showIcon);
|
||||
$avatar = $this->cache->get($cacheId);
|
||||
if (!$avatar) {
|
||||
$icon = $showIcon ? $this->iconFactory->getIconForRecord('be_users', $backendUser, IconSize::SMALL)->render() : '';
|
||||
$avatar
|
||||
= '<span class="avatar" style="--avatar-size: ' . $size . 'px;">'
|
||||
. '<span class="avatar-image">' . $this->getImgTag($backendUser, $size) . '</span>'
|
||||
. ($showIcon ? '<span class="avatar-icon">' . $icon . '</span>' : '')
|
||||
. '</span>';
|
||||
$this->cache->set($cacheId, $avatar);
|
||||
}
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an HTML <img> tag of given backend users avatar.
|
||||
*/
|
||||
protected function getImgTag(array $backendUser, int $size = 32): string
|
||||
{
|
||||
$avatarImage = $this->getImage($backendUser, $size);
|
||||
return '<img src="' . htmlspecialchars($avatarImage->getUrl()) . '" '
|
||||
. 'width="' . (int)$avatarImage->getWidth() . '" '
|
||||
. 'height="' . (int)$avatarImage->getHeight() . '" '
|
||||
. 'alt="" '
|
||||
. 'aria-hidden="true" '
|
||||
. 'loading="lazy" />';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Image from first provider that returns one.
|
||||
*/
|
||||
protected function getImage(array $backendUser, int $size): Image
|
||||
{
|
||||
foreach ($this->avatarProviders as $provider) {
|
||||
$avatarImage = $provider->getImage($backendUser, $size);
|
||||
if (!empty($avatarImage)) {
|
||||
return $avatarImage;
|
||||
}
|
||||
}
|
||||
return GeneralUtility::makeInstance(
|
||||
Image::class,
|
||||
(string)PathUtility::getSystemResourceUri('EXT:core/Resources/Public/Icons/T3Icons/svgs/avatar/avatar-default.svg'),
|
||||
$size,
|
||||
$size
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the registered avatar providers
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function validateAvatarProviders(): void
|
||||
{
|
||||
foreach ($this->avatarProviders as $provider) {
|
||||
if (!($provider instanceof AvatarProviderInterface)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
'Avatar provider must implement interface "%s", "%s" given.',
|
||||
AvatarProviderInterface::class,
|
||||
get_debug_type($provider),
|
||||
),
|
||||
1439317802,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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\Backend\Avatar;
|
||||
|
||||
/**
|
||||
* Contract for avatar providers that ensure how an avatar should be rendered for a given Backend User
|
||||
*/
|
||||
interface AvatarProviderInterface
|
||||
{
|
||||
/**
|
||||
* Returns an Image object, prepared for output, based on a given be_users record
|
||||
*
|
||||
* @param array $backendUser be_users record
|
||||
* @param int $size
|
||||
* @return Image|null
|
||||
*/
|
||||
public function getImage(array $backendUser, $size);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?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\Backend\Avatar;
|
||||
|
||||
use TYPO3\CMS\Backend\Attribute\AsAvatarProvider;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Avatar Provider used for rendering avatars based on local files (based on FAL), stored in the be_users.avatar
|
||||
* relation field with sys_file_reference.
|
||||
*/
|
||||
#[AsAvatarProvider('defaultAvatarProvider')]
|
||||
class DefaultAvatarProvider implements AvatarProviderInterface
|
||||
{
|
||||
/**
|
||||
* Return an Image object for rendering the avatar, based on a FAL-based file
|
||||
*
|
||||
* @param array $backendUser be_users record
|
||||
* @param int $size
|
||||
* @return Image|null
|
||||
*/
|
||||
public function getImage(array $backendUser, $size)
|
||||
{
|
||||
$fileUid = $this->getAvatarFileUid($backendUser['uid']);
|
||||
if ($fileUid === 0) {
|
||||
// Early return if there is no valid image file UID
|
||||
return null;
|
||||
}
|
||||
// Get file object
|
||||
try {
|
||||
$file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($fileUid);
|
||||
$processedImage = $file->process(
|
||||
ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
|
||||
['width' => $size . 'c', 'height' => $size . 'c']
|
||||
);
|
||||
|
||||
$publicUrl = $processedImage->getPublicUrl();
|
||||
if ($publicUrl) {
|
||||
$image = GeneralUtility::makeInstance(
|
||||
Image::class,
|
||||
$publicUrl,
|
||||
$processedImage->getProperty('width'),
|
||||
$processedImage->getProperty('height')
|
||||
);
|
||||
} else {
|
||||
$image = null;
|
||||
}
|
||||
} catch (FileDoesNotExistException $e) {
|
||||
// No image found
|
||||
$image = null;
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sys_file UID of the avatar of the given backend user ID
|
||||
*
|
||||
* @param int $backendUserId the UID of the be_users record
|
||||
* @return int the sys_file UID or 0 if none found
|
||||
*/
|
||||
protected function getAvatarFileUid($backendUserId)
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference');
|
||||
$fileUid = $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((int)$backendUserId, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
|
||||
return (int)$fileUid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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\Backend\Avatar;
|
||||
|
||||
/**
|
||||
* Acts as a pseudo model for holding all information of an avatar image
|
||||
* Holds url + dimensions of avatar image
|
||||
*/
|
||||
class Image
|
||||
{
|
||||
/**
|
||||
* Url of avatar image. Needs to be relative to the website root or an absolute URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $width;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $height;
|
||||
|
||||
/**
|
||||
* @param string $url url of image. Needs to be relative to the website root or an absolute URL.
|
||||
* @param int $width width of image
|
||||
* @param int $height height of image
|
||||
*/
|
||||
public function __construct($url, $width, $height)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->width = (int)$width;
|
||||
$this->height = (int)$height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the URL to the avatar image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getWidth()
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getHeight()
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Value object representing a bookmark in the backend.
|
||||
*
|
||||
* Bookmarks provide quick access to frequently used backend locations
|
||||
* and are displayed in the bookmark toolbar dropdown.
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
readonly class Bookmark implements \JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $route,
|
||||
public string $arguments,
|
||||
public string $title,
|
||||
public int|string $groupId,
|
||||
public string $iconIdentifier,
|
||||
public string $iconOverlayIdentifier,
|
||||
public string $module,
|
||||
public string $href,
|
||||
public bool $editable = false,
|
||||
public bool $accessible = false,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'route' => $this->route,
|
||||
'arguments' => $this->arguments,
|
||||
'title' => $this->title,
|
||||
'groupId' => $this->groupId,
|
||||
'iconIdentifier' => $this->iconIdentifier,
|
||||
'iconOverlayIdentifier' => $this->iconOverlayIdentifier,
|
||||
'module' => $this->module,
|
||||
'href' => $this->href,
|
||||
'editable' => $this->editable,
|
||||
'accessible' => $this->accessible,
|
||||
];
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Value object representing a bookmark group.
|
||||
*
|
||||
* Groups are used to organize bookmarks in the backend toolbar.
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
readonly class BookmarkGroup implements \JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public int|string $id,
|
||||
public string $label,
|
||||
public BookmarkGroupType $type,
|
||||
public int $sorting = 0,
|
||||
public bool $editable = false,
|
||||
public bool $selectable = true,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'label' => $this->label,
|
||||
'type' => $this->type->value,
|
||||
'priority' => $this->type->getPriority(),
|
||||
'sorting' => $this->sorting,
|
||||
'editable' => $this->editable,
|
||||
'selectable' => $this->selectable,
|
||||
];
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Defines the different types/sources of bookmark groups.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
enum BookmarkGroupType: string
|
||||
{
|
||||
/**
|
||||
* System groups defined via UserTSconfig (options.bookmarkGroups.X = "Label").
|
||||
* These have positive integer IDs and include the default bookmark groups.
|
||||
*/
|
||||
case SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* Global groups (negative IDs) - bookmarks visible to all users but only admins can add to them.
|
||||
* These mirror the system groups but with negative IDs.
|
||||
*/
|
||||
case GLOBAL = 'global';
|
||||
|
||||
/**
|
||||
* User-created groups stored in sys_be_shortcuts_group table.
|
||||
* These have UUID identifiers and are specific to the user who created them.
|
||||
*/
|
||||
case USER = 'user';
|
||||
|
||||
/**
|
||||
* Returns the priority for this group type.
|
||||
* Lower values appear first: user → system → global
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::USER => 0,
|
||||
self::SYSTEM => 1,
|
||||
self::GLOBAL => 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
|
||||
/**
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
readonly class BookmarkRepository
|
||||
{
|
||||
protected const TABLE_NAME = 'sys_be_shortcuts';
|
||||
protected const GROUP_TABLE_NAME = 'sys_be_shortcuts_group';
|
||||
|
||||
public function __construct(
|
||||
protected ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$row = $queryBuilder->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
|
||||
return $row !== false ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
* @return array<int, array> Indexed by bookmark ID
|
||||
*/
|
||||
public function findByIds(array $ids): array
|
||||
{
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$result = $queryBuilder->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->in(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
|
||||
)
|
||||
)
|
||||
->executeQuery();
|
||||
|
||||
$bookmarks = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$bookmarks[(int)$row['uid']] = $row;
|
||||
}
|
||||
|
||||
return $bookmarks;
|
||||
}
|
||||
|
||||
public function findByUser(int $userId): array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
|
||||
$constraints = [];
|
||||
|
||||
// User's own bookmarks with a non-negative sc_group value.
|
||||
// Bookmarks in user-created groups also match here, as their sc_group defaults to 0.
|
||||
$constraints[] = $queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
'sc_group',
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
|
||||
// Global bookmarks (negative sc_group) - visible to all users
|
||||
$constraints[] = $queryBuilder->expr()->lt(
|
||||
'sc_group',
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
);
|
||||
|
||||
$result = $queryBuilder->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where($queryBuilder->expr()->or(...$constraints))
|
||||
->orderBy('sc_group')
|
||||
->addOrderBy('sorting')
|
||||
->executeQuery();
|
||||
|
||||
$bookmarks = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$bookmarks[] = $row;
|
||||
}
|
||||
|
||||
return $bookmarks;
|
||||
}
|
||||
|
||||
public function exists(int $userId, string $routeIdentifier, string $arguments): bool
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
|
||||
$uid = $queryBuilder->select('uid')
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq('route', $queryBuilder->createNamedParameter($routeIdentifier)),
|
||||
$queryBuilder->expr()->eq('arguments', $queryBuilder->createNamedParameter($arguments))
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
|
||||
return (bool)$uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|false The new bookmark ID or false on failure
|
||||
*/
|
||||
public function insert(int $userId, string $routeIdentifier, string $arguments, string $title): int|false
|
||||
{
|
||||
$connection = $this->connectionPool->getConnectionForTable(self::TABLE_NAME);
|
||||
$affectedRows = $connection->insert(
|
||||
self::TABLE_NAME,
|
||||
[
|
||||
'userid' => $userId,
|
||||
'route' => $routeIdentifier,
|
||||
'arguments' => $arguments,
|
||||
'description' => $title ?: 'Bookmark',
|
||||
'sorting' => $GLOBALS['EXEC_TIME'],
|
||||
]
|
||||
);
|
||||
|
||||
if ($affectedRows === 1) {
|
||||
return (int)$connection->lastInsertId();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(
|
||||
int $id,
|
||||
?int $userId,
|
||||
string $title,
|
||||
int|string $groupId,
|
||||
bool $allowGlobalGroups = true
|
||||
): int {
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->update(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->set('description', $title);
|
||||
|
||||
if (is_string($groupId)) {
|
||||
$queryBuilder->set('group_uuid', $groupId);
|
||||
$queryBuilder->set('sc_group', BookmarkService::GROUP_DEFAULT);
|
||||
} else {
|
||||
$effectiveGroupId = $allowGlobalGroups ? $groupId : max(BookmarkService::GROUP_DEFAULT, $groupId);
|
||||
$queryBuilder->set('sc_group', $effectiveGroupId);
|
||||
$queryBuilder->set('group_uuid', null);
|
||||
}
|
||||
|
||||
if ($userId !== null) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
if (!$allowGlobalGroups) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->gte(
|
||||
'sc_group',
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $queryBuilder->executeStatement();
|
||||
}
|
||||
|
||||
public function delete(int $id, ?int $userId = null): int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->delete(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
|
||||
if ($userId !== null) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $queryBuilder->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $ids
|
||||
*/
|
||||
public function deleteMultiple(array $ids, int $userId): int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
|
||||
return $queryBuilder->delete(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->in(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function updateSorting(int $id, int $userId, int $sorting): int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
|
||||
return $queryBuilder->update(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->set('sorting', $sorting)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $ids
|
||||
*/
|
||||
public function moveToGroup(array $ids, int $userId, int|string $groupId, bool $allowGlobalGroups = true): int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->update(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->in(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
|
||||
if (is_string($groupId)) {
|
||||
$queryBuilder->set('group_uuid', $groupId);
|
||||
$queryBuilder->set('sc_group', BookmarkService::GROUP_DEFAULT);
|
||||
} else {
|
||||
$effectiveGroupId = $allowGlobalGroups ? $groupId : max(BookmarkService::GROUP_DEFAULT, $groupId);
|
||||
$queryBuilder->set('sc_group', $effectiveGroupId);
|
||||
$queryBuilder->set('group_uuid', null);
|
||||
}
|
||||
|
||||
return $queryBuilder->executeStatement();
|
||||
}
|
||||
|
||||
public function findGroupsByUser(int $userId): array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME);
|
||||
|
||||
$result = $queryBuilder
|
||||
->select('*')
|
||||
->from(self::GROUP_TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('userid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT))
|
||||
)
|
||||
->orderBy('sorting', 'ASC')
|
||||
->executeQuery();
|
||||
|
||||
$groups = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$groups[] = $row;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function findGroupByUuid(string $uuid): ?array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME);
|
||||
$row = $queryBuilder
|
||||
->select('*')
|
||||
->from(self::GROUP_TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uuid', $queryBuilder->createNamedParameter($uuid))
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
|
||||
return $row !== false ? $row : null;
|
||||
}
|
||||
|
||||
public function createGroup(int $userId, string $label): ?string
|
||||
{
|
||||
$uuid = (string)Uuid::v4();
|
||||
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME);
|
||||
|
||||
$maxSorting = $queryBuilder
|
||||
->select('sorting')
|
||||
->from(self::GROUP_TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('userid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT))
|
||||
)
|
||||
->orderBy('sorting', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
$sorting = ($maxSorting !== false ? (int)$maxSorting : 0) + 1;
|
||||
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME);
|
||||
$affectedRows = $queryBuilder
|
||||
->insert(self::GROUP_TABLE_NAME)
|
||||
->values([
|
||||
'uuid' => $uuid,
|
||||
'userid' => $userId,
|
||||
'label' => $label,
|
||||
'sorting' => $sorting,
|
||||
])
|
||||
->executeStatement();
|
||||
|
||||
return $affectedRows === 1 ? $uuid : null;
|
||||
}
|
||||
|
||||
public function updateGroup(string $uuid, string $label): int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME);
|
||||
|
||||
return $queryBuilder
|
||||
->update(self::GROUP_TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uuid', $queryBuilder->createNamedParameter($uuid))
|
||||
)
|
||||
->set('label', $label)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function deleteGroup(string $uuid): int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME);
|
||||
|
||||
return $queryBuilder
|
||||
->delete(self::GROUP_TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uuid', $queryBuilder->createNamedParameter($uuid))
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $uuids
|
||||
*/
|
||||
public function reorderGroups(array $uuids, int $userId): void
|
||||
{
|
||||
$sorting = 0;
|
||||
foreach ($uuids as $uuid) {
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME);
|
||||
$queryBuilder
|
||||
->update(self::GROUP_TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uuid', $queryBuilder->createNamedParameter($uuid)),
|
||||
$queryBuilder->expr()->eq('userid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT))
|
||||
)
|
||||
->set('sorting', $sorting++)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
public function moveBookmarksFromGroupToDefault(string $uuid, int $userId): int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
|
||||
return $queryBuilder
|
||||
->update(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('group_uuid', $queryBuilder->createNamedParameter($uuid)),
|
||||
$queryBuilder->expr()->eq('userid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT))
|
||||
)
|
||||
->set('group_uuid', null)
|
||||
->set('sc_group', BookmarkService::GROUP_DEFAULT)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Backend\Bookmark\Security\BookmarkGroupVoter;
|
||||
use TYPO3\CMS\Backend\Backend\Bookmark\Security\BookmarkVoter;
|
||||
use TYPO3\CMS\Backend\Module\ModuleProvider;
|
||||
use TYPO3\CMS\Backend\Routing\Router;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
|
||||
/**
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class BookmarkService
|
||||
{
|
||||
use Traits\RouteParserTrait;
|
||||
|
||||
public const GROUP_DEFAULT = 0;
|
||||
public const GROUP_SUPERGLOBAL = -100;
|
||||
|
||||
public function __construct(
|
||||
protected BookmarkRepository $bookmarkRepository,
|
||||
protected BookmarkVoter $bookmarkVoter,
|
||||
protected BookmarkGroupVoter $bookmarkGroupVoter,
|
||||
protected IconFactory $iconFactory,
|
||||
protected ModuleProvider $moduleProvider,
|
||||
protected Router $router,
|
||||
protected UriBuilder $uriBuilder,
|
||||
) {}
|
||||
|
||||
protected function getRouter(): Router
|
||||
{
|
||||
return $this->router;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (bool)($this->getBackendUser()->getTSConfig()['options.']['enableBookmarks'] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Bookmark[]
|
||||
*/
|
||||
public function getBookmarks(): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
$rows = $this->bookmarkRepository->findByUser($userId);
|
||||
|
||||
// Build list of valid group IDs from existing groups
|
||||
$groups = $this->getGroups();
|
||||
$validGroupIds = array_map(static fn(BookmarkGroup $g) => $g->id, $groups);
|
||||
|
||||
$bookmarks = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
// Skip bookmarks the user cannot read (e.g., global bookmarks they can't navigate to)
|
||||
if (!$this->bookmarkVoter->vote(BookmarkVoter::READ, $row, $backendUser)) {
|
||||
continue;
|
||||
}
|
||||
$row = $this->migrateBookmarkGroup($row, $validGroupIds);
|
||||
$bookmark = $this->createBookmarkFromRow($row, $backendUser);
|
||||
if ($bookmark !== null) {
|
||||
$bookmarks[] = $bookmark;
|
||||
}
|
||||
}
|
||||
|
||||
return $bookmarks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures bookmark is assigned to a valid group for output.
|
||||
*/
|
||||
private function migrateBookmarkGroup(array $row, array $validGroupIds): array
|
||||
{
|
||||
$groupUuid = $row['group_uuid'] ?? null;
|
||||
$scGroup = (int)($row['sc_group'] ?? 0);
|
||||
$groupId = $groupUuid !== null ? $groupUuid : $scGroup;
|
||||
|
||||
if (in_array($groupId, $validGroupIds, true)) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
$row['group_uuid'] = null;
|
||||
$row['sc_group'] = $scGroup < 0 ? self::GROUP_SUPERGLOBAL : self::GROUP_DEFAULT;
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BookmarkGroup[]
|
||||
*/
|
||||
public function getGroups(): array
|
||||
{
|
||||
$groups = [];
|
||||
$backendUser = $this->getBackendUser();
|
||||
$languageService = $this->getLanguageService();
|
||||
$globalPrefix = $languageService->sL('core.bookmarks:global');
|
||||
|
||||
// User-created groups
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
$userGroupRows = $this->bookmarkRepository->findGroupsByUser($userId);
|
||||
foreach ($userGroupRows as $index => $row) {
|
||||
$groups[] = $this->createBookmarkGroup(
|
||||
$row['uuid'],
|
||||
$row['label'] ?? '',
|
||||
BookmarkGroupType::USER,
|
||||
(int)($row['sorting'] ?? $index),
|
||||
$backendUser,
|
||||
isset($row['userid']) ? (int)$row['userid'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
// TSconfig groups and their global counterparts
|
||||
$configGroups = $backendUser->getTSConfig()['options.']['bookmarkGroups.'] ?? [];
|
||||
$tsconfigIndex = 0;
|
||||
if (is_array($configGroups)) {
|
||||
foreach ($configGroups as $groupId => $configLabel) {
|
||||
$groupId = (int)$groupId;
|
||||
if ($groupId <= 0 || $groupId === 100 || $configLabel === '' || $configLabel === null) {
|
||||
continue;
|
||||
}
|
||||
$label = $languageService->sL((string)$configLabel);
|
||||
$sorting = $tsconfigIndex++;
|
||||
|
||||
$groups[] = $this->createBookmarkGroup(
|
||||
$groupId,
|
||||
$label,
|
||||
BookmarkGroupType::SYSTEM,
|
||||
$sorting,
|
||||
$backendUser,
|
||||
);
|
||||
|
||||
$groups[] = $this->createBookmarkGroup(
|
||||
-$groupId,
|
||||
$globalPrefix . ': ' . $label,
|
||||
BookmarkGroupType::GLOBAL,
|
||||
$sorting,
|
||||
$backendUser,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Superglobal group
|
||||
$groups[] = $this->createBookmarkGroup(
|
||||
self::GROUP_SUPERGLOBAL,
|
||||
$globalPrefix . ': ' . $languageService->sL('core.bookmarks:all'),
|
||||
BookmarkGroupType::GLOBAL,
|
||||
0,
|
||||
$backendUser,
|
||||
);
|
||||
|
||||
// Default group
|
||||
$groups[] = $this->createBookmarkGroup(
|
||||
self::GROUP_DEFAULT,
|
||||
$languageService->sL('core.bookmarks:group_default'),
|
||||
BookmarkGroupType::SYSTEM,
|
||||
100,
|
||||
$backendUser,
|
||||
);
|
||||
|
||||
// Sort by type priority, then by sorting value
|
||||
$priorityKeys = array_map(static fn(BookmarkGroup $g) => $g->type->getPriority(), $groups);
|
||||
$sortingKeys = array_map(static fn(BookmarkGroup $g) => $g->sorting, $groups);
|
||||
array_multisort($priorityKeys, SORT_ASC, SORT_NUMERIC, $sortingKeys, SORT_ASC, SORT_NUMERIC, $groups);
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getBookmark(int $id): ?Bookmark
|
||||
{
|
||||
$row = $this->bookmarkRepository->findById($id);
|
||||
if ($row === null) {
|
||||
return null;
|
||||
}
|
||||
$backendUser = $this->getBackendUser();
|
||||
if (!$this->bookmarkVoter->vote(BookmarkVoter::READ, $row, $backendUser)) {
|
||||
return null;
|
||||
}
|
||||
$groups = $this->getGroups();
|
||||
$validGroupIds = array_map(static fn(BookmarkGroup $g) => $g->id, $groups);
|
||||
$row = $this->migrateBookmarkGroup($row, $validGroupIds);
|
||||
return $this->createBookmarkFromRow($row, $backendUser);
|
||||
}
|
||||
|
||||
public function hasBookmark(string $routeIdentifier, string $arguments): bool
|
||||
{
|
||||
$userId = (int)$this->getBackendUser()->user['uid'];
|
||||
return $this->bookmarkRepository->exists($userId, $routeIdentifier, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|false The ID of the newly created bookmark, or false on failure
|
||||
*/
|
||||
public function createBookmark(string $routeIdentifier, string $arguments = '', string $title = ''): int|false
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
|
||||
if (!$this->bookmarkVoter->vote(BookmarkVoter::CREATE, [], $backendUser)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->router->hasRoute($routeIdentifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($arguments !== '' && !json_validate($arguments)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
$result = $this->bookmarkRepository->insert($userId, $routeIdentifier, $arguments, $title);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, error?: string}
|
||||
*/
|
||||
public function updateBookmark(int $id, string $title, int|string $groupId): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
|
||||
// Check if bookmark exists
|
||||
$bookmark = $this->bookmarkRepository->findById($id);
|
||||
if ($bookmark === null) {
|
||||
return $this->errorResponse(
|
||||
'core.bookmarks:error.notFound.message'
|
||||
);
|
||||
}
|
||||
|
||||
// Check edit permission
|
||||
if (!$this->bookmarkVoter->vote(BookmarkVoter::EDIT, $bookmark, $backendUser)) {
|
||||
return $this->errorResponse(
|
||||
'core.bookmarks:error.accessDenied.message'
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user can use global groups
|
||||
if (is_int($groupId) && $groupId < 0 && !$backendUser->isAdmin()) {
|
||||
return $this->errorResponse(
|
||||
'core.bookmarks:error.globalGroupNotAllowed.message'
|
||||
);
|
||||
}
|
||||
|
||||
$userId = $backendUser->isAdmin() ? null : (int)$backendUser->user['uid'];
|
||||
$allowGlobalGroups = $backendUser->isAdmin();
|
||||
|
||||
$this->bookmarkRepository->update(
|
||||
$id,
|
||||
$userId,
|
||||
$title,
|
||||
$groupId,
|
||||
$allowGlobalGroups
|
||||
);
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: false, error: string}
|
||||
*/
|
||||
private function errorResponse(string $label): array
|
||||
{
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $this->getLanguageService()->sL($label),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, error?: string}
|
||||
*/
|
||||
public function deleteBookmark(int $id): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$bookmark = $this->bookmarkRepository->findById($id);
|
||||
if ($bookmark === null) {
|
||||
return $this->errorResponse(
|
||||
'core.bookmarks:error.notFound.message'
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->bookmarkVoter->vote(BookmarkVoter::DELETE, $bookmark, $backendUser)) {
|
||||
return $this->errorResponse(
|
||||
'core.bookmarks:error.accessDenied.message'
|
||||
);
|
||||
}
|
||||
|
||||
$affectedRows = $this->bookmarkRepository->delete($id);
|
||||
|
||||
if ($affectedRows !== 1) {
|
||||
return $this->errorResponse(
|
||||
'core.bookmarks:error.deleteFailed.message'
|
||||
);
|
||||
}
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $bookmarkIds
|
||||
*/
|
||||
public function reorderBookmarks(array $bookmarkIds): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
|
||||
// Fetch all bookmarks in one query
|
||||
$bookmarks = $this->bookmarkRepository->findByIds($bookmarkIds);
|
||||
|
||||
$sorting = 0;
|
||||
foreach ($bookmarkIds as $bookmarkId) {
|
||||
$bookmark = $bookmarks[$bookmarkId] ?? null;
|
||||
if ($bookmark !== null && $this->bookmarkVoter->vote(BookmarkVoter::EDIT, $bookmark, $backendUser)) {
|
||||
$this->bookmarkRepository->updateSorting($bookmarkId, $userId, $sorting++);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $bookmarkIds
|
||||
*/
|
||||
public function deleteBookmarks(array $bookmarkIds): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
|
||||
// Fetch all bookmarks in one query
|
||||
$bookmarks = $this->bookmarkRepository->findByIds($bookmarkIds);
|
||||
|
||||
$manageableIds = [];
|
||||
foreach ($bookmarkIds as $bookmarkId) {
|
||||
$bookmark = $bookmarks[$bookmarkId] ?? null;
|
||||
if ($bookmark !== null && $this->bookmarkVoter->vote(BookmarkVoter::DELETE, $bookmark, $backendUser)) {
|
||||
$manageableIds[] = $bookmarkId;
|
||||
}
|
||||
}
|
||||
|
||||
if ($manageableIds !== []) {
|
||||
$this->bookmarkRepository->deleteMultiple($manageableIds, $userId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $bookmarkIds
|
||||
*/
|
||||
public function moveBookmarks(array $bookmarkIds, int|string $groupId): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
$allowGlobalGroups = $backendUser->isAdmin();
|
||||
|
||||
// Validate target group access for user-created groups
|
||||
if (is_string($groupId)) {
|
||||
$group = $this->bookmarkRepository->findGroupByUuid($groupId);
|
||||
if ($group === null || !$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::READ, $group, $backendUser)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all bookmarks in one query and filter to those user can edit
|
||||
$bookmarks = $this->bookmarkRepository->findByIds($bookmarkIds);
|
||||
|
||||
$manageableIds = [];
|
||||
foreach ($bookmarkIds as $bookmarkId) {
|
||||
$bookmark = $bookmarks[$bookmarkId] ?? null;
|
||||
if ($bookmark !== null && $this->bookmarkVoter->vote(BookmarkVoter::EDIT, $bookmark, $backendUser)) {
|
||||
$manageableIds[] = $bookmarkId;
|
||||
}
|
||||
}
|
||||
|
||||
if ($manageableIds === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->bookmarkRepository->moveToGroup(
|
||||
$manageableIds,
|
||||
$userId,
|
||||
$groupId,
|
||||
$allowGlobalGroups
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createGroup(string $label): ?BookmarkGroup
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
|
||||
if (!$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::CREATE, [], $backendUser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
$uuid = $this->bookmarkRepository->createGroup($userId, $label);
|
||||
|
||||
if ($uuid === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $this->bookmarkRepository->findGroupByUuid($uuid);
|
||||
if ($row === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createBookmarkGroup(
|
||||
$row['uuid'],
|
||||
$row['label'] ?? '',
|
||||
BookmarkGroupType::USER,
|
||||
0,
|
||||
$backendUser,
|
||||
isset($row['userid']) ? (int)$row['userid'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
public function updateGroup(string $uuid, string $label): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$group = $this->bookmarkRepository->findGroupByUuid($uuid);
|
||||
if ($group === null || !$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::EDIT, $group, $backendUser)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->bookmarkRepository->updateGroup($uuid, $label);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteGroup(string $uuid): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$group = $this->bookmarkRepository->findGroupByUuid($uuid);
|
||||
if ($group === null || !$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::DELETE, $group, $backendUser)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
$this->bookmarkRepository->moveBookmarksFromGroupToDefault($uuid, $userId);
|
||||
$affectedRows = $this->bookmarkRepository->deleteGroup($uuid);
|
||||
|
||||
return $affectedRows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $uuids
|
||||
*/
|
||||
public function reorderGroups(array $uuids): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$userId = (int)$backendUser->user['uid'];
|
||||
|
||||
// Get all user's groups in one query
|
||||
$userGroups = $this->bookmarkRepository->findGroupsByUser($userId);
|
||||
$groupsByUuid = [];
|
||||
foreach ($userGroups as $group) {
|
||||
$groupsByUuid[$group['uuid']] = $group;
|
||||
}
|
||||
|
||||
// Verify all provided UUIDs can be edited by the user
|
||||
foreach ($uuids as $uuid) {
|
||||
$group = $groupsByUuid[$uuid] ?? null;
|
||||
if ($group === null || !$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::EDIT, $group, $backendUser)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->bookmarkRepository->reorderGroups($uuids, $userId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createBookmarkFromRow(array $row, BackendUserAuthentication $user): ?Bookmark
|
||||
{
|
||||
$routeIdentifier = $row['route'] ?? '';
|
||||
|
||||
try {
|
||||
$arguments = json_decode($row['arguments'] ?? '', true, 64, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_array($arguments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$moduleName = $this->getModuleNameFromRouteIdentifier($routeIdentifier);
|
||||
if ($moduleName === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$accessible = $this->bookmarkVoter->vote(BookmarkVoter::NAVIGATE, $row, $user);
|
||||
$editable = $this->bookmarkVoter->vote(BookmarkVoter::EDIT, $row, $user);
|
||||
|
||||
$groupUuid = $row['group_uuid'] ?? null;
|
||||
$groupId = $groupUuid !== null ? $groupUuid : (int)$row['sc_group'];
|
||||
$bookmarkData = $this->parseRecordEditData($routeIdentifier, $arguments);
|
||||
$iconData = $this->resolveBookmarkIcon($routeIdentifier, $moduleName, $bookmarkData);
|
||||
$href = $accessible ? (string)$this->uriBuilder->buildUriFromRoute($routeIdentifier, $arguments) : '';
|
||||
|
||||
return new Bookmark(
|
||||
id: (int)$row['uid'],
|
||||
route: $routeIdentifier,
|
||||
arguments: $row['arguments'] ?? '',
|
||||
title: ($row['description'] ?? false) ?: 'Bookmark',
|
||||
groupId: $groupId,
|
||||
iconIdentifier: $iconData['identifier'],
|
||||
iconOverlayIdentifier: $iconData['overlay'],
|
||||
module: $moduleName,
|
||||
href: $href,
|
||||
editable: $editable,
|
||||
accessible: $accessible,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{identifier: string, overlay: string}
|
||||
*/
|
||||
private function resolveBookmarkIcon(string $routeIdentifier, string $moduleName, array $bookmarkData): array
|
||||
{
|
||||
$identifier = '';
|
||||
$overlay = '';
|
||||
|
||||
switch ($routeIdentifier) {
|
||||
case 'record_edit':
|
||||
$table = $bookmarkData['table'] ?? '';
|
||||
$recordid = $bookmarkData['recordid'] ?? 0;
|
||||
|
||||
$action = $bookmarkData['action'] ?? '';
|
||||
if ($action === 'edit') {
|
||||
$row = BackendUtility::getRecordWSOL($table, (int)$recordid) ?? [];
|
||||
$icon = $this->iconFactory->getIconForRecord($table, $row, IconSize::SMALL);
|
||||
} elseif ($action === 'new') {
|
||||
$icon = $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL);
|
||||
} else {
|
||||
$icon = $this->iconFactory->getIcon('empty-empty', IconSize::SMALL);
|
||||
}
|
||||
$identifier = $icon->getIdentifier();
|
||||
$overlay = $icon->getOverlayIcon()?->getIdentifier() ?? '';
|
||||
break;
|
||||
|
||||
case 'file_edit':
|
||||
$identifier = 'mimetypes-text-html';
|
||||
break;
|
||||
|
||||
default:
|
||||
$iconIdentifier = '';
|
||||
if ($module = $this->moduleProvider->getModule($moduleName, null, false)) {
|
||||
$iconIdentifier = $module->getIconIdentifier();
|
||||
if ($iconIdentifier === '' && $module->getParentModule()) {
|
||||
$iconIdentifier = $module->getParentModule()->getIconIdentifier();
|
||||
}
|
||||
}
|
||||
if ($iconIdentifier === '') {
|
||||
$iconIdentifier = 'empty-empty';
|
||||
}
|
||||
$identifier = $iconIdentifier;
|
||||
}
|
||||
|
||||
return [
|
||||
'identifier' => $identifier,
|
||||
'overlay' => $overlay,
|
||||
];
|
||||
}
|
||||
|
||||
private function createBookmarkGroup(
|
||||
int|string $id,
|
||||
string $label,
|
||||
BookmarkGroupType $type,
|
||||
int $sorting,
|
||||
BackendUserAuthentication $user,
|
||||
?int $userid = null
|
||||
): BookmarkGroup {
|
||||
$voterData = ['id' => $id, 'userid' => $userid];
|
||||
$editable = $this->bookmarkGroupVoter->vote(BookmarkGroupVoter::EDIT, $voterData, $user);
|
||||
$selectable = $this->bookmarkGroupVoter->vote(BookmarkGroupVoter::SELECT, $voterData, $user);
|
||||
|
||||
return new BookmarkGroup(
|
||||
id: $id,
|
||||
label: $label,
|
||||
type: $type,
|
||||
sorting: $sorting,
|
||||
editable: $editable,
|
||||
selectable: $selectable,
|
||||
);
|
||||
}
|
||||
|
||||
private function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?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 Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
|
||||
/**
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class BookmarkGroupVoter
|
||||
{
|
||||
public const string READ = 'read';
|
||||
public const string CREATE = 'create';
|
||||
public const string EDIT = 'edit';
|
||||
public const string DELETE = 'delete';
|
||||
public const string SELECT = 'select';
|
||||
|
||||
public function vote(string $attribute, array $group, BackendUserAuthentication $user): bool
|
||||
{
|
||||
return match ($attribute) {
|
||||
self::READ => $this->canRead($group, $user),
|
||||
self::CREATE => $this->canCreate($user),
|
||||
self::EDIT => $this->canEdit($group, $user),
|
||||
self::DELETE => $this->canDelete($group, $user),
|
||||
self::SELECT => $this->canSelect($group, $user),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
private function canCreate(BackendUserAuthentication $user): bool
|
||||
{
|
||||
return isset($user->user['uid']) && (int)$user->user['uid'] > 0;
|
||||
}
|
||||
|
||||
private function canRead(array $group, BackendUserAuthentication $user): bool
|
||||
{
|
||||
$userId = (int)$user->user['uid'];
|
||||
return (int)$group['userid'] === $userId;
|
||||
}
|
||||
|
||||
private function canEdit(array $group, BackendUserAuthentication $user): bool
|
||||
{
|
||||
$groupId = $group['id'] ?? null;
|
||||
|
||||
// System/global groups (integer IDs) are not editable
|
||||
if (is_int($groupId) || is_numeric($groupId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// User-created groups (UUID strings) are editable only if user owns them
|
||||
if (!isset($group['userid'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userId = (int)$user->user['uid'];
|
||||
return (int)$group['userid'] === $userId;
|
||||
}
|
||||
|
||||
private function canDelete(array $group, BackendUserAuthentication $user): bool
|
||||
{
|
||||
// Delete permission mirrors edit permission
|
||||
return $this->canEdit($group, $user);
|
||||
}
|
||||
|
||||
private function canSelect(array $group, BackendUserAuthentication $user): bool
|
||||
{
|
||||
$groupId = $group['id'] ?? 0;
|
||||
|
||||
// Global groups (negative IDs) are only selectable by admins
|
||||
if (is_int($groupId) && $groupId < 0) {
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\Traits;
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\Router;
|
||||
|
||||
/**
|
||||
* Shared route parsing functionality for bookmark components.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait RouteParserTrait
|
||||
{
|
||||
abstract protected function getRouter(): Router;
|
||||
|
||||
private function getModuleNameFromRouteIdentifier(string $routeIdentifier): string
|
||||
{
|
||||
if (in_array($routeIdentifier, ['record_edit', 'file_edit'], true)) {
|
||||
return $routeIdentifier;
|
||||
}
|
||||
return (string)($this->getRouter()->getRoute($routeIdentifier)?->getOption('module')?->getIdentifier() ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{table?: string, recordid?: string, action?: string}
|
||||
*/
|
||||
private function parseRecordEditData(string $routeIdentifier, array $arguments): array
|
||||
{
|
||||
if ($routeIdentifier !== 'record_edit' || !is_array($arguments['edit'] ?? null)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$table = key($arguments['edit']);
|
||||
$tableData = current($arguments['edit']);
|
||||
$recordId = is_array($tableData) ? key($tableData) : null;
|
||||
|
||||
if (!is_string($table) || (!is_string($recordId) && !is_int($recordId))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$recordId = (string)$recordId;
|
||||
if (str_ends_with($recordId, ',')) {
|
||||
$recordId = substr($recordId, 0, -1);
|
||||
}
|
||||
|
||||
return [
|
||||
'table' => $table,
|
||||
'recordid' => $recordId,
|
||||
'action' => $arguments['edit'][$table][$recordId] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
|
||||
enum ColorScheme: string
|
||||
{
|
||||
case auto = 'auto';
|
||||
case light = 'light';
|
||||
case dark = 'dark';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::auto => 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:colorScheme.auto',
|
||||
self::light => 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:colorScheme.light',
|
||||
self::dark => 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:colorScheme.dark',
|
||||
};
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::auto => 'actions-circle-half',
|
||||
self::light => 'actions-brightness-high',
|
||||
self::dark => 'actions-moon',
|
||||
};
|
||||
}
|
||||
|
||||
public static function getAvailableItemsForSelection(): array
|
||||
{
|
||||
return [
|
||||
['label' => self::auto->getLabel(), 'value' => self::auto->value],
|
||||
['label' => self::light->getLabel(), 'value' => self::light->value],
|
||||
['label' => self::dark->getLabel(), 'value' => self::dark->value],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?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\Event;
|
||||
|
||||
use TYPO3\CMS\Backend\Backend\ToolbarItems\ClearCacheToolbarItem;
|
||||
|
||||
/**
|
||||
* An event to modify the clear cache actions, shown in the TYPO3 Backend top toolbar
|
||||
*
|
||||
* @phpstan-import-type CacheAction from ClearCacheToolbarItem
|
||||
*/
|
||||
final class ModifyClearCacheActionsEvent
|
||||
{
|
||||
/**
|
||||
* @param list<CacheAction> $cacheActions
|
||||
* @param list<non-empty-string> $cacheActionIdentifiers
|
||||
*/
|
||||
public function __construct(private array $cacheActions, private array $cacheActionIdentifiers) {}
|
||||
|
||||
/**
|
||||
* @param CacheAction $cacheAction
|
||||
*/
|
||||
public function addCacheAction(array $cacheAction): void
|
||||
{
|
||||
$this->cacheActions[] = $cacheAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<CacheAction> $cacheActions
|
||||
*/
|
||||
public function setCacheActions(array $cacheActions): void
|
||||
{
|
||||
$this->cacheActions = $cacheActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<CacheAction>
|
||||
*/
|
||||
public function getCacheActions(): array
|
||||
{
|
||||
return $this->cacheActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $cacheActionIdentifier
|
||||
*/
|
||||
public function addCacheActionIdentifier(string $cacheActionIdentifier): void
|
||||
{
|
||||
$this->cacheActionIdentifiers[] = $cacheActionIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<non-empty-string> $cacheActionIdentifiers
|
||||
*/
|
||||
public function setCacheActionIdentifiers(array $cacheActionIdentifiers): void
|
||||
{
|
||||
$this->cacheActionIdentifiers = $cacheActionIdentifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<non-empty-string>
|
||||
*/
|
||||
public function getCacheActionIdentifiers(): array
|
||||
{
|
||||
return $this->cacheActionIdentifiers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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\Event;
|
||||
|
||||
use TYPO3\CMS\Backend\Backend\ToolbarItems\SystemInformationToolbarItem;
|
||||
|
||||
/**
|
||||
* An event to enrich the system information toolbar in the TYPO3 Backend top toolbar
|
||||
* with various information
|
||||
*/
|
||||
final readonly class SystemInformationToolbarCollectorEvent
|
||||
{
|
||||
public function __construct(private SystemInformationToolbarItem $toolbarItem) {}
|
||||
|
||||
public function getToolbarItem(): SystemInformationToolbarItem
|
||||
{
|
||||
return $this->toolbarItem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @internal This is a concrete controller implementation and is not part of TYPO3 Core API.
|
||||
*/
|
||||
enum QrCodeSize: string
|
||||
{
|
||||
case SMALL = 'small';
|
||||
case MEDIUM = 'medium';
|
||||
case LARGE = 'large';
|
||||
case MEGA = 'mega';
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::SMALL => 64,
|
||||
self::MEDIUM => 128,
|
||||
self::LARGE => 256,
|
||||
self::MEGA => 512,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
enum ThumbnailSize: string
|
||||
{
|
||||
case DEFAULT = 'default';
|
||||
case SMALL = 'small';
|
||||
case MEDIUM = 'medium';
|
||||
case LARGE = 'large';
|
||||
|
||||
public function getDimensions(): array
|
||||
{
|
||||
return array_map(static fn(int $size) => $size . 'm', $this->getBaseDimensions());
|
||||
}
|
||||
|
||||
public function getCroppedDimensions(): array
|
||||
{
|
||||
return array_map(static fn(int $size) => $size . 'c', $this->getBaseDimensions());
|
||||
}
|
||||
|
||||
private function getBaseDimensions(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::DEFAULT, self::SMALL => [32, 32],
|
||||
self::MEDIUM => [64, 64],
|
||||
self::LARGE => [96, 96],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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\ToolbarItems;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Backend\Bookmark\BookmarkService;
|
||||
use TYPO3\CMS\Backend\Toolbar\RequestAwareToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
|
||||
/**
|
||||
* Class to render the bookmark menu toolbar.
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class BookmarkToolbarItem implements ToolbarItemInterface, RequestAwareToolbarItemInterface
|
||||
{
|
||||
private ServerRequestInterface $request;
|
||||
|
||||
public function __construct(
|
||||
private readonly BookmarkService $bookmarkService,
|
||||
private readonly BackendViewFactory $backendViewFactory,
|
||||
) {}
|
||||
|
||||
public function setRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has access to this toolbar item.
|
||||
*/
|
||||
public function checkAccess(): bool
|
||||
{
|
||||
return $this->bookmarkService->isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render bookmark icon.
|
||||
*/
|
||||
public function getItem(): string
|
||||
{
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
return $view->render('ToolbarItems/BookmarkToolbarItemItem');
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop-down.
|
||||
*/
|
||||
public function hasDropDown(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop-down content.
|
||||
* The dropdown contains a custom element that fetches data via AJAX.
|
||||
*/
|
||||
public function getDropDown(): string
|
||||
{
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
return $view->render('ToolbarItems/BookmarkToolbarItemDropDown');
|
||||
}
|
||||
|
||||
/**
|
||||
* This toolbar item needs no additional attributes.
|
||||
*/
|
||||
public function getAdditionalAttributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others.
|
||||
*/
|
||||
public function getIndex(): int
|
||||
{
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?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\ToolbarItems;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Toolbar\RequestAwareToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
|
||||
/**
|
||||
* Render cache clearing toolbar item.
|
||||
* Adds a dropdown if there are more than one item to clear (usually for admins to render the flush all caches).
|
||||
* The dropdown items can be manipulated using ModifyClearCacheActionsEvent.
|
||||
*
|
||||
* @phpstan-type CacheAction array{
|
||||
* id: non-empty-string,
|
||||
* endpoint: non-empty-string,
|
||||
* iconIdentifier: non-empty-string,
|
||||
* title: non-empty-string,
|
||||
* description?: non-empty-string,
|
||||
* severity?: 'notice'|'info'|'succcess'|'warning'|'error'|'danger',
|
||||
* }
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class ClearCacheToolbarItem implements ToolbarItemInterface, RequestAwareToolbarItemInterface
|
||||
{
|
||||
/**
|
||||
* @var list<CacheAction>
|
||||
*/
|
||||
protected array $cacheActions = [];
|
||||
|
||||
/**
|
||||
* @var list<non-empty-string>
|
||||
*/
|
||||
protected array $optionValues = [];
|
||||
|
||||
private ServerRequestInterface $request;
|
||||
|
||||
public function __construct(
|
||||
UriBuilder $uriBuilder,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
private readonly BackendViewFactory $backendViewFactory,
|
||||
) {
|
||||
$isAdmin = $this->getBackendUser()->isAdmin();
|
||||
$userTsConfig = $this->getBackendUser()->getTSConfig();
|
||||
|
||||
// Clear all page-related caches
|
||||
if ($isAdmin || ($userTsConfig['options.']['clearCache.']['pages'] ?? false)) {
|
||||
$this->cacheActions[] = [
|
||||
'id' => 'pages',
|
||||
'title' => 'core.cache:group.pages.label',
|
||||
'description' => 'core.cache:group.pages.description',
|
||||
'endpoint' => (string)$uriBuilder->buildUriFromRoute('ajax_clearcache_group_pages'),
|
||||
'severity' => 'success',
|
||||
'iconIdentifier' => 'actions-bolt-alt',
|
||||
];
|
||||
$this->optionValues[] = 'pages';
|
||||
}
|
||||
|
||||
// Clearing of all caches is only shown if explicitly enabled via TSConfig
|
||||
// or if BE-User is admin and the TSconfig explicitly disables the possibility for admins.
|
||||
// This is useful for big production systems where admins accidentally could slow down the system.
|
||||
if (($userTsConfig['options.']['clearCache.']['all'] ?? false)
|
||||
|| ($isAdmin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true))
|
||||
) {
|
||||
$this->cacheActions[] = [
|
||||
'id' => 'all',
|
||||
'title' => 'core.cache:group.all.label',
|
||||
'description' => 'core.cache:group.all.description',
|
||||
'endpoint' => (string)$uriBuilder->buildUriFromRoute('ajax_clearcache_group_all'),
|
||||
'severity' => 'danger',
|
||||
'iconIdentifier' => 'actions-bolt-alt',
|
||||
];
|
||||
$this->optionValues[] = 'all';
|
||||
}
|
||||
|
||||
$event = new ModifyClearCacheActionsEvent($this->cacheActions, $this->optionValues);
|
||||
$event = $eventDispatcher->dispatch($event);
|
||||
$this->cacheActions = $event->getCacheActions();
|
||||
|
||||
$this->optionValues = $event->getCacheActionIdentifiers();
|
||||
}
|
||||
|
||||
public function setRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has access to this toolbar item.
|
||||
*/
|
||||
public function checkAccess(): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
if ($backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
foreach ($this->optionValues as $value) {
|
||||
if ($backendUser->getTSConfig()['options.']['clearCache.'][$value] ?? false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render clear cache icon, based on the option if there is more than one icon or just one.
|
||||
*/
|
||||
public function getItem(): string
|
||||
{
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
if ($this->hasDropDown()) {
|
||||
return $view->render('ToolbarItems/ClearCacheToolbarItem');
|
||||
}
|
||||
$cacheAction = end($this->cacheActions);
|
||||
$view->assignMultiple([
|
||||
'endpoint' => $cacheAction['endpoint'],
|
||||
'title' => $cacheAction['title'],
|
||||
'iconIdentifier' => $cacheAction['iconIdentifier'],
|
||||
]);
|
||||
return $view->render('ToolbarItems/ClearCacheToolbarItemSingle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop-down.
|
||||
*/
|
||||
public function getDropDown(): string
|
||||
{
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
$view->assign('cacheActions', $this->cacheActions);
|
||||
return $view->render('ToolbarItems/ClearCacheToolbarItemDropDown');
|
||||
}
|
||||
|
||||
/**
|
||||
* No additional attributes needed.
|
||||
*/
|
||||
public function getAdditionalAttributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop-down, if there is more than one cache action available for the current Backend user.
|
||||
*/
|
||||
public function hasDropDown(): bool
|
||||
{
|
||||
return count($this->cacheActions) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others
|
||||
*/
|
||||
public function getIndex(): int
|
||||
{
|
||||
return 20;
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?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\ToolbarItems;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Module\ModuleProvider;
|
||||
use TYPO3\CMS\Backend\Toolbar\RequestAwareToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
|
||||
/**
|
||||
* Adds backend live search to the toolbar by adding JavaScript and adding an input search field
|
||||
*/
|
||||
class LiveSearchToolbarItem implements ToolbarItemInterface, RequestAwareToolbarItemInterface
|
||||
{
|
||||
private ServerRequestInterface $request;
|
||||
|
||||
public function __construct(
|
||||
private readonly ModuleProvider $moduleProvider,
|
||||
private readonly BackendViewFactory $backendViewFactory,
|
||||
) {}
|
||||
|
||||
public function setRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has access to this toolbar item.
|
||||
* Live search depends on the records module and only available when that module is allowed.
|
||||
*/
|
||||
public function checkAccess(): bool
|
||||
{
|
||||
return $this->moduleProvider->accessGranted('records', $this->getBackendUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render search field.
|
||||
*/
|
||||
public function getItem(): string
|
||||
{
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
return $view->render('ToolbarItems/LiveSearchToolbarItem');
|
||||
}
|
||||
|
||||
/**
|
||||
* This item needs additional attributes.
|
||||
*/
|
||||
public function getAdditionalAttributes(): array
|
||||
{
|
||||
return ['class' => 't3js-toolbar-item-search'];
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has no drop-down.
|
||||
*/
|
||||
public function hasDropDown(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* No drop-down here.
|
||||
*/
|
||||
public function getDropDown(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others, live search should be very right.
|
||||
*/
|
||||
public function getIndex(): int
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
<?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\ToolbarItems;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Backend\Event\SystemInformationToolbarCollectorEvent;
|
||||
use TYPO3\CMS\Backend\Toolbar\InformationStatus;
|
||||
use TYPO3\CMS\Backend\Toolbar\RequestAwareToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\CommandUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Render system information toolbar item and drop-down.
|
||||
* Provides some events for other extensions to add information.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class SystemInformationToolbarItem implements ToolbarItemInterface, RequestAwareToolbarItemInterface
|
||||
{
|
||||
private ServerRequestInterface $request;
|
||||
protected array $systemInformation = [];
|
||||
protected InformationStatus $highestSeverity;
|
||||
protected string $severityBadgeClass = '';
|
||||
protected array $systemMessages = [];
|
||||
protected int $systemMessageTotalCount = 0;
|
||||
|
||||
public function __construct(
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly Typo3Version $typo3Version,
|
||||
private readonly BackendViewFactory $backendViewFactory,
|
||||
) {
|
||||
$this->highestSeverity = InformationStatus::INFO;
|
||||
}
|
||||
|
||||
public function setRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a system message.
|
||||
* This is a callback method for signal receivers.
|
||||
*
|
||||
* @param string $text The text to be displayed
|
||||
* @param InformationStatus $status The status of this system message
|
||||
* @param int $count Will be added to the total count
|
||||
* @param string $module The associated module
|
||||
* @param string $params Query string with additional parameters
|
||||
*/
|
||||
public function addSystemMessage($text, InformationStatus $status = InformationStatus::OK, $count = 0, $module = '', $params = ''): void
|
||||
{
|
||||
$this->systemMessageTotalCount += $count;
|
||||
|
||||
// define the severity for the badge
|
||||
if ($status->isGreaterThan($this->highestSeverity)) {
|
||||
$this->highestSeverity = $status;
|
||||
}
|
||||
|
||||
$this->systemMessages[] = [
|
||||
'module' => $module,
|
||||
'params' => $params,
|
||||
'count' => $count,
|
||||
'status' => $status->value,
|
||||
'text' => $text,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a system information.
|
||||
* This is a callback method for signal receivers.
|
||||
*
|
||||
* @param string $title The title of this system information, typically a LLL:EXT:... label string
|
||||
* @param string $value The associated value
|
||||
* @param string $iconIdentifier The icon identifier
|
||||
* @param InformationStatus $status The status of this system information
|
||||
*/
|
||||
public function addSystemInformation($title, $value, $iconIdentifier, InformationStatus $status = InformationStatus::NOTICE): void
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => $title,
|
||||
'value' => $value,
|
||||
'iconIdentifier' => $iconIdentifier,
|
||||
'status' => $status->value,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has access to this toolbar item.
|
||||
*/
|
||||
public function checkAccess(): bool
|
||||
{
|
||||
return $this->getBackendUserAuthentication()->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render system information dropdown.
|
||||
*/
|
||||
public function getItem(): string
|
||||
{
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
return $view->render('ToolbarItems/SystemInformationToolbarItem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop-down
|
||||
*/
|
||||
public function getDropDown(): string
|
||||
{
|
||||
if (!$this->checkAccess()) {
|
||||
return '';
|
||||
}
|
||||
$this->collectInformation();
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
$view->assignMultiple([
|
||||
'messages' => $this->systemMessages,
|
||||
'count' => $this->systemMessageTotalCount > 99 ? '99+' : $this->systemMessageTotalCount,
|
||||
'severityBadgeClass' => $this->severityBadgeClass,
|
||||
'systemInformation' => $this->systemInformation,
|
||||
]);
|
||||
return $view->render('ToolbarItems/SystemInformationDropDown');
|
||||
}
|
||||
|
||||
/**
|
||||
* No additional attributes needed.
|
||||
*/
|
||||
public function getAdditionalAttributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop-down.
|
||||
*/
|
||||
public function hasDropDown(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others
|
||||
*/
|
||||
public function getIndex(): int
|
||||
{
|
||||
return 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the information for the drop-down.
|
||||
*/
|
||||
protected function collectInformation(): void
|
||||
{
|
||||
$this->addTypo3Version();
|
||||
$this->addInstallationMode();
|
||||
$this->addWebServer();
|
||||
$this->addPhpVersion();
|
||||
$this->addDebugger();
|
||||
$this->addDatabase();
|
||||
$this->addApplicationContext();
|
||||
$this->addGitRevision();
|
||||
$this->addOperatingSystem();
|
||||
$this->eventDispatcher->dispatch(new SystemInformationToolbarCollectorEvent($this));
|
||||
$this->severityBadgeClass = $this->highestSeverity !== InformationStatus::NOTICE ? 'badge-' . $this->highestSeverity->value : '';
|
||||
}
|
||||
|
||||
protected function addTypo3Version(): void
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.typo3-version',
|
||||
'value' => $this->typo3Version->getVersion(),
|
||||
'iconIdentifier' => 'information-typo3-version',
|
||||
];
|
||||
}
|
||||
|
||||
protected function addInstallationMode(): void
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.installationMethod',
|
||||
'value' => Environment::isComposerMode()
|
||||
? $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.installationMethod.composer')
|
||||
: $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.installationMethod.classic'),
|
||||
'iconIdentifier' => 'actions-package',
|
||||
];
|
||||
}
|
||||
|
||||
protected function addWebServer(): void
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.webserver',
|
||||
'value' => $_SERVER['SERVER_SOFTWARE'] ?? '',
|
||||
'iconIdentifier' => 'information-webserver',
|
||||
];
|
||||
}
|
||||
|
||||
protected function addPhpVersion(): void
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.phpversion',
|
||||
'value' => PHP_VERSION,
|
||||
'iconIdentifier' => 'information-php-version',
|
||||
];
|
||||
}
|
||||
|
||||
protected function addDebugger(): void
|
||||
{
|
||||
$knownDebuggers = ['xdebug', 'Zend Debugger'];
|
||||
foreach ($knownDebuggers as $debugger) {
|
||||
if (extension_loaded($debugger)) {
|
||||
$debuggerVersion = phpversion($debugger) ?: '';
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.debugger',
|
||||
'value' => sprintf('%s %s', $debugger, $debuggerVersion),
|
||||
'iconIdentifier' => 'information-debugger',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function addDatabase(): void
|
||||
{
|
||||
foreach (GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames() as $connectionName) {
|
||||
$serverVersion = '[' . $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.database.offline') . ']';
|
||||
$success = true;
|
||||
try {
|
||||
$serverVersion = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionByName($connectionName)
|
||||
->getPlatformServerVersion();
|
||||
} catch (\Exception $exception) {
|
||||
$success = false;
|
||||
}
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.database',
|
||||
'titleAddition' => $connectionName,
|
||||
'value' => $serverVersion,
|
||||
'status' => $success ? InformationStatus::NOTICE->value : InformationStatus::ERROR->value,
|
||||
'iconIdentifier' => 'information-database',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function addApplicationContext(): void
|
||||
{
|
||||
$applicationContext = Environment::getContext();
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.applicationcontext',
|
||||
'value' => (string)$applicationContext,
|
||||
'status' => $applicationContext->isProduction() ? InformationStatus::NOTICE->value : InformationStatus::WARNING->value,
|
||||
'iconIdentifier' => 'information-application-context',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current GIT revision and branch
|
||||
*/
|
||||
protected function addGitRevision(): void
|
||||
{
|
||||
if (!str_ends_with($this->typo3Version->getVersion(), '-dev') || $this->isFunctionDisabled('exec')) {
|
||||
return;
|
||||
}
|
||||
// check if git exists
|
||||
$returnCode = 0;
|
||||
CommandUtility::exec('git --version', $_, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
// git is not available
|
||||
return;
|
||||
}
|
||||
|
||||
$revision = CommandUtility::exec('git rev-parse --short HEAD');
|
||||
$branch = CommandUtility::exec('git rev-parse --abbrev-ref HEAD');
|
||||
if ($revision === false || $branch === false) {
|
||||
return;
|
||||
}
|
||||
$revision = trim($revision);
|
||||
$branch = trim($branch);
|
||||
if ($revision !== '' && $branch !== '') {
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.gitrevision',
|
||||
'value' => sprintf('%s [%s]', $revision, $branch),
|
||||
'iconIdentifier' => 'information-git',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the system kernel and version
|
||||
*/
|
||||
protected function addOperatingSystem(): void
|
||||
{
|
||||
switch (PHP_OS_FAMILY) {
|
||||
case 'Linux':
|
||||
$icon = 'linux';
|
||||
break;
|
||||
case 'Darwin':
|
||||
$icon = 'apple';
|
||||
break;
|
||||
case 'Windows':
|
||||
$icon = 'windows';
|
||||
break;
|
||||
default:
|
||||
$icon = 'unknown';
|
||||
}
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.operatingsystem',
|
||||
'value' => PHP_OS . ' ' . php_uname('r'),
|
||||
'iconIdentifier' => 'information-os-' . $icon,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given PHP function is disabled in the system.
|
||||
*/
|
||||
protected function isFunctionDisabled(string $functionName): bool
|
||||
{
|
||||
$disabledFunctions = GeneralUtility::trimExplode(',', (string)ini_get('disable_functions'));
|
||||
if (!empty($disabledFunctions)) {
|
||||
return in_array($functionName, $disabledFunctions, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getBackendUserAuthentication(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?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\ToolbarItems;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Backend\ColorScheme;
|
||||
use TYPO3\CMS\Backend\Module\ModuleProvider;
|
||||
use TYPO3\CMS\Backend\Toolbar\RequestAwareToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* User toolbar item and drop-down.
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class UserToolbarItem implements ToolbarItemInterface, RequestAwareToolbarItemInterface
|
||||
{
|
||||
private ServerRequestInterface $request;
|
||||
|
||||
public function __construct(
|
||||
private readonly ModuleProvider $moduleProvider,
|
||||
private readonly BackendViewFactory $backendViewFactory,
|
||||
) {}
|
||||
|
||||
public function setRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item is always enabled.
|
||||
*/
|
||||
public function checkAccess(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render username and an icon.
|
||||
*/
|
||||
public function getItem(): string
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
$view->assignMultiple([
|
||||
'currentUser' => $backendUser->user,
|
||||
'switchUserMode' => (int)$backendUser->getOriginalUserIdWhenInSwitchUserMode(),
|
||||
]);
|
||||
return $view->render('ToolbarItems/UserToolbarItem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop-down content.
|
||||
*/
|
||||
public function getDropDown(): string
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
|
||||
$mostRecentUsers = [];
|
||||
if ($backendUser->isAdmin()
|
||||
&& $backendUser->getOriginalUserIdWhenInSwitchUserMode() === null
|
||||
&& isset($backendUser->uc['recentSwitchedToUsers'])
|
||||
&& is_array($backendUser->uc['recentSwitchedToUsers'])
|
||||
) {
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
|
||||
$result = $queryBuilder
|
||||
->select('uid', 'username', 'realName')
|
||||
->from('be_users')
|
||||
->where(
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($backendUser->uc['recentSwitchedToUsers'], Connection::PARAM_INT_ARRAY))
|
||||
)->executeQuery();
|
||||
|
||||
// Flip the array to have a "sorted" list of items
|
||||
$mostRecentUsers = array_flip($backendUser->uc['recentSwitchedToUsers']);
|
||||
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$mostRecentUsers[$row['uid']] = $row;
|
||||
}
|
||||
|
||||
// Remove any item that is not an array (means, the stored uid is not available anymore)
|
||||
$mostRecentUsers = array_filter($mostRecentUsers, is_array(...));
|
||||
|
||||
$availableUsers = array_keys($mostRecentUsers);
|
||||
if (!empty(array_diff($backendUser->uc['recentSwitchedToUsers'], $availableUsers))) {
|
||||
$backendUser->uc['recentSwitchedToUsers'] = $availableUsers;
|
||||
$backendUser->writeUC();
|
||||
}
|
||||
}
|
||||
|
||||
$modules = null;
|
||||
if ($userModule = $this->moduleProvider->getModuleForMenu('user', $backendUser)) {
|
||||
$modules = $userModule->getSubModules();
|
||||
}
|
||||
$helpModules = null;
|
||||
if ($helpModule = $this->moduleProvider->getModuleForMenu('help', $this->getBackendUser())) {
|
||||
$helpModules = $helpModule->getSubModules();
|
||||
}
|
||||
$view = $this->backendViewFactory->create($this->request);
|
||||
$view->assignMultiple([
|
||||
'modules' => $modules,
|
||||
'helpModules' => $helpModules,
|
||||
'switchUserMode' => $this->getBackendUser()->getOriginalUserIdWhenInSwitchUserMode() !== null,
|
||||
'recentUsers' => $mostRecentUsers,
|
||||
'colorSchemeSwitchEnabled' => $this->getColorSchemeSwitchEnabled(),
|
||||
'activeColorScheme' => $backendUser->uc['colorScheme'] ?? 'auto',
|
||||
'colorSchemes' => $this->getColorSchemes(),
|
||||
]);
|
||||
return $view->render('ToolbarItems/UserToolbarItemDropDown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an additional class if user is in "switch user" mode.
|
||||
*/
|
||||
public function getAdditionalAttributes(): array
|
||||
{
|
||||
$result = [
|
||||
'class' => 'toolbar-item-user',
|
||||
];
|
||||
if ($this->getBackendUser()->getOriginalUserIdWhenInSwitchUserMode()) {
|
||||
$result['class'] .= ' su-user';
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop-down.
|
||||
*/
|
||||
public function hasDropDown(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others.
|
||||
*/
|
||||
public function getIndex(): int
|
||||
{
|
||||
return 90;
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
protected function getColorSchemeSwitchEnabled(): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$userTS = $backendUser->getTSConfig();
|
||||
|
||||
return !isset($userTS['setup.']['fields.']['colorScheme.']['disabled']) || $userTS['setup.']['fields.']['colorScheme.']['disabled'] !== '1';
|
||||
}
|
||||
|
||||
protected function getColorSchemes(): array
|
||||
{
|
||||
$schemes = [];
|
||||
|
||||
foreach (ColorScheme::cases() as $scheme) {
|
||||
$schemeItem = [
|
||||
'label' => $this->getLanguageService()->sL($scheme->getLabel()),
|
||||
'value' => $scheme->value,
|
||||
'icon' => $scheme->getIcon(),
|
||||
];
|
||||
|
||||
$schemes[] = $schemeItem;
|
||||
}
|
||||
|
||||
return $schemes;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user