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