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