Test Failed
Branch master (7b1793)
by Tymoteusz
36:50 queued 18:38
created

LocalDriver::getFilesInFolder()   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 7
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);
0 ignored issues
show
Bug introduced by
Are you sure substr($entry->getPathname(), $pathLength) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

549
            $entryIdentifier = '/' . /** @scrutinizer ignore-type */ substr($entry->getPathname(), $pathLength);
Loading history...
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 View Code Duplication
            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 View Code Duplication
        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));
0 ignored issues
show
Bug introduced by
It seems like filemtime($sourcePath) can also be of type false; however, parameter $time of touch() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

939
        touch($temporaryPath, /** @scrutinizer ignore-type */ filemtime($sourcePath));
Loading history...
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 View Code Duplication
        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 View Code Duplication
        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);
0 ignored issues
show
Bug Best Practice introduced by
The expression return file_get_contents($filePath) could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
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