first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,228 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
/*
* 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!
*/
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
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 = '';
/**
* Lightweight constructor, just to be able to call ->canHandle(). Rest of the initialization is done
* in the initialize() method
*
* @param string $table
* @param string $identifier
* @param string $context
*/
public function __construct(string $table, string $identifier, string $context = '')
{
$this->table = $table;
$this->identifier = $identifier;
$this->context = $context;
$this->languageService = $GLOBALS['LANG'];
$this->backendUser = $GLOBALS['BE_USER'];
}
/**
* 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.
*
* @return int
*/
public function getPriority(): int
{
return 100;
}
/**
* Whether this provider can handle given request (usually a check based on table, uid and context)
*
* @return bool
*/
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->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
*
* @param array $items
* @return array
*/
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
*
* @param array $itemsConfiguration
* @return array
*/
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'], Icon::SIZE_SMALL)->render() : '',
'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
*
* @param string $itemName
* @return array
*/
protected function getAdditionalAttributes(string $itemName): array
{
return [];
}
/**
* Checks whether certain item can be rendered (e.g. check for disabled items or permissions)
*
* @param string $itemName
* @param string $type
* @return bool
*/
protected function canRender(string $itemName, string $type): bool
{
return true;
}
/**
* Returns a clicked record identifier
*
* @return string
*/
protected function getIdentifier(): string
{
return '';
}
}
@@ -0,0 +1,565 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
/*
* 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!
*/
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
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.new',
'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'
],
'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' => [
'newWizard' => [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_newWizard',
'iconIdentifier' => 'actions-page-new',
'callbackAction' => 'newPageWizard',
],
'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',
],
'openListModule' => [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_db_list',
'iconIdentifier' => 'actions-system-list-open',
'callbackAction' => 'openListModule',
],
'mountAsTreeRoot' => [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint',
'iconIdentifier' => 'actions-pagetree-mountroot',
'callbackAction' => 'mountAsTreeRoot',
],
],
],
'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' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache',
'iconIdentifier' => 'actions-system-cache-clear',
'callbackAction' => 'clearCache',
],
];
/**
* @var bool
*/
protected $languageAccess = false;
/**
* Checks if the provider can add items to the menu
*
* @return bool
*/
public function canHandle(): bool
{
return $this->table === 'pages';
}
/**
* @return int
*/
public function getPriority(): int
{
return 100;
}
/**
* @param string $itemName
* @param string $type
* @return bool
*/
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':
case 'newWizard':
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 'delete':
$canRender = $this->canBeDeleted();
break;
case 'history':
$canRender = $this->canShowHistory();
break;
case 'openListModule':
$canRender = $this->canOpenListModule();
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()
{
$this->pagePermissions = $this->backendUser->calcPerms($this->record);
$this->languageAccess = $this->hasLanguageAccess();
}
/**
* Checks if the user may create pages below the given page
*
* @return bool
*/
protected function canBeCreated(): bool
{
if (!$this->backendUser->checkLanguageAccess(0)) {
return false;
}
return $this->hasPagePermission(Permission::PAGE_NEW);
}
/**
* Checks if the user has editing rights
*
* @return bool
*/
protected function canBeEdited(): bool
{
if (!$this->languageAccess) {
return false;
}
if ($this->isRoot()) {
return false;
}
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) {
return false;
}
if ($this->backendUser->isAdmin()) {
return true;
}
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) {
return false;
}
return !$this->isRecordLocked() && $this->hasPagePermission(Permission::PAGE_EDIT);
}
/**
* Check if a page is locked
*
* @return bool
*/
protected function isRecordLocked(): bool
{
return (bool)$this->record['editlock'];
}
/**
* Checks if the page is allowed to can be cut
*
* @return bool
*/
protected function canBeCut(): bool
{
if (!$this->languageAccess) {
return false;
}
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['languageField'])
&& !in_array($this->record[$GLOBALS['TCA'][$this->table]['ctrl']['languageField']], [0, -1])
) {
return false;
}
return !$this->isWebMount()
&& $this->canBeEdited()
&& !$this->isDeletePlaceholder();
}
/**
* Checks if the page is allowed to be copied
*
* @return bool
*/
protected function canBeCopied(): bool
{
if (!$this->languageAccess) {
return false;
}
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['languageField'])
&& !in_array($this->record[$GLOBALS['TCA'][$this->table]['ctrl']['languageField']], [0, -1])
) {
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
*
* @return bool
*/
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
*
* @return bool
*/
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
*
* @return bool
*/
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
*
* @return bool
*/
protected function canBeDeleted(): bool
{
if (!$this->languageAccess) {
return false;
}
return !$this->isDeletePlaceholder()
&& !$this->isRecordLocked()
&& !$this->isDeletionDisabledInTS()
&& $this->hasPagePermission(Permission::PAGE_DELETE);
}
/**
* Checks if the page is allowed to be viewed in frontend
*
* @return bool
*/
protected function canBeViewed(): bool
{
return !$this->isRoot() && !$this->isDeleted();
}
/**
* Checks if the page is allowed to show info
*
* @return bool
*/
protected function canShowInfo(): bool
{
return !$this->isRoot();
}
/**
* Checks if the user has clear cache rights
*
* @return bool
*/
protected function canClearCache(): bool
{
return !$this->isRoot()
&& ($this->backendUser->isAdmin() || $this->backendUser->getTSConfig()['options.']['clearCache.']['pages'] ?? false);
}
/**
* Determines whether this node is deleted.
*
* @return bool
*/
protected function isDeleted(): bool
{
return !empty($this->record['deleted']) || $this->isDeletePlaceholder();
}
/**
* Returns true if current record is a root page
*
* @return bool
*/
protected function isRoot()
{
return (int)$this->identifier === 0;
}
/**
* Returns true if current record is a web mount
*
* @return bool
*/
protected function isWebMount()
{
return in_array($this->identifier, $this->backendUser->returnWebmounts());
}
/**
* @param string $itemName
* @return array
*/
protected function getAdditionalAttributes(string $itemName): array
{
$attributes = [];
if ($itemName === 'view') {
$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') {
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$attributes += [
'data-pages-sort-url' => (string)$uriBuilder->buildUriFromRoute('pages_sort', ['id' => $this->record['uid']]),
];
}
if ($itemName === 'pagesNewMultiple') {
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$attributes += [
'data-pages-new-multiple-url' => (string)$uriBuilder->buildUriFromRoute('pages_new', ['id' => $this->record['uid']]),
];
}
if ($itemName === 'edit') {
$attributes = [
'data-pages-language-uid' => $this->record['sys_language_uid']
];
}
return $attributes;
}
/**
* @return int
*/
protected function getPreviewPid(): int
{
return (int)$this->record['sys_language_uid'] === 0 ? (int)$this->record['uid'] : (int)$this->record['l10n_parent'];
}
/**
* Returns the view link
*
* @return string
*/
protected function getViewLink(): string
{
$language = (int)$this->record['sys_language_uid'];
$additionalParams = ($language > 0) ? '&L=' . $language : '';
return BackendUtility::getPreviewUrl(
$this->getPreviewPid(),
'',
null,
'',
'',
$additionalParams
);
}
/**
* Returns true if a current user has access to the language of the record
*
* @see BackendUserAuthentication::checkLanguageAccess()
* @return bool
*/
protected function hasLanguageAccess(): bool
{
if ($this->backendUser->isAdmin()) {
return true;
}
$languageField = $GLOBALS['TCA'][$this->table]['ctrl']['languageField'] ?? '';
if ($languageField !== '' && isset($this->record[$languageField])) {
return $this->backendUser->checkLanguageAccess((int)$this->record[$languageField]);
}
return true;
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
/*
* 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!
*/
/**
* Interface for context menu items providers
*/
interface ProviderInterface
{
/**
* @param array $items
* @return array
*/
public function addItems(array $items): array;
/**
* Returns the priority of the provider. Higher priority value means provider is executed first
*
* @return int
*/
public function getPriority(): int;
/**
* Checks if the provider can add items to the menu
*
* @return bool
*/
public function canHandle(): bool;
}
@@ -0,0 +1,696 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
/*
* 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!
*/
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
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 int
*/
protected $pagePermissions = 0;
/**
* @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-add',
'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-add',
'callbackAction' => 'newContentWizard',
],
'openListModule' => [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_db_list',
'iconIdentifier' => 'actions-system-list-open',
'callbackAction' => 'openListModule',
],
],
],
'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',
],
];
/**
* Whether this provider should kick in
*
* @return bool
*/
public function canHandle(): bool
{
if (in_array($this->table, ['sys_file', 'sys_filemounts', 'sys_file_storage', 'pages'], true)
|| strpos($this->table, '-drag') !== false) {
return false;
}
return isset($GLOBALS['TCA'][$this->table]);
}
/**
* Initialize db record
*/
protected function initialize()
{
parent::initialize();
$this->record = BackendUtility::getRecordWSOL($this->table, $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.
*
* @return int
*/
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.
*
* @param array $items
* @return array
*/
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)
*
* @param string $itemName
* @param string $type
* @return bool
*/
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':
case 'new':
$canRender = $this->canBeEdited();
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 'openListModule':
$canRender = $this->canOpenListModule();
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 = $this->backendUser->calcPerms($this->pageRecord);
}
/**
* Returns true if a current user have access to given permission
*
* @see BackendUserAuthentication::doesUserHaveAccess()
* @param int $permission
* @return bool
*/
protected function hasPagePermission(int $permission): bool
{
return $this->backendUser->isAdmin() || ($this->pagePermissions & $permission) == $permission;
}
/**
* Additional attributes for JS
*
* @param string $itemName
* @return array
*/
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') {
$moduleName = BackendUtility::getPagesTSconfig($this->record['pid'])['mod.']['newContentElementWizard.']['override']
?? 'new_content_element_wizard';
$urlParameters = [
'id' => $this->record['pid'],
'sys_language_uid' => $this->record['sys_language_uid'],
'colPos' => $this->record['colPos'],
'uid_pid' => -$this->record['uid']
];
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$url = (string)$uriBuilder->buildUriFromRoute($moduleName, $urlParameters);
$attributes += [
'data-new-wizard-url' => htmlspecialchars($url),
'data-title' => $this->languageService->getLL('newContentElement'),
];
}
if ($itemName === 'delete') {
$attributes += $this->getDeleteAdditionalAttributes();
}
if ($itemName === 'openListModule') {
$attributes += [
'data-page-uid' => $this->record['pid']
];
}
if ($itemName === 'pasteAfter') {
$attributes += $this->getPasteAdditionalAttributes('after');
}
return $attributes;
}
/**
* Additional attributes for the 'view' item
*
* @return array
*/
protected function getViewAdditionalAttributes(): array
{
$attributes = [];
$viewLink = $this->getViewLink();
if ($viewLink) {
$attributes += [
'data-preview-url' => htmlspecialchars($viewLink),
];
}
return $attributes;
}
/**
* Additional attributes for the hide & unhide items
*
* @return array
*/
protected function getEnableDisableAdditionalAttributes(): array
{
return [
'data-disable-field' => $GLOBALS['TCA'][$this->table]['ctrl']['enablecolumns']['disabled'] ?? ''
];
}
/**
* Additional attributes for the pasteInto and pasteAfter items
*
* @param string $type "after" or "into"
* @return array
*/
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),
GeneralUtility::fixed_lgd_cs($selItem['_RECORD_TITLE'], $this->backendUser->uc['titleLen']),
GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($this->table, $this->record), $this->backendUser->uc['titleLen'])
);
$attributes += [
'data-title' => htmlspecialchars($title),
'data-message' => htmlspecialchars($confirmMessage),
'data-button-close-text' => htmlspecialchars($closeText),
'data-button-ok-text' => htmlspecialchars($okText),
];
}
return $attributes;
}
/**
* Additional data for a "delete" action (confirmation modal title and message)
*
* @return array
*/
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_common.xlf:delete');
$attributes = [];
if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) {
$recordTitle = GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($this->table, $this->record), $this->backendUser->uc['titleLen']);
$title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete');
$confirmMessage = sprintf(
$this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'),
$recordTitle
);
$confirmMessage .= BackendUtility::referenceCount(
$this->table,
$this->record['uid'],
' ' . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord')
);
$confirmMessage .= BackendUtility::translationCount(
$this->table,
$this->record['uid'],
' ' . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
);
$attributes += [
'data-title' => htmlspecialchars($title),
'data-message' => htmlspecialchars($confirmMessage),
'data-button-close-text' => htmlspecialchars($closeText),
'data-button-ok-text' => htmlspecialchars($okText),
];
}
return $attributes;
}
/**
* Returns id of the Page used for preview
*
* @return int
*/
protected function getPreviewPid(): int
{
return (int)$this->record['pid'];
}
/**
* Returns the view link
*
* @return string
*/
protected function getViewLink(): string
{
$anchorSection = '';
$additionalParams = '';
if ($this->table === 'tt_content') {
$anchorSection = '#c' . $this->record['uid'];
$language = (int)$this->record[$GLOBALS['TCA']['tt_content']['ctrl']['languageField']];
if ($language > 0) {
$additionalParams = '&L=' . $language;
}
}
return BackendUtility::getPreviewUrl(
$this->getPreviewPid(),
'',
null,
$anchorSection,
'',
$additionalParams
);
}
/**
* Checks if the page is allowed to show info
*
* @return bool
*/
protected function canShowInfo(): bool
{
return true;
}
/**
* Checks if the page is allowed to show info
*
* @return bool
*/
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
*
* @return bool
*/
protected function canBeViewed(): bool
{
return $this->table === 'tt_content';
}
/**
* Whether a record can be edited
*
* @return bool
*/
protected function canBeEdited(): bool
{
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) {
return false;
}
if ($this->backendUser->isAdmin()) {
return true;
}
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) {
return false;
}
$access = !$this->isRecordLocked()
&& $this->backendUser->check('tables_modify', $this->table)
&& $this->hasPagePermission(Permission::CONTENT_EDIT)
&& $this->backendUser->recordEditAccessInternals($this->table, $this->record);
return $access;
}
/**
* Checks if disableDelete flag is set in TSConfig for the current table
*
* @return bool
*/
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 page
*
* @return bool
*/
protected function canBeDeleted(): bool
{
return !$this->isDeletionDisabledInTS() && $this->canBeEdited();
}
/**
* Returns true if current record can be unhidden/enabled
*
* @return bool
*/
protected function canBeEnabled(): bool
{
return $this->hasDisableColumnWithValue(1) && $this->canBeEdited();
}
/**
* Returns true if current record can be hidden
*
* @return bool
*/
protected function canBeDisabled(): bool
{
return $this->hasDisableColumnWithValue(0) && $this->canBeEdited();
}
/**
* Returns true new content element wizard can be shown
*
* @return bool
*/
protected function canOpenNewCEWizard(): bool
{
return $this->table === 'tt_content'
&& (bool)(BackendUtility::getPagesTSconfig($this->record['pid'])['mod.']['web_layout.']['disableNewContentElementWizard'] ?? true)
&& $this->canBeEdited();
}
/**
* @return bool
*/
protected function canOpenListModule(): bool
{
return $this->backendUser->check('modules', 'web_list');
}
/**
* @return bool
*/
protected function canBeCopied(): bool
{
return !$this->isRecordInClipboard('copy')
&& !$this->isRecordATranslation();
}
/**
* @return bool
*/
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)
*
* @return bool
*/
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
*
* @param int $value
* @return bool
*/
protected function hasDisableColumnWithValue(int $value): bool
{
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['enablecolumns']['disabled'])) {
$hiddenFieldName = $GLOBALS['TCA'][$this->table]['ctrl']['enablecolumns']['disabled'];
if (
$hiddenFieldName !== '' && !empty($GLOBALS['TCA'][$this->table]['columns'][$hiddenFieldName])
&& (
empty($GLOBALS['TCA'][$this->table]['columns'][$hiddenFieldName]['exclude'])
|| $this->backendUser->check('non_exclude_fields', $this->table . ':' . $hiddenFieldName)
)
) {
return (int)$this->record[$hiddenFieldName] === (int)$value;
}
}
return false;
}
/**
* Record is locked if page is locked or page is not locked but record is
*
* @return bool
*/
protected function isRecordLocked(): bool
{
return (int)$this->pageRecord['editlock'] === 1
|| isset($GLOBALS['TCA'][$this->table]['ctrl']['editlock'])
&& (int)$this->record[$GLOBALS['TCA'][$this->table]['ctrl']['editlock']] === 1;
}
/**
* Returns true is a current record is a delete placeholder
*
* @return bool
*/
protected function isDeletePlaceholder(): bool
{
if (!isset($this->record['t3ver_state'])) {
return false;
}
return VersionState::cast($this->record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER);
}
/**
* Checks if current record is in the "normal" pad of the clipboard
*
* @param string $mode "copy", "cut" or '' for any mode
* @return bool
*/
protected function isRecordInClipboard(string $mode = ''): bool
{
$isSelected = '';
if ($this->clipboard->current === 'normal') {
$isSelected = $this->clipboard->isSelected($this->table, $this->record['uid']);
}
return $mode === '' ? !empty($isSelected) : $isSelected === $mode;
}
/**
* Returns true is a record ia a translation
*
* @return bool
*/
protected function isRecordATranslation(): bool
{
return BackendUtility::isTableLocalizable($this->table) && (int)$this->record[$GLOBALS['TCA'][$this->table]['ctrl']['transOrigPointerField']] !== 0;
}
/**
* @return string
*/
protected function getIdentifier(): string
{
return $this->record['uid'];
}
}