99f6bd9a34c6c2601e10cc67a8c101e9097fcbab
[Packages/TYPO3.CMS.git] / typo3 / sysext / recordlist / Classes / Browser / FileBrowser.php
1 <?php
2 namespace TYPO3\CMS\Recordlist\Browser;
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\Tree\View\ElementBrowserFolderTreeView;
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
19 use TYPO3\CMS\Core\Imaging\Icon;
20 use TYPO3\CMS\Core\Resource\Exception;
21 use TYPO3\CMS\Core\Resource\File;
22 use TYPO3\CMS\Core\Resource\FileInterface;
23 use TYPO3\CMS\Core\Resource\FileRepository;
24 use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
25 use TYPO3\CMS\Core\Resource\Folder;
26 use TYPO3\CMS\Core\Resource\ProcessedFile;
27 use TYPO3\CMS\Core\Resource\ResourceFactory;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29 use TYPO3\CMS\Core\Utility\HttpUtility;
30 use TYPO3\CMS\Core\Utility\MathUtility;
31 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
32 use TYPO3\CMS\Recordlist\View\FolderUtilityRenderer;
33
34 /**
35 * Browser for files
36 * @internal This class is a specific LinkBrowser implementation and is not part of the TYPO3's Core API.
37 */
38 class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterface, LinkParameterProviderInterface
39 {
40 /**
41 * When you click a folder name/expand icon to see the content of a certain file folder,
42 * this value will contain the path of the expanded file folder.
43 * If the value is NOT set, then it will be restored from the module session data.
44 * Example value: "/www/htdocs/typo3/32/3dsplm/fileadmin/css/"
45 *
46 * @var string|null
47 */
48 protected $expandFolder;
49
50 /**
51 * @var Folder
52 */
53 protected $selectedFolder;
54
55 /**
56 * Holds information about files
57 *
58 * @var mixed[][]
59 */
60 protected $elements = [];
61
62 /**
63 * @var string
64 */
65 protected $searchWord;
66
67 /**
68 * @var FileRepository
69 */
70 protected $fileRepository;
71
72 /**
73 * @var array
74 */
75 protected $thumbnailConfiguration = [];
76
77 /**
78 * Loads additional JavaScript
79 */
80 protected function initialize()
81 {
82 parent::initialize();
83 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/BrowseFiles');
84 $this->fileRepository = GeneralUtility::makeInstance(FileRepository::class);
85
86 $thumbnailConfig = $this->getBackendUser()->getTSConfig()['options.']['file_list.']['thumbnail.'] ?? [];
87 if (isset($thumbnailConfig['width']) && MathUtility::canBeInterpretedAsInteger($thumbnailConfig['width'])) {
88 $this->thumbnailConfiguration['width'] = (int)$thumbnailConfig['width'];
89 }
90 if (isset($thumbnailConfig['height']) && MathUtility::canBeInterpretedAsInteger($thumbnailConfig['height'])) {
91 $this->thumbnailConfiguration['height'] = (int)$thumbnailConfig['height'];
92 }
93 }
94
95 /**
96 * Checks additional GET/POST requests
97 */
98 protected function initVariables()
99 {
100 parent::initVariables();
101 $this->expandFolder = GeneralUtility::_GP('expandFolder');
102 $this->searchWord = (string)GeneralUtility::_GP('searchWord');
103 }
104
105 /**
106 * Session data for this class can be set from outside with this method.
107 *
108 * @param mixed[] $data Session data array
109 * @return array[] Session data and boolean which indicates that data needs to be stored in session because it's changed
110 */
111 public function processSessionData($data)
112 {
113 if ($this->expandFolder !== null) {
114 $data['expandFolder'] = $this->expandFolder;
115 $store = true;
116 } else {
117 $this->expandFolder = $data['expandFolder'];
118 $store = false;
119 }
120 return [$data, $store];
121 }
122
123 /**
124 * @return string HTML content
125 */
126 public function render()
127 {
128 $backendUser = $this->getBackendUser();
129
130 // The key number 3 of the bparams contains the "allowed" string. Disallowed is not passed to
131 // the element browser at all but only filtered out in DataHandler afterwards
132 $allowedFileExtensions = GeneralUtility::trimExplode(',', explode('|', $this->bparams)[3], true);
133 if (!empty($allowedFileExtensions) && $allowedFileExtensions[0] !== 'sys_file' && $allowedFileExtensions[0] !== '*') {
134 // Create new filter object
135 $filterObject = GeneralUtility::makeInstance(FileExtensionFilter::class);
136 $filterObject->setAllowedFileExtensions($allowedFileExtensions);
137 // Set file extension filters on all storages
138 $storages = $backendUser->getFileStorages();
139 /** @var \TYPO3\CMS\Core\Resource\ResourceStorage $storage */
140 foreach ($storages as $storage) {
141 $storage->addFileAndFolderNameFilter([$filterObject, 'filterFileList']);
142 }
143 }
144 if ($this->expandFolder) {
145 $fileOrFolderObject = null;
146
147 // Try to fetch the folder the user had open the last time he browsed files
148 // Fallback to the default folder in case the last used folder is not existing
149 try {
150 $fileOrFolderObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($this->expandFolder);
151 } catch (Exception $accessException) {
152 // We're just catching the exception here, nothing to be done if folder does not exist or is not accessible.
153 } catch (\InvalidArgumentException $driverMissingExecption) {
154 // We're just catching the exception here, nothing to be done if the driver does not exist anymore.
155 }
156
157 if ($fileOrFolderObject instanceof Folder) {
158 // It's a folder
159 $this->selectedFolder = $fileOrFolderObject;
160 } elseif ($fileOrFolderObject instanceof FileInterface) {
161 // It's a file
162 $this->selectedFolder = $fileOrFolderObject->getParentFolder();
163 }
164 }
165 // Or get the user's default upload folder
166 if (!$this->selectedFolder) {
167 try {
168 [, $pid, $table,, $field] = explode('-', explode('|', $this->bparams)[4]);
169 $this->selectedFolder = $backendUser->getDefaultUploadFolder($pid, $table, $field);
170 } catch (\Exception $e) {
171 // The configured default user folder does not exist
172 }
173 }
174 // Build the file upload and folder creation form
175 $uploadForm = '';
176 $createFolder = '';
177 if ($this->selectedFolder) {
178 $folderUtilityRenderer = GeneralUtility::makeInstance(FolderUtilityRenderer::class, $this);
179 $uploadForm = $folderUtilityRenderer->uploadForm($this->selectedFolder, $allowedFileExtensions);
180 $createFolder = $folderUtilityRenderer->createFolder($this->selectedFolder);
181 }
182
183 // Getting flag for showing/not showing thumbnails:
184 $noThumbs = $backendUser->getTSConfig()['options.']['noThumbsInEB'] ?? false;
185 $_MOD_SETTINGS = [];
186 if (!$noThumbs) {
187 // MENU-ITEMS, fetching the setting for thumbnails from File>List module:
188 $_MOD_MENU = ['displayThumbs' => ''];
189 $_MCONF['name'] = 'file_list';
190 $_MOD_SETTINGS = BackendUtility::getModuleData($_MOD_MENU, GeneralUtility::_GP('SET'), $_MCONF['name']);
191 }
192 $displayThumbs = $_MOD_SETTINGS['displayThumbs'] ?? false;
193 $noThumbs = $noThumbs ?: !$displayThumbs;
194 // Create folder tree:
195 /** @var ElementBrowserFolderTreeView $folderTree */
196 $folderTree = GeneralUtility::makeInstance(ElementBrowserFolderTreeView::class);
197 $folderTree->setLinkParameterProvider($this);
198 $tree = $folderTree->getBrowsableTree();
199 if ($this->selectedFolder) {
200 $files = $this->renderFilesInFolder($this->selectedFolder, $allowedFileExtensions, $noThumbs);
201 } else {
202 $files = '';
203 }
204
205 $this->initDocumentTemplate();
206 // Starting content:
207 $content = $this->doc->startPage(htmlspecialchars($this->getLanguageService()->getLL('fileSelector')));
208
209 // Putting the parts together, side by side:
210 $markup = [];
211 $markup[] = '<!-- Wrapper table for folder tree / filelist: -->';
212 $markup[] = '<div class="element-browser">';
213 $markup[] = ' <div class="element-browser-panel element-browser-main">';
214 $markup[] = ' <div class="element-browser-main-sidebar">';
215 $markup[] = ' <div class="element-browser-body">';
216 $markup[] = ' ' . $tree;
217 $markup[] = ' </div>';
218 $markup[] = ' </div>';
219 $markup[] = ' <div class="element-browser-main-content">';
220 $markup[] = ' <div class="element-browser-body">';
221 $markup[] = ' ' . $this->doc->getFlashMessages();
222 $markup[] = ' ' . $files;
223 $markup[] = ' ' . $uploadForm;
224 $markup[] = ' ' . $createFolder;
225 $markup[] = ' </div>';
226 $markup[] = ' </div>';
227 $markup[] = ' </div>';
228 $markup[] = '</div>';
229 $content .= implode('', $markup);
230
231 // Ending page, returning content:
232 $content .= $this->doc->endPage();
233 return $this->doc->insertStylesAndJS($content);
234 }
235
236 /**
237 * For TYPO3 Element Browser: Expand folder of files.
238 *
239 * @param Folder $folder The folder path to expand
240 * @param array $extensionList List of fileextensions to show
241 * @param bool $noThumbs Whether to show thumbnails or not. If set, no thumbnails are shown.
242 * @return string HTML output
243 */
244 public function renderFilesInFolder(Folder $folder, array $extensionList = [], $noThumbs = false)
245 {
246 if (!$folder->checkActionPermission('read')) {
247 return '';
248 }
249 $lang = $this->getLanguageService();
250 $titleLen = (int)$this->getBackendUser()->uc['titleLen'];
251
252 if ($this->searchWord !== '') {
253 $files = $this->fileRepository->searchByName($folder, $this->searchWord);
254 } else {
255 $extensionList = !empty($extensionList) && $extensionList[0] === '*' ? [] : $extensionList;
256 $files = $this->getFilesInFolder($folder, $extensionList);
257 }
258 $filesCount = count($files);
259
260 $lines = [];
261
262 // Create the header of current folder:
263 $folderIcon = $this->iconFactory->getIconForResource($folder, Icon::SIZE_SMALL);
264
265 $lines[] = '
266 <tr>
267 <th class="col-title nowrap">' . $folderIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($folder->getStorage()->getName() . ':' . $folder->getReadablePath(), $titleLen)) . '</th>
268 <th class="col-control nowrap"></th>
269 <th class="col-clipboard nowrap">
270 <a href="#" class="btn btn-default" id="t3js-importSelection" title="' . htmlspecialchars($lang->getLL('importSelection')) . '">' . $this->iconFactory->getIcon('actions-document-import-t3d', Icon::SIZE_SMALL) . '</a>
271 <a href="#" class="btn btn-default" id="t3js-toggleSelection" title="' . htmlspecialchars($lang->getLL('toggleSelection')) . '">' . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL) . '</a>
272 </th>
273 <th class="nowrap">&nbsp;</th>
274 </tr>';
275
276 if ($filesCount === 0) {
277 $lines[] = '
278 <tr>
279 <td colspan="4">No files found.</td>
280 </tr>';
281 }
282
283 foreach ($files as $fileObject) {
284 $fileExtension = $fileObject->getExtension();
285 // Thumbnail/size generation:
286 $imgInfo = [];
287 if (!$noThumbs && GeneralUtility::inList(strtolower($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] . ',' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext']), strtolower($fileExtension))) {
288 $processedFile = $fileObject->process(
289 ProcessedFile::CONTEXT_IMAGEPREVIEW,
290 $this->thumbnailConfiguration
291 );
292 $imageUrl = $processedFile->getPublicUrl(true);
293 $imgInfo = [
294 $fileObject->getProperty('width'),
295 $fileObject->getProperty('height')
296 ];
297 $pDim = $imgInfo[0] . 'x' . $imgInfo[1] . ' pixels';
298 $clickIcon = '<img src="' . $imageUrl . '"'
299 . ' width="' . $processedFile->getProperty('width') . '"'
300 . ' height="' . $processedFile->getProperty('height') . '"'
301 . ' hspace="5" vspace="5" border="1" />';
302 } else {
303 $clickIcon = '';
304 $pDim = '';
305 }
306 // Create file icon:
307 $size = ' (' . GeneralUtility::formatSize($fileObject->getSize(), $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:byteSizeUnits')) . ($pDim ? ', ' . $pDim : '') . ')';
308 $icon = '<span title="id=' . htmlspecialchars($fileObject->getUid()) . '">' . $this->iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL) . '</span>';
309 // Create links for adding the file:
310 $filesIndex = count($this->elements);
311 $this->elements['file_' . $filesIndex] = [
312 'type' => 'file',
313 'table' => 'sys_file',
314 'uid' => $fileObject->getUid(),
315 'fileName' => $fileObject->getName(),
316 'filePath' => $fileObject->getUid(),
317 'fileExt' => $fileExtension,
318 'fileIcon' => $icon
319 ];
320 if ($this->fileIsSelectableInFileList($fileObject, $imgInfo)) {
321 $ATag = '<a href="#" class="btn btn-default" title="' . htmlspecialchars($fileObject->getName()) . '" data-file-index="' . htmlspecialchars($filesIndex) . '" data-close="0">';
322 $ATag .= '<span title="' . htmlspecialchars($lang->getLL('addToList')) . '">' . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . '</span>';
323 $ATag_alt = '<a href="#" title="' . htmlspecialchars($fileObject->getName()) . $size . '" data-file-index="' . htmlspecialchars($filesIndex) . '" data-close="1">';
324 $ATag_e = '</a>';
325 $bulkCheckBox = '<label class="btn btn-default btn-checkbox"><input type="checkbox" class="typo3-bulk-item" name="file_' . $filesIndex . '" value="0" /><span class="t3-icon fa"></span></label>';
326 } else {
327 $ATag = '';
328 $ATag_alt = '';
329 $ATag_e = '';
330 $bulkCheckBox = '';
331 }
332 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
333 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
334 // Create link to showing details about the file in a window:
335 $Ahref = (string)$uriBuilder->buildUriFromRoute('show_item', [
336 'type' => 'file',
337 'table' => '_FILE',
338 'uid' => $fileObject->getCombinedIdentifier(),
339 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
340 ]);
341
342 // Combine the stuff:
343 $filenameAndIcon = $ATag_alt . $icon . htmlspecialchars(GeneralUtility::fixed_lgd_cs($fileObject->getName(), $titleLen)) . $ATag_e;
344 // Show element:
345 $lines[] = '
346 <tr class="file_list_normal">
347 <td class="col-title nowrap">' . $filenameAndIcon . '&nbsp;</td>
348 <td class="col-control">
349 <div class="btn-group">' . $ATag . $ATag_e . '
350 <a href="' . htmlspecialchars($Ahref) . '" class="btn btn-default" title="' . htmlspecialchars($lang->getLL('info')) . '">' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL) . '</a>
351 </td>
352 <td class="col-clipboard" valign="top">' . $bulkCheckBox . '</td>
353 <td class="nowrap">&nbsp;' . $pDim . '</td>
354 </tr>';
355 if ($pDim) {
356 $lines[] = '
357 <tr>
358 <td class="filelistThumbnail" colspan="4">' . $ATag_alt . $clickIcon . $ATag_e . '</td>
359 </tr>';
360 }
361 }
362
363 $markup = [];
364 $markup[] = '<h3>' . htmlspecialchars($lang->getLL('files')) . ' ' . $filesCount . ':</h3>';
365 $markup[] = GeneralUtility::makeInstance(FolderUtilityRenderer::class, $this)->getFileSearchField($this->searchWord);
366 $markup[] = '<div id="filelist">';
367 $markup[] = ' ' . $this->getBulkSelector($filesCount);
368 $markup[] = ' <!-- Filelisting -->';
369 $markup[] = ' <div class="table-fit">';
370 $markup[] = ' <table class="table table-striped table-hover" id="typo3-filelist">';
371 $markup[] = ' ' . implode('', $lines);
372 $markup[] = ' </table>';
373 $markup[] = ' </div>';
374 $markup[] = ' </div>';
375 $content = implode('', $markup);
376
377 return $content;
378 }
379
380 /**
381 * Get a list of Files in a folder filtered by extension
382 *
383 * @param Folder $folder
384 * @param array $extensionList
385 * @return File[]
386 */
387 protected function getFilesInFolder(Folder $folder, array $extensionList)
388 {
389 if (!empty($extensionList)) {
390 /** @var FileExtensionFilter $filter */
391 $filter = GeneralUtility::makeInstance(FileExtensionFilter::class);
392 $filter->setAllowedFileExtensions($extensionList);
393 $folder->setFileAndFolderNameFilters([[$filter, 'filterFileList']]);
394 }
395 return $folder->getFiles();
396 }
397
398 /**
399 * Get the HTML data required for a bulk selection of files of the TYPO3 Element Browser.
400 *
401 * @param int $filesCount Number of files currently displayed
402 * @return string HTML data required for a bulk selection of files - if $filesCount is 0, nothing is returned
403 */
404 protected function getBulkSelector($filesCount)
405 {
406 if (!$filesCount) {
407 return '';
408 }
409
410 $lang = $this->getLanguageService();
411 $out = '';
412
413 // Getting flag for showing/not showing thumbnails:
414 $noThumbsInEB = $this->getBackendUser()->getTSConfig()['options.']['noThumbsInEB'] ?? false;
415 if (!$noThumbsInEB && $this->selectedFolder) {
416 // MENU-ITEMS, fetching the setting for thumbnails from File>List module:
417 $_MOD_MENU = ['displayThumbs' => ''];
418 $_MCONF['name'] = 'file_list';
419 $_MOD_SETTINGS = BackendUtility::getModuleData($_MOD_MENU, GeneralUtility::_GP('SET'), $_MCONF['name']);
420 $addParams = HttpUtility::buildQueryString($this->getUrlParameters(['identifier' => $this->selectedFolder->getCombinedIdentifier()]), '&');
421 $thumbNailCheck = '<div class="checkbox" style="padding:5px 0 15px 0"><label for="checkDisplayThumbs">'
422 . BackendUtility::getFuncCheck(
423 '',
424 'SET[displayThumbs]',
425 $_MOD_SETTINGS['displayThumbs'],
426 $this->thisScript,
427 $addParams,
428 'id="checkDisplayThumbs"'
429 )
430 . htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:displayThumbs')) . '</label></div>';
431 $out .= $thumbNailCheck;
432 } else {
433 $out .= '<div style="padding-top: 15px;"></div>';
434 }
435 return $out;
436 }
437
438 /**
439 * Checks if the given file is selectable in the filelist.
440 *
441 * By default all files are selectable. This method may be overwritten in child classes.
442 *
443 * @param FileInterface $file
444 * @param mixed[] $imgInfo Image dimensions from \TYPO3\CMS\Core\Imaging\GraphicalFunctions::getImageDimensions()
445 * @return bool TRUE if file is selectable.
446 */
447 protected function fileIsSelectableInFileList(FileInterface $file, array $imgInfo)
448 {
449 return true;
450 }
451
452 /**
453 * @return string[] Array of body-tag attributes
454 */
455 protected function getBodyTagAttributes()
456 {
457 return [
458 'data-mode' => 'file',
459 'data-elements' => json_encode($this->elements)
460 ];
461 }
462
463 /**
464 * @param array $values Array of values to include into the parameters
465 * @return string[] Array of parameters which have to be added to URLs
466 */
467 public function getUrlParameters(array $values)
468 {
469 return [
470 'mode' => 'file',
471 'expandFolder' => $values['identifier'] ?? $this->expandFolder,
472 'bparams' => $this->bparams
473 ];
474 }
475
476 /**
477 * @param array $values Values to be checked
478 * @return bool Returns TRUE if the given values match the currently selected item
479 */
480 public function isCurrentlySelectedItem(array $values)
481 {
482 return false;
483 }
484
485 /**
486 * Returns the URL of the current script
487 *
488 * @return string
489 */
490 public function getScriptUrl()
491 {
492 return $this->thisScript;
493 }
494 }