Passed
Branch master (6c65a4)
by Christian
27:15 queued 11:09
created

LocalDriver::getFileInFolder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace TYPO3\CMS\Core\Resource\Driver;
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\Core\Resource\Exception;
18
use TYPO3\CMS\Core\Resource\FolderInterface;
19
use TYPO3\CMS\Core\Resource\ResourceStorage;
20
use TYPO3\CMS\Core\Type\File\FileInfo;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Core\Utility\PathUtility;
23
24
/**
25
 * Driver for the local file system
26
 */
27
class LocalDriver extends AbstractHierarchicalFilesystemDriver
28
{
29
    /**
30
     * @var string
31
     */
32
    const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
33
34
    /**
35
     * The absolute base path. It always contains a trailing slash.
36
     *
37
     * @var string
38
     */
39
    protected $absoluteBasePath;
40
41
    /**
42
     * A list of all supported hash algorithms, written all lower case.
43
     *
44
     * @var array
45
     */
46
    protected $supportedHashAlgorithms = ['sha1', 'md5'];
47
48
    /**
49
     * The base URL that points to this driver's storage. As long is this
50
     * is not set, it is assumed that this folder is not publicly available
51
     *
52
     * @var string
53
     */
54
    protected $baseUri = null;
55
56
    /** @var array */
57
    protected $mappingFolderNameToRole = [
58
        '_recycler_' => FolderInterface::ROLE_RECYCLER,
59
        '_temp_' => FolderInterface::ROLE_TEMPORARY,
60
        'user_upload' => FolderInterface::ROLE_USERUPLOAD,
61
    ];
62
63
    /**
64
     * @param array $configuration
65
     */
66
    public function __construct(array $configuration = [])
67
    {
68
        parent::__construct($configuration);
69
        // The capabilities default of this driver. See CAPABILITY_* constants for possible values
70
        $this->capabilities =
71
            ResourceStorage::CAPABILITY_BROWSABLE
72
            | ResourceStorage::CAPABILITY_PUBLIC
73
            | ResourceStorage::CAPABILITY_WRITABLE;
74
    }
75
76
    /**
77
     * Merges the capabilites merged by the user at the storage
78
     * configuration into the actual capabilities of the driver
79
     * and returns the result.
80
     *
81
     * @param int $capabilities
82
     * @return int
83
     */
84
    public function mergeConfigurationCapabilities($capabilities)
85
    {
86
        $this->capabilities &= $capabilities;
87
        return $this->capabilities;
88
    }
89
90
    /**
91
     * Processes the configuration for this driver.
92
     */
93
    public function processConfiguration()
94
    {
95
        $this->absoluteBasePath = $this->calculateBasePath($this->configuration);
96
        $this->determineBaseUrl();
97
        if ($this->baseUri === null) {
98
            // remove public flag
99
            $this->capabilities &= ~ResourceStorage::CAPABILITY_PUBLIC;
100
        }
101
    }
102
103
    /**
104
     * Initializes this object. This is called by the storage after the driver
105
     * has been attached.
106
     */
107
    public function initialize()
108
    {
109
    }
110
111
    /**
112
     * Determines the base URL for this driver, from the configuration or
113
     * the TypoScript frontend object
114
     */
115
    protected function determineBaseUrl()
116
    {
117
        // only calculate baseURI if the storage does not enforce jumpUrl Script
118
        if ($this->hasCapability(ResourceStorage::CAPABILITY_PUBLIC)) {
119
            if (GeneralUtility::isFirstPartOfStr($this->absoluteBasePath, PATH_site)) {
120
                // use site-relative URLs
121
                $temporaryBaseUri = rtrim(PathUtility::stripPathSitePrefix($this->absoluteBasePath), '/');
122
                if ($temporaryBaseUri !== '') {
123
                    $uriParts = explode('/', $temporaryBaseUri);
124
                    $uriParts = array_map('rawurlencode', $uriParts);
125
                    $temporaryBaseUri = implode('/', $uriParts) . '/';
126
                }
127
                $this->baseUri = $temporaryBaseUri;
128
            } elseif (isset($this->configuration['baseUri']) && GeneralUtility::isValidUrl($this->configuration['baseUri'])) {
129
                $this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
130
            }
131
        }
132
    }
133
134
    /**
135
     * Calculates the absolute path to this drivers storage location.
136
     *
137
     * @throws Exception\InvalidConfigurationException
138
     * @param array $configuration
139
     * @return string
140
     */
141
    protected function calculateBasePath(array $configuration)
142
    {
143
        if (!array_key_exists('basePath', $configuration) || empty($configuration['basePath'])) {
144
            throw new Exception\InvalidConfigurationException(
145
                'Configuration must contain base path.',
146
                1346510477
147
            );
148
        }
149
150
        if ($configuration['pathType'] === 'relative') {
151
            $relativeBasePath = $configuration['basePath'];
152
            $absoluteBasePath = PATH_site . $relativeBasePath;
153
        } else {
154
            $absoluteBasePath = $configuration['basePath'];
155
        }
156
        $absoluteBasePath = $this->canonicalizeAndCheckFilePath($absoluteBasePath);
157
        $absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
158
        if (!is_dir($absoluteBasePath)) {
159
            throw new Exception\InvalidConfigurationException(
160
                'Base path "' . $absoluteBasePath . '" does not exist or is no directory.',
161
                1299233097
162
            );
163
        }
164
        return $absoluteBasePath;
165
    }
166
167
    /**
168
     * Returns the public URL to a file.
169
     * For the local driver, this will always return a path relative to PATH_site.
170
     *
171
     * @param string $identifier
172
     * @return string
173
     * @throws \TYPO3\CMS\Core\Resource\Exception
174
     */
175
    public function getPublicUrl($identifier)
176
    {
177
        $publicUrl = null;
178
        if ($this->baseUri !== null) {
179
            $uriParts = explode('/', ltrim($identifier, '/'));
180
            $uriParts = array_map('rawurlencode', $uriParts);
181
            $identifier = implode('/', $uriParts);
182
            $publicUrl = $this->baseUri . $identifier;
183
        }
184
        return $publicUrl;
185
    }
186
187
    /**
188
     * Returns the Identifier of the root level folder of the storage.
189
     *
190
     * @return string
191
     */
192
    public function getRootLevelFolder()
193
    {
194
        return '/';
195
    }
196
197
    /**
198
     * Returns identifier of the default folder new files should be put into.
199
     *
200
     * @return string
201
     */
202
    public function getDefaultFolder()
203
    {
204
        $identifier = '/user_upload/';
205
        $createFolder = !$this->folderExists($identifier);
206
        if ($createFolder === true) {
207
            $identifier = $this->createFolder('user_upload');
208
        }
209
        return $identifier;
210
    }
211
212
    /**
213
     * Creates a folder, within a parent folder.
214
     * If no parent folder is given, a rootlevel folder will be created
215
     *
216
     * @param string $newFolderName
217
     * @param string $parentFolderIdentifier
218
     * @param bool $recursive
219
     * @return string the Identifier of the new folder
220
     */
221
    public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false)
222
    {
223
        $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
224
        $newFolderName = trim($newFolderName, '/');
225
        if ($recursive == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
226
            $newFolderName = $this->sanitizeFileName($newFolderName);
227
            $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
228
            GeneralUtility::mkdir($this->getAbsolutePath($newIdentifier));
229
        } else {
230
            $parts = GeneralUtility::trimExplode('/', $newFolderName);
231
            $parts = array_map([$this, 'sanitizeFileName'], $parts);
232
            $newFolderName = implode('/', $parts);
233
            $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
234
            GeneralUtility::mkdir_deep($this->getAbsolutePath($parentFolderIdentifier) . '/' . $newFolderName);
235
        }
236
        return $newIdentifier;
237
    }
238
239
    /**
240
     * Returns information about a file.
241
     *
242
     * @param string $fileIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
243
     * @param array $propertiesToExtract Array of properties which should be extracted, if empty all will be extracted
244
     * @return array
245
     * @throws \InvalidArgumentException
246
     */
247
    public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = [])
248
    {
249
        $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
250
        // don't use $this->fileExists() because we need the absolute path to the file anyways, so we can directly
251
        // use PHP's filesystem method.
252
        if (!file_exists($absoluteFilePath) || !is_file($absoluteFilePath)) {
253
            throw new \InvalidArgumentException('File ' . $fileIdentifier . ' does not exist.', 1314516809);
254
        }
255
256
        $dirPath = PathUtility::dirname($fileIdentifier);
257
        $dirPath = $this->canonicalizeAndCheckFolderIdentifier($dirPath);
258
        return $this->extractFileInformation($absoluteFilePath, $dirPath, $propertiesToExtract);
259
    }
260
261
    /**
262
     * Returns information about a folder.
263
     *
264
     * @param string $folderIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
265
     * @return array
266
     * @throws Exception\FolderDoesNotExistException
267
     */
268
    public function getFolderInfoByIdentifier($folderIdentifier)
269
    {
270
        $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
271
272
        if (!$this->folderExists($folderIdentifier)) {
273
            throw new Exception\FolderDoesNotExistException(
274
                'Folder "' . $folderIdentifier . '" does not exist.',
275
                1314516810
276
            );
277
        }
278
        $absolutePath = $this->getAbsolutePath($folderIdentifier);
279
        return [
280
            'identifier' => $folderIdentifier,
281
            'name' => PathUtility::basename($folderIdentifier),
282
            'mtime' => filemtime($absolutePath),
283
            'ctime' => filectime($absolutePath),
284
            'storage' => $this->storageUid
285
        ];
286
    }
287
288
    /**
289
     * Returns a string where any character not matching [.a-zA-Z0-9_-] is
290
     * substituted by '_'
291
     * Trailing dots are removed
292
     *
293
     * Previously in \TYPO3\CMS\Core\Utility\File\BasicFileUtility::cleanFileName()
294
     *
295
     * @param string $fileName Input string, typically the body of a fileName
296
     * @param string $charset Charset of the a fileName (defaults to utf-8)
297
     * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
298
     * @throws Exception\InvalidFileNameException
299
     */
300
    public function sanitizeFileName($fileName, $charset = 'utf-8')
301
    {
302
        // Handle UTF-8 characters
303
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
304
            // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
305
            $cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName));
306
        } else {
307
            $fileName = $this->getCharsetConversion()->specCharsToASCII($charset, $fileName);
308
            // Replace unwanted characters by underscores
309
            $cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName));
310
        }
311
        // Strip trailing dots and return
312
        $cleanFileName = rtrim($cleanFileName, '.');
313
        if ($cleanFileName === '') {
314
            throw new Exception\InvalidFileNameException(
315
                'File name ' . $fileName . ' is invalid.',
316
                1320288991
317
            );
318
        }
319
        return $cleanFileName;
320
    }
321
322
    /**
323
     * Generic wrapper for extracting a list of items from a path.
324
     *
325
     * @param string $folderIdentifier
326
     * @param int $start The position to start the listing; if not set, start from the beginning
327
     * @param int $numberOfItems The number of items to list; if set to zero, all items are returned
328
     * @param array $filterMethods The filter methods used to filter the directory items
329
     * @param bool $includeFiles
330
     * @param bool $includeDirs
331
     * @param bool $recursive
332
     * @param string $sort Property name used to sort the items.
333
     *                     Among them may be: '' (empty, no sorting), name,
334
     *                     fileext, size, tstamp and rw.
335
     *                     If a driver does not support the given property, it
336
     *                     should fall back to "name".
337
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
338
     * @return array
339
     * @throws \InvalidArgumentException
340
     */
341
    protected function getDirectoryItemList($folderIdentifier, $start = 0, $numberOfItems = 0, array $filterMethods, $includeFiles = true, $includeDirs = true, $recursive = false, $sort = '', $sortRev = false)
342
    {
343
        $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
344
        $realPath = $this->getAbsolutePath($folderIdentifier);
345
        if (!is_dir($realPath)) {
346
            throw new \InvalidArgumentException(
347
                'Cannot list items in directory ' . $folderIdentifier . ' - does not exist or is no directory',
348
                1314349666
349
            );
350
        }
351
352
        $items = $this->retrieveFileAndFoldersInPath($realPath, $recursive, $includeFiles, $includeDirs, $sort, $sortRev);
353
        $iterator = new \ArrayIterator($items);
354
        if ($iterator->count() === 0) {
355
            return [];
356
        }
357
358
        // $c is the counter for how many items we still have to fetch (-1 is unlimited)
359
        $c = $numberOfItems > 0 ? $numberOfItems : - 1;
360
        $items = [];
361
        while ($iterator->valid() && ($numberOfItems === 0 || $c > 0)) {
362
            // $iteratorItem is the file or folder name
363
            $iteratorItem = $iterator->current();
364
            // go on to the next iterator item now as we might skip this one early
365
            $iterator->next();
366
367
            try {
368
                if (
369
                !$this->applyFilterMethodsToDirectoryItem(
370
                    $filterMethods,
371
                    $iteratorItem['name'],
372
                    $iteratorItem['identifier'],
373
                    $this->getParentFolderIdentifierOfIdentifier($iteratorItem['identifier'])
374
                )
375
                ) {
376
                    continue;
377
                }
378
                if ($start > 0) {
379
                    $start--;
380
                } else {
381
                    $items[$iteratorItem['identifier']] = $iteratorItem['identifier'];
382
                    // Decrement item counter to make sure we only return $numberOfItems
383
                    // we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
384
                    // item here
385
                    --$c;
386
                }
387
            } catch (Exception\InvalidPathException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
388
            }
389
        }
390
        return $items;
391
    }
392
393
    /**
394
     * Applies a set of filter methods to a file name to find out if it should be used or not. This is e.g. used by
395
     * directory listings.
396
     *
397
     * @param array $filterMethods The filter methods to use
398
     * @param string $itemName
399
     * @param string $itemIdentifier
400
     * @param string $parentIdentifier
401
     * @throws \RuntimeException
402
     * @return bool
403
     */
404
    protected function applyFilterMethodsToDirectoryItem(array $filterMethods, $itemName, $itemIdentifier, $parentIdentifier)
405
    {
406
        foreach ($filterMethods as $filter) {
407
            if (is_callable($filter)) {
408
                $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, [], $this);
409
                // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE
410
                // If calling the method succeeded and thus we can't use that as a return value.
411
                if ($result === -1) {
412
                    return false;
413
                }
414
                if ($result === false) {
415
                    throw new \RuntimeException(
416
                        'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1],
417
                        1476046425
418
                    );
419
                }
420
            }
421
        }
422
        return true;
423
    }
424
425
    /**
426
     * Returns a file inside the specified path
427
     *
428
     * @param string $fileName
429
     * @param string $folderIdentifier
430
     * @return string File Identifier
431
     */
432
    public function getFileInFolder($fileName, $folderIdentifier)
433
    {
434
        return $this->canonicalizeAndCheckFileIdentifier($folderIdentifier . '/' . $fileName);
435
    }
436
437
    /**
438
     * Returns a list of files inside the specified path
439
     *
440
     * @param string $folderIdentifier
441
     * @param int $start
442
     * @param int $numberOfItems
443
     * @param bool $recursive
444
     * @param array $filenameFilterCallbacks The method callbacks to use for filtering the items
445
     * @param string $sort Property name used to sort the items.
446
     *                     Among them may be: '' (empty, no sorting), name,
447
     *                     fileext, size, tstamp and rw.
448
     *                     If a driver does not support the given property, it
449
     *                     should fall back to "name".
450
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
451
     * @return array of FileIdentifiers
452
     */
453
    public function getFilesInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = false, array $filenameFilterCallbacks = [], $sort = '', $sortRev = false)
454
    {
455
        return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, true, false, $recursive, $sort, $sortRev);
456
    }
457
458
    /**
459
     * Returns the number of files inside the specified path
460
     *
461
     * @param string $folderIdentifier
462
     * @param bool $recursive
463
     * @param array $filenameFilterCallbacks callbacks for filtering the items
464
     * @return int Number of files in folder
465
     */
466
    public function countFilesInFolder($folderIdentifier, $recursive = false, array $filenameFilterCallbacks = [])
467
    {
468
        return count($this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filenameFilterCallbacks));
469
    }
470
471
    /**
472
     * Returns a list of folders inside the specified path
473
     *
474
     * @param string $folderIdentifier
475
     * @param int $start
476
     * @param int $numberOfItems
477
     * @param bool $recursive
478
     * @param array $folderNameFilterCallbacks The method callbacks to use for filtering the items
479
     * @param string $sort Property name used to sort the items.
480
     *                     Among them may be: '' (empty, no sorting), name,
481
     *                     fileext, size, tstamp and rw.
482
     *                     If a driver does not support the given property, it
483
     *                     should fall back to "name".
484
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
485
     * @return array of Folder Identifier
486
     */
487
    public function getFoldersInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = false, array $folderNameFilterCallbacks = [], $sort = '', $sortRev = false)
488
    {
489
        return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, false, true, $recursive, $sort, $sortRev);
490
    }
491
492
    /**
493
     * Returns the number of folders inside the specified path
494
     *
495
     * @param string  $folderIdentifier
496
     * @param bool $recursive
497
     * @param array   $folderNameFilterCallbacks callbacks for filtering the items
498
     * @return int Number of folders in folder
499
     */
500
    public function countFoldersInFolder($folderIdentifier, $recursive = false, array $folderNameFilterCallbacks = [])
501
    {
502
        return count($this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $folderNameFilterCallbacks));
503
    }
504
505
    /**
506
     * Returns a list with the names of all files and folders in a path, optionally recursive.
507
     *
508
     * @param string $path The absolute path
509
     * @param bool $recursive If TRUE, recursively fetches files and folders
510
     * @param bool $includeFiles
511
     * @param bool $includeDirs
512
     * @param string $sort Property name used to sort the items.
513
     *                     Among them may be: '' (empty, no sorting), name,
514
     *                     fileext, size, tstamp and rw.
515
     *                     If a driver does not support the given property, it
516
     *                     should fall back to "name".
517
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
518
     * @return array
519
     */
520
    protected function retrieveFileAndFoldersInPath($path, $recursive = false, $includeFiles = true, $includeDirs = true, $sort = '', $sortRev = false)
521
    {
522
        $pathLength = strlen($this->getAbsoluteBasePath());
523
        $iteratorMode = \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::FOLLOW_SYMLINKS;
524
        if ($recursive) {
525
            $iterator = new \RecursiveIteratorIterator(
526
                new \RecursiveDirectoryIterator($path, $iteratorMode),
527
                \RecursiveIteratorIterator::SELF_FIRST,
528
                \RecursiveIteratorIterator::CATCH_GET_CHILD
529
            );
530
        } else {
531
            $iterator = new \RecursiveDirectoryIterator($path, $iteratorMode);
532
        }
533
534
        $directoryEntries = [];
535
        while ($iterator->valid()) {
536
            /** @var $entry \SplFileInfo */
537
            $entry = $iterator->current();
538
            $isFile = $entry->isFile();
539
            $isDirectory = $isFile ? false : $entry->isDir();
540
            if (
541
                (!$isFile && !$isDirectory) // skip non-files/non-folders
542
                || ($isFile && !$includeFiles) // skip files if they are excluded
543
                || ($isDirectory && !$includeDirs) // skip directories if they are excluded
544
                || $entry->getFilename() === '' // skip empty entries
545
            ) {
546
                $iterator->next();
547
                continue;
548
            }
549
            $entryIdentifier = '/' . substr($entry->getPathname(), $pathLength);
550
            $entryName = PathUtility::basename($entryIdentifier);
551
            if ($isDirectory) {
552
                $entryIdentifier .= '/';
553
            }
554
            $entryArray = [
555
                'identifier' => $entryIdentifier,
556
                'name' => $entryName,
557
                'type' => $isDirectory ? 'dir' : 'file'
558
            ];
559
            $directoryEntries[$entryIdentifier] = $entryArray;
560
            $iterator->next();
561
        }
562
        return $this->sortDirectoryEntries($directoryEntries, $sort, $sortRev);
563
    }
564
565
    /**
566
     * Sort the directory entries by a certain key
567
     *
568
     * @param array $directoryEntries Array of directory entry arrays from
569
     *                                retrieveFileAndFoldersInPath()
570
     * @param string $sort Property name used to sort the items.
571
     *                     Among them may be: '' (empty, no sorting), name,
572
     *                     fileext, size, tstamp and rw.
573
     *                     If a driver does not support the given property, it
574
     *                     should fall back to "name".
575
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
576
     * @return array Sorted entries. Content of the keys is undefined.
577
     */
578
    protected function sortDirectoryEntries($directoryEntries, $sort = '', $sortRev = false)
579
    {
580
        $entriesToSort = [];
581
        foreach ($directoryEntries as $entryArray) {
582
            $dir      = pathinfo($entryArray['name'], PATHINFO_DIRNAME) . '/';
583
            $fullPath = $this->getAbsoluteBasePath() . $entryArray['identifier'];
584
            switch ($sort) {
585
                case 'size':
586
                    $sortingKey = '0';
587
                    if ($entryArray['type'] === 'file') {
588
                        $sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'size');
589
                    }
590
                    // Add a character for a natural order sorting
591
                    $sortingKey .= 's';
592
                    break;
593
                case 'rw':
594
                    $perms = $this->getPermissions($entryArray['identifier']);
595
                    $sortingKey = ($perms['r'] ? 'R' : '')
596
                        . ($perms['w'] ? 'W' : '');
597
                    break;
598
                case 'fileext':
599
                    $sortingKey = pathinfo($entryArray['name'], PATHINFO_EXTENSION);
600
                    break;
601
                case 'tstamp':
602
                    $sortingKey = '0';
603
                    if ($entryArray['type'] === 'file') {
604
                        $sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'mtime');
605
                    }
606
                    // Add a character for a natural order sorting
607
                    $sortingKey .= 't';
608
                    break;
609
                case 'name':
610
                case 'file':
611
                default:
612
                    $sortingKey = $entryArray['name'];
613
            }
614
            $i = 0;
615
            while (isset($entriesToSort[$sortingKey . $i])) {
616
                $i++;
617
            }
618
            $entriesToSort[$sortingKey . $i] = $entryArray;
619
        }
620
        uksort($entriesToSort, 'strnatcasecmp');
621
622
        if ($sortRev) {
623
            $entriesToSort = array_reverse($entriesToSort);
624
        }
625
626
        return $entriesToSort;
627
    }
628
629
    /**
630
     * Extracts information about a file from the filesystem.
631
     *
632
     * @param string $filePath The absolute path to the file
633
     * @param string $containerPath The relative path to the file's container
634
     * @param array $propertiesToExtract array of properties which should be returned, if empty all will be extracted
635
     * @return array
636
     */
637
    protected function extractFileInformation($filePath, $containerPath, array $propertiesToExtract = [])
638
    {
639
        if (empty($propertiesToExtract)) {
640
            $propertiesToExtract = [
641
                'size', 'atime', 'atime', 'mtime', 'ctime', 'mimetype', 'name',
642
                'identifier', 'identifier_hash', 'storage', 'folder_hash'
643
            ];
644
        }
645
        $fileInformation = [];
646
        foreach ($propertiesToExtract as $property) {
647
            $fileInformation[$property] = $this->getSpecificFileInformation($filePath, $containerPath, $property);
648
        }
649
        return $fileInformation;
650
    }
651
652
    /**
653
     * Extracts a specific FileInformation from the FileSystems.
654
     *
655
     * @param string $fileIdentifier
656
     * @param string $containerPath
657
     * @param string $property
658
     *
659
     * @return bool|int|string
660
     * @throws \InvalidArgumentException
661
     */
662
    public function getSpecificFileInformation($fileIdentifier, $containerPath, $property)
663
    {
664
        $identifier = $this->canonicalizeAndCheckFileIdentifier($containerPath . PathUtility::basename($fileIdentifier));
665
666
        /** @var FileInfo $fileInfo */
667
        $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $fileIdentifier);
0 ignored issues
show
Bug introduced by
$fileIdentifier of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

667
        $fileInfo = GeneralUtility::makeInstance(FileInfo::class, /** @scrutinizer ignore-type */ $fileIdentifier);
Loading history...
668
        switch ($property) {
669
            case 'size':
670
                return $fileInfo->getSize();
671
            case 'atime':
672
                return $fileInfo->getATime();
673
            case 'mtime':
674
                return $fileInfo->getMTime();
675
            case 'ctime':
676
                return $fileInfo->getCTime();
677
            case 'name':
678
                return PathUtility::basename($fileIdentifier);
0 ignored issues
show
Bug introduced by
$fileIdentifier of type array is incompatible with the type string expected by parameter $path of TYPO3\CMS\Core\Utility\PathUtility::basename(). ( Ignorable by Annotation )

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

678
                return PathUtility::basename(/** @scrutinizer ignore-type */ $fileIdentifier);
Loading history...
679
            case 'mimetype':
680
                return (string)$fileInfo->getMimeType();
681
            case 'identifier':
682
                return $identifier;
683
            case 'storage':
684
                return $this->storageUid;
685
            case 'identifier_hash':
686
                return $this->hashIdentifier($identifier);
687
            case 'folder_hash':
688
                return $this->hashIdentifier($this->getParentFolderIdentifierOfIdentifier($identifier));
689
            default:
690
                throw new \InvalidArgumentException(sprintf('The information "%s" is not available.', $property), 1476047422);
691
        }
692
    }
693
694
    /**
695
     * Returns the absolute path of the folder this driver operates on.
696
     *
697
     * @return string
698
     */
699
    protected function getAbsoluteBasePath()
700
    {
701
        return $this->absoluteBasePath;
702
    }
703
704
    /**
705
     * Returns the absolute path of a file or folder.
706
     *
707
     * @param string $fileIdentifier
708
     * @return string
709
     * @throws Exception\InvalidPathException
710
     */
711
    protected function getAbsolutePath($fileIdentifier)
712
    {
713
        $relativeFilePath = ltrim($this->canonicalizeAndCheckFileIdentifier($fileIdentifier), '/');
714
        $path = $this->absoluteBasePath . $relativeFilePath;
715
        return $path;
716
    }
717
718
    /**
719
     * Creates a (cryptographic) hash for a file.
720
     *
721
     * @param string $fileIdentifier
722
     * @param string $hashAlgorithm The hash algorithm to use
723
     * @return string
724
     * @throws \RuntimeException
725
     * @throws \InvalidArgumentException
726
     */
727
    public function hash($fileIdentifier, $hashAlgorithm)
728
    {
729
        if (!in_array($hashAlgorithm, $this->supportedHashAlgorithms)) {
730
            throw new \InvalidArgumentException('Hash algorithm "' . $hashAlgorithm . '" is not supported.', 1304964032);
731
        }
732
        switch ($hashAlgorithm) {
733
            case 'sha1':
734
                $hash = sha1_file($this->getAbsolutePath($fileIdentifier));
735
                break;
736
            case 'md5':
737
                $hash = md5_file($this->getAbsolutePath($fileIdentifier));
738
                break;
739
            default:
740
                throw new \RuntimeException('Hash algorithm ' . $hashAlgorithm . ' is not implemented.', 1329644451);
741
        }
742
        return $hash;
743
    }
744
745
    /**
746
     * Adds a file from the local server hard disk to a given path in TYPO3s virtual file system.
747
     * This assumes that the local file exists, so no further check is done here!
748
     * After a successful the original file must not exist anymore.
749
     *
750
     * @param string $localFilePath (within PATH_site)
751
     * @param string $targetFolderIdentifier
752
     * @param string $newFileName optional, if not given original name is used
753
     * @param bool $removeOriginal if set the original file will be removed after successful operation
754
     * @return string the identifier of the new file
755
     * @throws \RuntimeException
756
     * @throws \InvalidArgumentException
757
     */
758
    public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true)
759
    {
760
        $localFilePath = $this->canonicalizeAndCheckFilePath($localFilePath);
761
        // as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under PATH_site
762
        // thus, it is not checked here
763
        // @todo is check in storage
764
        if (GeneralUtility::isFirstPartOfStr($localFilePath, $this->absoluteBasePath) && $this->storageUid > 0) {
765
            throw new \InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
766
        }
767
        $newFileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath));
768
        $newFileIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $newFileName;
769
        $targetPath = $this->getAbsolutePath($newFileIdentifier);
770
771
        if ($removeOriginal) {
772
            if (is_uploaded_file($localFilePath)) {
773
                $result = move_uploaded_file($localFilePath, $targetPath);
774
            } else {
775
                $result = rename($localFilePath, $targetPath);
776
            }
777
        } else {
778
            $result = copy($localFilePath, $targetPath);
779
        }
780
        if ($result === false || !file_exists($targetPath)) {
781
            throw new \RuntimeException(
782
                'Adding file ' . $localFilePath . ' at ' . $newFileIdentifier . ' failed.',
783
                1476046453
784
            );
785
        }
786
        clearstatcache();
787
        // Change the permissions of the file
788
        GeneralUtility::fixPermissions($targetPath);
789
        return $newFileIdentifier;
790
    }
791
792
    /**
793
     * Checks if a file exists.
794
     *
795
     * @param string $fileIdentifier
796
     *
797
     * @return bool
798
     */
799
    public function fileExists($fileIdentifier)
800
    {
801
        $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
802
        return is_file($absoluteFilePath);
803
    }
804
805
    /**
806
     * Checks if a file inside a folder exists
807
     *
808
     * @param string $fileName
809
     * @param string $folderIdentifier
810
     * @return bool
811
     */
812
    public function fileExistsInFolder($fileName, $folderIdentifier)
813
    {
814
        $identifier = $folderIdentifier . '/' . $fileName;
815
        $identifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
816
        return $this->fileExists($identifier);
817
    }
818
819
    /**
820
     * Checks if a folder exists.
821
     *
822
     * @param string $folderIdentifier
823
     *
824
     * @return bool
825
     */
826
    public function folderExists($folderIdentifier)
827
    {
828
        $absoluteFilePath = $this->getAbsolutePath($folderIdentifier);
829
        return is_dir($absoluteFilePath);
830
    }
831
832
    /**
833
     * Checks if a folder inside a folder exists.
834
     *
835
     * @param string $folderName
836
     * @param string $folderIdentifier
837
     * @return bool
838
     */
839
    public function folderExistsInFolder($folderName, $folderIdentifier)
840
    {
841
        $identifier = $folderIdentifier . '/' . $folderName;
842
        $identifier = $this->canonicalizeAndCheckFolderIdentifier($identifier);
843
        return $this->folderExists($identifier);
844
    }
845
846
    /**
847
     * Returns the Identifier for a folder within a given folder.
848
     *
849
     * @param string $folderName The name of the target folder
850
     * @param string $folderIdentifier
851
     * @return string
852
     */
853
    public function getFolderInFolder($folderName, $folderIdentifier)
854
    {
855
        $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier . '/' . $folderName);
856
        return $folderIdentifier;
857
    }
858
859
    /**
860
     * Replaces the contents (and file-specific metadata) of a file object with a local file.
861
     *
862
     * @param string $fileIdentifier
863
     * @param string $localFilePath
864
     * @return bool TRUE if the operation succeeded
865
     * @throws \RuntimeException
866
     */
867
    public function replaceFile($fileIdentifier, $localFilePath)
868
    {
869
        $filePath = $this->getAbsolutePath($fileIdentifier);
870
        if (is_uploaded_file($localFilePath)) {
871
            $result = move_uploaded_file($localFilePath, $filePath);
872
        } else {
873
            $result = rename($localFilePath, $filePath);
874
        }
875
        GeneralUtility::fixPermissions($filePath);
876
        if ($result === false) {
877
            throw new \RuntimeException('Replacing file ' . $fileIdentifier . ' with ' . $localFilePath . ' failed.', 1315314711);
878
        }
879
        return $result;
880
    }
881
882
    /**
883
     * Copies a file *within* the current storage.
884
     * Note that this is only about an intra-storage copy action, where a file is just
885
     * copied to another folder in the same storage.
886
     *
887
     * @param string $fileIdentifier
888
     * @param string $targetFolderIdentifier
889
     * @param string $fileName
890
     * @return string the Identifier of the new file
891
     */
892
    public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName)
893
    {
894
        $sourcePath = $this->getAbsolutePath($fileIdentifier);
895
        $newIdentifier = $targetFolderIdentifier . '/' . $fileName;
896
        $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
897
898
        $absoluteFilePath = $this->getAbsolutePath($newIdentifier);
899
        copy($sourcePath, $absoluteFilePath);
900
        GeneralUtility::fixPermissions($absoluteFilePath);
901
        return $newIdentifier;
902
    }
903
904
    /**
905
     * Moves a file *within* the current storage.
906
     * Note that this is only about an inner-storage move action, where a file is just
907
     * moved to another folder in the same storage.
908
     *
909
     * @param string $fileIdentifier
910
     * @param string $targetFolderIdentifier
911
     * @param string $newFileName
912
     * @return string
913
     * @throws \RuntimeException
914
     */
915
    public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName)
916
    {
917
        $sourcePath = $this->getAbsolutePath($fileIdentifier);
918
        $targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
919
        $targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
920
        $result = rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
921
        if ($result === false) {
922
            throw new \RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
923
        }
924
        return $targetIdentifier;
925
    }
926
927
    /**
928
     * Copies a file to a temporary path and returns that path.
929
     *
930
     * @param string $fileIdentifier
931
     * @return string The temporary path
932
     * @throws \RuntimeException
933
     */
934
    protected function copyFileToTemporaryPath($fileIdentifier)
935
    {
936
        $sourcePath = $this->getAbsolutePath($fileIdentifier);
937
        $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
938
        $result = copy($sourcePath, $temporaryPath);
939
        touch($temporaryPath, filemtime($sourcePath));
940
        if ($result === false) {
941
            throw new \RuntimeException(
942
                'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
943
                1320577649
944
            );
945
        }
946
        return $temporaryPath;
947
    }
948
949
    /**
950
     * Moves a file or folder to the given directory, renaming the source in the process if
951
     * a file or folder of the same name already exists in the target path.
952
     *
953
     * @param string $filePath
954
     * @param string $recycleDirectory
955
     * @return bool
956
     */
957
    protected function recycleFileOrFolder($filePath, $recycleDirectory)
958
    {
959
        $destinationFile = $recycleDirectory . '/' . PathUtility::basename($filePath);
960
        if (file_exists($destinationFile)) {
961
            $timeStamp = \DateTimeImmutable::createFromFormat('U.u', microtime(true))->format('YmdHisu');
0 ignored issues
show
Bug introduced by
The call to DateTimeImmutable::createFromFormat() has too few arguments starting with timezone. ( Ignorable by Annotation )

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

961
            $timeStamp = \DateTimeImmutable::/** @scrutinizer ignore-call */ createFromFormat('U.u', microtime(true))->format('YmdHisu');

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
962
            $destinationFile = $recycleDirectory . '/' . $timeStamp . '_' . PathUtility::basename($filePath);
963
        }
964
        $result = rename($filePath, $destinationFile);
965
        // Update the mtime for the file, so the recycler garbage collection task knows which files to delete
966
        // Using ctime() is not possible there since this is not supported on Windows
967
        if ($result) {
968
            touch($destinationFile);
969
        }
970
        return $result;
971
    }
972
973
    /**
974
     * Creates a map of old and new file/folder identifiers after renaming or
975
     * moving a folder. The old identifier is used as the key, the new one as the value.
976
     *
977
     * @param array $filesAndFolders
978
     * @param string $sourceFolderIdentifier
979
     * @param string $targetFolderIdentifier
980
     *
981
     * @return array
982
     * @throws Exception\FileOperationErrorException
983
     */
984
    protected function createIdentifierMap(array $filesAndFolders, $sourceFolderIdentifier, $targetFolderIdentifier)
985
    {
986
        $identifierMap = [];
987
        $identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
988
        foreach ($filesAndFolders as $oldItem) {
989
            if ($oldItem['type'] === 'dir') {
990
                $oldIdentifier = $oldItem['identifier'];
991
                $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
992
                    str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
993
                );
994
            } else {
995
                $oldIdentifier = $oldItem['identifier'];
996
                $newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
997
                    str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
998
                );
999
            }
1000
            if (!file_exists($this->getAbsolutePath($newIdentifier))) {
1001
                throw new Exception\FileOperationErrorException(
1002
                    sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
1003
                    1330119453
1004
                );
1005
            }
1006
            $identifierMap[$oldIdentifier] = $newIdentifier;
1007
        }
1008
        return $identifierMap;
1009
    }
1010
1011
    /**
1012
     * Folder equivalent to moveFileWithinStorage().
1013
     *
1014
     * @param string $sourceFolderIdentifier
1015
     * @param string $targetFolderIdentifier
1016
     * @param string $newFolderName
1017
     *
1018
     * @return array A map of old to new file identifiers
1019
     * @throws \RuntimeException
1020
     */
1021
    public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
1022
    {
1023
        $sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
1024
        $relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
1025
        $targetPath = $this->getAbsolutePath($relativeTargetPath);
1026
        // get all files and folders we are going to move, to have a map for updating later.
1027
        $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
1028
        $result = rename($sourcePath, $targetPath);
1029
        if ($result === false) {
1030
            throw new \RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
1031
        }
1032
        // Create a mapping from old to new identifiers
1033
        $identifierMap = $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
1034
        return $identifierMap;
1035
    }
1036
1037
    /**
1038
     * Folder equivalent to copyFileWithinStorage().
1039
     *
1040
     * @param string $sourceFolderIdentifier
1041
     * @param string $targetFolderIdentifier
1042
     * @param string $newFolderName
1043
     *
1044
     * @return bool
1045
     * @throws Exception\FileOperationErrorException
1046
     */
1047
    public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
1048
    {
1049
        // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
1050
        // We can thus rely on this folder being present and just create the subfolder we want to copy to.
1051
        $newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
1052
        $sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
1053
        $targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
1054
1055
        mkdir($targetFolderPath);
1056
        /** @var $iterator \RecursiveDirectoryIterator */
1057
        $iterator = new \RecursiveIteratorIterator(
1058
            new \RecursiveDirectoryIterator($sourceFolderPath),
0 ignored issues
show
Bug introduced by
The call to RecursiveDirectoryIterator::__construct() has too few arguments starting with flags. ( Ignorable by Annotation )

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

1058
            /** @scrutinizer ignore-call */ 
1059
            new \RecursiveDirectoryIterator($sourceFolderPath),

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1059
            \RecursiveIteratorIterator::SELF_FIRST,
1060
            \RecursiveIteratorIterator::CATCH_GET_CHILD
1061
        );
1062
        // Rewind the iterator as this is important for some systems e.g. Windows
1063
        $iterator->rewind();
1064
        while ($iterator->valid()) {
1065
            /** @var $current \RecursiveDirectoryIterator */
1066
            $current = $iterator->current();
1067
            $fileName = $current->getFilename();
1068
            $itemSubPath = GeneralUtility::fixWindowsFilePath($iterator->getSubPathname());
1069
            if ($current->isDir() && !($fileName === '..' || $fileName === '.')) {
1070
                GeneralUtility::mkdir($targetFolderPath . '/' . $itemSubPath);
1071
            } elseif ($current->isFile()) {
1072
                $copySourcePath = $sourceFolderPath . '/' . $itemSubPath;
1073
                $copyTargetPath = $targetFolderPath . '/' . $itemSubPath;
1074
                $result = copy($copySourcePath, $copyTargetPath);
1075
                if ($result === false) {
1076
                    // rollback
1077
                    GeneralUtility::rmdir($targetFolderIdentifier, true);
1078
                    throw new Exception\FileOperationErrorException(
1079
                        'Copying resource "' . $copySourcePath . '" to "' . $copyTargetPath . '" failed.',
1080
                        1330119452
1081
                    );
1082
                }
1083
            }
1084
            $iterator->next();
1085
        }
1086
        GeneralUtility::fixPermissions($targetFolderPath, true);
1087
        return true;
1088
    }
1089
1090
    /**
1091
     * Renames a file in this storage.
1092
     *
1093
     * @param string $fileIdentifier
1094
     * @param string $newName The target path (including the file name!)
1095
     * @return string The identifier of the file after renaming
1096
     * @throws Exception\ExistingTargetFileNameException
1097
     * @throws \RuntimeException
1098
     */
1099
    public function renameFile($fileIdentifier, $newName)
1100
    {
1101
        // Makes sure the Path given as parameter is valid
1102
        $newName = $this->sanitizeFileName($newName);
1103
        $newIdentifier = rtrim(GeneralUtility::fixWindowsFilePath(PathUtility::dirname($fileIdentifier)), '/') . '/' . $newName;
1104
        $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
1105
        // The target should not exist already
1106
        if ($this->fileExists($newIdentifier)) {
1107
            throw new Exception\ExistingTargetFileNameException(
1108
                'The target file "' . $newIdentifier . '" already exists.',
1109
                1320291063
1110
            );
1111
        }
1112
        $sourcePath = $this->getAbsolutePath($fileIdentifier);
1113
        $targetPath = $this->getAbsolutePath($newIdentifier);
1114
        $result = rename($sourcePath, $targetPath);
1115
        if ($result === false) {
1116
            throw new \RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
1117
        }
1118
        return $newIdentifier;
1119
    }
1120
1121
    /**
1122
     * Renames a folder in this storage.
1123
     *
1124
     * @param string $folderIdentifier
1125
     * @param string $newName
1126
     * @return array A map of old to new file identifiers of all affected files and folders
1127
     * @throws \RuntimeException if renaming the folder failed
1128
     */
1129
    public function renameFolder($folderIdentifier, $newName)
1130
    {
1131
        $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
1132
        $newName = $this->sanitizeFileName($newName);
1133
1134
        $newIdentifier = PathUtility::dirname($folderIdentifier) . '/' . $newName;
1135
        $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
1136
1137
        $sourcePath = $this->getAbsolutePath($folderIdentifier);
1138
        $targetPath = $this->getAbsolutePath($newIdentifier);
1139
        // get all files and folders we are going to move, to have a map for updating later.
1140
        $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
1141
        $result = rename($sourcePath, $targetPath);
1142
        if ($result === false) {
1143
            throw new \RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
1144
        }
1145
        try {
1146
            // Create a mapping from old to new identifiers
1147
            $identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
1148
        } catch (\Exception $e) {
1149
            rename($targetPath, $sourcePath);
1150
            throw new \RuntimeException(
1151
                sprintf(
1152
                    'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
1153
                    $sourcePath,
1154
                    $targetPath,
1155
                    $e->getMessage()
1156
                ),
1157
                1334160746
1158
            );
1159
        }
1160
        return $identifierMap;
1161
    }
1162
1163
    /**
1164
     * Removes a file from the filesystem. This does not check if the file is
1165
     * still used or if it is a bad idea to delete it for some other reason
1166
     * this has to be taken care of in the upper layers (e.g. the Storage)!
1167
     *
1168
     * @param string $fileIdentifier
1169
     * @return bool TRUE if deleting the file succeeded
1170
     * @throws \RuntimeException
1171
     */
1172
    public function deleteFile($fileIdentifier)
1173
    {
1174
        $filePath = $this->getAbsolutePath($fileIdentifier);
1175
        $recycleDirectory = $this->getRecycleDirectory($filePath);
1176
        if (!empty($recycleDirectory)) {
1177
            $result = $this->recycleFileOrFolder($filePath, $recycleDirectory);
1178
        } else {
1179
            $result = unlink($filePath);
1180
        }
1181
        if ($result === false) {
1182
            throw new \RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
1183
        }
1184
        return $result;
1185
    }
1186
1187
    /**
1188
     * Removes a folder from this storage.
1189
     *
1190
     * @param string $folderIdentifier
1191
     * @param bool $deleteRecursively
1192
     * @return bool
1193
     * @throws Exception\FileOperationErrorException
1194
     * @throws Exception\InvalidPathException
1195
     */
1196
    public function deleteFolder($folderIdentifier, $deleteRecursively = false)
1197
    {
1198
        $folderPath = $this->getAbsolutePath($folderIdentifier);
1199
        $recycleDirectory = $this->getRecycleDirectory($folderPath);
1200
        if (!empty($recycleDirectory) && $folderPath !== $recycleDirectory) {
1201
            $result = $this->recycleFileOrFolder($folderPath, $recycleDirectory);
1202
        } else {
1203
            $result = GeneralUtility::rmdir($folderPath, $deleteRecursively);
1204
        }
1205
        if ($result === false) {
1206
            throw new Exception\FileOperationErrorException(
1207
                'Deleting folder "' . $folderIdentifier . '" failed.',
1208
                1330119451
1209
            );
1210
        }
1211
        return $result;
1212
    }
1213
1214
    /**
1215
     * Checks if a folder contains files and (if supported) other folders.
1216
     *
1217
     * @param string $folderIdentifier
1218
     * @return bool TRUE if there are no files and folders within $folder
1219
     */
1220
    public function isFolderEmpty($folderIdentifier)
1221
    {
1222
        $path = $this->getAbsolutePath($folderIdentifier);
1223
        $dirHandle = opendir($path);
1224
        while ($entry = readdir($dirHandle)) {
0 ignored issues
show
Bug introduced by
It seems like $dirHandle can also be of type false; however, parameter $dir_handle of readdir() does only seem to accept resource, 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

1224
        while ($entry = readdir(/** @scrutinizer ignore-type */ $dirHandle)) {
Loading history...
1225
            if ($entry !== '.' && $entry !== '..') {
1226
                closedir($dirHandle);
0 ignored issues
show
Bug introduced by
It seems like $dirHandle can also be of type false; however, parameter $dir_handle of closedir() does only seem to accept resource, 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

1226
                closedir(/** @scrutinizer ignore-type */ $dirHandle);
Loading history...
1227
                return false;
1228
            }
1229
        }
1230
        closedir($dirHandle);
1231
        return true;
1232
    }
1233
1234
    /**
1235
     * Returns (a local copy of) a file for processing it. This makes a copy
1236
     * first when in writable mode, so if you change the file, you have to update it yourself afterwards.
1237
     *
1238
     * @param string $fileIdentifier
1239
     * @param bool $writable Set this to FALSE if you only need the file for read operations.
1240
     *                          This might speed up things, e.g. by using a cached local version.
1241
     *                          Never modify the file if you have set this flag!
1242
     * @return string The path to the file on the local disk
1243
     */
1244
    public function getFileForLocalProcessing($fileIdentifier, $writable = true)
1245
    {
1246
        if ($writable === false) {
1247
            return $this->getAbsolutePath($fileIdentifier);
1248
        }
1249
        return $this->copyFileToTemporaryPath($fileIdentifier);
1250
    }
1251
1252
    /**
1253
     * Returns the permissions of a file/folder as an array (keys r, w) of boolean flags
1254
     *
1255
     * @param string $identifier
1256
     * @return array
1257
     * @throws Exception\ResourcePermissionsUnavailableException
1258
     */
1259
    public function getPermissions($identifier)
1260
    {
1261
        $path = $this->getAbsolutePath($identifier);
1262
        $permissionBits = fileperms($path);
1263
        if ($permissionBits === false) {
1264
            throw new Exception\ResourcePermissionsUnavailableException('Error while fetching permissions for ' . $path, 1319455097);
1265
        }
1266
        return [
1267
            'r' => (bool)is_readable($path),
1268
            'w' => (bool)is_writable($path)
1269
        ];
1270
    }
1271
1272
    /**
1273
     * Checks if a given identifier is within a container, e.g. if
1274
     * a file or folder is within another folder. It will also return
1275
     * TRUE if both canonicalized identifiers are equal.
1276
     *
1277
     * @param string $folderIdentifier
1278
     * @param string $identifier identifier to be checked against $folderIdentifier
1279
     * @return bool TRUE if $content is within or matches $folderIdentifier
1280
     */
1281
    public function isWithin($folderIdentifier, $identifier)
1282
    {
1283
        $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
1284
        $entryIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
1285
        if ($folderIdentifier === $entryIdentifier) {
1286
            return true;
1287
        }
1288
        // File identifier canonicalization will not modify a single slash so
1289
        // we must not append another slash in that case.
1290
        if ($folderIdentifier !== '/') {
1291
            $folderIdentifier .= '/';
1292
        }
1293
        return GeneralUtility::isFirstPartOfStr($entryIdentifier, $folderIdentifier);
1294
    }
1295
1296
    /**
1297
     * Creates a new (empty) file and returns the identifier.
1298
     *
1299
     * @param string $fileName
1300
     * @param string $parentFolderIdentifier
1301
     * @return string
1302
     * @throws Exception\InvalidFileNameException
1303
     * @throws \RuntimeException
1304
     */
1305
    public function createFile($fileName, $parentFolderIdentifier)
1306
    {
1307
        if (!$this->isValidFilename($fileName)) {
1308
            throw new Exception\InvalidFileNameException(
1309
                'Invalid characters in fileName "' . $fileName . '"',
1310
                1320572272
1311
            );
1312
        }
1313
        $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
1314
        $fileIdentifier =  $this->canonicalizeAndCheckFileIdentifier(
1315
            $parentFolderIdentifier . $this->sanitizeFileName(ltrim($fileName, '/'))
1316
        );
1317
        $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
1318
        $result = touch($absoluteFilePath);
1319
        GeneralUtility::fixPermissions($absoluteFilePath);
1320
        clearstatcache();
1321
        if ($result !== true) {
1322
            throw new \RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
1323
        }
1324
        return $fileIdentifier;
1325
    }
1326
1327
    /**
1328
     * Returns the contents of a file. Beware that this requires to load the
1329
     * complete file into memory and also may require fetching the file from an
1330
     * external location. So this might be an expensive operation (both in terms of
1331
     * processing resources and money) for large files.
1332
     *
1333
     * @param string $fileIdentifier
1334
     * @return string The file contents
1335
     */
1336
    public function getFileContents($fileIdentifier)
1337
    {
1338
        $filePath = $this->getAbsolutePath($fileIdentifier);
1339
        return file_get_contents($filePath);
1340
    }
1341
1342
    /**
1343
     * Sets the contents of a file to the specified value.
1344
     *
1345
     * @param string $fileIdentifier
1346
     * @param string $contents
1347
     * @return int The number of bytes written to the file
1348
     * @throws \RuntimeException if the operation failed
1349
     */
1350
    public function setFileContents($fileIdentifier, $contents)
1351
    {
1352
        $filePath = $this->getAbsolutePath($fileIdentifier);
1353
        $result = file_put_contents($filePath, $contents);
1354
1355
        // Make sure later calls to filesize() etc. return correct values.
1356
        clearstatcache(true, $filePath);
1357
1358
        if ($result === false) {
1359
            throw new \RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
1360
        }
1361
        return $result;
1362
    }
1363
1364
    /**
1365
     * Returns the role of an item (currently only folders; can later be extended for files as well)
1366
     *
1367
     * @param string $folderIdentifier
1368
     * @return string
1369
     */
1370
    public function getRole($folderIdentifier)
1371
    {
1372
        $name = PathUtility::basename($folderIdentifier);
1373
        $role = $this->mappingFolderNameToRole[$name] ?? FolderInterface::ROLE_DEFAULT;
1374
        return $role;
1375
    }
1376
1377
    /**
1378
     * Directly output the contents of the file to the output
1379
     * buffer. Should not take care of header files or flushing
1380
     * buffer before. Will be taken care of by the Storage.
1381
     *
1382
     * @param string $identifier
1383
     */
1384
    public function dumpFileContents($identifier)
1385
    {
1386
        readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier)), 0);
1387
    }
1388
1389
    /**
1390
     * Get the path of the nearest recycler folder of a given $path.
1391
     * Return an empty string if there is no recycler folder available.
1392
     *
1393
     * @param string $path
1394
     * @return string
1395
     */
1396
    protected function getRecycleDirectory($path)
1397
    {
1398
        $recyclerSubdirectory = array_search(FolderInterface::ROLE_RECYCLER, $this->mappingFolderNameToRole, true);
1399
        if ($recyclerSubdirectory === false) {
1400
            return '';
1401
        }
1402
        $rootDirectory = rtrim($this->getAbsolutePath($this->getRootLevelFolder()), '/');
1403
        $searchDirectory = PathUtility::dirname($path);
1404
        // Check if file or folder to be deleted is inside a recycler directory
1405
        if ($this->getRole($searchDirectory) === FolderInterface::ROLE_RECYCLER) {
1406
            $searchDirectory = PathUtility::dirname($searchDirectory);
1407
            // Check if file or folder to be deleted is inside the root recycler
1408
            if ($searchDirectory == $rootDirectory) {
1409
                return '';
1410
            }
1411
            $searchDirectory = PathUtility::dirname($searchDirectory);
1412
        }
1413
        // Search for the closest recycler directory
1414
        while ($searchDirectory) {
1415
            $recycleDirectory = $searchDirectory . '/' . $recyclerSubdirectory;
1416
            if (is_dir($recycleDirectory)) {
1417
                return $recycleDirectory;
1418
            }
1419
            if ($searchDirectory === $rootDirectory) {
1420
                return '';
1421
            }
1422
            $searchDirectory = PathUtility::dirname($searchDirectory);
1423
        }
1424
1425
        return '';
1426
    }
1427
}
1428