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