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