TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\ContextMenu;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\ItemProvidersRegistry;
|
||||
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface;
|
||||
|
||||
/**
|
||||
* Class for generating the click menu
|
||||
* @internal
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class ContextMenu
|
||||
{
|
||||
protected ItemProvidersRegistry $itemProvidersRegistry;
|
||||
|
||||
public function __construct(ItemProvidersRegistry $itemProvidersRegistry)
|
||||
{
|
||||
$this->itemProvidersRegistry = $itemProvidersRegistry;
|
||||
}
|
||||
|
||||
public function getItems(string $table, string $identifier, string $context = ''): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($this->getAvailableProviders($table, $identifier, $context) as $provider) {
|
||||
$items = $provider->addItems($items);
|
||||
}
|
||||
return $this->cleanItems($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ProviderInterface[]
|
||||
*/
|
||||
protected function getAvailableProviders(string $table, string $identifier, string $context): array
|
||||
{
|
||||
$providers = $this->itemProvidersRegistry->getItemProviders();
|
||||
$availableProviders = [];
|
||||
foreach ($providers as $provider) {
|
||||
$provider->setContext($table, $identifier, $context);
|
||||
if ($provider->canHandle()) {
|
||||
$priority = $provider->getPriority();
|
||||
$availableProviders[$priority] = $provider;
|
||||
}
|
||||
}
|
||||
krsort($availableProviders);
|
||||
return $availableProviders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up double dividers.
|
||||
* Don't render menu when there are no item or submenu.
|
||||
*/
|
||||
protected function cleanItems(array $items): array
|
||||
{
|
||||
$canRender = false;
|
||||
$prevItemWasDivider = false;
|
||||
|
||||
foreach ($items as $key => $item) {
|
||||
// Assign the key as the identifier for each item.
|
||||
// This is needed for the JavaScript to render a single node
|
||||
$items[$key]['identifier'] = $key;
|
||||
|
||||
if ($item['type'] === 'item') {
|
||||
$canRender = true;
|
||||
$prevItemWasDivider = false;
|
||||
continue;
|
||||
}
|
||||
if ($item['type'] === 'divider') {
|
||||
if ($prevItemWasDivider === true) {
|
||||
unset($items[$key]);
|
||||
} else {
|
||||
$prevItemWasDivider = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($item['type'] === 'submenu') {
|
||||
$childItems = $this->cleanItems($item['childItems']);
|
||||
if (empty($childItems)) {
|
||||
unset($items[$key]);
|
||||
} else {
|
||||
$items[$key]['childItems'] = $childItems;
|
||||
$canRender = true;
|
||||
$prevItemWasDivider = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($canRender) {
|
||||
//Remove first and last divider
|
||||
$fistItem = reset($items);
|
||||
if ($fistItem['type'] === 'divider') {
|
||||
$key = key($items);
|
||||
unset($items[$key]);
|
||||
}
|
||||
$lastItem = end($items);
|
||||
if ($lastItem['type'] === 'divider') {
|
||||
$key = key($items);
|
||||
unset($items[$key]);
|
||||
}
|
||||
} else {
|
||||
//no menu when there are no item or submenu
|
||||
$items = [];
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
use TYPO3\CMS\Backend\Clipboard\Clipboard;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Abstract provider is a base class for context menu item providers
|
||||
*/
|
||||
class AbstractProvider implements ProviderInterface
|
||||
{
|
||||
/**
|
||||
* Language Service property. Used to access localized labels
|
||||
*
|
||||
* @var LanguageService
|
||||
*/
|
||||
protected $languageService;
|
||||
|
||||
/**
|
||||
* @var BackendUserAuthentication
|
||||
*/
|
||||
protected $backendUser;
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Backend\Clipboard\Clipboard
|
||||
*/
|
||||
protected $clipboard;
|
||||
|
||||
/**
|
||||
* Array of items the class is providing
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $itemsConfiguration = [];
|
||||
|
||||
/**
|
||||
* Click menu items disabled by TSConfig
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $disabledItems = [];
|
||||
|
||||
/**
|
||||
* Current table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = '';
|
||||
|
||||
/**
|
||||
* @var string clicked record identifier (usually uid or file combined identifier)
|
||||
*/
|
||||
protected $identifier = '';
|
||||
|
||||
/**
|
||||
* Context - from where the click menu was triggered (e.g. 'tree')
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $context = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->languageService = $GLOBALS['LANG'];
|
||||
$this->backendUser = $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
public function setContext(string $table, string $identifier, string $context = ''): void
|
||||
{
|
||||
$this->table = $table;
|
||||
$this->identifier = $identifier;
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider initialization, heavy stuff
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
$this->initClipboard();
|
||||
$this->initDisabledItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider priority which is used for determining the order in which providers are adding items
|
||||
* to the result array. Highest priority means provider is evaluated first.
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this provider can handle given request (usually a check based on table, uid and context)
|
||||
*/
|
||||
public function canHandle(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize clipboard object - necessary for all copy/cut/paste operations
|
||||
*/
|
||||
protected function initClipboard()
|
||||
{
|
||||
$clipboard = GeneralUtility::makeInstance(Clipboard::class);
|
||||
$clipboard->initializeClipboard();
|
||||
// This locks the clipboard to the Normal for this request.
|
||||
$clipboard->lockToNormal();
|
||||
// This removes all no longer existing elements
|
||||
$clipboard->cleanCurrent();
|
||||
// This stores the changed clipboard data
|
||||
$clipboard->endClipboard();
|
||||
$this->clipboard = $clipboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills $this->disabledItems with the values from TSConfig.
|
||||
* Disabled items can be set separately for each context.
|
||||
*/
|
||||
protected function initDisabledItems()
|
||||
{
|
||||
if ($this->context) {
|
||||
$tsConfigValue = $this->backendUser->getTSConfig()['options.']['contextMenu.']['table.'][$this->table . '.'][$this->context . '.']['disableItems'] ?? '';
|
||||
} else {
|
||||
$tsConfigValue = $this->backendUser->getTSConfig()['options.']['contextMenu.']['table.'][$this->table . '.']['disableItems'] ?? '';
|
||||
}
|
||||
$this->disabledItems = GeneralUtility::trimExplode(',', $tsConfigValue, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new items to the given array or modifies existing items
|
||||
*/
|
||||
public function addItems(array $items): array
|
||||
{
|
||||
$this->initialize();
|
||||
$items += $this->prepareItems($this->itemsConfiguration);
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts item configuration (from $this->itemsConfiguration) into an array ready for returning by controller
|
||||
*/
|
||||
protected function prepareItems(array $itemsConfiguration): array
|
||||
{
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$items = [];
|
||||
foreach ($itemsConfiguration as $name => $configuration) {
|
||||
$type = !empty($configuration['type']) ? $configuration['type'] : 'item';
|
||||
if ($this->canRender($name, $type)) {
|
||||
$items[$name] = [
|
||||
'type' => $type,
|
||||
'label' => !empty($configuration['label']) ? htmlspecialchars($this->languageService->sL($configuration['label'])) : '',
|
||||
'icon' => !empty($configuration['iconIdentifier']) ? $iconFactory->getIcon($configuration['iconIdentifier'], IconSize::SMALL)->render('inline') : '',
|
||||
'additionalAttributes' => $this->getAdditionalAttributes($name),
|
||||
'callbackAction' => !empty($configuration['callbackAction']) ? $configuration['callbackAction'] : '',
|
||||
];
|
||||
if ($type === 'submenu') {
|
||||
$items[$name]['childItems'] = $this->prepareItems($configuration['childItems']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of additional attributes for given item. Additional attributes are used to pass item specific data
|
||||
* to the JS. E.g. message for the delete confirmation dialog
|
||||
*/
|
||||
protected function getAdditionalAttributes(string $itemName): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether certain item can be rendered (e.g. check for disabled items or permissions)
|
||||
*/
|
||||
protected function canRender(string $itemName, string $type): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clicked record identifier
|
||||
*/
|
||||
protected function getIdentifier(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
/**
|
||||
* Registry class for context menu item provider
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ItemProvidersRegistry
|
||||
{
|
||||
protected array $itemProviders = [];
|
||||
|
||||
public function __construct(iterable $itemProviders)
|
||||
{
|
||||
foreach ($itemProviders as $itemProvider) {
|
||||
if ($itemProvider instanceof ProviderInterface) {
|
||||
$this->itemProviders[] = $itemProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered item providers
|
||||
*
|
||||
* @return ProviderInterface[]
|
||||
*/
|
||||
public function getItemProviders(): array
|
||||
{
|
||||
return $this->itemProviders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
|
||||
use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Context menu item provider for pages table
|
||||
*/
|
||||
class PageProvider extends RecordProvider
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'pages';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $itemsConfiguration = [
|
||||
'view' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view',
|
||||
'iconIdentifier' => 'actions-view-page',
|
||||
'callbackAction' => 'viewRecord',
|
||||
],
|
||||
'edit' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit',
|
||||
'iconIdentifier' => 'actions-page-open',
|
||||
'callbackAction' => 'editRecord',
|
||||
],
|
||||
'new' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.newSubpage',
|
||||
'iconIdentifier' => 'actions-page-new',
|
||||
'callbackAction' => 'newRecord',
|
||||
],
|
||||
'info' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info',
|
||||
'iconIdentifier' => 'actions-document-info',
|
||||
'callbackAction' => 'openInfoPopUp',
|
||||
],
|
||||
'qrcode' => [
|
||||
'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:showPageQrCode',
|
||||
'iconIdentifier' => 'actions-qrcode',
|
||||
'callbackAction' => 'showQrCode',
|
||||
],
|
||||
'divider1' => [
|
||||
'type' => 'divider',
|
||||
],
|
||||
'copy' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
|
||||
'iconIdentifier' => 'actions-edit-copy',
|
||||
'callbackAction' => 'copy',
|
||||
],
|
||||
'copyRelease' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
|
||||
'iconIdentifier' => 'actions-edit-copy-release',
|
||||
'callbackAction' => 'clipboardRelease',
|
||||
],
|
||||
'cut' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut',
|
||||
'iconIdentifier' => 'actions-edit-cut',
|
||||
'callbackAction' => 'cut',
|
||||
],
|
||||
'cutRelease' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease',
|
||||
'iconIdentifier' => 'actions-edit-cut-release',
|
||||
'callbackAction' => 'clipboardRelease',
|
||||
],
|
||||
'pasteAfter' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter',
|
||||
'iconIdentifier' => 'actions-document-paste-after',
|
||||
'callbackAction' => 'pasteAfter',
|
||||
],
|
||||
'pasteInto' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteinto',
|
||||
'iconIdentifier' => 'actions-document-paste-into',
|
||||
'callbackAction' => 'pasteInto',
|
||||
],
|
||||
'divider2' => [
|
||||
'type' => 'divider',
|
||||
],
|
||||
'more' => [
|
||||
'type' => 'submenu',
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more',
|
||||
'iconIdentifier' => '',
|
||||
'callbackAction' => 'openSubmenu',
|
||||
'childItems' => [
|
||||
'pagesSort' => [
|
||||
'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_sort.xlf:title',
|
||||
'iconIdentifier' => 'actions-page-move',
|
||||
'callbackAction' => 'pagesSort',
|
||||
],
|
||||
'pagesNewMultiple' => [
|
||||
'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_new.xlf:title',
|
||||
'iconIdentifier' => 'apps-pagetree-drag-move-between',
|
||||
'callbackAction' => 'pagesNewMultiple',
|
||||
],
|
||||
'mountAsTreeRoot' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint',
|
||||
'iconIdentifier' => 'actions-pagetree-mountroot',
|
||||
'callbackAction' => 'mountAsTreeRoot',
|
||||
],
|
||||
'showInMenus' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_showInMenus',
|
||||
'iconIdentifier' => 'actions-view',
|
||||
'callbackAction' => 'showInMenus',
|
||||
],
|
||||
'hideInMenus' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_hideInMenus',
|
||||
'iconIdentifier' => 'actions-ban',
|
||||
'callbackAction' => 'hideInMenus',
|
||||
],
|
||||
],
|
||||
],
|
||||
'divider3' => [
|
||||
'type' => 'divider',
|
||||
],
|
||||
'enable' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable',
|
||||
'iconIdentifier' => 'actions-edit-unhide',
|
||||
'callbackAction' => 'enableRecord',
|
||||
],
|
||||
'disable' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable',
|
||||
'iconIdentifier' => 'actions-edit-hide',
|
||||
'callbackAction' => 'disableRecord',
|
||||
],
|
||||
'delete' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete',
|
||||
'iconIdentifier' => 'actions-edit-delete',
|
||||
'callbackAction' => 'deleteRecord',
|
||||
],
|
||||
'history' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history',
|
||||
'iconIdentifier' => 'actions-document-history-open',
|
||||
'callbackAction' => 'openHistoryPopUp',
|
||||
],
|
||||
'clearCache' => [
|
||||
'label' => 'core.cache:page.label',
|
||||
'iconIdentifier' => 'actions-system-cache-clear',
|
||||
'callbackAction' => 'clearCache',
|
||||
],
|
||||
];
|
||||
|
||||
protected bool $languageAccess = false;
|
||||
|
||||
/**
|
||||
* Checks if the provider can add items to the menu
|
||||
*/
|
||||
public function canHandle(): bool
|
||||
{
|
||||
return $this->table === 'pages';
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
protected function canRender(string $itemName, string $type): bool
|
||||
{
|
||||
if (in_array($type, ['divider', 'submenu'], true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($itemName, $this->disabledItems, true)) {
|
||||
return false;
|
||||
}
|
||||
$canRender = false;
|
||||
switch ($itemName) {
|
||||
case 'view':
|
||||
case 'qrcode':
|
||||
$canRender = $this->canBeViewed();
|
||||
break;
|
||||
case 'edit':
|
||||
$canRender = $this->canBeEdited();
|
||||
break;
|
||||
case 'new':
|
||||
case 'pagesNewMultiple':
|
||||
$canRender = $this->canBeCreated();
|
||||
break;
|
||||
case 'info':
|
||||
$canRender = $this->canShowInfo();
|
||||
break;
|
||||
case 'enable':
|
||||
$canRender = $this->canBeEnabled();
|
||||
break;
|
||||
case 'disable':
|
||||
$canRender = $this->canBeDisabled();
|
||||
break;
|
||||
case 'showInMenus':
|
||||
$canRender = $this->canBeToggled('nav_hide', 1);
|
||||
break;
|
||||
case 'hideInMenus':
|
||||
$canRender = $this->canBeToggled('nav_hide', 0);
|
||||
break;
|
||||
case 'delete':
|
||||
$canRender = $this->canBeDeleted();
|
||||
break;
|
||||
case 'history':
|
||||
$canRender = $this->canShowHistory();
|
||||
break;
|
||||
case 'pagesSort':
|
||||
$canRender = $this->canBeSorted();
|
||||
break;
|
||||
case 'mountAsTreeRoot':
|
||||
$canRender = !$this->isRoot();
|
||||
break;
|
||||
case 'copy':
|
||||
$canRender = $this->canBeCopied();
|
||||
break;
|
||||
case 'copyRelease':
|
||||
$canRender = $this->isRecordInClipboard('copy');
|
||||
break;
|
||||
case 'cut':
|
||||
$canRender = $this->canBeCut() && !$this->isRecordInClipboard('cut');
|
||||
break;
|
||||
case 'cutRelease':
|
||||
$canRender = $this->isRecordInClipboard('cut');
|
||||
break;
|
||||
case 'pasteAfter':
|
||||
$canRender = $this->canBePastedAfter();
|
||||
break;
|
||||
case 'pasteInto':
|
||||
$canRender = $this->canBePastedInto();
|
||||
break;
|
||||
case 'clearCache':
|
||||
$canRender = $this->canClearCache();
|
||||
break;
|
||||
}
|
||||
return $canRender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves calculated permissions for a page to speed things up
|
||||
*/
|
||||
protected function initPermissions(): void
|
||||
{
|
||||
$this->pagePermissions = new Permission($this->backendUser->calcPerms($this->record));
|
||||
$this->languageAccess = $this->hasLanguageAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user may create pages below the given page
|
||||
*/
|
||||
protected function canBeCreated(): bool
|
||||
{
|
||||
if (!$this->backendUser->checkLanguageAccess(0)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getLanguageField() !== ''
|
||||
&& !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->backendUser->check('tables_modify', $this->table)) {
|
||||
return false;
|
||||
}
|
||||
return $this->hasPagePermission(Permission::PAGE_NEW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has editing rights
|
||||
*/
|
||||
protected function canBeEdited(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
if ($this->isRoot()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessAdminOnly)) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->backendUser->check('tables_modify', $this->table)) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isRecordLocked() && $this->hasPagePermission(Permission::PAGE_EDIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is locked
|
||||
*/
|
||||
protected function isRecordLocked(): bool
|
||||
{
|
||||
return (bool)$this->record[$this->getSchema()->getCapability(TcaSchemaCapability::EditLock)->getFieldName()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to can be cut
|
||||
*/
|
||||
protected function canBeCut(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getLanguageField() !== ''
|
||||
&& !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->backendUser->check('tables_modify', $this->table)) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isWebMount()
|
||||
&& $this->canBeEdited()
|
||||
&& !$this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to be copied
|
||||
*/
|
||||
protected function canBeCopied(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getLanguageField() !== ''
|
||||
&& !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->backendUser->check('tables_select', $this->table)) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isRoot()
|
||||
&& !$this->isWebMount()
|
||||
&& !$this->isRecordInClipboard('copy')
|
||||
&& $this->hasPagePermission(Permission::PAGE_SHOW)
|
||||
&& !$this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if something can be pasted into the node
|
||||
*/
|
||||
protected function canBePastedInto(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
$clipboardElementCount = count($this->clipboard->elFromTable($this->table));
|
||||
|
||||
return $clipboardElementCount
|
||||
&& $this->canBeCreated()
|
||||
&& !$this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if something can be pasted after the node
|
||||
*/
|
||||
protected function canBePastedAfter(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
$clipboardElementCount = count($this->clipboard->elFromTable($this->table));
|
||||
return $clipboardElementCount
|
||||
&& $this->canBeCreated()
|
||||
&& !$this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sub pages of given page can be sorted
|
||||
*/
|
||||
protected function canBeSorted(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
return $this->backendUser->check('tables_modify', $this->table)
|
||||
&& $this->hasPagePermission(Permission::CONTENT_EDIT)
|
||||
&& !$this->isDeletePlaceholder()
|
||||
&& $this->backendUser->workspace === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to be removed
|
||||
*/
|
||||
protected function canBeDeleted(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isRoot()
|
||||
&& !$this->isDeletePlaceholder()
|
||||
&& !$this->isRecordLocked()
|
||||
&& !$this->isDeletionDisabledInTS()
|
||||
&& $this->hasPagePermission(Permission::PAGE_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to be viewed in frontend
|
||||
*/
|
||||
protected function canBeViewed(): bool
|
||||
{
|
||||
return !$this->isRoot()
|
||||
&& !$this->isDeleted()
|
||||
&& $this->previewLinkCanBeBuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to show info
|
||||
*/
|
||||
protected function canShowInfo(): bool
|
||||
{
|
||||
return !$this->isRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has clear cache rights
|
||||
*/
|
||||
protected function canClearCache(): bool
|
||||
{
|
||||
return !$this->isRoot()
|
||||
&& ($this->backendUser->isAdmin() || ($this->backendUser->getTSConfig()['options.']['clearCache.']['pages'] ?? false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this node is deleted.
|
||||
*/
|
||||
protected function isDeleted(): bool
|
||||
{
|
||||
return !empty($this->record['deleted']) || $this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if current record is a root page
|
||||
*/
|
||||
protected function isRoot(): bool
|
||||
{
|
||||
return (int)$this->identifier === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if current record is a web mount
|
||||
*/
|
||||
protected function isWebMount(): bool
|
||||
{
|
||||
return in_array($this->identifier, $this->backendUser->getWebmounts());
|
||||
}
|
||||
|
||||
protected function getAdditionalAttributes(string $itemName): array
|
||||
{
|
||||
$attributes = [];
|
||||
if ($itemName === 'view' || $itemName === 'qrcode') {
|
||||
$attributes += $this->getViewAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'enable' || $itemName === 'disable') {
|
||||
$attributes += $this->getEnableDisableAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'delete') {
|
||||
$attributes += $this->getDeleteAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'pasteInto') {
|
||||
$attributes += $this->getPasteAdditionalAttributes('into');
|
||||
}
|
||||
if ($itemName === 'pasteAfter') {
|
||||
$attributes += $this->getPasteAdditionalAttributes('after');
|
||||
}
|
||||
if ($itemName === 'pagesSort') {
|
||||
$attributes += [
|
||||
'data-pages-sort-url' => (string)$this->uriBuilder->buildUriFromRoute('pages_sort', ['id' => $this->record['uid'] ?? null]),
|
||||
];
|
||||
}
|
||||
if ($itemName === 'pagesNewMultiple') {
|
||||
$attributes += [
|
||||
'data-pages-new-multiple-url' => (string)$this->uriBuilder->buildUriFromRoute('pages_new', ['id' => $this->record['uid'] ?? 0]),
|
||||
];
|
||||
}
|
||||
|
||||
if ($itemName === 'edit') {
|
||||
$attributes = [
|
||||
'data-pages-language-uid' => $this->record[$this->getLanguageField()] ?? null,
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
protected function getPreviewPid(): int
|
||||
{
|
||||
return (int)($this->record[$this->getLanguageField()] ?? 0) === 0 ? (int)$this->record['uid'] : (int)$this->record['l10n_parent'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view link
|
||||
*/
|
||||
protected function getViewLink(): string
|
||||
{
|
||||
return (string)PreviewUriBuilder::create($this->record)->buildUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user has access to this column, the doktype
|
||||
* is not excluded and that it contains the given value.
|
||||
*/
|
||||
protected function canBeToggled(string $fieldName, int $value): bool
|
||||
{
|
||||
if (!$this->languageAccess || $this->isRoot()) {
|
||||
return false;
|
||||
}
|
||||
$field = $this->getSchema()->getField($fieldName);
|
||||
if ($field->supportsAccessControl()
|
||||
&& !$this->isExcludedDoktype()
|
||||
&& $this->backendUser->check('non_exclude_fields', $this->table . ':' . $fieldName)
|
||||
&& $this->backendUser->check('tables_modify', $this->table)
|
||||
) {
|
||||
return (int)$this->record[$fieldName] === $value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a current user has access to the language of the record
|
||||
*
|
||||
* @see BackendUserAuthentication::checkLanguageAccess()
|
||||
*/
|
||||
protected function hasLanguageAccess(): bool
|
||||
{
|
||||
if ($this->backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if (($languageField = $this->getLanguageField()) !== '' && isset($this->record[$languageField])) {
|
||||
return $this->backendUser->checkLanguageAccess((int)$this->record[$languageField]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the page doktype is excluded
|
||||
*/
|
||||
protected function isExcludedDoktype(): bool
|
||||
{
|
||||
$doktypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class);
|
||||
return !$doktypeRegistry->isPageTypeViewable((int)($this->record['doktype'] ?? 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
/**
|
||||
* Interface for context menu items providers
|
||||
*/
|
||||
interface ProviderInterface
|
||||
{
|
||||
public function addItems(array $items): array;
|
||||
|
||||
/**
|
||||
* Returns the priority of the provider. Higher priority value means provider is executed first
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Checks if the provider can add items to the menu
|
||||
*/
|
||||
public function canHandle(): bool;
|
||||
|
||||
/**
|
||||
* Initialize the current context.
|
||||
* This method is called directly after fetching the provider from the container.
|
||||
*/
|
||||
public function setContext(string $table, string $identifier, string $context = ''): void;
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
|
||||
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\JsConfirmation;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchema;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Versioning\VersionState;
|
||||
|
||||
/**
|
||||
* Class responsible for providing click menu items for db records which don't have custom provider (as e.g. pages)
|
||||
*/
|
||||
class RecordProvider extends AbstractProvider
|
||||
{
|
||||
/**
|
||||
* Database record
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $record = [];
|
||||
|
||||
/**
|
||||
* Database record of the page $this->record is placed on
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $pageRecord = [];
|
||||
|
||||
/**
|
||||
* Local cache for the result of BackendUserAuthentication::calcPerms()
|
||||
*
|
||||
* @var Permission
|
||||
*/
|
||||
protected $pagePermissions;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $itemsConfiguration = [
|
||||
'view' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view',
|
||||
'iconIdentifier' => 'actions-view',
|
||||
'callbackAction' => 'viewRecord',
|
||||
],
|
||||
'edit' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit',
|
||||
'iconIdentifier' => 'actions-open',
|
||||
'callbackAction' => 'editRecord',
|
||||
],
|
||||
'new' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.new',
|
||||
'iconIdentifier' => 'actions-plus',
|
||||
'callbackAction' => 'newRecord',
|
||||
],
|
||||
'info' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info',
|
||||
'iconIdentifier' => 'actions-document-info',
|
||||
'callbackAction' => 'openInfoPopUp',
|
||||
],
|
||||
'divider1' => [
|
||||
'type' => 'divider',
|
||||
],
|
||||
'copy' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
|
||||
'iconIdentifier' => 'actions-edit-copy',
|
||||
'callbackAction' => 'copy',
|
||||
],
|
||||
'copyRelease' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
|
||||
'iconIdentifier' => 'actions-edit-copy-release',
|
||||
'callbackAction' => 'clipboardRelease',
|
||||
],
|
||||
'cut' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut',
|
||||
'iconIdentifier' => 'actions-edit-cut',
|
||||
'callbackAction' => 'cut',
|
||||
],
|
||||
'cutRelease' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease',
|
||||
'iconIdentifier' => 'actions-edit-cut-release',
|
||||
'callbackAction' => 'clipboardRelease',
|
||||
],
|
||||
'pasteAfter' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter',
|
||||
'iconIdentifier' => 'actions-document-paste-after',
|
||||
'callbackAction' => 'pasteAfter',
|
||||
],
|
||||
'divider2' => [
|
||||
'type' => 'divider',
|
||||
],
|
||||
'more' => [
|
||||
'type' => 'submenu',
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more',
|
||||
'iconIdentifier' => '',
|
||||
'callbackAction' => 'openSubmenu',
|
||||
'childItems' => [
|
||||
'newWizard' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_newWizard',
|
||||
'iconIdentifier' => 'actions-plus',
|
||||
'callbackAction' => 'newContentWizard',
|
||||
],
|
||||
],
|
||||
],
|
||||
'divider3' => [
|
||||
'type' => 'divider',
|
||||
],
|
||||
'enable' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable',
|
||||
'iconIdentifier' => 'actions-edit-unhide',
|
||||
'callbackAction' => 'enableRecord',
|
||||
],
|
||||
'disable' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable',
|
||||
'iconIdentifier' => 'actions-edit-hide',
|
||||
'callbackAction' => 'disableRecord',
|
||||
],
|
||||
'delete' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete',
|
||||
'iconIdentifier' => 'actions-edit-delete',
|
||||
'callbackAction' => 'deleteRecord',
|
||||
],
|
||||
'history' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history',
|
||||
'iconIdentifier' => 'actions-document-history-open',
|
||||
'callbackAction' => 'openHistoryPopUp',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
protected readonly UriBuilder $uriBuilder,
|
||||
protected readonly LocalizationRepository $localizationRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this provider should kick in
|
||||
*/
|
||||
public function canHandle(): bool
|
||||
{
|
||||
if (in_array($this->table, ['sys_file', 'pages'], true)) {
|
||||
return false;
|
||||
}
|
||||
return $this->tcaSchemaFactory->has($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize db record
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
$this->record = BackendUtility::getRecordWSOL($this->table, (int)$this->identifier);
|
||||
$this->initPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority is set to lower then default value, in order to skip this provider if there is less generic provider available.
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* This provider works as a fallback if there is no provider dedicated for certain table, thus it's only kicking in when $items are empty.
|
||||
*/
|
||||
public function addItems(array $items): array
|
||||
{
|
||||
if (!empty($items)) {
|
||||
return $items;
|
||||
}
|
||||
$this->initialize();
|
||||
return $this->prepareItems($this->itemsConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a given item can be rendered (e.g. user has enough permissions)
|
||||
*/
|
||||
protected function canRender(string $itemName, string $type): bool
|
||||
{
|
||||
if (in_array($type, ['divider', 'submenu'], true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($itemName, $this->disabledItems, true)) {
|
||||
return false;
|
||||
}
|
||||
$canRender = false;
|
||||
switch ($itemName) {
|
||||
case 'view':
|
||||
$canRender = $this->canBeViewed();
|
||||
break;
|
||||
case 'edit':
|
||||
$canRender = $this->canBeEdited();
|
||||
break;
|
||||
case 'new':
|
||||
$canRender = $this->canBeNew();
|
||||
break;
|
||||
case 'newWizard':
|
||||
$canRender = $this->canOpenNewCEWizard();
|
||||
break;
|
||||
case 'info':
|
||||
$canRender = $this->canShowInfo();
|
||||
break;
|
||||
case 'enable':
|
||||
$canRender = $this->canBeEnabled();
|
||||
break;
|
||||
case 'disable':
|
||||
$canRender = $this->canBeDisabled();
|
||||
break;
|
||||
case 'delete':
|
||||
$canRender = $this->canBeDeleted();
|
||||
break;
|
||||
case 'history':
|
||||
$canRender = $this->canShowHistory();
|
||||
break;
|
||||
case 'copy':
|
||||
$canRender = $this->canBeCopied();
|
||||
break;
|
||||
case 'copyRelease':
|
||||
$canRender = $this->isRecordInClipboard('copy');
|
||||
break;
|
||||
case 'cut':
|
||||
$canRender = $this->canBeCut();
|
||||
break;
|
||||
case 'cutRelease':
|
||||
$canRender = $this->isRecordInClipboard('cut');
|
||||
break;
|
||||
case 'pasteAfter':
|
||||
$canRender = $this->canBePastedAfter();
|
||||
break;
|
||||
}
|
||||
return $canRender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves calculated permissions for a page containing given record, to speed things up
|
||||
*/
|
||||
protected function initPermissions()
|
||||
{
|
||||
$this->pageRecord = BackendUtility::getRecord('pages', $this->record['pid']) ?? [];
|
||||
$this->pagePermissions = new Permission($this->backendUser->calcPerms($this->pageRecord));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a current user have access to given permission
|
||||
*
|
||||
* @see BackendUserAuthentication::doesUserHaveAccess()
|
||||
*/
|
||||
protected function hasPagePermission(int $permission): bool
|
||||
{
|
||||
return $this->backendUser->isAdmin() || $this->pagePermissions->isGranted($permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional attributes for JS
|
||||
*/
|
||||
protected function getAdditionalAttributes(string $itemName): array
|
||||
{
|
||||
$attributes = [];
|
||||
if ($itemName === 'view') {
|
||||
$attributes += $this->getViewAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'enable' || $itemName === 'disable') {
|
||||
$attributes += $this->getEnableDisableAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'newWizard' && $this->table === 'tt_content') {
|
||||
$urlParameters = [
|
||||
'id' => $this->record['pid'],
|
||||
'sys_language_uid' => $this->record[$this->getLanguageField()] ?? null,
|
||||
'colPos' => $this->record['colPos'],
|
||||
'uid_pid' => -$this->record['uid'],
|
||||
];
|
||||
$url = (string)$this->uriBuilder->buildUriFromRoute('new_content_element_wizard', $urlParameters);
|
||||
$attributes += [
|
||||
'data-new-wizard-url' => $url,
|
||||
'data-title' => $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:newContentElement'),
|
||||
];
|
||||
}
|
||||
if ($itemName === 'delete') {
|
||||
$attributes += $this->getDeleteAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'pasteAfter') {
|
||||
$attributes += $this->getPasteAdditionalAttributes('after');
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional attributes for the 'view' item
|
||||
*/
|
||||
protected function getViewAdditionalAttributes(): array
|
||||
{
|
||||
$attributes = [];
|
||||
$viewLink = $this->getViewLink();
|
||||
if ($viewLink) {
|
||||
$attributes += [
|
||||
'data-preview-url' => $viewLink,
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional attributes for the hide & unhide items
|
||||
*/
|
||||
protected function getEnableDisableAdditionalAttributes(): array
|
||||
{
|
||||
$hiddenFieldName = '';
|
||||
if (($schema = $this->getSchema())?->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
|
||||
$hiddenFieldName = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
|
||||
}
|
||||
return [
|
||||
'data-disable-field' => $hiddenFieldName,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional attributes for the pasteInto and pasteAfter items
|
||||
*
|
||||
* @param string $type "after" or "into"
|
||||
*/
|
||||
protected function getPasteAdditionalAttributes(string $type): array
|
||||
{
|
||||
$closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel');
|
||||
$okText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:ok');
|
||||
$attributes = [];
|
||||
if ($this->backendUser->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) {
|
||||
$selItem = $this->clipboard->getSelectedRecord();
|
||||
$title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste');
|
||||
|
||||
$confirmMessage = sprintf(
|
||||
$this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.'
|
||||
. ($this->clipboard->currentMode() === 'copy' ? 'copy' : 'move') . '_' . $type),
|
||||
BackendUtility::cropToTitleLength($selItem['_RECORD_TITLE']),
|
||||
BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($this->table, $this->record))
|
||||
);
|
||||
$attributes += [
|
||||
'data-title' => $title,
|
||||
'data-message' => $confirmMessage,
|
||||
'data-button-close-text' => $closeText,
|
||||
'data-button-ok-text' => $okText,
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional data for a "delete" action (confirmation modal title and message)
|
||||
*/
|
||||
protected function getDeleteAdditionalAttributes(): array
|
||||
{
|
||||
$closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel');
|
||||
$okText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete');
|
||||
$attributes = [];
|
||||
if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) {
|
||||
$title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete.title');
|
||||
$recordInfo = BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($this->table, $this->record));
|
||||
if ($this->backendUser->shallDisplayDebugInformation()) {
|
||||
$recordInfo .= ' [' . $this->table . ':' . $this->record['uid'] . ']';
|
||||
}
|
||||
$confirmMessage = sprintf(
|
||||
$this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'),
|
||||
trim($recordInfo)
|
||||
);
|
||||
$confirmMessage .= BackendUtility::referenceCount(
|
||||
$this->table,
|
||||
$this->record['uid'],
|
||||
LF . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord')
|
||||
);
|
||||
$translationCount = count($this->localizationRepository->getRecordTranslations($this->table, $this->record['uid']));
|
||||
if ($translationCount > 0) {
|
||||
$confirmMessage .= LF . sprintf(
|
||||
$this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'),
|
||||
$translationCount
|
||||
);
|
||||
}
|
||||
|
||||
$attributes += [
|
||||
'data-title' => $title,
|
||||
'data-message' => $confirmMessage,
|
||||
'data-button-close-text' => $closeText,
|
||||
'data-button-ok-text' => $okText,
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns id of the Page used for preview
|
||||
*/
|
||||
protected function getPreviewPid(): int
|
||||
{
|
||||
return (int)$this->record['pid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view link
|
||||
*/
|
||||
protected function getViewLink(): string
|
||||
{
|
||||
return (string)PreviewUriBuilder::createForRecordPreview(
|
||||
$this->table,
|
||||
$this->record,
|
||||
$this->pageRecord['uid'] ?? 0
|
||||
)->buildUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to show info
|
||||
*/
|
||||
protected function canShowInfo(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to show info
|
||||
*/
|
||||
protected function canShowHistory(): bool
|
||||
{
|
||||
$userTsConfig = $this->backendUser->getTSConfig();
|
||||
return (bool)trim($userTsConfig['options.']['showHistory.'][$this->table] ?? $userTsConfig['options.']['showHistory'] ?? '1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the record can be previewed in frontend
|
||||
*/
|
||||
protected function canBeViewed(): bool
|
||||
{
|
||||
return $this->previewLinkCanBeBuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a record can be edited
|
||||
*/
|
||||
protected function canBeEdited(): bool
|
||||
{
|
||||
if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessAdminOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$access = !$this->isRecordLocked()
|
||||
&& $this->backendUser->check('tables_modify', $this->table)
|
||||
&& $this->hasPagePermission(Permission::CONTENT_EDIT)
|
||||
&& $this->backendUser->checkRecordEditAccess($this->table, $this->record)->isAllowed;
|
||||
return $access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a record can be created
|
||||
*/
|
||||
protected function canBeNew(): bool
|
||||
{
|
||||
return $this->canBeEdited() && !$this->isRecordATranslation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if disableDelete flag is set in TSConfig for the current table
|
||||
*/
|
||||
protected function isDeletionDisabledInTS(): bool
|
||||
{
|
||||
return (bool)trim(
|
||||
$this->backendUser->getTSConfig()['options.']['disableDelete.'][$this->table]
|
||||
?? $this->backendUser->getTSConfig()['options.']['disableDelete']
|
||||
?? ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has the right to delete the record
|
||||
*/
|
||||
protected function canBeDeleted(): bool
|
||||
{
|
||||
return !$this->isDeletionDisabledInTS()
|
||||
&& !$this->isRecordCurrentBackendUser()
|
||||
&& $this->canBeEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if current record can be unhidden/enabled
|
||||
*/
|
||||
protected function canBeEnabled(): bool
|
||||
{
|
||||
return $this->hasDisableColumnWithValue(1) && $this->canBeEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if current record can be hidden
|
||||
*/
|
||||
protected function canBeDisabled(): bool
|
||||
{
|
||||
return $this->hasDisableColumnWithValue(0)
|
||||
&& !$this->isRecordCurrentBackendUser()
|
||||
&& $this->canBeEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true new content element wizard can be shown
|
||||
*/
|
||||
protected function canOpenNewCEWizard(): bool
|
||||
{
|
||||
return $this->table === 'tt_content' && $this->canBeEdited() && !$this->isRecordATranslation();
|
||||
}
|
||||
|
||||
protected function canBeCopied(): bool
|
||||
{
|
||||
return !$this->isRecordInClipboard('copy')
|
||||
&& !$this->isRecordATranslation();
|
||||
}
|
||||
|
||||
protected function canBeCut(): bool
|
||||
{
|
||||
return !$this->isRecordInClipboard('cut')
|
||||
&& $this->canBeEdited()
|
||||
&& !$this->isRecordATranslation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste after is only shown for records from the same table (comparing record in clipboard and record clicked)
|
||||
*/
|
||||
protected function canBePastedAfter(): bool
|
||||
{
|
||||
$clipboardElementCount = count($this->clipboard->elFromTable($this->table));
|
||||
|
||||
return $clipboardElementCount
|
||||
&& $this->backendUser->check('tables_modify', $this->table)
|
||||
&& $this->hasPagePermission(Permission::CONTENT_EDIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if table have "disable" column (e.g. "hidden"), if user has access to this column
|
||||
* and if it contains given value
|
||||
*/
|
||||
protected function hasDisableColumnWithValue(int $value): bool
|
||||
{
|
||||
if (!$this->getSchema()?->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
|
||||
return false;
|
||||
}
|
||||
$hiddenField = $this->getSchema()->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getField();
|
||||
if (!$hiddenField->supportsAccessControl() || $this->backendUser->check('non_exclude_fields', $this->table . ':' . $hiddenField->getName())) {
|
||||
return (int)($this->record[$hiddenField->getName()] ?? 0) === $value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record is locked if page is locked or page is not locked but record is
|
||||
*/
|
||||
protected function isRecordLocked(): bool
|
||||
{
|
||||
if (($pageSchema = $this->tcaSchemaFactory->get('pages'))->hasCapability(TcaSchemaCapability::EditLock)
|
||||
&& ($this->pageRecord[$pageSchema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (!$this->getSchema()?->hasCapability(TcaSchemaCapability::EditLock)) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$this->record[$this->getSchema()->getCapability(TcaSchemaCapability::EditLock)->getFieldName()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is a current record is a delete placeholder
|
||||
*/
|
||||
protected function isDeletePlaceholder(): bool
|
||||
{
|
||||
return VersionState::tryFrom($this->record['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current record is in the "normal" pad of the clipboard
|
||||
*
|
||||
* @param string $mode "copy", "cut" or '' for any mode
|
||||
*/
|
||||
protected function isRecordInClipboard(string $mode = ''): bool
|
||||
{
|
||||
$isSelected = '';
|
||||
if ($this->clipboard->current === 'normal' && isset($this->record['uid'])) {
|
||||
$isSelected = $this->clipboard->isSelected($this->table, $this->record['uid']);
|
||||
}
|
||||
return $mode === '' ? !empty($isSelected) : $isSelected === $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is a record ia a translation
|
||||
*/
|
||||
protected function isRecordATranslation(): bool
|
||||
{
|
||||
if (!$this->getSchema()?->isLanguageAware()) {
|
||||
return false;
|
||||
}
|
||||
return (int)$this->record[$this->getSchema()->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true in case the current record is the current backend user
|
||||
*/
|
||||
protected function isRecordCurrentBackendUser(): bool
|
||||
{
|
||||
return $this->table === 'be_users' && (int)($this->record['uid'] ?? 0) === $this->backendUser->getUserId();
|
||||
}
|
||||
|
||||
protected function getIdentifier(): string
|
||||
{
|
||||
return $this->record['uid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a view link can be built for the record
|
||||
*/
|
||||
protected function previewLinkCanBeBuild(): bool
|
||||
{
|
||||
return $this->getViewLink() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured language field
|
||||
*/
|
||||
protected function getLanguageField(): string
|
||||
{
|
||||
if (!$this->getSchema()?->isLanguageAware()) {
|
||||
return '';
|
||||
}
|
||||
return $this->getSchema()->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
|
||||
}
|
||||
|
||||
protected function getSchema(): ?TcaSchema
|
||||
{
|
||||
if ($this->tcaSchemaFactory->has($this->table)) {
|
||||
return $this->tcaSchemaFactory->get($this->table);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
|
||||
/**
|
||||
* Context menu item provider adding "Site Settings" and "Edit Site Configuration" for pages that are site roots
|
||||
*/
|
||||
final class SiteSettingsProvider extends AbstractProvider
|
||||
{
|
||||
private const array ITEMS_CONFIGURATION = [
|
||||
'editSiteConfiguration' => [
|
||||
'label' => 'backend.siteconfiguration:contextMenu.editSiteConfiguration',
|
||||
'iconIdentifier' => 'actions-window',
|
||||
'callbackAction' => 'openSiteConfiguration',
|
||||
],
|
||||
'editSiteSettings' => [
|
||||
'label' => 'backend.siteconfiguration:contextMenu.editSiteSettings',
|
||||
'iconIdentifier' => 'actions-window-cog',
|
||||
'callbackAction' => 'openSiteSettings',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly SiteFinder $siteFinder,
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function canHandle(): bool
|
||||
{
|
||||
// Site configuration module requires admin access
|
||||
return $this->table === 'pages' && $this->backendUser->isAdmin();
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
|
||||
public function addItems(array $items): array
|
||||
{
|
||||
$this->initDisabledItems();
|
||||
|
||||
// Add site items after "edit" item
|
||||
$localItems = $this->prepareItems(self::ITEMS_CONFIGURATION);
|
||||
$position = array_search('edit', array_keys($items), true);
|
||||
if ($position !== false) {
|
||||
$items = [
|
||||
...array_slice($items, 0, $position + 1, true),
|
||||
...$localItems,
|
||||
...array_slice($items, $position + 1, null, true),
|
||||
];
|
||||
} else {
|
||||
$items = [...$items, ...$localItems];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
protected function canRender(string $itemName, string $type): bool
|
||||
{
|
||||
if (in_array($itemName, $this->disabledItems, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($itemName === 'editSiteSettings' || $itemName === 'editSiteConfiguration') {
|
||||
return $this->canOpenSiteSettings();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getAdditionalAttributes(string $itemName): array
|
||||
{
|
||||
$pageId = (int)$this->identifier;
|
||||
try {
|
||||
$site = $this->siteFinder->getSiteByRootPageId($pageId);
|
||||
|
||||
if ($itemName === 'editSiteSettings') {
|
||||
return [
|
||||
'data-site-settings-url' => (string)$this->uriBuilder->buildUriFromRoute(
|
||||
'site_configuration.editSettings',
|
||||
['site' => $site->getIdentifier()]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if ($itemName === 'editSiteConfiguration') {
|
||||
return [
|
||||
'data-site-configuration-url' => (string)$this->uriBuilder->buildUriFromRoute(
|
||||
'site_configuration.edit',
|
||||
['site' => $site->getIdentifier()]
|
||||
),
|
||||
];
|
||||
}
|
||||
} catch (SiteNotFoundException) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function canOpenSiteSettings(): bool
|
||||
{
|
||||
// Check if this page is a site root
|
||||
$pageId = (int)$this->identifier;
|
||||
if ($pageId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->siteFinder->getSiteByRootPageId($pageId);
|
||||
return true;
|
||||
} catch (SiteNotFoundException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user