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