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