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