=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|[shortmd5 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) * * @var array */ public $clipData = []; /** * @var int */ public $changed = 0; /** * @var string */ public $current = ''; /** * @var int */ public $lockToNormal = 0; /** * If set, clipboard is displaying files. * * @var bool */ public $fileMode = false; /** * @var IconFactory */ protected $iconFactory; /** * @var StandaloneView */ protected $view; /** * Construct * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException * @throws \InvalidArgumentException */ public function __construct() { $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); $this->view = $this->getStandaloneView(); } /***************************************** * * Initialize * ****************************************/ /** * Initialize the clipboard from the be_user session */ public function initializeClipboard() { // Get data $clipData = $this->getBackendUser()->getModuleData('clipboard', $this->getBackendUser()->getTSConfigVal('options.saveClipboard') ? '' : 'ses'); // NumberTabs $clNP = $this->getBackendUser()->getTSConfigVal('options.clipboardNumberPads'); if (MathUtility::canBeInterpretedAsInteger($clNP) && $clNP >= 0) { $this->numberTabs = MathUtility::forceIntegerInRange($clNP, 0, 20); } // Resets/reinstates the clipboard pads $this->clipData['normal'] = is_array($clipData['normal']) ? $clipData['normal'] : []; for ($a = 1; $a <= $this->numberTabs; $a++) { $this->clipData['tab_' . $a] = is_array($clipData['tab_' . $a]) ? $clipData['tab_' . $a] : []; } // Setting the current pad pointer ($this->current)) $this->clipData['current'] = ($this->current = isset($this->clipData[$clipData['current']]) ? $clipData['current'] : 'normal'); } /** * 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() { $this->lockToNormal = 1; $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($cmd) { if (is_array($cmd['el'])) { foreach ($cmd['el'] as $k => $v) { if ($this->current === 'normal') { unset($this->clipData['normal']); } if ($v) { $this->clipData[$this->current]['el'][$k] = $v; } else { $this->removeElement($k); } $this->changed = 1; } } // Change clipboard pad (if not locked to normal) if ($cmd['setP']) { $this->setCurrentPad($cmd['setP']); } // Remove element (value = item ident: DB; '[tablename]|[uid]' FILE: '_FILE|[shortmd5 hash of path]' if ($cmd['remove']) { $this->removeElement($cmd['remove']); $this->changed = 1; } // Remove all on current pad (value = pad-ident) if ($cmd['removeAll']) { $this->clipData[$cmd['removeAll']] = []; $this->changed = 1; } // Set copy mode of the tab if (isset($cmd['setCopyMode'])) { $this->clipData[$this->current]['mode'] = $this->isElements() ? ($cmd['setCopyMode'] ? 'copy' : '') : ''; $this->changed = 1; } } /** * Setting the current pad on clipboard * * @param string $padIdent Key in the array $this->clipData */ public function setCurrentPad($padIdent) { // Change clipboard pad (if not locked to normal) if (!$this->lockToNormal && $this->current != $padIdent) { if (isset($this->clipData[$padIdent])) { $this->clipData['current'] = ($this->current = $padIdent); } 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 = 1; } } /** * 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() { if ($this->changed) { $this->saveClipboard(); } $this->changed = 0; } /** * 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|int $removeDeselected Can be set in order to remove entries which are marked for deselection. * @return array Processed input $CBarr */ public function cleanUpCBC($CBarr, $table, $removeDeselected = 0) { if (is_array($CBarr)) { foreach ($CBarr as $k => $v) { $p = explode('|', $k); if ((string)$p[0] != (string)$table || $removeDeselected && !$v) { unset($CBarr[$k]); } } } return $CBarr; } /***************************************** * * Clipboard HTML renderings * ****************************************/ /** * Prints the clipboard * * @return string HTML output * @throws \BadFunctionCallException */ public function printClipboard() { $languageService = $this->getLanguageService(); $elementCount = count($this->elFromTable($this->fileMode ? '_FILE' : '')); // Copymode Selector menu $copymodeUrl = GeneralUtility::linkThisScript(); $this->view->assign('actionCopyModeUrl', htmlspecialchars(GeneralUtility::quoteJSvalue($copymodeUrl . '&CB[setCopyMode]='))); $this->view->assign('actionCopyModeUrl1', htmlspecialchars(GeneralUtility::quoteJSvalue($copymodeUrl . '&CB[setCopyMode]=1'))); $this->view->assign('currentMode', $this->currentMode()); $this->view->assign('elementCount', $elementCount); if ($elementCount) { $removeAllUrl = GeneralUtility::linkThisScript(['CB' => ['removeAll' => $this->current]]); $this->view->assign('removeAllUrl', $removeAllUrl); // Selector menu + clear button $optionArray = []; // Import / Export link: if (ExtensionManagementUtility::isLoaded('impexp')) { $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = $uriBuilder->buildUriFromRoute('xMOD_tximpexp', $this->exportClipElementParameters()); $optionArray[] = [ 'label' => $this->clLabel('export', 'rm'), 'uri' => (string)$url ]; } // Edit: if (!$this->fileMode) { $optionArray[] = [ 'label' => $this->clLabel('edit', 'rm'), 'uri' => '#', 'additionalAttributes' => [ 'onclick' => htmlspecialchars('window.location.href=' . GeneralUtility::quoteJSvalue($this->editUrl() . '&returnUrl=') . '+top.rawurlencode(window.location.href);'), ] ]; } // Delete referenced elements: $confirmationCheck = false; if ($this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE)) { $confirmationCheck = true; } $confirmationMessage = sprintf( $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.deleteClip'), $elementCount ); $title = $languageService ->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clipboard.delete_elements'); $returnUrl = $this->deleteUrl(true, $this->fileMode); $btnOkText = $languageService ->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_elements.yes'); $btnCancelText = $languageService ->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_elements.no'); $optionArray[] = [ 'label' => htmlspecialchars($title), 'uri' => $returnUrl, 'additionalAttributes' => [ 'class' => $confirmationCheck ? 't3js-modal-trigger' : '', ], 'data' => [ 'severity' => 'warning', 'button-close-text' => htmlspecialchars($btnCancelText), 'button-ok-text' => htmlspecialchars($btnOkText), 'content' => htmlspecialchars($confirmationMessage), 'title' => htmlspecialchars($title) ] ]; // Clear clipboard $optionArray[] = [ 'label' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clipboard.clear_clipboard', true), 'uri' => $removeAllUrl . '#clip_head' ]; $this->view->assign('optionArray', $optionArray); } // Print header and content for the NORMAL tab: $this->view->assign('current', $this->current); $tabArray = []; $tabArray['normal'] = [ 'id' => 'normal', 'number' => 0, 'url' => GeneralUtility::linkThisScript(['CB' => ['setP' => 'normal']]), 'description' => 'normal-description', 'label' => 'labels.normal', 'padding' => $this->padTitle('normal') ]; if ($this->current === 'normal') { $tabArray['normal']['content'] = $this->getContentFromTab('normal'); } // Print header and content for the NUMERIC tabs: for ($a = 1; $a <= $this->numberTabs; $a++) { $tabArray['tab_' . $a] = [ 'id' => 'tab_' . $a, 'number' => $a, 'url' => GeneralUtility::linkThisScript(['CB' => ['setP' => 'tab_' . $a]]), 'description' => 'cliptabs-description', 'label' => 'labels.cliptabs-name', 'padding' => $this->padTitle('tab_' . $a) ]; if ($this->current === 'tab_' . $a) { $tabArray['tab_' . $a]['content'] = $this->getContentFromTab('tab_' . $a); } } $this->view->assign('clipboardHeader', BackendUtility::wrapInHelp('xMOD_csh_corebe', 'list_clipboard', $this->clLabel('clipboard', 'buttons'))); $this->view->assign('tabArray', $tabArray); return $this->view->render(); } /** * Print the content on a pad. Called from ->printClipboard() * * @access private * @param string $pad Pad reference * @return array Array with table rows for the clipboard. */ public function getContentFromTab($pad) { $lines = []; if (is_array($this->clipData[$pad]['el'] ?? false)) { foreach ($this->clipData[$pad]['el'] as $k => $v) { if ($v) { list($table, $uid) = explode('|', $k); // Rendering files/directories on the clipboard if ($table === '_FILE') { $fileObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($v); if ($fileObject) { $thumb = []; $folder = $fileObject instanceof \TYPO3\CMS\Core\Resource\Folder; $size = $folder ? '' : '(' . GeneralUtility::formatSize($fileObject->getSize()) . 'bytes)'; if ( !$folder && GeneralUtility::inList( $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileObject->getExtension() ) ) { $thumb = [ 'image' => $fileObject->process(\TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGEPREVIEW, [])->getPublicUrl(true), 'title' => htmlspecialchars($fileObject->getName()) ]; } $lines[] = [ 'icon' => '' . $this->iconFactory->getIconForResource( $fileObject, Icon::SIZE_SMALL )->render() . '', 'title' => $this->linkItemText(htmlspecialchars(GeneralUtility::fixed_lgd_cs( $fileObject->getName(), $this->getBackendUser()->uc['titleLen'] )), $fileObject->getName()), 'thumb' => $thumb, 'infoLink' => htmlspecialchars('top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($v) . '); return false;'), 'removeLink' => $this->removeUrl('_FILE', GeneralUtility::shortMD5($v)) ]; } else { // If the file did not exist (or is illegal) then it is removed from the clipboard immediately: unset($this->clipData[$pad]['el'][$k]); $this->changed = 1; } } else { // Rendering records: $rec = BackendUtility::getRecordWSOL($table, $uid); if (is_array($rec)) { $lines[] = [ 'icon' => $this->linkItemText($this->iconFactory->getIconForRecord( $table, $rec, Icon::SIZE_SMALL )->render(), $rec, $table), 'title' => $this->linkItemText(htmlspecialchars(GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle( $table, $rec ), $this->getBackendUser()->uc['titleLen'])), $rec, $table), 'infoLink' => htmlspecialchars('top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', \'' . (int)$uid . '\'); return false;'), 'removeLink' => $this->removeUrl($table, $uid) ]; $localizationData = $this->getLocalizations($table, $rec); if (!empty($localizationData)) { $lines = array_merge($lines, $localizationData); } } else { unset($this->clipData[$pad]['el'][$k]); $this->changed = 1; } } } } } $this->endClipboard(); return $lines; } /** * Returns true if the clipboard contains elements * * @return bool */ public function hasElements() { 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 $parentRec The current record * @return array HTML table rows */ public function getLocalizations($table, $parentRec) { $lines = []; $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl']; if (BackendUtility::isTableLocalizable($table)) { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $queryBuilder ->select('*') ->from($table) ->where( $queryBuilder->expr()->eq( $tcaCtrl['transOrigPointerField'], $queryBuilder->createNamedParameter($parentRec['uid'], \PDO::PARAM_INT) ), $queryBuilder->expr()->neq( $tcaCtrl['languageField'], $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) ), $queryBuilder->expr()->gt( 'pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) ) ); if (isset($tcaCtrl['versioningWS']) && $tcaCtrl['versioningWS']) { $queryBuilder ->andWhere( $queryBuilder->expr()->eq( 't3ver_wsid', $queryBuilder->createNamedParameter($parentRec['t3ver_wsid'], \PDO::PARAM_INT) ) ); } $rows = $queryBuilder->execute()->fetchAll(); if (is_array($rows)) { foreach ($rows as $rec) { $lines[] = [ 'icon' => $this->iconFactory->getIconForRecord($table, $rec, Icon::SIZE_SMALL)->render(), 'title' => htmlspecialchars(GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($table, $rec), $this->getBackendUser()->uc['titleLen'])) ]; } } } return $lines; } /** * Warps title with number of elements if any. * * @param string $pad Pad reference * @return string padding */ public function padTitle($pad) { $el = count($this->elFromTable($this->fileMode ? '_FILE' : '', $pad)); if ($el) { return ' (' . ($pad === 'normal' ? ($this->clipData['normal']['mode'] === 'copy' ? $this->clLabel('copy', 'cm') : $this->clLabel('cut', 'cm')) : htmlspecialchars($el)) . ')'; } return ''; } /** * Wraps the title of the items listed in link-tags. The items will link to the page/folder where they originate from * * @param string $str Title of element - must be htmlspecialchar'ed on beforehand. * @param mixed $rec If array, a record is expected. If string, its a path * @param string $table Table name * @return string */ public function linkItemText($str, $rec, $table = '') { $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); if (is_array($rec) && $table) { if ($this->fileMode) { $str = '' . $str . ''; } else { $str = '' . $str . ''; } } elseif (file_exists($rec)) { if (!$this->fileMode) { $str = '' . $str . ''; } elseif (ExtensionManagementUtility::isLoaded('filelist')) { $str = '' . $str . ''; } } return $str; } /** * Returns the select-url for database elements * * @param string $table Table name * @param int $uid Uid of record * @param bool|int $copy If set, copymode will be enabled * @param bool|int $deselect If set, the link will deselect, otherwise select. * @param array $baseArray The base array of GET vars to be sent in addition. Notice that current GET vars WILL automatically be included. * @return string URL linking to the current script but with the CB array set to select the element with table/uid */ public function selUrlDB($table, $uid, $copy = 0, $deselect = 0, $baseArray = []) { $CB = ['el' => [rawurlencode($table . '|' . $uid) => $deselect ? 0 : 1]]; if ($copy) { $CB['setCopyMode'] = 1; } $baseArray['CB'] = $CB; return GeneralUtility::linkThisScript($baseArray); } /** * Returns the select-url for files * * @param string $path Filepath * @param bool|int $copy If set, copymode will be enabled * @param bool|int $deselect If set, the link will deselect, otherwise select. * @param array $baseArray The base array of GET vars to be sent in addition. Notice that current GET vars WILL automatically be included. * @return string URL linking to the current script but with the CB array set to select the path */ public function selUrlFile($path, $copy = 0, $deselect = 0, $baseArray = []) { $CB = ['el' => [rawurlencode('_FILE|' . GeneralUtility::shortMD5($path)) => $deselect ? '' : $path]]; if ($copy) { $CB['setCopyMode'] = 1; } $baseArray['CB'] = $CB; return GeneralUtility::linkThisScript($baseArray); } /** * 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 mixed $uid "destination": can be positive or negative indicating how the paste is done (paste into / paste after) * @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) * @return string */ public function pasteUrl($table, $uid, $setRedirect = true, array $update = null) { $urlParameters = [ 'CB[paste]' => $table . '|' . $uid, 'CB[pad]' => $this->current ]; if ($setRedirect) { $urlParameters['redirect'] = GeneralUtility::linkThisScript(['CB' => '']); } if (is_array($update)) { $urlParameters['CB[update]'] = $update; } $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); return (string)$uriBuilder->buildUriFromRoute($table === '_FILE' ? 'tce_file' : 'tce_db', $urlParameters); } /** * deleteUrl for current pad * * @param bool $setRedirect If set, then the redirect URL will point back to the current script, but with CB reset. * @param bool $file If set, then the URL will link to the tce_file.php script in the typo3/ dir. * @return string */ public function deleteUrl($setRedirect = true, $file = false) { $urlParameters = [ 'CB[delete]' => 1, 'CB[pad]' => $this->current ]; if ($setRedirect) { $urlParameters['redirect'] = GeneralUtility::linkThisScript(['CB' => '']); } $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); return (string)$uriBuilder->buildUriFromRoute($file ? 'tce_file' : 'tce_db', $urlParameters); } /** * editUrl of all current elements * ONLY database * Links to FormEngine * * @return string The URL to FormEngine with parameters. */ public function editUrl() { $parameters = []; // All records $elements = $this->elFromTable(''); foreach ($elements as $tP => $value) { list($table, $uid) = explode('|', $tP); $parameters['edit[' . $table . '][' . $uid . ']'] = 'edit'; } $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); return (string)$uriBuilder->buildUriFromRoute('record_edit', $parameters); } /** * Returns the remove-url (file and db) * for file $table='_FILE' and $uid = shortmd5 hash of path * * @param string $table Tablename * @param string $uid Uid integer/shortmd5 hash * @return string URL */ public function removeUrl($table, $uid) { return GeneralUtility::linkThisScript(['CB' => ['remove' => $table . '|' . $uid]]); } /** * Returns confirm JavaScript message * * @param string $table Table name * @param mixed $rec For records its an array, for files its a string (path) * @param string $type Type-code * @param array $clElements Array of selected elements * @param string $columnLabel Name of the content column * @return string the text for a confirm message */ public function confirmMsgText($table, $rec, $type, $clElements, $columnLabel = '') { if ($this->getBackendUser()->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) { $labelKey = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.' . ($this->currentMode() === 'copy' ? 'copy' : 'move') . ($this->current === 'normal' ? '' : 'cb') . '_' . $type; $msg = $this->getLanguageService()->sL($labelKey . ($columnLabel ? '_colPos' : '')); if ($table === '_FILE') { $thisRecTitle = basename($rec); if ($this->current === 'normal') { $selItem = reset($clElements); $selRecTitle = basename($selItem); } else { $selRecTitle = count($clElements); } } else { $thisRecTitle = $table === 'pages' && !is_array($rec) ? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] : BackendUtility::getRecordTitle($table, $rec); if ($this->current === 'normal') { $selItem = $this->getSelectedRecord(); $selRecTitle = $selItem['_RECORD_TITLE']; } else { $selRecTitle = count($clElements); } } // @TODO // This can get removed as soon as the "_colPos" label is translated // into all available locallang languages. if (!$msg && $columnLabel) { $thisRecTitle .= ' | ' . $columnLabel; $msg = $this->getLanguageService()->sL($labelKey); } // Message $conf = sprintf( $msg, GeneralUtility::fixed_lgd_cs($selRecTitle, 30), GeneralUtility::fixed_lgd_cs($thisRecTitle, 30), GeneralUtility::fixed_lgd_cs($columnLabel, 30) ); } else { $conf = ''; } return $conf; } /** * Clipboard label - getting from "EXT:core/Resources/Private/Language/locallang_core.xlf:" * * @param string $key Label Key * @param string $Akey Alternative key to "labels * @return string */ public function clLabel($key, $Akey = 'labels') { return htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:' . $Akey . '.' . $key)); } /** * Creates GET parameters for linking to the export module. * * @return array GET parameters for current clipboard content to be exported */ protected function exportClipElementParameters() { // Init $pad = $this->current; $params = []; $params['tx_impexp']['action'] = 'export'; // Traverse items: if (is_array($this->clipData[$pad]['el'] ?? false)) { foreach ($this->clipData[$pad]['el'] as $k => $v) { if ($v) { list($table, $uid) = explode('|', $k); // Rendering files/directories on the clipboard if ($table === '_FILE') { if (file_exists($v) && GeneralUtility::isAllowedAbsPath($v)) { $params['tx_impexp'][is_dir($v) ? 'dir' : 'file'][] = $v; } } else { // Rendering records: $rec = BackendUtility::getRecord($table, $uid); if (is_array($rec)) { $params['tx_impexp']['record'][] = $table . ':' . $uid; } } } } } return $params; } /***************************************** * * Helper functions * ****************************************/ /** * Removes element on clipboard * * @param string $el Key of element in ->clipData array */ public function removeElement($el) { unset($this->clipData[$this->current]['el'][$el]); $this->changed = 1; } /** * Saves the clipboard, no questions asked. * Use ->endClipboard normally (as it checks if changes has been done so saving is necessary) * * @access private */ public function saveClipboard() { $this->getBackendUser()->pushModuleData('clipboard', $this->clipData); } /** * Returns the current mode, 'copy' or 'cut' * * @return string "copy" or "cut */ public function currentMode() { 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() { if (is_array($this->clipData[$this->current]['el'] ?? false)) { foreach ($this->clipData[$this->current]['el'] as $k => $v) { list($table, $uid) = explode('|', $k); if ($table !== '_FILE') { if (!$v || !is_array(BackendUtility::getRecord($table, $uid, 'uid'))) { unset($this->clipData[$this->current]['el'][$k]); $this->changed = 1; } } else { if (!$v) { unset($this->clipData[$this->current]['el'][$k]); $this->changed = 1; } else { try { ResourceFactory::getInstance()->retrieveFileOrFolderObject($v); } catch (\TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException $e) { // The file has been deleted in the meantime, so just remove it silently unset($this->clipData[$this->current]['el'][$k]); } } } } } } /** * 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 $pad Can optionally be used to set another pad than the current. * @return array Array with keys from the CB. */ public function elFromTable($matchTable = '', $pad = '') { $pad = $pad ? $pad : $this->current; $list = []; if (is_array($this->clipData[$pad]['el'] ?? false)) { foreach ($this->clipData[$pad]['el'] as $k => $v) { if ($v) { list($table, $uid) = explode('|', $k); if ($table !== '_FILE') { if ((!$matchTable || (string)$table == (string)$matchTable) && $GLOBALS['TCA'][$table]) { $list[$k] = $pad === 'normal' ? $v : $uid; } } else { if ((string)$table == (string)$matchTable) { $list[$k] = $v; } } } } } return $list; } /** * Verifies if the item $table/$uid is on the current pad. * If the pad is "normal", the mode value is returned if the element existed. Thus you'll know if the item was copy or cut moded... * * @param string $table Table name, (_FILE for files...) * @param int $uid Element uid (path for files) * @return string */ public function isSelected($table, $uid) { $k = $table . '|' . $uid; return !empty($this->clipData[$this->current]['el'][$k]) ? ($this->current === 'normal' ? $this->currentMode() : 1) : ''; } /** * Returns item record $table,$uid if selected on current clipboard * If table and uid is blank, the first element is returned. * Makes sense only for DB records - not files! * * @param string $table Table name * @param int|string $uid Element uid * @return array Element record with extra field _RECORD_TITLE set to the title of the record */ public function getSelectedRecord($table = '', $uid = '') { if (!$table && !$uid) { $elArr = $this->elFromTable(''); reset($elArr); list($table, $uid) = explode('|', key($elArr)); } if ($this->isSelected($table, $uid)) { $selRec = BackendUtility::getRecordWSOL($table, $uid); $selRec['_RECORD_TITLE'] = BackendUtility::getRecordTitle($table, $selRec); return $selRec; } return []; } /** * 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. */ public function isElements() { return is_array($this->clipData[$this->current]['el']) && !empty($this->clipData[$this->current]['el']); } /***************************************** * * FOR USE IN tce_db.php: * ****************************************/ /** * Applies the proper paste configuration in the $cmd array send to tce_db.php. * $ref is the target, see description below. * The current pad is pasted * * $ref: [tablename]:[paste-uid]. * Tablename is the name of the table from which elements *on the current clipboard* is pasted with the 'pid' paste-uid. * No tablename means that all items on the clipboard (non-files) are pasted. This requires paste-uid to be positive though. * so 'tt_content:-3' means 'paste tt_content elements on the clipboard to AFTER tt_content:3 record * 'tt_content:30' means 'paste tt_content elements on the clipboard into page with id 30 * ':30' means 'paste ALL database elements on the clipboard into page with id 30 * ':-30' not valid. * * @param string $ref [tablename]:[paste-uid], see description * @param array $CMD Command-array * @param array|null $update If additional values should get set in the copied/moved record this will be an array containing key=>value pairs * @return array Modified Command-array */ public function makePasteCmdArray($ref, $CMD, array $update = null) { list($pTable, $pUid) = explode('|', $ref); $pUid = (int)$pUid; // pUid must be set and if pTable is not set (that means paste ALL elements) // the uid MUST be positive/zero (pointing to page id) if ($pTable || $pUid >= 0) { $elements = $this->elFromTable($pTable); // So the order is preserved. $elements = array_reverse($elements); $mode = $this->currentMode() === 'copy' ? 'copy' : 'move'; // Traverse elements and make CMD array foreach ($elements as $tP => $value) { list($table, $uid) = explode('|', $tP); if (!is_array($CMD[$table])) { $CMD[$table] = []; } if (is_array($update)) { $CMD[$table][$uid][$mode] = [ 'action' => 'paste', 'target' => $pUid, 'update' => $update, ]; } else { $CMD[$table][$uid][$mode] = $pUid; } if ($mode === 'move') { $this->removeElement($tP); } } $this->endClipboard(); } return $CMD; } /** * Delete record entries in CMD array * * @param array $CMD Command-array * @return array Modified Command-array */ public function makeDeleteCmdArray($CMD) { // all records $elements = $this->elFromTable(''); foreach ($elements as $tP => $value) { list($table, $uid) = explode('|', $tP); if (!is_array($CMD[$table])) { $CMD[$table] = []; } $CMD[$table][$uid]['delete'] = 1; $this->removeElement($tP); } $this->endClipboard(); return $CMD; } /***************************************** * * FOR USE IN tce_file.php: * ****************************************/ /** * Applies the proper paste configuration in the $file array send to tce_file.php. * The current pad is pasted * * @param string $ref Reference to element (splitted by "|") * @param array $FILE Command-array * @return array Modified Command-array */ public function makePasteCmdArray_file($ref, $FILE) { list($pTable, $pUid) = explode('|', $ref); $elements = $this->elFromTable('_FILE'); $mode = $this->currentMode() === 'copy' ? 'copy' : 'move'; // Traverse elements and make CMD array foreach ($elements as $tP => $path) { $FILE[$mode][] = ['data' => $path, 'target' => $pUid]; if ($mode === 'move') { $this->removeElement($tP); } } $this->endClipboard(); return $FILE; } /** * Delete files in CMD array * * @param array $FILE Command-array * @return array Modified Command-array */ public function makeDeleteCmdArray_file($FILE) { $elements = $this->elFromTable('_FILE'); // Traverse elements and make CMD array foreach ($elements as $tP => $path) { $FILE['delete'][] = ['data' => $path]; $this->removeElement($tP); } $this->endClipboard(); return $FILE; } /** * Returns LanguageService * * @return \TYPO3\CMS\Core\Localization\LanguageService */ protected function getLanguageService() { return $GLOBALS['LANG']; } /** * Returns the current BE user. * * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication */ protected function getBackendUser() { return $GLOBALS['BE_USER']; } /** * returns a new standalone view, shorthand function * * @return StandaloneView * @throws \InvalidArgumentException * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException */ protected function getStandaloneView() { /** @var StandaloneView $view */ $view = GeneralUtility::makeInstance(StandaloneView::class); $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]); $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]); $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]); $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/Clipboard/Main.html')); $view->getRequest()->setControllerExtensionName('Backend'); return $view; } }