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