diff --git a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
index 911b02221c90..e775852903df 100644
--- a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
@@ -163,7 +163,7 @@ public function checkQuota($path, $length = null) {
$path = rtrim($parentPath, '/') . '/' . $info['name'];
}
$freeSpace = $this->getFreeSpace($path);
- if ($freeSpace !== FileInfo::SPACE_UNKNOWN && $length > $freeSpace) {
+ if ($freeSpace !== FileInfo::SPACE_UNKNOWN && $freeSpace !== FileInfo::SPACE_UNLIMITED && $length > $freeSpace) {
if (isset($chunkHandler)) {
$chunkHandler->cleanup();
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php
index b9aabce2005b..d8b5b88b3e26 100644
--- a/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php
@@ -116,6 +116,11 @@ public function quotaOkayProvider() {
[-2, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
[-2, ['CONTENT-LENGTH' => '512']],
[-2, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+ // \OCP\Files\FileInfo::SPACE-UNLIMITED = -3
+ [-3, []],
+ [-3, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [-3, ['CONTENT-LENGTH' => '512']],
+ [-3, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
];
}
@@ -159,6 +164,13 @@ public function quotaChunkedOkProvider() {
[-2, 128, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
[-2, 128, ['CONTENT-LENGTH' => '512']],
[-2, 128, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+ // \OCP\Files\FileInfo::SPACE-UNLIMITED = -3
+ [-3, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [-3, 0, ['CONTENT-LENGTH' => '512']],
+ [-3, 0, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
+ [-3, 128, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
+ [-3, 128, ['CONTENT-LENGTH' => '512']],
+ [-3, 128, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']],
];
}
diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php
index 4fe50aff88e7..bed3181cdcdc 100644
--- a/lib/private/Files/Storage/DAV.php
+++ b/lib/private/Files/Storage/DAV.php
@@ -46,10 +46,11 @@
use OCP\Files\StorageInvalidException;
use OCP\Files\StorageNotAvailableException;
use OCP\Util;
-use Sabre\DAV\Client;
use Sabre\DAV\Xml\Property\ResourceType;
use Sabre\HTTP\ClientException;
use Sabre\HTTP\ClientHttpException;
+use Sabre\DAV\Exception\InsufficientStorage;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
/**
* Class DAV
@@ -82,6 +83,9 @@ class DAV extends Common {
/** @var \OCP\Http\Client\IClientService */
private $httpClientService;
+ /** @var \OCP\Http\Client\IWebDavClientService */
+ private $webDavClientService;
+
/**
* @param array $params
* @throws \Exception
@@ -89,6 +93,7 @@ class DAV extends Common {
public function __construct($params) {
$this->statCache = new ArrayCache();
$this->httpClientService = \OC::$server->getHTTPClientService();
+ $this->webDavClientService = \OC::$server->getWebDavClientService();
if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
$host = $params['host'];
//remove leading http[s], will be generated in createBaseUri()
@@ -109,17 +114,6 @@ public function __construct($params) {
} else {
$this->secure = false;
}
- if ($this->secure === true) {
- // inject mock for testing
- $certManager = \OC::$server->getCertificateManager();
- if (is_null($certManager)) { //no user
- $certManager = \OC::$server->getCertificateManager(null);
- }
- $certPath = $certManager->getAbsoluteBundlePath();
- if (file_exists($certPath)) {
- $this->certPath = $certPath;
- }
- }
$this->root = isset($params['root']) ? $params['root'] : '/';
if (!$this->root || $this->root[0] != '/') {
$this->root = '/' . $this->root;
@@ -128,7 +122,7 @@ public function __construct($params) {
$this->root .= '/';
}
} else {
- throw new \Exception('Invalid webdav storage configuration');
+ throw new \InvalidArgumentException('Invalid webdav storage configuration');
}
}
@@ -141,22 +135,13 @@ protected function init() {
$settings = [
'baseUri' => $this->createBaseUri(),
'userName' => $this->user,
- 'password' => $this->password,
+ 'password' => $this->password
];
if (isset($this->authType)) {
$settings['authType'] = $this->authType;
}
- $proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
- if($proxy !== '') {
- $settings['proxy'] = $proxy;
- }
-
- $this->client = new Client($settings);
- $this->client->setThrowExceptions(true);
- if ($this->secure === true && $this->certPath) {
- $this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
- }
+ $this->client = $this->webDavClientService->newClient($settings);
}
/**
@@ -234,6 +219,13 @@ public function opendir($path) {
$content[] = $file;
}
return IteratorDirectory::wrap($content);
+ } catch (ClientHttpException $e) {
+ if ($e->getHttpStatus() === Http::STATUS_NOT_FOUND) {
+ $this->statCache->clear($path . '/');
+ $this->statCache->set($path, false);
+ return false;
+ }
+ $this->convertException($e, $path);
} catch (\Exception $e) {
$this->convertException($e, $path);
}
@@ -355,7 +347,7 @@ public function fopen($path, $mode) {
&& $e->getResponse()->getStatusCode() === Http::STATUS_NOT_FOUND) {
return false;
} else {
- throw $e;
+ $this->convertException($e);
}
}
@@ -364,6 +356,7 @@ public function fopen($path, $mode) {
throw new \OCP\Lock\LockedException($path);
} else {
Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), Util::ERROR);
+ // FIXME: why not returning false here ?!
}
}
@@ -442,7 +435,7 @@ public function free_space($path) {
public function touch($path, $mtime = null) {
$this->init();
if (is_null($mtime)) {
- $mtime = time();
+ $mtime = \OC::$server->getTimeFactory()->getTime();
}
$path = $this->cleanPath($path);
@@ -454,6 +447,7 @@ public function touch($path, $mtime = null) {
// non-owncloud clients might not have accepted the property, need to recheck it
$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
if ($response === false) {
+ // file disappeared since ?
return false;
}
if (isset($response['{DAV:}getlastmodified'])) {
@@ -503,14 +497,17 @@ protected function uploadFile($path, $target) {
$this->statCache->remove($target);
$source = fopen($path, 'r');
- $this->httpClientService
- ->newClient()
- ->put($this->createBaseUri() . $this->encodePath($target), [
- 'body' => $source,
- 'auth' => [$this->user, $this->password]
- ]);
-
$this->removeCachedFile($target);
+ try {
+ $this->httpClientService
+ ->newClient()
+ ->put($this->createBaseUri() . $this->encodePath($target), [
+ 'body' => $source,
+ 'auth' => [$this->user, $this->password]
+ ]);
+ } catch (\Exception $e) {
+ $this->convertException($e);
+ }
}
/** {@inheritdoc} */
@@ -661,6 +658,9 @@ private function simpleResponse($method, $path, $body, $expected) {
$this->statCache->set($path, false);
return false;
}
+ if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED && $method === 'MKCOL') {
+ return false;
+ }
$this->convertException($e, $path);
} catch (\Exception $e) {
@@ -775,10 +775,7 @@ public function hasUpdated($path, $time) {
}
if (isset($response['{DAV:}getetag'])) {
$cachedData = $this->getCache()->get($path);
- $etag = null;
- if (isset($response['{DAV:}getetag'])) {
- $etag = trim($response['{DAV:}getetag'], '"');
- }
+ $etag = trim($response['{DAV:}getetag'], '"');
if (!empty($etag) && $cachedData['etag'] !== $etag) {
return true;
} else if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
@@ -828,30 +825,48 @@ private function convertException(Exception $e, $path = '') {
\OC::$server->getLogger()->logException($e);
Util::writeLog('files_external', $e->getMessage(), Util::ERROR);
if ($e instanceof ClientHttpException) {
- if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
- throw new \OCP\Lock\LockedException($path);
+ $this->throwByStatusCode($e->getHttpStatus(), $e, $path);
+ } else if ($e instanceof \GuzzleHttp\Exception\RequestException) {
+ if ($e->getResponse() instanceof ResponseInterface) {
+ $this->throwByStatusCode($e->getResponse()->getStatusCode(), $e);
}
- if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
- // either password was changed or was invalid all along
- throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
- } else if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
- // ignore exception for MethodNotAllowed, false will be returned
- return;
- }
- throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
- } else if ($e instanceof ClientException) {
// connection timeout or refused, server could be temporarily down
throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
} else if ($e instanceof \InvalidArgumentException) {
// parse error because the server returned HTML instead of XML,
// possibly temporarily down
throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
- } else if (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
+ } else if (($e instanceof StorageNotAvailableException)
+ || ($e instanceof StorageInvalidException)
+ || ($e instanceof \Sabre\DAV\Exception
+ )) {
// rethrow
throw $e;
}
// TODO: only log for now, but in the future need to wrap/rethrow exception
}
+
+ /**
+ * Throw exception by status code
+ *
+ * @param int $statusCode status code
+ * @param string $path optional path for some exceptions
+ * @throws \Exception Sabre or ownCloud exceptions
+ */
+ private function throwByStatusCode($statusCode, $e, $path = '') {
+ switch ($statusCode) {
+ case Http::STATUS_LOCKED:
+ throw new \OCP\Lock\LockedException($path);
+ case Http::STATUS_UNAUTHORIZED:
+ // either password was changed or was invalid all along
+ throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
+ case Http::STATUS_INSUFFICIENT_STORAGE:
+ throw new InsufficientStorage();
+ case Http::STATUS_FORBIDDEN:
+ throw new Forbidden('Forbidden');
+ }
+ throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
+ }
}
diff --git a/lib/private/Http/Client/WebDavClientService.php b/lib/private/Http/Client/WebDavClientService.php
new file mode 100644
index 000000000000..84ad5aacf8f9
--- /dev/null
+++ b/lib/private/Http/Client/WebDavClientService.php
@@ -0,0 +1,97 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OC\Http\Client;
+
+use OCP\Http\Client\IWebDavClientService;
+use OCP\IConfig;
+use OCP\ICertificateManager;
+use Sabre\DAV\Client;
+
+/**
+ * Class WebDavClientService
+ *
+ * @package OC\Http
+ */
+class WebDavClientService implements IWebDavClientService {
+ /** @var IConfig */
+ private $config;
+ /** @var ICertificateManager */
+ private $certificateManager;
+
+ /**
+ * @param IConfig $config
+ * @param ICertificateManager $certificateManager
+ */
+ public function __construct(IConfig $config,
+ ICertificateManager $certificateManager) {
+ $this->config = $config;
+ $this->certificateManager = $certificateManager;
+ }
+
+ /**
+ * Instantiate new Sabre client
+ *
+ * Settings are provided through the 'settings' argument. The following
+ * settings are supported:
+ *
+ * * baseUri
+ * * userName (optional)
+ * * password (optional)
+ * * proxy (optional)
+ * * authType (optional)
+ * * encoding (optional)
+ *
+ * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST
+ * and self::AUTH_NTLM. If you know which authentication method will be
+ * used, it's recommended to set it, as it will save a great deal of
+ * requests to 'discover' this information.
+ *
+ * Encoding is a bitmap with one of the ENCODING constants.
+ *
+ * @param array $settings Sabre client settings
+ * @return Client
+ */
+ public function newClient($settings) {
+ if (!isset($settings['proxy'])) {
+ $proxy = $this->config->getSystemValue('proxy', '');
+ if($proxy !== '') {
+ $settings['proxy'] = $proxy;
+ }
+ }
+
+ $certPath = null;
+ if (strpos($settings['baseUri'], 'https') === 0) {
+ $certPath = $this->certificateManager->getAbsoluteBundlePath();
+ if (!file_exists($certPath)) {
+ $certPath = null;
+ }
+ }
+
+ $client = new Client($settings);
+ $client->setThrowExceptions(true);
+
+ if ($certPath !== null) {
+ $client->addCurlSetting(CURLOPT_CAINFO, $certPath);
+ }
+ return $client;
+ }
+}
diff --git a/lib/private/Server.php b/lib/private/Server.php
index a20801cf889b..c84edfe31f86 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -100,6 +100,7 @@
use OC\Files\External\Service\UserGlobalStoragesService;
use OC\Files\External\Service\GlobalStoragesService;
use OC\Files\External\Service\DBConfigService;
+use OC\Http\Client\WebDavClientService;
use Symfony\Component\EventDispatcher\GenericEvent;
/**
@@ -275,6 +276,10 @@ public function __construct($webRoot, \OC\Config $config) {
return new \OC\Authentication\Token\DefaultTokenProvider($mapper, $crypto, $config, $logger, $timeFactory);
});
$this->registerAlias('OC\Authentication\Token\IProvider', 'OC\Authentication\Token\DefaultTokenProvider');
+ $this->registerService('TimeFactory', function() {
+ return new TimeFactory();
+ });
+ $this->registerAlias('OCP\AppFramework\Utility\ITimeFactory', 'TimeFactory');
$this->registerService('UserSession', function (Server $c) {
$manager = $c->getUserManager();
$session = new \OC\Session\Memory('');
@@ -496,6 +501,14 @@ public function __construct($webRoot, \OC\Config $config) {
new \OC\Security\CertificateManager($uid, new View(), $c->getConfig())
);
});
+ $this->registerService('WebDavClientService', function (Server $c) {
+ $user = \OC_User::getUser();
+ $uid = $user ? $user : null;
+ return new WebDavClientService(
+ $c->getConfig(),
+ new \OC\Security\CertificateManager($uid, new View(), $c->getConfig())
+ );
+ });
$this->registerService('EventLogger', function (Server $c) {
$eventLogger = new EventLogger();
if ($c->getSystemConfig()->getValue('debug', false)) {
@@ -1269,6 +1282,15 @@ public function getHTTPClientService() {
return $this->query('HttpClientService');
}
+ /**
+ * Returns an instance of the Webdav client service
+ *
+ * @return \OCP\Http\Client\IWebDavClientService
+ */
+ public function getWebDavClientService() {
+ return $this->query('WebDavClientService');
+ }
+
/**
* Create a new event source
*
@@ -1522,4 +1544,11 @@ public function getShareManager() {
public function getThemeService() {
return $this->query('\OCP\Theme\IThemeService');
}
+
+ /**
+ * @return ITimeFactory
+ */
+ public function getTimeFactory() {
+ return $this->query('\OCP\AppFramework\Utility\ITimeFactory');
+ }
}
diff --git a/lib/public/Http/Client/IWebDavClientService.php b/lib/public/Http/Client/IWebDavClientService.php
new file mode 100644
index 000000000000..950e55f52575
--- /dev/null
+++ b/lib/public/Http/Client/IWebDavClientService.php
@@ -0,0 +1,56 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCP\Http\Client;
+
+use Sabre\HTTP\Client;
+
+/**
+ * Interface IWebDavClientService
+ *
+ * @package OCP\Http
+ * @since 10.0.4
+ */
+interface IWebDavClientService {
+ /**
+ * Settings are provided through the 'settings' argument. The following
+ * settings are supported:
+ *
+ * * baseUri
+ * * userName (optional)
+ * * password (optional)
+ * * proxy (optional)
+ * * authType (optional)
+ * * encoding (optional)
+ *
+ * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST
+ * and self::AUTH_NTLM. If you know which authentication method will be
+ * used, it's recommended to set it, as it will save a great deal of
+ * requests to 'discover' this information.
+ *
+ * Encoding is a bitmap with one of the ENCODING constants.
+ *
+ * @param $settings Sabre client settings
+ * @return Client
+ * @since 10.0.4
+ */
+ public function newClient($settings);
+}
diff --git a/tests/integration/features/bootstrap/Provisioning.php b/tests/integration/features/bootstrap/Provisioning.php
index d9d6d8a91c5e..392f7cfebe30 100644
--- a/tests/integration/features/bootstrap/Provisioning.php
+++ b/tests/integration/features/bootstrap/Provisioning.php
@@ -640,6 +640,7 @@ public function userHasAQuotaOf($user, $quota)
// method used from BasicStructure trait
$this->sendingToWith("PUT", "/cloud/users/" . $user, $body);
+ PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
diff --git a/tests/integration/features/external-storage.feature b/tests/integration/features/external-storage.feature
index ad378a21da0d..631804dabc26 100644
--- a/tests/integration/features/external-storage.feature
+++ b/tests/integration/features/external-storage.feature
@@ -62,6 +62,7 @@ Feature: external-storage
@local_storage
Scenario: Upload a file to external storage while quota is set on home storage
Given user "user0" exists
+ And as an "admin"
And user "user0" has a quota of "1 B"
And as an "user0"
When user "user0" uploads file "data/textfile.txt" to "/local_storage/testquota.txt" with all mechanisms
diff --git a/tests/integration/features/sharing-v1.feature b/tests/integration/features/sharing-v1.feature
index 9b571315c4ed..d8eb3a600555 100644
--- a/tests/integration/features/sharing-v1.feature
+++ b/tests/integration/features/sharing-v1.feature
@@ -598,6 +598,7 @@ Feature: sharing
Given using old dav path
And user "user0" exists
And user "user1" exists
+ And as an "admin"
And user "user1" has a quota of "0"
And user "user0" moved file "/welcome.txt" to "/myfile.txt"
And file "myfile.txt" of user "user0" is shared with user "user1"
diff --git a/tests/integration/federation_features/federated.feature b/tests/integration/federation_features/federated.feature
index ea0fba3dcee1..ef9e2044fd62 100644
--- a/tests/integration/federation_features/federated.feature
+++ b/tests/integration/federation_features/federated.feature
@@ -222,16 +222,32 @@ Feature: federated
And as an "user0"
And etag of element "/PARENT" of user "user0" has changed
- @local_storage
Scenario: Upload file to received federated share while quota is set on home storage
Given using server "REMOTE"
+ And as an "admin"
And user "user1" exists
+ And user "user1" has a quota of "20 B"
And using server "LOCAL"
And user "user0" exists
And user "user0" from server "LOCAL" shares "/PARENT" with user "user1" from server "REMOTE"
And user "user1" from server "REMOTE" accepts last pending share
And using server "REMOTE"
- When user "user1" uploads file "data/textfile.txt" to "/PARENT/testquota.txt" with all mechanisms
+ When user "user1" uploads file "data/textfile.txt" to "/PARENT (2)/testquota.txt" with all mechanisms
Then the HTTP status code of all upload responses should be "201"
- And as "user0" the file "/PARENT/textquota.txt" exists
+ And using server "LOCAL"
+ And as "user0" the file "/PARENT (2)/textquota.txt" exists
+
+ Scenario: Upload file to received federated share while quota is set on remote storage
+ Given using server "REMOTE"
+ And as an "admin"
+ And user "user1" exists
+ And using server "LOCAL"
+ And as an "admin"
+ And user "user0" exists
+ And user "user0" has a quota of "20 B"
+ And user "user0" from server "LOCAL" shares "/PARENT" with user "user1" from server "REMOTE"
+ And user "user1" from server "REMOTE" accepts last pending share
+ And using server "REMOTE"
+ When user "user1" uploads file "data/textfile.txt" to "/PARENT (2)/testquota.txt" with all mechanisms
+ Then the HTTP status code of all upload responses should be "507"
diff --git a/tests/lib/Files/Storage/DavTest.php b/tests/lib/Files/Storage/DavTest.php
new file mode 100644
index 000000000000..b430f5ba7d88
--- /dev/null
+++ b/tests/lib/Files/Storage/DavTest.php
@@ -0,0 +1,1319 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace Test\Files\Storage;
+
+use Test\TestCase;
+use OCP\Http\Client\IClientService;
+use OCP\Http\Client\IClient;
+use Sabre\DAV\Client;
+use OCP\Http\Client\IWebDavClientService;
+use Sabre\HTTP\ClientHttpException;
+use OCP\Lock\LockedException;
+use OCP\AppFramework\Http;
+use OCP\Files\StorageInvalidException;
+use GuzzleHttp\Exception\ServerException;
+use GuzzleHttp\Exception\ClientException;
+use Sabre\DAV\Exception\InsufficientStorage;
+use Sabre\DAV\Exception\Forbidden;
+use OCP\Files\StorageNotAvailableException;
+use OC\Files\Storage\DAV;
+use OCP\Files\FileInfo;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OC\Files\Cache\Cache;
+
+/**
+ * Class DavTest
+ *
+ * @group DB
+ *
+ * @package Test\Files\Storage
+ */
+class DavTest extends TestCase {
+
+ /**
+ * @var DAV | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $instance;
+
+ /**
+ * @var IClientService | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $httpClientService;
+
+ /**
+ * @var IWebDavClientService | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $webDavClientService;
+
+ /**
+ * @var Client | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $davClient;
+
+ /**
+ * @var IClient | \PHPUnit_Framework_MockObject_MockObject
+ **/
+ private $httpClient;
+
+ /**
+ * @var ITimeFactory | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $timeFactory;
+
+ /**
+ * @var Cache | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $cache;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->httpClientService = $this->createMock(IClientService::class);
+ $this->overwriteService('HttpClientService', $this->httpClientService);
+
+ $this->webDavClientService = $this->createMock(IWebDavClientService::class);
+ $this->overwriteService('WebDavClientService', $this->webDavClientService);
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->overwriteService('TimeFactory', $this->timeFactory);
+
+ $this->httpClient = $this->createMock(IClient::class);
+ $this->httpClientService->method('newClient')->willReturn($this->httpClient);
+
+ $this->davClient = $this->createMock(Client::class);
+ $this->webDavClientService->method('newClient')->willReturn($this->davClient);
+
+ $this->instance = $this->getMockBuilder(\OC\Files\Storage\DAV::class)
+ ->setConstructorArgs([[
+ 'user' => 'davuser',
+ 'password' => 'davpassword',
+ 'host' => 'davhost',
+ 'root' => 'davroot',
+ 'secure' => true
+ ]])
+ ->setMethods(['getCache'])
+ ->getMock();
+
+ $this->cache = $this->createMock(Cache::class);
+ $this->instance->method('getCache')->willReturn($this->cache);
+ }
+
+ protected function tearDown() {
+ $this->restoreService('HttpClientService');
+ $this->restoreService('WebDavClientService');
+ $this->restoreService('TimeFactory');
+ parent::tearDown();
+ }
+
+ public function testId() {
+ $this->assertEquals('webdav::davuser@davhost//davroot/', $this->instance->getId());
+ }
+
+ public function instantiateWebDavClientDataProvider() {
+ return [
+ [false, 'http'],
+ [true, 'https'],
+ ];
+ }
+
+ /**
+ * @dataProvider instantiateWebDavClientDataProvider
+ */
+ public function testInstantiateWebDavClient($secure, $protocol) {
+ $this->restoreService('WebDavClientService');
+ $this->webDavClientService = $this->createMock(IWebDavClientService::class);
+ $this->overwriteService('WebDavClientService', $this->webDavClientService);
+ $this->webDavClientService->expects($this->once())
+ ->method('newClient')
+ ->with([
+ 'baseUri' => $protocol . '://davhost/davroot/',
+ 'userName' => 'davuser',
+ 'password' => 'davpassword',
+ 'authType' => 'basic',
+ ])
+ ->willReturn($this->davClient);
+
+ $this->instance = new \OC\Files\Storage\DAV([
+ 'user' => 'davuser',
+ 'password' => 'davpassword',
+ 'host' => 'davhost',
+ 'root' => 'davroot',
+ 'authType' => 'basic',
+ 'secure' => $secure
+ ]);
+
+ // trigger lazy init
+ $this->instance->mkdir('/test');
+ }
+
+ public function invalidConfigDataProvider() {
+ return [
+ [[
+ 'user' => 'davuser',
+ 'password' => 'davpassword',
+ 'root' => 'davroot',
+ ], [
+ 'user' => 'davuser',
+ 'host' => 'davhost',
+ 'root' => 'davroot',
+ ], [
+ 'password' => 'davpassword',
+ 'host' => 'davhost',
+ 'root' => 'davroot',
+ ]],
+ ];
+ }
+
+ /**
+ * @dataProvider invalidConfigDataProvider
+ * @expectedException \InvalidArgumentException
+ */
+ public function testInstantiateWebDavClientInvalidConfig($params) {
+ new \OC\Files\Storage\DAV($params);
+ }
+
+ private function createClientHttpException($statusCode) {
+ $response = $this->createMock(\Sabre\HTTP\ResponseInterface::class);
+ $response->method('getStatusText')->willReturn('');
+ $response->method('getStatus')->willReturn($statusCode);
+ return new ClientHttpException($response);
+ }
+
+ private function createGuzzleClientException($statusCode) {
+ $request = $this->createMock(\GuzzleHttp\Message\RequestInterface::class);
+ $response = $this->createMock(\GuzzleHttp\Message\ResponseInterface::class);
+ $response->method('getStatusCode')->willReturn($statusCode);
+ return new ClientException('ClientException', $request, $response);
+ }
+
+ private function createGuzzleServerException($statusCode) {
+ $request = $this->createMock(\GuzzleHttp\Message\RequestInterface::class);
+ $response = $this->createMock(\GuzzleHttp\Message\ResponseInterface::class);
+ $response->method('getStatusCode')->willReturn($statusCode);
+ return new ServerException('ServerException', $request, $response);
+ }
+
+ public function convertExceptionDataProvider() {
+ $statusCases = [
+ [Http::STATUS_UNAUTHORIZED, StorageInvalidException::class],
+ [Http::STATUS_LOCKED, LockedException::class],
+ [Http::STATUS_INSUFFICIENT_STORAGE, InsufficientStorage::class],
+ [Http::STATUS_FORBIDDEN, Forbidden::class],
+ [Http::STATUS_INTERNAL_SERVER_ERROR, StorageNotAvailableException::class],
+ ];
+
+ $testCases = [
+ [new \Sabre\DAV\Exception\Forbidden('Forbidden'), \Sabre\DAV\Exception\Forbidden::class],
+ [new \InvalidArgumentException(), StorageNotAvailableException::class],
+ [new StorageNotAvailableException(), StorageNotAvailableException::class],
+ [new StorageInvalidException(), StorageInvalidException::class],
+ ];
+
+ // map to ClientHttpException
+ foreach ($statusCases as $statusCase) {
+ $testCases[] = [$this->createClientHttpException($statusCase[0]), $statusCase[1]];
+ $testCases[] = [$this->createGuzzleClientException($statusCase[0]), $statusCase[1]];
+ $testCases[] = [$this->createGuzzleServerException($statusCase[0]), $statusCase[1]];
+ }
+
+ // one case where Guzzle response is null, for whatever reason
+ $testCases[] = [
+ new ServerException(
+ 'ServerException with no response',
+ $this->createMock(\GuzzleHttp\Message\RequestInterface::class),
+ null
+ ),
+ StorageNotAvailableException::class
+ ];
+
+ return $testCases;
+ }
+
+ /**
+ * @dataProvider convertExceptionDataProvider
+ */
+ public function testConvertException($inputException, $expectedExceptionClass) {
+ $this->davClient->method('propfind')->will($this->throwException($inputException));
+
+ $thrownException = null;
+ try {
+ $this->instance->opendir('/test');
+ } catch (\Exception $e) {
+ $thrownException = $e;
+ }
+
+ $this->assertNotNull($thrownException);
+ $this->assertInstanceOf($expectedExceptionClass, $thrownException);
+ }
+
+ public function testMkdir() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('MKCOL', 'new%25dir', null)
+ ->willReturn(['statusCode' => Http::STATUS_CREATED]);
+
+ $this->assertTrue($this->instance->mkdir('/new%dir'));
+ }
+
+ public function testMkdirAlreadyExists() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('MKCOL', 'new%25dir', null)
+ ->willThrowException($this->createClientHttpException(Http::STATUS_METHOD_NOT_ALLOWED));
+
+ $this->assertFalse($this->instance->mkdir('/new%dir'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testMkdirException() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('MKCOL', 'new%25dir', null)
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->mkdir('/new%dir');
+ }
+
+ public function testRmdir() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('DELETE', 'old%25dir', null)
+ ->willReturn(['statusCode' => Http::STATUS_NO_CONTENT]);
+
+ $this->assertTrue($this->instance->rmdir('/old%dir'));
+ }
+
+ public function testRmdirUnexist() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('DELETE', 'old%25dir', null)
+ ->willReturn(['statusCode' => Http::STATUS_NOT_FOUND]);
+
+ $this->assertFalse($this->instance->rmdir('/old%dir'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testRmdirException() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('DELETE', 'old%25dir', null)
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->rmdir('/old%dir');
+ }
+
+ public function testOpenDir() {
+ $responseBody = [
+ // root entry
+ 'some%25dir' => [],
+ 'some%25dir/first%25folder' => [],
+ 'some%25dir/second' => [],
+ ];
+
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', [], 1)
+ ->willReturn($responseBody);
+
+ // do operation twice, second time will not trigger propfind
+ // due to stat cache
+ $i = 0;
+ while ($i < 1) {
+ $i++;
+
+ $dir = $this->instance->opendir('/some%dir');
+ $entries = [];
+ while ($entry = readdir($dir)) {
+ $entries[] = $entry;
+ }
+
+ $this->assertCount(2, $entries);
+ $this->assertEquals('first%folder', $entries[0]);
+ $this->assertEquals('second', $entries[1]);
+ }
+ }
+
+ public function testOpenDirNotFound() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', [], 1)
+ ->willThrowException($this->createClientHttpException(Http::STATUS_NOT_FOUND));
+
+ $this->assertFalse($this->instance->opendir('/some%dir'));
+ }
+
+ public function testOpenDirNotFound2() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', [], 1)
+ ->willReturn(false);
+
+ $this->assertFalse($this->instance->opendir('/some%dir'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testOpenDirException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', [], 1)
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->opendir('/some%dir');
+ }
+
+ private function getResourceTypeResponse($isDir) {
+ $resourceTypeObj = $this->getMockBuilder('\stdclass')
+ ->setMethods(['getValue'])
+ ->getMock();
+ if ($isDir) {
+ $resourceTypeObj->method('getValue')
+ ->willReturn(['{DAV:}collection']);
+ } else {
+ $resourceTypeObj->method('getValue')
+ ->willReturn([]);
+ }
+ return $resourceTypeObj;
+ }
+
+ public function testFileTypeDir() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25type', $this->contains('{DAV:}resourcetype'))
+ ->willReturn([
+ '{DAV:}resourcetype' => $this->getResourceTypeResponse(true)
+ ]);
+
+ $this->assertEquals('dir', $this->instance->filetype('/some%dir/file%type'));
+ }
+
+ public function testFileTypeFile() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25type', $this->contains('{DAV:}resourcetype'))
+ ->willReturn([]);
+
+ $this->assertEquals('file', $this->instance->filetype('/some%dir/file%type'));
+ }
+
+ public function testFileTypeNotFound() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25type', $this->contains('{DAV:}resourcetype'))
+ ->willThrowException($this->createClientHttpException(Http::STATUS_NOT_FOUND));
+
+ $this->assertFalse($this->instance->filetype('/some%dir/file%type'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testFileTypeException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25type', $this->contains('{DAV:}resourcetype'))
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->filetype('/some%dir/file%type');
+ }
+
+ public function testFileExists() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willReturn(true);
+
+ $this->assertTrue($this->instance->file_exists('/some%dir/file%.txt'));
+
+ // stat cache: calling again does not redo a propfind
+ $this->assertTrue($this->instance->file_exists('/some%dir/file%.txt'));
+ }
+
+ public function testFileExistsDoesNot() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willReturn(false);
+
+ $this->assertFalse($this->instance->file_exists('/some%dir/file%.txt'));
+
+ // stat cache: calling again does not redo a propfind
+ $this->assertFalse($this->instance->file_exists('/some%dir/file%.txt'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testFileExistsException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->file_exists('/some%dir/file%.txt');
+ }
+
+ public function testUnlink() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('DELETE', 'old%25file.txt', null)
+ ->willReturn(['statusCode' => Http::STATUS_NO_CONTENT]);
+
+ $this->assertTrue($this->instance->unlink('/old%file.txt'));
+ }
+
+ public function testUnlinkUnexist() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('DELETE', 'old%25file.txt', null)
+ ->willReturn(['statusCode' => Http::STATUS_NOT_FOUND]);
+
+ $this->assertFalse($this->instance->unlink('/old%file.txt'));
+ }
+
+ public function testUnlinkUnexist2() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('DELETE', 'old%25file.txt', null)
+ ->willThrowException($this->createClientHttpException(Http::STATUS_NOT_FOUND));
+
+ $this->assertFalse($this->instance->unlink('/old%file.txt'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testUnlinkException() {
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with('DELETE', 'old%25file.txt', null)
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->unlink('/old%file.txt');
+ }
+
+ public function testFopenRead() {
+ $response = $this->createMock(\GuzzleHttp\Message\ResponseInterface::class);
+ $response->method('getStatusCode')->willReturn(Http::STATUS_OK);
+ $response->method('getBody')->willReturn(fopen('data://text/plain,response body', 'r'));
+
+ $this->httpClient->expects($this->once())
+ ->method('get')
+ ->with(
+ 'https://davhost/davroot/some%25dir/file%25.txt', [
+ 'auth' => ['davuser', 'davpassword'],
+ 'stream' => true
+ ]
+ )
+ ->willReturn($response);
+
+ $fh = $this->instance->fopen('/some%dir/file%.txt', 'r');
+ $contents = stream_get_contents($fh);
+ fclose($fh);
+
+ $this->assertEquals('response body', $contents);
+ }
+
+ public function testFopenReadNotFound() {
+ $this->httpClient->expects($this->once())
+ ->method('get')
+ ->with(
+ 'https://davhost/davroot/some%25dir/file%25.txt', [
+ 'auth' => ['davuser', 'davpassword'],
+ 'stream' => true
+ ]
+ )
+ ->willThrowException($this->createGuzzleClientException(Http::STATUS_NOT_FOUND));
+
+ $this->assertFalse($this->instance->fopen('/some%dir/file%.txt', 'r'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testFopenReadException() {
+ $this->httpClient->expects($this->once())
+ ->method('get')
+ ->with(
+ 'https://davhost/davroot/some%25dir/file%25.txt', [
+ 'auth' => ['davuser', 'davpassword'],
+ 'stream' => true
+ ]
+ )
+ ->willThrowException($this->createGuzzleClientException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->fopen('/some%dir/file%.txt', 'r');
+ }
+
+ /**
+ * @expectedException \OCP\Lock\LockedException
+ */
+ public function testFopenReadLockedException() {
+ $response = $this->createMock(\GuzzleHttp\Message\ResponseInterface::class);
+ $response->method('getStatusCode')->willReturn(Http::STATUS_LOCKED);
+ $response->method('getBody')->willReturn(fopen('data://text/plain,response body', 'r'));
+
+ $this->httpClient->expects($this->once())
+ ->method('get')
+ ->with(
+ 'https://davhost/davroot/some%25dir/file%25.txt', [
+ 'auth' => ['davuser', 'davpassword'],
+ 'stream' => true
+ ]
+ )
+ ->willReturn($response);
+
+ $this->instance->fopen('/some%dir/file%.txt', 'r');
+ }
+
+ public function testFopenWriteNewFile() {
+ // file_exists
+ $this->davClient->expects($this->at(0))
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willReturn(false);
+
+ // isCreatable on parent / getPermissions
+ $this->davClient->expects($this->at(1))
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{http://owncloud.org/ns}permissions'))
+ ->willReturn([
+ '{http://owncloud.org/ns}permissions' => 'RDWCK'
+ ]);
+
+ $uploadUrl = null;
+ $uploadOptions = null;
+ $this->httpClient->expects($this->once())
+ ->method('put')
+ ->will($this->returnCallback(function($url, $options) use (&$uploadUrl, &$uploadOptions) {
+ $uploadUrl = $url;
+ $uploadOptions = $options;
+ }));
+
+ $fh = $this->instance->fopen('/some%dir/file%.txt', 'w');
+ fwrite($fh, 'whatever');
+ fclose($fh);
+
+ $this->assertEquals('https://davhost/davroot/some%25dir/file%25.txt', $uploadUrl);
+ $this->assertEquals(['davuser', 'davpassword'], $uploadOptions['auth']);
+ $this->assertEquals('whatever', stream_get_contents($uploadOptions['body']));
+ }
+
+ public function testFopenWriteNewFileNoPermission() {
+ // file_exists
+ $this->davClient->expects($this->at(0))
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willReturn(false);
+
+ // isCreatable on parent / getPermissions
+ $this->davClient->expects($this->at(1))
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{http://owncloud.org/ns}permissions'))
+ ->willReturn([
+ '{http://owncloud.org/ns}permissions' => 'R'
+ ]);
+
+ $this->httpClient->expects($this->never())
+ ->method('put');
+
+ $this->assertFalse($this->instance->fopen('/some%dir/file%.txt', 'w'));
+ }
+
+ public function testFopenWriteExistingFile() {
+ // file_exists, and cached response for isUpdatable / getPermissions
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willReturn([
+ '{http://owncloud.org/ns}permissions' => 'RDWCK'
+ ]);
+
+ $uploadUrl = null;
+ $uploadOptions = null;
+ $this->httpClient->expects($this->once())
+ ->method('put')
+ ->will($this->returnCallback(function($url, $options) use (&$uploadUrl, &$uploadOptions) {
+ $uploadUrl = $url;
+ $uploadOptions = $options;
+ }));
+
+ $fh = $this->instance->fopen('/some%dir/file%.txt', 'w');
+ fwrite($fh, 'whatever');
+ fclose($fh);
+
+ $this->assertEquals('https://davhost/davroot/some%25dir/file%25.txt', $uploadUrl);
+ $this->assertEquals(['davuser', 'davpassword'], $uploadOptions['auth']);
+ $this->assertEquals('whatever', stream_get_contents($uploadOptions['body']));
+ }
+
+ public function testFopenWriteExistingFileNoPermission() {
+ // file_exists, and cached response for isUpdatable / getPermissions
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willReturn([
+ '{http://owncloud.org/ns}permissions' => 'R'
+ ]);
+
+ $this->httpClient->expects($this->never())
+ ->method('put');
+
+ $this->assertFalse($this->instance->fopen('/some%dir/file%.txt', 'w'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testFopenWriteExceptionEarly() {
+ // file_exists, and cached response for isUpdatable / getPermissions
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->httpClient->expects($this->never())
+ ->method('put');
+
+ $this->instance->fopen('/some%dir/file%.txt', 'w');
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testFopenWriteExceptionLate() {
+ // file_exists, and cached response for isUpdatable / getPermissions
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willReturn([
+ '{http://owncloud.org/ns}permissions' => 'RDWCK'
+ ]);
+
+ $uploadUrl = null;
+ $uploadOptions = null;
+ $this->httpClient->expects($this->once())
+ ->method('put')
+ ->willThrowException($this->createGuzzleClientException(Http::STATUS_FORBIDDEN));
+
+ $fh = $this->instance->fopen('/some%dir/file%.txt', 'w');
+ fwrite($fh, 'whatever');
+ fclose($fh);
+ }
+
+ public function freespaceProvider() {
+ return [
+ [false, FileInfo::SPACE_UNKNOWN],
+ [['{DAV:}quota-available-bytes' => 123], 123],
+ [['{DAV:}quota-available-bytes' => FileInfo::SPACE_UNKNOWN], FileInfo::SPACE_UNKNOWN],
+ [[], FileInfo::SPACE_UNKNOWN],
+ ];
+ }
+
+ /**
+ * @dataProvider freespaceProvider
+ */
+ public function testFreeSpace($propFindResponse, $apiResponse) {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{DAV:}quota-available-bytes'), 0)
+ ->willReturn($propFindResponse);
+
+ $this->assertEquals($apiResponse, $this->instance->free_space('/some%dir'));
+ }
+
+ public function testFreeSpaceException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->assertEquals(FileInfo::SPACE_UNKNOWN, $this->instance->free_space('/some%dir'));
+ }
+
+ public function touchProvider() {
+ return [
+ // server accepted mtime
+ [1508496363, null, '2017-10-20T12:46:03+02:00', true],
+ // server did not accept mtime
+ [1508496363, null, '2017-10-20T12:40:00+02:00', false],
+ // time factory generated mtime
+ [null, 1508496363, '2017-10-20T12:46:03+02:00', true],
+ ];
+ }
+
+ /**
+ * @dataProvider touchProvider
+ */
+ public function testTouchExisting($setMtime, $factoryTime, $readMtime, $expectedResult) {
+ if ($factoryTime !== null) {
+ $this->timeFactory->expects($this->once())
+ ->method('getTime')
+ ->willReturn($factoryTime);
+ } else {
+ $this->timeFactory->expects($this->never())
+ ->method('getTime');
+ }
+
+ // file_exists
+ $this->davClient->expects($this->at(0))
+ ->method('propfind')
+ ->with('some%25dir')
+ ->willReturn([]);
+
+ $this->davClient->expects($this->at(1))
+ ->method('proppatch')
+ ->with('some%25dir', ['{DAV:}lastmodified' => $setMtime || $factoryTime]);
+
+ // propfind after proppatch, to check if applied
+ $this->davClient->expects($this->at(2))
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{DAV:}getlastmodified'), 0)
+ ->willReturn([
+ '{DAV:}getlastmodified' => $readMtime
+ ]);
+
+ $this->assertEquals($expectedResult, $this->instance->touch('/some%dir', $setMtime));
+ }
+
+ public function testTouchNonExisting() {
+ // file_exists
+ $this->davClient->expects($this->at(0))
+ ->method('propfind')
+ ->with('some%25dir/file%25.txt')
+ ->willReturn(false);
+
+ // isCreatable on parent / getPermissions
+ $this->davClient->expects($this->at(1))
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{http://owncloud.org/ns}permissions'))
+ ->willReturn([
+ '{http://owncloud.org/ns}permissions' => 'RDWCK'
+ ]);
+
+ $uploadUrl = null;
+ $uploadOptions = null;
+ $this->httpClient->expects($this->once())
+ ->method('put')
+ ->will($this->returnCallback(function($url, $options) use (&$uploadUrl, &$uploadOptions) {
+ $uploadUrl = $url;
+ $uploadOptions = $options;
+ }));
+
+ $this->assertTrue($this->instance->touch('/some%dir/file%.txt'));
+
+ $this->assertEquals('https://davhost/davroot/some%25dir/file%25.txt', $uploadUrl);
+ $this->assertEquals(['davuser', 'davpassword'], $uploadOptions['auth']);
+ $this->assertEquals('', stream_get_contents($uploadOptions['body']));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testTouchException() {
+ // file_exists
+ $this->davClient->expects($this->at(0))
+ ->method('propfind')
+ ->with('some%25dir')
+ ->willReturn([]);
+
+ $this->davClient->expects($this->at(1))
+ ->method('proppatch')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->touch('/some%dir', 1508496363);
+ }
+
+ public function testTouchNotFound() {
+ // file_exists
+ $this->davClient->expects($this->at(0))
+ ->method('propfind')
+ ->with('some%25dir')
+ ->willReturn([]);
+
+ $this->davClient->expects($this->at(1))
+ ->method('proppatch');
+
+ // maybe the file disappeared in-between ?
+ $this->davClient->expects($this->at(2))
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{DAV:}getlastmodified'), 0)
+ ->willReturn(false);
+
+ $this->assertFalse($this->instance->touch('/some%dir', 1508496363));
+ }
+
+ public function testTouchNoServerSupport() {
+ // file_exists
+ $this->davClient->expects($this->at(0))
+ ->method('propfind')
+ ->with('some%25dir')
+ ->willReturn([]);
+
+ $this->davClient->expects($this->at(1))
+ ->method('proppatch')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_NOT_IMPLEMENTED));
+
+ $this->assertFalse($this->instance->touch('/some%dir', 1508496363));
+ }
+
+ public function renameDataProvider() {
+ return [
+ ['MOVE', 'rename', false, ''],
+ ['MOVE', 'rename', true, '/'],
+ ['COPY', 'copy', false, ''],
+ ['COPY', 'copy', true, '/'],
+ ];
+ }
+
+ /**
+ * @dataProvider renameDataProvider
+ */
+ public function testRename($httpMethod, $storageMethod, $isDir, $extra) {
+ $mock = $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('new%25path/new%25file.txt', $this->contains('{DAV:}resourcetype'));
+ $mock->willReturn([
+ '{DAV:}resourcetype' => $this->getResourceTypeResponse($isDir)
+ ]);
+
+ $this->davClient->expects($this->once())
+ ->method('request')
+ ->with($httpMethod, 'old%25path/old%25file.txt', null, ['Destination' => 'https://davhost/davroot/new%25path/new%25file.txt' . $extra])
+ ->willReturn(['statusCode' => Http::STATUS_OK]);
+
+ $this->assertTrue($this->instance->$storageMethod('/old%path/old%file.txt', '/new%path/new%file.txt'));
+ }
+
+ public function statDataProvider() {
+ return [
+ [[
+ '{DAV:}getlastmodified' => '2017-10-20T12:46:03+02:00',
+ '{DAV:}getcontentlength' => 1024,
+ ], [
+ 'mtime' => 1508496363,
+ 'size' => 1024
+ ]],
+ [[
+ '{DAV:}getlastmodified' => '2017-10-20T12:46:03+02:00',
+ ], [
+ 'mtime' => 1508496363,
+ 'size' => 0
+ ]],
+ ];
+ }
+
+ /**
+ * @dataProvider statDataProvider
+ */
+ public function testStat($davResponse, $apiResponse) {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25type', $this->logicalAnd($this->contains('{DAV:}getlastmodified'), $this->contains('{DAV:}getcontentlength')))
+ ->willReturn($davResponse);
+
+ $this->assertEquals($apiResponse, $this->instance->stat('/some%dir/file%type'));
+ }
+
+ public function testStatRoot() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('', $this->logicalAnd($this->contains('{DAV:}getlastmodified'), $this->contains('{DAV:}getcontentlength')))
+ ->willReturn(['{DAV:}getlastmodified' => '2017-10-20T12:46:03+02:00']);
+
+ $this->assertEquals(['mtime' => 1508496363, 'size' => 0], $this->instance->stat(''));
+ $this->assertEquals(['mtime' => 1508496363, 'size' => 0], $this->instance->stat(''));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testStatException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->stat('/some%dir/file%type');
+ }
+
+ public function mimeTypeDataProvider() {
+ return [
+ [
+ [
+ '{DAV:}resourcetype' => $this->getResourceTypeResponse(true),
+ ],
+ 'httpd/unix-directory'
+ ],
+ [
+ [
+ '{DAV:}resourcetype' => $this->getResourceTypeResponse(false),
+ '{DAV:}getcontenttype' => 'text/plain'
+ ],
+ 'text/plain'
+ ],
+ [
+ [
+ '{DAV:}getcontenttype' => 'text/plain'
+ ],
+ 'text/plain'
+ ],
+ [
+ [
+ ],
+ false
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider mimeTypeDataProvider
+ */
+ public function testMimeType($davResponse, $apiResponse) {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir/file%25type', $this->logicalAnd($this->contains('{DAV:}resourcetype'), $this->contains('{DAV:}getcontenttype')))
+ ->willReturn($davResponse);
+
+ $this->assertEquals($apiResponse, $this->instance->getMimeType('/some%dir/file%type'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testMimeTypeException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->getMimeType('/some%dir/file%type');
+ }
+
+ public function permissionsDataProvider() {
+ return [
+ ['CK', true, true, false, false],
+ ['W', false, true, false, false],
+ ['D', false, false, true, false],
+ ['R', false, false, false, true],
+ ['RDWCK', true, true, true, true],
+ ];
+ }
+
+ /**
+ * @dataProvider permissionsDataProvider
+ */
+ public function testPermissions($perms, $creatable, $updatable, $deletable, $sharable) {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{http://owncloud.org/ns}permissions'))
+ ->willReturn([
+ '{http://owncloud.org/ns}permissions' => $perms
+ ]);
+
+ $path = 'some%dir';
+ $this->assertEquals($creatable, $this->instance->isCreatable($path));
+ $this->assertEquals($updatable, $this->instance->isUpdatable($path));
+ $this->assertEquals($deletable, $this->instance->isDeletable($path));
+ $this->assertEquals($sharable, $this->instance->isSharable($path));
+ }
+
+ public function testNoPermissionsDir() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{http://owncloud.org/ns}permissions'))
+ ->willReturn(['{DAV:}resourcetype' => $this->getResourceTypeResponse(true)]);
+
+ // all perms given
+ $path = 'some%dir';
+ $this->assertTrue($this->instance->isCreatable($path));
+ $this->assertTrue($this->instance->isUpdatable($path));
+ $this->assertTrue($this->instance->isDeletable($path));
+ $this->assertTrue($this->instance->isSharable($path));
+
+ }
+
+ public function testNoPermissionsFile() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{http://owncloud.org/ns}permissions'))
+ ->willReturn(['{DAV:}resourcetype' => $this->getResourceTypeResponse(false)]);
+
+ // all perms given except create
+ $path = 'some%dir';
+ $this->assertFalse($this->instance->isCreatable($path));
+ $this->assertTrue($this->instance->isUpdatable($path));
+ $this->assertTrue($this->instance->isDeletable($path));
+ $this->assertTrue($this->instance->isSharable($path));
+
+ }
+
+ public function testGetPermissionsUnexist() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{http://owncloud.org/ns}permissions'))
+ ->willReturn(false);
+
+ // all perms given
+ $path = 'some%dir';
+ $this->assertFalse($this->instance->isCreatable($path));
+ $this->assertFalse($this->instance->isUpdatable($path));
+ $this->assertFalse($this->instance->isDeletable($path));
+ $this->assertFalse($this->instance->isSharable($path));
+ $this->assertEquals(0, $this->instance->getPermissions($path));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testGetPermissionsException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $path = 'some%dir';
+ $this->instance->isSharable($path);
+ }
+
+ public function testGetEtag() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{DAV:}getetag'))
+ ->willReturn([
+ '{DAV:}getetag' => '"thisisanetagisntit"'
+ ]);
+
+ $this->assertEquals('thisisanetagisntit', $this->instance->getETag('some%dir'));
+ }
+
+ public function testGetEtagFallback() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{DAV:}getetag'))
+ ->willReturn([]);
+
+ // unique id
+ $this->assertInternalType('string', $this->instance->getETag('some%dir'));
+ }
+
+ public function testGetEtagUnexist() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{DAV:}getetag'))
+ ->willReturn(false);
+
+ $this->assertNull($this->instance->getETag('some%dir'));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testGetEtagException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir', $this->contains('{DAV:}getetag'))
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->getETag('some%dir');
+ }
+
+ public function hasUpdatedDataProvider() {
+ return [
+ // etag did not change
+ [
+ [
+ '{DAV:}getetag' => '"oldetag"'
+ ],
+ [
+ 'etag' => 'oldetag'
+ ],
+ false
+ ],
+ // etag changed
+ [
+ [
+ '{DAV:}getetag' => '"newetag"'
+ ],
+ [
+ 'etag' => 'oldetag'
+ ],
+ true
+ ],
+ // etag did not change, share permissions did
+ [
+ [
+ '{DAV:}getetag' => '"oldetag"',
+ '{http://open-collaboration-services.org/ns}share-permissions' => 1
+ ],
+ [
+ 'etag' => 'oldetag',
+ 'permissions' => 31
+ ],
+ true
+ ],
+ // etag did not change, share permissions did not
+ [
+ [
+ '{DAV:}getetag' => '"oldetag"',
+ '{http://open-collaboration-services.org/ns}share-permissions' => 1
+ ],
+ [
+ 'etag' => 'oldetag',
+ 'permissions' => 1
+ ],
+ false
+ ],
+ // etag did not change, regular permissions did
+ [
+ [
+ '{DAV:}getetag' => '"oldetag"',
+ '{http://owncloud.org/ns}permissions' => 1
+ ],
+ [
+ 'etag' => 'oldetag',
+ 'permissions' => 31
+ ],
+ true
+ ],
+ // etag did not change, regular permissions did not
+ [
+ [
+ '{DAV:}getetag' => '"oldetag"',
+ '{http://owncloud.org/ns}permissions' => 1
+ ],
+ [
+ 'etag' => 'oldetag',
+ 'permissions' => 1
+ ],
+ false
+ ],
+ // no etag, fallback to last modified, unchanged case
+ [
+ [
+ '{DAV:}getlastmodified' => '2017-10-20T12:46:03+02:00'
+ ],
+ null,
+ false
+ ],
+ // no etag, fallback to last modified, older case
+ [
+ [
+ '{DAV:}getlastmodified' => '2017-10-20T12:40:03+02:00'
+ ],
+ null,
+ false
+ ],
+ // no etag, fallback to last modified, remote mtime higher case
+ [
+ [
+ '{DAV:}getlastmodified' => '2017-10-20T12:50:03+02:00'
+ ],
+ null,
+ true
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider hasUpdatedDataProvider
+ */
+ public function testHasUpdated($davResponse, $cacheResponse, $expectedResult) {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->with('some%25dir',
+ $this->logicalAnd(
+ $this->contains('{DAV:}getetag'),
+ $this->contains('{DAV:}getlastmodified'),
+ $this->contains('{http://owncloud.org/ns}permissions'),
+ $this->contains('{http://open-collaboration-services.org/ns}share-permissions')
+ )
+ )
+ ->willReturn($davResponse);
+
+ if ($cacheResponse !== null) {
+ $this->cache->expects($this->once())
+ ->method('get')
+ ->with('some%dir')
+ ->willReturn($cacheResponse);
+ } else {
+ $this->cache->expects($this->never())
+ ->method('get');
+ }
+
+ $this->assertEquals($expectedResult, $this->instance->hasUpdated('some%dir', 1508496363));
+ }
+
+ public function testHasUpdatedPathNotfound() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willReturn(false);
+
+ $this->assertFalse($this->instance->hasUpdated('some%dir', 1508496363));
+ }
+
+ /**
+ * @expectedException \OCP\Files\StorageNotAvailableException
+ */
+ public function testHasUpdatedRootPathNotfound() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willReturn(false);
+
+ $this->instance->hasUpdated('', 1508496363);
+ }
+
+ /**
+ * @expectedException \OCP\Files\StorageNotAvailableException
+ */
+ public function testHasUpdatedRootPathMethodNotAllowed() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_METHOD_NOT_ALLOWED));
+
+ $this->instance->hasUpdated('', 1508496363);
+ }
+
+ /**
+ * @expectedException \OCP\Files\StorageNotAvailableException
+ */
+ public function testHasUpdatedMethodNotAllowed() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_METHOD_NOT_ALLOWED));
+
+ $this->assertFalse($this->instance->hasUpdated('some%dir', 1508496363));
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testHasUpdatedException() {
+ $this->davClient->expects($this->once())
+ ->method('propfind')
+ ->willThrowException($this->createClientHttpException(Http::STATUS_FORBIDDEN));
+
+ $this->instance->hasUpdated('some%dir', 1508496363);
+ }
+}
+
diff --git a/tests/lib/Http/Client/WebDavClientServiceTest.php b/tests/lib/Http/Client/WebDavClientServiceTest.php
new file mode 100644
index 000000000000..460bbe9a4358
--- /dev/null
+++ b/tests/lib/Http/Client/WebDavClientServiceTest.php
@@ -0,0 +1,101 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace Test\Http\Client;
+
+use OCP\IConfig;
+use OCP\ICertificateManager;
+use OC\Http\Client\WebDavClientService;
+use Sabre\DAV\Client;
+use OCP\ITempManager;
+
+/**
+ * Class WebDavClientServiceTest
+ */
+class WebDavClientServiceTest extends \Test\TestCase {
+ /**
+ * @var ITempManager
+ */
+ private $tempManager;
+
+ public function setUp() {
+ parent::setUp();
+ $this->tempManager = \OC::$server->getTempManager();
+ }
+
+ public function tearDown() {
+ $this->tempManager->clean();
+ parent::tearDown();
+ }
+
+ public function testNewClient() {
+ $config = $this->createMock(IConfig::class);
+ $certificateManager = $this->createMock(ICertificateManager::class);
+ $certificateManager->method('getAbsoluteBundlePath')
+ ->willReturn($this->tempManager->getTemporaryFolder());
+
+ $clientService = new WebDavClientService($config, $certificateManager);
+
+ $client = $clientService->newClient([
+ 'baseUri' => 'https://davhost/davroot/',
+ 'userName' => 'davUser'
+ ]);
+
+ $this->assertInstanceOf(Client::class, $client);
+ }
+
+ public function testNewClientWithProxy() {
+ $config = $this->createMock(IConfig::class);
+ $config->expects($this->once())
+ ->method('getSystemValue')
+ ->with('proxy', '')
+ ->willReturn('proxyhost');
+
+ $certificateManager = $this->createMock(ICertificateManager::class);
+ $certificateManager->method('getAbsoluteBundlePath')
+ ->willReturn($this->tempManager->getTemporaryFolder());
+
+ $clientService = new WebDavClientService($config, $certificateManager);
+
+ $client = $clientService->newClient([
+ 'baseUri' => 'https://davhost/davroot/',
+ 'userName' => 'davUser'
+ ]);
+
+ $this->assertInstanceOf(Client::class, $client);
+ }
+
+ public function testNewClientWithoutCertificate() {
+ $config = $this->createMock(IConfig::class);
+ $certificateManager = $this->createMock(ICertificateManager::class);
+ $certificateManager->method('getAbsoluteBundlePath')
+ ->willReturn($this->tempManager->getTemporaryFolder() . '/unexist');
+
+ $clientService = new WebDavClientService($config, $certificateManager);
+
+ $client = $clientService->newClient([
+ 'baseUri' => 'https://davhost/davroot/',
+ 'userName' => 'davUser'
+ ]);
+
+ $this->assertInstanceOf(Client::class, $client);
+ }
+}