[BUGFIX] Make meta data editable for non-writable storages
[Packages/TYPO3.CMS.git] / typo3 / sysext / filelist / Classes / FileList.php
1 <?php
2 namespace TYPO3\CMS\Filelist;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Clipboard\Clipboard;
18 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
19 use TYPO3\CMS\Backend\Routing\UriBuilder;
20 use TYPO3\CMS\Backend\Utility\BackendUtility;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
23 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24 use TYPO3\CMS\Core\Imaging\Icon;
25 use TYPO3\CMS\Core\Imaging\IconFactory;
26 use TYPO3\CMS\Core\Messaging\FlashMessage;
27 use TYPO3\CMS\Core\Messaging\FlashMessageService;
28 use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
29 use TYPO3\CMS\Core\Resource\File;
30 use TYPO3\CMS\Core\Resource\Folder;
31 use TYPO3\CMS\Core\Resource\FolderInterface;
32 use TYPO3\CMS\Core\Resource\InaccessibleFolder;
33 use TYPO3\CMS\Core\Resource\ProcessedFile;
34 use TYPO3\CMS\Core\Resource\ResourceFactory;
35 use TYPO3\CMS\Core\Resource\Utility\ListUtility;
36 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
37 use TYPO3\CMS\Core\Utility\GeneralUtility;
38 use TYPO3\CMS\Core\Utility\MathUtility;
39 use TYPO3\CMS\Filelist\Configuration\ThumbnailConfiguration;
40 use TYPO3\CMS\Filelist\Controller\FileListController;
41
42 /**
43 * Class for rendering of File>Filelist
44 * @internal this is a concrete TYPO3 controller implementation and solely used for EXT:filelist and not part of TYPO3's Core API.
45 */
46 class FileList
47 {
48 /**
49 * Default Max items shown
50 *
51 * @var int
52 */
53 public $iLimit = 40;
54
55 /**
56 * Thumbnails on records containing files (pictures)
57 *
58 * @var bool
59 */
60 public $thumbs = false;
61
62 /**
63 * Space icon used for alignment when no button is available
64 *
65 * @var string
66 */
67 public $spaceIcon;
68
69 /**
70 * Max length of strings
71 *
72 * @var int
73 */
74 public $fixedL = 30;
75
76 /**
77 * The field to sort by
78 *
79 * @var string
80 */
81 public $sort = '';
82
83 /**
84 * Reverse sorting flag
85 *
86 * @var bool
87 */
88 public $sortRev = 1;
89
90 /**
91 * @var int
92 */
93 public $firstElementNumber = 0;
94
95 /**
96 * @var bool
97 */
98 public $clipBoard = 0;
99
100 /**
101 * @var bool
102 */
103 public $bigControlPanel = 0;
104
105 /**
106 * @var string
107 */
108 public $JScode = '';
109
110 /**
111 * String with accumulated HTML content
112 *
113 * @var string
114 */
115 public $HTMLcode = '';
116
117 /**
118 * @var int
119 */
120 public $totalbytes = 0;
121
122 /**
123 * @var array
124 */
125 public $dirs = [];
126
127 /**
128 * @var array
129 */
130 public $files = [];
131
132 /**
133 * @var string
134 */
135 public $path = '';
136
137 /**
138 * OBSOLETE - NOT USED ANYMORE. leftMargin
139 *
140 * @var int
141 */
142 public $leftMargin = 0;
143
144 /**
145 * This could be set to the total number of items. Used by the fwd_rew_navigation...
146 *
147 * @var string
148 */
149 public $totalItems = '';
150
151 /**
152 * Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column.
153 *
154 * @var array
155 */
156 public $fieldArray = [];
157
158 /**
159 * Set to zero, if you don't want a left-margin with addElement function
160 *
161 * @var int
162 */
163 public $setLMargin = 1;
164
165 /**
166 * Contains page translation languages
167 *
168 * @var array
169 */
170 public $pageOverlays = [];
171
172 /**
173 * Counter increased for each element. Used to index elements for the JavaScript-code that transfers to the clipboard
174 *
175 * @var int
176 */
177 public $counter = 0;
178
179 /**
180 * Contains sys language icons and titles
181 *
182 * @var array
183 */
184 public $languageIconTitles = [];
185
186 /**
187 * Script URL
188 *
189 * @var string
190 */
191 public $thisScript = '';
192
193 /**
194 * If set this is <td> CSS-classname for odd columns in addElement. Used with db_layout / pages section
195 *
196 * @var string
197 */
198 public $oddColumnsCssClass = '';
199
200 /**
201 * Counting the elements no matter what
202 *
203 * @var int
204 */
205 public $eCounter = 0;
206
207 /**
208 * @var TranslationConfigurationProvider
209 */
210 public $translateTools;
211
212 /**
213 * Keys are fieldnames and values are td-parameters to add in addElement(), please use $addElement_tdCSSClass for CSS-classes;
214 *
215 * @var array
216 */
217 public $addElement_tdParams = [];
218
219 /**
220 * @var int
221 */
222 public $no_noWrap = 0;
223
224 /**
225 * @var int
226 */
227 public $showIcon = 1;
228
229 /**
230 * Keys are fieldnames and values are td-css-classes to add in addElement();
231 *
232 * @var array
233 */
234 public $addElement_tdCssClass = [];
235
236 /**
237 * @var Folder
238 */
239 protected $folderObject;
240
241 /**
242 * @var array
243 */
244 public $CBnames = [];
245
246 /**
247 * @var Clipboard $clipObj
248 */
249 public $clipObj;
250
251 /**
252 * @var ResourceFactory
253 */
254 protected $resourceFactory;
255
256 /**
257 * @var IconFactory
258 */
259 protected $iconFactory;
260
261 /**
262 * @var int
263 */
264 protected $id = 0;
265
266 /**
267 * @var FileListController
268 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
269 */
270 protected $fileListController;
271
272 /**
273 * @var ThumbnailConfiguration
274 */
275 protected $thumbnailConfiguration;
276
277 /**
278 * Construct
279 *
280 * @param FileListController $fileListController @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
281 */
282 public function __construct(FileListController $fileListController = null)
283 {
284 $backendUser = $this->getBackendUser();
285 if (isset($backendUser->uc['titleLen']) && $backendUser->uc['titleLen'] > 0) {
286 $this->fixedL = $backendUser->uc['titleLen'];
287 }
288 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
289 $this->getTranslateTools();
290 $this->determineScriptUrl();
291 $this->fileListController = $fileListController;
292 $this->thumbnailConfiguration = GeneralUtility::makeInstance(ThumbnailConfiguration::class);
293 $this->iLimit = MathUtility::forceIntegerInRange(
294 $backendUser->getTSConfig()['options.']['file_list.']['filesPerPage'] ?? $this->iLimit,
295 1
296 );
297 }
298
299 /**
300 * @param ResourceFactory $resourceFactory
301 */
302 public function injectResourceFactory(ResourceFactory $resourceFactory)
303 {
304 $this->resourceFactory = $resourceFactory;
305 }
306
307 /**
308 * Initialization of class
309 *
310 * @param Folder $folderObject The folder to work on
311 * @param int $pointer Pointer
312 * @param bool $sort Sorting column
313 * @param bool $sortRev Sorting direction
314 * @param bool $clipBoard
315 * @param bool $bigControlPanel Show clipboard flag
316 */
317 public function start(Folder $folderObject, $pointer, $sort, $sortRev, $clipBoard = false, $bigControlPanel = false)
318 {
319 $this->folderObject = $folderObject;
320 $this->counter = 0;
321 $this->totalbytes = 0;
322 $this->JScode = '';
323 $this->HTMLcode = '';
324 $this->path = $folderObject->getReadablePath();
325 $this->sort = $sort;
326 $this->sortRev = $sortRev;
327 $this->firstElementNumber = $pointer;
328 $this->clipBoard = $clipBoard;
329 $this->bigControlPanel = $bigControlPanel;
330 // Setting the maximum length of the filenames to the user's settings or minimum 30 (= $this->fixedL)
331 $this->fixedL = max($this->fixedL, $this->getBackendUser()->uc['titleLen']);
332 $this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_common.xlf');
333 $this->resourceFactory = ResourceFactory::getInstance();
334 }
335
336 /**
337 * Reading files and directories, counting elements and generating the list in ->HTMLcode
338 */
339 public function generateList()
340 {
341 $this->HTMLcode .= $this->getTable('fileext,tstamp,size,rw,_REF_');
342 }
343
344 /**
345 * Wrapping input string in a link with clipboard command.
346 *
347 * @param string $string String to be linked - must be htmlspecialchar'ed / prepared before.
348 * @param string $_ unused
349 * @param string $cmd "cmd" value
350 * @param string $warning Warning for JS confirm message
351 * @return string Linked string
352 */
353 public function linkClipboardHeaderIcon($string, $_, $cmd, $warning = '')
354 {
355 $jsCode = 'document.dblistForm.cmd.value=' . GeneralUtility::quoteJSvalue($cmd)
356 . ';document.dblistForm.submit();';
357
358 $attributes = [];
359 if ($warning) {
360 $attributes['class'] = 'btn btn-default t3js-modal-trigger';
361 $attributes['data-href'] = 'javascript:' . $jsCode;
362 $attributes['data-severity'] = 'warning';
363 $attributes['data-content'] = $warning;
364 } else {
365 $attributes['class'] = 'btn btn-default';
366 $attributes['onclick'] = $jsCode . 'return false;';
367 }
368
369 $attributesString = '';
370 foreach ($attributes as $key => $value) {
371 $attributesString .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
372 }
373 return '<a href="#" ' . $attributesString . '>' . $string . '</a>';
374 }
375
376 /**
377 * Returns a table with directories and files listed.
378 *
379 * @param array $rowlist Array of files from path
380 * @return string HTML-table
381 */
382 public function getTable($rowlist)
383 {
384 // prepare space icon
385 $this->spaceIcon = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
386
387 // @todo use folder methods directly when they support filters
388 $storage = $this->folderObject->getStorage();
389 $storage->resetFileAndFolderNameFiltersToDefault();
390
391 // Only render the contents of a browsable storage
392 if ($this->folderObject->getStorage()->isBrowsable()) {
393 try {
394 $foldersCount = $storage->countFoldersInFolder($this->folderObject);
395 $filesCount = $storage->countFilesInFolder($this->folderObject);
396 } catch (InsufficientFolderAccessPermissionsException $e) {
397 $foldersCount = 0;
398 $filesCount = 0;
399 }
400
401 if ($foldersCount <= $this->firstElementNumber) {
402 $foldersFrom = false;
403 $foldersNum = false;
404 } else {
405 $foldersFrom = $this->firstElementNumber;
406 if ($this->firstElementNumber + $this->iLimit > $foldersCount) {
407 $foldersNum = $foldersCount - $this->firstElementNumber;
408 } else {
409 $foldersNum = $this->iLimit;
410 }
411 }
412 if ($foldersCount >= $this->firstElementNumber + $this->iLimit) {
413 $filesFrom = false;
414 $filesNum = false;
415 } else {
416 if ($this->firstElementNumber <= $foldersCount) {
417 $filesFrom = 0;
418 $filesNum = $this->iLimit - $foldersNum;
419 } else {
420 $filesFrom = $this->firstElementNumber - $foldersCount;
421 if ($filesFrom + $this->iLimit > $filesCount) {
422 $filesNum = $filesCount - $filesFrom;
423 } else {
424 $filesNum = $this->iLimit;
425 }
426 }
427 }
428
429 $folders = $storage->getFoldersInFolder($this->folderObject, $foldersFrom, $foldersNum, true, false, trim($this->sort), (bool)$this->sortRev);
430 $files = $this->folderObject->getFiles($filesFrom, $filesNum, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, false, trim($this->sort), (bool)$this->sortRev);
431 $this->totalItems = $foldersCount + $filesCount;
432 // Adds the code of files/dirs
433 $out = '';
434 $titleCol = 'file';
435 // Cleaning rowlist for duplicates and place the $titleCol as the first column always!
436 $rowlist = '_LOCALIZATION_,' . $rowlist;
437 $rowlist = GeneralUtility::rmFromList($titleCol, $rowlist);
438 $rowlist = GeneralUtility::uniqueList($rowlist);
439 $rowlist = $rowlist ? $titleCol . ',' . $rowlist : $titleCol;
440 if ($this->clipBoard) {
441 $rowlist = str_replace('_LOCALIZATION_,', '_LOCALIZATION_,_CLIPBOARD_,', $rowlist);
442 $this->addElement_tdCssClass['_CLIPBOARD_'] = 'col-clipboard';
443 }
444 if ($this->bigControlPanel) {
445 $rowlist = str_replace('_LOCALIZATION_,', '_LOCALIZATION_,_CONTROL_,', $rowlist);
446 $this->addElement_tdCssClass['_CONTROL_'] = 'col-control';
447 }
448 $this->fieldArray = explode(',', $rowlist);
449
450 // Add classes to table cells
451 $this->addElement_tdCssClass[$titleCol] = 'col-title col-responsive';
452 $this->addElement_tdCssClass['_LOCALIZATION_'] = 'col-localizationa';
453
454 $folders = ListUtility::resolveSpecialFolderNames($folders);
455
456 $iOut = '';
457 // Directories are added
458 $this->eCounter = $this->firstElementNumber;
459 list(, $code) = $this->fwd_rwd_nav();
460 $iOut .= $code;
461
462 $iOut .= $this->formatDirList($folders);
463 // Files are added
464 $iOut .= $this->formatFileList($files);
465
466 $this->eCounter = $this->firstElementNumber + $this->iLimit < $this->totalItems
467 ? $this->firstElementNumber + $this->iLimit
468 : -1;
469 list(, $code) = $this->fwd_rwd_nav();
470 $iOut .= $code;
471
472 // Header line is drawn
473 $theData = [];
474 foreach ($this->fieldArray as $v) {
475 if ($v === '_CLIPBOARD_' && $this->clipBoard) {
476 $cells = [];
477 $table = '_FILE';
478 $elFromTable = $this->clipObj->elFromTable($table);
479 if (!empty($elFromTable) && $this->folderObject->checkActionPermission('write')) {
480 $addPasteButton = true;
481 $elToConfirm = [];
482 foreach ($elFromTable as $key => $element) {
483 $clipBoardElement = $this->resourceFactory->retrieveFileOrFolderObject($element);
484 if ($clipBoardElement instanceof Folder && $clipBoardElement->getStorage()->isWithinFolder($clipBoardElement, $this->folderObject)) {
485 $addPasteButton = false;
486 }
487 $elToConfirm[$key] = $clipBoardElement->getName();
488 }
489 if ($addPasteButton) {
490 $cells[] = '<a class="btn btn-default t3js-modal-trigger"' .
491 ' href="' . htmlspecialchars($this->clipObj->pasteUrl(
492 '_FILE',
493 $this->folderObject->getCombinedIdentifier()
494 )) . '"'
495 . ' data-content="' . htmlspecialchars($this->clipObj->confirmMsgText(
496 '_FILE',
497 $this->path,
498 'into',
499 $elToConfirm
500 )) . '"'
501 . ' data-severity="warning"'
502 . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_paste')) . '"'
503 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_paste')) . '">'
504 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)
505 ->render()
506 . '</a>';
507 }
508 }
509 if ($this->clipObj->current !== 'normal' && $iOut) {
510 $cells[] = $this->linkClipboardHeaderIcon('<span title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_selectMarked')) . '">' . $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL)->render() . '</span>', $table, 'setCB');
511 $cells[] = $this->linkClipboardHeaderIcon('<span title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_deleteMarked')) . '">' . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render(), $table, 'delete', $this->getLanguageService()->getLL('clip_deleteMarkedWarning'));
512 $onClick = 'checkOffCB(' . GeneralUtility::quoteJSvalue(implode(',', $this->CBnames)) . ', this); return false;';
513 $cells[] = '<a class="btn btn-default" rel="" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_markRecords')) . '">' . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a>';
514 }
515 $theData[$v] = implode('', $cells);
516 } else {
517 // Normal row:
518 $theT = $this->linkWrapSort(htmlspecialchars($this->getLanguageService()->getLL('c_' . $v)), $this->folderObject->getCombinedIdentifier(), $v);
519 $theData[$v] = $theT;
520 }
521 }
522
523 $out .= '<thead>' . $this->addElement(1, '', $theData, '', '', '', 'th') . '</thead>';
524 $out .= '<tbody>' . $iOut . '</tbody>';
525 // half line is drawn
526 // finish
527 $out = '
528 <!--
529 Filelist table:
530 -->
531 <div class="panel panel-default">
532 <div class="table-fit">
533 <table class="table table-striped table-hover" id="typo3-filelist">
534 ' . $out . '
535 </table>
536 </div>
537 </div>';
538 } else {
539 /** @var FlashMessage $flashMessage */
540 $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $this->getLanguageService()->getLL('storageNotBrowsableMessage'), $this->getLanguageService()->getLL('storageNotBrowsableTitle'), FlashMessage::INFO);
541 /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
542 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
543 /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
544 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
545 $defaultFlashMessageQueue->enqueue($flashMessage);
546 $out = '';
547 }
548 return $out;
549 }
550
551 /**
552 * Returns a table-row with the content from the fields in the input data array.
553 * OBS: $this->fieldArray MUST be set! (represents the list of fields to display)
554 *
555 * @param int $h Is an integer >=0 and denotes how tall an element is. Set to '0' makes a halv line, -1 = full line, set to 1 makes a 'join' and above makes 'line'
556 * @param string $icon Is the <img>+<a> of the record. If not supplied the first 'join'-icon will be a 'line' instead
557 * @param array $data Is the dataarray, record with the fields. Notice: These fields are (currently) NOT htmlspecialchar'ed before being wrapped in <td>-tags
558 * @param string $rowParams Is insert in the <tr>-tags. Must carry a ' ' as first character
559 * @param string $_ OBSOLETE - NOT USED ANYMORE. $lMargin is the leftMargin (int)
560 * @param string $_2 OBSOLETE - NOT USED ANYMORE. Is the HTML <img>-tag for an alternative 'gfx/ol/line.gif'-icon (used in the top)
561 * @param string $colType Defines the tag being used for the columns. Default is td.
562 *
563 * @return string HTML content for the table row
564 */
565 public function addElement($h, $icon, $data, $rowParams = '', $_ = '', $_2 = '', $colType = 'td')
566 {
567 $colType = ($colType === 'th') ? 'th' : 'td';
568 $noWrap = $this->no_noWrap ? '' : ' nowrap';
569 // Start up:
570 $l10nParent = (int)($data['_l10nparent_'] ?? 0);
571 $out = '
572 <!-- Element, begin: -->
573 <tr ' . $rowParams . ' data-uid="' . (int)($data['uid'] ?? 0) . '" data-l10nparent="' . $l10nParent . '">';
574 // Show icon and lines
575 if ($this->showIcon) {
576 $out .= '
577 <' . $colType . ' class="col-icon nowrap">';
578 if (!$h) {
579 $out .= '&nbsp;';
580 } else {
581 for ($a = 0; $a < $h; $a++) {
582 if (!$a) {
583 if ($icon) {
584 $out .= $icon;
585 }
586 }
587 }
588 }
589 $out .= '</' . $colType . '>
590 ';
591 }
592 // Init rendering.
593 $colsp = '';
594 $lastKey = '';
595 $c = 0;
596 $ccount = 0;
597 // __label is used as the label key to circumvent problems with uid used as label (see #67756)
598 // as it was introduced later on, check if it really exists before using it
599 $fields = $this->fieldArray;
600 if ($colType === 'td' && array_key_exists('__label', $data)) {
601 $fields[0] = '__label';
602 }
603 // Traverse field array which contains the data to present:
604 foreach ($fields as $vKey) {
605 if (isset($data[$vKey])) {
606 if ($lastKey) {
607 $cssClass = $this->addElement_tdCssClass[$lastKey] ?? '';
608 if ($this->oddColumnsCssClass && $ccount % 2 == 0) {
609 $cssClass = implode(' ', [$cssClass, $this->oddColumnsCssClass]);
610 }
611 $out .= '
612 <' . $colType . ' class="' . $cssClass . $noWrap . '"' . $colsp . ($this->addElement_tdParams[$lastKey] ?? '') . '>' . $data[$lastKey] . '</' . $colType . '>';
613 }
614 $lastKey = $vKey;
615 $c = 1;
616 $ccount++;
617 } else {
618 if (!$lastKey) {
619 $lastKey = $vKey;
620 }
621 $c++;
622 }
623 if ($c > 1) {
624 $colsp = ' colspan="' . $c . '"';
625 } else {
626 $colsp = '';
627 }
628 }
629 if ($lastKey) {
630 $cssClass = $this->addElement_tdCssClass[$lastKey] ?? '';
631 if ($this->oddColumnsCssClass) {
632 $cssClass = implode(' ', [$cssClass, $this->oddColumnsCssClass]);
633 }
634 $out .= '
635 <' . $colType . ' class="' . $cssClass . $noWrap . '"' . $colsp . ($this->addElement_tdParams[$lastKey] ?? '') . '>' . $data[$lastKey] . '</' . $colType . '>';
636 }
637 // End row
638 $out .= '
639 </tr>';
640 // Return row.
641 return $out;
642 }
643
644 /**
645 * Dummy function, used to write the top of a table listing.
646 */
647 public function writeTop()
648 {
649 }
650
651 /**
652 * Creates a forward/reverse button based on the status of ->eCounter, ->firstElementNumber, ->iLimit
653 *
654 * @param string $table Table name
655 * @return array array([boolean], [HTML]) where [boolean] is 1 for reverse element, [HTML] is the table-row code for the element
656 */
657 public function fwd_rwd_nav($table = '')
658 {
659 $code = '';
660 if ($this->eCounter >= $this->firstElementNumber && $this->eCounter < $this->firstElementNumber + $this->iLimit) {
661 if ($this->firstElementNumber && $this->eCounter == $this->firstElementNumber) {
662 // Reverse
663 $theData = [];
664 $titleCol = $this->fieldArray[0];
665 $theData[$titleCol] = $this->fwd_rwd_HTML('fwd', $this->eCounter, $table);
666 $code = $this->addElement(1, '', $theData, 'class="fwd_rwd_nav"');
667 }
668 return [1, $code];
669 }
670 if ($this->eCounter == $this->firstElementNumber + $this->iLimit) {
671 // Forward
672 $theData = [];
673 $titleCol = $this->fieldArray[0];
674 $theData[$titleCol] = $this->fwd_rwd_HTML('rwd', $this->eCounter, $table);
675 $code = $this->addElement(1, '', $theData, 'class="fwd_rwd_nav"');
676 }
677 return [0, $code];
678 }
679
680 /**
681 * Creates the button with link to either forward or reverse
682 *
683 * @param string $type Type: "fwd" or "rwd
684 * @param int $pointer Pointer
685 * @param string $table Table name
686 * @return string
687 * @internal
688 */
689 public function fwd_rwd_HTML($type, $pointer, $table = '')
690 {
691 $content = '';
692 $tParam = $table ? '&table=' . rawurlencode($table) : '';
693 switch ($type) {
694 case 'fwd':
695 $href = $this->listURL() . '&pointer=' . ($pointer - $this->iLimit) . $tParam;
696 $content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
697 'actions-move-up',
698 Icon::SIZE_SMALL
699 )->render() . '</a> <i>[' . (max(0, $pointer - $this->iLimit) + 1) . ' - ' . $pointer . ']</i>';
700 break;
701 case 'rwd':
702 $href = $this->listURL() . '&pointer=' . $pointer . $tParam;
703 $content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
704 'actions-move-down',
705 Icon::SIZE_SMALL
706 )->render() . '</a> <i>[' . ($pointer + 1) . ' - ' . $this->totalItems . ']</i>';
707 break;
708 }
709 return $content;
710 }
711
712 /**
713 * Returning JavaScript for ClipBoard functionality.
714 *
715 * @return string
716 */
717 public function CBfunctions()
718 {
719 return '
720 // checkOffCB()
721 function checkOffCB(listOfCBnames, link) { //
722 var checkBoxes, flag, i;
723 var checkBoxes = listOfCBnames.split(",");
724 if (link.rel === "") {
725 link.rel = "allChecked";
726 flag = true;
727 } else {
728 link.rel = "";
729 flag = false;
730 }
731 for (i = 0; i < checkBoxes.length; i++) {
732 setcbValue(checkBoxes[i], flag);
733 }
734 }
735 // cbValue()
736 function cbValue(CBname) { //
737 var CBfullName = "CBC["+CBname+"]";
738 return (document.dblistForm[CBfullName] && document.dblistForm[CBfullName].checked ? 1 : 0);
739 }
740 // setcbValue()
741 function setcbValue(CBname,flag) { //
742 CBfullName = "CBC["+CBname+"]";
743 if(document.dblistForm[CBfullName]) {
744 document.dblistForm[CBfullName].checked = flag ? "on" : 0;
745 }
746 }
747
748 ';
749 }
750
751 /**
752 * Initializes page languages and icons
753 */
754 public function initializeLanguages()
755 {
756 // Look up page overlays:
757 $localizationParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
758 $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'];
759 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
760 ->getQueryBuilderForTable('pages');
761 $queryBuilder->getRestrictions()
762 ->removeAll()
763 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
764 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
765 $result = $queryBuilder
766 ->select('*')
767 ->from('pages')
768 ->where(
769 $queryBuilder->expr()->andX(
770 $queryBuilder->expr()->eq(
771 $localizationParentField,
772 $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
773 ),
774 $queryBuilder->expr()->gt(
775 $languageField,
776 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
777 )
778 )
779 )
780 ->execute();
781
782 $this->pageOverlays = [];
783 while ($row = $result->fetch()) {
784 $this->pageOverlays[$row[$languageField]] = $row;
785 }
786
787 $this->languageIconTitles = $this->getTranslateTools()->getSystemLanguages($this->id);
788 }
789
790 /**
791 * Return the icon for the language
792 *
793 * @param int $sys_language_uid Sys language uid
794 * @param bool $addAsAdditionalText If set to true, only the flag is returned
795 * @return string Language icon
796 */
797 public function languageFlag($sys_language_uid, $addAsAdditionalText = true)
798 {
799 $out = '';
800 $title = htmlspecialchars($this->languageIconTitles[$sys_language_uid]['title']);
801 if ($this->languageIconTitles[$sys_language_uid]['flagIcon']) {
802 $out .= '<span title="' . $title . '">' . $this->iconFactory->getIcon(
803 $this->languageIconTitles[$sys_language_uid]['flagIcon'],
804 Icon::SIZE_SMALL
805 )->render() . '</span>';
806 if (!$addAsAdditionalText) {
807 return $out;
808 }
809 $out .= '&nbsp;';
810 }
811 $out .= $title;
812 return $out;
813 }
814
815 /**
816 * If there is a parent folder and user has access to it, return an icon
817 * which is linked to the filelist of the parent folder.
818 *
819 * @param Folder $currentFolder
820 * @return string
821 */
822 protected function getLinkToParentFolder(Folder $currentFolder)
823 {
824 $levelUp = '';
825 try {
826 $currentStorage = $currentFolder->getStorage();
827 $parentFolder = $currentFolder->getParentFolder();
828 if ($parentFolder->getIdentifier() !== $currentFolder->getIdentifier() && $currentStorage->isWithinFileMountBoundaries($parentFolder)) {
829 $levelUp = $this->linkWrapDir(
830 '<span title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.upOneLevel')) . '">'
831 . $this->iconFactory->getIcon('actions-view-go-up', Icon::SIZE_SMALL)->render()
832 . '</span>',
833 $parentFolder
834 );
835 }
836 } catch (\Exception $e) {
837 }
838 return $levelUp;
839 }
840
841 /**
842 * Gets the number of files and total size of a folder
843 *
844 * @return string
845 */
846 public function getFolderInfo()
847 {
848 if ($this->counter == 1) {
849 $fileLabel = htmlspecialchars($this->getLanguageService()->getLL('file'));
850 } else {
851 $fileLabel = htmlspecialchars($this->getLanguageService()->getLL('files'));
852 }
853 return $this->counter . ' ' . $fileLabel . ', ' . GeneralUtility::formatSize($this->totalbytes, htmlspecialchars($this->getLanguageService()->getLL('byteSizeUnits')));
854 }
855
856 /**
857 * This returns tablerows for the directories in the array $items['sorting'].
858 *
859 * @param Folder[] $folders Folders of \TYPO3\CMS\Core\Resource\Folder
860 * @return string HTML table rows.
861 */
862 public function formatDirList(array $folders)
863 {
864 $out = '';
865 foreach ($folders as $folderName => $folderObject) {
866 $role = $folderObject->getRole();
867 if ($role === FolderInterface::ROLE_PROCESSING) {
868 // don't show processing-folder
869 continue;
870 }
871 if ($role !== FolderInterface::ROLE_DEFAULT) {
872 $displayName = '<strong>' . htmlspecialchars($folderName) . '</strong>';
873 } else {
874 $displayName = htmlspecialchars($folderName);
875 }
876
877 $isLocked = $folderObject instanceof InaccessibleFolder;
878 $isWritable = $folderObject->checkActionPermission('write');
879
880 // Initialization
881 $this->counter++;
882
883 // The icon with link
884 $theIcon = '<span title="' . htmlspecialchars($folderName) . '">' . $this->iconFactory->getIconForResource($folderObject, Icon::SIZE_SMALL)->render() . '</span>';
885 if (!$isLocked) {
886 $theIcon = BackendUtility::wrapClickMenuOnIcon($theIcon, 'sys_file', $folderObject->getCombinedIdentifier());
887 }
888
889 // Preparing and getting the data-array
890 $theData = [];
891 if ($isLocked) {
892 foreach ($this->fieldArray as $field) {
893 $theData[$field] = '';
894 }
895 $theData['file'] = $displayName;
896 } else {
897 foreach ($this->fieldArray as $field) {
898 switch ($field) {
899 case 'size':
900 try {
901 $numFiles = $folderObject->getFileCount();
902 } catch (InsufficientFolderAccessPermissionsException $e) {
903 $numFiles = 0;
904 }
905 $theData[$field] = $numFiles . ' ' . htmlspecialchars($this->getLanguageService()->getLL(($numFiles === 1 ? 'file' : 'files')));
906 break;
907 case 'rw':
908 $theData[$field] = '<strong class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('read')) . '</strong>' . (!$isWritable ? '' : '<strong class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('write')) . '</strong>');
909 break;
910 case 'fileext':
911 $theData[$field] = htmlspecialchars($this->getLanguageService()->getLL('folder'));
912 break;
913 case 'tstamp':
914 $tstamp = $folderObject->getModificationTime();
915 $theData[$field] = $tstamp ? BackendUtility::date($tstamp) : '-';
916 break;
917 case 'file':
918 $theData[$field] = $this->linkWrapDir($displayName, $folderObject);
919 break;
920 case '_CONTROL_':
921 $theData[$field] = $this->makeEdit($folderObject);
922 break;
923 case '_CLIPBOARD_':
924 $theData[$field] = $this->makeClip($folderObject);
925 break;
926 case '_REF_':
927 $theData[$field] = $this->makeRef($folderObject);
928 break;
929 default:
930 $theData[$field] = GeneralUtility::fixed_lgd_cs($theData[$field] ?? '', $this->fixedL);
931 }
932 }
933 }
934 $out .= $this->addElement(1, $theIcon, $theData);
935 }
936 return $out;
937 }
938
939 /**
940 * Wraps the directory-titles
941 *
942 * @param string $title String to be wrapped in links
943 * @param Folder $folderObject Folder to work on
944 * @return string HTML
945 */
946 public function linkWrapDir($title, Folder $folderObject)
947 {
948 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
949 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
950 $href = (string)$uriBuilder->buildUriFromRoute('file_FilelistList', ['id' => $folderObject->getCombinedIdentifier()]);
951 $onclick = ' onclick="' . htmlspecialchars('top.document.getElementsByName("nav_frame")[0].contentWindow.Tree.highlightActiveItem("file","folder' . GeneralUtility::md5int($folderObject->getCombinedIdentifier()) . '_"+top.fsMod.currentBank)') . '"';
952 // Sometimes $code contains plain HTML tags. In such a case the string should not be modified!
953 if ((string)$title === strip_tags($title)) {
954 return '<a href="' . htmlspecialchars($href) . '"' . $onclick . ' title="' . htmlspecialchars($title) . '">' . $title . '</a>';
955 }
956 return '<a href="' . htmlspecialchars($href) . '"' . $onclick . '>' . $title . '</a>';
957 }
958
959 /**
960 * Wraps filenames in links which opens the metadata editor.
961 *
962 * @param string $code String to be wrapped in links
963 * @param File $fileObject File to be linked
964 * @return string HTML
965 */
966 public function linkWrapFile($code, File $fileObject)
967 {
968 try {
969 if ($fileObject instanceof File && $fileObject->isIndexed() && $fileObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata')) {
970 $metaData = $fileObject->_getMetaData();
971 $urlParameters = [
972 'edit' => [
973 'sys_file_metadata' => [
974 $metaData['uid'] => 'edit'
975 ]
976 ],
977 'returnUrl' => $this->listURL()
978 ];
979 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
980 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
981 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
982 $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata'));
983 $code = '<a class="responsive-title" href="' . htmlspecialchars($url) . '" title="' . $title . '">' . $code . '</a>';
984 }
985 } catch (\Exception $e) {
986 // intentional fall-through
987 }
988 return $code;
989 }
990
991 /**
992 * Returns list URL; This is the URL of the current script with id and imagemode parameters, that's all.
993 * The URL however is not relative, otherwise GeneralUtility::sanitizeLocalUrl() would say that
994 * the URL would be invalid
995 *
996 * @param string $altId
997 * @param string $table Table name to display. Enter "-1" for the current table.
998 * @param string $exclList Comma separated list of fields NOT to include ("sortField", "sortRev" or "firstElementNumber")
999 *
1000 * @return string URL
1001 */
1002 public function listURL($altId = '', $table = '-1', $exclList = '')
1003 {
1004 return GeneralUtility::linkThisScript([
1005 'target' => rawurlencode($this->folderObject->getCombinedIdentifier()),
1006 'imagemode' => $this->thumbs
1007 ]);
1008 }
1009
1010 /**
1011 * This returns tablerows for the files in the array $items['sorting'].
1012 *
1013 * @param File[] $files File items
1014 * @return string HTML table rows.
1015 */
1016 public function formatFileList(array $files)
1017 {
1018 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1019 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1020 $out = '';
1021 // first two keys are "0" (default) and "-1" (multiple), after that comes the "other languages"
1022 $allSystemLanguages = GeneralUtility::makeInstance(TranslationConfigurationProvider::class)->getSystemLanguages();
1023 $systemLanguages = array_filter($allSystemLanguages, function ($languageRecord) {
1024 if ($languageRecord['uid'] === -1 || $languageRecord['uid'] === 0 || !$this->getBackendUser()->checkLanguageAccess($languageRecord['uid'])) {
1025 return false;
1026 }
1027 return true;
1028 });
1029
1030 foreach ($files as $fileObject) {
1031 // Initialization
1032 $this->counter++;
1033 $this->totalbytes += $fileObject->getSize();
1034 $ext = $fileObject->getExtension();
1035 $fileName = trim($fileObject->getName());
1036 // The icon with link
1037 $theIcon = '<span title="' . htmlspecialchars($fileName . ' [' . (int)$fileObject->getUid() . ']') . '">'
1038 . $this->iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render() . '</span>';
1039 $theIcon = BackendUtility::wrapClickMenuOnIcon($theIcon, 'sys_file', $fileObject->getCombinedIdentifier());
1040 // Preparing and getting the data-array
1041 $theData = [];
1042 foreach ($this->fieldArray as $field) {
1043 switch ($field) {
1044 case 'size':
1045 $theData[$field] = GeneralUtility::formatSize($fileObject->getSize(), htmlspecialchars($this->getLanguageService()->getLL('byteSizeUnits')));
1046 break;
1047 case 'rw':
1048 $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>');
1049 break;
1050 case 'fileext':
1051 $theData[$field] = strtoupper($ext);
1052 break;
1053 case 'tstamp':
1054 $theData[$field] = BackendUtility::date($fileObject->getModificationTime());
1055 break;
1056 case '_CONTROL_':
1057 $theData[$field] = $this->makeEdit($fileObject);
1058 break;
1059 case '_CLIPBOARD_':
1060 $theData[$field] = $this->makeClip($fileObject);
1061 break;
1062 case '_LOCALIZATION_':
1063 if (!empty($systemLanguages) && $fileObject->isIndexed() && $fileObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata')) {
1064 $metaDataRecord = $fileObject->_getMetaData();
1065 $translations = $this->getTranslationsForMetaData($metaDataRecord);
1066 $languageCode = '';
1067
1068 foreach ($systemLanguages as $language) {
1069 $languageId = $language['uid'];
1070 $flagIcon = $language['flagIcon'];
1071 if (array_key_exists($languageId, $translations)) {
1072 $title = htmlspecialchars(sprintf($this->getLanguageService()->getLL('editMetadataForLanguage'), $language['title']));
1073 // @todo the overlay for the flag needs to be added ($flagIcon . '-overlay')
1074 $urlParameters = [
1075 'edit' => [
1076 'sys_file_metadata' => [
1077 $translations[$languageId]['uid'] => 'edit'
1078 ]
1079 ],
1080 'returnUrl' => $this->listURL()
1081 ];
1082 $flagButtonIcon = $this->iconFactory->getIcon($flagIcon, Icon::SIZE_SMALL, 'overlay-edit')->render();
1083 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1084 $languageCode .= '<a href="' . htmlspecialchars($url) . '" class="btn btn-default" title="' . $title . '">'
1085 . $flagButtonIcon . '</a>';
1086 } else {
1087 $parameters = [
1088 'justLocalized' => 'sys_file_metadata:' . $metaDataRecord['uid'] . ':' . $languageId,
1089 'returnUrl' => $this->listURL()
1090 ];
1091 $returnUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', $parameters);
1092 $href = BackendUtility::getLinkToDataHandlerAction(
1093 '&cmd[sys_file_metadata][' . $metaDataRecord['uid'] . '][localize]=' . $languageId,
1094 $returnUrl
1095 );
1096 $flagButtonIcon = '<span title="' . htmlspecialchars(sprintf($this->getLanguageService()->getLL('createMetadataForLanguage'), $language['title'])) . '">' . $this->iconFactory->getIcon($flagIcon, Icon::SIZE_SMALL, 'overlay-new')->render() . '</span>';
1097 $languageCode .= '<a href="' . htmlspecialchars($href) . '" class="btn btn-default">' . $flagButtonIcon . '</a> ';
1098 }
1099 }
1100
1101 // Hide flag button bar when not translated yet
1102 $theData[$field] = ' <div class="localisationData btn-group" data-fileid="' . $fileObject->getUid() . '"' .
1103 (empty($translations) ? ' style="display: none;"' : '') . '>' . $languageCode . '</div>';
1104 $theData[$field] .= '<a class="btn btn-default filelist-translationToggler" data-fileid="' . $fileObject->getUid() . '">' .
1105 '<span title="' . htmlspecialchars($this->getLanguageService()->getLL('translateMetadata')) . '">'
1106 . $this->iconFactory->getIcon('mimetypes-x-content-page-language-overlay', Icon::SIZE_SMALL)->render() . '</span>'
1107 . '</a>';
1108 }
1109 break;
1110 case '_REF_':
1111 $theData[$field] = $this->makeRef($fileObject);
1112 break;
1113 case 'file':
1114 // Edit metadata of file
1115 $theData[$field] = $this->linkWrapFile(htmlspecialchars($fileName), $fileObject);
1116
1117 if ($fileObject->isMissing()) {
1118 $theData[$field] .= '<span class="label label-danger label-space-left">'
1119 . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
1120 . '</span>';
1121 // Thumbnails?
1122 } elseif ($this->thumbs && ($this->isImage($ext) || $this->isMediaFile($ext))) {
1123 $processedFile = $fileObject->process(
1124 ProcessedFile::CONTEXT_IMAGEPREVIEW,
1125 [
1126 'width' => $this->thumbnailConfiguration->getWidth(),
1127 'height' => $this->thumbnailConfiguration->getHeight()
1128 ]
1129 );
1130 if ($processedFile) {
1131 $thumbUrl = $processedFile->getPublicUrl(true);
1132 $theData[$field] .= '<br /><img src="' . htmlspecialchars($thumbUrl) . '" ' .
1133 'width="' . $processedFile->getProperty('width') . '" ' .
1134 'height="' . $processedFile->getProperty('height') . '" ' .
1135 'title="' . htmlspecialchars($fileName) . '" alt="" />';
1136 }
1137 }
1138 break;
1139 default:
1140 $theData[$field] = '';
1141 if ($fileObject->hasProperty($field)) {
1142 $theData[$field] = htmlspecialchars(GeneralUtility::fixed_lgd_cs($fileObject->getProperty($field), $this->fixedL));
1143 }
1144 }
1145 }
1146 $out .= $this->addElement(1, $theIcon, $theData);
1147 }
1148 return $out;
1149 }
1150
1151 /**
1152 * Fetch the translations for a sys_file_metadata record
1153 *
1154 * @param $metaDataRecord
1155 * @return array keys are the sys_language uids, values are the $rows
1156 */
1157 protected function getTranslationsForMetaData($metaDataRecord)
1158 {
1159 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_metadata');
1160 $queryBuilder->getRestrictions()->removeAll();
1161 $translationRecords = $queryBuilder->select('*')
1162 ->from('sys_file_metadata')
1163 ->where(
1164 $queryBuilder->expr()->eq(
1165 $GLOBALS['TCA']['sys_file_metadata']['ctrl']['transOrigPointerField'],
1166 $queryBuilder->createNamedParameter($metaDataRecord['uid'], \PDO::PARAM_INT)
1167 ),
1168 $queryBuilder->expr()->gt(
1169 $GLOBALS['TCA']['sys_file_metadata']['ctrl']['languageField'],
1170 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1171 )
1172 )
1173 ->execute()
1174 ->fetchAll();
1175
1176 $translations = [];
1177 foreach ($translationRecords as $record) {
1178 $translations[$record[$GLOBALS['TCA']['sys_file_metadata']['ctrl']['languageField']]] = $record;
1179 }
1180 return $translations;
1181 }
1182
1183 /**
1184 * Returns TRUE if $ext is an image-extension according to $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
1185 *
1186 * @param string $ext File extension
1187 * @return bool
1188 */
1189 public function isImage($ext)
1190 {
1191 return GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], strtolower($ext));
1192 }
1193
1194 /**
1195 * Returns TRUE if $ext is an media-extension according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext']
1196 *
1197 * @param string $ext File extension
1198 * @return bool
1199 */
1200 public function isMediaFile($ext)
1201 {
1202 return GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'], strtolower($ext));
1203 }
1204
1205 /**
1206 * Wraps the directory-titles ($code) in a link to filelist/Modules/Filelist/index.php (id=$path) and sorting commands...
1207 *
1208 * @param string $code String to be wrapped
1209 * @param string $folderIdentifier ID (path)
1210 * @param string $col Sorting column
1211 * @return string HTML
1212 */
1213 public function linkWrapSort($code, $folderIdentifier, $col)
1214 {
1215 $params = ['id' => $folderIdentifier, 'SET' => ['sort' => $col]];
1216
1217 if ($this->sort === $col) {
1218 // Check reverse sorting
1219 $params['SET']['reverse'] = ($this->sortRev ? '0' : '1');
1220 $sortArrow = $this->iconFactory->getIcon('status-status-sorting-' . ($this->sortRev ? 'desc' : 'asc'), Icon::SIZE_SMALL)->render();
1221 } else {
1222 $params['SET']['reverse'] = 0;
1223 $sortArrow = '';
1224 }
1225 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1226 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1227 $href = (string)$uriBuilder->buildUriFromRoute('file_FilelistList', $params);
1228 return '<a href="' . htmlspecialchars($href) . '">' . $code . ' ' . $sortArrow . '</a>';
1229 }
1230
1231 /**
1232 * Creates the clipboard control pad
1233 *
1234 * @param File|Folder $fileOrFolderObject Array with information about the file/directory for which to make the clipboard panel for the listing.
1235 * @return string HTML-table
1236 */
1237 public function makeClip($fileOrFolderObject)
1238 {
1239 if (!$fileOrFolderObject->checkActionPermission('read')) {
1240 return '';
1241 }
1242 $cells = [];
1243 $fullIdentifier = $fileOrFolderObject->getCombinedIdentifier();
1244 $fullName = $fileOrFolderObject->getName();
1245 $md5 = GeneralUtility::shortMD5($fullIdentifier);
1246 // For normal clipboard, add copy/cut buttons:
1247 if ($this->clipObj->current === 'normal') {
1248 $isSel = $this->clipObj->isSelected('_FILE', $md5);
1249 $copyTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy'));
1250 $cutTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut'));
1251 $copyIcon = $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL)->render();
1252 $cutIcon = $this->iconFactory->getIcon('actions-edit-cut', Icon::SIZE_SMALL)->render();
1253
1254 if ($isSel === 'copy') {
1255 $copyIcon = $this->iconFactory->getIcon('actions-edit-copy-release', Icon::SIZE_SMALL)->render();
1256 } elseif ($isSel === 'cut') {
1257 $cutIcon = $this->iconFactory->getIcon('actions-edit-cut-release', Icon::SIZE_SMALL)->render();
1258 }
1259
1260 $cells[] = '<a class="btn btn-default" href="' . htmlspecialchars($this->clipObj->selUrlFile(
1261 $fullIdentifier,
1262 1,
1263 $isSel === 'copy'
1264 )) . '" title="' . $copyTitle . '">' . $copyIcon . '</a>';
1265 // we can only cut if file can be moved
1266 if ($fileOrFolderObject->checkActionPermission('move')) {
1267 $cells[] = '<a class="btn btn-default" href="' . htmlspecialchars($this->clipObj->selUrlFile(
1268 $fullIdentifier,
1269 0,
1270 $isSel === 'cut'
1271 )) . '" title="' . $cutTitle . '">' . $cutIcon . '</a>';
1272 } else {
1273 $cells[] = $this->spaceIcon;
1274 }
1275 } else {
1276 // For numeric pads, add select checkboxes:
1277 $n = '_FILE|' . $md5;
1278 $this->CBnames[] = $n;
1279 $checked = $this->clipObj->isSelected('_FILE', $md5) ? ' checked="checked"' : '';
1280 $cells[] = '<input type="hidden" name="CBH[' . $n . ']" value="0" /><label class="btn btn-default btn-checkbox"><input type="checkbox" name="CBC[' . $n . ']" value="' . htmlspecialchars($fullIdentifier) . '" ' . $checked . ' /><span class="t3-icon fa"></span></label>';
1281 }
1282 // Display PASTE button, if directory:
1283 $elFromTable = $this->clipObj->elFromTable('_FILE');
1284 if ($fileOrFolderObject instanceof Folder && !empty($elFromTable) && $fileOrFolderObject->checkActionPermission('write')) {
1285 $addPasteButton = true;
1286 $elToConfirm = [];
1287 foreach ($elFromTable as $key => $element) {
1288 $clipBoardElement = $this->resourceFactory->retrieveFileOrFolderObject($element);
1289 if ($clipBoardElement instanceof Folder && $clipBoardElement->getStorage()->isWithinFolder($clipBoardElement, $fileOrFolderObject)) {
1290 $addPasteButton = false;
1291 }
1292 $elToConfirm[$key] = $clipBoardElement->getName();
1293 }
1294 if ($addPasteButton) {
1295 $cells[] = '<a class="btn btn-default t3js-modal-trigger" '
1296 . ' href="' . htmlspecialchars($this->clipObj->pasteUrl('_FILE', $fullIdentifier)) . '"'
1297 . ' data-content="' . htmlspecialchars($this->clipObj->confirmMsgText('_FILE', $fullName, 'into', $elToConfirm)) . '"'
1298 . ' data-severity="warning"'
1299 . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"'
1300 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"'
1301 . '>'
1302 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
1303 . '</a>';
1304 }
1305 }
1306 // Compile items into a DIV-element:
1307 return ' <div class="btn-group" role="group">' . implode('', $cells) . '</div>';
1308 }
1309
1310 /**
1311 * Creates the edit control section
1312 *
1313 * @param File|Folder $fileOrFolderObject Array with information about the file/directory for which to make the edit control section for the listing.
1314 * @return string HTML-table
1315 */
1316 public function makeEdit($fileOrFolderObject)
1317 {
1318 $cells = [];
1319 $fullIdentifier = $fileOrFolderObject->getCombinedIdentifier();
1320 $md5 = GeneralUtility::shortMD5($fullIdentifier);
1321 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1322 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1323
1324 // Edit file content (if editable)
1325 if ($fileOrFolderObject instanceof File && $fileOrFolderObject->checkActionPermission('write') && GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'], $fileOrFolderObject->getExtension())) {
1326 $url = (string)$uriBuilder->buildUriFromRoute('file_edit', ['target' => $fullIdentifier]);
1327 $editOnClick = 'top.list_frame.location.href=' . GeneralUtility::quoteJSvalue($url) . '+\'&returnUrl=\'+top.rawurlencode(top.list_frame.document.location.pathname+top.list_frame.document.location.search);return false;';
1328 $cells['edit'] = '<a href="#" class="btn btn-default" onclick="' . htmlspecialchars($editOnClick) . '" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editcontent') . '">'
1329 . $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render()
1330 . '</a>';
1331 } else {
1332 $cells['edit'] = $this->spaceIcon;
1333 }
1334
1335 // Edit metadata of file
1336 if ($fileOrFolderObject instanceof File && $fileOrFolderObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata')) {
1337 $metaData = $fileOrFolderObject->_getMetaData();
1338 $urlParameters = [
1339 'edit' => [
1340 'sys_file_metadata' => [
1341 $metaData['uid'] => 'edit'
1342 ]
1343 ],
1344 'returnUrl' => $this->listURL()
1345 ];
1346 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1347 $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata'));
1348 $cells['metadata'] = '<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . $title . '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
1349 }
1350
1351 // document view
1352 if ($fileOrFolderObject instanceof File) {
1353 $fileUrl = $fileOrFolderObject->getPublicUrl(true);
1354 if ($fileUrl) {
1355 $aOnClick = 'return top.openUrlInWindow(' . GeneralUtility::quoteJSvalue($fileUrl) . ', \'WebFile\');';
1356 $cells['view'] = '<a href="#" class="btn btn-default" onclick="' . htmlspecialchars($aOnClick) . '" 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>';
1357 } else {
1358 $cells['view'] = $this->spaceIcon;
1359 }
1360 } else {
1361 $cells['view'] = $this->spaceIcon;
1362 }
1363
1364 // replace file
1365 if ($fileOrFolderObject instanceof File && $fileOrFolderObject->checkActionPermission('replace')) {
1366 $url = (string)$uriBuilder->buildUriFromRoute('file_replace', ['target' => $fullIdentifier, 'uid' => $fileOrFolderObject->getUid()]);
1367 $replaceOnClick = 'top.list_frame.location.href = ' . GeneralUtility::quoteJSvalue($url) . '+\'&returnUrl=\'+top.rawurlencode(top.list_frame.document.location.pathname+top.list_frame.document.location.search);return false;';
1368 $cells['replace'] = '<a href="#" class="btn btn-default" onclick="' . $replaceOnClick . '" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.replace') . '">' . $this->iconFactory->getIcon('actions-edit-replace', Icon::SIZE_SMALL)->render() . '</a>';
1369 }
1370
1371 // rename the file
1372 if ($fileOrFolderObject->checkActionPermission('rename')) {
1373 $url = (string)$uriBuilder->buildUriFromRoute('file_rename', ['target' => $fullIdentifier]);
1374 $renameOnClick = 'top.list_frame.location.href = ' . GeneralUtility::quoteJSvalue($url) . '+\'&returnUrl=\'+top.rawurlencode(top.list_frame.document.location.pathname+top.list_frame.document.location.search);return false;';
1375 $cells['rename'] = '<a href="#" class="btn btn-default" onclick="' . htmlspecialchars($renameOnClick) . '" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.rename') . '">' . $this->iconFactory->getIcon('actions-edit-rename', Icon::SIZE_SMALL)->render() . '</a>';
1376 } else {
1377 $cells['rename'] = $this->spaceIcon;
1378 }
1379
1380 // upload files
1381 if ($fileOrFolderObject->getStorage()->checkUserActionPermission('add', 'File') && $fileOrFolderObject->checkActionPermission('write')) {
1382 if ($fileOrFolderObject instanceof Folder) {
1383 $url = (string)$uriBuilder->buildUriFromRoute('file_upload', ['target' => $fullIdentifier]);
1384 $uploadOnClick = 'top.list_frame.location.href = ' . GeneralUtility::quoteJSvalue($url) . '+\'&returnUrl=\'+top.rawurlencode(top.list_frame.document.location.pathname+top.list_frame.document.location.search);return false;';
1385 $cells['upload'] = '<a href="#" class="btn btn-default" onclick="' . htmlspecialchars($uploadOnClick) . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.upload')) . '">' . $this->iconFactory->getIcon('actions-edit-upload', Icon::SIZE_SMALL)->render() . '</a>';
1386 }
1387 }
1388
1389 if ($fileOrFolderObject->checkActionPermission('read')) {
1390 $infoOnClick = '';
1391 if ($fileOrFolderObject instanceof Folder) {
1392 $infoOnClick = 'top.TYPO3.InfoWindow.showItem(\'_FOLDER\', ' . GeneralUtility::quoteJSvalue($fullIdentifier) . ');return false;';
1393 } elseif ($fileOrFolderObject instanceof File) {
1394 $infoOnClick = 'top.TYPO3.InfoWindow.showItem(\'_FILE\', ' . GeneralUtility::quoteJSvalue($fullIdentifier) . ');return false;';
1395 }
1396 $cells['info'] = '<a href="#" class="btn btn-default" onclick="' . htmlspecialchars($infoOnClick) . '" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info') . '">' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '</a>';
1397 } else {
1398 $cells['info'] = $this->spaceIcon;
1399 }
1400
1401 // delete the file
1402 if ($fileOrFolderObject->checkActionPermission('delete')) {
1403 $identifier = $fileOrFolderObject->getIdentifier();
1404 if ($fileOrFolderObject instanceof Folder) {
1405 $referenceCountText = BackendUtility::referenceCount('_FILE', $identifier, ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFolder'));
1406 $deleteType = 'delete_folder';
1407 } else {
1408 $referenceCountText = BackendUtility::referenceCount('sys_file', $fileOrFolderObject->getUid(), ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFile'));
1409 $deleteType = 'delete_file';
1410 }
1411
1412 if ($this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE)) {
1413 $confirmationCheck = '1';
1414 } else {
1415 $confirmationCheck = '0';
1416 }
1417
1418 $deleteUrl = (string)$uriBuilder->buildUriFromRoute('tce_file');
1419 $confirmationMessage = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'), $fileOrFolderObject->getName()) . $referenceCountText;
1420 $title = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete');
1421 $cells['delete'] = '<a href="#" class="btn btn-default t3js-filelist-delete" data-content="' . htmlspecialchars($confirmationMessage)
1422 . '" data-check="' . $confirmationCheck
1423 . '" data-delete-url="' . htmlspecialchars($deleteUrl)
1424 . '" data-title="' . htmlspecialchars($title)
1425 . '" data-identifier="' . htmlspecialchars($fileOrFolderObject->getCombinedIdentifier())
1426 . '" data-delete-type="' . $deleteType
1427 . '" title="' . htmlspecialchars($title) . '">'
1428 . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
1429 } else {
1430 $cells['delete'] = $this->spaceIcon;
1431 }
1432
1433 // Hook for manipulating edit icons.
1434 $cells['__fileOrFolderObject'] = $fileOrFolderObject;
1435 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['fileList']['editIconsHook'] ?? [] as $className) {
1436 $hookObject = GeneralUtility::makeInstance($className);
1437 if (!$hookObject instanceof FileListEditIconHookInterface) {
1438 throw new \UnexpectedValueException(
1439 $className . ' must implement interface ' . FileListEditIconHookInterface::class,
1440 1235225797
1441 );
1442 }
1443 $hookObject->manipulateEditIcons($cells, $this);
1444 }
1445 unset($cells['__fileOrFolderObject']);
1446 // Compile items into a DIV-element:
1447 return '<div class="btn-group">' . implode('', $cells) . '</div>';
1448 }
1449
1450 /**
1451 * Make reference count
1452 *
1453 * @param File|Folder $fileOrFolderObject Array with information about the file/directory for which to make the clipboard panel for the listing.
1454 * @return string HTML
1455 */
1456 public function makeRef($fileOrFolderObject)
1457 {
1458 if ($fileOrFolderObject instanceof FolderInterface) {
1459 return '-';
1460 }
1461 // Look up the file in the sys_refindex.
1462 // Exclude sys_file_metadata records as these are no use references
1463 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
1464 $referenceCount = $queryBuilder->count('*')
1465 ->from('sys_refindex')
1466 ->where(
1467 $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
1468 $queryBuilder->expr()->eq(
1469 'ref_table',
1470 $queryBuilder->createNamedParameter('sys_file', \PDO::PARAM_STR)
1471 ),
1472 $queryBuilder->expr()->eq(
1473 'ref_uid',
1474 $queryBuilder->createNamedParameter($fileOrFolderObject->getUid(), \PDO::PARAM_INT)
1475 ),
1476 $queryBuilder->expr()->neq(
1477 'tablename',
1478 $queryBuilder->createNamedParameter('sys_file_metadata', \PDO::PARAM_STR)
1479 )
1480 )
1481 ->execute()
1482 ->fetchColumn();
1483
1484 return $this->generateReferenceToolTip($referenceCount, '\'_FILE\', ' . GeneralUtility::quoteJSvalue($fileOrFolderObject->getCombinedIdentifier()));
1485 }
1486
1487 /**
1488 * Returns an instance of LanguageService
1489 *
1490 * @return \TYPO3\CMS\Core\Localization\LanguageService
1491 */
1492 protected function getLanguageService()
1493 {
1494 return $GLOBALS['LANG'];
1495 }
1496
1497 /**
1498 * Returns the current BE user.
1499 *
1500 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1501 */
1502 protected function getBackendUser()
1503 {
1504 return $GLOBALS['BE_USER'];
1505 }
1506
1507 /**
1508 * Sets the script url depending on being a module or script request
1509 */
1510 protected function determineScriptUrl()
1511 {
1512 if ($routePath = GeneralUtility::_GP('route')) {
1513 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1514 $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
1515 } else {
1516 $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
1517 }
1518 }
1519
1520 /**
1521 * @return string
1522 */
1523 protected function getThisScript()
1524 {
1525 return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
1526 }
1527
1528 /**
1529 * Gets an instance of TranslationConfigurationProvider
1530 *
1531 * @return TranslationConfigurationProvider
1532 */
1533 protected function getTranslateTools()
1534 {
1535 if (!isset($this->translateTools)) {
1536 $this->translateTools = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
1537 }
1538 return $this->translateTools;
1539 }
1540
1541 /**
1542 * Generates HTML code for a Reference tooltip out of
1543 * sys_refindex records you hand over
1544 *
1545 * @param int $references number of records from sys_refindex table
1546 * @param string $launchViewParameter JavaScript String, which will be passed as parameters to top.TYPO3.InfoWindow.showItem
1547 * @return string
1548 */
1549 protected function generateReferenceToolTip($references, $launchViewParameter = '')
1550 {
1551 if (!$references) {
1552 $htmlCode = '-';
1553 } else {
1554 $htmlCode = '<a href="#"';
1555 if ($launchViewParameter !== '') {
1556 $htmlCode .= ' onclick="' . htmlspecialchars(
1557 'top.TYPO3.InfoWindow.showItem(' . $launchViewParameter . '); return false;'
1558 ) . '"';
1559 }
1560 $htmlCode .= ' title="' . htmlspecialchars(
1561 $this->getLanguageService()->sL(
1562 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references'
1563 ) . ' (' . $references . ')'
1564 ) . '">';
1565 $htmlCode .= $references;
1566 $htmlCode .= '</a>';
1567 }
1568 return $htmlCode;
1569 }
1570 }