[BUGFIX] SabreDAV exceptions are not caught
[TYPO3CMS/Extensions/fal_webdav.git] / Classes / Driver / WebDavDriver.php
1 <?php
2 namespace TYPO3\FalWebdav\Driver;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2011 Andreas Wolf
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 include_once 'Sabre/autoload.php';
30
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33
34 class WebDavDriver extends \TYPO3\CMS\Core\Resource\Driver\AbstractDriver {
35
36 /**
37 * The base URL of the WebDAV share. Always ends with a trailing slash.
38 *
39 * @var string
40 */
41 protected $baseUrl;
42
43 /**
44 * The base path of the WebDAV store. This is the URL without protocol, host and port (i.e., only the path on the host).
45 * Always ends with a trailing slash.
46 *
47 * @var string
48 */
49 protected $basePath;
50
51 /**
52 * @var \Sabre_DAV_Client
53 */
54 protected $davClient;
55
56 /**
57 * The username to use for connecting to the storage.
58 *
59 * @var string
60 */
61 protected $username;
62
63 /**
64 * The password to use for connecting to the storage.
65 *
66 * @var string
67 */
68 protected $password;
69
70 /**
71 * @var \TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend
72 */
73 protected $directoryListingCache;
74
75 /**
76 * @var \TYPO3\CMS\Core\Log\Logger
77 */
78 protected $logger;
79
80 public function __construct(array $configuration = array()) {
81 // TODO Iterate through all string properties and trim them...
82 $configuration['baseUrl'] = trim($configuration['baseUrl']);
83 $password = \TYPO3\FalWebdav\Utility\EncryptionUtility::decryptPassword($configuration['password']);
84
85 // TODO check useAuthentication configuration option
86 $this->password = $password;
87 $this->username = $configuration['username'];
88
89 $this->directoryListingCache = $GLOBALS['typo3CacheManager']->getCache('tx_falwebdav_directorylisting');
90
91 $this->logger = GeneralUtility::makeInstance('TYPO3\CMS\Core\Log\LogManager')->getLogger(__CLASS__);
92
93 parent::__construct($configuration);
94 }
95
96 /**
97 * Initializes this object. This is called by the storage after the driver has been attached.
98 *
99 * @return void
100 */
101 public function initialize() {
102 $this->capabilities = \TYPO3\CMS\Core\Resource\ResourceStorage::CAPABILITY_BROWSABLE
103 + \TYPO3\CMS\Core\Resource\ResourceStorage::CAPABILITY_PUBLIC
104 + \TYPO3\CMS\Core\Resource\ResourceStorage::CAPABILITY_WRITABLE;
105 }
106
107 /**
108 * Inject method for the DAV client. Mostly useful for unit tests.
109 *
110 * @param \Sabre_DAV_Client $client
111 */
112 public function injectDavClient(\Sabre_DAV_Client $client) {
113 $this->davClient = $client;
114 }
115
116 /**
117 * Processes the configuration coming from the storage record and prepares the SabreDAV object.
118 *
119 * @return void
120 * @throws \InvalidArgumentException
121 */
122 public function processConfiguration() {
123 $urlInfo = parse_url($this->configuration['baseUrl']);
124 if ($urlInfo === FALSE) {
125 throw new \InvalidArgumentException('Invalid base URL configured for WebDAV driver: ' . $this->configuration['baseUrl'], 1325771040);
126 }
127 $this->basePath = rtrim($urlInfo['path'], '/') . '/';
128
129 $username = $urlInfo['user'] ? $urlInfo['user'] : $this->username;
130 $password = $urlInfo['pass'] ? $urlInfo['pass'] : $this->password;
131
132 $extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['fal_webdav']);
133 $configuration['enableZeroByteFilesIndexing'] = (bool)$extConf['enableZeroByteFilesIndexing'];
134
135 $settings = array(
136 'baseUri' => $this->configuration['baseUrl'],
137 'userName' => $username,
138 'password' => $password
139 );
140 unset($urlInfo['user']);
141 unset($urlInfo['pass']);
142 $this->baseUrl = \TYPO3\CMS\Core\Utility\HttpUtility::buildUrl($urlInfo);
143
144 $this->davClient = new \Sabre_DAV_Client($settings);
145 }
146
147 /**
148 * Checks if a configuration is valid for this driver.
149 *
150 * Throws an exception if a configuration will not work.
151 *
152 * @param array $configuration
153 * @return void
154 */
155 public static function verifyConfiguration(array $configuration) {
156 // TODO: Implement verifyConfiguration() method.
157 }
158
159 /**
160 * Executes a MOVE request from $oldPath to $newPath.
161 *
162 * @param string $oldPath
163 * @param string $newPath
164 * @return array The result as returned by SabreDAV
165 */
166 public function executeMoveRequest($oldPath, $newPath) {
167 $oldUrl = $this->baseUrl . ltrim($oldPath, '/');
168 $newUrl = $this->baseUrl . ltrim($newPath, '/');
169
170 // force overwriting the file (header Overwrite: T) because the Storage already handled possible conflicts
171 // for us
172 return $this->executeDavRequest('MOVE', $oldUrl, NULL, array('Destination' => $newUrl, 'Overwrite' => 'T'));
173 }
174
175 /**
176 * Executes a request on the DAV driver.
177 *
178 * @param string $method
179 * @param string $url
180 * @param string $body
181 * @param array $headers
182 * @return array
183 */
184 protected function executeDavRequest($method, $url, $body = NULL, array $headers = array()) {
185 try {
186 return $this->davClient->request($method, $url, $body, $headers);
187 } catch (\Exception $exception) {
188 $this->logger->error(sprintf(
189 'Error while executing DAV request. Original message: "%s" (Exception id: %u)',
190 $exception->getMessage(), $exception->getCode()
191 ));
192 $this->storage->markAsTemporaryOffline();
193 return array();
194 }
195 }
196
197
198
199 /**
200 * Executes a PROPFIND request on the given URL and returns the result array
201 *
202 * @param string $url
203 * @return array
204 */
205 protected function davPropFind($url) {
206 return $this->davClient->propfind($url, array(
207 '{DAV:}resourcetype',
208 '{DAV:}creationdate',
209 '{DAV:}getcontentlength',
210 '{DAV:}getlastmodified'
211 ), 1);
212 // TODO throw exception on error
213 }
214
215 /**
216 * Checks if a given resource exists in this DAV share.
217 *
218 * @param string $resourcePath The path to the resource, i.e. a regular identifier as used everywhere else here.
219 * @return bool
220 * @throws \InvalidArgumentException
221 */
222 public function resourceExists($resourcePath) {
223 if ($resourcePath == '') {
224 throw new \InvalidArgumentException('Resource path cannot be empty');
225 }
226 $url = $this->baseUrl . ltrim($resourcePath, '/');
227 try {
228 $this->executeDavRequest('HEAD', $url);
229 } catch (\Sabre_DAV_Exception_NotFound $exception) {
230 return FALSE;
231 }
232 // TODO check if other status codes may also indicate that the file is present
233 return TRUE;
234 }
235
236
237 /**
238 * Returns the complete URL to a file. This is not necessarily the publicly available URL!
239 *
240 * @param string|\TYPO3\CMS\Core\Resource\FileInterface|\TYPO3\CMS\Core\Resource\Folder $file The file object or its identifier
241 * @return string
242 */
243 protected function getResourceUrl($file) {
244 if (is_object($file)) {
245 return $this->baseUrl . ltrim($file->getIdentifier(), '/');
246 } else {
247 return $this->baseUrl . ltrim($file, '/');
248 }
249 }
250
251 /**
252 * Returns the public URL to a file.
253 *
254 * @param \TYPO3\CMS\Core\Resource\ResourceInterface $resource
255 * @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)
256 * @return string
257 */
258 public function getPublicUrl(\TYPO3\CMS\Core\Resource\ResourceInterface $resource, $relativeToCurrentScript = FALSE) {
259 if ($this->storage->isPublic()) {
260 // as the storage is marked as public, we can simply use the public URL here.
261 return $this->getResourceUrl($resource);
262 }
263 }
264
265 /**
266 * Creates a (cryptographic) hash for a file.
267 *
268 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
269 * @param string $hashAlgorithm The hash algorithm to use
270 * @return string
271 * TODO switch parameter order?
272 */
273 public function hash(\TYPO3\CMS\Core\Resource\FileInterface $file, $hashAlgorithm) {
274 // TODO add unit test
275 $fileCopy = $this->copyFileToTemporaryPath($file);
276
277 switch ($hashAlgorithm) {
278 case 'sha1':
279 return sha1_file($fileCopy);
280 break;
281 }
282
283 unlink($fileCopy);
284 }
285
286 /**
287 * Creates a new file and returns the matching file object for it.
288 *
289 * @param string $fileName
290 * @param \TYPO3\CMS\Core\Resource\Folder $parentFolder
291 * @return \TYPO3\CMS\Core\Resource\FileInterface
292 */
293 public function createFile($fileName, \TYPO3\CMS\Core\Resource\Folder $parentFolder) {
294 $fileIdentifier = $parentFolder->getIdentifier() . $fileName;
295 $fileUrl = $this->baseUrl . ltrim($fileIdentifier, '/');
296
297 $this->executeDavRequest('PUT', $fileUrl, '');
298
299 $this->removeCacheForPath($parentFolder->getIdentifier());
300
301 return $this->getFile($fileIdentifier);
302 }
303
304 /**
305 * Returns the contents of a file. Beware that this requires to load the complete file into memory and also may
306 * require fetching the file from an external location. So this might be an expensive operation (both in terms of
307 * processing resources and money) for large files.
308 *
309 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
310 * @return string The file contents
311 */
312 public function getFileContents(\TYPO3\CMS\Core\Resource\FileInterface $file) {
313 $fileUrl = $this->baseUrl . ltrim($file->getIdentifier(), '/');
314
315 $result = $this->executeDavRequest('GET', $fileUrl);
316
317 return $result['body'];
318 }
319
320 /**
321 * Sets the contents of a file to the specified value.
322 *
323 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
324 * @param string $contents
325 * @return bool TRUE if setting the contents succeeded
326 * @throws \RuntimeException if the operation failed
327 */
328 public function setFileContents(\TYPO3\CMS\Core\Resource\FileInterface $file, $contents) {
329 // Apache returns a "204 no content" status after a successful put operation
330
331 $fileUrl = $this->getResourceUrl($file);
332 $result = $this->executeDavRequest('PUT', $fileUrl, $contents);
333
334 $this->removeCacheForPath(dirname($file->getIdentifier()));
335
336 // TODO check result
337 }
338
339 /**
340 * Adds a file from the local server hard disk to a given path in TYPO3s virtual file system.
341 *
342 * This assumes that the local file exists, so no further check is done here!
343 *
344 * @param string $localFilePath
345 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
346 * @param string $fileName The name to add the file under
347 * @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.
348 * @return \TYPO3\CMS\Core\Resource\FileInterface
349 * @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException
350 * @throws \RuntimeException
351 */
352 public function addFile($localFilePath, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $fileName, \TYPO3\CMS\Core\Resource\AbstractFile $updateFileObject = NULL) {
353 $fileIdentifier = $targetFolder->getIdentifier() . $fileName;
354 $fileUrl = $this->baseUrl . ltrim($fileIdentifier);
355
356 $fileHandle = fopen($localFilePath, 'r');
357 if (!is_resource($fileHandle)) {
358 throw new \RuntimeException('Could not open handle for ' . $localFilePath, 1325959310);
359 }
360 $result = $this->executeDavRequest('PUT', $fileUrl, $fileHandle);
361
362 // TODO check result
363
364 $this->removeCacheForPath($targetFolder->getIdentifier());
365
366 return $this->getFile($fileIdentifier);
367 }
368
369 /**
370 * Checks if a file exists.
371 *
372 * @param string $identifier
373 * @return bool
374 */
375 public function fileExists($identifier) {
376 return substr($identifier, -1) !== '/' && $this->resourceExists($identifier);
377 }
378
379 /**
380 * Checks if a file inside a storage folder exists.
381 *
382 * @param string $fileName
383 * @param \TYPO3\CMS\Core\Resource\Folder $folder
384 * @return boolean
385 */
386 public function fileExistsInFolder($fileName, \TYPO3\CMS\Core\Resource\Folder $folder) {
387 // TODO add unit test
388 $fileIdentifier = $folder->getIdentifier() . $fileName;
389
390 return $this->fileExists($fileIdentifier);
391 }
392
393 /**
394 * Returns a (local copy of) a file for processing it. When changing the file, you have to take care of replacing the
395 * current version yourself!
396 *
397 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
398 * @param bool $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!
399 * @return string The path to the file on the local disk
400 */
401 public function getFileForLocalProcessing(\TYPO3\CMS\Core\Resource\FileInterface $file, $writable = TRUE) {
402 return $this->copyFileToTemporaryPath($file);
403 }
404
405 /**
406 * Returns the permissions of a file as an array (keys r, w) of boolean flags
407 *
408 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
409 * @return array
410 */
411 public function getFilePermissions(\TYPO3\CMS\Core\Resource\FileInterface $file) {
412 return array('r' => TRUE, 'w' => TRUE);
413 }
414
415 /**
416 * Returns the permissions of a folder as an array (keys r, w) of boolean flags
417 *
418 * @param \TYPO3\CMS\Core\Resource\Folder $folder
419 * @return array
420 */
421 public function getFolderPermissions(\TYPO3\CMS\Core\Resource\Folder $folder) {
422 return array('r' => TRUE, 'w' => TRUE);
423 }
424
425 /**
426 * Renames a file
427 *
428 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
429 * @param string $newName
430 * @return string The new identifier of the file
431 */
432 public function renameFile(\TYPO3\CMS\Core\Resource\FileInterface $file, $newName) {
433 // TODO add unit test
434 // Renaming works by invoking the MOVE method on the source URL and providing the new destination in the
435 // "Destination:" HTTP header.
436 $sourcePath = $file->getIdentifier();
437 $targetPath = dirname($file->getIdentifier()) . '/' . $newName;
438
439 $this->executeMoveRequest($sourcePath, $targetPath);
440
441 $this->removeCacheForPath(dirname($file->getIdentifier()));
442
443 return $targetPath;
444 }
445
446 /**
447 * Replaces the contents (and file-specific metadata) of a file object with a local file.
448 *
449 * @param \TYPO3\CMS\Core\Resource\AbstractFile $file
450 * @param string $localFilePath
451 * @return bool
452 * @throws \RuntimeException
453 */
454 public function replaceFile(\TYPO3\CMS\Core\Resource\AbstractFile $file, $localFilePath) {
455 $fileUrl = $this->getResourceUrl($file);
456 $fileHandle = fopen($localFilePath, 'r');
457 if (!is_resource($fileHandle)) {
458 throw new \RuntimeException('Could not open handle for ' . $localFilePath, 1325959311);
459 }
460
461 $this->removeCacheForPath(dirname($file->getIdentifier()));
462
463 $this->executeDavRequest('PUT', $fileUrl, $fileHandle);
464 }
465
466 /**
467 * Returns information about a file for a given file identifier.
468 *
469 * @param string $identifier The (relative) path to the file.
470 * @return array
471 */
472 public function getFileInfoByIdentifier($identifier) {
473 $fileUrl = $this->baseUrl . ltrim($identifier, '/');
474
475 try {
476 $properties = $this->executeDavRequest('PROPFIND', $fileUrl);
477 $properties = $this->davClient->parseMultiStatus($properties['body']);
478 $properties = $properties[$this->basePath . ltrim($identifier, '/')][200];
479
480 // TODO make this more robust (check if properties are available etc.)
481 $fileInfo = array(
482 'mtime' => strtotime($properties['{DAV:}getlastmodified']),
483 'ctime' => strtotime($properties['{DAV:}creationdate']),
484 'mimetype' => $properties['{DAV:}getcontenttype'],
485 'name' => basename($identifier),
486 'size' => $properties['{DAV:}getcontentlength'],
487 'identifier' => $identifier,
488 'storage' => $this->storage->getUid()
489 );
490 } catch (\Exception $exception) {
491 $fileInfo = array(
492 'name' => basename($identifier),
493 'identifier' => $identifier,
494 'storage' => $this->storage->getUid()
495 );
496 }
497
498 return $fileInfo;
499 }
500
501 /**
502 * Returns a list of files inside the specified path
503 *
504 * @param string $path
505 * @param integer $start The position to start the listing; if not set, start from the beginning
506 * @param integer $numberOfItems The number of items to list; if not set, return all items
507 * @param array $filenameFilterCallbacks Callback methods used for filtering the file list.
508 * @param array $fileData Two-dimensional, identifier-indexed array of file index records from the database
509 * @return array
510 */
511 // TODO add unit tests
512 public function getFileList($path, $start = 0, $numberOfItems = 0, array $filenameFilterCallbacks = array(), $fileData = array()) {
513 return $this->getDirectoryItemList($path, $start, $numberOfItems, $filenameFilterCallbacks, 'getFileList_itemCallback');
514 }
515
516 /**
517 * Returns a list of all folders in a given path
518 *
519 * @param string $path
520 * @param integer $start The position to start the listing; if not set, start from the beginning
521 * @param integer $numberOfItems The number of items to list; if not set, return all items
522 * @param array $foldernameFilterCallbacks Callback methods used for filtering the file list.
523 * @return array
524 */
525 public function getFolderList($path, $start = 0, $numberOfItems = 0, array $foldernameFilterCallbacks = array()) {
526 return $this->getDirectoryItemList($path, $start, $numberOfItems, $foldernameFilterCallbacks, 'getFolderList_itemCallback');
527 }
528
529 /**
530 * Returns a folder within the given folder. Use this method instead of doing your own string manipulation magic
531 * on the identifiers because non-hierarchical storages might fail otherwise.
532 *
533 * @param $name
534 * @param \TYPO3\CMS\Core\Resource\Folder $parentFolder
535 * @return \TYPO3\CMS\Core\Resource\Folder
536 */
537 public function getFolderInFolder($name, \TYPO3\CMS\Core\Resource\Folder $parentFolder) {
538 $folderIdentifier = $parentFolder->getIdentifier() . $name . '/';
539 return $this->getFolder($folderIdentifier);
540 }
541 /**
542 * Generic handler method for directory listings - gluing together the listing items is done
543 *
544 * @param string $path
545 * @param integer $start
546 * @param integer $numberOfItems
547 * @param array $filterMethods
548 * @param callable $itemHandlerMethod
549 * @return array
550 */
551 // TODO implement pre-loaded array rows
552 protected function getDirectoryItemList($path, $start, $numberOfItems, $filterMethods, $itemHandlerMethod) {
553 $path = ltrim($path, '/');
554 $url = $this->baseUrl . $path;
555 // the full (web) path to the current folder on the web server
556 $basePath = $this->basePath . ltrim($path, '/');
557
558 // Try to fetch the raw server response for the given path from our cache. We cache the raw response -
559 // although it might be a bit larger than the processed result - because we mainly do the caching to avoid
560 // the costly server calls - and we might save the most time and load when having the next pages already at
561 // hand for a file browser or the like.
562 $cacheKey = $this->getCacheIdentifierForPath($path);
563 if (!$properties = $this->directoryListingCache->get($cacheKey)) {
564 $properties = $this->davPropFind($url);
565
566 // the returned items are indexed by their key, so sort them here to return the correct items.
567 // At least Apache does not sort them before returning
568 uksort($properties, 'strnatcasecmp');
569
570 // TODO set cache lifetime
571 $this->directoryListingCache->set($cacheKey, $properties);
572 }
573 $propertyIterator = new \ArrayIterator($properties);
574
575 // TODO handle errors
576
577 if ($path !== '' && $path != '/') {
578 $path = '/' . trim($path, '/') . '/';
579 }
580
581 // if we have only one entry, this is the folder we are currently in, so there are no items -> return an empty array
582 if (count($properties) == 1) {
583 return array();
584 }
585
586 $c = $numberOfItems > 0 ? $numberOfItems : $propertyIterator->count();
587 $propertyIterator->seek($start);
588
589 $items = array();
590 while ($propertyIterator->valid() && $c > 0) {
591 $item = $propertyIterator->current();
592 // the full (web) path to the current item on the server
593 $filePath = $propertyIterator->key();
594 $itemName = substr($filePath, strlen($basePath));
595 $propertyIterator->next();
596
597 if ($this->applyFilterMethodsToDirectoryItem($filterMethods, $itemName, $filePath, $basePath, array('item' => $item)) === FALSE) {
598 continue;
599 }
600
601 list($key, $entry) = $this->$itemHandlerMethod($item, $filePath, $basePath, $path);
602
603 if (empty($entry)) {
604 continue;
605 }
606
607 $items[$key] = $entry;
608
609 --$c;
610 }
611
612 return $items;
613 }
614
615 /**
616 * Returns the cache identifier for a given path.
617 *
618 * @param string $path
619 * @return string
620 */
621 protected function getCacheIdentifierForPath($path) {
622 return sha1($this->storage->getUid() . ':' . trim($path, '/') . '/');
623 }
624
625 /**
626 * Flushes the cache for a given path inside this storage.
627 *
628 * @param $path
629 * @return void
630 */
631 protected function removeCacheForPath($path) {
632 $this->directoryListingCache->remove($this->getCacheIdentifierForPath($path));
633 }
634
635 /**
636 * Callback method that extracts file information from a single entry inside a DAV PROPFIND response. Called by getDirectoryItemList.
637 *
638 * @param array $item The information about the item as fetched from the server
639 * @param string $filePath The full path to the item
640 * @param string $basePath The path of the queried folder
641 * @param string $path The queried path (inside the WebDAV storage)
642 * @return array
643 */
644 protected function getFileList_itemCallback(array $item, $filePath, $basePath, $path) {
645 if ($item['{DAV:}resourcetype']->is('{DAV:}collection')) {
646 return array('', array());
647 }
648 $fileName = substr($filePath, strlen($basePath));
649
650 // check if the zero bytes should not be indexed
651 if ($this->configuration['enableZeroByteFilesIndexing'] === FALSE && $item['{DAV:}getcontentlength'] == 0) {
652 return array('', array());
653 }
654
655 // TODO add more information
656 return array($fileName, array(
657 'name' => $fileName,
658 'identifier' => $path . $fileName,
659 'creationDate' => strtotime($item['{DAV:}creationdate']),
660 'storage' => $this->storage->getUid()
661 ));
662 }
663
664 /**
665 * Callback method that extracts folder information from a single entry inside a DAV PROPFIND response. Called by getDirectoryItemList.
666 *
667 * @param array $item The information about the item as fetched from the server
668 * @param string $filePath The full path to the item
669 * @param string $basePath The path of the queried folder
670 * @param string $path The queried path (inside the WebDAV storage)
671 * @return array
672 */
673 protected function getFolderList_itemCallback(array $item, $filePath, $basePath, $path) {
674 if (!$item['{DAV:}resourcetype']->is('{DAV:}collection')) {
675 return array('', array());
676 }
677 $folderName = trim(substr($filePath, strlen($basePath)), '/');
678
679 if ($folderName == '') {
680 return array('', array());
681 }
682
683 // TODO add more information
684 return array($folderName, array(
685 'name' => $folderName,
686 'identifier' => $path . trim($folderName, '/') . '/',
687 'creationDate' => strtotime($item['{DAV:}creationdate']),
688 'storage' => $this->storage->getUid()
689 ));
690 }
691
692 /**
693 * Copies a file to a temporary path and returns that path. You have to take care of removing the temporary file yourself!
694 *
695 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
696 * @return string The temporary path
697 */
698 public function copyFileToTemporaryPath(\TYPO3\CMS\Core\Resource\FileInterface $file) {
699 $temporaryPath = \TYPO3\CMS\Core\Utility\GeneralUtility::tempnam('vfs-tempfile-');
700 $fileUrl = $this->getResourceUrl($file);
701
702 $result = $this->executeDavRequest('GET', $fileUrl);
703 file_put_contents($temporaryPath, $result['body']);
704
705 return $temporaryPath;
706 }
707
708 /**
709 * Moves a file *within* the current storage.
710 * Note that this is only about an intra-storage move action, where a file is just
711 * moved to another folder in the same storage.
712 *
713 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
714 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
715 * @param string $fileName
716 * @return string The new identifier of the file
717 * @throws \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException
718 */
719 public function moveFileWithinStorage(\TYPO3\CMS\Core\Resource\FileInterface $file, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $fileName) {
720 $newPath = $targetFolder->getIdentifier() . $fileName;
721
722 try {
723 $result = $this->executeMoveRequest($file->getIdentifier(), $newPath);
724 } catch (\Sabre_DAV_Exception $e) {
725 // TODO insert correct exception here
726 throw new \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException('Moving file ' . $file->getIdentifier()
727 . ' to ' . $newPath . ' failed.', 1325848030);
728 }
729 // TODO check if there are some return codes that signalize an error, but do not throw an exception
730 // status codes: 204: file was overwritten; 201: file was created;
731
732 return $targetFolder->getIdentifier() . $fileName;
733 }
734
735 /**
736 * Copies a file *within* the current storage.
737 * Note that this is only about an intra-storage copy action, where a file is just
738 * copied to another folder in the same storage.
739 *
740 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
741 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
742 * @param string $fileName
743 * @return \TYPO3\CMS\Core\Resource\FileInterface The new (copied) file object.
744 * @throws \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException
745 */
746 public function copyFileWithinStorage(\TYPO3\CMS\Core\Resource\FileInterface $file, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $fileName) {
747 $oldFileUrl = $this->getResourceUrl($file);
748 $newFileUrl = $this->getResourceUrl($targetFolder) . $fileName;
749 $newFileIdentifier = $targetFolder->getIdentifier() . $fileName;
750
751 try {
752 // force overwriting the file (header Overwrite: T) because the Storage already handled possible conflicts
753 // for us
754 $result = $this->executeDavRequest('COPY', $oldFileUrl, NULL, array('Destination' => $newFileUrl, 'Overwrite' => 'T'));
755 } catch (\Sabre_DAV_Exception $e) {
756 // TODO insert correct exception here
757 throw new \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException('Copying file ' . $file->getIdentifier() . ' to '
758 . $newFileIdentifier . ' failed.', 1325848030);
759 }
760 // TODO check if there are some return codes that signalize an error, but do not throw an exception
761 // status codes: 204: file was overwritten; 201: file was created;
762
763 return $this->getFile($newFileIdentifier);
764 }
765
766 /**
767 * Folder equivalent to moveFileWithinStorage().
768 *
769 * @param \TYPO3\CMS\Core\Resource\Folder $folderToMove
770 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
771 * @param string $newFolderName
772 * @return array Mapping of old file identifiers to new ones
773 * @throws \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException
774 */
775 public function moveFolderWithinStorage(\TYPO3\CMS\Core\Resource\Folder $folderToMove, \TYPO3\CMS\Core\Resource\Folder $targetFolder,
776 $newFolderName) {
777 $newFolderIdentifier = $targetFolder->getIdentifier() . $newFolderName . '/';
778
779 try {
780 $result = $this->executeMoveRequest($folderToMove->getIdentifier(), $newFolderIdentifier);
781 } catch (\Sabre_DAV_Exception $e) {
782 // TODO insert correct exception here
783 throw new \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException('Moving folder ' . $folderToMove->getIdentifier()
784 . ' to ' . $newFolderIdentifier . ' failed: ' . $e->getMessage(), 1326135944);
785 }
786 // TODO check if there are some return codes that signalize an error, but do not throw an exception
787 // status codes: 204: file was overwritten; 201: file was created;
788
789 // TODO extract mapping of old to new identifiers from server response
790 }
791
792 /**
793 * Folder equivalent to copyFileWithinStorage().
794 *
795 * @param \TYPO3\CMS\Core\Resource\Folder $folderToMove
796 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
797 * @param string $newFolderName
798 * @return bool
799 * @throws \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException
800 */
801 public function copyFolderWithinStorage(\TYPO3\CMS\Core\Resource\Folder $folderToMove, \TYPO3\CMS\Core\Resource\Folder $targetFolder,
802 $newFolderName) {
803 $oldFolderUrl = $this->getResourceUrl($folderToMove);
804 $newFolderUrl = $this->getResourceUrl($targetFolder) . $newFolderName . '/';
805 $newFolderIdentifier = $targetFolder->getIdentifier() . $newFolderName . '/';
806
807 try {
808 $result = $this->executeDavRequest('COPY', $oldFolderUrl, NULL, array('Destination' => $newFolderUrl, 'Overwrite' => 'T'));
809 } catch (\Sabre_DAV_Exception $e) {
810 // TODO insert correct exception here
811 throw new \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException('Moving folder ' . $folderToMove->getIdentifier()
812 . ' to ' . $newFolderIdentifier . ' failed.', 1326135944);
813 }
814 // TODO check if there are some return codes that signalize an error, but do not throw an exception
815 // status codes: 204: file was overwritten; 201: file was created;
816
817 return $newFolderIdentifier;
818 }
819
820 /**
821 * Removes a file from this storage. This does not check if the file is still used or if it is a bad idea to delete
822 * it for some other reason - this has to be taken care of in the upper layers (e.g. the Storage)!
823 *
824 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
825 * @return boolean TRUE if the operation succeeded
826 */
827 public function deleteFile(\TYPO3\CMS\Core\Resource\FileInterface $file) {
828 // TODO add unit tests
829 $fileUrl = $this->baseUrl . ltrim($file->getIdentifier(), '/');
830
831 $result = $this->executeDavRequest('DELETE', $fileUrl);
832
833 // 204 is derived from the answer Apache gives - there might be other status codes that indicate success
834 return ($result['statusCode'] == 204);
835 }
836
837 /**
838 * Adds a file at the specified location. This should only be used internally.
839 *
840 * @param string $localFilePath
841 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
842 * @param string $targetFileName
843 * @return string The new identifier of the file
844 */
845 public function addFileRaw($localFilePath, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $targetFileName) {
846 return $this->addFile($localFilePath, $targetFolder, $targetFileName)->getIdentifier();
847 }
848
849 /**
850 * Deletes a file without access and usage checks. This should only be used internally.
851 *
852 * This accepts an identifier instead of an object because we might want to delete files that have no object
853 * associated with (or we don't want to create an object for) them - e.g. when moving a file to another storage.
854 *
855 * @param string $identifier
856 * @return bool TRUE if removing the file succeeded
857 */
858 public function deleteFileRaw($identifier) {
859 return $this->deleteFile($this->getFile($identifier));
860 }
861
862 /**
863 * Returns the root level folder of the storage.
864 *
865 * @return \TYPO3\CMS\Core\Resource\Folder
866 */
867 public function getRootLevelFolder() {
868 return $this->getFolder('/');
869 }
870
871 /**
872 * Returns the default folder new files should be put into.
873 *
874 * @return \TYPO3\CMS\Core\Resource\Folder
875 */
876 public function getDefaultFolder() {
877 return $this->getFolder('/');
878 }
879
880 /**
881 * Creates a folder.
882 *
883 * @param string $newFolderName
884 * @param \TYPO3\CMS\Core\Resource\Folder $parentFolder
885 * @return \TYPO3\CMS\Core\Resource\Folder The new (created) folder object
886 */
887 public function createFolder($newFolderName, \TYPO3\CMS\Core\Resource\Folder $parentFolder) {
888 // We add a slash to the path as some actions require a trailing slash on some servers.
889 // Apache's mod_dav e.g. does not do it for this action, but it does not do harm either, so we add it anyways
890 $folderPath = $parentFolder->getIdentifier() . $newFolderName . '/';
891 $folderUrl = $this->baseUrl . ltrim($folderPath, '/');
892
893 $this->executeDavRequest('MKCOL', $folderUrl);
894
895 $this->removeCacheForPath($parentFolder->getIdentifier());
896
897 /** @var $factory \TYPO3\CMS\Core\Resource\ResourceFactory */
898 $factory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Core\Resource\ResourceFactory');
899 return $factory->createFolderObject($this->storage, $folderPath, $newFolderName);
900 }
901
902 /**
903 * Checks if a folder exists
904 *
905 * @param string $identifier
906 * @return bool
907 */
908 public function folderExists($identifier) {
909 // TODO add unit test
910 // TODO check if this test suffices to find out if the resource really is a folder - it might not do with some implementations
911 $identifier = '/' . trim($identifier, '/') . '/';
912 return $this->resourceExists($identifier);
913 }
914
915 /**
916 * Checks if a file inside a storage folder exists.
917 *
918 * @param string $folderName
919 * @param \TYPO3\CMS\Core\Resource\Folder $folder
920 * @return bool
921 */
922 public function folderExistsInFolder($folderName, \TYPO3\CMS\Core\Resource\Folder $folder) {
923 $folderIdentifier = $folder->getIdentifier() . $folderName . '/';
924 return $this->resourceExists($folderIdentifier);
925 }
926
927 /**
928 * Checks if a given identifier is within a container, e.g. if a file or folder is within another folder.
929 * This can be used to check for webmounts.
930 *
931 * @param \TYPO3\CMS\Core\Resource\Folder $container
932 * @param string $content
933 * @return bool
934 */
935 public function isWithin(\TYPO3\CMS\Core\Resource\Folder $container, $content) {
936 // TODO extend this to also support objects as $content
937 $folderPath = $container->getIdentifier();
938 $content = '/' . ltrim($content, '/');
939
940 return \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($content, $folderPath);
941 }
942
943 /**
944 * Removes a folder from this storage.
945 *
946 * @param \TYPO3\CMS\Core\Resource\Folder $folder
947 * @param bool $deleteRecursively
948 * @return boolean
949 */
950 public function deleteFolder(\TYPO3\CMS\Core\Resource\Folder $folder, $deleteRecursively = FALSE) {
951 $folderUrl = $this->getResourceUrl($folder);
952
953 $this->removeCacheForPath(dirname($folder->getIdentifier()));
954
955 // We don't need to specify a depth header when deleting (see sect. 9.6.1 of RFC #4718)
956 $this->executeDavRequest('DELETE', $folderUrl, '', array());
957 }
958
959 /**
960 * Renames a folder in this storage.
961 *
962 * @param \TYPO3\CMS\Core\Resource\Folder $folder
963 * @param string $newName The new folder name
964 * @return string The new identifier of the folder if the operation succeeds
965 * @throws \RuntimeException if renaming the folder failed
966 * @throws \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException
967 */
968 public function renameFolder(\TYPO3\CMS\Core\Resource\Folder $folder, $newName) {
969 $sourcePath = $folder->getIdentifier();
970 $targetPath = dirname($folder->getIdentifier()) . '/' . $newName . '/';
971
972 try {
973 $result = $this->executeMoveRequest($sourcePath, $targetPath);
974 } catch (\Sabre_DAV_Exception $e) {
975 // TODO insert correct exception here
976 throw new \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException('Renaming ' . $sourcePath . ' to '
977 . $targetPath . ' failed.', 1325848030);
978 }
979
980 $this->removeCacheForPath(dirname($folder->getIdentifier()));
981
982 return $targetPath;
983 }
984
985 /**
986 * Checks if a folder contains files and (if supported) other folders.
987 *
988 * @param \TYPO3\CMS\Core\Resource\Folder $folder
989 * @return bool TRUE if there are no files and folders within $folder
990 */
991 public function isFolderEmpty(\TYPO3\CMS\Core\Resource\Folder $folder) {
992 $folderUrl = $this->getResourceUrl($folder);
993
994 $folderContents = $this->davPropFind($folderUrl);
995
996 return (count($folderContents) == 1);
997 }
998 }