1<?php
2declare(strict_types = 1);
3namespace TYPO3\CMS\Filelist\ContextMenu\ItemProviders;
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18use TYPO3\CMS\Backend\Utility\BackendUtility;
19use TYPO3\CMS\Core\Resource\File;
20use TYPO3\CMS\Core\Resource\Folder;
21use TYPO3\CMS\Core\Resource\ResourceFactory;
22use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
23use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25/**
26 * Provides click menu items for files and folders
27 */
28class FileProvider extends \TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider
29{
30    /**
31     * @var File|Folder
32     */
33    protected $record;
34
35    /**
36     * @var array
37     */
38    protected $itemsConfiguration = [
39        'edit' => [
40            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit',
41            'iconIdentifier' => 'actions-page-open',
42            'callbackAction' => 'editFile'
43        ],
44        'rename' => [
45            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.rename',
46            'iconIdentifier' => 'actions-edit-rename',
47            'callbackAction' => 'renameFile'
48        ],
49        'upload' => [
50            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.upload',
51            'iconIdentifier' => 'actions-edit-upload',
52            'callbackAction' => 'uploadFile'
53        ],
54        'new' => [
55            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.new',
56            'iconIdentifier' => 'actions-document-new',
57            'callbackAction' => 'createFile'
58        ],
59        'info' => [
60            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info',
61            'iconIdentifier' => 'actions-document-info',
62            'callbackAction' => 'openInfoPopUp'
63        ],
64        'divider' => [
65            'type' => 'divider'
66        ],
67        'copy' => [
68            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
69            'iconIdentifier' => 'actions-edit-copy',
70            'callbackAction' => 'copyFile'
71        ],
72        'copyRelease' => [
73            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
74            'iconIdentifier' => 'actions-edit-copy-release',
75            'callbackAction' => 'copyReleaseFile'
76        ],
77        'cut' => [
78            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut',
79            'iconIdentifier' => 'actions-edit-cut',
80            'callbackAction' => 'cutFile'
81        ],
82        'cutRelease' => [
83            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease',
84            'iconIdentifier' => 'actions-edit-cut-release',
85            'callbackAction' => 'cutReleaseFile'
86        ],
87        'pasteInto' => [
88            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteinto',
89            'iconIdentifier' => 'actions-document-paste-into',
90            'callbackAction' => 'pasteFileInto'
91        ],
92        'divider2' => [
93            'type' => 'divider'
94        ],
95        'delete' => [
96            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete',
97            'iconIdentifier' => 'actions-edit-delete',
98            'callbackAction' => 'deleteFile'
99        ],
100    ];
101
102    /**
103     * @return bool
104     */
105    public function canHandle(): bool
106    {
107        return $this->table === 'sys_file';
108    }
109
110    /**
111     * Initialize file object
112     */
113    protected function initialize()
114    {
115        parent::initialize();
116        $fileObject = ResourceFactory::getInstance()
117                ->retrieveFileOrFolderObject($this->identifier);
118        $this->record = $fileObject;
119    }
120
121    /**
122     * Checks whether certain item can be rendered (e.g. check for disabled items or permissions)
123     *
124     * @param string $itemName
125     * @param string $type
126     * @return bool
127     */
128    protected function canRender(string $itemName, string $type): bool
129    {
130        if (in_array($type, ['divider', 'submenu'], true)) {
131            return true;
132        }
133        if (in_array($itemName, $this->disabledItems, true)) {
134            return false;
135        }
136        $canRender = false;
137        switch ($itemName) {
138            //just for files
139            case 'edit':
140                $canRender = $this->canBeEdited();
141                break;
142            case 'info':
143                $canRender = $this->canShowInfo();
144                break;
145
146            //just for folders
147            case 'upload':
148            case 'new':
149                $canRender = $this->canCreateNew();
150                break;
151            case 'pasteInto':
152                $canRender = $this->canBePastedInto();
153                break;
154
155            //for both files and folders
156            case 'rename':
157                $canRender = $this->canBeRenamed();
158                break;
159            case 'copy':
160                $canRender = $this->canBeCopied();
161                break;
162            case 'copyRelease':
163                $canRender = $this->isRecordInClipboard('copy');
164                break;
165            case 'cut':
166                $canRender = $this->canBeCut();
167                break;
168            case 'cutRelease':
169                $canRender = $this->isRecordInClipboard('cut');
170                break;
171            case 'delete':
172                $canRender = $this->canBeDeleted();
173                break;
174        }
175        return $canRender;
176    }
177
178    /**
179     * @return bool
180     */
181    protected function canBeEdited(): bool
182    {
183        return $this->isFile()
184           && $this->record->checkActionPermission('write')
185           && GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'], $this->record->getExtension());
186    }
187
188    /**
189     * @return bool
190     */
191    protected function canBeRenamed(): bool
192    {
193        return $this->record->checkActionPermission('rename');
194    }
195
196    /**
197     * @return bool
198     */
199    protected function canBeDeleted(): bool
200    {
201        return $this->record->checkActionPermission('delete');
202    }
203
204    /**
205     * @return bool
206     */
207    protected function canShowInfo(): bool
208    {
209        return $this->isFile();
210    }
211
212    /**
213     * @return bool
214     */
215    protected function canCreateNew(): bool
216    {
217        return $this->isFolder() && $this->record->checkActionPermission('write');
218    }
219
220    /**
221     * @return bool
222     */
223    protected function canBeCopied(): bool
224    {
225        return $this->record->checkActionPermission('read') && $this->record->checkActionPermission('copy') && !$this->isRecordInClipboard('copy');
226    }
227
228    /**
229     * @return bool
230     */
231    protected function canBeCut(): bool
232    {
233        return $this->record->checkActionPermission('move') && !$this->isRecordInClipboard('cut');
234    }
235
236    /**
237     * @return bool
238     */
239    protected function canBePastedInto(): bool
240    {
241        $elArr = $this->clipboard->elFromTable('_FILE');
242        if (empty($elArr)) {
243            return false;
244        }
245        $selItem = reset($elArr);
246        $fileOrFolderInClipBoard = ResourceFactory::getInstance()->retrieveFileOrFolderObject($selItem);
247
248        return $this->isFolder()
249            && $this->record->checkActionPermission('write')
250            && (
251                !$fileOrFolderInClipBoard instanceof Folder
252                || !$fileOrFolderInClipBoard->getStorage()->isWithinFolder($fileOrFolderInClipBoard, $this->record)
253            )
254            && $this->isFoldersAreInTheSameRoot($fileOrFolderInClipBoard);
255    }
256
257    /**
258     * Checks if folder and record are in the same filemount
259     * Cannot copy folders between filemounts
260     *
261     * @param  File|Folder $fileOrFolderInClipBoard
262     * @return bool
263     */
264    protected function isFoldersAreInTheSameRoot($fileOrFolderInClipBoard): bool
265    {
266        return (!$fileOrFolderInClipBoard instanceof Folder)
267            || (
268                $this->record->getStorage()->getRootLevelFolder()->getCombinedIdentifier()
269                == $fileOrFolderInClipBoard->getStorage()->getRootLevelFolder()->getCombinedIdentifier()
270            );
271    }
272
273    /**
274     * Checks if a file record is in the "normal" pad of the clipboard
275     *
276     * @param string $mode "copy", "cut" or '' for any mode
277     * @return bool
278     */
279    protected function isRecordInClipboard(string $mode = ''): bool
280    {
281        if ($mode !== '' && !$this->record->checkActionPermission($mode)) {
282            return false;
283        }
284        $isSelected = '';
285        // Pseudo table name for use in the clipboard.
286        $table = '_FILE';
287        $uid = GeneralUtility::shortMD5($this->record->getCombinedIdentifier());
288        if ($this->clipboard->current === 'normal') {
289            $isSelected = $this->clipboard->isSelected($table, $uid);
290        }
291        return $mode === '' ? !empty($isSelected) : $isSelected === $mode;
292    }
293
294    /**
295     * @return bool
296     */
297    protected function isStorageRoot(): bool
298    {
299        return $this->record->getIdentifier() === $this->record->getStorage()->getRootLevelFolder()->getIdentifier();
300    }
301
302    /**
303     * @return bool
304     */
305    protected function isFile(): bool
306    {
307        return $this->record instanceof File;
308    }
309
310    /**
311     * @return bool
312     */
313    protected function isFolder(): bool
314    {
315        return $this->record instanceof Folder;
316    }
317
318    /**
319     * @param string $itemName
320     * @return array
321     */
322    protected function getAdditionalAttributes(string $itemName): array
323    {
324        $attributes = [
325            'data-callback-module' => 'TYPO3/CMS/Filelist/ContextMenuActions'
326        ];
327        if ($itemName === 'delete' && $this->backendUser->jsConfirmation(JsConfirmation::DELETE)) {
328            $recordTitle = GeneralUtility::fixed_lgd_cs($this->record->getName(), $this->backendUser->uc['titleLen']);
329
330            $title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete');
331            $confirmMessage = sprintf(
332                $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'),
333                $recordTitle
334            );
335            if ($this->isFolder()) {
336                $confirmMessage .= BackendUtility::referenceCount(
337                    '_FILE',
338                    $this->record->getIdentifier(),
339                    ' ' . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFolder')
340                );
341            } else {
342                $confirmMessage .= BackendUtility::referenceCount(
343                    'sys_file',
344                    $this->record->getUid(),
345                    ' ' . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFile')
346                );
347            }
348            $closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:button.cancel');
349            $deleteText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:button.delete');
350            $attributes += [
351                'data-title' => htmlspecialchars($title),
352                'data-message' => htmlspecialchars($confirmMessage),
353                'data-button-close-text' => htmlspecialchars($closeText),
354                'data-button-ok-text' => htmlspecialchars($deleteText),
355            ];
356        }
357        if ($itemName === 'pasteInto' && $this->backendUser->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) {
358            $elArr = $this->clipboard->elFromTable('_FILE');
359            $selItem = reset($elArr);
360            $fileOrFolderInClipBoard = ResourceFactory::getInstance()->retrieveFileOrFolderObject($selItem);
361
362            $title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste');
363
364            $confirmMessage = sprintf(
365                $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.'
366                    . ($this->clipboard->currentMode() === 'copy' ? 'copy' : 'move') . '_into'),
367                $fileOrFolderInClipBoard->getName(),
368                $this->record->getName()
369            );
370            $closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:button.cancel');
371            $okLabel = $this->clipboard->currentMode() === 'copy' ? 'copy' : 'pasteinto';
372            $okText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . $okLabel);
373            $attributes += [
374                'data-title' => htmlspecialchars($title),
375                'data-message' => htmlspecialchars($confirmMessage),
376                'data-button-close-text' => htmlspecialchars($closeText),
377                'data-button-ok-text' => htmlspecialchars($okText),
378            ];
379        }
380
381        return $attributes;
382    }
383
384    /**
385     * @return string
386     */
387    protected function getIdentifier(): string
388    {
389        return $this->record->getCombinedIdentifier();
390    }
391}
392