Passed
Push — master ( 3e1ea9...e51c76 )
by
unknown
14:02
created

FileList::isEditMetadataAllowed()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 3
nc 3
nop 1
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\Filelist;
17
18
use TYPO3\CMS\Backend\Clipboard\Clipboard;
19
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
20
use TYPO3\CMS\Backend\Routing\UriBuilder;
21
use TYPO3\CMS\Backend\Utility\BackendUtility;
22
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Imaging\Icon;
25
use TYPO3\CMS\Core\Imaging\IconFactory;
26
use TYPO3\CMS\Core\Localization\LanguageService;
27
use TYPO3\CMS\Core\Resource\AbstractFile;
28
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
29
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
30
use TYPO3\CMS\Core\Resource\File;
31
use TYPO3\CMS\Core\Resource\FileInterface;
32
use TYPO3\CMS\Core\Resource\Folder;
33
use TYPO3\CMS\Core\Resource\FolderInterface;
34
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
35
use TYPO3\CMS\Core\Resource\ProcessedFile;
36
use TYPO3\CMS\Core\Resource\ResourceFactory;
37
use TYPO3\CMS\Core\Resource\ResourceInterface;
38
use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
39
use TYPO3\CMS\Core\Resource\Utility\ListUtility;
40
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
41
use TYPO3\CMS\Core\Utility\GeneralUtility;
42
use TYPO3\CMS\Core\Utility\MathUtility;
43
use TYPO3\CMS\Core\Utility\PathUtility;
44
45
/**
46
 * Class for rendering of File>Filelist (basically used in FileListController)
47
 * @see \TYPO3\CMS\Filelist\Controller\FileListController
48
 * @internal this is a concrete TYPO3 controller implementation and solely used for EXT:filelist and not part of TYPO3's Core API.
49
 */
50
class FileList
51
{
52
    /**
53
     * Default Max items shown
54
     *
55
     * @var int
56
     */
57
    public $iLimit = 40;
58
59
    /**
60
     * Thumbnails on records containing files (pictures)
61
     *
62
     * @var bool
63
     */
64
    public $thumbs = false;
65
66
    /**
67
     * Space icon used for alignment when no button is available
68
     *
69
     * @var string
70
     */
71
    public $spaceIcon;
72
73
    /**
74
     * Max length of strings
75
     *
76
     * @var int
77
     */
78
    public $fixedL = 30;
79
80
    /**
81
     * The field to sort by
82
     *
83
     * @var string
84
     */
85
    public $sort = '';
86
87
    /**
88
     * Reverse sorting flag
89
     *
90
     * @var bool
91
     */
92
    public $sortRev = true;
93
94
    /**
95
     * @var int
96
     */
97
    public $firstElementNumber = 0;
98
99
    /**
100
     * @var int
101
     */
102
    public $totalbytes = 0;
103
104
    /**
105
     * This could be set to the total number of items. Used by the fwd_rew_navigation...
106
     *
107
     * @var int
108
     */
109
    public $totalItems = 0;
110
111
    /**
112
     * Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column.
113
     *
114
     * @var array
115
     */
116
    public $fieldArray = [];
117
118
    /**
119
     * Counter increased for each element. Used to index elements for the JavaScript-code that transfers to the clipboard
120
     *
121
     * @var int
122
     */
123
    public $counter = 0;
124
125
    /**
126
     * @var TranslationConfigurationProvider
127
     */
128
    public $translateTools;
129
130
    /**
131
     * Keys are fieldnames and values are td-css-classes to add in addElement();
132
     *
133
     * @var array
134
     */
135
    public $addElement_tdCssClass = [
136
        '_CONTROL_' => 'col-control',
137
        '_CLIPBOARD_' => 'col-clipboard',
138
        'file' => 'col-title col-responsive',
139
        '_LOCALIZATION_' => 'col-localizationa',
140
    ];
141
142
    /**
143
     * @var Folder
144
     */
145
    protected $folderObject;
146
147
    /**
148
     * @var array
149
     */
150
    public $CBnames = [];
151
152
    /**
153
     * @var Clipboard $clipObj
154
     */
155
    public $clipObj;
156
157
    /**
158
     * @var ResourceFactory
159
     */
160
    protected $resourceFactory;
161
162
    /**
163
     * @var IconFactory
164
     */
165
    protected $iconFactory;
166
167
    /**
168
     * @var int
169
     */
170
    protected $id = 0;
171
172
    /**
173
     * @var UriBuilder
174
     */
175
    protected $uriBuilder;
176
177
    protected ?FileSearchDemand $searchDemand = null;
178
179
    public function __construct()
180
    {
181
        // Setting the maximum length of the filenames to the user's settings or minimum 30 (= $this->fixedL)
182
        $this->fixedL = max($this->fixedL, $this->getBackendUser()->uc['titleLen'] ?? 1);
183
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
184
        $this->translateTools = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
185
        $this->iLimit = MathUtility::forceIntegerInRange(
186
            $this->getBackendUser()->getTSConfig()['options.']['file_list.']['filesPerPage'] ?? $this->iLimit,
187
            1
188
        );
189
        // Create clipboard object and initialize that
190
        $this->clipObj = GeneralUtility::makeInstance(Clipboard::class);
191
        $this->clipObj->fileMode = true;
192
        $this->clipObj->initializeClipboard();
193
        $this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
194
        $this->getLanguageService()->includeLLFile('EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf');
195
        $this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_common.xlf');
196
        $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
197
        $this->spaceIcon = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
198
    }
199
200
    /**
201
     * Initialization of class
202
     *
203
     * @param Folder $folderObject The folder to work on
204
     * @param int $pointer Pointer
205
     * @param string $sort Sorting column
206
     * @param bool $sortRev Sorting direction
207
     * @param bool $clipBoard
208
     */
209
    public function start(Folder $folderObject, $pointer, $sort, $sortRev, $clipBoard = false)
210
    {
211
        $this->folderObject = $folderObject;
212
        $this->counter = 0;
213
        $this->totalbytes = 0;
214
        $this->sort = $sort;
215
        $this->sortRev = $sortRev;
216
        $this->firstElementNumber = $pointer;
217
        // Cleaning rowlist for duplicates and place the $titleCol as the first column always!
218
        $rowlist = 'file,_LOCALIZATION_,_CONTROL_,fileext,tstamp,size,rw,_REF_';
219
        if ($clipBoard) {
220
            $rowlist = str_replace('_CONTROL_,', '_CONTROL_,_CLIPBOARD_,', $rowlist);
221
        }
222
        $this->fieldArray = explode(',', $rowlist);
223
    }
224
225
    /**
226
     * Wrapping input string in a link with clipboard command.
227
     *
228
     * @param string $string String to be linked - must be htmlspecialchar'ed / prepared before.
229
     * @param string $cmd "cmd" value
230
     * @param string $warning Warning for JS confirm message
231
     * @return string Linked string
232
     */
233
    public function linkClipboardHeaderIcon($string, $cmd, $warning = '')
234
    {
235
        if ($warning) {
236
            $attributes['class'] = 'btn btn-default t3js-modal-trigger';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$attributes was never initialized. Although not strictly required by PHP, it is generally a good practice to add $attributes = array(); before regardless.
Loading history...
237
            $attributes['data-severity'] = 'warning';
238
            $attributes['data-bs-content'] = $warning;
239
            $attributes['data-event-name'] = 'filelist:clipboard:cmd';
240
            $attributes['data-event-payload'] = $cmd;
241
        } else {
242
            $attributes['class'] = 'btn btn-default';
243
            $attributes['data-filelist-clipboard-cmd'] = $cmd;
244
        }
245
246
        return '<button type="button" ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $string . '</button>';
247
    }
248
249
    /**
250
     * Returns a table with directories and files listed.
251
     *
252
     * @param FileSearchDemand|null $searchDemand
253
     * @return string HTML-table
254
     */
255
    public function getTable(?FileSearchDemand $searchDemand = null): string
256
    {
257
        if ($searchDemand !== null) {
258
            // Store given search demand
259
            $this->searchDemand = $searchDemand;
260
            // Search currently only works for files
261
            $folders = [];
262
            // Find files by the given search demand
263
            $files = iterator_to_array($this->folderObject->searchFiles($this->searchDemand));
264
            // @todo Currently files, which got deleted in the file system, are still found.
265
            //       Therefore we have to ask their parent folder if it still contains the file.
266
            $files = array_filter($files, static function (FileInterface $file): bool {
267
                try {
268
                    if ($file->getParentFolder()->hasFile($file->getName())) {
269
                        return true;
270
                    }
271
                } catch (ResourceDoesNotExistException $e) {
272
                    // Nothing to do, file does not longer exist in folder
273
                }
274
                return false;
275
            });
276
277
            // @todo We have to manually slice the search result, since it may
278
            //       contain invalid files, which were manually filtered out above.
279
            //       This should be fixed, so we can use the $firstResult and $maxResults
280
            //       properties of the search demand directly.
281
            $this->totalItems = count($files);
282
            $filesNum = $this->firstElementNumber + $this->iLimit > $this->totalItems
283
                ? $this->totalItems - $this->firstElementNumber
284
                : $this->iLimit;
285
            $files = array_slice($files, $this->firstElementNumber, $filesNum);
286
287
            // Add special "Path" field for the search result
288
            array_unshift($this->fieldArray, '_PATH_');
289
        } else {
290
            // @todo use folder methods directly when they support filters
291
            $storage = $this->folderObject->getStorage();
292
            $storage->resetFileAndFolderNameFiltersToDefault();
293
294
            // Only render the contents of a browsable storage
295
            if (!$this->folderObject->getStorage()->isBrowsable()) {
296
                return '';
297
            }
298
            try {
299
                $foldersCount = $storage->countFoldersInFolder($this->folderObject);
300
                $filesCount = $storage->countFilesInFolder($this->folderObject);
301
            } catch (InsufficientFolderAccessPermissionsException $e) {
302
                $foldersCount = 0;
303
                $filesCount = 0;
304
            }
305
306
            if ($foldersCount <= $this->firstElementNumber) {
307
                $foldersFrom = false;
308
                $foldersNum = false;
309
            } else {
310
                $foldersFrom = $this->firstElementNumber;
311
                if ($this->firstElementNumber + $this->iLimit > $foldersCount) {
312
                    $foldersNum = $foldersCount - $this->firstElementNumber;
313
                } else {
314
                    $foldersNum = $this->iLimit;
315
                }
316
            }
317
            if ($foldersCount >= $this->firstElementNumber + $this->iLimit) {
318
                $filesFrom = false;
319
                $filesNum  = false;
320
            } elseif ($this->firstElementNumber <= $foldersCount) {
321
                $filesFrom = 0;
322
                $filesNum  = $this->iLimit - $foldersNum;
323
            } else {
324
                $filesFrom = $this->firstElementNumber - $foldersCount;
325
                if ($filesFrom + $this->iLimit > $filesCount) {
326
                    $filesNum = $filesCount - $filesFrom;
327
                } else {
328
                    $filesNum = $this->iLimit;
329
                }
330
            }
331
332
            $folders = $storage->getFoldersInFolder($this->folderObject, $foldersFrom, $foldersNum, true, false, trim($this->sort), (bool)$this->sortRev);
0 ignored issues
show
Bug introduced by
It seems like $foldersNum can also be of type false; however, parameter $maxNumberOfItems of TYPO3\CMS\Core\Resource\...e::getFoldersInFolder() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

332
            $folders = $storage->getFoldersInFolder($this->folderObject, $foldersFrom, /** @scrutinizer ignore-type */ $foldersNum, true, false, trim($this->sort), (bool)$this->sortRev);
Loading history...
Bug introduced by
It seems like $foldersFrom can also be of type false; however, parameter $start of TYPO3\CMS\Core\Resource\...e::getFoldersInFolder() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

332
            $folders = $storage->getFoldersInFolder($this->folderObject, /** @scrutinizer ignore-type */ $foldersFrom, $foldersNum, true, false, trim($this->sort), (bool)$this->sortRev);
Loading history...
333
            $files = $this->folderObject->getFiles($filesFrom, $filesNum, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, false, trim($this->sort), (bool)$this->sortRev);
0 ignored issues
show
Bug introduced by
It seems like $filesNum can also be of type false; however, parameter $numberOfItems of TYPO3\CMS\Core\Resource\Folder::getFiles() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

333
            $files = $this->folderObject->getFiles($filesFrom, /** @scrutinizer ignore-type */ $filesNum, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, false, trim($this->sort), (bool)$this->sortRev);
Loading history...
Bug introduced by
It seems like $filesFrom can also be of type false; however, parameter $start of TYPO3\CMS\Core\Resource\Folder::getFiles() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

333
            $files = $this->folderObject->getFiles(/** @scrutinizer ignore-type */ $filesFrom, $filesNum, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, false, trim($this->sort), (bool)$this->sortRev);
Loading history...
334
            $this->totalItems = $foldersCount + $filesCount;
335
336
            // Adds the code of files/dirs
337
            $folders = ListUtility::resolveSpecialFolderNames($folders);
338
        }
339
340
        $iOut = '';
341
        // Directories are added
342
        $iOut .= $this->fwd_rwd_nav($this->firstElementNumber);
343
344
        $iOut .= $this->formatDirList($folders);
345
        // Files are added
346
        $iOut .= $this->formatFileList($files);
347
348
        $amountOfItemsShownOnCurrentPage = $this->firstElementNumber + $this->iLimit < $this->totalItems
349
            ? $this->firstElementNumber + $this->iLimit
350
            : -1;
351
        $iOut .= $this->fwd_rwd_nav($amountOfItemsShownOnCurrentPage);
352
353
        // Header line is drawn
354
        $theData = [];
355
        foreach ($this->fieldArray as $v) {
356
            if ($v === '_CLIPBOARD_') {
357
                $theData[$v] = $this->renderClipboardHeaderRow(!empty($iOut));
358
            } elseif ($v === '_REF_') {
359
                $theData[$v] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._REF_'));
360
            } elseif ($v === '_PATH_') {
361
                $theData[$v] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._PATH_'));
362
            } else {
363
                // Normal row
364
                $theData[$v]  = $this->linkWrapSort($v);
365
            }
366
        }
367
368
        return '
369
            <div class="mb-4 mt-2">
370
                <div class="table-fit mb-0">
371
                    <table class="table table-striped table-hover" id="typo3-filelist">
372
                        <thead>' . $this->addElement('', $theData, true) . '</thead>
373
                        <tbody>' . $iOut . '</tbody>
374
                    </table>
375
                </div>
376
            </div>';
377
    }
378
379
    protected function renderClipboardHeaderRow(bool $hasContent): string
380
    {
381
        $cells = [];
382
        $elFromTable = $this->clipObj->elFromTable('_FILE');
383
        if (!empty($elFromTable) && $this->folderObject->checkActionPermission('write')) {
384
            $clipboardMode = $this->clipObj->clipData[$this->clipObj->current]['mode'] ?? '';
385
            $permission = $clipboardMode === 'copy' ? 'copy' : 'move';
386
            $addPasteButton = $this->folderObject->checkActionPermission($permission);
387
            $elToConfirm = [];
388
            foreach ($elFromTable as $key => $element) {
389
                $clipBoardElement = $this->resourceFactory->retrieveFileOrFolderObject($element);
390
                if ($clipBoardElement instanceof Folder && $clipBoardElement->getStorage()->isWithinFolder($clipBoardElement, $this->folderObject)) {
391
                    $addPasteButton = false;
392
                }
393
                $elToConfirm[$key] = $clipBoardElement->getName();
394
            }
395
            if ($addPasteButton) {
396
                $cells[] = '<a class="btn btn-default t3js-modal-trigger"' .
397
                    ' href="' . htmlspecialchars($this->clipObj->pasteUrl(
398
                        '_FILE',
399
                        $this->folderObject->getCombinedIdentifier()
400
                    )) . '"'
401
                    . ' data-bs-content="' . htmlspecialchars($this->clipObj->confirmMsgText(
402
                        '_FILE',
403
                        $this->folderObject->getReadablePath(),
404
                        'into',
405
                        $elToConfirm
406
                    )) . '"'
407
                    . ' data-severity="warning"'
408
                    . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_paste')) . '"'
409
                    . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_paste')) . '">'
410
                    . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)
411
                        ->render()
412
                    . '</a>';
413
            } else {
414
                $cells[] = $this->spaceIcon;
415
            }
416
        }
417
        if ($this->clipObj->current !== 'normal' && $hasContent) {
418
            $cells[] = $this->linkClipboardHeaderIcon('<span title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_selectMarked')) . '">' . $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL)->render() . '</span>', 'setCB');
419
            $cells[] = $this->linkClipboardHeaderIcon('<span title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_deleteMarked')) . '">' . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</span>', 'delete', $this->getLanguageService()->getLL('clip_deleteMarkedWarning'));
420
            $cells[] = '<a class="btn btn-default t3js-toggle-all-checkboxes" data-checkboxes-names="' . htmlspecialchars(implode(',', $this->CBnames)) . '" rel="" href="#" title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_markRecords')) . '">' . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a>';
421
        }
422
        if (!empty($cells)) {
423
            return '<div class="btn-group">' . implode('', $cells) . '</div>';
424
        }
425
        return '';
426
    }
427
428
    /**
429
     * Returns a table-row with the content from the fields in the input data array.
430
     * OBS: $this->fieldArray MUST be set! (represents the list of fields to display)
431
     *
432
     * @param string $icon Is the <img>+<a> of the record. If not supplied the first 'join'-icon will be a 'line' instead
433
     * @param array $data Is the data array, record with the fields. Notice: These fields are (currently) NOT htmlspecialchar'ed before being wrapped in <td>-tags
434
     * @param bool $isTableHeader Whether the element to be added is a table header
435
     *
436
     * @return string HTML content for the table row
437
     */
438
    public function addElement(string $icon, array $data, bool $isTableHeader = false): string
439
    {
440
        // Initialize additional data attributes for the row
441
        // Note: To be consistent with the other $data values, the additional data attributes
442
        // are not htmlspecialchar'ed before being added to the table row. Therefore it
443
        // has to be ensured they are properly escaped when applied to the $data array!
444
        $dataAttributes = [];
445
        foreach (['type', 'file-uid', 'metadata-uid', 'folder-identifier', 'combined-identifier'] as $dataAttribute) {
446
            if (isset($data[$dataAttribute])) {
447
                $dataAttributes['data-' . $dataAttribute] = $data[$dataAttribute];
448
                // Unset as we don't need them anymore, when building the table cells
449
                unset($data[$dataAttribute]);
450
            }
451
        }
452
453
        // Initialize rendering.
454
        $cols = [];
455
        $colType = $isTableHeader ? 'th' : 'td';
456
        $colspan = '';
457
        $colspanCounter = 0;
458
        $lastField = '';
459
        // Traverse field array which contains the data to present:
460
        foreach ($this->fieldArray as $fieldName) {
461
            if (isset($data[$fieldName])) {
462
                if ($lastField && isset($data[$lastField])) {
463
                    $cssClass = $this->addElement_tdCssClass[$lastField] ?? '';
464
                    $cols[] = '<' . $colType . ' class="' . $cssClass . '"' . $colspan . '>' . $data[$lastField] . '</' . $colType . '>';
465
                }
466
                $lastField = $fieldName;
467
                $colspanCounter = 1;
468
            } else {
469
                if (!$lastField) {
470
                    $lastField = $fieldName;
471
                }
472
                $colspanCounter++;
473
            }
474
            $colspan = ($colspanCounter > 1) ? ' colspan="' . $colspanCounter . '"' : '';
475
        }
476
        if ($lastField) {
477
            $cssClass = $this->addElement_tdCssClass[$lastField] ?? '';
478
            $cols[] = '<' . $colType . ' class="' . $cssClass . '"' . $colspan . '>' . $data[$lastField] . '</' . $colType . '>';
479
        }
480
481
        // Add the the table row
482
        return '
483
            <tr ' . GeneralUtility::implodeAttributes($dataAttributes) . '>
484
                <' . $colType . ' class="col-icon nowrap">' . ($icon ?: '') . '</' . $colType . '>'
485
                . implode(PHP_EOL, $cols) .
486
            '</tr>';
487
    }
488
489
    /**
490
     * Creates a forward/reverse button based on the status of ->eCounter, ->firstElementNumber, ->iLimit
491
     *
492
     * @return string the table-row code for the element
493
     */
494
    public function fwd_rwd_nav(int $currentItemCount): string
495
    {
496
        $code = '';
497
        if ($currentItemCount >= $this->firstElementNumber && $currentItemCount < $this->firstElementNumber + $this->iLimit) {
498
            if ($this->firstElementNumber && $currentItemCount == $this->firstElementNumber) {
499
                // 	Reverse
500
                $theData = [];
501
                $href = $this->listURL(['pointer' => ($currentItemCount - $this->iLimit)]);
502
                $theData['file'] = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
503
                    'actions-move-up',
504
                    Icon::SIZE_SMALL
505
                )->render() . ' <i>[' . (max(0, $currentItemCount - $this->iLimit) + 1) . ' - ' . $currentItemCount . ']</i></a>';
506
                $code = $this->addElement('', $theData);
507
            }
508
            return $code;
509
        }
510
        if ($currentItemCount === $this->firstElementNumber + $this->iLimit) {
511
            // 	Forward
512
            $theData = [];
513
            $href = $this->listURL(['pointer' => $currentItemCount]);
514
            $theData['file'] = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
515
                'actions-move-down',
516
                Icon::SIZE_SMALL
517
            )->render() . ' <i>[' . ($currentItemCount + 1) . ' - ' . $this->totalItems . ']</i></a>';
518
            $code = $this->addElement('', $theData);
519
        }
520
        return $code;
521
    }
522
523
    /**
524
     * Gets the number of files and total size of a folder
525
     *
526
     * @return string
527
     */
528
    public function getFolderInfo()
529
    {
530
        if ($this->counter == 1) {
531
            $fileLabel = htmlspecialchars($this->getLanguageService()->getLL('file'));
532
        } else {
533
            $fileLabel = htmlspecialchars($this->getLanguageService()->getLL('files'));
534
        }
535
        return $this->counter . ' ' . $fileLabel . ', ' . GeneralUtility::formatSize($this->totalbytes, htmlspecialchars($this->getLanguageService()->getLL('byteSizeUnits')));
536
    }
537
538
    /**
539
     * This returns tablerows for the directories in the array $items['sorting'].
540
     *
541
     * @param Folder[] $folders Folders of \TYPO3\CMS\Core\Resource\Folder
542
     * @return string HTML table rows.
543
     */
544
    public function formatDirList(array $folders)
545
    {
546
        $out = '';
547
        foreach ($folders as $folderName => $folderObject) {
548
            $role = $folderObject->getRole();
549
            if ($role === FolderInterface::ROLE_PROCESSING) {
550
                // don't show processing-folder
551
                continue;
552
            }
553
            if ($role !== FolderInterface::ROLE_DEFAULT) {
554
                $displayName = '<strong>' . htmlspecialchars($folderName) . '</strong>';
555
            } else {
556
                $displayName = htmlspecialchars($folderName);
557
            }
558
559
            $isLocked = $folderObject instanceof InaccessibleFolder;
560
            $isWritable = $folderObject->checkActionPermission('write');
561
562
            // Initialization
563
            $this->counter++;
564
565
            // The icon with link
566
            $theIcon = '<span title="' . htmlspecialchars($folderName) . '">' . $this->iconFactory->getIconForResource($folderObject, Icon::SIZE_SMALL)->render() . '</span>';
567
            if (!$isLocked) {
568
                $theIcon = (string)BackendUtility::wrapClickMenuOnIcon($theIcon, 'sys_file', $folderObject->getCombinedIdentifier());
569
            }
570
571
            // Preparing and getting the data-array
572
            $theData = [
573
                'type' => 'folder',
574
                'folder-identifier' => htmlspecialchars($folderObject->getIdentifier()),
575
                'combined-identifier' => htmlspecialchars($folderObject->getCombinedIdentifier()),
576
            ];
577
            if ($isLocked) {
578
                foreach ($this->fieldArray as $field) {
579
                    $theData[$field] = '';
580
                }
581
                $theData['file'] = $displayName;
582
            } else {
583
                foreach ($this->fieldArray as $field) {
584
                    switch ($field) {
585
                        case 'size':
586
                            try {
587
                                $numFiles = $folderObject->getFileCount();
588
                            } catch (InsufficientFolderAccessPermissionsException $e) {
589
                                $numFiles = 0;
590
                            }
591
                            $theData[$field] = $numFiles . ' ' . htmlspecialchars($this->getLanguageService()->getLL(($numFiles === 1 ? 'file' : 'files')));
592
                            break;
593
                        case 'rw':
594
                            $theData[$field] = '<strong class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('read')) . '</strong>' . (!$isWritable ? '' : '<strong class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('write')) . '</strong>');
595
                            break;
596
                        case 'fileext':
597
                            $theData[$field] = htmlspecialchars($this->getLanguageService()->getLL('folder'));
598
                            break;
599
                        case 'tstamp':
600
                            $tstamp = $folderObject->getModificationTime();
601
                            $theData[$field] = $tstamp ? BackendUtility::date($tstamp) : '-';
602
                            break;
603
                        case 'file':
604
                            $theData[$field] = $this->linkWrapDir($displayName, $folderObject);
605
                            break;
606
                        case '_CONTROL_':
607
                            $theData[$field] = $this->makeEdit($folderObject);
608
                            break;
609
                        case '_CLIPBOARD_':
610
                            $theData[$field] = $this->makeClip($folderObject);
611
                            break;
612
                        case '_REF_':
613
                            $theData[$field] = $this->makeRef($folderObject);
614
                            break;
615
                        case '_PATH_':
616
                            $theData[$field] = $this->makePath($folderObject);
617
                            break;
618
                        default:
619
                            $theData[$field] = GeneralUtility::fixed_lgd_cs($theData[$field] ?? '', $this->fixedL);
620
                    }
621
                }
622
            }
623
            $out .= $this->addElement($theIcon, $theData);
624
        }
625
        return $out;
626
    }
627
628
    /**
629
     * Wraps the directory-titles
630
     *
631
     * @param string $title String to be wrapped in links
632
     * @param Folder $folderObject Folder to work on
633
     * @return string HTML
634
     */
635
    public function linkWrapDir($title, Folder $folderObject)
636
    {
637
        $href = $this->listURL(['id' => $folderObject->getCombinedIdentifier(), 'searchTerm' => '', 'pointer' => 0]);
638
        $triggerTreeUpdateAttribute = sprintf(
639
            ' data-tree-update-request="%s"',
640
            htmlspecialchars($folderObject->getCombinedIdentifier())
641
        );
642
        // Sometimes $code contains plain HTML tags. In such a case the string should not be modified!
643
        if ((string)$title === strip_tags($title)) {
644
            return '<a href="' . htmlspecialchars($href) . '"' . $triggerTreeUpdateAttribute . ' title="' . htmlspecialchars($title) . '">' . $title . '</a>';
645
        }
646
        return '<a href="' . htmlspecialchars($href) . '"' . $triggerTreeUpdateAttribute . '>' . $title . '</a>';
647
    }
648
649
    /**
650
     * Wraps filenames in links which opens the metadata editor.
651
     *
652
     * @param string $code String to be wrapped in links
653
     * @param File $fileObject File to be linked
654
     * @return string HTML
655
     */
656
    public function linkWrapFile($code, File $fileObject)
657
    {
658
        try {
659
            if ($this->isEditMetadataAllowed($fileObject)) {
660
                $metaData = $fileObject->getMetaData()->get();
661
                $urlParameters = [
662
                    'edit' => [
663
                        'sys_file_metadata' => [
664
                            $metaData['uid'] => 'edit'
665
                        ]
666
                    ],
667
                    'returnUrl' => $this->listURL()
668
                ];
669
                $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
670
                $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata'));
671
                $code = '<a class="responsive-title" href="' . htmlspecialchars($url) . '" title="' . $title . '">' . $code . '</a>';
672
            }
673
        } catch (\Exception $e) {
674
            // intentional fall-through
675
        }
676
        return $code;
677
    }
678
679
    /**
680
     * Returns list URL; This is the URL of the current script with id and imagemode parameters, that's all.
681
     *
682
     * @return string URL
683
     */
684
    public function listURL(array $params = []): string
685
    {
686
        $params = array_replace_recursive([
687
            'pointer' => $this->firstElementNumber,
688
            'id' => $this->folderObject->getCombinedIdentifier(),
689
            'searchTerm' => $this->searchDemand ? $this->searchDemand->getSearchTerm() : ''
690
        ], $params);
691
        $params = array_filter($params);
692
        return (string)$this->uriBuilder->buildUriFromRoute('file_FilelistList', $params);
693
    }
694
695
    protected function getAvailableSystemLanguages(): array
696
    {
697
        // first two keys are "0" (default) and "-1" (multiple), after that comes the "other languages"
698
        $allSystemLanguages = $this->translateTools->getSystemLanguages();
699
        return array_filter($allSystemLanguages, function ($languageRecord) {
700
            if ($languageRecord['uid'] === -1 || $languageRecord['uid'] === 0 || !$this->getBackendUser()->checkLanguageAccess($languageRecord['uid'])) {
701
                return false;
702
            }
703
            return true;
704
        });
705
    }
706
    /**
707
     * This returns tablerows for the files in the array $items['sorting'].
708
     *
709
     * @param File[] $files File items
710
     * @return string HTML table rows.
711
     */
712
    public function formatFileList(array $files)
713
    {
714
        $out = '';
715
        $systemLanguages = $this->getAvailableSystemLanguages();
716
        foreach ($files as $fileObject) {
717
            // Initialization
718
            $this->counter++;
719
            $this->totalbytes += $fileObject->getSize();
720
            $ext = $fileObject->getExtension();
721
            $fileUid = $fileObject->getUid();
722
            $fileName = trim($fileObject->getName());
723
            // The icon with link
724
            $theIcon = '<span title="' . htmlspecialchars($fileName . ' [' . $fileUid . ']') . '">'
725
                . $this->iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render() . '</span>';
726
            $theIcon = (string)BackendUtility::wrapClickMenuOnIcon($theIcon, 'sys_file', $fileObject->getCombinedIdentifier());
727
            // Preparing and getting the data-array
728
            $theData = [
729
                'type' => 'file',
730
                'file-uid' => $fileUid
731
            ];
732
            if ($this->isEditMetadataAllowed($fileObject) && $fileObject->getMetaData()->offsetExists('uid')) {
733
                $theData['metadata-uid'] = htmlspecialchars((string)$fileObject->getMetaData()->offsetGet('uid'));
734
            }
735
            foreach ($this->fieldArray as $field) {
736
                switch ($field) {
737
                    case 'size':
738
                        $theData[$field] = GeneralUtility::formatSize((int)$fileObject->getSize(), htmlspecialchars($this->getLanguageService()->getLL('byteSizeUnits')));
739
                        break;
740
                    case 'rw':
741
                        $theData[$field] = '' . (!$fileObject->checkActionPermission('read') ? ' ' : '<strong class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('read')) . '</strong>') . (!$fileObject->checkActionPermission('write') ? '' : '<strong class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('write')) . '</strong>');
742
                        break;
743
                    case 'fileext':
744
                        $theData[$field] = htmlspecialchars(strtoupper($ext));
745
                        break;
746
                    case 'tstamp':
747
                        $theData[$field] = BackendUtility::date($fileObject->getModificationTime());
748
                        break;
749
                    case '_CONTROL_':
750
                        $theData[$field] = $this->makeEdit($fileObject);
751
                        break;
752
                    case '_CLIPBOARD_':
753
                        $theData[$field] = $this->makeClip($fileObject);
754
                        break;
755
                    case '_LOCALIZATION_':
756
                        if (!empty($systemLanguages) && $fileObject->isIndexed() && $fileObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata') && !empty($GLOBALS['TCA']['sys_file_metadata']['ctrl']['languageField'] ?? null)) {
757
                            $metaDataRecord = $fileObject->getMetaData()->get();
758
                            $translations = $this->getTranslationsForMetaData($metaDataRecord);
759
                            $languageCode = '';
760
761
                            foreach ($systemLanguages as $language) {
762
                                $languageId = $language['uid'];
763
                                $flagIcon = $language['flagIcon'];
764
                                if (array_key_exists($languageId, $translations)) {
765
                                    $title = htmlspecialchars(sprintf($this->getLanguageService()->getLL('editMetadataForLanguage'), $language['title']));
766
                                    $urlParameters = [
767
                                        'edit' => [
768
                                            'sys_file_metadata' => [
769
                                                $translations[$languageId]['uid'] => 'edit'
770
                                            ]
771
                                        ],
772
                                        'returnUrl' => $this->listURL()
773
                                    ];
774
                                    $flagButtonIcon = $this->iconFactory->getIcon($flagIcon, Icon::SIZE_SMALL, 'overlay-edit')->render();
775
                                    $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
776
                                    $languageCode .= '<a href="' . htmlspecialchars($url) . '" class="btn btn-default" title="' . $title . '">'
777
                                        . $flagButtonIcon . '</a>';
778
                                } else {
779
                                    $parameters = [
780
                                        'justLocalized' => 'sys_file_metadata:' . $metaDataRecord['uid'] . ':' . $languageId,
781
                                        'returnUrl' => $this->listURL()
782
                                    ];
783
                                    $returnUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $parameters);
784
                                    $href = BackendUtility::getLinkToDataHandlerAction(
785
                                        '&cmd[sys_file_metadata][' . $metaDataRecord['uid'] . '][localize]=' . $languageId,
786
                                        $returnUrl
787
                                    );
788
                                    $flagButtonIcon = '<span title="' . htmlspecialchars(sprintf($this->getLanguageService()->getLL('createMetadataForLanguage'), $language['title'])) . '">' . $this->iconFactory->getIcon($flagIcon, Icon::SIZE_SMALL, 'overlay-new')->render() . '</span>';
789
                                    $languageCode .= '<a href="' . htmlspecialchars($href) . '" class="btn btn-default">' . $flagButtonIcon . '</a> ';
790
                                }
791
                            }
792
793
                            // Hide flag button bar when not translated yet
794
                            $theData[$field] = ' <div class="localisationData btn-group' . (empty($translations) ? ' hidden' : '') . '" data-fileid="' . $fileUid . '">'
795
                                . $languageCode . '</div>';
796
                            $theData[$field] .= '<a class="btn btn-default filelist-translationToggler" data-fileid="' . $fileUid . '">' .
797
                                '<span title="' . htmlspecialchars($this->getLanguageService()->getLL('translateMetadata')) . '">'
798
                                . $this->iconFactory->getIcon('mimetypes-x-content-page-language-overlay', Icon::SIZE_SMALL)->render() . '</span>'
799
                                . '</a>';
800
                        }
801
                        break;
802
                    case '_REF_':
803
                        $theData[$field] = $this->makeRef($fileObject);
804
                        break;
805
                    case '_PATH_':
806
                        $theData[$field] = $this->makePath($fileObject);
807
                        break;
808
                    case 'file':
809
                        // Edit metadata of file
810
                        $theData[$field] = $this->linkWrapFile(htmlspecialchars($fileName), $fileObject);
811
812
                        if ($fileObject->isMissing()) {
813
                            $theData[$field] .= '<span class="label label-danger label-space-left">'
814
                                . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
815
                                . '</span>';
816
                        // Thumbnails?
817
                        } elseif ($this->thumbs && ($fileObject->isImage() || $fileObject->isMediaFile())) {
818
                            $processedFile = $fileObject->process(
819
                                ProcessedFile::CONTEXT_IMAGEPREVIEW,
820
                                [
821
                                    'width' => (int)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['thumbnail.']['width'] ?? 64),
822
                                    'height' => (int)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['thumbnail.']['height'] ?? 64),
823
                                ]
824
                            );
825
                            $theData[$field] .= '<br /><img src="' . htmlspecialchars(PathUtility::getAbsoluteWebPath($processedFile->getPublicUrl() ?? '')) . '" ' .
826
                                'width="' . htmlspecialchars($processedFile->getProperty('width')) . '" ' .
0 ignored issues
show
Bug introduced by
It seems like $processedFile->getProperty('width') can also be of type null; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

826
                                'width="' . htmlspecialchars(/** @scrutinizer ignore-type */ $processedFile->getProperty('width')) . '" ' .
Loading history...
827
                                'height="' . htmlspecialchars($processedFile->getProperty('height')) . '" ' .
828
                                'title="' . htmlspecialchars($fileName) . '" alt="" />';
829
                        }
830
                        break;
831
                    default:
832
                        $theData[$field] = '';
833
                        if ($fileObject->hasProperty($field)) {
834
                            $theData[$field] = htmlspecialchars(GeneralUtility::fixed_lgd_cs($fileObject->getProperty($field), $this->fixedL));
835
                        }
836
                }
837
            }
838
            $out .= $this->addElement($theIcon, $theData);
839
        }
840
        return $out;
841
    }
842
843
    /**
844
     * Fetch the translations for a sys_file_metadata record
845
     *
846
     * @param array $metaDataRecord
847
     * @return array keys are the sys_language uids, values are the $rows
848
     */
849
    protected function getTranslationsForMetaData($metaDataRecord)
850
    {
851
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_metadata');
852
        $queryBuilder->getRestrictions()->removeAll();
853
        $translationRecords = $queryBuilder->select('*')
854
            ->from('sys_file_metadata')
855
            ->where(
856
                $queryBuilder->expr()->eq(
857
                    $GLOBALS['TCA']['sys_file_metadata']['ctrl']['transOrigPointerField'],
858
                    $queryBuilder->createNamedParameter($metaDataRecord['uid'], \PDO::PARAM_INT)
859
                ),
860
                $queryBuilder->expr()->gt(
861
                    $GLOBALS['TCA']['sys_file_metadata']['ctrl']['languageField'],
862
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
863
                )
864
            )
865
            ->execute()
866
            ->fetchAll();
867
868
        $translations = [];
869
        foreach ($translationRecords as $record) {
870
            $translations[$record[$GLOBALS['TCA']['sys_file_metadata']['ctrl']['languageField']]] = $record;
871
        }
872
        return $translations;
873
    }
874
875
    /**
876
     * Wraps the directory-titles ($code) in a link to filelist/Modules/Filelist/index.php (id=$path) and sorting commands...
877
     *
878
     * @param string $col Sorting column
879
     * @return string HTML
880
     */
881
    public function linkWrapSort($col)
882
    {
883
        $code = htmlspecialchars($this->getLanguageService()->getLL('c_' . $col));
884
        $params = ['SET' => ['sort' => $col], 'pointer' => 0];
885
886
        if ($this->sort === $col) {
887
            // Check reverse sorting
888
            $params['SET']['reverse'] = ($this->sortRev ? '0' : '1');
889
            $sortArrow = $this->iconFactory->getIcon('status-status-sorting-' . ($this->sortRev ? 'desc' : 'asc'), Icon::SIZE_SMALL)->render();
890
        } else {
891
            $params['SET']['reverse'] = 0;
892
            $sortArrow = '';
893
        }
894
        $href = $this->listURL($params);
895
        return '<a href="' . htmlspecialchars($href) . '">' . $code . ' ' . $sortArrow . '</a>';
896
    }
897
898
    /**
899
     * Creates the clipboard control pad
900
     *
901
     * @param File|Folder $fileOrFolderObject Array with information about the file/directory for which to make the clipboard panel for the listing.
902
     * @return string HTML-table
903
     */
904
    public function makeClip($fileOrFolderObject)
905
    {
906
        if (!$fileOrFolderObject->checkActionPermission('read')) {
907
            return '';
908
        }
909
        $cells = [];
910
        $fullIdentifier = $fileOrFolderObject->getCombinedIdentifier();
911
        $fullName = $fileOrFolderObject->getName();
912
        $md5 = GeneralUtility::shortMD5($fullIdentifier);
913
        // For normal clipboard, add copy/cut buttons:
914
        if ($this->clipObj->current === 'normal') {
915
            $isSel = $this->clipObj->isSelected('_FILE', $md5);
0 ignored issues
show
Bug introduced by
$md5 of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Clipbo...Clipboard::isSelected(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

915
            $isSel = $this->clipObj->isSelected('_FILE', /** @scrutinizer ignore-type */ $md5);
Loading history...
916
            $copyTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy'));
917
            $cutTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut'));
918
            $copyIcon = $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL)->render();
919
            $cutIcon = $this->iconFactory->getIcon('actions-edit-cut', Icon::SIZE_SMALL)->render();
920
921
            if ($isSel === 'copy') {
922
                $copyIcon = $this->iconFactory->getIcon('actions-edit-copy-release', Icon::SIZE_SMALL)->render();
923
            } elseif ($isSel === 'cut') {
924
                $cutIcon = $this->iconFactory->getIcon('actions-edit-cut-release', Icon::SIZE_SMALL)->render();
925
            }
926
927
            if ($fileOrFolderObject->checkActionPermission('copy')) {
928
                $cells[] = '<a class="btn btn-default" href="' . htmlspecialchars($this->clipObj->selUrlFile(
929
                    $fullIdentifier,
930
                    1,
931
                    $isSel === 'copy'
932
                )) . '" title="' . $copyTitle . '">' . $copyIcon . '</a>';
933
            } else {
934
                $cells[] = $this->spaceIcon;
935
            }
936
            // we can only cut if file can be moved
937
            if ($fileOrFolderObject->checkActionPermission('move')) {
938
                $cells[] = '<a class="btn btn-default" href="' . htmlspecialchars($this->clipObj->selUrlFile(
939
                    $fullIdentifier,
940
                    0,
941
                    $isSel === 'cut'
942
                )) . '" title="' . $cutTitle . '">' . $cutIcon . '</a>';
943
            } else {
944
                $cells[] = $this->spaceIcon;
945
            }
946
        } else {
947
            // For numeric pads, add select checkboxes:
948
            $n = '_FILE|' . $md5;
949
            $this->CBnames[] = $n;
950
            $checked = $this->clipObj->isSelected('_FILE', $md5) ? ' checked="checked"' : '';
951
            $cells[] = '<label class="btn btn-default btn-checkbox"><input type="checkbox" name="CBC[' . $n . ']" value="' . htmlspecialchars($fullIdentifier) . '" ' . $checked . ' /><span class="t3-icon fa"></span><input type="hidden" name="CBH[' . $n . ']" value="0" /></label>';
952
        }
953
        // Display PASTE button, if directory:
954
        $elFromTable = $this->clipObj->elFromTable('_FILE');
955
        if ($fileOrFolderObject instanceof Folder && !empty($elFromTable) && $fileOrFolderObject->checkActionPermission('write')) {
956
            $clipboardMode = $this->clipObj->clipData[$this->clipObj->current]['mode'] ?? '';
957
            $permission = $clipboardMode === 'copy' ? 'copy' : 'move';
958
            $addPasteButton = $this->folderObject->checkActionPermission($permission);
959
            $elToConfirm = [];
960
            foreach ($elFromTable as $key => $element) {
961
                $clipBoardElement = $this->resourceFactory->retrieveFileOrFolderObject($element);
962
                if ($clipBoardElement instanceof Folder && $clipBoardElement->getStorage()->isWithinFolder($clipBoardElement, $fileOrFolderObject)) {
963
                    $addPasteButton = false;
964
                }
965
                $elToConfirm[$key] = $clipBoardElement->getName();
966
            }
967
            if ($addPasteButton) {
968
                $cells[] = '<a class="btn btn-default t3js-modal-trigger" '
969
                    . ' href="' . htmlspecialchars($this->clipObj->pasteUrl('_FILE', $fullIdentifier)) . '"'
970
                    . ' data-bs-content="' . htmlspecialchars($this->clipObj->confirmMsgText('_FILE', $fullName, 'into', $elToConfirm)) . '"'
971
                    . ' data-severity="warning"'
972
                    . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"'
973
                    . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"'
974
                    . '>'
975
                    . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
976
                    . '</a>';
977
            } else {
978
                $cells[] = $this->spaceIcon;
979
            }
980
        }
981
        // Compile items into a DIV-element:
982
        return ' <div class="btn-group" role="group">' . implode('', $cells) . '</div>';
983
    }
984
985
    /**
986
     * Creates the edit control section
987
     *
988
     * @param File|Folder $fileOrFolderObject Array with information about the file/directory for which to make the edit control section for the listing.
989
     * @return string HTML-table
990
     */
991
    public function makeEdit($fileOrFolderObject)
992
    {
993
        $cells = [];
994
        $fullIdentifier = $fileOrFolderObject->getCombinedIdentifier();
995
996
        // Edit file content (if editable)
997
        if ($fileOrFolderObject instanceof File && $fileOrFolderObject->checkActionPermission('write') && $fileOrFolderObject->isTextFile()) {
998
            $attributes = [
999
                'href' => (string)$this->uriBuilder->buildUriFromRoute('file_edit', ['target' => $fullIdentifier, 'returnUrl' => $this->listURL()]),
1000
                'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editcontent'),
1001
            ];
1002
            $cells['edit'] = '<a class="btn btn-default" ' . GeneralUtility::implodeAttributes($attributes, true) . '>'
1003
                . $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render()
1004
                . '</a>';
1005
        } else {
1006
            $cells['edit'] = $this->spaceIcon;
1007
        }
1008
1009
        // Edit metadata of file
1010
        if ($fileOrFolderObject instanceof File && $this->isEditMetadataAllowed($fileOrFolderObject)) {
1011
            $metaData = $fileOrFolderObject->getMetaData()->get();
1012
            $urlParameters = [
1013
                'edit' => [
1014
                    'sys_file_metadata' => [
1015
                        $metaData['uid'] => 'edit'
1016
                    ]
1017
                ],
1018
                'returnUrl' => $this->listURL()
1019
            ];
1020
            $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1021
            $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata'));
1022
            $cells['metadata'] = '<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . $title . '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
1023
        }
1024
1025
        // document view
1026
        if ($fileOrFolderObject instanceof File) {
1027
            $fileUrl = $fileOrFolderObject->getPublicUrl();
1028
            if ($fileUrl) {
1029
                $cells['view'] = '<a href="' . htmlspecialchars(PathUtility::getAbsoluteWebPath($fileUrl)) . '" target="_blank" class="btn btn-default" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view') . '">' . $this->iconFactory->getIcon('actions-document-view', Icon::SIZE_SMALL)->render() . '</a>';
1030
            } else {
1031
                $cells['view'] = $this->spaceIcon;
1032
            }
1033
        } else {
1034
            $cells['view'] = $this->spaceIcon;
1035
        }
1036
1037
        // replace file
1038
        if ($fileOrFolderObject instanceof File && $fileOrFolderObject->checkActionPermission('replace')) {
1039
            $attributes = [
1040
                'href' => (string)$this->uriBuilder->buildUriFromRoute('file_replace', ['target' => $fullIdentifier, 'uid' => $fileOrFolderObject->getUid(), 'returnUrl' => $this->listURL()]),
1041
                'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.replace'),
1042
            ];
1043
            $cells['replace'] = '<a class="btn btn-default" ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $this->iconFactory->getIcon('actions-edit-replace', Icon::SIZE_SMALL)->render() . '</a>';
1044
        }
1045
1046
        // rename the file
1047
        if ($fileOrFolderObject->checkActionPermission('rename')) {
1048
            $attributes = [
1049
                'href' => (string)$this->uriBuilder->buildUriFromRoute('file_rename', ['target' => $fullIdentifier, 'returnUrl' => $this->listURL()]),
1050
                'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.rename'),
1051
            ];
1052
            $cells['rename'] = '<a class="btn btn-default" ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $this->iconFactory->getIcon('actions-edit-rename', Icon::SIZE_SMALL)->render() . '</a>';
1053
        } else {
1054
            $cells['rename'] = $this->spaceIcon;
1055
        }
1056
1057
        // upload files
1058
        if ($fileOrFolderObject->getStorage()->checkUserActionPermission('add', 'File') && $fileOrFolderObject->checkActionPermission('write')) {
1059
            if ($fileOrFolderObject instanceof Folder) {
1060
                $attributes = [
1061
                    'href' => (string)$this->uriBuilder->buildUriFromRoute('file_upload', ['target' => $fullIdentifier, 'returnUrl' => $this->listURL()]),
1062
                    'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.upload'),
1063
                ];
1064
                $cells['upload'] = '<a class="btn btn-default" ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $this->iconFactory->getIcon('actions-edit-upload', Icon::SIZE_SMALL)->render() . '</a>';
1065
            }
1066
        }
1067
1068
        if ($fileOrFolderObject->checkActionPermission('read')) {
1069
            $attributes = [
1070
                'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info'),
1071
            ];
1072
            if ($fileOrFolderObject instanceof Folder || $fileOrFolderObject instanceof File) {
0 ignored issues
show
introduced by
$fileOrFolderObject is always a sub-type of TYPO3\CMS\Core\Resource\File.
Loading history...
1073
                $attributes['data-filelist-show-item-type'] = $fileOrFolderObject instanceof File ? '_FILE' : '_FOLDER';
1074
                $attributes['data-filelist-show-item-identifier'] = $fullIdentifier;
1075
            }
1076
            $cells['info'] = '<a href="#" class="btn btn-default" ' . GeneralUtility::implodeAttributes($attributes, true) . '>'
1077
                . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '</a>';
1078
        } else {
1079
            $cells['info'] = $this->spaceIcon;
1080
        }
1081
1082
        // delete the file
1083
        if ($fileOrFolderObject->checkActionPermission('delete')) {
1084
            $identifier = $fileOrFolderObject->getIdentifier();
1085
            if ($fileOrFolderObject instanceof Folder) {
1086
                $referenceCountText = BackendUtility::referenceCount('_FILE', $identifier, ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFolder'));
1087
                $deleteType = 'delete_folder';
1088
            } else {
1089
                $referenceCountText = BackendUtility::referenceCount('sys_file', (string)$fileOrFolderObject->getUid(), ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFile'));
1090
                $deleteType = 'delete_file';
1091
            }
1092
1093
            if ($this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE)) {
1094
                $confirmationCheck = '1';
1095
            } else {
1096
                $confirmationCheck = '0';
1097
            }
1098
1099
            $deleteUrl = (string)$this->uriBuilder->buildUriFromRoute('tce_file');
1100
            $confirmationMessage = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'), $fileOrFolderObject->getName()) . $referenceCountText;
1101
            $title = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete');
1102
            $cells['delete'] = '<a href="#" class="btn btn-default t3js-filelist-delete" data-bs-content="' . htmlspecialchars($confirmationMessage)
1103
                . '" data-check="' . $confirmationCheck
1104
                . '" data-delete-url="' . htmlspecialchars($deleteUrl)
1105
                . '" data-title="' . htmlspecialchars($title)
1106
                . '" data-identifier="' . htmlspecialchars($fileOrFolderObject->getCombinedIdentifier())
1107
                . '" data-delete-type="' . $deleteType
1108
                . '" title="' . htmlspecialchars($title) . '">'
1109
                . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
1110
        } else {
1111
            $cells['delete'] = $this->spaceIcon;
1112
        }
1113
1114
        // Hook for manipulating edit icons.
1115
        $cells['__fileOrFolderObject'] = $fileOrFolderObject;
1116
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['fileList']['editIconsHook'] ?? [] as $className) {
1117
            $hookObject = GeneralUtility::makeInstance($className);
1118
            if (!$hookObject instanceof FileListEditIconHookInterface) {
1119
                throw new \UnexpectedValueException(
1120
                    $className . ' must implement interface ' . FileListEditIconHookInterface::class,
1121
                    1235225797
1122
                );
1123
            }
1124
            $hookObject->manipulateEditIcons($cells, $this);
1125
        }
1126
        unset($cells['__fileOrFolderObject']);
1127
        // Compile items into a dropdown
1128
        $cellOutput = '';
1129
        $output = '';
1130
        foreach ($cells as $key => $action) {
1131
            if (in_array($key, ['view', 'metadata', 'delete'])) {
1132
                $output .= $action;
1133
                continue;
1134
            }
1135
            if ($action === $this->spaceIcon) {
1136
                continue;
1137
            }
1138
            // This is a backwards-compat layer for the existing hook items, which will be removed in TYPO3 v12.
1139
            $action = str_replace('btn btn-default', 'dropdown-item', $action);
1140
            $title = [];
1141
            preg_match('/title="([^"]*)"/', $action, $title);
1142
            if (empty($title)) {
1143
                preg_match('/aria-label="([^"]*)"/', $action, $title);
1144
            }
1145
            if (!empty($title[1] ?? '')) {
1146
                $action = str_replace('</a>', ' ' . $title[1] . '</a>', $action);
1147
                $action = str_replace('</button>', ' ' . $title[1] . '</button>', $action);
1148
            }
1149
            $cellOutput .= '<li>' . $action . '</li>';
1150
        }
1151
        $icon = $this->iconFactory->getIcon('actions-menu-alternative', Icon::SIZE_SMALL);
1152
        $output .= '<div class="btn-group dropdown position-static">' .
1153
            '<a href="#actions_' . $fileOrFolderObject->getHashedIdentifier() . '" class="btn btn-default dropdown-toggle dropdown-toggle-no-chevron" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">' . $icon->render() . '</a>' .
1154
            '<ul id="actions_' . $fileOrFolderObject->getHashedIdentifier() . '" class="dropdown-menu dropdown-list">' . $cellOutput . '</ul>' .
1155
            '</div>';
1156
        return '<div class="btn-group position-static">' . $output . '</div>';
1157
    }
1158
1159
    /**
1160
     * Make reference count
1161
     *
1162
     * @param File|Folder $fileOrFolderObject Array with information about the file/directory for which to make the clipboard panel for the listing.
1163
     * @return string HTML
1164
     */
1165
    public function makeRef($fileOrFolderObject)
1166
    {
1167
        if ($fileOrFolderObject instanceof FolderInterface) {
1168
            return '-';
1169
        }
1170
        // Look up the file in the sys_refindex.
1171
        // Exclude sys_file_metadata records as these are no use references
1172
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
1173
        $referenceCount = $queryBuilder->count('*')
1174
            ->from('sys_refindex')
1175
            ->where(
1176
                $queryBuilder->expr()->eq(
1177
                    'ref_table',
1178
                    $queryBuilder->createNamedParameter('sys_file', \PDO::PARAM_STR)
1179
                ),
1180
                $queryBuilder->expr()->eq(
1181
                    'ref_uid',
1182
                    $queryBuilder->createNamedParameter($fileOrFolderObject->getUid(), \PDO::PARAM_INT)
1183
                ),
1184
                $queryBuilder->expr()->neq(
1185
                    'tablename',
1186
                    $queryBuilder->createNamedParameter('sys_file_metadata', \PDO::PARAM_STR)
1187
                )
1188
            )
1189
            ->execute()
1190
            ->fetchColumn();
1191
1192
        return $this->generateReferenceToolTip($referenceCount, $fileOrFolderObject);
1193
    }
1194
1195
    /**
1196
     * Generate readable path
1197
     *
1198
     * @param ResourceInterface $resource
1199
     * @return string
1200
     */
1201
    protected function makePath(ResourceInterface $resource): string
1202
    {
1203
        $folder = null;
1204
        $method = 'getReadablePath';
1205
1206
        if ($resource instanceof FileInterface) {
1207
            $folder = $resource->getParentFolder();
1208
        } elseif ($resource instanceof FolderInterface) {
1209
            $folder = $resource;
1210
        }
1211
1212
        if ($folder === null || !is_callable([$folder, $method])) {
1213
            return '';
1214
        }
1215
1216
        return htmlspecialchars($folder->$method());
1217
    }
1218
1219
    /**
1220
     * Returns an instance of LanguageService
1221
     *
1222
     * @return LanguageService
1223
     */
1224
    protected function getLanguageService()
1225
    {
1226
        return $GLOBALS['LANG'];
1227
    }
1228
1229
    /**
1230
     * Returns the current BE user.
1231
     *
1232
     * @return BackendUserAuthentication
1233
     */
1234
    protected function getBackendUser()
1235
    {
1236
        return $GLOBALS['BE_USER'];
1237
    }
1238
1239
    /**
1240
     * Generates HTML code for a Reference tooltip out of
1241
     * sys_refindex records you hand over
1242
     *
1243
     * @param int $references number of records from sys_refindex table
1244
     * @param AbstractFile $fileObject
1245
     * @return string
1246
     */
1247
    protected function generateReferenceToolTip($references, $fileObject)
1248
    {
1249
        if (!$references) {
1250
            return '-';
1251
        }
1252
        $attributes = [
1253
            'data-filelist-show-item-type' => '_FILE',
1254
            'data-filelist-show-item-identifier' => $fileObject->getCombinedIdentifier(),
1255
            'title' => $this->getLanguageService()
1256
                ->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references')
1257
                . ' (' . $references . ')'
1258
        ];
1259
        $htmlCode = '<a href="#" ' . GeneralUtility::implodeAttributes($attributes, true) . '">';
1260
        $htmlCode .= $references;
1261
        $htmlCode .= '</a>';
1262
        return $htmlCode;
1263
    }
1264
1265
    protected function isEditMetadataAllowed(File $file): bool
1266
    {
1267
        return $file->isIndexed()
1268
            && $file->checkActionPermission('editMeta')
1269
            && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata');
1270
    }
1271
}
1272