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