Passed
Push — master ( 29ec4f...169eb0 )
by
unknown
17:16
created

Clipboard::initializeClipboard()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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