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