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