[TASK] Removes extra empty lines
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / Driver / LocalDriver.php
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 */
28 class LocalDriver extends AbstractHierarchicalFilesystemDriver
29 {
30 /**
31 * @var string
32 */
33 const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
34
35 /**
36 * The absolute base path. It always contains a trailing slash.
37 *
38 * @var string
39 */
40 protected $absoluteBasePath;
41
42 /**
43 * A list of all supported hash algorithms, written all lower case.
44 *
45 * @var array
46 */
47 protected $supportedHashAlgorithms = array('sha1', 'md5');
48
49 /**
50 * The base URL that points to this driver's storage. As long is this
51 * is not set, it is assumed that this folder is not publicly available
52 *
53 * @var string
54 */
55 protected $baseUri = null;
56
57 /**
58 * @var \TYPO3\CMS\Core\Charset\CharsetConverter
59 */
60 protected $charsetConversion;
61
62 /** @var array */
63 protected $mappingFolderNameToRole = array(
64 '_recycler_' => FolderInterface::ROLE_RECYCLER,
65 '_temp_' => FolderInterface::ROLE_TEMPORARY,
66 'user_upload' => FolderInterface::ROLE_USERUPLOAD,
67 );
68
69 /**
70 * @param array $configuration
71 */
72 public function __construct(array $configuration = array())
73 {
74 parent::__construct($configuration);
75 // The capabilities default of this driver. See CAPABILITY_* constants for possible values
76 $this->capabilities =
77 ResourceStorage::CAPABILITY_BROWSABLE
78 | ResourceStorage::CAPABILITY_PUBLIC
79 | ResourceStorage::CAPABILITY_WRITABLE;
80 }
81
82 /**
83 * Merges the capabilites merged by the user at the storage
84 * configuration into the actual capabilities of the driver
85 * and returns the result.
86 *
87 * @param int $capabilities
88 * @return int
89 */
90 public function mergeConfigurationCapabilities($capabilities)
91 {
92 $this->capabilities &= $capabilities;
93 return $this->capabilities;
94 }
95
96 /**
97 * Processes the configuration for this driver.
98 *
99 * @return void
100 */
101 public function processConfiguration()
102 {
103 $this->absoluteBasePath = $this->calculateBasePath($this->configuration);
104 $this->determineBaseUrl();
105 if ($this->baseUri === null) {
106 // remove public flag
107 $this->capabilities &= ~ResourceStorage::CAPABILITY_PUBLIC;
108 }
109 }
110
111 /**
112 * Initializes this object. This is called by the storage after the driver
113 * has been attached.
114 *
115 * @return void
116 */
117 public function initialize()
118 {
119 }
120
121 /**
122 * Determines the base URL for this driver, from the configuration or
123 * the TypoScript frontend object
124 *
125 * @return void
126 */
127 protected function determineBaseUrl()
128 {
129 // only calculate baseURI if the storage does not enforce jumpUrl Script
130 if ($this->hasCapability(ResourceStorage::CAPABILITY_PUBLIC)) {
131 if (GeneralUtility::isFirstPartOfStr($this->absoluteBasePath, PATH_site)) {
132 // use site-relative URLs
133 $temporaryBaseUri = rtrim(PathUtility::stripPathSitePrefix($this->absoluteBasePath), '/');
134 if ($temporaryBaseUri !== '') {
135 $uriParts = explode('/', $temporaryBaseUri);
136 $uriParts = array_map('rawurlencode', $uriParts);
137 $temporaryBaseUri = implode('/', $uriParts) . '/';
138 }
139 $this->baseUri = $temporaryBaseUri;
140 } elseif (isset($this->configuration['baseUri']) && GeneralUtility::isValidUrl($this->configuration['baseUri'])) {
141 $this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
142 }
143 }
144 }
145
146 /**
147 * Calculates the absolute path to this drivers storage location.
148 *
149 * @throws Exception\InvalidConfigurationException
150 * @param array $configuration
151 * @return string
152 */
153 protected function calculateBasePath(array $configuration)
154 {
155 if (!array_key_exists('basePath', $configuration) || empty($configuration['basePath'])) {
156 throw new Exception\InvalidConfigurationException(
157 'Configuration must contain base path.',
158 1346510477
159 );
160 }
161
162 if ($configuration['pathType'] === 'relative') {
163 $relativeBasePath = $configuration['basePath'];
164 $absoluteBasePath = PATH_site . $relativeBasePath;
165 } else {
166 $absoluteBasePath = $configuration['basePath'];
167 }
168 $absoluteBasePath = $this->canonicalizeAndCheckFilePath($absoluteBasePath);
169 $absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
170 if (!is_dir($absoluteBasePath)) {
171 throw new Exception\InvalidConfigurationException(
172 'Base path "' . $absoluteBasePath . '" does not exist or is no directory.',
173 1299233097
174 );
175 }
176 return $absoluteBasePath;
177 }
178
179 /**
180 * Returns the public URL to a file.
181 * For the local driver, this will always return a path relative to PATH_site.
182 *
183 * @param string $identifier
184 * @return string
185 * @throws \TYPO3\CMS\Core\Resource\Exception
186 */
187 public function getPublicUrl($identifier)
188 {
189 $publicUrl = null;
190 if ($this->baseUri !== null) {
191 $uriParts = explode('/', ltrim($identifier, '/'));
192 $uriParts = array_map('rawurlencode', $uriParts);
193 $identifier = implode('/', $uriParts);
194 $publicUrl = $this->baseUri . $identifier;
195 }
196 return $publicUrl;
197 }
198
199 /**
200 * Returns the Identifier of the root level folder of the storage.
201 *
202 * @return string
203 */
204 public function getRootLevelFolder()
205 {
206 return '/';
207 }
208
209 /**
210 * Returns identifier of the default folder new files should be put into.
211 *
212 * @return string
213 */
214 public function getDefaultFolder()
215 {
216 $identifier = '/user_upload/';
217 $createFolder = !$this->folderExists($identifier);
218 if ($createFolder === true) {
219 $identifier = $this->createFolder('user_upload');
220 }
221 return $identifier;
222 }
223
224 /**
225 * Creates a folder, within a parent folder.
226 * If no parent folder is given, a rootlevel folder will be created
227 *
228 * @param string $newFolderName
229 * @param string $parentFolderIdentifier
230 * @param bool $recursive
231 * @return string the Identifier of the new folder
232 */
233 public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false)
234 {
235 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
236 $newFolderName = trim($newFolderName, '/');
237 if ($recursive == false) {
238 $newFolderName = $this->sanitizeFileName($newFolderName);
239 $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
240 GeneralUtility::mkdir($this->getAbsolutePath($newIdentifier));
241 } else {
242 $parts = GeneralUtility::trimExplode('/', $newFolderName);
243 $parts = array_map(array($this, 'sanitizeFileName'), $parts);
244 $newFolderName = implode('/', $parts);
245 $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
246 GeneralUtility::mkdir_deep($this->getAbsolutePath($parentFolderIdentifier) . '/', $newFolderName);
247 }
248 return $newIdentifier;
249 }
250
251 /**
252 * Returns information about a file.
253 *
254 * @param string $fileIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
255 * @param array $propertiesToExtract Array of properties which should be extracted, if empty all will be extracted
256 * @return array
257 * @throws \InvalidArgumentException
258 */
259 public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = array())
260 {
261 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
262 // don't use $this->fileExists() because we need the absolute path to the file anyways, so we can directly
263 // use PHP's filesystem method.
264 if (!file_exists($absoluteFilePath) || !is_file($absoluteFilePath)) {
265 throw new \InvalidArgumentException('File ' . $fileIdentifier . ' does not exist.', 1314516809);
266 }
267
268 $dirPath = PathUtility::dirname($fileIdentifier);
269 $dirPath = $this->canonicalizeAndCheckFolderIdentifier($dirPath);
270 return $this->extractFileInformation($absoluteFilePath, $dirPath, $propertiesToExtract);
271 }
272
273 /**
274 * Returns information about a folder.
275 *
276 * @param string $folderIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
277 * @return array
278 * @throws Exception\FolderDoesNotExistException
279 */
280 public function getFolderInfoByIdentifier($folderIdentifier)
281 {
282 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
283
284 if (!$this->folderExists($folderIdentifier)) {
285 throw new Exception\FolderDoesNotExistException(
286 'Folder "' . $folderIdentifier . '" does not exist.',
287 1314516810
288 );
289 }
290 return array(
291 'identifier' => $folderIdentifier,
292 'name' => PathUtility::basename($folderIdentifier),
293 'storage' => $this->storageUid
294 );
295 }
296
297 /**
298 * Returns a string where any character not matching [.a-zA-Z0-9_-] is
299 * substituted by '_'
300 * Trailing dots are removed
301 *
302 * Previously in \TYPO3\CMS\Core\Utility\File\BasicFileUtility::cleanFileName()
303 *
304 * @param string $fileName Input string, typically the body of a fileName
305 * @param string $charset Charset of the a fileName (defaults to current charset; depending on context)
306 * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
307 * @throws Exception\InvalidFileNameException
308 */
309 public function sanitizeFileName($fileName, $charset = '')
310 {
311 // Handle UTF-8 characters
312 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
313 // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
314 $cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName));
315 } else {
316 // Define character set
317 if (!$charset) {
318 if (TYPO3_MODE === 'FE') {
319 $charset = $GLOBALS['TSFE']->renderCharset;
320 } else {
321 // default for Backend
322 $charset = 'utf-8';
323 }
324 }
325 // If a charset was found, convert fileName
326 if ($charset) {
327 $fileName = $this->getCharsetConversion()->specCharsToASCII($charset, $fileName);
328 }
329 // Replace unwanted characters by underscores
330 $cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName));
331 }
332 // Strip trailing dots and return
333 $cleanFileName = rtrim($cleanFileName, '.');
334 if ($cleanFileName === '') {
335 throw new Exception\InvalidFileNameException(
336 'File name ' . $fileName . ' is invalid.',
337 1320288991
338 );
339 }
340 return $cleanFileName;
341 }
342
343 /**
344 * Generic wrapper for extracting a list of items from a path.
345 *
346 * @param string $folderIdentifier
347 * @param int $start The position to start the listing; if not set, start from the beginning
348 * @param int $numberOfItems The number of items to list; if set to zero, all items are returned
349 * @param array $filterMethods The filter methods used to filter the directory items
350 * @param bool $includeFiles
351 * @param bool $includeDirs
352 * @param bool $recursive
353 * @param string $sort Property name used to sort the items.
354 * Among them may be: '' (empty, no sorting), name,
355 * fileext, size, tstamp and rw.
356 * If a driver does not support the given property, it
357 * should fall back to "name".
358 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
359 * @return array
360 * @throws \InvalidArgumentException
361 */
362 protected function getDirectoryItemList($folderIdentifier, $start = 0, $numberOfItems = 0, array $filterMethods, $includeFiles = true, $includeDirs = true, $recursive = false, $sort = '', $sortRev = false)
363 {
364 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
365 $realPath = $this->getAbsolutePath($folderIdentifier);
366 if (!is_dir($realPath)) {
367 throw new \InvalidArgumentException(
368 'Cannot list items in directory ' . $folderIdentifier . ' - does not exist or is no directory',
369 1314349666
370 );
371 }
372
373 if ($start > 0) {
374 $start--;
375 }
376
377 $items = $this->retrieveFileAndFoldersInPath($realPath, $recursive, $includeFiles, $includeDirs, $sort, $sortRev);
378 $iterator = new \ArrayIterator($items);
379 if ($iterator->count() === 0) {
380 return array();
381 }
382 $iterator->seek($start);
383
384 // $c is the counter for how many items we still have to fetch (-1 is unlimited)
385 $c = $numberOfItems > 0 ? $numberOfItems : - 1;
386 $items = array();
387 while ($iterator->valid() && ($numberOfItems === 0 || $c > 0)) {
388 // $iteratorItem is the file or folder name
389 $iteratorItem = $iterator->current();
390 // go on to the next iterator item now as we might skip this one early
391 $iterator->next();
392
393 if (
394 !$this->applyFilterMethodsToDirectoryItem(
395 $filterMethods,
396 $iteratorItem['name'],
397 $iteratorItem['identifier'],
398 $this->getParentFolderIdentifierOfIdentifier($iteratorItem['identifier'])
399 )
400 ) {
401 continue;
402 }
403
404 $items[$iteratorItem['identifier']] = $iteratorItem['identifier'];
405 // Decrement item counter to make sure we only return $numberOfItems
406 // we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
407 // item here
408 --$c;
409 }
410 return $items;
411 }
412
413 /**
414 * 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
415 * directory listings.
416 *
417 * @param array $filterMethods The filter methods to use
418 * @param string $itemName
419 * @param string $itemIdentifier
420 * @param string $parentIdentifier
421 * @throws \RuntimeException
422 * @return bool
423 */
424 protected function applyFilterMethodsToDirectoryItem(array $filterMethods, $itemName, $itemIdentifier, $parentIdentifier)
425 {
426 foreach ($filterMethods as $filter) {
427 if (is_callable($filter)) {
428 $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, array(), $this);
429 // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE
430 // If calling the method succeeded and thus we can't use that as a return value.
431 if ($result === -1) {
432 return false;
433 } elseif ($result === false) {
434 throw new \RuntimeException('Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1]);
435 }
436 }
437 }
438 return true;
439 }
440
441 /**
442 * Returns a file inside the specified path
443 *
444 * @param string $fileName
445 * @param string $folderIdentifier
446 * @return string File Identifier
447 */
448 public function getFileInFolder($fileName, $folderIdentifier)
449 {
450 return $this->canonicalizeAndCheckFileIdentifier($folderIdentifier . '/' . $fileName);
451 }
452
453 /**
454 * Returns a list of files inside the specified path
455 *
456 * @param string $folderIdentifier
457 * @param int $start
458 * @param int $numberOfItems
459 * @param bool $recursive
460 * @param array $filenameFilterCallbacks The method callbacks to use for filtering the items
461 * @param string $sort Property name used to sort the items.
462 * Among them may be: '' (empty, no sorting), name,
463 * fileext, size, tstamp and rw.
464 * If a driver does not support the given property, it
465 * should fall back to "name".
466 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
467 * @return array of FileIdentifiers
468 */
469 public function getFilesInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = false, array $filenameFilterCallbacks = array(), $sort = '', $sortRev = false)
470 {
471 return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, true, false, $recursive, $sort, $sortRev);
472 }
473
474 /**
475 * Returns the number of files inside the specified path
476 *
477 * @param string $folderIdentifier
478 * @param bool $recursive
479 * @param array $filenameFilterCallbacks callbacks for filtering the items
480 * @return int Number of files in folder
481 */
482 public function countFilesInFolder($folderIdentifier, $recursive = false, array $filenameFilterCallbacks = array())
483 {
484 return count($this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filenameFilterCallbacks));
485 }
486
487 /**
488 * Returns a list of folders inside the specified path
489 *
490 * @param string $folderIdentifier
491 * @param int $start
492 * @param int $numberOfItems
493 * @param bool $recursive
494 * @param array $folderNameFilterCallbacks The method callbacks to use for filtering the items
495 * @param string $sort Property name used to sort the items.
496 * Among them may be: '' (empty, no sorting), name,
497 * fileext, size, tstamp and rw.
498 * If a driver does not support the given property, it
499 * should fall back to "name".
500 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
501 * @return array of Folder Identifier
502 */
503 public function getFoldersInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = false, array $folderNameFilterCallbacks = array(), $sort = '', $sortRev = false)
504 {
505 return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, false, true, $recursive, $sort, $sortRev);
506 }
507
508 /**
509 * Returns the number of folders inside the specified path
510 *
511 * @param string $folderIdentifier
512 * @param bool $recursive
513 * @param array $folderNameFilterCallbacks callbacks for filtering the items
514 * @return int Number of folders in folder
515 */
516 public function countFoldersInFolder($folderIdentifier, $recursive = false, array $folderNameFilterCallbacks = array())
517 {
518 return count($this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $folderNameFilterCallbacks));
519 }
520
521 /**
522 * Returns a list with the names of all files and folders in a path, optionally recursive.
523 *
524 * @param string $path The absolute path
525 * @param bool $recursive If TRUE, recursively fetches files and folders
526 * @param bool $includeFiles
527 * @param bool $includeDirs
528 * @param string $sort Property name used to sort the items.
529 * Among them may be: '' (empty, no sorting), name,
530 * fileext, size, tstamp and rw.
531 * If a driver does not support the given property, it
532 * should fall back to "name".
533 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
534 * @return array
535 */
536 protected function retrieveFileAndFoldersInPath($path, $recursive = false, $includeFiles = true, $includeDirs = true, $sort = '', $sortRev = false)
537 {
538 $pathLength = strlen($this->getAbsoluteBasePath());
539 $iteratorMode = \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::FOLLOW_SYMLINKS;
540 if ($recursive) {
541 $iterator = new \RecursiveIteratorIterator(
542 new \RecursiveDirectoryIterator($path, $iteratorMode),
543 \RecursiveIteratorIterator::SELF_FIRST
544 );
545 } else {
546 $iterator = new \RecursiveDirectoryIterator($path, $iteratorMode);
547 }
548
549 $directoryEntries = array();
550 while ($iterator->valid()) {
551 /** @var $entry \SplFileInfo */
552 $entry = $iterator->current();
553 // skip non-files/non-folders, and empty entries
554 if ((!$entry->isFile() && !$entry->isDir()) || $entry->getFilename() == '' ||
555 ($entry->isFile() && !$includeFiles) || ($entry->isDir() && !$includeDirs)) {
556 $iterator->next();
557 continue;
558 }
559 $entryIdentifier = '/' . substr($entry->getPathname(), $pathLength);
560 $entryName = PathUtility::basename($entryIdentifier);
561 if ($entry->isDir()) {
562 $entryIdentifier .= '/';
563 }
564 $entryArray = array(
565 'identifier' => $entryIdentifier,
566 'name' => $entryName,
567 'type' => $entry->isDir() ? 'dir' : 'file'
568 );
569 $directoryEntries[$entryIdentifier] = $entryArray;
570 $iterator->next();
571 }
572 return $this->sortDirectoryEntries($directoryEntries, $sort, $sortRev);
573 }
574
575 /**
576 * Sort the directory entries by a certain key
577 *
578 * @param array $directoryEntries Array of directory entry arrays from
579 * retrieveFileAndFoldersInPath()
580 * @param string $sort Property name used to sort the items.
581 * Among them may be: '' (empty, no sorting), name,
582 * fileext, size, tstamp and rw.
583 * If a driver does not support the given property, it
584 * should fall back to "name".
585 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
586 * @return array Sorted entries. Content of the keys is undefined.
587 */
588 protected function sortDirectoryEntries($directoryEntries, $sort = '', $sortRev = false)
589 {
590 $entriesToSort = array();
591 foreach ($directoryEntries as $entryArray) {
592 $dir = pathinfo($entryArray['name'], PATHINFO_DIRNAME) . '/';
593 $fullPath = $this->getAbsoluteBasePath() . $entryArray['identifier'];
594 switch ($sort) {
595 case 'size':
596 if ($entryArray['type'] === 'file') {
597 $sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'size');
598 } else {
599 $sortingKey = '0';
600 }
601 break;
602 case 'rw':
603 $perms = $this->getPermissions($entryArray['identifier']);
604 $sortingKey = ($perms['r'] ? 'R' : '')
605 . ($perms['w'] ? 'W' : '');
606 break;
607 case 'fileext':
608 $sortingKey = pathinfo($entryArray['name'], PATHINFO_EXTENSION);
609 break;
610 case 'tstamp':
611 if ($entryArray['type'] === 'file') {
612 $sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'mtime');
613 } else {
614 $sortingKey = '0';
615 }
616 break;
617 case 'name':
618 case 'file':
619 default:
620 $sortingKey = $entryArray['name'];
621 }
622 $i = 0;
623 while (isset($entriesToSort[$sortingKey . $i])) {
624 $i++;
625 }
626 $entriesToSort[$sortingKey . $i] = $entryArray;
627 }
628 uksort($entriesToSort, 'strnatcasecmp');
629
630 if ($sortRev) {
631 $entriesToSort = array_reverse($entriesToSort);
632 }
633
634 return $entriesToSort;
635 }
636
637 /**
638 * Extracts information about a file from the filesystem.
639 *
640 * @param string $filePath The absolute path to the file
641 * @param string $containerPath The relative path to the file's container
642 * @param array $propertiesToExtract array of properties which should be returned, if empty all will be extracted
643 * @return array
644 */
645 protected function extractFileInformation($filePath, $containerPath, array $propertiesToExtract = array())
646 {
647 if (empty($propertiesToExtract)) {
648 $propertiesToExtract = array(
649 'size', 'atime', 'atime', 'mtime', 'ctime', 'mimetype', 'name',
650 'identifier', 'identifier_hash', 'storage', 'folder_hash'
651 );
652 }
653 $fileInformation = array();
654 foreach ($propertiesToExtract as $property) {
655 $fileInformation[$property] = $this->getSpecificFileInformation($filePath, $containerPath, $property);
656 }
657 return $fileInformation;
658 }
659
660 /**
661 * Extracts a specific FileInformation from the FileSystems.
662 *
663 * @param string $fileIdentifier
664 * @param string $containerPath
665 * @param string $property
666 *
667 * @return bool|int|string
668 * @throws \InvalidArgumentException
669 */
670 public function getSpecificFileInformation($fileIdentifier, $containerPath, $property)
671 {
672 $identifier = $this->canonicalizeAndCheckFileIdentifier($containerPath . PathUtility::basename($fileIdentifier));
673
674 /** @var FileInfo $fileInfo */
675 $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $fileIdentifier);
676 switch ($property) {
677 case 'size':
678 return $fileInfo->getSize();
679 case 'atime':
680 return $fileInfo->getATime();
681 case 'mtime':
682 return $fileInfo->getMTime();
683 case 'ctime':
684 return $fileInfo->getCTime();
685 case 'name':
686 return PathUtility::basename($fileIdentifier);
687 case 'mimetype':
688 return (string)$fileInfo->getMimeType();
689 case 'identifier':
690 return $identifier;
691 case 'storage':
692 return $this->storageUid;
693 case 'identifier_hash':
694 return $this->hashIdentifier($identifier);
695 case 'folder_hash':
696 return $this->hashIdentifier($this->getParentFolderIdentifierOfIdentifier($identifier));
697 default:
698 throw new \InvalidArgumentException(sprintf('The information "%s" is not available.', $property));
699 }
700 }
701
702 /**
703 * Returns the absolute path of the folder this driver operates on.
704 *
705 * @return string
706 */
707 protected function getAbsoluteBasePath()
708 {
709 return $this->absoluteBasePath;
710 }
711
712 /**
713 * Returns the absolute path of a file or folder.
714 *
715 * @param string $fileIdentifier
716 * @return string
717 * @throws Exception\InvalidPathException
718 */
719 protected function getAbsolutePath($fileIdentifier)
720 {
721 $relativeFilePath = ltrim($this->canonicalizeAndCheckFileIdentifier($fileIdentifier), '/');
722 $path = $this->absoluteBasePath . $relativeFilePath;
723 return $path;
724 }
725
726 /**
727 * Creates a (cryptographic) hash for a file.
728 *
729 * @param string $fileIdentifier
730 * @param string $hashAlgorithm The hash algorithm to use
731 * @return string
732 * @throws \RuntimeException
733 * @throws \InvalidArgumentException
734 */
735 public function hash($fileIdentifier, $hashAlgorithm)
736 {
737 if (!in_array($hashAlgorithm, $this->supportedHashAlgorithms)) {
738 throw new \InvalidArgumentException('Hash algorithm "' . $hashAlgorithm . '" is not supported.', 1304964032);
739 }
740 switch ($hashAlgorithm) {
741 case 'sha1':
742 $hash = sha1_file($this->getAbsolutePath($fileIdentifier));
743 break;
744 case 'md5':
745 $hash = md5_file($this->getAbsolutePath($fileIdentifier));
746 break;
747 default:
748 throw new \RuntimeException('Hash algorithm ' . $hashAlgorithm . ' is not implemented.', 1329644451);
749 }
750 return $hash;
751 }
752
753 /**
754 * Adds a file from the local server hard disk to a given path in TYPO3s virtual file system.
755 * This assumes that the local file exists, so no further check is done here!
756 * After a successful the original file must not exist anymore.
757 *
758 * @param string $localFilePath (within PATH_site)
759 * @param string $targetFolderIdentifier
760 * @param string $newFileName optional, if not given original name is used
761 * @param bool $removeOriginal if set the original file will be removed after successful operation
762 * @return string the identifier of the new file
763 * @throws \RuntimeException
764 * @throws \InvalidArgumentException
765 */
766 public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true)
767 {
768 $localFilePath = $this->canonicalizeAndCheckFilePath($localFilePath);
769 // as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under PATH_site
770 // thus, it is not checked here
771 // @todo is check in storage
772 if (GeneralUtility::isFirstPartOfStr($localFilePath, $this->absoluteBasePath) && $this->storageUid > 0) {
773 throw new \InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
774 }
775 $newFileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath));
776 $newFileIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $newFileName;
777 $targetPath = $this->getAbsolutePath($newFileIdentifier);
778
779 if ($removeOriginal) {
780 if (is_uploaded_file($localFilePath)) {
781 $result = move_uploaded_file($localFilePath, $targetPath);
782 } else {
783 $result = rename($localFilePath, $targetPath);
784 }
785 } else {
786 $result = copy($localFilePath, $targetPath);
787 }
788 if ($result === false || !file_exists($targetPath)) {
789 throw new \RuntimeException('Adding file ' . $localFilePath . ' at ' . $newFileIdentifier . ' failed.');
790 }
791 clearstatcache();
792 // Change the permissions of the file
793 GeneralUtility::fixPermissions($targetPath);
794 return $newFileIdentifier;
795 }
796
797 /**
798 * Checks if a file exists.
799 *
800 * @param string $fileIdentifier
801 *
802 * @return bool
803 */
804 public function fileExists($fileIdentifier)
805 {
806 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
807 return is_file($absoluteFilePath);
808 }
809
810 /**
811 * Checks if a file inside a folder exists
812 *
813 * @param string $fileName
814 * @param string $folderIdentifier
815 * @return bool
816 */
817 public function fileExistsInFolder($fileName, $folderIdentifier)
818 {
819 $identifier = $folderIdentifier . '/' . $fileName;
820 $identifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
821 return $this->fileExists($identifier);
822 }
823
824 /**
825 * Checks if a folder exists.
826 *
827 * @param string $folderIdentifier
828 *
829 * @return bool
830 */
831 public function folderExists($folderIdentifier)
832 {
833 $absoluteFilePath = $this->getAbsolutePath($folderIdentifier);
834 return is_dir($absoluteFilePath);
835 }
836
837 /**
838 * Checks if a folder inside a folder exists.
839 *
840 * @param string $folderName
841 * @param string $folderIdentifier
842 * @return bool
843 */
844 public function folderExistsInFolder($folderName, $folderIdentifier)
845 {
846 $identifier = $folderIdentifier . '/' . $folderName;
847 $identifier = $this->canonicalizeAndCheckFolderIdentifier($identifier);
848 return $this->folderExists($identifier);
849 }
850
851 /**
852 * Returns the Identifier for a folder within a given folder.
853 *
854 * @param string $folderName The name of the target folder
855 * @param string $folderIdentifier
856 * @return string
857 */
858 public function getFolderInFolder($folderName, $folderIdentifier)
859 {
860 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier . '/' . $folderName);
861 return $folderIdentifier;
862 }
863
864 /**
865 * Replaces the contents (and file-specific metadata) of a file object with a local file.
866 *
867 * @param string $fileIdentifier
868 * @param string $localFilePath
869 * @return bool TRUE if the operation succeeded
870 * @throws \RuntimeException
871 */
872 public function replaceFile($fileIdentifier, $localFilePath)
873 {
874 $filePath = $this->getAbsolutePath($fileIdentifier);
875 $result = rename($localFilePath, $filePath);
876 GeneralUtility::fixPermissions($filePath);
877 if ($result === false) {
878 throw new \RuntimeException('Replacing file ' . $fileIdentifier . ' with ' . $localFilePath . ' failed.', 1315314711);
879 }
880 return $result;
881 }
882
883 /**
884 * Copies a file *within* the current storage.
885 * Note that this is only about an intra-storage copy action, where a file is just
886 * copied to another folder in the same storage.
887 *
888 * @param string $fileIdentifier
889 * @param string $targetFolderIdentifier
890 * @param string $fileName
891 * @return string the Identifier of the new file
892 */
893 public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName)
894 {
895 $sourcePath = $this->getAbsolutePath($fileIdentifier);
896 $newIdentifier = $targetFolderIdentifier . '/' . $fileName;
897 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
898
899 $absoluteFilePath = $this->getAbsolutePath($newIdentifier);
900 copy($sourcePath, $absoluteFilePath);
901 GeneralUtility::fixPermissions($absoluteFilePath);
902 return $newIdentifier;
903 }
904
905 /**
906 * Moves a file *within* the current storage.
907 * Note that this is only about an inner-storage move action, where a file is just
908 * moved to another folder in the same storage.
909 *
910 * @param string $fileIdentifier
911 * @param string $targetFolderIdentifier
912 * @param string $newFileName
913 * @return string
914 * @throws \RuntimeException
915 */
916 public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName)
917 {
918 $sourcePath = $this->getAbsolutePath($fileIdentifier);
919 $targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
920 $targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
921 $result = rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
922 if ($result === false) {
923 throw new \RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
924 }
925 return $targetIdentifier;
926 }
927
928 /**
929 * Copies a file to a temporary path and returns that path.
930 *
931 * @param string $fileIdentifier
932 * @return string The temporary path
933 * @throws \RuntimeException
934 */
935 protected function copyFileToTemporaryPath($fileIdentifier)
936 {
937 $sourcePath = $this->getAbsolutePath($fileIdentifier);
938 $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
939 $result = copy($sourcePath, $temporaryPath);
940 touch($temporaryPath, filemtime($sourcePath));
941 if ($result === false) {
942 throw new \RuntimeException(
943 'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
944 1320577649
945 );
946 }
947 return $temporaryPath;
948 }
949
950 /**
951 * Creates a map of old and new file/folder identifiers after renaming or
952 * moving a folder. The old identifier is used as the key, the new one as the value.
953 *
954 * @param array $filesAndFolders
955 * @param string $sourceFolderIdentifier
956 * @param string $targetFolderIdentifier
957 *
958 * @return array
959 * @throws Exception\FileOperationErrorException
960 */
961 protected function createIdentifierMap(array $filesAndFolders, $sourceFolderIdentifier, $targetFolderIdentifier)
962 {
963 $identifierMap = array();
964 $identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
965 foreach ($filesAndFolders as $oldItem) {
966 if ($oldItem['type'] == 'dir') {
967 $oldIdentifier = $oldItem['identifier'];
968 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
969 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
970 );
971 } else {
972 $oldIdentifier = $oldItem['identifier'];
973 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
974 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
975 );
976 }
977 if (!file_exists($this->getAbsolutePath($newIdentifier))) {
978 throw new Exception\FileOperationErrorException(
979 sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
980 1330119453
981 );
982 }
983 $identifierMap[$oldIdentifier] = $newIdentifier;
984 }
985 return $identifierMap;
986 }
987
988 /**
989 * Folder equivalent to moveFileWithinStorage().
990 *
991 * @param string $sourceFolderIdentifier
992 * @param string $targetFolderIdentifier
993 * @param string $newFolderName
994 *
995 * @return array A map of old to new file identifiers
996 * @throws \RuntimeException
997 */
998 public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
999 {
1000 $sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
1001 $relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
1002 $targetPath = $this->getAbsolutePath($relativeTargetPath);
1003 // get all files and folders we are going to move, to have a map for updating later.
1004 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
1005 $result = rename($sourcePath, $targetPath);
1006 if ($result === false) {
1007 throw new \RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
1008 }
1009 // Create a mapping from old to new identifiers
1010 $identifierMap = $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
1011 return $identifierMap;
1012 }
1013
1014 /**
1015 * Folder equivalent to copyFileWithinStorage().
1016 *
1017 * @param string $sourceFolderIdentifier
1018 * @param string $targetFolderIdentifier
1019 * @param string $newFolderName
1020 *
1021 * @return bool
1022 * @throws Exception\FileOperationErrorException
1023 */
1024 public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
1025 {
1026 // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
1027 // We can thus rely on this folder being present and just create the subfolder we want to copy to.
1028 $newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
1029 $sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
1030 $targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
1031
1032 mkdir($targetFolderPath);
1033 /** @var $iterator \RecursiveDirectoryIterator */
1034 $iterator = new \RecursiveIteratorIterator(
1035 new \RecursiveDirectoryIterator($sourceFolderPath),
1036 \RecursiveIteratorIterator::SELF_FIRST
1037 );
1038 // Rewind the iterator as this is important for some systems e.g. Windows
1039 $iterator->rewind();
1040 while ($iterator->valid()) {
1041 /** @var $current \RecursiveDirectoryIterator */
1042 $current = $iterator->current();
1043 $fileName = $current->getFilename();
1044 $itemSubPath = GeneralUtility::fixWindowsFilePath($iterator->getSubPathname());
1045 if ($current->isDir() && !($fileName === '..' || $fileName === '.')) {
1046 GeneralUtility::mkdir($targetFolderPath . '/' . $itemSubPath);
1047 } elseif ($current->isFile()) {
1048 $result = copy($sourceFolderPath . '/' . $itemSubPath, $targetFolderPath . '/' . $itemSubPath);
1049 if ($result === false) {
1050 // rollback
1051 GeneralUtility::rmdir($targetFolderIdentifier, true);
1052 throw new Exception\FileOperationErrorException(
1053 'Copying file "' . $sourceFolderPath . $itemSubPath . '" to "' . $targetFolderPath . $itemSubPath . '" failed.',
1054 1330119452
1055 );
1056 }
1057 }
1058 $iterator->next();
1059 }
1060 GeneralUtility::fixPermissions($targetFolderPath, true);
1061 return true;
1062 }
1063
1064 /**
1065 * Renames a file in this storage.
1066 *
1067 * @param string $fileIdentifier
1068 * @param string $newName The target path (including the file name!)
1069 * @return string The identifier of the file after renaming
1070 * @throws Exception\ExistingTargetFileNameException
1071 * @throws \RuntimeException
1072 */
1073 public function renameFile($fileIdentifier, $newName)
1074 {
1075 // Makes sure the Path given as parameter is valid
1076 $newName = $this->sanitizeFileName($newName);
1077 $newIdentifier = rtrim(GeneralUtility::fixWindowsFilePath(PathUtility::dirname($fileIdentifier)), '/') . '/' . $newName;
1078 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
1079 // The target should not exist already
1080 if ($this->fileExists($newIdentifier)) {
1081 throw new Exception\ExistingTargetFileNameException(
1082 'The target file "' . $newIdentifier . '" already exists.',
1083 1320291063
1084 );
1085 }
1086 $sourcePath = $this->getAbsolutePath($fileIdentifier);
1087 $targetPath = $this->getAbsolutePath($newIdentifier);
1088 $result = rename($sourcePath, $targetPath);
1089 if ($result === false) {
1090 throw new \RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
1091 }
1092 return $newIdentifier;
1093 }
1094
1095 /**
1096 * Renames a folder in this storage.
1097 *
1098 * @param string $folderIdentifier
1099 * @param string $newName
1100 * @return array A map of old to new file identifiers of all affected files and folders
1101 * @throws \RuntimeException if renaming the folder failed
1102 */
1103 public function renameFolder($folderIdentifier, $newName)
1104 {
1105 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
1106 $newName = $this->sanitizeFileName($newName);
1107
1108 $newIdentifier = PathUtility::dirname($folderIdentifier) . '/' . $newName;
1109 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
1110
1111 $sourcePath = $this->getAbsolutePath($folderIdentifier);
1112 $targetPath = $this->getAbsolutePath($newIdentifier);
1113 // get all files and folders we are going to move, to have a map for updating later.
1114 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
1115 $result = rename($sourcePath, $targetPath);
1116 if ($result === false) {
1117 throw new \RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
1118 }
1119 try {
1120 // Create a mapping from old to new identifiers
1121 $identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
1122 } catch (\Exception $e) {
1123 rename($targetPath, $sourcePath);
1124 throw new \RuntimeException(
1125 sprintf(
1126 'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
1127 $sourcePath, $targetPath, $e->getMessage()
1128 ),
1129 1334160746
1130 );
1131 }
1132 return $identifierMap;
1133 }
1134
1135 /**
1136 * Removes a file from the filesystem. This does not check if the file is
1137 * still used or if it is a bad idea to delete it for some other reason
1138 * this has to be taken care of in the upper layers (e.g. the Storage)!
1139 *
1140 * @param string $fileIdentifier
1141 * @return bool TRUE if deleting the file succeeded
1142 * @throws \RuntimeException
1143 */
1144 public function deleteFile($fileIdentifier)
1145 {
1146 $filePath = $this->getAbsolutePath($fileIdentifier);
1147 $result = unlink($filePath);
1148 if ($result === false) {
1149 throw new \RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
1150 }
1151 return $result;
1152 }
1153
1154 /**
1155 * Removes a folder from this storage.
1156 *
1157 * @param string $folderIdentifier
1158 * @param bool $deleteRecursively
1159 * @return bool
1160 * @throws Exception\FileOperationErrorException
1161 * @throws Exception\InvalidPathException
1162 */
1163 public function deleteFolder($folderIdentifier, $deleteRecursively = false)
1164 {
1165 $folderPath = $this->getAbsolutePath($folderIdentifier);
1166 $result = GeneralUtility::rmdir($folderPath, $deleteRecursively);
1167 if ($result === false) {
1168 throw new Exception\FileOperationErrorException(
1169 'Deleting folder "' . $folderIdentifier . '" failed.',
1170 1330119451
1171 );
1172 }
1173 return $result;
1174 }
1175
1176 /**
1177 * Checks if a folder contains files and (if supported) other folders.
1178 *
1179 * @param string $folderIdentifier
1180 * @return bool TRUE if there are no files and folders within $folder
1181 */
1182 public function isFolderEmpty($folderIdentifier)
1183 {
1184 $path = $this->getAbsolutePath($folderIdentifier);
1185 $dirHandle = opendir($path);
1186 while ($entry = readdir($dirHandle)) {
1187 if ($entry !== '.' && $entry !== '..') {
1188 closedir($dirHandle);
1189 return false;
1190 }
1191 }
1192 closedir($dirHandle);
1193 return true;
1194 }
1195
1196 /**
1197 * Returns (a local copy of) a file for processing it. This makes a copy
1198 * first when in writable mode, so if you change the file, you have to update it yourself afterwards.
1199 *
1200 * @param string $fileIdentifier
1201 * @param bool $writable Set this to FALSE if you only need the file for read operations.
1202 * This might speed up things, e.g. by using a cached local version.
1203 * Never modify the file if you have set this flag!
1204 * @return string The path to the file on the local disk
1205 */
1206 public function getFileForLocalProcessing($fileIdentifier, $writable = true)
1207 {
1208 if ($writable === false) {
1209 return $this->getAbsolutePath($fileIdentifier);
1210 } else {
1211 return $this->copyFileToTemporaryPath($fileIdentifier);
1212 }
1213 }
1214
1215 /**
1216 * Returns the permissions of a file/folder as an array (keys r, w) of boolean flags
1217 *
1218 * @param string $identifier
1219 * @return array
1220 * @throws Exception\ResourcePermissionsUnavailableException
1221 */
1222 public function getPermissions($identifier)
1223 {
1224 $path = $this->getAbsolutePath($identifier);
1225 $permissionBits = fileperms($path);
1226 if ($permissionBits === false) {
1227 throw new Exception\ResourcePermissionsUnavailableException('Error while fetching permissions for ' . $path, 1319455097);
1228 }
1229 return array(
1230 'r' => (bool)is_readable($path),
1231 'w' => (bool)is_writable($path)
1232 );
1233 }
1234
1235 /**
1236 * Checks if a given identifier is within a container, e.g. if
1237 * a file or folder is within another folder. It will also return
1238 * TRUE if both canonicalized identifiers are equal.
1239 *
1240 * @param string $folderIdentifier
1241 * @param string $identifier identifier to be checked against $folderIdentifier
1242 * @return bool TRUE if $content is within or matches $folderIdentifier
1243 */
1244 public function isWithin($folderIdentifier, $identifier)
1245 {
1246 $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
1247 $entryIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
1248 if ($folderIdentifier === $entryIdentifier) {
1249 return true;
1250 }
1251 // File identifier canonicalization will not modify a single slash so
1252 // we must not append another slash in that case.
1253 if ($folderIdentifier !== '/') {
1254 $folderIdentifier .= '/';
1255 }
1256 return GeneralUtility::isFirstPartOfStr($entryIdentifier, $folderIdentifier);
1257 }
1258
1259 /**
1260 * Creates a new (empty) file and returns the identifier.
1261 *
1262 * @param string $fileName
1263 * @param string $parentFolderIdentifier
1264 * @return string
1265 * @throws Exception\InvalidFileNameException
1266 * @throws \RuntimeException
1267 */
1268 public function createFile($fileName, $parentFolderIdentifier)
1269 {
1270 if (!$this->isValidFilename($fileName)) {
1271 throw new Exception\InvalidFileNameException(
1272 'Invalid characters in fileName "' . $fileName . '"',
1273 1320572272
1274 );
1275 }
1276 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
1277 $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
1278 $parentFolderIdentifier . $this->sanitizeFileName(ltrim($fileName, '/'))
1279 );
1280 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
1281 $result = touch($absoluteFilePath);
1282 GeneralUtility::fixPermissions($absoluteFilePath);
1283 clearstatcache();
1284 if ($result !== true) {
1285 throw new \RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
1286 }
1287 return $fileIdentifier;
1288 }
1289
1290 /**
1291 * Returns the contents of a file. Beware that this requires to load the
1292 * complete file into memory and also may require fetching the file from an
1293 * external location. So this might be an expensive operation (both in terms of
1294 * processing resources and money) for large files.
1295 *
1296 * @param string $fileIdentifier
1297 * @return string The file contents
1298 */
1299 public function getFileContents($fileIdentifier)
1300 {
1301 $filePath = $this->getAbsolutePath($fileIdentifier);
1302 return file_get_contents($filePath);
1303 }
1304
1305 /**
1306 * Sets the contents of a file to the specified value.
1307 *
1308 * @param string $fileIdentifier
1309 * @param string $contents
1310 * @return int The number of bytes written to the file
1311 * @throws \RuntimeException if the operation failed
1312 */
1313 public function setFileContents($fileIdentifier, $contents)
1314 {
1315 $filePath = $this->getAbsolutePath($fileIdentifier);
1316 $result = file_put_contents($filePath, $contents);
1317
1318 // Make sure later calls to filesize() etc. return correct values.
1319 clearstatcache(true, $filePath);
1320
1321 if ($result === false) {
1322 throw new \RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
1323 }
1324 return $result;
1325 }
1326
1327 /**
1328 * Gets the charset conversion object.
1329 *
1330 * @return \TYPO3\CMS\Core\Charset\CharsetConverter
1331 */
1332 protected function getCharsetConversion()
1333 {
1334 if (!isset($this->charsetConversion)) {
1335 if (TYPO3_MODE === 'FE') {
1336 $this->charsetConversion = $GLOBALS['TSFE']->csConvObj;
1337 } elseif (is_object($GLOBALS['LANG'])) {
1338 // BE assumed:
1339 $this->charsetConversion = $GLOBALS['LANG']->csConvObj;
1340 } else {
1341 // The object may not exist yet, so we need to create it now. Happens in the Install Tool for example.
1342 $this->charsetConversion = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Charset\CharsetConverter::class);
1343 }
1344 }
1345 return $this->charsetConversion;
1346 }
1347
1348 /**
1349 * Returns the role of an item (currently only folders; can later be extended for files as well)
1350 *
1351 * @param string $folderIdentifier
1352 * @return string
1353 */
1354 public function getRole($folderIdentifier)
1355 {
1356 $name = PathUtility::basename($folderIdentifier);
1357 $role = $this->mappingFolderNameToRole[$name];
1358 if (empty($role)) {
1359 $role = FolderInterface::ROLE_DEFAULT;
1360 }
1361 return $role;
1362 }
1363
1364 /**
1365 * Directly output the contents of the file to the output
1366 * buffer. Should not take care of header files or flushing
1367 * buffer before. Will be taken care of by the Storage.
1368 *
1369 * @param string $identifier
1370 * @return void
1371 */
1372 public function dumpFileContents($identifier)
1373 {
1374 readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier)), 0);
1375 }
1376 }