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