Passed
Push — master ( a34d06...fb1b17 )
by
unknown
15:42
created

FileList::makePath()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

339
            $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

339
            $folders = $storage->getFoldersInFolder($this->folderObject, /** @scrutinizer ignore-type */ $foldersFrom, $foldersNum, true, false, trim($this->sort), (bool)$this->sortRev);
Loading history...
340
            $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

340
            $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

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

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

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