[FEATURE] Bundle WebDAV access in separate class
authorAndreas Wolf <andreas.wolf@typo3.org>
Wed, 3 Jun 2015 13:59:08 +0000 (15:59 +0200)
committerAndreas Wolf <andreas.wolf@typo3.org>
Wed, 3 Jun 2015 14:03:21 +0000 (16:03 +0200)
Classes/Dav/WebDavFrontend.php [new file with mode: 0644]
Classes/Driver/WebDavDriver.php

diff --git a/Classes/Dav/WebDavFrontend.php b/Classes/Dav/WebDavFrontend.php
new file mode 100644 (file)
index 0000000..af7e4ae
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+namespace TYPO3\FalWebdav\Dav;
+
+/**
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Sabre\DAV;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
+use TYPO3\FalWebdav\Utility\UrlTools;
+
+
+/**
+ * Utility class for doing DAV requests to the server and decoding the results into a form usable by the driver
+ *
+ *
+ */
+class WebDavFrontend {
+
+       /**
+        * @var WebDavClient
+        */
+       protected $davClient;
+
+       /**
+        * The storage base URL, has to be already URL-encoded
+        *
+        * @var string
+        */
+       protected $baseUrl;
+
+       /**
+        * @var string
+        */
+       protected $basePath;
+
+       /**
+        * @var \TYPO3\CMS\Core\Log\Logger
+        */
+       protected $logger;
+
+       /**
+        * @var int
+        */
+       protected $storageUid;
+
+
+       public function __construct(WebDavClient $client, $baseUrl, $storageUid) {
+               $this->logger = GeneralUtility::makeInstance('TYPO3\CMS\Core\Log\LogManager')->getLogger(__CLASS__);
+
+               $this->davClient = $client;
+               $this->baseUrl = rtrim($baseUrl, '/') . '/';
+               $urlParts = parse_url($this->baseUrl);
+               $this->basePath = $urlParts['path'];
+               $this->storageUid = $storageUid;
+       }
+
+       /**
+        * @param string $url
+        * @return string
+        */
+       protected function encodeUrl($url) {
+               $urlParts = parse_url($url);
+               $urlParts['path'] = $this->urlEncodePath($urlParts['path']);
+
+               return HttpUtility::buildUrl($urlParts);
+       }
+
+       /**
+        * Wrapper around the PROPFIND method of the WebDAV client to get proper local error handling.
+        *
+        * @param string $url
+        * @return array
+        * @throws DAV\Exception\NotFound if the given URL does not hold a resource
+        *
+        * TODO define proper error handling for other cases
+        */
+       public function propFind($url) {
+               // TODO check cache
+               $encodedUrl = $this->encodeUrl($url);
+
+               try {
+                       // propfind() already decodes the XML, so we get back an array
+                       $propfindResultArray = $this->davClient->propfind($encodedUrl, NULL, 1);
+
+                       // the returned items are indexed by their key, so sort them here to return the correct items.
+                       // At least Apache does not sort them before returning
+                       uksort($propfindResultArray, 'strnatcasecmp');
+
+                       // TODO store result in cache
+
+                       return $propfindResultArray;
+               } catch (DAV\Exception\NotFound $exception) {
+                       $this->logger->warning('URL not found: ' . $url);
+                       // If a file is not found, we have to deal with that on a higher level, so throw the exception again
+                       throw $exception;
+               } catch (DAV\Exception $exception) {
+                       // log all other exceptions
+                       $this->logger->error(sprintf(
+                               'Error while executing DAV PROPFIND request. Original message: "%s" (Exception %s, id: %u)',
+                               $exception->getMessage(), get_class($exception), $exception->getCode()
+                       ));
+                       // TODO check how we can let this propagate to the driver
+                       return array();
+               }
+       }
+
+       /**
+        *
+        * @param string $path
+        * @return array A list of file names in the path
+        */
+       public function listFiles($path) {
+               $this->logger->debug("cache identifier: " . $this->getCacheIdentifierForResponse($path));
+
+               // TODO check cache
+
+               $files = $this->listItems($path, function ($currentItem) {
+                       if (substr($currentItem, -1) == '/') {
+                               return FALSE;
+                       }
+                       return TRUE;
+               });
+
+               // TODO store result in cache
+
+               return $files;
+       }
+
+       /**
+        * @param string $path
+        * @return array A list of folder names in the path
+        */
+       public function listFolders($path) {
+               // TODO caching
+               $folders = $this->listItems($path, function ($currentItem) {
+                       if (substr($currentItem, -1) != '/') {
+                               return FALSE;
+                       }
+                       return TRUE;
+               });
+
+               return $folders;
+       }
+
+       protected function listItems($path, $itemFilterCallback) {
+               $path = trim($path, '/');
+               if (strlen($path) > 0) {
+                       $path .= '/';
+               }
+               $url = $this->baseUrl . $path;
+               $urlParts = parse_url($url);
+               $unencodedBasePath = $urlParts['path'];
+
+               $result = $this->propFind($url);
+
+               // remove first entry, as it is the folder itself
+               array_shift($result);
+
+               $files = array();
+               // $filePath contains the path part of the URL, no server name and protocol!
+               foreach ($result as $filePath => $fileInfo) {
+                       $decodedFilePath = urldecode($filePath);
+                       $decodedFilePath = substr($decodedFilePath, strlen($unencodedBasePath));
+                       // ignore folder entries
+                       if (!$itemFilterCallback($decodedFilePath)) {
+                               continue;
+                       }
+
+                       // TODO if depth is > 1, we will also include deeper entries here, which we should not
+
+                       $files[] = basename($decodedFilePath);
+               }
+
+               return $files;
+       }
+
+       /**
+        * Returns information about the given file.
+        *
+        * TODO define what to return
+        *
+        * @param string $path
+        * @return array
+        */
+       public function getFileInfo($path) {
+               $url = $this->baseUrl . $path;
+
+               $result = $this->propFind($url);
+
+               return $this->extractFileInfo($path, $result[$this->basePath . $this->urlEncodePath($path)]);
+       }
+
+       protected function extractFileInfo($path, $propFindArray) {
+$this->logger->debug("File info for $path: " . json_encode($propFindArray));
+               $fileInfo = array(
+                       'mtime' => (int)strtotime($propFindArray['{DAV:}getlastmodified']),
+                       'ctime' => (int)strtotime($propFindArray['{DAV:}creationdate']),
+                       'mimetype' => (string)$propFindArray['{DAV:}getcontenttype'],
+                       'name' => basename($path),
+                       'size' => (int)$propFindArray['{DAV:}getcontentlength'],
+                       'identifier' => '/' . $path,
+                       'storage' => $this->storageUid
+               );
+
+               return $fileInfo;
+       }
+
+       /**
+        * Returns the cache identifier for the raw response for a given path
+        *
+        * @param string $path
+        * @return string
+        */
+       protected function getCacheIdentifierForResponse($path) {
+               return 'davResponse-' . sha1($this->baseUrl . ':' . trim($path, '/') . '/');
+       }
+
+       /**
+        * Returns the cache identifier for the file list of a given path.
+        *
+        * @param string $path
+        * @return string
+        */
+       protected function getCacheIdentifierForFileList($path) {
+               return 'filelist-' . sha1($this->baseUrl . ':' . trim($path, '/') . '/');
+       }
+
+       /**
+        * Returns the cache identifier for the file list of a given path.
+        *
+        * @param string $path
+        * @return string
+        */
+       protected function getCacheIdentifierForFileInfo($path) {
+               return 'fileinfo-' . sha1($this->baseUrl . ':' . trim($path, '/') . '/');
+       }
+
+       /**
+        * @param $path
+        * @return string
+        */
+       protected function urlEncodePath($path) {
+               // using urlencode() does not work because it encodes a space as "+" and not as "%20".
+               return implode('/', array_map('rawurlencode', explode('/', $path)));
+       }
+
+}
index 5cf60a8..a9e09b8 100644 (file)
@@ -25,6 +25,7 @@ use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
+use TYPO3\FalWebdav\Dav\WebDavFrontend;
 use TYPO3\FalWebdav\Dav\WebDavClient;
 use TYPO3\FalWebdav\Utility\EncryptionUtility;
 
@@ -82,6 +83,11 @@ class WebDavDriver extends AbstractDriver {
        protected $directoryListingCache;
 
        /**
+        * @var WebDavFrontend
+        */
+       protected $frontend;
+
+       /**
         * @var \TYPO3\CMS\Core\Log\Logger
         */
        protected $logger;
@@ -121,6 +127,17 @@ class WebDavDriver extends AbstractDriver {
                $this->directoryListingCache = $cache;
        }
 
+       public function injectFrontend(WebDavFrontend $frontend) {
+               $this->frontend = $frontend;
+       }
+
+       protected function getFrontend() {
+               if (!$this->frontend) {
+                       $this->frontend = new WebDavFrontend($this->davClient, $this->baseUrl, $this->storageUid);
+               }
+               return $this->frontend;
+       }
+
        /**
         * @return FrontendInterface
         */
@@ -211,6 +228,13 @@ class WebDavDriver extends AbstractDriver {
                return $this->executeDavRequest('MOVE', $oldUrl, NULL, array('Destination' => $newUrl, 'Overwrite' => 'T'));
        }
 
+       protected function encodeUrl($url) {
+               $urlParts = parse_url($url);
+               $urlParts['path'] = implode('/', array_map('rawurlencode', explode('/', $urlParts['path'])));
+
+               return HttpUtility::buildUrl($urlParts);
+       }
+
        /**
         * Executes a request on the DAV driver.
         *
@@ -223,6 +247,7 @@ class WebDavDriver extends AbstractDriver {
         */
        protected function executeDavRequest($method, $url, $body = NULL, array $headers = array()) {
                try {
+                       $url = $this->encodeUrl($url);
                        return $this->davClient->request($method, $url, $body, $headers);
                } catch (\Sabre\DAV\Exception\NotFound $exception) {
                        // If a file is not found, we have to deal with that on a higher level, so throw the exception again
@@ -241,30 +266,6 @@ class WebDavDriver extends AbstractDriver {
 
 
        /**
-        * Executes a PROPFIND request on the given URL and returns the result array
-        *
-        * @param string $url
-        * @return array
-        * @throws \Exception If anything goes wrong
-        */
-       protected function davPropFind($url) {
-               try {
-                       return $this->davClient->propFind($url, NULL, 1);
-               } catch (\Sabre\DAV\Exception\NotFound $exception) {
-                       // If a file is not found, we have to deal with that on a higher level, so throw the exception again
-                       throw $exception;
-               } catch (DAV\Exception $exception) {
-                       // log all other exceptions
-                       $this->logger->error(sprintf(
-                               'Error while executing DAV PROPFIND request. Original message: "%s" (Exception %s, id: %u)',
-                               $exception->getMessage(), get_class($exception), $exception->getCode()
-                       ));
-                       // TODO check how we can let this propagate to the driver
-                       return array();
-               }
-       }
-
-       /**
         * Checks if a given resource exists in this DAV share.
         *
         * @param string $resourcePath The path to the resource, i.e. a regular identifier as used everywhere else here.
@@ -500,109 +501,10 @@ class WebDavDriver extends AbstractDriver {
         * @return array
         */
        public function getFileInfoByIdentifier($identifier, array $propertiesToExtract = array()) {
+               assert($identifier[0] === '/', 'Identifier must start with a slash, got ' . $identifier);
                $fileUrl = $this->baseUrl . ltrim($identifier, '/');
 
-               try {
-                       $properties = $this->executeDavRequest('PROPFIND', $fileUrl);
-                       $properties = $this->davClient->parseMultiStatus($properties['body']);
-                       $properties = $properties[$this->basePath . ltrim($identifier, '/')][200];
-
-                       // TODO make this more robust (check if properties are available etc.)
-                       $fileInfo = array(
-                               'mtime' => strtotime($properties['{DAV:}getlastmodified']),
-                               'ctime' => strtotime($properties['{DAV:}creationdate']),
-                               'mimetype' => $properties['{DAV:}getcontenttype'],
-                               'name' => basename($identifier),
-                               'size' => $properties['{DAV:}getcontentlength'],
-                               'identifier' => $identifier,
-                               'storage' => $this->storageUid
-                       );
-               } catch (DAV\Exception $exception) {
-                       $fileInfo = array(
-                               'name' => basename($identifier),
-                               'identifier' => $identifier,
-                               'storage' => $this->storageUid
-                       );
-               }
-
-               return $fileInfo;
-       }
-
-       /**
-        * Generic handler method for directory listings - gluing together the listing items is done
-        *
-        * @param string $path
-        * @param integer $start
-        * @param integer $numberOfItems
-        * @param array $filterMethods
-        * @param callable $itemHandlerMethod
-        * @return array
-        */
-       // TODO implement pre-loaded array rows
-       protected function getDirectoryItemList($path, $start, $numberOfItems, $filterMethods, $itemHandlerMethod) {
-               $path = ltrim($path, '/');
-               $url = $this->baseUrl . $path;
-                       // the full (web) path to the current folder on the web server
-               $basePath = $this->basePath . ltrim($path, '/');
-
-                       // Try to fetch the raw server response for the given path from our cache. We cache the raw response -
-                       // although it might be a bit larger than the processed result - because we mainly do the caching to avoid
-                       // the costly server calls - and we might save the most time and load when having the next pages already at
-                       // hand for a file browser or the like.
-               $cacheKey = $this->getCacheIdentifierForPath($path);
-               if (!$properties = $this->getCache()->get($cacheKey)) {
-                       $properties = $this->davPropFind($url);
-
-                       // the returned items are indexed by their key, so sort them here to return the correct items.
-                       // At least Apache does not sort them before returning
-                       uksort($properties, 'strnatcasecmp');
-
-                       // TODO set cache lifetime
-                       $this->getCache()->set($cacheKey, $properties);
-               }
-
-               // if we have only one entry, this is the folder we are currently in, so there are no items -> return an empty array
-               if (count($properties) == 1) {
-                       return array();
-               }
-
-               $propertyIterator = new \ArrayIterator($properties);
-
-               // TODO handle errors
-
-               if ($path !== '' && $path != '/') {
-                       $path = '/' . trim($path, '/') . '/';
-               }
-
-               $c = $numberOfItems > 0 ? $numberOfItems : $propertyIterator->count();
-               $propertyIterator->seek($start);
-
-               $items = array();
-               while ($propertyIterator->valid() && $c > 0) {
-                       $item = $propertyIterator->current();
-                       // the full (web) path to the current item on the server
-                       $filePath = $propertyIterator->key();
-                       $itemName = substr($filePath, strlen($basePath));
-                       $propertyIterator->next();
-
-                       /* TODO check if we still need this, reimplement in case
-                       if ($this->applyFilterMethodsToDirectoryItem($filterMethods, $itemName, $filePath, $basePath, array('item' => $item)) === FALSE) {
-                               continue;
-                       }*/
-
-                       list($key, $entry) = $this->$itemHandlerMethod($item, $filePath, $basePath, $path);
-
-                       if (empty($entry)) {
-                               continue;
-                       }
-
-                       // paths need to include the leading slash, otherwise fetching a directory list might end in a endless loop
-                       $items[$key] = '/' . $entry;
-
-                       --$c;
-               }
-
-               return $items;
+               return $this->getFrontend()->getFileInfo($identifier);
        }
 
        /**
@@ -620,57 +522,13 @@ class WebDavDriver extends AbstractDriver {
         *
         * @param $path
         * @return void
+        * @deprecated this should be moved to WebDavFrontend
         */
        protected function removeCacheForPath($path) {
                $this->getCache()->remove($this->getCacheIdentifierForPath($path));
        }
 
        /**
-        * Callback method that extracts file information from a single entry inside a DAV PROPFIND response. Called by getDirectoryItemList.
-        *
-        * @param array $item The information about the item as fetched from the server
-        * @param string $filePath The full path to the item
-        * @param string $basePath The path of the queried folder
-        * @param string $path The queried path (inside the WebDAV storage)
-        * @return array
-        */
-       protected function getFileList_itemCallback(array $item, $filePath, $basePath, $path) {
-               if ($item['{DAV:}resourcetype']->is('{DAV:}collection')) {
-                       return array('', '');
-               }
-               $fileName = substr($filePath, strlen($basePath));
-
-                       // check if the zero bytes should not be indexed
-               if ($this->configuration['enableZeroByteFilesIndexing'] === FALSE && $item['{DAV:}getcontentlength'] == 0) {
-                       return array('', '');
-               }
-
-               return array($fileName, $path . $fileName);
-       }
-
-       /**
-        * Callback method that extracts folder information from a single entry inside a DAV PROPFIND response. Called by getDirectoryItemList.
-        *
-        * @param array $item The information about the item as fetched from the server
-        * @param string $filePath The full path to the item
-        * @param string $basePath The path of the queried folder
-        * @param string $path The queried path (inside the WebDAV storage)
-        * @return array
-        */
-       protected function getFolderList_itemCallback(array $item, $filePath, $basePath, $path) {
-               if (!$item['{DAV:}resourcetype']->is('{DAV:}collection')) {
-                       return array('', '');
-               }
-               $folderName = trim(substr($filePath, strlen($basePath)), '/');
-
-               if ($folderName == '') {
-                       return array('', '');
-               }
-
-               return array($folderName, $path . trim($folderName, '/') . '/');
-       }
-
-       /**
         * Copies a file to a temporary path and returns that path. You have to take care of removing the temporary file yourself!
         *
         * @param string $fileIdentifier
@@ -947,7 +805,7 @@ class WebDavDriver extends AbstractDriver {
        public function isFolderEmpty($folderIdentifier) {
                $folderUrl = $this->getResourceUrl($folderIdentifier);
 
-               $folderContents = $this->davPropFind($folderUrl);
+               $folderContents = $this->frontend->propFind($folderUrl);
 
                return (count($folderContents) == 1);
        }
@@ -1066,7 +924,14 @@ class WebDavDriver extends AbstractDriver {
         */
        public function getFilesInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = FALSE,
                                         array $filenameFilterCallbacks = array()) {
-               return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, 'getFileList_itemCallback');
+               $files = $this->getFrontend()->listFiles($folderIdentifier);
+
+               $items = array();
+               foreach ($files as $filename) {
+                       $items[$filename] = $folderIdentifier . $filename;
+               }
+
+               return $items;
        }
 
        /**
@@ -1082,6 +947,13 @@ class WebDavDriver extends AbstractDriver {
         */
        public function getFoldersInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = FALSE,
                                           array $folderNameFilterCallbacks = array()) {
-               return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, 'getFolderList_itemCallback');
+               $folders = $this->getFrontend()->listFolders($folderIdentifier);
+
+               $items = array();
+               foreach ($folders as $name) {
+                       $items[$name] = $folderIdentifier . $name . '/';
+               }
+
+               return $items;
        }
 }