831 lines
34 KiB
PHP
831 lines
34 KiB
PHP
<?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\Clipboard;
|
|
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use TYPO3\CMS\Backend\Clipboard\Type\CountMode;
|
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
|
use TYPO3\CMS\Core\Authentication\JsConfirmation;
|
|
use TYPO3\CMS\Core\Database\Connection;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
|
use TYPO3\CMS\Core\Imaging\IconFactory;
|
|
use TYPO3\CMS\Core\Imaging\IconSize;
|
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
|
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
|
|
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
|
|
use TYPO3\CMS\Core\Resource\File;
|
|
use TYPO3\CMS\Core\Resource\Folder;
|
|
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
|
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
|
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
|
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
|
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
|
use TYPO3\CMS\Core\Utility\PathUtility;
|
|
|
|
/**
|
|
* TYPO3 clipboard for records and files
|
|
*
|
|
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
|
*/
|
|
class Clipboard
|
|
{
|
|
/**
|
|
* Clipboard data kept here
|
|
*
|
|
* Keys:
|
|
* 'normal'
|
|
* 'tab_[x]' where x is >=1 and denotes the pad-number
|
|
* 'mode' : 'copy' means copy-mode, default = moving ('cut')
|
|
* 'el' : Array of elements:
|
|
* DB: keys = '[tablename]|[uid]' eg. 'tt_content:123'
|
|
* DB: values = 1 (basically insignificant)
|
|
* FILE: keys = '_FILE|[md5 of path]' eg. '_FILE|9ebc7e5c74'
|
|
* FILE: values = The full filepath, eg. '/www/htdocs/typo3/32/dummy/fileadmin/sem1_3_examples/alternative_index.php'
|
|
* or 'C:/www/htdocs/typo3/32/dummy/fileadmin/sem1_3_examples/alternative_index.php'
|
|
*
|
|
* 'current' pointer to current tab (among the above...)
|
|
*
|
|
* The virtual tablename '_FILE' will always indicate files/folders. When checking for elements from eg. 'all tables'
|
|
* (by using an empty string) '_FILE' entries are excluded (so in effect only DB elements are counted)
|
|
*/
|
|
public array $clipData = [];
|
|
|
|
public bool $changed = false;
|
|
|
|
public string $current = '';
|
|
|
|
public bool $lockToNormal = false;
|
|
|
|
public int $numberOfPads = 3;
|
|
|
|
protected ?ServerRequestInterface $request = null;
|
|
|
|
public function __construct(
|
|
protected readonly IconFactory $iconFactory,
|
|
protected readonly UriBuilder $uriBuilder,
|
|
protected readonly ResourceFactory $resourceFactory,
|
|
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
|
) {}
|
|
|
|
/*****************************************
|
|
*
|
|
* Initialize
|
|
*
|
|
****************************************/
|
|
/**
|
|
* Initialize the clipboard from the be_user session
|
|
*/
|
|
public function initializeClipboard(?ServerRequestInterface $request = null): void
|
|
{
|
|
// Initialize the request
|
|
// @todo: Clipboard does two things: It is a repository to find out which records
|
|
// are in the clipboard, and it is a class to help with rendering the
|
|
// clipboard. $request is optional and only used in rendering.
|
|
// It would be better to split these two aspects into single classes.
|
|
$this->request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? null;
|
|
|
|
$userTsConfig = $this->getBackendUser()->getTSConfig();
|
|
// Get data
|
|
$clipData = $this->getBackendUser()->getModuleData('clipboard', !empty($userTsConfig['options.']['saveClipboard']) ? '' : 'ses') ?: [];
|
|
$clipData += ['normal' => []];
|
|
$this->numberOfPads = MathUtility::forceIntegerInRange((int)($userTsConfig['options.']['clipboardNumberPads'] ?? 3), 0, 20);
|
|
// Resets/reinstates the clipboard pads
|
|
$this->clipData['normal'] = is_array($clipData['normal']) ? $clipData['normal'] : [];
|
|
for ($a = 1; $a <= $this->numberOfPads; $a++) {
|
|
$index = 'tab_' . $a;
|
|
$this->clipData[$index] = is_iterable($clipData[$index] ?? null) ? $clipData[$index] : [];
|
|
}
|
|
// Setting the current pad pointer ($this->current))
|
|
$current = (string)($clipData['current'] ?? '');
|
|
$this->current = isset($this->clipData[$current]) ? $current : 'normal';
|
|
$this->clipData['current'] = $this->current;
|
|
}
|
|
|
|
/**
|
|
* Call this method after initialization if you want to lock the clipboard to operate on the normal pad only.
|
|
* Trying to switch pad through ->setCmd will not work.
|
|
* This is used by the clickmenu since it only allows operation on single elements at a time (that is the "normal" pad)
|
|
*/
|
|
public function lockToNormal(): void
|
|
{
|
|
$this->lockToNormal = true;
|
|
$this->current = 'normal';
|
|
}
|
|
|
|
/**
|
|
* The array $cmd may hold various keys which notes some action to take.
|
|
* Normally perform only one action at a time.
|
|
* In scripts like db_list.php / filelist/mod1/index.php the GET-var CB is used to control the clipboard.
|
|
*
|
|
* Selecting / Deselecting elements
|
|
* Array $cmd['el'] has keys = element-ident, value = element value (see description of clipData array in header)
|
|
* Selecting elements for 'copy' should be done by simultaneously setting setCopyMode.
|
|
*
|
|
* @param array $cmd Array of actions, see function description
|
|
*/
|
|
public function setCmd(array $cmd): void
|
|
{
|
|
$cmd['el'] ??= [];
|
|
$cmd['el'] = is_iterable($cmd['el']) ? $cmd['el'] : [];
|
|
foreach ($cmd['el'] as $key => $value) {
|
|
if ($this->current === 'normal') {
|
|
unset($this->clipData['normal']);
|
|
}
|
|
if ($value) {
|
|
$this->clipData[$this->current]['el'][$key] = $value;
|
|
} else {
|
|
$this->removeElement((string)$key);
|
|
}
|
|
$this->changed = true;
|
|
}
|
|
// Change clipboard pad (if not locked to normal)
|
|
if ($cmd['setP'] ?? false) {
|
|
$this->setCurrentPad((string)$cmd['setP']);
|
|
}
|
|
// Remove element (value = item ident: DB; '[tablename]|[uid]' FILE: '_FILE|[md5 hash of path]'
|
|
if ($cmd['remove'] ?? false) {
|
|
$this->removeElement((string)$cmd['remove']);
|
|
$this->changed = true;
|
|
}
|
|
// Remove all on current pad (value = pad-ident)
|
|
if ($cmd['removeAll'] ?? false) {
|
|
$this->clipData[$cmd['removeAll']] = [];
|
|
$this->changed = true;
|
|
}
|
|
// Set copy mode of the tab
|
|
if (isset($cmd['setCopyMode'])) {
|
|
$this->clipData[$this->current]['mode'] = $cmd['setCopyMode'] ? 'copy' : '';
|
|
$this->changed = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setting the current pad on clipboard
|
|
*
|
|
* @param string $padIdentifier Key in the array $this->clipData
|
|
*/
|
|
public function setCurrentPad(string $padIdentifier): void
|
|
{
|
|
// Change clipboard pad (if not locked to normal)
|
|
if (!$this->lockToNormal && $this->current !== $padIdentifier) {
|
|
if (isset($this->clipData[$padIdentifier])) {
|
|
$this->clipData['current'] = ($this->current = $padIdentifier);
|
|
}
|
|
if ($this->current !== 'normal' || !$this->isElements()) {
|
|
$this->clipData[$this->current]['mode'] = '';
|
|
}
|
|
// Setting mode to default (move) if no items on it or if not 'normal'
|
|
$this->changed = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call this after initialization and setCmd in order to save the clipboard to the user session.
|
|
* The function will check if the internal flag ->changed has been set and if so, save the clipboard. Else not.
|
|
*/
|
|
public function endClipboard(): void
|
|
{
|
|
if ($this->changed) {
|
|
$this->saveClipboard();
|
|
}
|
|
$this->changed = false;
|
|
}
|
|
|
|
/**
|
|
* Cleans up an incoming element array $CBarr (Array selecting/deselecting elements)
|
|
*
|
|
* @param array $CBarr Element array from outside ("key" => "selected/deselected")
|
|
* @param string $table The 'table which is allowed'. Must be set.
|
|
* @param bool $removeDeselected Can be set in order to remove entries which are marked for deselection.
|
|
* @return array Processed input $CBarr
|
|
*/
|
|
public function cleanUpCBC(array $CBarr, string $table, bool $removeDeselected = false): array
|
|
{
|
|
foreach ($CBarr as $reference => $value) {
|
|
[$referenceTable] = explode('|', $reference, 2);
|
|
if ($referenceTable !== $table || ($removeDeselected && !$value)) {
|
|
unset($CBarr[$reference]);
|
|
}
|
|
}
|
|
return $CBarr;
|
|
}
|
|
|
|
public function getClipboardData(string $table = ''): array
|
|
{
|
|
$lang = $this->getLanguageService();
|
|
|
|
$clipboardData = [
|
|
'current' => $this->current,
|
|
'copyMode' => $this->currentMode(),
|
|
'elementCount' => count($this->elFromTable($table)),
|
|
];
|
|
|
|
// Initialize tabs by adding the "normal" tab
|
|
$tabs = [
|
|
[
|
|
'identifier' => 'normal',
|
|
'info' => $this->getTabInfo('normal', $table),
|
|
'title' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.normal'),
|
|
'description' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.normal-description'),
|
|
'items' => $this->current === 'normal' ? $this->getTabItems('normal', $table) : [],
|
|
],
|
|
];
|
|
// Add numeric tabs
|
|
for ($a = 1; $a <= $this->numberOfPads; $a++) {
|
|
$tabs[] = [
|
|
'identifier' => 'tab_' . $a,
|
|
'info' => $this->getTabInfo('tab_' . $a, $table),
|
|
'title' => sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cliptabs-name'), (string)$a),
|
|
'description' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cliptabs-description'),
|
|
'items' => $this->current === 'tab_' . $a ? $this->getTabItems('tab_' . $a, $table) : [],
|
|
];
|
|
}
|
|
// Add tabs to clipboard Data
|
|
$clipboardData['tabs'] = $tabs;
|
|
|
|
return $clipboardData;
|
|
}
|
|
|
|
/**
|
|
* Get the items for the given pad identifier
|
|
*
|
|
* @param string $padIdentifier Pad reference
|
|
* @return array The tab items
|
|
*/
|
|
protected function getTabItems(string $padIdentifier, string $currentTable): array
|
|
{
|
|
if (!is_array($this->clipData[$padIdentifier]['el'] ?? false)) {
|
|
return [];
|
|
}
|
|
|
|
$records = [];
|
|
foreach ($this->clipData[$padIdentifier]['el'] as $reference => $value) {
|
|
if (!$value) {
|
|
// Skip element if empty value
|
|
continue;
|
|
}
|
|
[$table, $uid] = explode('|', $reference);
|
|
// Rendering files/directories on the clipboard
|
|
if ($table === '_FILE') {
|
|
$fileObject = $this->resourceFactory->retrieveFileOrFolderObject($value);
|
|
if ($fileObject) {
|
|
$thumb = '';
|
|
$folder = $fileObject instanceof Folder;
|
|
$size = $folder ? '' : '(' . GeneralUtility::formatSize((int)$fileObject->getSize()) . 'bytes)';
|
|
/** @var File $fileObject */
|
|
if (!$folder && ($fileObject->isImage() || $fileObject->isMediaFile())) {
|
|
$processedFile = $fileObject->process(
|
|
ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
|
|
[
|
|
'maxWidth' => 64,
|
|
'maxHeight' => 64,
|
|
]
|
|
);
|
|
$thumb = '<img src="' . htmlspecialchars($processedFile->getPublicUrl() ?? '') . '" '
|
|
. 'width="' . htmlspecialchars((string)$processedFile->getProperty('width')) . '" '
|
|
. 'height="' . htmlspecialchars((string)$processedFile->getProperty('height')) . '" '
|
|
. 'title="' . htmlspecialchars($processedFile->getName()) . '" alt="" loading="lazy" />';
|
|
}
|
|
$linkItemText = BackendUtility::cropToTitleLength($fileObject->getName());
|
|
$combinedIdentifier = $fileObject->getParentFolder()->getCombinedIdentifier();
|
|
$filesRequested = $currentTable === '_FILE';
|
|
$records[] = [
|
|
'identifier' => '_FILE|' . md5($value),
|
|
'icon' => $this->iconFactory
|
|
->getIconForResource($fileObject, IconSize::SMALL)
|
|
->setTitle($fileObject->getName() . ' ' . $size)
|
|
->render(),
|
|
'title' => $this->linkItemText(htmlspecialchars($linkItemText), $combinedIdentifier, $filesRequested),
|
|
'thumb' => $thumb,
|
|
'infoDataDispatch' => [
|
|
'action' => 'TYPO3.InfoWindow.showItem',
|
|
'args' => GeneralUtility::jsonEncodeForHtmlAttribute([$table, $value], false),
|
|
],
|
|
];
|
|
} else {
|
|
// If the file did not exist (or is illegal) then it is removed from the clipboard immediately:
|
|
unset($this->clipData[$padIdentifier]['el'][$reference]);
|
|
$this->changed = true;
|
|
}
|
|
} else {
|
|
// Rendering records:
|
|
$record = BackendUtility::getRecordWSOL($table, (int)$uid);
|
|
if (is_array($record)) {
|
|
$isRequestedTable = $currentTable !== '_FILE';
|
|
$records[] = [
|
|
'identifier' => $table . '|' . $uid,
|
|
'icon' => $this->iconFactory->getIconForRecord($table, $record, IconSize::SMALL)->render(),
|
|
'title' => $this->linkItemText(htmlspecialchars(BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle(
|
|
$table,
|
|
$record
|
|
))), $record, $isRequestedTable),
|
|
'infoDataDispatch' => [
|
|
'action' => 'TYPO3.InfoWindow.showItem',
|
|
'args' => GeneralUtility::jsonEncodeForHtmlAttribute([$table, (int)$uid], false),
|
|
],
|
|
];
|
|
|
|
$localizationData = $this->getLocalizations($table, $record, $isRequestedTable);
|
|
if (!empty($localizationData)) {
|
|
$records = array_merge($records, $localizationData);
|
|
}
|
|
} else {
|
|
unset($this->clipData[$padIdentifier]['el'][$reference]);
|
|
$this->changed = true;
|
|
}
|
|
}
|
|
}
|
|
$this->endClipboard();
|
|
return $records;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the clipboard contains elements
|
|
*/
|
|
public function hasElements(): bool
|
|
{
|
|
foreach ($this->clipData as $data) {
|
|
if (isset($data['el']) && is_array($data['el']) && !empty($data['el'])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gets all localizations of the current record.
|
|
*
|
|
* @param string $table The table
|
|
* @param array $parentRecord The parent record
|
|
* @param bool $isRequestedTable Whether the element is from the requested table
|
|
* @return array HTML table rows
|
|
*/
|
|
protected function getLocalizations(string $table, array $parentRecord, bool $isRequestedTable): array
|
|
{
|
|
if (!$this->tcaSchemaFactory->has($table)) {
|
|
return [];
|
|
}
|
|
$schema = $this->tcaSchemaFactory->get($table);
|
|
if (!$schema->isLanguageAware()) {
|
|
return [];
|
|
}
|
|
|
|
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
|
|
|
$records = [];
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
|
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
|
|
|
$queryBuilder
|
|
->select('*')
|
|
->from($table)
|
|
->where(
|
|
$queryBuilder->expr()->eq(
|
|
$languageCapability->getTranslationOriginPointerField()->getName(),
|
|
$queryBuilder->createNamedParameter((int)$parentRecord['uid'], Connection::PARAM_INT)
|
|
),
|
|
$queryBuilder->expr()->neq(
|
|
$languageCapability->getLanguageField()->getName(),
|
|
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
|
),
|
|
$queryBuilder->expr()->gt(
|
|
'pid',
|
|
$queryBuilder->createNamedParameter(-1, Connection::PARAM_INT)
|
|
)
|
|
)
|
|
->orderBy($languageCapability->getLanguageField()->getName());
|
|
|
|
foreach ($queryBuilder->executeQuery()->fetchAllAssociative() as $record) {
|
|
$title = htmlspecialchars(BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($table, $record)));
|
|
if (!$isRequestedTable) {
|
|
// In case the current table is not the requested table, e.g. "_FILE", wrap title in "muted" style
|
|
$title = '<span class="text-variant">' . $title . '</span>';
|
|
}
|
|
$records[] = [
|
|
'icon' => $this->iconFactory->getIconForRecord($table, $record, IconSize::SMALL)->render(),
|
|
'title' => $title,
|
|
'infoDataDispatch' => [
|
|
'action' => 'TYPO3.InfoWindow.showItem',
|
|
'args' => GeneralUtility::jsonEncodeForHtmlAttribute([$table, (int)$record['uid']], false),
|
|
],
|
|
];
|
|
}
|
|
|
|
return $records;
|
|
}
|
|
|
|
/**
|
|
* Additional information for the tab. This is either
|
|
* the current copyMode (for "normal") or the elements
|
|
* count, for numeric tabs. Latter will not be shown,
|
|
* in case no elements exist for the tab.
|
|
*
|
|
* @param string $padIdentifier Identifier for the clipboard pad
|
|
* @param string $table The table name to count for elements
|
|
*/
|
|
protected function getTabInfo(string $padIdentifier, string $table = ''): string
|
|
{
|
|
$el = count($this->elFromTable($table, $padIdentifier));
|
|
if (!$el) {
|
|
return '';
|
|
}
|
|
$modeLabel = ($this->clipData['normal']['mode'] ?? '') === 'copy' ? $this->clipboardLabel('cm.copy') : $this->clipboardLabel('cm.cut');
|
|
return ' (' . ($padIdentifier === 'normal' ? $modeLabel : htmlspecialchars((string)$el)) . ')';
|
|
}
|
|
|
|
/**
|
|
* Wraps the title of the element in a link to the page/folder where they originate from.
|
|
* Will be wrapped into "muted" style in case the element is not from the currently requested table.
|
|
*
|
|
* @param string $itemText Title of element - must be htmlspecialchar'ed on beforehand.
|
|
* @param array|string $reference If array, a record is expected. If string, its the folders' combined identifier
|
|
* @param bool $isRequestedTable Whether the element is from the requested table
|
|
*/
|
|
protected function linkItemText(string $itemText, $reference, bool $isRequestedTable): string
|
|
{
|
|
if (is_array($reference)) {
|
|
if ($isRequestedTable) {
|
|
// Wrap in link to corresponding page in recordlist in case current requested table matches
|
|
$itemText = '<a href="' . htmlspecialchars((string)$this->uriBuilder->buildUriFromRoute('records', ['id' => $reference['pid']])) . '">' . $itemText . '</a>';
|
|
} else {
|
|
$itemText = '<span class="text-variant">' . $itemText . '</span>';
|
|
}
|
|
} elseif (is_string($reference)) {
|
|
if ($isRequestedTable && ExtensionManagementUtility::isLoaded('filelist')) {
|
|
// Wrap in link to the files folder in case current requested table matches and filelist is loaded
|
|
$itemText = '<a href="' . htmlspecialchars((string)$this->uriBuilder->buildUriFromRoute('media_management', ['id' => $reference])) . '">' . $itemText . '</a>';
|
|
} else {
|
|
$itemText = '<span class="text-variant">' . $itemText . '</span>';
|
|
}
|
|
}
|
|
return $itemText;
|
|
}
|
|
|
|
/**
|
|
* Returns the select-url for database elements
|
|
*
|
|
* @param string $table Table name
|
|
* @param int $uid Uid of record
|
|
* @param bool $copy If set, copymode will be enabled
|
|
* @param bool $deselect If set, the link will deselect, otherwise select.
|
|
* @return string URL linking to the current script but with the CB array set to select the element with table/uid
|
|
*/
|
|
public function selUrlDB(string $table, int $uid, bool $copy = false, bool $deselect = false): string
|
|
{
|
|
return $this->buildUrl(['CB' => [
|
|
'el' => [
|
|
$table . '|' . $uid => $deselect ? 0 : 1,
|
|
],
|
|
'setCopyMode' => (int)$copy,
|
|
]]);
|
|
}
|
|
|
|
/**
|
|
* Returns the select-url for files
|
|
*
|
|
* @param string $path Filepath
|
|
* @param bool $copy If set, copymode will be enabled
|
|
* @param bool $deselect If set, the link will deselect, otherwise select.
|
|
* @return string URL linking to the current script but with the CB array set to select the path
|
|
*/
|
|
public function selUrlFile(string $path, bool $copy = false, bool $deselect = false): string
|
|
{
|
|
return $this->buildUrl(['CB' => [
|
|
'el' => [
|
|
'_FILE|' . md5($path) => $deselect ? '' : $path,
|
|
],
|
|
'setCopyMode' => (int)$copy,
|
|
]]);
|
|
}
|
|
|
|
/**
|
|
* pasteUrl of the element (database and file)
|
|
* For the meaning of $table and $uid, please read from ->makePasteCmdArray!!!
|
|
* The URL will point to tce_file or tce_db depending in $table
|
|
*
|
|
* @param string $table Tablename (_FILE for files)
|
|
* @param string|int $identifier "destination": can be positive or negative indicating how the paste is done
|
|
* (paste into / paste after). For files, this is the combined identifier.
|
|
* @param bool $setRedirect If set, then the redirect URL will point back to the current script, but with CB reset.
|
|
* @param array|null $update Additional key/value pairs which should get set in the moved/copied record (via DataHandler)
|
|
*/
|
|
public function pasteUrl(string $table, $identifier, bool $setRedirect = true, ?array $update = null): string
|
|
{
|
|
$urlParameters = [
|
|
'CB' => [
|
|
'paste' => $table . '|' . $identifier,
|
|
'pad' => $this->current,
|
|
],
|
|
];
|
|
if ($setRedirect) {
|
|
$urlParameters['redirect'] = $this->buildUrl(['CB' => []]);
|
|
}
|
|
if (is_array($update)) {
|
|
$urlParameters['CB']['update'] = $update;
|
|
}
|
|
return (string)$this->uriBuilder->buildUriFromRoute($table === '_FILE' ? 'tce_file' : 'tce_db', $urlParameters);
|
|
}
|
|
|
|
/**
|
|
* Returns confirm JavaScript message
|
|
*
|
|
* @param string $table Table name
|
|
* @param array|string $reference For records its an array, for files its a string (path)
|
|
* @param string $type Type-code
|
|
* @return string the text for a confirm message
|
|
*/
|
|
public function confirmMsgText(
|
|
string $table,
|
|
$reference,
|
|
string $type,
|
|
CountMode $countMode = CountMode::CURRENT,
|
|
): string {
|
|
if (!$this->getBackendUser()->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) {
|
|
return '';
|
|
}
|
|
|
|
$selectedElements = match ($countMode) {
|
|
CountMode::CURRENT => $this->elFromTable($table),
|
|
CountMode::ALL => $this->elFromTable(),
|
|
};
|
|
|
|
$labelKey = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.'
|
|
. ($this->currentMode() === 'copy' ? 'copy' : 'move')
|
|
. ($this->current === 'normal' ? '' : 'cb') . '_' . $type;
|
|
$confirmationMessage = $this->getLanguageService()->sL($labelKey);
|
|
|
|
if ($table === '_FILE' && is_string($reference)) {
|
|
$recordTitle = PathUtility::basename($reference);
|
|
if ($this->current === 'normal') {
|
|
$selectedItem = reset($selectedElements);
|
|
$selectedRecordTitle = PathUtility::basename($selectedItem);
|
|
} else {
|
|
$selectedRecordTitle = (string)count($selectedElements);
|
|
}
|
|
} else {
|
|
$recordTitle = $table === 'pages' && !is_array($reference)
|
|
? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']
|
|
: BackendUtility::getRecordTitle($table, $reference);
|
|
if ($this->current === 'normal') {
|
|
$selectedItem = $this->getSelectedRecord();
|
|
$selectedRecordTitle = $selectedItem['_RECORD_TITLE'];
|
|
} else {
|
|
$selectedRecordTitle = (string)count($selectedElements);
|
|
}
|
|
}
|
|
|
|
return sprintf(
|
|
$confirmationMessage,
|
|
GeneralUtility::fixed_lgd_cs($selectedRecordTitle, 30),
|
|
GeneralUtility::fixed_lgd_cs($recordTitle, 30)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clipboard label - getting from "EXT:core/Resources/Private/Language/locallang_core.xlf:"
|
|
*
|
|
* @param string $key Label Key
|
|
* @return string htmspecialchared' label
|
|
*/
|
|
protected function clipboardLabel(string $key): string
|
|
{
|
|
return htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:' . $key));
|
|
}
|
|
|
|
/*****************************************
|
|
*
|
|
* Helper functions
|
|
*
|
|
****************************************/
|
|
/**
|
|
* Removes element on clipboard
|
|
*
|
|
* @param string $elementKey Key of element in ->clipData array
|
|
*/
|
|
public function removeElement(string $elementKey): void
|
|
{
|
|
unset($this->clipData[$this->current]['el'][$elementKey]);
|
|
$this->changed = true;
|
|
}
|
|
|
|
/**
|
|
* Saves the clipboard, no questions asked.
|
|
* Use ->endClipboard normally (as it checks if changes has been done so saving is necessary)
|
|
*/
|
|
protected function saveClipboard(): void
|
|
{
|
|
$this->getBackendUser()->pushModuleData('clipboard', $this->clipData);
|
|
}
|
|
|
|
/**
|
|
* Returns the current mode, 'copy' or 'cut'
|
|
*
|
|
* @return string "copy" or "cut
|
|
*/
|
|
public function currentMode(): string
|
|
{
|
|
return ($this->clipData[$this->current]['mode'] ?? '') === 'copy' ? 'copy' : 'cut';
|
|
}
|
|
|
|
/**
|
|
* This traverses the elements on the current clipboard pane
|
|
* and unsets elements which does not exist anymore or are disabled.
|
|
*/
|
|
public function cleanCurrent(): void
|
|
{
|
|
if (!is_array($this->clipData[$this->current]['el'] ?? false)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($this->clipData[$this->current]['el'] as $reference => $value) {
|
|
[$table, $uid] = explode('|', $reference);
|
|
$unset = false;
|
|
|
|
if (!$value) {
|
|
$unset = true;
|
|
} elseif ($table === '_FILE') {
|
|
try {
|
|
$fileOrFolder = $this->resourceFactory->retrieveFileOrFolderObject($value);
|
|
|
|
if (($fileOrFolder instanceof File || $fileOrFolder instanceof Folder)
|
|
&& !$fileOrFolder->checkActionPermission('read')
|
|
) {
|
|
$unset = true;
|
|
}
|
|
} catch (InsufficientFolderAccessPermissionsException|ResourceDoesNotExistException) {
|
|
// If either the file has been deleted in the meantime or the user lacks permissions
|
|
// for the folder, we just remove the clipboard entry silently
|
|
$unset = true;
|
|
}
|
|
} elseif (!$this->isRecordAccessAllowed($table, (int)$uid)) {
|
|
$unset = true;
|
|
}
|
|
|
|
if ($unset) {
|
|
$this->removeElement($reference);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function isRecordAccessAllowed(string $table, int $uid): bool
|
|
{
|
|
$row = BackendUtility::getRecord($table, (int)$uid, ['uid', 'pid']);
|
|
if (!is_array($row)) {
|
|
return false;
|
|
}
|
|
|
|
if (!$this->getBackendUser()->check('tables_select', $table)) {
|
|
return false;
|
|
}
|
|
|
|
$schema = $this->tcaSchemaFactory->get($table);
|
|
$rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel);
|
|
|
|
$pid = (int)($table === 'pages' ? $row['uid'] : $row['pid']);
|
|
if ($pid === 0) {
|
|
return $this->getBackendUser()->isAdmin() || $rootLevelCapability->canAccessRecordsOnRootLevel();
|
|
}
|
|
|
|
$page = BackendUtility::getRecord('pages', $pid);
|
|
if (!is_array($page)) {
|
|
return false;
|
|
}
|
|
|
|
if (!$this->getBackendUser()->doesUserHaveAccess($page, Permission::PAGE_SHOW)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Counts the number of elements from the table $matchTable. If $matchTable is blank, all tables (except '_FILE' of course) is counted.
|
|
*
|
|
* @param string $matchTable Table to match/count for.
|
|
* @param string $padIdentifier Can optionally be used to set another pad than the current.
|
|
* @return array Array with keys from the CB.
|
|
*/
|
|
public function elFromTable(string $matchTable = '', string $padIdentifier = ''): array
|
|
{
|
|
$padIdentifier = $padIdentifier ?: $this->current;
|
|
|
|
if (!is_array($this->clipData[$padIdentifier]['el'] ?? false)) {
|
|
return [];
|
|
}
|
|
|
|
$elements = [];
|
|
foreach ($this->clipData[$padIdentifier]['el'] as $reference => $value) {
|
|
if (!$value) {
|
|
continue;
|
|
}
|
|
[$table, $uid] = explode('|', $reference);
|
|
if ($table !== '_FILE') {
|
|
if ((!$matchTable || $table === $matchTable) && $this->tcaSchemaFactory->has($table)) {
|
|
$elements[$reference] = $padIdentifier === 'normal' ? $value : $uid;
|
|
}
|
|
} elseif ($table === $matchTable) {
|
|
$elements[$reference] = $value;
|
|
}
|
|
}
|
|
return $elements;
|
|
}
|
|
|
|
/**
|
|
* Verifies if the item $table/$uid is on the current pad.
|
|
* If the pad is "normal" and the element exists, the mode value is returned.
|
|
* Thus you'll know if the item was copied or cut.
|
|
*
|
|
* @param string $table Table name, (_FILE for files...)
|
|
* @param string|int $identifier Either the records' uid or a filepath
|
|
* @return string If selected the current mode is returned, otherwise an empty string
|
|
*/
|
|
public function isSelected(string $table, $identifier): string
|
|
{
|
|
$key = $table . '|' . $identifier;
|
|
$mode = $this->current === 'normal' ? $this->currentMode() : 'any';
|
|
return !empty($this->clipData[$this->current]['el'][$key]) ? $mode : '';
|
|
}
|
|
|
|
/**
|
|
* Returns the first element on the current clipboard
|
|
* Makes sense only for DB records - not files!
|
|
*
|
|
* @return array Element record with extra field _RECORD_TITLE set to the title of the record
|
|
*/
|
|
public function getSelectedRecord(): array
|
|
{
|
|
$elements = $this->elFromTable();
|
|
reset($elements);
|
|
[$table, $uid] = explode('|', (string)key($elements));
|
|
if (!$this->isSelected($table, (int)$uid)) {
|
|
return [];
|
|
}
|
|
$selectedRecord = BackendUtility::getRecordWSOL($table, (int)$uid);
|
|
$selectedRecord['_RECORD_TITLE'] = BackendUtility::getRecordTitle($table, $selectedRecord);
|
|
return $selectedRecord;
|
|
}
|
|
|
|
/**
|
|
* Reports if the current pad has elements (does not check file/DB type OR if file/DBrecord exists or not. Only counting array)
|
|
*
|
|
* @return bool TRUE if elements exist.
|
|
*/
|
|
protected function isElements(): bool
|
|
{
|
|
return is_array($this->clipData[$this->current]['el'] ?? null) && !empty($this->clipData[$this->current]['el']);
|
|
}
|
|
|
|
protected function getLanguageService(): LanguageService
|
|
{
|
|
return $GLOBALS['LANG'];
|
|
}
|
|
|
|
protected function getBackendUser(): BackendUserAuthentication
|
|
{
|
|
return $GLOBALS['BE_USER'];
|
|
}
|
|
|
|
/**
|
|
* Builds a URL to the current module with the received
|
|
* parameters, merged / replaced by additional parameters.
|
|
*/
|
|
protected function buildUrl(array $parameters = []): string
|
|
{
|
|
if ($this->request === null) {
|
|
throw new \RuntimeException(
|
|
'Request object must be set to generate clipboard URL\'s',
|
|
1633604720
|
|
);
|
|
}
|
|
return (string)$this->uriBuilder->buildUriFromRequest(
|
|
$this->request,
|
|
array_replace($this->request->getQueryParams(), $parameters)
|
|
);
|
|
}
|
|
}
|