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