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