d596903fa7ab9e305a5ad035c2d6c1ea61952499
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Clipboard / Clipboard.php
1 <?php
2 namespace TYPO3\CMS\Backend\Clipboard;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Routing\UriBuilder;
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
21 use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
22 use TYPO3\CMS\Core\Imaging\Icon;
23 use TYPO3\CMS\Core\Imaging\IconFactory;
24 use TYPO3\CMS\Core\Resource\ResourceFactory;
25 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
26 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\MathUtility;
29 use TYPO3\CMS\Core\Utility\PathUtility;
30 use TYPO3\CMS\Fluid\View\StandaloneView;
31
32 /**
33 * TYPO3 clipboard for records and files
34 *
35 * @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
36 */
37 class Clipboard
38 {
39 /**
40 * @var int
41 */
42 public $numberTabs = 3;
43
44 /**
45 * Clipboard data kept here
46 *
47 * Keys:
48 * 'normal'
49 * 'tab_[x]' where x is >=1 and denotes the pad-number
50 * 'mode' : 'copy' means copy-mode, default = moving ('cut')
51 * 'el' : Array of elements:
52 * DB: keys = '[tablename]|[uid]' eg. 'tt_content:123'
53 * DB: values = 1 (basically insignificant)
54 * FILE: keys = '_FILE|[shortmd5 of path]' eg. '_FILE|9ebc7e5c74'
55 * FILE: values = The full filepath, eg. '/www/htdocs/typo3/32/dummy/fileadmin/sem1_3_examples/alternative_index.php'
56 * or 'C:/www/htdocs/typo3/32/dummy/fileadmin/sem1_3_examples/alternative_index.php'
57 *
58 * 'current' pointer to current tab (among the above...)
59 *
60 * The virtual tablename '_FILE' will always indicate files/folders. When checking for elements from eg. 'all tables'
61 * (by using an empty string) '_FILE' entries are excluded (so in effect only DB elements are counted)
62 *
63 * @var array
64 */
65 public $clipData = [];
66
67 /**
68 * @var int
69 */
70 public $changed = 0;
71
72 /**
73 * @var string
74 */
75 public $current = '';
76
77 /**
78 * @var int
79 */
80 public $lockToNormal = 0;
81
82 /**
83 * If set, clipboard is displaying files.
84 *
85 * @var bool
86 */
87 public $fileMode = false;
88
89 /**
90 * @var IconFactory
91 */
92 protected $iconFactory;
93
94 /**
95 * @var StandaloneView
96 */
97 protected $view;
98
99 /**
100 * Construct
101 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException
102 * @throws \InvalidArgumentException
103 */
104 public function __construct()
105 {
106 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
107 $this->view = $this->getStandaloneView();
108 }
109
110 /*****************************************
111 *
112 * Initialize
113 *
114 ****************************************/
115 /**
116 * Initialize the clipboard from the be_user session
117 */
118 public function initializeClipboard()
119 {
120 $userTsConfig = $this->getBackendUser()->getTSConfig();
121 // Get data
122 $clipData = $this->getBackendUser()->getModuleData('clipboard', $userTsConfig['options.']['saveClipboard'] ? '' : 'ses');
123 $this->numberTabs = MathUtility::forceIntegerInRange((int)($userTsConfig['options.']['clipboardNumberPads'] ?? 3), 0, 20);
124 // Resets/reinstates the clipboard pads
125 $this->clipData['normal'] = is_array($clipData['normal']) ? $clipData['normal'] : [];
126 for ($a = 1; $a <= $this->numberTabs; $a++) {
127 $this->clipData['tab_' . $a] = is_array($clipData['tab_' . $a]) ? $clipData['tab_' . $a] : [];
128 }
129 // Setting the current pad pointer ($this->current))
130 $this->clipData['current'] = ($this->current = isset($this->clipData[$clipData['current']]) ? $clipData['current'] : 'normal');
131 }
132
133 /**
134 * Call this method after initialization if you want to lock the clipboard to operate on the normal pad only.
135 * Trying to switch pad through ->setCmd will not work.
136 * This is used by the clickmenu since it only allows operation on single elements at a time (that is the "normal" pad)
137 */
138 public function lockToNormal()
139 {
140 $this->lockToNormal = 1;
141 $this->current = 'normal';
142 }
143
144 /**
145 * The array $cmd may hold various keys which notes some action to take.
146 * Normally perform only one action at a time.
147 * In scripts like db_list.php / filelist/mod1/index.php the GET-var CB is used to control the clipboard.
148 *
149 * Selecting / Deselecting elements
150 * Array $cmd['el'] has keys = element-ident, value = element value (see description of clipData array in header)
151 * Selecting elements for 'copy' should be done by simultaneously setting setCopyMode.
152 *
153 * @param array $cmd Array of actions, see function description
154 */
155 public function setCmd($cmd)
156 {
157 if (is_array($cmd['el'])) {
158 foreach ($cmd['el'] as $k => $v) {
159 if ($this->current === 'normal') {
160 unset($this->clipData['normal']);
161 }
162 if ($v) {
163 $this->clipData[$this->current]['el'][$k] = $v;
164 } else {
165 $this->removeElement($k);
166 }
167 $this->changed = 1;
168 }
169 }
170 // Change clipboard pad (if not locked to normal)
171 if ($cmd['setP']) {
172 $this->setCurrentPad($cmd['setP']);
173 }
174 // Remove element (value = item ident: DB; '[tablename]|[uid]' FILE: '_FILE|[shortmd5 hash of path]'
175 if ($cmd['remove']) {
176 $this->removeElement($cmd['remove']);
177 $this->changed = 1;
178 }
179 // Remove all on current pad (value = pad-ident)
180 if ($cmd['removeAll']) {
181 $this->clipData[$cmd['removeAll']] = [];
182 $this->changed = 1;
183 }
184 // Set copy mode of the tab
185 if (isset($cmd['setCopyMode'])) {
186 $this->clipData[$this->current]['mode'] = $this->isElements() ? ($cmd['setCopyMode'] ? 'copy' : '') : '';
187 $this->changed = 1;
188 }
189 }
190
191 /**
192 * Setting the current pad on clipboard
193 *
194 * @param string $padIdent Key in the array $this->clipData
195 */
196 public function setCurrentPad($padIdent)
197 {
198 // Change clipboard pad (if not locked to normal)
199 if (!$this->lockToNormal && $this->current != $padIdent) {
200 if (isset($this->clipData[$padIdent])) {
201 $this->clipData['current'] = ($this->current = $padIdent);
202 }
203 if ($this->current !== 'normal' || !$this->isElements()) {
204 $this->clipData[$this->current]['mode'] = '';
205 }
206 // Setting mode to default (move) if no items on it or if not 'normal'
207 $this->changed = 1;
208 }
209 }
210
211 /**
212 * Call this after initialization and setCmd in order to save the clipboard to the user session.
213 * The function will check if the internal flag ->changed has been set and if so, save the clipboard. Else not.
214 */
215 public function endClipboard()
216 {
217 if ($this->changed) {
218 $this->saveClipboard();
219 }
220 $this->changed = 0;
221 }
222
223 /**
224 * Cleans up an incoming element array $CBarr (Array selecting/deselecting elements)
225 *
226 * @param array $CBarr Element array from outside ("key" => "selected/deselected")
227 * @param string $table The 'table which is allowed'. Must be set.
228 * @param bool|int $removeDeselected Can be set in order to remove entries which are marked for deselection.
229 * @return array Processed input $CBarr
230 */
231 public function cleanUpCBC($CBarr, $table, $removeDeselected = 0)
232 {
233 if (is_array($CBarr)) {
234 foreach ($CBarr as $k => $v) {
235 $p = explode('|', $k);
236 if ((string)$p[0] != (string)$table || $removeDeselected && !$v) {
237 unset($CBarr[$k]);
238 }
239 }
240 }
241 return $CBarr;
242 }
243
244 /*****************************************
245 *
246 * Clipboard HTML renderings
247 *
248 ****************************************/
249 /**
250 * Prints the clipboard
251 *
252 * @return string HTML output
253 * @throws \BadFunctionCallException
254 */
255 public function printClipboard()
256 {
257 $languageService = $this->getLanguageService();
258 $elementCount = count($this->elFromTable($this->fileMode ? '_FILE' : ''));
259 // Copymode Selector menu
260 $copymodeUrl = GeneralUtility::linkThisScript();
261
262 $this->view->assign('actionCopyModeUrl', htmlspecialchars(GeneralUtility::quoteJSvalue($copymodeUrl . '&CB[setCopyMode]=')));
263 $this->view->assign('actionCopyModeUrl1', htmlspecialchars(GeneralUtility::quoteJSvalue($copymodeUrl . '&CB[setCopyMode]=1')));
264 $this->view->assign('currentMode', $this->currentMode());
265 $this->view->assign('elementCount', $elementCount);
266
267 if ($elementCount) {
268 $removeAllUrl = GeneralUtility::linkThisScript(['CB' => ['removeAll' => $this->current]]);
269 $this->view->assign('removeAllUrl', $removeAllUrl);
270
271 // Selector menu + clear button
272 $optionArray = [];
273 // Import / Export link:
274 if (ExtensionManagementUtility::isLoaded('impexp')) {
275 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
276 $url = $uriBuilder->buildUriFromRoute('tx_impexp_export', $this->exportClipElementParameters());
277 $optionArray[] = [
278 'label' => $this->clLabel('export', 'rm'),
279 'uri' => (string)$url
280 ];
281 }
282 // Edit:
283 if (!$this->fileMode) {
284 $optionArray[] = [
285 'label' => $this->clLabel('edit', 'rm'),
286 'uri' => '#',
287 'additionalAttributes' => [
288 'onclick' => htmlspecialchars('window.location.href=' . GeneralUtility::quoteJSvalue($this->editUrl() . '&returnUrl=') . '+encodeURIComponent(window.location.href);'),
289 ]
290 ];
291 }
292
293 // Delete referenced elements:
294 $confirmationCheck = false;
295 if ($this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE)) {
296 $confirmationCheck = true;
297 }
298
299 $confirmationMessage = sprintf(
300 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.deleteClip'),
301 $elementCount
302 );
303 $title = $languageService
304 ->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clipboard.delete_elements');
305 $returnUrl = $this->deleteUrl(true, $this->fileMode);
306 $btnOkText = $languageService
307 ->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_elements.yes');
308 $btnCancelText = $languageService
309 ->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_elements.no');
310 $optionArray[] = [
311 'label' => htmlspecialchars($title),
312 'uri' => $returnUrl,
313 'additionalAttributes' => [
314 'class' => $confirmationCheck ? 't3js-modal-trigger' : '',
315 ],
316 'data' => [
317 'severity' => 'warning',
318 'button-close-text' => htmlspecialchars($btnCancelText),
319 'button-ok-text' => htmlspecialchars($btnOkText),
320 'content' => htmlspecialchars($confirmationMessage),
321 'title' => htmlspecialchars($title)
322 ]
323 ];
324
325 // Clear clipboard
326 $optionArray[] = [
327 'label' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clipboard.clear_clipboard'),
328 'uri' => $removeAllUrl . '#clip_head'
329 ];
330 $this->view->assign('optionArray', $optionArray);
331 }
332
333 // Print header and content for the NORMAL tab:
334 $this->view->assign('current', $this->current);
335 $tabArray = [];
336 $tabArray['normal'] = [
337 'id' => 'normal',
338 'number' => 0,
339 'url' => GeneralUtility::linkThisScript(['CB' => ['setP' => 'normal']]),
340 'description' => 'normal-description',
341 'label' => 'labels.normal',
342 'padding' => $this->padTitle('normal')
343 ];
344 if ($this->current === 'normal') {
345 $tabArray['normal']['content'] = $this->getContentFromTab('normal');
346 }
347 // Print header and content for the NUMERIC tabs:
348 for ($a = 1; $a <= $this->numberTabs; $a++) {
349 $tabArray['tab_' . $a] = [
350 'id' => 'tab_' . $a,
351 'number' => $a,
352 'url' => GeneralUtility::linkThisScript(['CB' => ['setP' => 'tab_' . $a]]),
353 'description' => 'cliptabs-description',
354 'label' => 'labels.cliptabs-name',
355 'padding' => $this->padTitle('tab_' . $a)
356 ];
357 if ($this->current === 'tab_' . $a) {
358 $tabArray['tab_' . $a]['content'] = $this->getContentFromTab('tab_' . $a);
359 }
360 }
361 $this->view->assign('clipboardHeader', BackendUtility::wrapInHelp('xMOD_csh_corebe', 'list_clipboard', $this->clLabel('clipboard', 'buttons')));
362 $this->view->assign('tabArray', $tabArray);
363 return $this->view->render();
364 }
365
366 /**
367 * Print the content on a pad. Called from ->printClipboard()
368 *
369 * @internal
370 * @param string $pad Pad reference
371 * @return array Array with table rows for the clipboard.
372 */
373 public function getContentFromTab($pad)
374 {
375 $lines = [[]];
376 if (is_array($this->clipData[$pad]['el'] ?? false)) {
377 foreach ($this->clipData[$pad]['el'] as $k => $v) {
378 if ($v) {
379 list($table, $uid) = explode('|', $k);
380 // Rendering files/directories on the clipboard
381 if ($table === '_FILE') {
382 $fileObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($v);
383 if ($fileObject) {
384 $thumb = [];
385 $folder = $fileObject instanceof \TYPO3\CMS\Core\Resource\Folder;
386 $size = $folder ? '' : '(' . GeneralUtility::formatSize($fileObject->getSize()) . 'bytes)';
387 if (
388 !$folder
389 && GeneralUtility::inList(
390 $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
391 $fileObject->getExtension()
392 )
393 ) {
394 $thumb = [
395 'image' => $fileObject->process(\TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGEPREVIEW, [])->getPublicUrl(true),
396 'title' => htmlspecialchars($fileObject->getName())
397 ];
398 }
399 $lines[] = [
400 'icon' => '<span title="' . htmlspecialchars($fileObject->getName() . ' ' . $size) . '">' . $this->iconFactory->getIconForResource(
401 $fileObject,
402 Icon::SIZE_SMALL
403 )->render() . '</span>',
404 'title' => $this->linkItemText(htmlspecialchars(GeneralUtility::fixed_lgd_cs(
405 $fileObject->getName(),
406 $this->getBackendUser()->uc['titleLen']
407 )), $fileObject->getName()),
408 'thumb' => $thumb,
409 'infoLink' => htmlspecialchars('top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($v) . '); return false;'),
410 'removeLink' => $this->removeUrl('_FILE', GeneralUtility::shortMD5($v))
411 ];
412 } else {
413 // If the file did not exist (or is illegal) then it is removed from the clipboard immediately:
414 unset($this->clipData[$pad]['el'][$k]);
415 $this->changed = 1;
416 }
417 } else {
418 // Rendering records:
419 $rec = BackendUtility::getRecordWSOL($table, $uid);
420 if (is_array($rec)) {
421 $lines[] = [
422 'icon' => $this->linkItemText($this->iconFactory->getIconForRecord(
423 $table,
424 $rec,
425 Icon::SIZE_SMALL
426 )->render(), $rec, $table),
427 'title' => $this->linkItemText(htmlspecialchars(GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle(
428 $table,
429 $rec
430 ), $this->getBackendUser()->uc['titleLen'])), $rec, $table),
431 'infoLink' => htmlspecialchars('top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', \'' . (int)$uid . '\'); return false;'),
432 'removeLink' => $this->removeUrl($table, $uid)
433 ];
434
435 $localizationData = $this->getLocalizations($table, $rec);
436 if (!empty($localizationData)) {
437 $lines[] = $localizationData;
438 }
439 } else {
440 unset($this->clipData[$pad]['el'][$k]);
441 $this->changed = 1;
442 }
443 }
444 }
445 }
446 }
447 $this->endClipboard();
448 return array_merge(...$lines);
449 }
450
451 /**
452 * Returns true if the clipboard contains elements
453 *
454 * @return bool
455 */
456 public function hasElements()
457 {
458 foreach ($this->clipData as $data) {
459 if (isset($data['el']) && is_array($data['el']) && !empty($data['el'])) {
460 return true;
461 }
462 }
463
464 return false;
465 }
466
467 /**
468 * Gets all localizations of the current record.
469 *
470 * @param string $table The table
471 * @param array $parentRec The current record
472 * @return array HTML table rows
473 */
474 public function getLocalizations($table, $parentRec)
475 {
476 $lines = [];
477 $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
478 $workspaceId = (int)$this->getBackendUser()->workspace;
479
480 if (BackendUtility::isTableLocalizable($table)) {
481 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
482 $queryBuilder->getRestrictions()
483 ->removeAll()
484 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
485
486 $queryBuilder
487 ->select('*')
488 ->from($table)
489 ->where(
490 $queryBuilder->expr()->eq(
491 $tcaCtrl['transOrigPointerField'],
492 $queryBuilder->createNamedParameter($parentRec['uid'], \PDO::PARAM_INT)
493 ),
494 $queryBuilder->expr()->neq(
495 $tcaCtrl['languageField'],
496 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
497 ),
498 $queryBuilder->expr()->gt(
499 'pid',
500 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
501 )
502 )
503 ->orderBy($tcaCtrl['languageField']);
504
505 if (BackendUtility::isTableWorkspaceEnabled($table)) {
506 $queryBuilder->getRestrictions()->add(
507 GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId)
508 );
509 }
510 $rows = $queryBuilder->execute()->fetchAll();
511 if (is_array($rows)) {
512 foreach ($rows as $rec) {
513 $lines[] = [
514 'icon' => $this->iconFactory->getIconForRecord($table, $rec, Icon::SIZE_SMALL)->render(),
515 'title' => htmlspecialchars(GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($table, $rec), $this->getBackendUser()->uc['titleLen']))
516 ];
517 }
518 }
519 }
520 return $lines;
521 }
522
523 /**
524 * Warps title with number of elements if any.
525 *
526 * @param string $pad Pad reference
527 * @return string padding
528 */
529 public function padTitle($pad)
530 {
531 $el = count($this->elFromTable($this->fileMode ? '_FILE' : '', $pad));
532 if ($el) {
533 return ' (' . ($pad === 'normal' ? ($this->clipData['normal']['mode'] === 'copy' ? $this->clLabel('copy', 'cm') : $this->clLabel('cut', 'cm')) : htmlspecialchars($el)) . ')';
534 }
535 return '';
536 }
537
538 /**
539 * Wraps the title of the items listed in link-tags. The items will link to the page/folder where they originate from
540 *
541 * @param string $str Title of element - must be htmlspecialchar'ed on beforehand.
542 * @param mixed $rec If array, a record is expected. If string, its a path
543 * @param string $table Table name
544 * @return string
545 */
546 public function linkItemText($str, $rec, $table = '')
547 {
548 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
549 if (is_array($rec) && $table) {
550 if ($this->fileMode) {
551 $str = '<span class="text-muted">' . $str . '</span>';
552 } else {
553 $str = '<a href="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute('web_list', ['id' => $rec['pid']])) . '">' . $str . '</a>';
554 }
555 } elseif (file_exists($rec)) {
556 if (!$this->fileMode) {
557 $str = '<span class="text-muted">' . $str . '</span>';
558 } elseif (ExtensionManagementUtility::isLoaded('filelist')) {
559 $str = '<a href="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute('file_list', ['id' => PathUtility::dirname($rec)])) . '">' . $str . '</a>';
560 }
561 }
562 return $str;
563 }
564
565 /**
566 * Returns the select-url for database elements
567 *
568 * @param string $table Table name
569 * @param int $uid Uid of record
570 * @param bool|int $copy If set, copymode will be enabled
571 * @param bool|int $deselect If set, the link will deselect, otherwise select.
572 * @param array $baseArray The base array of GET vars to be sent in addition. Notice that current GET vars WILL automatically be included.
573 * @return string URL linking to the current script but with the CB array set to select the element with table/uid
574 */
575 public function selUrlDB($table, $uid, $copy = 0, $deselect = 0, $baseArray = [])
576 {
577 $CB = ['el' => [rawurlencode($table . '|' . $uid) => $deselect ? 0 : 1]];
578 if ($copy) {
579 $CB['setCopyMode'] = 1;
580 }
581 $baseArray['CB'] = $CB;
582 return GeneralUtility::linkThisScript($baseArray);
583 }
584
585 /**
586 * Returns the select-url for files
587 *
588 * @param string $path Filepath
589 * @param bool|int $copy If set, copymode will be enabled
590 * @param bool|int $deselect If set, the link will deselect, otherwise select.
591 * @param array $baseArray The base array of GET vars to be sent in addition. Notice that current GET vars WILL automatically be included.
592 * @return string URL linking to the current script but with the CB array set to select the path
593 */
594 public function selUrlFile($path, $copy = 0, $deselect = 0, $baseArray = [])
595 {
596 $CB = ['el' => [rawurlencode('_FILE|' . GeneralUtility::shortMD5($path)) => $deselect ? '' : $path]];
597 if ($copy) {
598 $CB['setCopyMode'] = 1;
599 }
600 $baseArray['CB'] = $CB;
601 return GeneralUtility::linkThisScript($baseArray);
602 }
603
604 /**
605 * pasteUrl of the element (database and file)
606 * For the meaning of $table and $uid, please read from ->makePasteCmdArray!!!
607 * The URL will point to tce_file or tce_db depending in $table
608 *
609 * @param string $table Tablename (_FILE for files)
610 * @param mixed $uid "destination": can be positive or negative indicating how the paste is done (paste into / paste after)
611 * @param bool $setRedirect If set, then the redirect URL will point back to the current script, but with CB reset.
612 * @param array|null $update Additional key/value pairs which should get set in the moved/copied record (via DataHandler)
613 * @return string
614 */
615 public function pasteUrl($table, $uid, $setRedirect = true, array $update = null)
616 {
617 $urlParameters = [
618 'CB[paste]' => $table . '|' . $uid,
619 'CB[pad]' => $this->current
620 ];
621 if ($setRedirect) {
622 $urlParameters['redirect'] = GeneralUtility::linkThisScript(['CB' => '']);
623 }
624 if (is_array($update)) {
625 $urlParameters['CB[update]'] = $update;
626 }
627 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
628 return (string)$uriBuilder->buildUriFromRoute($table === '_FILE' ? 'tce_file' : 'tce_db', $urlParameters);
629 }
630
631 /**
632 * deleteUrl for current pad
633 *
634 * @param bool $setRedirect If set, then the redirect URL will point back to the current script, but with CB reset.
635 * @param bool $file If set, then the URL will link to the tce_file.php script in the typo3/ dir.
636 * @return string
637 */
638 public function deleteUrl($setRedirect = true, $file = false)
639 {
640 $urlParameters = [
641 'CB[delete]' => 1,
642 'CB[pad]' => $this->current
643 ];
644 if ($setRedirect) {
645 $urlParameters['redirect'] = GeneralUtility::linkThisScript(['CB' => '']);
646 }
647 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
648 return (string)$uriBuilder->buildUriFromRoute($file ? 'tce_file' : 'tce_db', $urlParameters);
649 }
650
651 /**
652 * editUrl of all current elements
653 * ONLY database
654 * Links to FormEngine
655 *
656 * @return string The URL to FormEngine with parameters.
657 */
658 public function editUrl()
659 {
660 $parameters = [];
661 // All records
662 $elements = $this->elFromTable('');
663 foreach ($elements as $tP => $value) {
664 list($table, $uid) = explode('|', $tP);
665 $parameters['edit[' . $table . '][' . $uid . ']'] = 'edit';
666 }
667 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
668 return (string)$uriBuilder->buildUriFromRoute('record_edit', $parameters);
669 }
670
671 /**
672 * Returns the remove-url (file and db)
673 * for file $table='_FILE' and $uid = shortmd5 hash of path
674 *
675 * @param string $table Tablename
676 * @param string $uid Uid integer/shortmd5 hash
677 * @return string URL
678 */
679 public function removeUrl($table, $uid)
680 {
681 return GeneralUtility::linkThisScript(['CB' => ['remove' => $table . '|' . $uid]]);
682 }
683
684 /**
685 * Returns confirm JavaScript message
686 *
687 * @param string $table Table name
688 * @param mixed $rec For records its an array, for files its a string (path)
689 * @param string $type Type-code
690 * @param array $clElements Array of selected elements
691 * @param string $columnLabel Name of the content column
692 * @return string the text for a confirm message
693 */
694 public function confirmMsgText($table, $rec, $type, $clElements, $columnLabel = '')
695 {
696 if ($this->getBackendUser()->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) {
697 $labelKey = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.' . ($this->currentMode() === 'copy' ? 'copy' : 'move') . ($this->current === 'normal' ? '' : 'cb') . '_' . $type;
698 $msg = $this->getLanguageService()->sL($labelKey . ($columnLabel ? '_colPos' : ''));
699 if ($table === '_FILE') {
700 $thisRecTitle = PathUtility::basename($rec);
701 if ($this->current === 'normal') {
702 $selItem = reset($clElements);
703 $selRecTitle = PathUtility::basename($selItem);
704 } else {
705 $selRecTitle = count($clElements);
706 }
707 } else {
708 $thisRecTitle = $table === 'pages' && !is_array($rec) ? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] : BackendUtility::getRecordTitle($table, $rec);
709 if ($this->current === 'normal') {
710 $selItem = $this->getSelectedRecord();
711 $selRecTitle = $selItem['_RECORD_TITLE'];
712 } else {
713 $selRecTitle = count($clElements);
714 }
715 }
716 // @TODO
717 // This can get removed as soon as the "_colPos" label is translated
718 // into all available locallang languages.
719 if (!$msg && $columnLabel) {
720 $thisRecTitle .= ' | ' . $columnLabel;
721 $msg = $this->getLanguageService()->sL($labelKey);
722 }
723
724 // Message
725 $conf = sprintf(
726 $msg,
727 GeneralUtility::fixed_lgd_cs($selRecTitle, 30),
728 GeneralUtility::fixed_lgd_cs($thisRecTitle, 30),
729 GeneralUtility::fixed_lgd_cs($columnLabel, 30)
730 );
731 } else {
732 $conf = '';
733 }
734 return $conf;
735 }
736
737 /**
738 * Clipboard label - getting from "EXT:core/Resources/Private/Language/locallang_core.xlf:"
739 *
740 * @param string $key Label Key
741 * @param string $Akey Alternative key to "labels
742 * @return string
743 */
744 public function clLabel($key, $Akey = 'labels')
745 {
746 return htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:' . $Akey . '.' . $key));
747 }
748
749 /**
750 * Creates GET parameters for linking to the export module.
751 *
752 * @return array GET parameters for current clipboard content to be exported
753 */
754 protected function exportClipElementParameters()
755 {
756 // Init
757 $pad = $this->current;
758 $params = [];
759 // Traverse items:
760 if (is_array($this->clipData[$pad]['el'] ?? false)) {
761 foreach ($this->clipData[$pad]['el'] as $k => $v) {
762 if ($v) {
763 list($table, $uid) = explode('|', $k);
764 // Rendering files/directories on the clipboard
765 if ($table === '_FILE') {
766 $file = ResourceFactory::getInstance()->getFileObjectFromCombinedIdentifier($v);
767 if ($file !== null) {
768 $params['tx_impexp']['record'][] = 'sys_file:' . $file->getUid();
769 }
770 } else {
771 // Rendering records:
772 $rec = BackendUtility::getRecord($table, $uid);
773 if (is_array($rec)) {
774 $params['tx_impexp']['record'][] = $table . ':' . $uid;
775 }
776 }
777 }
778 }
779 }
780 return $params;
781 }
782
783 /*****************************************
784 *
785 * Helper functions
786 *
787 ****************************************/
788 /**
789 * Removes element on clipboard
790 *
791 * @param string $el Key of element in ->clipData array
792 */
793 public function removeElement($el)
794 {
795 unset($this->clipData[$this->current]['el'][$el]);
796 $this->changed = 1;
797 }
798
799 /**
800 * Saves the clipboard, no questions asked.
801 * Use ->endClipboard normally (as it checks if changes has been done so saving is necessary)
802 *
803 * @internal
804 */
805 public function saveClipboard()
806 {
807 $this->getBackendUser()->pushModuleData('clipboard', $this->clipData);
808 }
809
810 /**
811 * Returns the current mode, 'copy' or 'cut'
812 *
813 * @return string "copy" or "cut
814 */
815 public function currentMode()
816 {
817 return ($this->clipData[$this->current]['mode'] ?? '') === 'copy' ? 'copy' : 'cut';
818 }
819
820 /**
821 * This traverses the elements on the current clipboard pane
822 * and unsets elements which does not exist anymore or are disabled.
823 */
824 public function cleanCurrent()
825 {
826 if (is_array($this->clipData[$this->current]['el'] ?? false)) {
827 foreach ($this->clipData[$this->current]['el'] as $k => $v) {
828 list($table, $uid) = explode('|', $k);
829 if ($table !== '_FILE') {
830 if (!$v || !is_array(BackendUtility::getRecord($table, $uid, 'uid'))) {
831 unset($this->clipData[$this->current]['el'][$k]);
832 $this->changed = 1;
833 }
834 } else {
835 if (!$v) {
836 unset($this->clipData[$this->current]['el'][$k]);
837 $this->changed = 1;
838 } else {
839 try {
840 ResourceFactory::getInstance()->retrieveFileOrFolderObject($v);
841 } catch (\TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException $e) {
842 // The file has been deleted in the meantime, so just remove it silently
843 unset($this->clipData[$this->current]['el'][$k]);
844 }
845 }
846 }
847 }
848 }
849 }
850
851 /**
852 * Counts the number of elements from the table $matchTable. If $matchTable is blank, all tables (except '_FILE' of course) is counted.
853 *
854 * @param string $matchTable Table to match/count for.
855 * @param string $pad Can optionally be used to set another pad than the current.
856 * @return array Array with keys from the CB.
857 */
858 public function elFromTable($matchTable = '', $pad = '')
859 {
860 $pad = $pad ? $pad : $this->current;
861 $list = [];
862 if (is_array($this->clipData[$pad]['el'] ?? false)) {
863 foreach ($this->clipData[$pad]['el'] as $k => $v) {
864 if ($v) {
865 list($table, $uid) = explode('|', $k);
866 if ($table !== '_FILE') {
867 if ((!$matchTable || (string)$table == (string)$matchTable) && $GLOBALS['TCA'][$table]) {
868 $list[$k] = $pad === 'normal' ? $v : $uid;
869 }
870 } else {
871 if ((string)$table == (string)$matchTable) {
872 $list[$k] = $v;
873 }
874 }
875 }
876 }
877 }
878 return $list;
879 }
880
881 /**
882 * Verifies if the item $table/$uid is on the current pad.
883 * 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...
884 *
885 * @param string $table Table name, (_FILE for files...)
886 * @param int $uid Element uid (path for files)
887 * @return string
888 */
889 public function isSelected($table, $uid)
890 {
891 $k = $table . '|' . $uid;
892 return !empty($this->clipData[$this->current]['el'][$k]) ? ($this->current === 'normal' ? $this->currentMode() : 1) : '';
893 }
894
895 /**
896 * Returns item record $table,$uid if selected on current clipboard
897 * If table and uid is blank, the first element is returned.
898 * Makes sense only for DB records - not files!
899 *
900 * @param string $table Table name
901 * @param int|string $uid Element uid
902 * @return array Element record with extra field _RECORD_TITLE set to the title of the record
903 */
904 public function getSelectedRecord($table = '', $uid = '')
905 {
906 if (!$table && !$uid) {
907 $elArr = $this->elFromTable('');
908 reset($elArr);
909 list($table, $uid) = explode('|', key($elArr));
910 }
911 if ($this->isSelected($table, $uid)) {
912 $selRec = BackendUtility::getRecordWSOL($table, $uid);
913 $selRec['_RECORD_TITLE'] = BackendUtility::getRecordTitle($table, $selRec);
914 return $selRec;
915 }
916 return [];
917 }
918
919 /**
920 * Reports if the current pad has elements (does not check file/DB type OR if file/DBrecord exists or not. Only counting array)
921 *
922 * @return bool TRUE if elements exist.
923 */
924 public function isElements()
925 {
926 return is_array($this->clipData[$this->current]['el']) && !empty($this->clipData[$this->current]['el']);
927 }
928
929 /**
930 * Applies the proper paste configuration in the $cmd array send to SimpleDataHandlerController (tce_db route)
931 * $ref is the target, see description below.
932 * The current pad is pasted
933 *
934 * $ref: [tablename]:[paste-uid].
935 * Tablename is the name of the table from which elements *on the current clipboard* is pasted with the 'pid' paste-uid.
936 * No tablename means that all items on the clipboard (non-files) are pasted. This requires paste-uid to be positive though.
937 * so 'tt_content:-3' means 'paste tt_content elements on the clipboard to AFTER tt_content:3 record
938 * 'tt_content:30' means 'paste tt_content elements on the clipboard into page with id 30
939 * ':30' means 'paste ALL database elements on the clipboard into page with id 30
940 * ':-30' not valid.
941 *
942 * @param string $ref [tablename]:[paste-uid], see description
943 * @param array $CMD Command-array
944 * @param array|null $update If additional values should get set in the copied/moved record this will be an array containing key=>value pairs
945 * @return array Modified Command-array
946 */
947 public function makePasteCmdArray($ref, $CMD, array $update = null)
948 {
949 list($pTable, $pUid) = explode('|', $ref);
950 $pUid = (int)$pUid;
951 // pUid must be set and if pTable is not set (that means paste ALL elements)
952 // the uid MUST be positive/zero (pointing to page id)
953 if ($pTable || $pUid >= 0) {
954 $elements = $this->elFromTable($pTable);
955 // So the order is preserved.
956 $elements = array_reverse($elements);
957 $mode = $this->currentMode() === 'copy' ? 'copy' : 'move';
958 // Traverse elements and make CMD array
959 foreach ($elements as $tP => $value) {
960 list($table, $uid) = explode('|', $tP);
961 if (!is_array($CMD[$table])) {
962 $CMD[$table] = [];
963 }
964 if (is_array($update)) {
965 $CMD[$table][$uid][$mode] = [
966 'action' => 'paste',
967 'target' => $pUid,
968 'update' => $update,
969 ];
970 } else {
971 $CMD[$table][$uid][$mode] = $pUid;
972 }
973 if ($mode === 'move') {
974 $this->removeElement($tP);
975 }
976 }
977 $this->endClipboard();
978 }
979 return $CMD;
980 }
981
982 /**
983 * Delete record entries in CMD array
984 *
985 * @param array $CMD Command-array
986 * @return array Modified Command-array
987 */
988 public function makeDeleteCmdArray($CMD)
989 {
990 // all records
991 $elements = $this->elFromTable('');
992 foreach ($elements as $tP => $value) {
993 list($table, $uid) = explode('|', $tP);
994 if (!is_array($CMD[$table])) {
995 $CMD[$table] = [];
996 }
997 $CMD[$table][$uid]['delete'] = 1;
998 $this->removeElement($tP);
999 }
1000 $this->endClipboard();
1001 return $CMD;
1002 }
1003
1004 /*****************************************
1005 *
1006 * FOR USE IN tce_file.php:
1007 *
1008 ****************************************/
1009 /**
1010 * Applies the proper paste configuration in the $file array send to tce_file.php.
1011 * The current pad is pasted
1012 *
1013 * @param string $ref Reference to element (splitted by "|")
1014 * @param array $FILE Command-array
1015 * @return array Modified Command-array
1016 */
1017 public function makePasteCmdArray_file($ref, $FILE)
1018 {
1019 list($pTable, $pUid) = explode('|', $ref);
1020 $elements = $this->elFromTable('_FILE');
1021 $mode = $this->currentMode() === 'copy' ? 'copy' : 'move';
1022 // Traverse elements and make CMD array
1023 foreach ($elements as $tP => $path) {
1024 $FILE[$mode][] = ['data' => $path, 'target' => $pUid];
1025 if ($mode === 'move') {
1026 $this->removeElement($tP);
1027 }
1028 }
1029 $this->endClipboard();
1030 return $FILE;
1031 }
1032
1033 /**
1034 * Delete files in CMD array
1035 *
1036 * @param array $FILE Command-array
1037 * @return array Modified Command-array
1038 */
1039 public function makeDeleteCmdArray_file($FILE)
1040 {
1041 $elements = $this->elFromTable('_FILE');
1042 // Traverse elements and make CMD array
1043 foreach ($elements as $tP => $path) {
1044 $FILE['delete'][] = ['data' => $path];
1045 $this->removeElement($tP);
1046 }
1047 $this->endClipboard();
1048 return $FILE;
1049 }
1050
1051 /**
1052 * Returns LanguageService
1053 *
1054 * @return \TYPO3\CMS\Core\Localization\LanguageService
1055 */
1056 protected function getLanguageService()
1057 {
1058 return $GLOBALS['LANG'];
1059 }
1060
1061 /**
1062 * Returns the current BE user.
1063 *
1064 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1065 */
1066 protected function getBackendUser()
1067 {
1068 return $GLOBALS['BE_USER'];
1069 }
1070
1071 /**
1072 * returns a new standalone view, shorthand function
1073 *
1074 * @return StandaloneView
1075 * @throws \InvalidArgumentException
1076 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException
1077 */
1078 protected function getStandaloneView()
1079 {
1080 /** @var StandaloneView $view */
1081 $view = GeneralUtility::makeInstance(StandaloneView::class);
1082 $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
1083 $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
1084 $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
1085
1086 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/Clipboard/Main.html'));
1087
1088 $view->getRequest()->setControllerExtensionName('Backend');
1089 return $view;
1090 }
1091 }