summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/dav/appinfo/info.xml22
-rw-r--r--apps/dav/appinfo/v1/publicwebdav.php (renamed from apps/files_sharing/publicwebdav.php)6
-rw-r--r--apps/dav/appinfo/v1/webdav.php56
-rw-r--r--apps/dav/lib/connector/publicauth.php (renamed from apps/files_sharing/lib/connector/publicauth.php)2
-rw-r--r--apps/dav/lib/connector/sabre/appenabledplugin.php89
-rw-r--r--apps/dav/lib/connector/sabre/auth.php150
-rw-r--r--apps/dav/lib/connector/sabre/blocklegacyclientplugin.php79
-rw-r--r--apps/dav/lib/connector/sabre/copyetagheaderplugin.php56
-rw-r--r--apps/dav/lib/connector/sabre/custompropertiesbackend.php355
-rw-r--r--apps/dav/lib/connector/sabre/directory.php276
-rw-r--r--apps/dav/lib/connector/sabre/dummygetresponseplugin.php69
-rw-r--r--apps/dav/lib/connector/sabre/exception/entitytoolarge.php44
-rw-r--r--apps/dav/lib/connector/sabre/exception/filelocked.php47
-rw-r--r--apps/dav/lib/connector/sabre/exception/invalidpath.php77
-rw-r--r--apps/dav/lib/connector/sabre/exception/unsupportedmediatype.php44
-rw-r--r--apps/dav/lib/connector/sabre/exceptionloggerplugin.php107
-rw-r--r--apps/dav/lib/connector/sabre/file.php499
-rw-r--r--apps/dav/lib/connector/sabre/filesplugin.php265
-rw-r--r--apps/dav/lib/connector/sabre/listenerplugin.php68
-rw-r--r--apps/dav/lib/connector/sabre/lockplugin.php95
-rw-r--r--apps/dav/lib/connector/sabre/maintenanceplugin.php92
-rw-r--r--apps/dav/lib/connector/sabre/node.php270
-rw-r--r--apps/dav/lib/connector/sabre/objecttree.php284
-rw-r--r--apps/dav/lib/connector/sabre/principal.php205
-rw-r--r--apps/dav/lib/connector/sabre/quotaplugin.php141
-rw-r--r--apps/dav/lib/connector/sabre/server.php44
-rw-r--r--apps/dav/lib/connector/sabre/serverfactory.php113
-rw-r--r--apps/dav/lib/connector/sabre/taglist.php103
-rw-r--r--apps/dav/lib/connector/sabre/tagsplugin.php292
-rw-r--r--apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php129
-rw-r--r--apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php69
-rw-r--r--apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php73
-rw-r--r--apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php43
-rw-r--r--apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php286
-rw-r--r--apps/dav/tests/unit/connector/sabre/directory.php192
-rw-r--r--apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php44
-rw-r--r--apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php71
-rw-r--r--apps/dav/tests/unit/connector/sabre/file.php829
-rw-r--r--apps/dav/tests/unit/connector/sabre/filesplugin.php254
-rw-r--r--apps/dav/tests/unit/connector/sabre/node.php52
-rw-r--r--apps/dav/tests/unit/connector/sabre/objecttree.php291
-rw-r--r--apps/dav/tests/unit/connector/sabre/principal.php250
-rw-r--r--apps/dav/tests/unit/connector/sabre/quotaplugin.php103
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/auth.php69
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php52
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php25
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php32
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php129
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/sapi.php61
-rw-r--r--apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php110
-rw-r--r--apps/dav/tests/unit/connector/sabre/tagsplugin.php398
-rw-r--r--apps/files/appinfo/info.xml4
-rw-r--r--apps/files/appinfo/remote.php4
-rw-r--r--apps/files_sharing/appinfo/info.xml1
54 files changed, 7510 insertions, 11 deletions
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml
new file mode 100644
index 00000000000..c80af15a29b
--- /dev/null
+++ b/apps/dav/appinfo/info.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<info>
+ <id>dav</id>
+ <name>WebDAV</name>
+ <description>ownCloud WebDAV endpoint</description>
+ <licence>AGPL</licence>
+ <author>owncloud.org</author>
+ <requiremin>9.0</requiremin>
+ <shipped>true</shipped>
+ <standalone/>
+ <default_enable/>
+ <types>
+ <filesystem/>
+ </types>
+ <remote>
+ <files>appinfo/v1/webdav.php</files>
+ <webdav>appinfo/v1/webdav.php</webdav>
+ </remote>
+ <public>
+ <webdav>appinfo/v1/publicwebdav.php</webdav>
+ </public>
+</info>
diff --git a/apps/files_sharing/publicwebdav.php b/apps/dav/appinfo/v1/publicwebdav.php
index 7d96c8e8c97..5bdfd94e658 100644
--- a/apps/files_sharing/publicwebdav.php
+++ b/apps/dav/appinfo/v1/publicwebdav.php
@@ -23,16 +23,16 @@
*/
// load needed apps
-$RUNTIME_APPTYPES = array('filesystem', 'authentication', 'logging');
+$RUNTIME_APPTYPES = ['filesystem', 'authentication', 'logging'];
OC_App::loadApps($RUNTIME_APPTYPES);
OC_Util::obEnd();
// Backends
-$authBackend = new OCA\Files_Sharing\Connector\PublicAuth(\OC::$server->getConfig());
+$authBackend = new OCA\DAV\Connector\PublicAuth(\OC::$server->getConfig());
-$serverFactory = new \OC\Connector\Sabre\ServerFactory(
+$serverFactory = new OCA\DAV\Connector\Sabre\ServerFactory(
\OC::$server->getConfig(),
\OC::$server->getLogger(),
\OC::$server->getDatabaseConnection(),
diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php
new file mode 100644
index 00000000000..610230fc816
--- /dev/null
+++ b/apps/dav/appinfo/v1/webdav.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Frank Karlitschek <frank@owncloud.org>
+ * @author Jakob Sack <mail@jakobsack.de>
+ * @author Jörn Friedrich Dreyer <jfd@butonic.de>
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+// no php execution timeout for webdav
+set_time_limit(0);
+
+// Turn off output buffering to prevent memory problems
+\OC_Util::obEnd();
+
+$serverFactory = new \OCA\DAV\Connector\Sabre\ServerFactory(
+ \OC::$server->getConfig(),
+ \OC::$server->getLogger(),
+ \OC::$server->getDatabaseConnection(),
+ \OC::$server->getUserSession(),
+ \OC::$server->getMountManager(),
+ \OC::$server->getTagManager(),
+ \OC::$server->getEventDispatcher()
+);
+
+// Backends
+$authBackend = new \OCA\DAV\Connector\Sabre\Auth();
+$requestUri = \OC::$server->getRequest()->getRequestUri();
+
+$server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, function() {
+ // use the view for the logged in user
+ return \OC\Files\Filesystem::getView();
+});
+
+// And off we go!
+$server->exec();
diff --git a/apps/files_sharing/lib/connector/publicauth.php b/apps/dav/lib/connector/publicauth.php
index 4ac355180fc..f37be41402a 100644
--- a/apps/files_sharing/lib/connector/publicauth.php
+++ b/apps/dav/lib/connector/publicauth.php
@@ -24,7 +24,7 @@
*
*/
-namespace OCA\Files_Sharing\Connector;
+namespace OCA\DAV\Connector;
class PublicAuth extends \Sabre\DAV\Auth\Backend\AbstractBasic {
diff --git a/apps/dav/lib/connector/sabre/appenabledplugin.php b/apps/dav/lib/connector/sabre/appenabledplugin.php
new file mode 100644
index 00000000000..e70512d0fd1
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/appenabledplugin.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCP\App\IAppManager;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\ServerPlugin;
+
+/**
+ * Plugin to check if an app is enabled for the current user
+ */
+class AppEnabledPlugin extends ServerPlugin {
+
+ /**
+ * Reference to main server object
+ *
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var string
+ */
+ private $app;
+
+ /**
+ * @var \OCP\App\IAppManager
+ */
+ private $appManager;
+
+ /**
+ * @param string $app
+ * @param \OCP\App\IAppManager $appManager
+ */
+ public function __construct($app, IAppManager $appManager) {
+ $this->app = $app;
+ $this->appManager = $appManager;
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->on('beforeMethod', array($this, 'checkAppEnabled'), 30);
+ }
+
+ /**
+ * This method is called before any HTTP after auth and checks if the user has access to the app
+ *
+ * @throws \Sabre\DAV\Exception\Forbidden
+ * @return bool
+ */
+ public function checkAppEnabled() {
+ if (!$this->appManager->isEnabledForUser($this->app)) {
+ throw new Forbidden();
+ }
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/auth.php b/apps/dav/lib/connector/sabre/auth.php
new file mode 100644
index 00000000000..2e52a179d29
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/auth.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * @author Arthur Schiwon <blizzz@owncloud.com>
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Christian Seiler <christian@iwakd.de>
+ * @author Jakob Sack <mail@jakobsack.de>
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Markus Goetz <markus@woboq.com>
+ * @author Michael Gapczynski <GapczynskiM@gmail.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+use Exception;
+use Sabre\DAV\Auth\Backend\AbstractBasic;
+use Sabre\DAV\Exception\NotAuthenticated;
+use Sabre\DAV\Exception\ServiceUnavailable;
+
+class Auth extends AbstractBasic {
+ const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
+
+ /**
+ * Whether the user has initially authenticated via DAV
+ *
+ * This is required for WebDAV clients that resent the cookies even when the
+ * account was changed.
+ *
+ * @see https://github.com/owncloud/core/issues/13245
+ *
+ * @param string $username
+ * @return bool
+ */
+ protected function isDavAuthenticated($username) {
+ return !is_null(\OC::$server->getSession()->get(self::DAV_AUTHENTICATED)) &&
+ \OC::$server->getSession()->get(self::DAV_AUTHENTICATED) === $username;
+ }
+
+ /**
+ * Validates a username and password
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ * @return bool
+ */
+ protected function validateUserPass($username, $password) {
+ if (\OC_User::isLoggedIn() &&
+ $this->isDavAuthenticated(\OC_User::getUser())
+ ) {
+ \OC_Util::setupFS(\OC_User::getUser());
+ \OC::$server->getSession()->close();
+ return true;
+ } else {
+ \OC_Util::setUpFS(); //login hooks may need early access to the filesystem
+ if(\OC_User::login($username, $password)) {
+ // make sure we use ownCloud's internal username here
+ // and not the HTTP auth supplied one, see issue #14048
+ $ocUser = \OC_User::getUser();
+ \OC_Util::setUpFS($ocUser);
+ \OC::$server->getSession()->set(self::DAV_AUTHENTICATED, $ocUser);
+ \OC::$server->getSession()->close();
+ return true;
+ } else {
+ \OC::$server->getSession()->close();
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Returns information about the currently logged in username.
+ *
+ * If nobody is currently logged in, this method should return null.
+ *
+ * @return string|null
+ */
+ public function getCurrentUser() {
+ $user = \OC_User::getUser();
+ if($user && $this->isDavAuthenticated($user)) {
+ return $user;
+ }
+ return null;
+ }
+
+ /**
+ * Override function here. We want to cache authentication cookies
+ * in the syncing client to avoid HTTP-401 roundtrips.
+ * If the sync client supplies the cookies, then OC_User::isLoggedIn()
+ * will return true and we can see this WebDAV request as already authenticated,
+ * even if there are no HTTP Basic Auth headers.
+ * In other case, just fallback to the parent implementation.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @param string $realm
+ * @return bool
+ * @throws ServiceUnavailable
+ */
+ public function authenticate(\Sabre\DAV\Server $server, $realm) {
+
+ try {
+ $result = $this->auth($server, $realm);
+ return $result;
+ } catch (NotAuthenticated $e) {
+ throw $e;
+ } catch (Exception $e) {
+ $class = get_class($e);
+ $msg = $e->getMessage();
+ throw new ServiceUnavailable("$class: $msg");
+ }
+ }
+
+ /**
+ * @param \Sabre\DAV\Server $server
+ * @param $realm
+ * @return bool
+ */
+ private function auth(\Sabre\DAV\Server $server, $realm) {
+ if (\OC_User::handleApacheAuth() ||
+ (\OC_User::isLoggedIn() && is_null(\OC::$server->getSession()->get(self::DAV_AUTHENTICATED)))
+ ) {
+ $user = \OC_User::getUser();
+ \OC_Util::setupFS($user);
+ $this->currentUser = $user;
+ \OC::$server->getSession()->close();
+ return true;
+ }
+
+ return parent::authenticate($server, $realm);
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/blocklegacyclientplugin.php b/apps/dav/lib/connector/sabre/blocklegacyclientplugin.php
new file mode 100644
index 00000000000..ed61f43a536
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/blocklegacyclientplugin.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCP\IConfig;
+use Sabre\HTTP\RequestInterface;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\Exception;
+
+/**
+ * Class BlockLegacyClientPlugin is used to detect old legacy sync clients and
+ * returns a 403 status to those clients
+ *
+ * @package OCA\DAV\Connector\Sabre
+ */
+class BlockLegacyClientPlugin extends ServerPlugin {
+ /** @var \Sabre\DAV\Server */
+ protected $server;
+ /** @var IConfig */
+ protected $config;
+
+ /**
+ * @param IConfig $config
+ */
+ public function __construct(IConfig $config) {
+ $this->config = $config;
+ }
+
+ /**
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+ $this->server = $server;
+ $this->server->on('beforeMethod', [$this, 'beforeHandler'], 200);
+ }
+
+ /**
+ * Detects all unsupported clients and throws a \Sabre\DAV\Exception\Forbidden
+ * exception which will result in a 403 to them.
+ * @param RequestInterface $request
+ * @throws \Sabre\DAV\Exception\Forbidden If the client version is not supported
+ */
+ public function beforeHandler(RequestInterface $request) {
+ $userAgent = $request->getHeader('User-Agent');
+ if($userAgent === null) {
+ return;
+ }
+
+ $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '1.7.0');
+
+ // Match on the mirall version which is in scheme "Mozilla/5.0 (%1) mirall/%2" or
+ // "mirall/%1" for older releases
+ preg_match("/(?:mirall\\/)([\d.]+)/i", $userAgent, $versionMatches);
+ if(isset($versionMatches[1]) &&
+ version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) {
+ throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.');
+ }
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/copyetagheaderplugin.php b/apps/dav/lib/connector/sabre/copyetagheaderplugin.php
new file mode 100644
index 00000000000..b33b208adad
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/copyetagheaderplugin.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use \Sabre\HTTP\RequestInterface;
+use \Sabre\HTTP\ResponseInterface;
+
+/**
+ * Copies the "Etag" header to "OC-Etag" after any request.
+ * This is a workaround for setups that automatically strip
+ * or mangle Etag headers.
+ */
+class CopyEtagHeaderPlugin extends \Sabre\DAV\ServerPlugin {
+ /**
+ * This initializes the plugin.
+ *
+ * @param \Sabre\DAV\Server $server Sabre server
+ *
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+ $server->on('afterMethod', array($this, 'afterMethod'));
+ }
+
+ /**
+ * After method, copy the "Etag" header to "OC-Etag" header.
+ *
+ * @param RequestInterface $request request
+ * @param ResponseInterface $response response
+ */
+ public function afterMethod(RequestInterface $request, ResponseInterface $response) {
+ $eTag = $response->getHeader('Etag');
+ if (!empty($eTag)) {
+ $response->setHeader('OC-ETag', $eTag);
+ }
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/custompropertiesbackend.php b/apps/dav/lib/connector/sabre/custompropertiesbackend.php
new file mode 100644
index 00000000000..ff35476319f
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/custompropertiesbackend.php
@@ -0,0 +1,355 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCP\IDBConnection;
+use OCP\IUser;
+use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\Tree;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Exception\ServiceUnavailable;
+
+class CustomPropertiesBackend implements BackendInterface {
+
+ /**
+ * Ignored properties
+ *
+ * @var array
+ */
+ private $ignoredProperties = array(
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{DAV:}quota-used-bytes',
+ '{DAV:}quota-available-bytes',
+ '{DAV:}quota-available-bytes',
+ '{http://owncloud.org/ns}permissions',
+ '{http://owncloud.org/ns}downloadURL',
+ '{http://owncloud.org/ns}dDC',
+ '{http://owncloud.org/ns}size',
+ );
+
+ /**
+ * @var Tree
+ */
+ private $tree;
+
+ /**
+ * @var IDBConnection
+ */
+ private $connection;
+
+ /**
+ * @var IUser
+ */
+ private $user;
+
+ /**
+ * Properties cache
+ *
+ * @var array
+ */
+ private $cache = [];
+
+ /**
+ * @param Tree $tree node tree
+ * @param IDBConnection $connection database connection
+ * @param IUser $user owner of the tree and properties
+ */
+ public function __construct(
+ Tree $tree,
+ IDBConnection $connection,
+ IUser $user) {
+ $this->tree = $tree;
+ $this->connection = $connection;
+ $this->user = $user->getUID();
+ }
+
+ /**
+ * Fetches properties for a path.
+ *
+ * @param string $path
+ * @param PropFind $propFind
+ * @return void
+ */
+ public function propFind($path, PropFind $propFind) {
+ try {
+ $node = $this->tree->getNodeForPath($path);
+ if (!($node instanceof Node)) {
+ return;
+ }
+ } catch (ServiceUnavailable $e) {
+ // might happen for unavailable mount points, skip
+ return;
+ } catch (NotFound $e) {
+ // in some rare (buggy) cases the node might not be found,
+ // we catch the exception to prevent breaking the whole list with a 404
+ // (soft fail)
+ \OC::$server->getLogger()->warning(
+ 'Could not get node for path: \"' . $path . '\" : ' . $e->getMessage(),
+ array('app' => 'files')
+ );
+ return;
+ }
+
+ $requestedProps = $propFind->get404Properties();
+
+ // these might appear
+ $requestedProps = array_diff(
+ $requestedProps,
+ $this->ignoredProperties
+ );
+
+ if (empty($requestedProps)) {
+ return;
+ }
+
+ if ($node instanceof Directory
+ && $propFind->getDepth() !== 0
+ ) {
+ // note: pre-fetching only supported for depth <= 1
+ $this->loadChildrenProperties($node, $requestedProps);
+ }
+
+ $props = $this->getProperties($node, $requestedProps);
+ foreach ($props as $propName => $propValue) {
+ $propFind->set($propName, $propValue);
+ }
+ }
+
+ /**
+ * Updates properties for a path
+ *
+ * @param string $path
+ * @param PropPatch $propPatch
+ *
+ * @return void
+ */
+ public function propPatch($path, PropPatch $propPatch) {
+ $node = $this->tree->getNodeForPath($path);
+ if (!($node instanceof Node)) {
+ return;
+ }
+
+ $propPatch->handleRemaining(function($changedProps) use ($node) {
+ return $this->updateProperties($node, $changedProps);
+ });
+ }
+
+ /**
+ * This method is called after a node is deleted.
+ *
+ * @param string $path path of node for which to delete properties
+ */
+ public function delete($path) {
+ $statement = $this->connection->prepare(
+ 'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'
+ );
+ $statement->execute(array($this->user, '/' . $path));
+ $statement->closeCursor();
+
+ unset($this->cache[$path]);
+ }
+
+ /**
+ * This method is called after a successful MOVE
+ *
+ * @param string $source
+ * @param string $destination
+ *
+ * @return void
+ */
+ public function move($source, $destination) {
+ $statement = $this->connection->prepare(
+ 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' .
+ ' WHERE `userid` = ? AND `propertypath` = ?'
+ );
+ $statement->execute(array('/' . $destination, $this->user, '/' . $source));
+ $statement->closeCursor();
+ }
+
+ /**
+ * Returns a list of properties for this nodes.;
+ * @param Node $node
+ * @param array $requestedProperties requested properties or empty array for "all"
+ * @return array
+ * @note The properties list is a list of propertynames the client
+ * requested, encoded as xmlnamespace#tagName, for example:
+ * http://www.example.org/namespace#author If the array is empty, all
+ * properties should be returned
+ */
+ private function getProperties(Node $node, array $requestedProperties) {
+ $path = $node->getPath();
+ if (isset($this->cache[$path])) {
+ return $this->cache[$path];
+ }
+
+ // TODO: chunking if more than 1000 properties
+ $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?';
+
+ $whereValues = array($this->user, $path);
+ $whereTypes = array(null, null);
+
+ if (!empty($requestedProperties)) {
+ // request only a subset
+ $sql .= ' AND `propertyname` in (?)';
+ $whereValues[] = $requestedProperties;
+ $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY;
+ }
+
+ $result = $this->connection->executeQuery(
+ $sql,
+ $whereValues,
+ $whereTypes
+ );
+
+ $props = [];
+ while ($row = $result->fetch()) {
+ $props[$row['propertyname']] = $row['propertyvalue'];
+ }
+
+ $result->closeCursor();
+
+ $this->cache[$path] = $props;
+ return $props;
+ }
+
+ /**
+ * Update properties
+ *
+ * @param Node $node node for which to update properties
+ * @param array $properties array of properties to update
+ *
+ * @return bool
+ */
+ private function updateProperties($node, $properties) {
+ $path = $node->getPath();
+
+ $deleteStatement = 'DELETE FROM `*PREFIX*properties`' .
+ ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
+
+ $insertStatement = 'INSERT INTO `*PREFIX*properties`' .
+ ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)';
+
+ $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' .
+ ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
+
+ // TODO: use "insert or update" strategy ?
+ $existing = $this->getProperties($node, array());
+ $this->connection->beginTransaction();
+ foreach ($properties as $propertyName => $propertyValue) {
+ // If it was null, we need to delete the property
+ if (is_null($propertyValue)) {
+ if (array_key_exists($propertyName, $existing)) {
+ $this->connection->executeUpdate($deleteStatement,
+ array(
+ $this->user,
+ $path,
+ $propertyName
+ )
+ );
+ }
+ } else {
+ if (!array_key_exists($propertyName, $existing)) {
+ $this->connection->executeUpdate($insertStatement,
+ array(
+ $this->user,
+ $path,
+ $propertyName,
+ $propertyValue
+ )
+ );
+ } else {
+ $this->connection->executeUpdate($updateStatement,
+ array(
+ $propertyValue,
+ $this->user,
+ $path,
+ $propertyName
+ )
+ );
+ }
+ }
+ }
+
+ $this->connection->commit();
+ unset($this->cache[$path]);
+
+ return true;
+ }
+
+ /**
+ * Bulk load properties for directory children
+ *
+ * @param Directory $node
+ * @param array $requestedProperties requested properties
+ *
+ * @return void
+ */
+ private function loadChildrenProperties(Directory $node, $requestedProperties) {
+ $path = $node->getPath();
+ if (isset($this->cache[$path])) {
+ // we already loaded them at some point
+ return;
+ }
+
+ $childNodes = $node->getChildren();
+ // pre-fill cache
+ foreach ($childNodes as $childNode) {
+ $this->cache[$childNode->getPath()] = [];
+ }
+
+ $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` LIKE ?';
+ $sql .= ' AND `propertyname` in (?) ORDER BY `propertypath`, `propertyname`';
+
+ $result = $this->connection->executeQuery(
+ $sql,
+ array($this->user, rtrim($path, '/') . '/%', $requestedProperties),
+ array(null, null, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
+ );
+
+ $oldPath = null;
+ $props = [];
+ while ($row = $result->fetch()) {
+ $path = $row['propertypath'];
+ if ($oldPath !== $path) {
+ // save previously gathered props
+ $this->cache[$oldPath] = $props;
+ $oldPath = $path;
+ // prepare props for next path
+ $props = [];
+ }
+ $props[$row['propertyname']] = $row['propertyvalue'];
+ }
+ if (!is_null($oldPath)) {
+ // save props from last run
+ $this->cache[$oldPath] = $props;
+ }
+
+ $result->closeCursor();
+ }
+
+}
diff --git a/apps/dav/lib/connector/sabre/directory.php b/apps/dav/lib/connector/sabre/directory.php
new file mode 100644
index 00000000000..8c736ea0108
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/directory.php
@@ -0,0 +1,276 @@
+<?php
+/**
+ * @author Arthur Schiwon <blizzz@owncloud.com>
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Björn Schießle <schiessle@owncloud.com>
+ * @author Jakob Sack <mail@jakobsack.de>
+ * @author Joas Schilling <nickvergessen@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Connector\Sabre\Exception\FileLocked;
+use OCP\Lock\ILockingProvider;
+use OCP\Lock\LockedException;
+use Sabre\DAV\Exception\Locked;
+
+class Directory extends \OCA\DAV\Connector\Sabre\Node
+ implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota {
+
+ /**
+ * Cached directory content
+ *
+ * @var \OCP\Files\FileInfo[]
+ */
+ private $dirContent;
+
+ /**
+ * Cached quota info
+ *
+ * @var array
+ */
+ private $quotaInfo;
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ * @throws Exception\EntityTooLarge
+ * @throws Exception\UnsupportedMediaType
+ * @throws FileLocked
+ * @throws InvalidPath
+ * @throws \Sabre\DAV\Exception
+ * @throws \Sabre\DAV\Exception\BadRequest
+ * @throws \Sabre\DAV\Exception\Forbidden
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
+ */
+ public function createFile($name, $data = null) {
+
+ try {
+ // for chunked upload also updating a existing file is a "createFile"
+ // because we create all the chunks before re-assemble them to the existing file.
+ if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
+
+ // exit if we can't create a new file and we don't updatable existing file
+ $info = \OC_FileChunking::decodeName($name);
+ if (!$this->fileView->isCreatable($this->path) &&
+ !$this->fileView->isUpdatable($this->path . '/' . $info['name'])
+ ) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+
+ } else {
+ // For non-chunked upload it is enough to check if we can create a new file
+ if (!$this->fileView->isCreatable($this->path)) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+ }
+
+ $this->fileView->verifyPath($this->path, $name);
+
+ $path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
+ // using a dummy FileInfo is acceptable here since it will be refreshed after the put is complete
+ $info = new \OC\Files\FileInfo($path, null, null, array(), null);
+ $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
+ $node->acquireLock(ILockingProvider::LOCK_SHARED);
+ return $node->put($data);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (\OCP\Files\InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @throws FileLocked
+ * @throws InvalidPath
+ * @throws \Sabre\DAV\Exception\Forbidden
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
+ */
+ public function createDirectory($name) {
+ try {
+ if (!$this->info->isCreatable()) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+
+ $this->fileView->verifyPath($this->path, $name);
+ $newPath = $this->path . '/' . $name;
+ if (!$this->fileView->mkdir($newPath)) {
+ throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
+ }
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (\OCP\Files\InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * @param string $name
+ * @param \OCP\Files\FileInfo $info
+ * @return \Sabre\DAV\INode
+ * @throws InvalidPath
+ * @throws \Sabre\DAV\Exception\NotFound
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
+ */
+ public function getChild($name, $info = null) {
+ $path = $this->path . '/' . $name;
+ if (is_null($info)) {
+ try {
+ $this->fileView->verifyPath($this->path, $name);
+ $info = $this->fileView->getFileInfo($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (\OCP\Files\InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ }
+ }
+
+ if (!$info) {
+ throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
+ }
+
+ if ($info['mimetype'] == 'httpd/unix-directory') {
+ $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info);
+ } else {
+ $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
+ }
+ return $node;
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return \Sabre\DAV\INode[]
+ */
+ public function getChildren() {
+ if (!is_null($this->dirContent)) {
+ return $this->dirContent;
+ }
+ try {
+ $folderContent = $this->fileView->getDirectoryContent($this->path);
+ } catch (LockedException $e) {
+ throw new Locked();
+ }
+
+ $nodes = array();
+ foreach ($folderContent as $info) {
+ $node = $this->getChild($info->getName(), $info);
+ $nodes[] = $node;
+ }
+ $this->dirContent = $nodes;
+ return $this->dirContent;
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+ // note: here we do NOT resolve the chunk file name to the real file name
+ // to make sure we return false when checking for file existence with a chunk
+ // file name.
+ // This is to make sure that "createFile" is still triggered
+ // (required old code) instead of "updateFile".
+ //
+ // TODO: resolve chunk file name here and implement "updateFile"
+ $path = $this->path . '/' . $name;
+ return $this->fileView->file_exists($path);
+
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself
+ *
+ * @return void
+ * @throws FileLocked
+ * @throws \Sabre\DAV\Exception\Forbidden
+ */
+ public function delete() {
+
+ if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+
+ try {
+ if (!$this->fileView->rmdir($this->path)) {
+ // assume it wasn't possible to remove due to permission issue
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Returns available diskspace information
+ *
+ * @return array
+ */
+ public function getQuotaInfo() {
+ if ($this->quotaInfo) {
+ return $this->quotaInfo;
+ }
+ try {
+ $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $this->info);
+ $this->quotaInfo = array(
+ $storageInfo['used'],
+ $storageInfo['free']
+ );
+ return $this->quotaInfo;
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ return array(0, 0);
+ }
+ }
+
+}
diff --git a/apps/dav/lib/connector/sabre/dummygetresponseplugin.php b/apps/dav/lib/connector/sabre/dummygetresponseplugin.php
new file mode 100644
index 00000000000..7c7a332fedd
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/dummygetresponseplugin.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\HTTP\RequestInterface;
+
+/**
+ * Class DummyGetResponsePlugin is a plugin used to not show a "Not implemented"
+ * error to clients that rely on verifying the functionality of the ownCloud
+ * WebDAV backend using a simple GET to /.
+ *
+ * This is considered a legacy behaviour and implementers should consider sending
+ * a PROPFIND request instead to verify whether the WebDAV component is working
+ * properly.
+ *
+ * FIXME: Remove once clients are all compliant.
+ *
+ * @package OCA\DAV\Connector\Sabre
+ */
+class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin {
+ /** @var \Sabre\DAV\Server */
+ protected $server;
+
+ /**
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ function initialize(\Sabre\DAV\Server $server) {
+ $this->server = $server;
+ $this->server->on('method:GET', [$this, 'httpGet'], 200);
+ }
+
+ /**
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return false
+ */
+ function httpGet(RequestInterface $request, ResponseInterface $response) {
+ $string = 'This is the WebDAV interface. It can only be accessed by ' .
+ 'WebDAV clients such as the ownCloud desktop sync client.';
+ $stream = fopen('php://memory','r+');
+ fwrite($stream, $string);
+ rewind($stream);
+
+ $response->setStatus(200);
+ $response->setBody($stream);
+
+ return false;
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/exception/entitytoolarge.php b/apps/dav/lib/connector/sabre/exception/entitytoolarge.php
new file mode 100644
index 00000000000..f5a7aa99c6d
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/exception/entitytoolarge.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OCA\DAV\Connector\Sabre\Exception;
+
+/**
+ * Entity Too Large
+ *
+ * This exception is thrown whenever a user tries to upload a file which exceeds hard limitations
+ *
+ */
+class EntityTooLarge extends \Sabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP status code for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 413;
+
+ }
+
+}
diff --git a/apps/dav/lib/connector/sabre/exception/filelocked.php b/apps/dav/lib/connector/sabre/exception/filelocked.php
new file mode 100644
index 00000000000..1e1585edbda
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/exception/filelocked.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Owen Winkler <a_github@midnightcircus.com>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre\Exception;
+
+use Exception;
+
+class FileLocked extends \Sabre\DAV\Exception {
+
+ public function __construct($message = "", $code = 0, Exception $previous = null) {
+ if($previous instanceof \OCP\Files\LockNotAcquiredException) {
+ $message = sprintf('Target file %s is locked by another process.', $previous->path);
+ }
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * Returns the HTTP status code for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 423;
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/exception/invalidpath.php b/apps/dav/lib/connector/sabre/exception/invalidpath.php
new file mode 100644
index 00000000000..608e427a5aa
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/exception/invalidpath.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre\Exception;
+
+use Sabre\DAV\Exception;
+
+class InvalidPath extends Exception {
+
+ const NS_OWNCLOUD = 'http://owncloud.org/ns';
+
+ /**
+ * @var bool
+ */
+ private $retry;
+
+ /**
+ * @param string $message
+ * @param bool $retry
+ */
+ public function __construct($message, $retry = false) {
+ parent::__construct($message);
+ $this->retry = $retry;
+ }
+
+ /**
+ * Returns the HTTP status code for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 400;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information
+ * into the WebDAV error response
+ *
+ * @param \Sabre\DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) {
+
+ // set ownCloud namespace
+ $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
+
+ // adding the retry node
+ $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true));
+ $errorNode->appendChild($error);
+
+ // adding the message node
+ $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage());
+ $errorNode->appendChild($error);
+ }
+
+}
diff --git a/apps/dav/lib/connector/sabre/exception/unsupportedmediatype.php b/apps/dav/lib/connector/sabre/exception/unsupportedmediatype.php
new file mode 100644
index 00000000000..96b9b8332de
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/exception/unsupportedmediatype.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OCA\DAV\Connector\Sabre\Exception;
+
+/**
+ * Unsupported Media Type
+ *
+ * This exception is thrown whenever a user tries to upload a file which holds content which is not allowed
+ *
+ */
+class UnsupportedMediaType extends \Sabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP status code for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 415;
+
+ }
+
+}
diff --git a/apps/dav/lib/connector/sabre/exceptionloggerplugin.php b/apps/dav/lib/connector/sabre/exceptionloggerplugin.php
new file mode 100644
index 00000000000..64ec5cfda82
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/exceptionloggerplugin.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCP\ILogger;
+use Sabre\DAV\Exception;
+use Sabre\HTTP\Response;
+
+class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
+ protected $nonFatalExceptions = array(
+ 'Sabre\DAV\Exception\NotAuthenticated' => true,
+ // the sync client uses this to find out whether files exist,
+ // so it is not always an error, log it as debug
+ 'Sabre\DAV\Exception\NotFound' => true,
+ // this one mostly happens when the same file is uploaded at
+ // exactly the same time from two clients, only one client
+ // wins, the second one gets "Precondition failed"
+ 'Sabre\DAV\Exception\PreconditionFailed' => true,
+ // forbidden can be expected when trying to upload to
+ // read-only folders for example
+ 'Sabre\DAV\Exception\Forbidden' => true,
+ );
+
+ /** @var string */
+ private $appName;
+
+ /** @var ILogger */
+ private $logger;
+
+ /**
+ * @param string $loggerAppName app name to use when logging
+ * @param ILogger $logger
+ */
+ public function __construct($loggerAppName, $logger) {
+ $this->appName = $loggerAppName;
+ $this->logger = $logger;
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+
+ $server->on('exception', array($this, 'logException'), 10);
+ }
+
+ /**
+ * Log exception
+ *
+ */
+ public function logException(\Exception $ex) {
+ $exceptionClass = get_class($ex);
+ $level = \OCP\Util::FATAL;
+ if (isset($this->nonFatalExceptions[$exceptionClass])) {
+ $level = \OCP\Util::DEBUG;
+ }
+
+ $message = $ex->getMessage();
+ if ($ex instanceof Exception) {
+ if (empty($message)) {
+ $response = new Response($ex->getHTTPCode());
+ $message = $response->getStatusText();
+ }
+ $message = "HTTP/1.1 {$ex->getHTTPCode()} $message";
+ }
+
+ $exception = [
+ 'Message' => $message,
+ 'Exception' => $exceptionClass,
+ 'Code' => $ex->getCode(),
+ 'Trace' => $ex->getTraceAsString(),
+ 'File' => $ex->getFile(),
+ 'Line' => $ex->getLine(),
+ ];
+ $this->logger->log($level, 'Exception: ' . json_encode($exception), ['app' => $this->appName]);
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/file.php b/apps/dav/lib/connector/sabre/file.php
new file mode 100644
index 00000000000..9e515cdc687
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/file.php
@@ -0,0 +1,499 @@
+<?php
+/**
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Björn Schießle <schiessle@owncloud.com>
+ * @author Jakob Sack <mail@jakobsack.de>
+ * @author Jörn Friedrich Dreyer <jfd@butonic.de>
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Owen Winkler <a_github@midnightcircus.com>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Thomas Tanghus <thomas@tanghus.net>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OC\Files\Filesystem;
+use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
+use OCA\DAV\Connector\Sabre\Exception\FileLocked;
+use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType;
+use OCP\Encryption\Exceptions\GenericEncryptionException;
+use OCP\Files\EntityTooLargeException;
+use OCP\Files\InvalidContentException;
+use OCP\Files\InvalidPathException;
+use OCP\Files\LockNotAcquiredException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\Lock\ILockingProvider;
+use OCP\Lock\LockedException;
+use Sabre\DAV\Exception;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotImplemented;
+use Sabre\DAV\Exception\ServiceUnavailable;
+use Sabre\DAV\IFile;
+
+class File extends Node implements IFile {
+
+ /**
+ * Updates the data
+ *
+ * The data argument is a readable stream resource.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * If you don't plan to store the file byte-by-byte, and you return a
+ * different object on a subsequent GET you are strongly recommended to not
+ * return an ETag, and just return null.
+ *
+ * @param resource $data
+ *
+ * @throws Forbidden
+ * @throws UnsupportedMediaType
+ * @throws BadRequest
+ * @throws Exception
+ * @throws EntityTooLarge
+ * @throws ServiceUnavailable
+ * @throws FileLocked
+ * @return string|null
+ */
+ public function put($data) {
+ try {
+ $exists = $this->fileView->file_exists($this->path);
+ if ($this->info && $exists && !$this->info->isUpdateable()) {
+ throw new Forbidden();
+ }
+ } catch (StorageNotAvailableException $e) {
+ throw new ServiceUnavailable("File is not updatable: " . $e->getMessage());
+ }
+
+ // verify path of the target
+ $this->verifyPath();
+
+ // chunked handling
+ if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ try {
+ return $this->createFileChunked($data);
+ } catch (\Exception $e) {
+ $this->convertToSabreException($e);
+ }
+ }
+
+ list($partStorage) = $this->fileView->resolvePath($this->path);
+ $needsPartFile = $this->needsPartFile($partStorage) && (strlen($this->path) > 1);
+
+ if ($needsPartFile) {
+ // mark file as partial while uploading (ignored by the scanner)
+ $partFilePath = $this->path . '.ocTransferId' . rand() . '.part';
+ } else {
+ // upload file directly as the final path
+ $partFilePath = $this->path;
+ }
+
+ // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
+ /** @var \OC\Files\Storage\Storage $partStorage */
+ list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath);
+ /** @var \OC\Files\Storage\Storage $storage */
+ list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
+ try {
+ $target = $partStorage->fopen($internalPartPath, 'wb');
+ if ($target === false) {
+ \OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::fopen() failed', \OCP\Util::ERROR);
+ // because we have no clue about the cause we can only throw back a 500/Internal Server Error
+ throw new Exception('Could not write file contents');
+ }
+ list($count,) = \OC_Helper::streamCopy($data, $target);
+ fclose($target);
+
+ // if content length is sent by client:
+ // double check if the file was fully received
+ // compare expected and actual size
+ if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] !== 'LOCK') {
+ $expected = $_SERVER['CONTENT_LENGTH'];
+ if ($count != $expected) {
+ throw new BadRequest('expected filesize ' . $expected . ' got ' . $count);
+ }
+ }
+
+ } catch (\Exception $e) {
+ if ($needsPartFile) {
+ $partStorage->unlink($internalPartPath);
+ }
+ $this->convertToSabreException($e);
+ }
+
+ try {
+ $view = \OC\Files\Filesystem::getView();
+ if ($view) {
+ $run = $this->emitPreHooks($exists);
+ } else {
+ $run = true;
+ }
+
+ try {
+ $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
+ } catch (LockedException $e) {
+ if ($needsPartFile) {
+ $partStorage->unlink($internalPartPath);
+ }
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+
+ if ($needsPartFile) {
+ // rename to correct path
+ try {
+ if ($run) {
+ $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
+ $fileExists = $storage->file_exists($internalPath);
+ }
+ if (!$run || $renameOkay === false || $fileExists === false) {
+ \OCP\Util::writeLog('webdav', 'renaming part file to final file failed', \OCP\Util::ERROR);
+ throw new Exception('Could not rename part file to final file');
+ }
+ } catch (\Exception $e) {
+ $partStorage->unlink($internalPartPath);
+ $this->convertToSabreException($e);
+ }
+ }
+
+ try {
+ $this->changeLock(ILockingProvider::LOCK_SHARED);
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+
+ // since we skipped the view we need to scan and emit the hooks ourselves
+ $this->fileView->getUpdater()->update($this->path);
+
+ if ($view) {
+ $this->emitPostHooks($exists);
+ }
+
+ // allow sync clients to send the mtime along in a header
+ $request = \OC::$server->getRequest();
+ if (isset($request->server['HTTP_X_OC_MTIME'])) {
+ if ($this->fileView->touch($this->path, $request->server['HTTP_X_OC_MTIME'])) {
+ header('X-OC-MTime: accepted');
+ }
+ }
+ $this->refreshInfo();
+ } catch (StorageNotAvailableException $e) {
+ throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage());
+ }
+
+ return '"' . $this->info->getEtag() . '"';
+ }
+
+ private function emitPreHooks($exists, $path = null) {
+ if (is_null($path)) {
+ $path = $this->path;
+ }
+ $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
+ $run = true;
+
+ if (!$exists) {
+ \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array(
+ \OC\Files\Filesystem::signal_param_path => $hookPath,
+ \OC\Files\Filesystem::signal_param_run => &$run,
+ ));
+ } else {
+ \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array(
+ \OC\Files\Filesystem::signal_param_path => $hookPath,
+ \OC\Files\Filesystem::signal_param_run => &$run,
+ ));
+ }
+ \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array(
+ \OC\Files\Filesystem::signal_param_path => $hookPath,
+ \OC\Files\Filesystem::signal_param_run => &$run,
+ ));
+ return $run;
+ }
+
+ private function emitPostHooks($exists, $path = null) {
+ if (is_null($path)) {
+ $path = $this->path;
+ }
+ $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
+ if (!$exists) {
+ \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array(
+ \OC\Files\Filesystem::signal_param_path => $hookPath
+ ));
+ } else {
+ \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array(
+ \OC\Files\Filesystem::signal_param_path => $hookPath
+ ));
+ }
+ \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array(
+ \OC\Files\Filesystem::signal_param_path => $hookPath
+ ));
+ }
+
+ /**
+ * Returns the data
+ *
+ * @return string|resource
+ * @throws Forbidden
+ * @throws ServiceUnavailable
+ */
+ public function get() {
+ //throw exception if encryption is disabled but files are still encrypted
+ try {
+ $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
+ if ($res === false) {
+ throw new ServiceUnavailable("Could not open file");
+ }
+ return $res;
+ } catch (GenericEncryptionException $e) {
+ // returning 503 will allow retry of the operation at a later point in time
+ throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage());
+ } catch (StorageNotAvailableException $e) {
+ throw new ServiceUnavailable("Failed to open file: " . $e->getMessage());
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Delete the current file
+ *
+ * @throws Forbidden
+ * @throws ServiceUnavailable
+ */
+ public function delete() {
+ if (!$this->info->isDeletable()) {
+ throw new Forbidden();
+ }
+
+ try {
+ if (!$this->fileView->unlink($this->path)) {
+ // assume it wasn't possible to delete due to permissions
+ throw new Forbidden();
+ }
+ } catch (StorageNotAvailableException $e) {
+ throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage());
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return mixed
+ */
+ public function getContentType() {
+ $mimeType = $this->info->getMimetype();
+
+ // PROPFIND needs to return the correct mime type, for consistency with the web UI
+ if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
+ return $mimeType;
+ }
+ return \OC_Helper::getSecureMimeType($mimeType);
+ }
+
+ /**
+ * @return array|false
+ */
+ public function getDirectDownload() {
+ if (\OCP\App::isEnabled('encryption')) {
+ return [];
+ }
+ /** @var \OCP\Files\Storage $storage */
+ list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
+ if (is_null($storage)) {
+ return [];
+ }
+
+ return $storage->getDirectDownload($internalPath);
+ }
+
+ /**
+ * @param resource $data
+ * @return null|string
+ * @throws Exception
+ * @throws BadRequest
+ * @throws NotImplemented
+ * @throws ServiceUnavailable
+ */
+ private function createFileChunked($data) {
+ list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($this->path);
+
+ $info = \OC_FileChunking::decodeName($name);
+ if (empty($info)) {
+ throw new NotImplemented('Invalid chunk name');
+ }
+ $chunk_handler = new \OC_FileChunking($info);
+ $bytesWritten = $chunk_handler->store($info['index'], $data);
+
+ //detect aborted upload
+ if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
+ if (isset($_SERVER['CONTENT_LENGTH'])) {
+ $expected = $_SERVER['CONTENT_LENGTH'];
+ if ($bytesWritten != $expected) {
+ $chunk_handler->remove($info['index']);
+ throw new BadRequest(
+ 'expected filesize ' . $expected . ' got ' . $bytesWritten);
+ }
+ }
+ }
+
+ if ($chunk_handler->isComplete()) {
+ list($storage,) = $this->fileView->resolvePath($path);
+ $needsPartFile = $this->needsPartFile($storage);
+ $partFile = null;
+
+ $targetPath = $path . '/' . $info['name'];
+ /** @var \OC\Files\Storage\Storage $targetStorage */
+ list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
+
+ $exists = $this->fileView->file_exists($targetPath);
+
+ try {
+ $this->emitPreHooks($exists, $targetPath);
+
+ $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
+
+ if ($needsPartFile) {
+ // we first assembly the target file as a part file
+ $partFile = $path . '/' . $info['name'] . '.ocTransferId' . $info['transferid'] . '.part';
+
+
+ list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile);
+
+
+ $chunk_handler->file_assemble($partStorage, $partInternalPath, $this->fileView->getAbsolutePath($targetPath));
+
+ // here is the final atomic rename
+ $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
+
+ $fileExists = $this->fileView->file_exists($targetPath);
+ if ($renameOkay === false || $fileExists === false) {
+ \OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::rename() failed', \OCP\Util::ERROR);
+ // only delete if an error occurred and the target file was already created
+ if ($fileExists) {
+ // set to null to avoid double-deletion when handling exception
+ // stray part file
+ $partFile = null;
+ $targetStorage->unlink($targetInternalPath);
+ }
+ $this->changeLock(ILockingProvider::LOCK_SHARED);
+ throw new Exception('Could not rename part file assembled from chunks');
+ }
+ } else {
+ // assemble directly into the final file
+ $chunk_handler->file_assemble($targetStorage, $targetInternalPath, $this->fileView->getAbsolutePath($targetPath));
+ }
+
+ // allow sync clients to send the mtime along in a header
+ $request = \OC::$server->getRequest();
+ if (isset($request->server['HTTP_X_OC_MTIME'])) {
+ if ($targetStorage->touch($targetInternalPath, $request->server['HTTP_X_OC_MTIME'])) {
+ header('X-OC-MTime: accepted');
+ }
+ }
+
+ $this->changeLock(ILockingProvider::LOCK_SHARED);
+
+ // since we skipped the view we need to scan and emit the hooks ourselves
+ $this->fileView->getUpdater()->update($targetPath);
+
+ $this->emitPostHooks($exists, $targetPath);
+
+ $info = $this->fileView->getFileInfo($targetPath);
+ return $info->getEtag();
+ } catch (\Exception $e) {
+ if ($partFile !== null) {
+ $targetStorage->unlink($targetInternalPath);
+ }
+ $this->convertToSabreException($e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether a part file is needed for the given storage
+ * or whether the file can be assembled/uploaded directly on the
+ * target storage.
+ *
+ * @param \OCP\Files\Storage $storage
+ * @return bool true if the storage needs part file handling
+ */
+ private function needsPartFile($storage) {
+ // TODO: in the future use ChunkHandler provided by storage
+ // and/or add method on Storage called "needsPartFile()"
+ return !$storage->instanceOfStorage('OCA\Files_Sharing\External\Storage') &&
+ !$storage->instanceOfStorage('OC\Files\Storage\OwnCloud');
+ }
+
+ /**
+ * Convert the given exception to a SabreException instance
+ *
+ * @param \Exception $e
+ *
+ * @throws \Sabre\DAV\Exception
+ */
+ private function convertToSabreException(\Exception $e) {
+ if ($e instanceof \Sabre\DAV\Exception) {
+ throw $e;
+ }
+ if ($e instanceof NotPermittedException) {
+ // a more general case - due to whatever reason the content could not be written
+ throw new Forbidden($e->getMessage(), 0, $e);
+ }
+ if ($e instanceof EntityTooLargeException) {
+ // the file is too big to be stored
+ throw new EntityTooLarge($e->getMessage(), 0, $e);
+ }
+ if ($e instanceof InvalidContentException) {
+ // the file content is not permitted
+ throw new UnsupportedMediaType($e->getMessage(), 0, $e);
+ }
+ if ($e instanceof InvalidPathException) {
+ // the path for the file was not valid
+ // TODO: find proper http status code for this case
+ throw new Forbidden($e->getMessage(), 0, $e);
+ }
+ if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
+ // the file is currently being written to by another process
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+ if ($e instanceof GenericEncryptionException) {
+ // returning 503 will allow retry of the operation at a later point in time
+ throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e);
+ }
+ if ($e instanceof StorageNotAvailableException) {
+ throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e);
+ }
+
+ throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/filesplugin.php b/apps/dav/lib/connector/sabre/filesplugin.php
new file mode 100644
index 00000000000..c9fc78a795f
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/filesplugin.php
@@ -0,0 +1,265 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use Sabre\DAV\IFile;
+use \Sabre\DAV\PropFind;
+use \Sabre\DAV\PropPatch;
+use \Sabre\HTTP\RequestInterface;
+use \Sabre\HTTP\ResponseInterface;
+
+class FilesPlugin extends \Sabre\DAV\ServerPlugin {
+
+ // namespace
+ const NS_OWNCLOUD = 'http://owncloud.org/ns';
+ const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
+ const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
+ const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
+ const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
+ const GETETAG_PROPERTYNAME = '{DAV:}getetag';
+ const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
+
+ /**
+ * Reference to main server object
+ *
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var \Sabre\DAV\Tree
+ */
+ private $tree;
+
+ /**
+ * Whether this is public webdav.
+ * If true, some returned information will be stripped off.
+ *
+ * @var bool
+ */
+ private $isPublic;
+
+ /**
+ * @var \OC\Files\View
+ */
+ private $fileView;
+
+ /**
+ * @param \Sabre\DAV\Tree $tree
+ * @param \OC\Files\View $view
+ * @param bool $isPublic
+ */
+ public function __construct(\Sabre\DAV\Tree $tree,
+ \OC\Files\View $view,
+ $isPublic = false) {
+ $this->tree = $tree;
+ $this->fileView = $view;
+ $this->isPublic = $isPublic;
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+
+ $server->xmlNamespaces[self::NS_OWNCLOUD] = 'oc';
+ $server->protectedProperties[] = self::FILEID_PROPERTYNAME;
+ $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
+ $server->protectedProperties[] = self::SIZE_PROPERTYNAME;
+ $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
+
+ // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
+ $allowedProperties = ['{DAV:}getetag'];
+ $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
+
+ $this->server = $server;
+ $this->server->on('propFind', array($this, 'handleGetProperties'));
+ $this->server->on('propPatch', array($this, 'handleUpdateProperties'));
+ $this->server->on('afterBind', array($this, 'sendFileIdHeader'));
+ $this->server->on('afterWriteContent', array($this, 'sendFileIdHeader'));
+ $this->server->on('afterMethod:GET', [$this,'httpGet']);
+ $this->server->on('afterResponse', function($request, ResponseInterface $response) {
+ $body = $response->getBody();
+ if (is_resource($body)) {
+ fclose($body);
+ }
+ });
+ $this->server->on('beforeMove', [$this, 'checkMove']);
+ }
+
+ /**
+ * Plugin that checks if a move can actually be performed.
+ * @param string $source source path
+ * @param string $destination destination path
+ * @throws \Sabre\DAV\Exception\Forbidden
+ */
+ function checkMove($source, $destination) {
+ list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($source);
+ list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination);
+
+ if ($sourceDir !== $destinationDir) {
+ $sourceFileInfo = $this->fileView->getFileInfo($source);
+
+ if (!$sourceFileInfo->isDeletable()) {
+ throw new \Sabre\DAV\Exception\Forbidden($source . " cannot be deleted");
+ }
+ }
+ }
+
+ /**
+ * Plugin that adds a 'Content-Disposition: attachment' header to all files
+ * delivered by SabreDAV.
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ */
+ function httpGet(RequestInterface $request, ResponseInterface $response) {
+ // Only handle valid files
+ $node = $this->tree->getNodeForPath($request->getPath());
+ if (!($node instanceof IFile)) return;
+
+ $response->addHeader('Content-Disposition', 'attachment');
+ }
+
+ /**
+ * Adds all ownCloud-specific properties
+ *
+ * @param PropFind $propFind
+ * @param \Sabre\DAV\INode $node
+ * @return void
+ */
+ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
+
+ if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
+
+ $propFind->handle(self::FILEID_PROPERTYNAME, function() use ($node) {
+ return $node->getFileId();
+ });
+
+ $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function() use ($node) {
+ $perms = $node->getDavPermissions();
+ if ($this->isPublic) {
+ // remove mount information
+ $perms = str_replace(['S', 'M'], '', $perms);
+ }
+ return $perms;
+ });
+
+ $propFind->handle(self::GETETAG_PROPERTYNAME, function() use ($node) {
+ return $node->getEtag();
+ });
+ }
+
+ if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
+ $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function() use ($node) {
+ /** @var $node \OCA\DAV\Connector\Sabre\File */
+ $directDownloadUrl = $node->getDirectDownload();
+ if (isset($directDownloadUrl['url'])) {
+ return $directDownloadUrl['url'];
+ }
+ return false;
+ });
+ }
+
+ if ($node instanceof \OCA\DAV\Connector\Sabre\Directory) {
+ $propFind->handle(self::SIZE_PROPERTYNAME, function() use ($node) {
+ return $node->getSize();
+ });
+ }
+ }
+
+ /**
+ * Update ownCloud-specific properties
+ *
+ * @param string $path
+ * @param PropPatch $propPatch
+ *
+ * @return void
+ */
+ public function handleUpdateProperties($path, PropPatch $propPatch) {
+ $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function($time) use ($path) {
+ if (empty($time)) {
+ return false;
+ }
+ $node = $this->tree->getNodeForPath($path);
+ if (is_null($node)) {
+ return 404;
+ }
+ $node->touch($time);
+ return true;
+ });
+ $propPatch->handle(self::GETETAG_PROPERTYNAME, function($etag) use ($path) {
+ if (empty($etag)) {
+ return false;
+ }
+ $node = $this->tree->getNodeForPath($path);
+ if (is_null($node)) {
+ return 404;
+ }
+ if ($node->setEtag($etag) !== -1) {
+ return true;
+ }
+ return false;
+ });
+ }
+
+ /**
+ * @param string $filePath
+ * @param \Sabre\DAV\INode $node
+ * @throws \Sabre\DAV\Exception\BadRequest
+ */
+ public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
+ // chunked upload handling
+ if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($filePath);
+ $info = \OC_FileChunking::decodeName($name);
+ if (!empty($info)) {
+ $filePath = $path . '/' . $info['name'];
+ }
+ }
+
+ // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
+ if (!$this->server->tree->nodeExists($filePath)) {
+ return;
+ }
+ $node = $this->server->tree->getNodeForPath($filePath);
+ if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
+ $fileId = $node->getFileId();
+ if (!is_null($fileId)) {
+ $this->server->httpResponse->setHeader('OC-FileId', $fileId);
+ }
+ }
+ }
+
+}
diff --git a/apps/dav/lib/connector/sabre/listenerplugin.php b/apps/dav/lib/connector/sabre/listenerplugin.php
new file mode 100644
index 00000000000..d537d0577c6
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/listenerplugin.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @author Joas Schilling <nickvergessen@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCP\AppFramework\Http;
+use OCP\SabrePluginEvent;
+use OCP\SabrePluginException;
+use Sabre\DAV\ServerPlugin;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+class ListenerPlugin extends ServerPlugin {
+ /** @var EventDispatcherInterface */
+ protected $dispatcher;
+
+ /**
+ * @param EventDispatcherInterface $dispatcher
+ */
+ public function __construct(EventDispatcherInterface $dispatcher) {
+ $this->dispatcher = $dispatcher;
+ }
+
+ /**
+ * This initialize the plugin
+ *
+ * @param \Sabre\DAV\Server $server
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+ $server->on('beforeMethod', array($this, 'emitListener'), 15);
+ }
+
+ /**
+ * This method is called before any HTTP method and returns http status code 503
+ * in case the system is in maintenance mode.
+ *
+ * @return bool
+ * @throws \Exception
+ */
+ public function emitListener() {
+ $event = new SabrePluginEvent();
+
+ $this->dispatcher->dispatch('OCA\DAV\Connector\Sabre::beforeMethod', $event);
+
+ if ($event->getStatusCode() !== Http::STATUS_OK) {
+ throw new SabrePluginException($event->getMessage(), $event->getStatusCode());
+ }
+
+ return true;
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/lockplugin.php b/apps/dav/lib/connector/sabre/lockplugin.php
new file mode 100644
index 00000000000..c564e066f8e
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/lockplugin.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * @author Robin Appelman <icewind@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\Exception\FileLocked;
+use OCA\DAV\Connector\Sabre\Node;
+use OCP\Lock\ILockingProvider;
+use OCP\Lock\LockedException;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+
+class LockPlugin extends ServerPlugin {
+ /**
+ * Reference to main server object
+ *
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var \Sabre\DAV\Tree
+ */
+ private $tree;
+
+ /**
+ * @param \Sabre\DAV\Tree $tree tree
+ */
+ public function __construct(Tree $tree) {
+ $this->tree = $tree;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+ $this->server = $server;
+ $this->server->on('beforeMethod', [$this, 'getLock'], 50);
+ $this->server->on('afterMethod', [$this, 'releaseLock'], 50);
+ }
+
+ public function getLock(RequestInterface $request) {
+ // we cant listen on 'beforeMethod:PUT' due to order of operations with setting up the tree
+ // so instead we limit ourselves to the PUT method manually
+ if ($request->getMethod() !== 'PUT') {
+ return;
+ }
+ try {
+ $node = $this->tree->getNodeForPath($request->getPath());
+ } catch (NotFound $e) {
+ return;
+ }
+ if ($node instanceof Node) {
+ try {
+ $node->acquireLock(ILockingProvider::LOCK_SHARED);
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+ }
+
+ public function releaseLock(RequestInterface $request) {
+ if ($request->getMethod() !== 'PUT') {
+ return;
+ }
+ try {
+ $node = $this->tree->getNodeForPath($request->getPath());
+ } catch (NotFound $e) {
+ return;
+ }
+ if ($node instanceof Node) {
+ $node->releaseLock(ILockingProvider::LOCK_SHARED);
+ }
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/maintenanceplugin.php b/apps/dav/lib/connector/sabre/maintenanceplugin.php
new file mode 100644
index 00000000000..b9b261fbe05
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/maintenanceplugin.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Joas Schilling <nickvergessen@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCP\IConfig;
+use Sabre\DAV\Exception\ServiceUnavailable;
+use Sabre\DAV\ServerPlugin;
+
+class MaintenancePlugin extends ServerPlugin {
+
+ /** @var IConfig */
+ private $config;
+
+ /**
+ * Reference to main server object
+ *
+ * @var Server
+ */
+ private $server;
+
+ /**
+ * @param IConfig $config
+ */
+ public function __construct(IConfig $config = null) {
+ $this->config = $config;
+ if (is_null($config)) {
+ $this->config = \OC::$server->getConfig();
+ }
+ }
+
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+ $this->server = $server;
+ $this->server->on('beforeMethod', array($this, 'checkMaintenanceMode'), 1);
+ }
+
+ /**
+ * This method is called before any HTTP method and returns http status code 503
+ * in case the system is in maintenance mode.
+ *
+ * @throws ServiceUnavailable
+ * @return bool
+ */
+ public function checkMaintenanceMode() {
+ if ($this->config->getSystemValue('singleuser', false)) {
+ throw new ServiceUnavailable('System in single user mode.');
+ }
+ if ($this->config->getSystemValue('maintenance', false)) {
+ throw new ServiceUnavailable('System in maintenance mode.');
+ }
+ if (\OC::checkUpgrade(false)) {
+ throw new ServiceUnavailable('Upgrade needed');
+ }
+
+ return true;
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/node.php b/apps/dav/lib/connector/sabre/node.php
new file mode 100644
index 00000000000..814aaceb077
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/node.php
@@ -0,0 +1,270 @@
+<?php
+/**
+ * @author Arthur Schiwon <blizzz@owncloud.com>
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Jakob Sack <mail@jakobsack.de>
+ * @author Jörn Friedrich Dreyer <jfd@butonic.de>
+ * @author Klaas Freitag <freitag@owncloud.com>
+ * @author Markus Goetz <markus@woboq.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+
+
+abstract class Node implements \Sabre\DAV\INode {
+
+ /**
+ * @var \OC\Files\View
+ */
+ protected $fileView;
+
+ /**
+ * The path to the current node
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * node properties cache
+ *
+ * @var array
+ */
+ protected $property_cache = null;
+
+ /**
+ * @var \OCP\Files\FileInfo
+ */
+ protected $info;
+
+ /**
+ * Sets up the node, expects a full path name
+ *
+ * @param \OC\Files\View $view
+ * @param \OCP\Files\FileInfo $info
+ */
+ public function __construct($view, $info) {
+ $this->fileView = $view;
+ $this->path = $this->fileView->getRelativePath($info->getPath());
+ $this->info = $info;
+ }
+
+ protected function refreshInfo() {
+ $this->info = $this->fileView->getFileInfo($this->path);
+ }
+
+ /**
+ * Returns the name of the node
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->info->getName();
+ }
+
+ /**
+ * Returns the full path
+ *
+ * @return string
+ */
+ public function getPath() {
+ return $this->path;
+ }
+
+ /**
+ * Renames the node
+ *
+ * @param string $name The new name
+ * @throws \Sabre\DAV\Exception\BadRequest
+ * @throws \Sabre\DAV\Exception\Forbidden
+ */
+ public function setName($name) {
+
+ // rename is only allowed if the update privilege is granted
+ if (!$this->info->isUpdateable()) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+
+ list($parentPath,) = \Sabre\HTTP\URLUtil::splitPath($this->path);
+ list(, $newName) = \Sabre\HTTP\URLUtil::splitPath($name);
+
+ // verify path of the target
+ $this->verifyPath();
+
+ $newPath = $parentPath . '/' . $newName;
+
+ $this->fileView->rename($this->path, $newPath);
+
+ $this->path = $newPath;
+
+ $this->refreshInfo();
+ }
+
+ public function setPropertyCache($property_cache) {
+ $this->property_cache = $property_cache;
+ }
+
+ /**
+ * Returns the last modification time, as a unix timestamp
+ *
+ * @return int timestamp as integer
+ */
+ public function getLastModified() {
+ $timestamp = $this->info->getMtime();
+ if (!empty($timestamp)) {
+ return (int)$timestamp;
+ }
+ return $timestamp;
+ }
+
+ /**
+ * sets the last modification time of the file (mtime) to the value given
+ * in the second parameter or to now if the second param is empty.
+ * Even if the modification time is set to a custom value the access time is set to now.
+ */
+ public function touch($mtime) {
+ $this->fileView->touch($this->path, $mtime);
+ $this->refreshInfo();
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the
+ * file. If the file changes, the ETag MUST change. The ETag is an
+ * arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return string
+ */
+ public function getETag() {
+ return '"' . $this->info->getEtag() . '"';
+ }
+
+ /**
+ * Sets the ETag
+ *
+ * @param string $etag
+ *
+ * @return int file id of updated file or -1 on failure
+ */
+ public function setETag($etag) {
+ return $this->fileView->putFileInfo($this->path, array('etag' => $etag));
+ }
+
+ /**
+ * Returns the size of the node, in bytes
+ *
+ * @return int|float
+ */
+ public function getSize() {
+ return $this->info->getSize();
+ }
+
+ /**
+ * Returns the cache's file id
+ *
+ * @return int
+ */
+ public function getId() {
+ return $this->info->getId();
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getFileId() {
+ if ($this->info->getId()) {
+ $instanceId = \OC_Util::getInstanceId();
+ $id = sprintf('%08d', $this->info->getId());
+ return $id . $instanceId;
+ }
+
+ return null;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getDavPermissions() {
+ $p = '';
+ if ($this->info->isShared()) {
+ $p .= 'S';
+ }
+ if ($this->info->isShareable()) {
+ $p .= 'R';
+ }
+ if ($this->info->isMounted()) {
+ $p .= 'M';
+ }
+ if ($this->info->isDeletable()) {
+ $p .= 'D';
+ }
+ if ($this->info->isDeletable()) {
+ $p .= 'NV'; // Renameable, Moveable
+ }
+ if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
+ if ($this->info->isUpdateable()) {
+ $p .= 'W';
+ }
+ } else {
+ if ($this->info->isCreatable()) {
+ $p .= 'CK';
+ }
+ }
+ return $p;
+ }
+
+ protected function verifyPath() {
+ try {
+ $fileName = basename($this->info->getPath());
+ $this->fileView->verifyPath($this->path, $fileName);
+ } catch (\OCP\Files\InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ }
+ }
+
+ /**
+ * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
+ */
+ public function acquireLock($type) {
+ $this->fileView->lockFile($this->path, $type);
+ }
+
+ /**
+ * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
+ */
+ public function releaseLock($type) {
+ $this->fileView->unlockFile($this->path, $type);
+ }
+
+ /**
+ * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
+ */
+ public function changeLock($type) {
+ $this->fileView->changeLock($this->path, $type);
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/objecttree.php b/apps/dav/lib/connector/sabre/objecttree.php
new file mode 100644
index 00000000000..80c0ef74610
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/objecttree.php
@@ -0,0 +1,284 @@
+<?php
+/**
+ * @author Björn Schießle <schiessle@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Connector\Sabre\Exception\FileLocked;
+use OC\Files\FileInfo;
+use OC\Files\Mount\MoveableMount;
+use OCP\Files\StorageInvalidException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\Lock\LockedException;
+
+class ObjectTree extends \Sabre\DAV\Tree {
+
+ /**
+ * @var \OC\Files\View
+ */
+ protected $fileView;
+
+ /**
+ * @var \OCP\Files\Mount\IMountManager
+ */
+ protected $mountManager;
+
+ /**
+ * Creates the object
+ */
+ public function __construct() {
+ }
+
+ /**
+ * @param \Sabre\DAV\INode $rootNode
+ * @param \OC\Files\View $view
+ * @param \OCP\Files\Mount\IMountManager $mountManager
+ */
+ public function init(\Sabre\DAV\INode $rootNode, \OC\Files\View $view, \OCP\Files\Mount\IMountManager $mountManager) {
+ $this->rootNode = $rootNode;
+ $this->fileView = $view;
+ $this->mountManager = $mountManager;
+ }
+
+ /**
+ * If the given path is a chunked file name, converts it
+ * to the real file name. Only applies if the OC-CHUNKED header
+ * is present.
+ *
+ * @param string $path chunk file path to convert
+ *
+ * @return string path to real file
+ */
+ private function resolveChunkFile($path) {
+ if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ // resolve to real file name to find the proper node
+ list($dir, $name) = \Sabre\HTTP\URLUtil::splitPath($path);
+ if ($dir == '/' || $dir == '.') {
+ $dir = '';
+ }
+
+ $info = \OC_FileChunking::decodeName($name);
+ // only replace path if it was really the chunked file
+ if (isset($info['transferid'])) {
+ // getNodePath is called for multiple nodes within a chunk
+ // upload call
+ $path = $dir . '/' . $info['name'];
+ $path = ltrim($path, '/');
+ }
+ }
+ return $path;
+ }
+
+ /**
+ * Returns the INode object for the requested path
+ *
+ * @param string $path
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
+ * @throws \Sabre\DAV\Exception\NotFound
+ * @return \Sabre\DAV\INode
+ */
+ public function getNodeForPath($path) {
+ if (!$this->fileView) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup');
+ }
+
+ $path = trim($path, '/');
+ if ($path) {
+ try {
+ $this->fileView->verifyPath($path, basename($path));
+ } catch (\OCP\Files\InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ }
+ }
+
+ if (isset($this->cache[$path])) {
+ return $this->cache[$path];
+ }
+
+ // Is it the root node?
+ if (!strlen($path)) {
+ return $this->rootNode;
+ }
+
+ if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
+ // read from storage
+ $absPath = $this->fileView->getAbsolutePath($path);
+ $mount = $this->fileView->getMount($path);
+ $storage = $mount->getStorage();
+ $internalPath = $mount->getInternalPath($absPath);
+ if ($storage) {
+ /**
+ * @var \OC\Files\Storage\Storage $storage
+ */
+ $scanner = $storage->getScanner($internalPath);
+ // get data directly
+ $data = $scanner->getData($internalPath);
+ $info = new FileInfo($absPath, $storage, $internalPath, $data, $mount);
+ } else {
+ $info = null;
+ }
+ } else {
+ // resolve chunk file name to real name, if applicable
+ $path = $this->resolveChunkFile($path);
+
+ // read from cache
+ try {
+ $info = $this->fileView->getFileInfo($path);
+ } catch (StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable('Storage not available');
+ } catch (StorageInvalidException $e) {
+ throw new \Sabre\DAV\Exception\NotFound('Storage ' . $path . ' is invalid');
+ } catch (LockedException $e) {
+ throw new \Sabre\DAV\Exception\Locked();
+ }
+ }
+
+ if (!$info) {
+ throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
+ }
+
+ if ($info->getType() === 'dir') {
+ $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info);
+ } else {
+ $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
+ }
+
+ $this->cache[$path] = $node;
+ return $node;
+
+ }
+
+ /**
+ * Moves a file from one location to another
+ *
+ * @param string $sourcePath The path to the file which should be moved
+ * @param string $destinationPath The full destination path, so not just the destination parent node
+ * @throws \Sabre\DAV\Exception\BadRequest
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
+ * @throws \Sabre\DAV\Exception\Forbidden
+ * @return int
+ */
+ public function move($sourcePath, $destinationPath) {
+ if (!$this->fileView) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup');
+ }
+
+ $targetNodeExists = $this->nodeExists($destinationPath);
+ $sourceNode = $this->getNodeForPath($sourcePath);
+ if ($sourceNode instanceof \Sabre\DAV\ICollection && $targetNodeExists) {
+ throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode . ', target exists');
+ }
+ list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($sourcePath);
+ list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destinationPath);
+
+ $isMovableMount = false;
+ $sourceMount = $this->mountManager->find($this->fileView->getAbsolutePath($sourcePath));
+ $internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
+ if ($sourceMount instanceof MoveableMount && $internalPath === '') {
+ $isMovableMount = true;
+ }
+
+ try {
+ $sameFolder = ($sourceDir === $destinationDir);
+ // if we're overwriting or same folder
+ if ($targetNodeExists || $sameFolder) {
+ // note that renaming a share mount point is always allowed
+ if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+ } else {
+ if (!$this->fileView->isCreatable($destinationDir)) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+ }
+
+ if (!$sameFolder) {
+ // moving to a different folder, source will be gone, like a deletion
+ // note that moving a share mount point is always allowed
+ if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+ }
+
+ $fileName = basename($destinationPath);
+ try {
+ $this->fileView->verifyPath($destinationDir, $fileName);
+ } catch (\OCP\Files\InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ }
+
+ $renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
+ if (!$renameOkay) {
+ throw new \Sabre\DAV\Exception\Forbidden('');
+ }
+ } catch (StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+
+ $this->markDirty($sourceDir);
+ $this->markDirty($destinationDir);
+
+ }
+
+ /**
+ * Copies a file or directory.
+ *
+ * This method must work recursively and delete the destination
+ * if it exists
+ *
+ * @param string $source
+ * @param string $destination
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
+ * @return void
+ */
+ public function copy($source, $destination) {
+ if (!$this->fileView) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup');
+ }
+
+ // this will trigger existence check
+ $this->getNodeForPath($source);
+
+ list($destinationDir, $destinationName) = \Sabre\HTTP\URLUtil::splitPath($destination);
+ try {
+ $this->fileView->verifyPath($destinationDir, $destinationName);
+ } catch (\OCP\Files\InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ }
+
+ try {
+ $this->fileView->copy($source, $destination);
+ } catch (StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
+
+ list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination);
+ $this->markDirty($destinationDir);
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/principal.php b/apps/dav/lib/connector/sabre/principal.php
new file mode 100644
index 00000000000..35215e1f63c
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/principal.php
@@ -0,0 +1,205 @@
+<?php
+/**
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Felix Moeller <mail@felixmoeller.de>
+ * @author Jakob Sack <mail@jakobsack.de>
+ * @author Jörn Friedrich Dreyer <jfd@butonic.de>
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Sebastian Döll <sebastian.doell@libasys.de>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Thomas Tanghus <thomas@tanghus.net>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCP\IUserManager;
+use OCP\IConfig;
+use \Sabre\DAV\PropPatch;
+
+class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface {
+ /** @var IConfig */
+ private $config;
+ /** @var IUserManager */
+ private $userManager;
+
+ /**
+ * @param IConfig $config
+ * @param IUserManager $userManager
+ */
+ public function __construct(IConfig $config,
+ IUserManager $userManager) {
+ $this->config = $config;
+ $this->userManager = $userManager;
+ }
+
+ /**
+ * Returns a list of principals based on a prefix.
+ *
+ * This prefix will often contain something like 'principals'. You are only
+ * expected to return principals that are in this base path.
+ *
+ * You are expected to return at least a 'uri' for every user, you can
+ * return any additional properties if you wish so. Common properties are:
+ * {DAV:}displayname
+ *
+ * @param string $prefixPath
+ * @return string[]
+ */
+ public function getPrincipalsByPrefix($prefixPath) {
+ $principals = [];
+
+ if ($prefixPath === 'principals') {
+ foreach($this->userManager->search('') as $user) {
+
+ $principal = [
+ 'uri' => 'principals/' . $user->getUID(),
+ '{DAV:}displayname' => $user->getUID(),
+ ];
+
+ $email = $this->config->getUserValue($user->getUID(), 'settings', 'email');
+ if(!empty($email)) {
+ $principal['{http://sabredav.org/ns}email-address'] = $email;
+ }
+
+ $principals[] = $principal;
+ }
+ }
+
+ return $principals;
+ }
+
+ /**
+ * Returns a specific principal, specified by it's path.
+ * The returned structure should be the exact same as from
+ * getPrincipalsByPrefix.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getPrincipalByPath($path) {
+ list($prefix, $name) = explode('/', $path);
+ $user = $this->userManager->get($name);
+
+ if ($prefix === 'principals' && !is_null($user)) {
+ $principal = [
+ 'uri' => 'principals/' . $user->getUID(),
+ '{DAV:}displayname' => $user->getUID(),
+ ];
+
+ $email = $this->config->getUserValue($user->getUID(), 'settings', 'email');
+ if($email) {
+ $principal['{http://sabredav.org/ns}email-address'] = $email;
+ }
+
+ return $principal;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the list of members for a group-principal
+ *
+ * @param string $principal
+ * @return string[]
+ * @throws \Sabre\DAV\Exception
+ */
+ public function getGroupMemberSet($principal) {
+ // TODO: for now the group principal has only one member, the user itself
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) {
+ throw new \Sabre\DAV\Exception('Principal not found');
+ }
+
+ return [$principal['uri']];
+ }
+
+ /**
+ * Returns the list of groups a principal is a member of
+ *
+ * @param string $principal
+ * @return array
+ * @throws \Sabre\DAV\Exception
+ */
+ public function getGroupMembership($principal) {
+ list($prefix, $name) = \Sabre\HTTP\URLUtil::splitPath($principal);
+
+ $group_membership = array();
+ if ($prefix === 'principals') {
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) {
+ throw new \Sabre\DAV\Exception('Principal not found');
+ }
+
+ // TODO: for now the user principal has only its own groups
+ return array(
+ 'principals/'.$name.'/calendar-proxy-read',
+ 'principals/'.$name.'/calendar-proxy-write',
+ // The addressbook groups are not supported in Sabre,
+ // see http://groups.google.com/group/sabredav-discuss/browse_thread/thread/ef2fa9759d55f8c#msg_5720afc11602e753
+ //'principals/'.$name.'/addressbook-proxy-read',
+ //'principals/'.$name.'/addressbook-proxy-write',
+ );
+ }
+ return $group_membership;
+ }
+
+ /**
+ * Updates the list of group members for a group principal.
+ *
+ * The principals should be passed as a list of uri's.
+ *
+ * @param string $principal
+ * @param array $members
+ * @throws \Sabre\DAV\Exception
+ */
+ public function setGroupMemberSet($principal, array $members) {
+ throw new \Sabre\DAV\Exception('Setting members of the group is not supported yet');
+ }
+
+ /**
+ * @param string $path
+ * @param PropPatch $propPatch
+ * @return int
+ */
+ function updatePrincipal($path, PropPatch $propPatch) {
+ return 0;
+ }
+
+ /**
+ * @param string $prefixPath
+ * @param array $searchProperties
+ * @param string $test
+ * @return array
+ */
+ function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
+ return [];
+ }
+
+ /**
+ * @param string $uri
+ * @param string $principalPrefix
+ * @return string
+ */
+ function findByUri($uri, $principalPrefix) {
+ return '';
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/quotaplugin.php b/apps/dav/lib/connector/sabre/quotaplugin.php
new file mode 100644
index 00000000000..8340d489dc0
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/quotaplugin.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * @author Felix Moeller <mail@felixmoeller.de>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
+ * @author scambra <sergio@entrecables.com>
+ * @author Scrutinizer Auto-Fixer <auto-fixer@scrutinizer-ci.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+/**
+ * This plugin check user quota and deny creating files when they exceeds the quota.
+ *
+ * @author Sergio Cambra
+ * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
+
+ /**
+ * @var \OC\Files\View
+ */
+ private $view;
+
+ /**
+ * Reference to main server object
+ *
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @param \OC\Files\View $view
+ */
+ public function __construct($view) {
+ $this->view = $view;
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the requires event subscriptions.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+
+ $this->server = $server;
+
+ $server->on('beforeWriteContent', array($this, 'checkQuota'), 10);
+ $server->on('beforeCreateFile', array($this, 'checkQuota'), 10);
+ }
+
+ /**
+ * This method is called before any HTTP method and validates there is enough free space to store the file
+ *
+ * @param string $uri
+ * @param null $data
+ * @throws \Sabre\DAV\Exception\InsufficientStorage
+ * @return bool
+ */
+ public function checkQuota($uri, $data = null) {
+ $length = $this->getLength();
+ if ($length) {
+ if (substr($uri, 0, 1) !== '/') {
+ $uri = '/' . $uri;
+ }
+ list($parentUri, $newName) = \Sabre\HTTP\URLUtil::splitPath($uri);
+ if(is_null($parentUri)) {
+ $parentUri = '';
+ }
+ $req = $this->server->httpRequest;
+ if ($req->getHeader('OC-Chunked')) {
+ $info = \OC_FileChunking::decodeName($newName);
+ $chunkHandler = new \OC_FileChunking($info);
+ // subtract the already uploaded size to see whether
+ // there is still enough space for the remaining chunks
+ $length -= $chunkHandler->getCurrentSize();
+ }
+ $freeSpace = $this->getFreeSpace($parentUri);
+ if ($freeSpace !== \OCP\Files\FileInfo::SPACE_UNKNOWN && $length > $freeSpace) {
+ if (isset($chunkHandler)) {
+ $chunkHandler->cleanup();
+ }
+ throw new \Sabre\DAV\Exception\InsufficientStorage();
+ }
+ }
+ return true;
+ }
+
+ public function getLength() {
+ $req = $this->server->httpRequest;
+ $length = $req->getHeader('X-Expected-Entity-Length');
+ if (!$length) {
+ $length = $req->getHeader('Content-Length');
+ }
+
+ $ocLength = $req->getHeader('OC-Total-Length');
+ if ($length && $ocLength) {
+ return max($length, $ocLength);
+ }
+
+ return $length;
+ }
+
+ /**
+ * @param string $parentUri
+ * @return mixed
+ */
+ public function getFreeSpace($parentUri) {
+ try {
+ $freeSpace = $this->view->free_space($parentUri);
+ return $freeSpace;
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ }
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/server.php b/apps/dav/lib/connector/sabre/server.php
new file mode 100644
index 00000000000..eafe1b537f8
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/server.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author scolebrook <scolebrook@mac.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+/**
+ * Class \OCA\DAV\Connector\Sabre\Server
+ *
+ * This class overrides some methods from @see \Sabre\DAV\Server.
+ *
+ * @see \Sabre\DAV\Server
+ */
+class Server extends \Sabre\DAV\Server {
+
+ /**
+ * @see \Sabre\DAV\Server
+ */
+ public function __construct($treeOrNode = null) {
+ parent::__construct($treeOrNode);
+ self::$exposeVersion = false;
+ $this->enablePropfindDepthInfinity = true;
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/serverfactory.php b/apps/dav/lib/connector/sabre/serverfactory.php
new file mode 100644
index 00000000000..b62f90ab802
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/serverfactory.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * @author Joas Schilling <nickvergessen@owncloud.com>
+ * @author Robin Appelman <icewind@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use OCP\Files\Mount\IMountManager;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\ILogger;
+use OCP\ITagManager;
+use OCP\IUserSession;
+use Sabre\DAV\Auth\Backend\BackendInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+class ServerFactory {
+ public function __construct(
+ IConfig $config,
+ ILogger $logger,
+ IDBConnection $databaseConnection,
+ IUserSession $userSession,
+ IMountManager $mountManager,
+ ITagManager $tagManager,
+ EventDispatcherInterface $dispatcher
+ ) {
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->databaseConnection = $databaseConnection;
+ $this->userSession = $userSession;
+ $this->mountManager = $mountManager;
+ $this->tagManager = $tagManager;
+ $this->dispatcher = $dispatcher;
+ }
+
+ /**
+ * @param string $baseUri
+ * @param string $requestUri
+ * @param BackendInterface $authBackend
+ * @param callable $viewCallBack callback that should return the view for the dav endpoint
+ * @return Server
+ */
+ public function createServer($baseUri, $requestUri, BackendInterface $authBackend, callable $viewCallBack) {
+ // Fire up server
+ $objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree();
+ $server = new \OCA\DAV\Connector\Sabre\Server($objectTree);
+ // Set URL explicitly due to reverse-proxy situations
+ $server->httpRequest->setUrl($requestUri);
+ $server->setBaseUri($baseUri);
+
+ // Load plugins
+ $defaults = new \OC_Defaults();
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config));
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config));
+ $server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, $defaults->getName()));
+ // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin());
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger));
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin($objectTree));
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\ListenerPlugin($this->dispatcher));
+
+ // wait with registering these until auth is handled and the filesystem is setup
+ $server->on('beforeMethod', function () use ($server, $objectTree, $viewCallBack) {
+ /** @var \OC\Files\View $view */
+ $view = $viewCallBack();
+ $rootInfo = $view->getFileInfo('');
+
+ // Create ownCloud Dir
+ if ($rootInfo->getType() === 'dir') {
+ $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo);
+ } else {
+ $root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo);
+ }
+ $objectTree->init($root, $view, $this->mountManager);
+
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesPlugin($objectTree, $view));
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view));
+
+ if($this->userSession->isLoggedIn()) {
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager));
+ // custom properties plugin must be the last one
+ $server->addPlugin(
+ new \Sabre\DAV\PropertyStorage\Plugin(
+ new \OCA\DAV\Connector\Sabre\CustomPropertiesBackend(
+ $objectTree,
+ $this->databaseConnection,
+ $this->userSession->getUser()
+ )
+ )
+ );
+ }
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin());
+ }, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request
+ return $server;
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/taglist.php b/apps/dav/lib/connector/sabre/taglist.php
new file mode 100644
index 00000000000..177cc23e805
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/taglist.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use Sabre\DAV;
+
+/**
+ * TagList property
+ *
+ * This property contains multiple "tag" elements, each containing a tag name.
+ */
+class TagList extends DAV\Property {
+ const NS_OWNCLOUD = 'http://owncloud.org/ns';
+
+ /**
+ * tags
+ *
+ * @var array
+ */
+ private $tags;
+
+ /**
+ * @param array $tags
+ */
+ public function __construct(array $tags) {
+ $this->tags = $tags;
+ }
+
+ /**
+ * Returns the tags
+ *
+ * @return array
+ */
+ public function getTags() {
+
+ return $this->tags;
+
+ }
+
+ /**
+ * Serializes this property.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $dom) {
+
+ $prefix = $server->xmlNamespaces[self::NS_OWNCLOUD];
+
+ foreach($this->tags as $tag) {
+
+ $elem = $dom->ownerDocument->createElement($prefix . ':tag');
+ $elem->appendChild($dom->ownerDocument->createTextNode($tag));
+
+ $dom->appendChild($elem);
+ }
+
+ }
+
+ /**
+ * Unserializes this property from a DOM Element
+ *
+ * This method returns an instance of this class.
+ * It will only decode tag values.
+ *
+ * @param \DOMElement $dom
+ * @param array $propertyMap
+ * @return \OCA\DAV\Connector\Sabre\TagList
+ */
+ static function unserialize(\DOMElement $dom, array $propertyMap) {
+
+ $tags = array();
+ foreach($dom->childNodes as $child) {
+ if (DAV\XMLUtil::toClarkNotation($child)==='{' . self::NS_OWNCLOUD . '}tag') {
+ $tags[] = $child->textContent;
+ }
+ }
+ return new self($tags);
+
+ }
+
+}
diff --git a/apps/dav/lib/connector/sabre/tagsplugin.php b/apps/dav/lib/connector/sabre/tagsplugin.php
new file mode 100644
index 00000000000..7446d97790b
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/tagsplugin.php
@@ -0,0 +1,292 @@
+<?php
+/**
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+use \Sabre\DAV\PropFind;
+use \Sabre\DAV\PropPatch;
+
+class TagsPlugin extends \Sabre\DAV\ServerPlugin
+{
+
+ // namespace
+ const NS_OWNCLOUD = 'http://owncloud.org/ns';
+ const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags';
+ const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite';
+ const TAG_FAVORITE = '_$!<Favorite>!$_';
+
+ /**
+ * Reference to main server object
+ *
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var \OCP\ITagManager
+ */
+ private $tagManager;
+
+ /**
+ * @var \OCP\ITags
+ */
+ private $tagger;
+
+ /**
+ * Array of file id to tags array
+ * The null value means the cache wasn't initialized.
+ *
+ * @var array
+ */
+ private $cachedTags;
+
+ /**
+ * @var \Sabre\DAV\Tree
+ */
+ private $tree;
+
+ /**
+ * @param \Sabre\DAV\Tree $tree tree
+ * @param \OCP\ITagManager $tagManager tag manager
+ */
+ public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) {
+ $this->tree = $tree;
+ $this->tagManager = $tagManager;
+ $this->tagger = null;
+ $this->cachedTags = array();
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(\Sabre\DAV\Server $server) {
+
+ $server->xmlNamespaces[self::NS_OWNCLOUD] = 'oc';
+ $server->propertyMap[self::TAGS_PROPERTYNAME] = 'OC\\Connector\\Sabre\\TagList';
+
+ $this->server = $server;
+ $this->server->on('propFind', array($this, 'handleGetProperties'));
+ $this->server->on('propPatch', array($this, 'handleUpdateProperties'));
+ }
+
+ /**
+ * Returns the tagger
+ *
+ * @return \OCP\ITags tagger
+ */
+ private function getTagger() {
+ if (!$this->tagger) {
+ $this->tagger = $this->tagManager->load('files');
+ }
+ return $this->tagger;
+ }
+
+ /**
+ * Returns tags and favorites.
+ *
+ * @param integer $fileId file id
+ * @return array list($tags, $favorite) with $tags as tag array
+ * and $favorite is a boolean whether the file was favorited
+ */
+ private function getTagsAndFav($fileId) {
+ $isFav = false;
+ $tags = $this->getTags($fileId);
+ if ($tags) {
+ $favPos = array_search(self::TAG_FAVORITE, $tags);
+ if ($favPos !== false) {
+ $isFav = true;
+ unset($tags[$favPos]);
+ }
+ }
+ return array($tags, $isFav);
+ }
+
+ /**
+ * Returns tags for the given file id
+ *
+ * @param integer $fileId file id
+ * @return array list of tags for that file
+ */
+ private function getTags($fileId) {
+ if (isset($this->cachedTags[$fileId])) {
+ return $this->cachedTags[$fileId];
+ } else {
+ $tags = $this->getTagger()->getTagsForObjects(array($fileId));
+ if ($tags !== false) {
+ if (empty($tags)) {
+ return array();
+ }
+ return current($tags);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates the tags of the given file id
+ *
+ * @param int $fileId
+ * @param array $tags array of tag strings
+ */
+ private function updateTags($fileId, $tags) {
+ $tagger = $this->getTagger();
+ $currentTags = $this->getTags($fileId);
+
+ $newTags = array_diff($tags, $currentTags);
+ foreach ($newTags as $tag) {
+ if ($tag === self::TAG_FAVORITE) {
+ continue;
+ }
+ $tagger->tagAs($fileId, $tag);
+ }
+ $deletedTags = array_diff($currentTags, $tags);
+ foreach ($deletedTags as $tag) {
+ if ($tag === self::TAG_FAVORITE) {
+ continue;
+ }
+ $tagger->unTag($fileId, $tag);
+ }
+ }
+
+ /**
+ * Adds tags and favorites properties to the response,
+ * if requested.
+ *
+ * @param PropFind $propFind
+ * @param \Sabre\DAV\INode $node
+ * @return void
+ */
+ public function handleGetProperties(
+ PropFind $propFind,
+ \Sabre\DAV\INode $node
+ ) {
+ if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
+ return;
+ }
+
+ // need prefetch ?
+ if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
+ && $propFind->getDepth() !== 0
+ && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
+ || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
+ )) {
+ // note: pre-fetching only supported for depth <= 1
+ $folderContent = $node->getChildren();
+ $fileIds[] = (int)$node->getId();
+ foreach ($folderContent as $info) {
+ $fileIds[] = (int)$info->getId();
+ }
+ $tags = $this->getTagger()->getTagsForObjects($fileIds);
+ if ($tags === false) {
+ // the tags API returns false on error...
+ $tags = array();
+ }
+
+ $this->cachedTags = $this->cachedTags + $tags;
+ $emptyFileIds = array_diff($fileIds, array_keys($tags));
+ // also cache the ones that were not found
+ foreach ($emptyFileIds as $fileId) {
+ $this->cachedTags[$fileId] = [];
+ }
+ }
+
+ $tags = null;
+ $isFav = null;
+
+ $propFind->handle(self::TAGS_PROPERTYNAME, function() use ($tags, &$isFav, $node) {
+ list($tags, $isFav) = $this->getTagsAndFav($node->getId());
+ return new TagList($tags);
+ });
+
+ $propFind->handle(self::FAVORITE_PROPERTYNAME, function() use ($isFav, $node) {
+ if (is_null($isFav)) {
+ list(, $isFav) = $this->getTagsAndFav($node->getId());
+ }
+ return $isFav;
+ });
+ }
+
+ /**
+ * Updates tags and favorites properties, if applicable.
+ *
+ * @param string $path
+ * @param PropPatch $propPatch
+ *
+ * @return void
+ */
+ public function handleUpdateProperties($path, PropPatch $propPatch) {
+ $propPatch->handle(self::TAGS_PROPERTYNAME, function($tagList) use ($path) {
+ $node = $this->tree->getNodeForPath($path);
+ if (is_null($node)) {
+ return 404;
+ }
+ $this->updateTags($node->getId(), $tagList->getTags());
+ return true;
+ });
+
+ $propPatch->handle(self::FAVORITE_PROPERTYNAME, function($favState) use ($path) {
+ $node = $this->tree->getNodeForPath($path);
+ if (is_null($node)) {
+ return 404;
+ }
+ if ((int)$favState === 1 || $favState === 'true') {
+ $this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
+ } else {
+ $this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
+ }
+
+ if (is_null($favState)) {
+ // confirm deletion
+ return 204;
+ }
+
+ return 200;
+ });
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php b/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php
new file mode 100644
index 00000000000..1e390cf15f7
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php
@@ -0,0 +1,129 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace Test\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
+use Test\TestCase;
+use OCP\IConfig;
+
+/**
+ * Class BlockLegacyClientPluginTest
+ *
+ * @package Test\Connector\Sabre
+ */
+class BlockLegacyClientPluginTest extends TestCase {
+ /** @var IConfig */
+ private $config;
+ /** @var BlockLegacyClientPlugin */
+ private $blockLegacyClientVersionPlugin;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->config = $this->getMock('\OCP\IConfig');
+ $this->blockLegacyClientVersionPlugin = new BlockLegacyClientPlugin($this->config);
+ }
+
+ /**
+ * @return array
+ */
+ public function oldDesktopClientProvider() {
+ return [
+ ['Mozilla/5.0 (1.5.0) mirall/1.5.0'],
+ ['mirall/1.5.0'],
+ ['mirall/1.5.4'],
+ ['mirall/1.6.0'],
+ ['Mozilla/5.0 (Bogus Text) mirall/1.6.9'],
+ ];
+ }
+
+ /**
+ * @dataProvider oldDesktopClientProvider
+ * @param string $userAgent
+ * @expectedException \Sabre\DAV\Exception\Forbidden
+ * @expectedExceptionMessage Unsupported client version.
+ */
+ public function testBeforeHandlerException($userAgent) {
+ /** @var \Sabre\HTTP\RequestInterface $request */
+ $request = $this->getMock('\Sabre\HTTP\RequestInterface');
+ $request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('User-Agent')
+ ->will($this->returnValue($userAgent));
+
+ $this->config
+ ->expects($this->once())
+ ->method('getSystemValue')
+ ->with('minimum.supported.desktop.version', '1.7.0')
+ ->will($this->returnValue('1.7.0'));
+
+ $this->blockLegacyClientVersionPlugin->beforeHandler($request);
+ }
+
+ /**
+ * @return array
+ */
+ public function newAndAlternateDesktopClientProvider() {
+ return [
+ ['Mozilla/5.0 (1.7.0) mirall/1.7.0'],
+ ['mirall/1.8.3'],
+ ['mirall/1.7.2'],
+ ['mirall/1.7.0'],
+ ['Mozilla/5.0 (Bogus Text) mirall/1.9.3'],
+ ];
+ }
+
+ /**
+ * @dataProvider newAndAlternateDesktopClientProvider
+ * @param string $userAgent
+ */
+ public function testBeforeHandlerSuccess($userAgent) {
+ /** @var \Sabre\HTTP\RequestInterface $request */
+ $request = $this->getMock('\Sabre\HTTP\RequestInterface');
+ $request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('User-Agent')
+ ->will($this->returnValue($userAgent));
+
+ $this->config
+ ->expects($this->once())
+ ->method('getSystemValue')
+ ->with('minimum.supported.desktop.version', '1.7.0')
+ ->will($this->returnValue('1.7.0'));
+
+ $this->blockLegacyClientVersionPlugin->beforeHandler($request);
+ }
+
+ public function testBeforeHandlerNoUserAgent() {
+ /** @var \Sabre\HTTP\RequestInterface $request */
+ $request = $this->getMock('\Sabre\HTTP\RequestInterface');
+ $request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('User-Agent')
+ ->will($this->returnValue(null));
+ $this->blockLegacyClientVersionPlugin->beforeHandler($request);
+ }
+
+}
diff --git a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php b/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php
new file mode 100644
index 00000000000..1fd89c84ff6
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace Test\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin;
+use Test\TestCase;
+
+/**
+ * Class DummyGetResponsePluginTest
+ *
+ * @package Test\Connector\Sabre
+ */
+class DummyGetResponsePluginTest extends TestCase {
+ /** @var DummyGetResponsePlugin */
+ private $dummyGetResponsePlugin;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->dummyGetResponsePlugin = new DummyGetResponsePlugin();
+ }
+
+ public function testInitialize() {
+ /** @var \Sabre\DAV\Server $server */
+ $server = $this->getMock('\Sabre\DAV\Server');
+ $server
+ ->expects($this->once())
+ ->method('on')
+ ->with('method:GET', [$this->dummyGetResponsePlugin, 'httpGet'], 200);
+
+ $this->dummyGetResponsePlugin->initialize($server);
+ }
+
+
+ public function testHttpGet() {
+ /** @var \Sabre\HTTP\RequestInterface $request */
+ $request = $this->getMock('\Sabre\HTTP\RequestInterface');
+ /** @var \Sabre\HTTP\ResponseInterface $response */
+ $response = $server = $this->getMock('\Sabre\HTTP\ResponseInterface');
+ $response
+ ->expects($this->once())
+ ->method('setBody');
+ $response
+ ->expects($this->once())
+ ->method('setStatus')
+ ->with(200);
+
+ $this->assertSame(false, $this->dummyGetResponsePlugin->httpGet($request, $response));
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php b/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php
new file mode 100644
index 00000000000..c0acd4fc3de
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace Test\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\MaintenancePlugin;
+use Test\TestCase;
+use OCP\IConfig;
+
+/**
+ * Class MaintenancePluginTest
+ *
+ * @package Test\Connector\Sabre
+ */
+class MaintenancePluginTest extends TestCase {
+ /** @var IConfig */
+ private $config;
+ /** @var MaintenancePlugin */
+ private $maintenancePlugin;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->config = $this->getMock('\OCP\IConfig');
+ $this->maintenancePlugin = new MaintenancePlugin($this->config);
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\ServiceUnavailable
+ * @expectedExceptionMessage System in single user mode.
+ */
+ public function testSingleUserMode() {
+ $this->config
+ ->expects($this->once())
+ ->method('getSystemValue')
+ ->with('singleuser', false)
+ ->will($this->returnValue(true));
+
+ $this->maintenancePlugin->checkMaintenanceMode();
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\ServiceUnavailable
+ * @expectedExceptionMessage System in single user mode.
+ */
+ public function testMaintenanceMode() {
+ $this->config
+ ->expects($this->exactly(1))
+ ->method('getSystemValue')
+ ->will($this->onConsecutiveCalls([false, true]));
+
+ $this->maintenancePlugin->checkMaintenanceMode();
+ }
+
+}
diff --git a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php b/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php
new file mode 100644
index 00000000000..2080755cd51
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Tests\Connector\Sabre;
+
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class CopyEtagPluginTest extends \Test\TestCase {
+
+ /**
+ * @var \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin
+ */
+ private $plugin;
+
+ public function setUp() {
+ parent::setUp();
+ $this->server = new \Sabre\DAV\Server();
+ $this->plugin = new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin();
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testCopyEtag() {
+ $request = new \Sabre\Http\Request();
+ $response = new \Sabre\Http\Response();
+ $response->setHeader('Etag', 'abcd');
+
+ $this->plugin->afterMethod($request, $response);
+
+ $this->assertEquals('abcd', $response->getHeader('OC-Etag'));
+ }
+
+ public function testNoopWhenEmpty() {
+ $request = new \Sabre\Http\Request();
+ $response = new \Sabre\Http\Response();
+
+ $this->plugin->afterMethod($request, $response);
+
+ $this->assertNull($response->getHeader('OC-Etag'));
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
new file mode 100644
index 00000000000..973a5d4c27b
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
@@ -0,0 +1,286 @@
+<?php
+
+namespace Tests\Connector\Sabre;
+
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class CustomPropertiesBackend extends \Test\TestCase {
+
+ /**
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var \Sabre\DAV\ObjectTree
+ */
+ private $tree;
+
+ /**
+ * @var \OCA\DAV\Connector\Sabre\CustomPropertiesBackend
+ */
+ private $plugin;
+
+ /**
+ * @var \OCP\IUser
+ */
+ private $user;
+
+ public function setUp() {
+ parent::setUp();
+ $this->server = new \Sabre\DAV\Server();
+ $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $userId = $this->getUniqueID('testcustompropertiesuser');
+
+ $this->user = $this->getMock('\OCP\IUser');
+ $this->user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue($userId));
+
+ $this->plugin = new \OCA\DAV\Connector\Sabre\CustomPropertiesBackend(
+ $this->tree,
+ \OC::$server->getDatabaseConnection(),
+ $this->user
+ );
+ }
+
+ public function tearDown() {
+ $connection = \OC::$server->getDatabaseConnection();
+ $deleteStatement = $connection->prepare(
+ 'DELETE FROM `*PREFIX*properties`' .
+ ' WHERE `userid` = ?'
+ );
+ $deleteStatement->execute(
+ array(
+ $this->user->getUID(),
+ )
+ );
+ $deleteStatement->closeCursor();
+ }
+
+ private function createTestNode($class) {
+ $node = $this->getMockBuilder($class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(123));
+
+ $node->expects($this->any())
+ ->method('getPath')
+ ->will($this->returnValue('/dummypath'));
+
+ return $node;
+ }
+
+ private function applyDefaultProps($path = '/dummypath') {
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch(array(
+ 'customprop' => 'value1',
+ 'customprop2' => 'value2',
+ ));
+
+ $this->plugin->propPatch(
+ $path,
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result['customprop']);
+ $this->assertEquals(200, $result['customprop2']);
+ }
+
+ /**
+ * Test that propFind on a missing file soft fails
+ */
+ public function testPropFindMissingFileSoftFail() {
+ $this->tree->expects($this->at(0))
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->throwException(new \Sabre\DAV\Exception\NotFound()));
+
+ $this->tree->expects($this->at(1))
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->throwException(new \Sabre\DAV\Exception\ServiceUnavailable()));
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummypath',
+ array(
+ 'customprop',
+ 'customprop2',
+ 'unsetprop',
+ ),
+ 0
+ );
+
+ $this->plugin->propFind(
+ '/dummypath',
+ $propFind
+ );
+
+ $this->plugin->propFind(
+ '/dummypath',
+ $propFind
+ );
+
+ // no exception, soft fail
+ $this->assertTrue(true);
+ }
+
+ /**
+ * Test setting/getting properties
+ */
+ public function testSetGetPropertiesForFile() {
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->returnValue($node));
+
+ $this->applyDefaultProps();
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummypath',
+ array(
+ 'customprop',
+ 'customprop2',
+ 'unsetprop',
+ ),
+ 0
+ );
+
+ $this->plugin->propFind(
+ '/dummypath',
+ $propFind
+ );
+
+ $this->assertEquals('value1', $propFind->get('customprop'));
+ $this->assertEquals('value2', $propFind->get('customprop2'));
+ $this->assertEquals(array('unsetprop'), $propFind->get404Properties());
+ }
+
+ /**
+ * Test getting properties from directory
+ */
+ public function testGetPropertiesForDirectory() {
+ $rootNode = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory');
+
+ $nodeSub = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $nodeSub->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(456));
+
+ $nodeSub->expects($this->any())
+ ->method('getPath')
+ ->will($this->returnValue('/dummypath/test.txt'));
+
+ $rootNode->expects($this->once())
+ ->method('getChildren')
+ ->will($this->returnValue(array($nodeSub)));
+
+ $this->tree->expects($this->at(0))
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->returnValue($rootNode));
+
+ $this->tree->expects($this->at(1))
+ ->method('getNodeForPath')
+ ->with('/dummypath/test.txt')
+ ->will($this->returnValue($nodeSub));
+
+ $this->tree->expects($this->at(2))
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->returnValue($rootNode));
+
+ $this->tree->expects($this->at(3))
+ ->method('getNodeForPath')
+ ->with('/dummypath/test.txt')
+ ->will($this->returnValue($nodeSub));
+
+ $this->applyDefaultProps('/dummypath');
+ $this->applyDefaultProps('/dummypath/test.txt');
+
+ $propNames = array(
+ 'customprop',
+ 'customprop2',
+ 'unsetprop',
+ );
+
+ $propFindRoot = new \Sabre\DAV\PropFind(
+ '/dummypath',
+ $propNames,
+ 1
+ );
+
+ $propFindSub = new \Sabre\DAV\PropFind(
+ '/dummypath/test.txt',
+ $propNames,
+ 0
+ );
+
+ $this->plugin->propFind(
+ '/dummypath',
+ $propFindRoot
+ );
+
+ $this->plugin->propFind(
+ '/dummypath/test.txt',
+ $propFindSub
+ );
+
+ // TODO: find a way to assert that no additional SQL queries were
+ // run while doing the second propFind
+
+ $this->assertEquals('value1', $propFindRoot->get('customprop'));
+ $this->assertEquals('value2', $propFindRoot->get('customprop2'));
+ $this->assertEquals(array('unsetprop'), $propFindRoot->get404Properties());
+
+ $this->assertEquals('value1', $propFindSub->get('customprop'));
+ $this->assertEquals('value2', $propFindSub->get('customprop2'));
+ $this->assertEquals(array('unsetprop'), $propFindSub->get404Properties());
+ }
+
+ /**
+ * Test delete property
+ */
+ public function testDeleteProperty() {
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->returnValue($node));
+
+ $this->applyDefaultProps();
+
+ $propPatch = new \Sabre\DAV\PropPatch(array(
+ 'customprop' => null,
+ ));
+
+ $this->plugin->propPatch(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(204, $result['customprop']);
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/directory.php b/apps/dav/tests/unit/connector/sabre/directory.php
new file mode 100644
index 00000000000..d85290df80a
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/directory.php
@@ -0,0 +1,192 @@
+<?php
+
+/**
+ * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class Test_OC_Connector_Sabre_Directory extends \Test\TestCase {
+
+ /** @var OC\Files\View | PHPUnit_Framework_MockObject_MockObject */
+ private $view;
+ /** @var OC\Files\FileInfo | PHPUnit_Framework_MockObject_MockObject */
+ private $info;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->view = $this->getMock('OC\Files\View', array(), array(), '', false);
+ $this->info = $this->getMock('OC\Files\FileInfo', array(), array(), '', false);
+ }
+
+ private function getDir($path = '/') {
+ $this->view->expects($this->once())
+ ->method('getRelativePath')
+ ->will($this->returnValue($path));
+
+ $this->info->expects($this->once())
+ ->method('getPath')
+ ->will($this->returnValue($path));
+
+ return new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\Forbidden
+ */
+ public function testDeleteRootFolderFails() {
+ $this->info->expects($this->any())
+ ->method('isDeletable')
+ ->will($this->returnValue(true));
+ $this->view->expects($this->never())
+ ->method('rmdir');
+ $dir = $this->getDir();
+ $dir->delete();
+ }
+
+ /**
+ *
+ */
+ public function testDeleteFolderWhenAllowed() {
+ // deletion allowed
+ $this->info->expects($this->once())
+ ->method('isDeletable')
+ ->will($this->returnValue(true));
+
+ // but fails
+ $this->view->expects($this->once())
+ ->method('rmdir')
+ ->with('sub')
+ ->will($this->returnValue(true));
+
+ $dir = $this->getDir('sub');
+ $dir->delete();
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\Forbidden
+ */
+ public function testDeleteFolderFailsWhenNotAllowed() {
+ $this->info->expects($this->once())
+ ->method('isDeletable')
+ ->will($this->returnValue(false));
+
+ $dir = $this->getDir('sub');
+ $dir->delete();
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\Forbidden
+ */
+ public function testDeleteFolderThrowsWhenDeletionFailed() {
+ // deletion allowed
+ $this->info->expects($this->once())
+ ->method('isDeletable')
+ ->will($this->returnValue(true));
+
+ // but fails
+ $this->view->expects($this->once())
+ ->method('rmdir')
+ ->with('sub')
+ ->will($this->returnValue(false));
+
+ $dir = $this->getDir('sub');
+ $dir->delete();
+ }
+
+ public function testGetChildren() {
+ $info1 = $this->getMockBuilder('OC\Files\FileInfo')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $info2 = $this->getMockBuilder('OC\Files\FileInfo')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $info1->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('first'));
+ $info1->expects($this->any())
+ ->method('getEtag')
+ ->will($this->returnValue('abc'));
+ $info2->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('second'));
+ $info2->expects($this->any())
+ ->method('getEtag')
+ ->will($this->returnValue('def'));
+
+ $this->view->expects($this->once())
+ ->method('getDirectoryContent')
+ ->with('')
+ ->will($this->returnValue(array($info1, $info2)));
+
+ $this->view->expects($this->any())
+ ->method('getRelativePath')
+ ->will($this->returnValue(''));
+
+ $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
+ $nodes = $dir->getChildren();
+
+ $this->assertEquals(2, count($nodes));
+
+ // calling a second time just returns the cached values,
+ // does not call getDirectoryContents again
+ $dir->getChildren();
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\ServiceUnavailable
+ */
+ public function testGetChildThrowStorageNotAvailableException() {
+ $this->view->expects($this->once())
+ ->method('getFileInfo')
+ ->willThrowException(new \OCP\Files\StorageNotAvailableException());
+
+ $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
+ $dir->getChild('.');
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath
+ */
+ public function testGetChildThrowInvalidPath() {
+ $this->view->expects($this->once())
+ ->method('verifyPath')
+ ->willThrowException(new \OCP\Files\InvalidPathException());
+ $this->view->expects($this->never())
+ ->method('getFileInfo');
+
+ $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
+ $dir->getChild('.');
+ }
+
+ public function testGetQuotaInfo() {
+ $storage = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Quota')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $storage->expects($this->once())
+ ->method('instanceOfStorage')
+ ->with('\OC\Files\Storage\Wrapper\Quota')
+ ->will($this->returnValue(true));
+
+ $storage->expects($this->once())
+ ->method('getQuota')
+ ->will($this->returnValue(1000));
+
+ $storage->expects($this->once())
+ ->method('free_space')
+ ->will($this->returnValue(800));
+
+ $this->info->expects($this->once())
+ ->method('getSize')
+ ->will($this->returnValue(200));
+
+ $this->info->expects($this->once())
+ ->method('getStorage')
+ ->will($this->returnValue($storage));
+
+ $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info);
+ $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
new file mode 100644
index 00000000000..4c0af58ffea
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Test\Connector\Sabre\Exception;
+
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+
+/**
+ * Copyright (c) 2015 Thomas Müller <deepdiver@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class InvalidPathTest extends \Test\TestCase {
+
+ public function testSerialization() {
+
+ // create xml doc
+ $DOM = new \DOMDocument('1.0','utf-8');
+ $DOM->formatOutput = true;
+ $error = $DOM->createElementNS('DAV:','d:error');
+ $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ // serialize the exception
+ $message = "1234567890";
+ $retry = false;
+ $expectedXml = <<<EOD
+<?xml version="1.0" encoding="utf-8"?>
+<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns">
+ <o:retry xmlns:o="o:">false</o:retry>
+ <o:reason xmlns:o="o:">1234567890</o:reason>
+</d:error>
+
+EOD;
+
+ $ex = new InvalidPath($message, $retry);
+ $server = $this->getMock('Sabre\DAV\Server');
+ $ex->serialize($server, $error);
+
+ // assert
+ $xml = $DOM->saveXML();
+ $this->assertEquals($expectedXml, $xml);
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php b/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php
new file mode 100644
index 00000000000..d85aa5a9cc3
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * Copyright (c) 2015 Thomas Müller <deepdiver@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace Test\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin as PluginToTest;
+use OC\Log;
+use OCP\ILogger;
+use PHPUnit_Framework_MockObject_MockObject;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Server;
+use Test\TestCase;
+
+class TestLogger extends Log {
+ public $message;
+ public $level;
+
+ public function __construct($logger = null) {
+ //disable original constructor
+ }
+
+ public function log($level, $message, array $context = array()) {
+ $this->level = $level;
+ $this->message = $message;
+ }
+}
+
+class ExceptionLoggerPlugin extends TestCase {
+
+ /** @var Server */
+ private $server;
+
+ /** @var PluginToTest */
+ private $plugin;
+
+ /** @var TestLogger | PHPUnit_Framework_MockObject_MockObject */
+ private $logger;
+
+ private function init() {
+ $this->server = new Server();
+ $this->logger = new TestLogger();
+ $this->plugin = new PluginToTest('unit-test', $this->logger);
+ $this->plugin->initialize($this->server);
+ }
+
+ /**
+ * @dataProvider providesExceptions
+ */
+ public function testLogging($expectedLogLevel, $expectedMessage, $exception) {
+ $this->init();
+ $this->plugin->logException($exception);
+
+ $this->assertEquals($expectedLogLevel, $this->logger->level);
+ $this->assertStringStartsWith('Exception: {"Message":"' . $expectedMessage, $this->logger->message);
+ }
+
+ public function providesExceptions() {
+ return [
+ [0, 'HTTP\/1.1 404 Not Found', new NotFound()],
+ [4, 'HTTP\/1.1 400 This path leads to nowhere', new InvalidPath('This path leads to nowhere')]
+ ];
+ }
+
+}
diff --git a/apps/dav/tests/unit/connector/sabre/file.php b/apps/dav/tests/unit/connector/sabre/file.php
new file mode 100644
index 00000000000..6685ddac913
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/file.php
@@ -0,0 +1,829 @@
+<?php
+/**
+ * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace Test\Connector\Sabre;
+
+use Test\HookHelper;
+use OC\Files\Filesystem;
+use OCP\Lock\ILockingProvider;
+
+class File extends \Test\TestCase {
+
+ /**
+ * @var string
+ */
+ private $user;
+
+ public function setUp() {
+ parent::setUp();
+
+ \OC_Hook::clear();
+
+ $this->user = $this->getUniqueID('user_');
+ $userManager = \OC::$server->getUserManager();
+ $userManager->createUser($this->user, 'pass');
+
+ $this->loginAsUser($this->user);
+ }
+
+ public function tearDown() {
+ $userManager = \OC::$server->getUserManager();
+ $userManager->get($this->user)->delete();
+ unset($_SERVER['HTTP_OC_CHUNKED']);
+
+ parent::tearDown();
+ }
+
+ private function getStream($string) {
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $string);
+ fseek($stream, 0);
+ return $stream;
+ }
+
+
+ public function fopenFailuresProvider() {
+ return [
+ [
+ // return false
+ null,
+ '\Sabre\Dav\Exception',
+ false
+ ],
+ [
+ new \OCP\Files\NotPermittedException(),
+ 'Sabre\DAV\Exception\Forbidden'
+ ],
+ [
+ new \OCP\Files\EntityTooLargeException(),
+ 'OCA\DAV\Connector\Sabre\Exception\EntityTooLarge'
+ ],
+ [
+ new \OCP\Files\InvalidContentException(),
+ 'OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType'
+ ],
+ [
+ new \OCP\Files\InvalidPathException(),
+ 'Sabre\DAV\Exception\Forbidden'
+ ],
+ [
+ new \OCP\Files\LockNotAcquiredException('/test.txt', 1),
+ 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
+ ],
+ [
+ new \OCP\Lock\LockedException('/test.txt'),
+ 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
+ ],
+ [
+ new \OCP\Encryption\Exceptions\GenericEncryptionException(),
+ 'Sabre\DAV\Exception\ServiceUnavailable'
+ ],
+ [
+ new \OCP\Files\StorageNotAvailableException(),
+ 'Sabre\DAV\Exception\ServiceUnavailable'
+ ],
+ [
+ new \Sabre\DAV\Exception('Generic sabre exception'),
+ 'Sabre\DAV\Exception',
+ false
+ ],
+ [
+ new \Exception('Generic exception'),
+ 'Sabre\DAV\Exception'
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider fopenFailuresProvider
+ */
+ public function testSimplePutFails($thrownException, $expectedException, $checkPreviousClass = true) {
+ // setup
+ $storage = $this->getMock(
+ '\OC\Files\Storage\Local',
+ ['fopen'],
+ [['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]]
+ );
+ \OC\Files\Filesystem::mount($storage, [], $this->user . '/');
+ $view = $this->getMock('\OC\Files\View', array('getRelativePath', 'resolvePath'), array());
+ $view->expects($this->atLeastOnce())
+ ->method('resolvePath')
+ ->will($this->returnCallback(
+ function ($path) use ($storage) {
+ return [$storage, $path];
+ }
+ ));
+
+ if ($thrownException !== null) {
+ $storage->expects($this->once())
+ ->method('fopen')
+ ->will($this->throwException($thrownException));
+ } else {
+ $storage->expects($this->once())
+ ->method('fopen')
+ ->will($this->returnValue(false));
+ }
+
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->will($this->returnArgument(0));
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $caughtException = null;
+ try {
+ $file->put('test data');
+ } catch (\Exception $e) {
+ $caughtException = $e;
+ }
+
+ $this->assertInstanceOf($expectedException, $caughtException);
+ if ($checkPreviousClass) {
+ $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
+ }
+
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Test putting a file using chunking
+ *
+ * @dataProvider fopenFailuresProvider
+ */
+ public function testChunkedPutFails($thrownException, $expectedException, $checkPreviousClass = false) {
+ // setup
+ $storage = $this->getMock(
+ '\OC\Files\Storage\Local',
+ ['fopen'],
+ [['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]]
+ );
+ \OC\Files\Filesystem::mount($storage, [], $this->user . '/');
+ $view = $this->getMock('\OC\Files\View', ['getRelativePath', 'resolvePath'], []);
+ $view->expects($this->atLeastOnce())
+ ->method('resolvePath')
+ ->will($this->returnCallback(
+ function ($path) use ($storage) {
+ return [$storage, $path];
+ }
+ ));
+
+ if ($thrownException !== null) {
+ $storage->expects($this->once())
+ ->method('fopen')
+ ->will($this->throwException($thrownException));
+ } else {
+ $storage->expects($this->once())
+ ->method('fopen')
+ ->will($this->returnValue(false));
+ }
+
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->will($this->returnArgument(0));
+
+ $_SERVER['HTTP_OC_CHUNKED'] = true;
+
+ $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-0', null, null, [
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ], null);
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // put first chunk
+ $this->assertNull($file->put('test data one'));
+
+ $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-1', null, null, [
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ], null);
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $caughtException = null;
+ try {
+ // last chunk
+ $file->acquireLock(ILockingProvider::LOCK_SHARED);
+ $file->put('test data two');
+ $file->releaseLock(ILockingProvider::LOCK_SHARED);
+ } catch (\Exception $e) {
+ $caughtException = $e;
+ }
+
+ $this->assertInstanceOf($expectedException, $caughtException);
+ if ($checkPreviousClass) {
+ $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
+ }
+
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Simulate putting a file to the given path.
+ *
+ * @param string $path path to put the file into
+ * @param string $viewRoot root to use for the view
+ *
+ * @return result of the PUT operaiton which is usually the etag
+ */
+ private function doPut($path, $viewRoot = null) {
+ $view = \OC\Files\Filesystem::getView();
+ if (!is_null($viewRoot)) {
+ $view = new \OC\Files\View($viewRoot);
+ } else {
+ $viewRoot = '/' . $this->user . '/files';
+ }
+
+ $info = new \OC\Files\FileInfo(
+ $viewRoot . '/' . ltrim($path, '/'),
+ null,
+ null,
+ ['permissions' => \OCP\Constants::PERMISSION_ALL],
+ null
+ );
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // beforeMethod locks
+ $view->lockFile($path, ILockingProvider::LOCK_SHARED);
+
+ $result = $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
+
+ return $result;
+ }
+
+ /**
+ * Test putting a single file
+ */
+ public function testPutSingleFile() {
+ $this->assertNotEmpty($this->doPut('/foo.txt'));
+ }
+
+ /**
+ * Test putting a file using chunking
+ */
+ public function testChunkedPut() {
+ $_SERVER['HTTP_OC_CHUNKED'] = true;
+ $this->assertNull($this->doPut('/test.txt-chunking-12345-2-0'));
+ $this->assertNotEmpty($this->doPut('/test.txt-chunking-12345-2-1'));
+ }
+
+ /**
+ * Test that putting a file triggers create hooks
+ */
+ public function testPutSingleFileTriggersHooks() {
+ HookHelper::setUpHooks();
+
+ $this->assertNotEmpty($this->doPut('/foo.txt'));
+
+ $this->assertCount(4, HookHelper::$hookCalls);
+ $this->assertHookCall(
+ HookHelper::$hookCalls[0],
+ Filesystem::signal_create,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[1],
+ Filesystem::signal_write,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[2],
+ Filesystem::signal_post_create,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[3],
+ Filesystem::signal_post_write,
+ '/foo.txt'
+ );
+ }
+
+ /**
+ * Test that putting a file triggers update hooks
+ */
+ public function testPutOverwriteFileTriggersHooks() {
+ $view = \OC\Files\Filesystem::getView();
+ $view->file_put_contents('/foo.txt', 'some content that will be replaced');
+
+ HookHelper::setUpHooks();
+
+ $this->assertNotEmpty($this->doPut('/foo.txt'));
+
+ $this->assertCount(4, HookHelper::$hookCalls);
+ $this->assertHookCall(
+ HookHelper::$hookCalls[0],
+ Filesystem::signal_update,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[1],
+ Filesystem::signal_write,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[2],
+ Filesystem::signal_post_update,
+ '/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[3],
+ Filesystem::signal_post_write,
+ '/foo.txt'
+ );
+ }
+
+ /**
+ * Test that putting a file triggers hooks with the correct path
+ * if the passed view was chrooted (can happen with public webdav
+ * where the root is the share root)
+ */
+ public function testPutSingleFileTriggersHooksDifferentRoot() {
+ $view = \OC\Files\Filesystem::getView();
+ $view->mkdir('noderoot');
+
+ HookHelper::setUpHooks();
+
+ // happens with public webdav where the view root is the share root
+ $this->assertNotEmpty($this->doPut('/foo.txt', '/' . $this->user . '/files/noderoot'));
+
+ $this->assertCount(4, HookHelper::$hookCalls);
+ $this->assertHookCall(
+ HookHelper::$hookCalls[0],
+ Filesystem::signal_create,
+ '/noderoot/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[1],
+ Filesystem::signal_write,
+ '/noderoot/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[2],
+ Filesystem::signal_post_create,
+ '/noderoot/foo.txt'
+ );
+ $this->assertHookCall(
+ HookHelper::$hookCalls[3],
+ Filesystem::signal_post_write,
+ '/noderoot/foo.txt'
+ );
+ }
+
+ public static function cancellingHook($params) {
+ self::$hookCalls[] = array(
+ 'signal' => Filesystem::signal_post_create,
+ 'params' => $params
+ );
+ }
+
+ /**
+ * Test put file with cancelled hook
+ */
+ public function testPutSingleFileCancelPreHook() {
+ \OCP\Util::connectHook(
+ Filesystem::CLASSNAME,
+ Filesystem::signal_create,
+ '\Test\HookHelper',
+ 'cancellingCallback'
+ );
+
+ // action
+ $thrown = false;
+ try {
+ $this->doPut('/foo.txt');
+ } catch (\Sabre\DAV\Exception $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles(), 'No stray part files');
+ }
+
+ /**
+ * Test exception when the uploaded size did not match
+ */
+ public function testSimplePutFailsSizeCheck() {
+ // setup
+ $view = $this->getMock('\OC\Files\View',
+ array('rename', 'getRelativePath', 'filesize'));
+ $view->expects($this->any())
+ ->method('rename')
+ ->withAnyParameters()
+ ->will($this->returnValue(false));
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->will($this->returnArgument(0));
+
+ $view->expects($this->any())
+ ->method('filesize')
+ ->will($this->returnValue(123456));
+
+ $_SERVER['CONTENT_LENGTH'] = 123456;
+ $_SERVER['REQUEST_METHOD'] = 'PUT';
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $thrown = false;
+ try {
+ // beforeMethod locks
+ $view->lockFile('/test.txt', ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile('/test.txt', ILockingProvider::LOCK_SHARED);
+ } catch (\Sabre\DAV\Exception\BadRequest $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Test exception during final rename in simple upload mode
+ */
+ public function testSimplePutFailsMoveFromStorage() {
+ $view = new \OC\Files\View('/' . $this->user . '/files');
+
+ // simulate situation where the target file is locked
+ $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
+
+ $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $thrown = false;
+ try {
+ // beforeMethod locks
+ $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+ } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Test exception during final rename in chunk upload mode
+ */
+ public function testChunkedPutFailsFinalRename() {
+ $view = new \OC\Files\View('/' . $this->user . '/files');
+
+ // simulate situation where the target file is locked
+ $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
+
+ $_SERVER['HTTP_OC_CHUNKED'] = true;
+
+ $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-0', null, null, [
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ], null);
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $this->assertNull($file->put('test data one'));
+
+ $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-1', null, null, [
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ], null);
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $thrown = false;
+ try {
+ $file->put($this->getStream('test data'));
+ } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Test put file with invalid chars
+ */
+ public function testSimplePutInvalidChars() {
+ // setup
+ $view = $this->getMock('\OC\Files\View', array('getRelativePath'));
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->will($this->returnArgument(0));
+
+ $info = new \OC\Files\FileInfo('/*', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $thrown = false;
+ try {
+ // beforeMethod locks
+ $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+ } catch (\OCA\DAV\Connector\Sabre\Exception\InvalidPath $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ * Test setting name with setName() with invalid chars
+ *
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath
+ */
+ public function testSetNameInvalidChars() {
+ // setup
+ $view = $this->getMock('\OC\Files\View', array('getRelativePath'));
+
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->will($this->returnArgument(0));
+
+ $info = new \OC\Files\FileInfo('/*', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file->setName('/super*star.txt');
+ }
+
+ /**
+ */
+ public function testUploadAbort() {
+ // setup
+ $view = $this->getMock('\OC\Files\View',
+ array('rename', 'getRelativePath', 'filesize'));
+ $view->expects($this->any())
+ ->method('rename')
+ ->withAnyParameters()
+ ->will($this->returnValue(false));
+ $view->expects($this->any())
+ ->method('getRelativePath')
+ ->will($this->returnArgument(0));
+ $view->expects($this->any())
+ ->method('filesize')
+ ->will($this->returnValue(123456));
+
+ $_SERVER['CONTENT_LENGTH'] = 12345;
+ $_SERVER['REQUEST_METHOD'] = 'PUT';
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $thrown = false;
+ try {
+ // beforeMethod locks
+ $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+
+ $file->put($this->getStream('test data'));
+
+ // afterMethod unlocks
+ $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
+ } catch (\Sabre\DAV\Exception\BadRequest $e) {
+ $thrown = true;
+ }
+
+ $this->assertTrue($thrown);
+ $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
+ }
+
+ /**
+ *
+ */
+ public function testDeleteWhenAllowed() {
+ // setup
+ $view = $this->getMock('\OC\Files\View',
+ array());
+
+ $view->expects($this->once())
+ ->method('unlink')
+ ->will($this->returnValue(true));
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $file->delete();
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\Forbidden
+ */
+ public function testDeleteThrowsWhenDeletionNotAllowed() {
+ // setup
+ $view = $this->getMock('\OC\Files\View',
+ array());
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => 0
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $file->delete();
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\Forbidden
+ */
+ public function testDeleteThrowsWhenDeletionFailed() {
+ // setup
+ $view = $this->getMock('\OC\Files\View',
+ array());
+
+ // but fails
+ $view->expects($this->once())
+ ->method('unlink')
+ ->will($this->returnValue(false));
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $file->delete();
+ }
+
+ /**
+ * Asserts hook call
+ *
+ * @param array $callData hook call data to check
+ * @param string $signal signal name
+ * @param string $hookPath hook path
+ */
+ protected function assertHookCall($callData, $signal, $hookPath) {
+ $this->assertEquals($signal, $callData['signal']);
+ $params = $callData['params'];
+ $this->assertEquals(
+ $hookPath,
+ $params[Filesystem::signal_param_path]
+ );
+ }
+
+ /**
+ * Test whether locks are set before and after the operation
+ */
+ public function testPutLocking() {
+ $view = new \OC\Files\View('/' . $this->user . '/files/');
+
+ $path = 'test-locking.txt';
+ $info = new \OC\Files\FileInfo(
+ '/' . $this->user . '/files/' . $path,
+ null,
+ null,
+ ['permissions' => \OCP\Constants::PERMISSION_ALL],
+ null
+ );
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ $this->assertFalse(
+ $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
+ 'File unlocked before put'
+ );
+ $this->assertFalse(
+ $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
+ 'File unlocked before put'
+ );
+
+ $wasLockedPre = false;
+ $wasLockedPost = false;
+ $eventHandler = $this->getMockBuilder('\stdclass')
+ ->setMethods(['writeCallback', 'postWriteCallback'])
+ ->getMock();
+
+ // both pre and post hooks might need access to the file,
+ // so only shared lock is acceptable
+ $eventHandler->expects($this->once())
+ ->method('writeCallback')
+ ->will($this->returnCallback(
+ function () use ($view, $path, &$wasLockedPre) {
+ $wasLockedPre = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
+ $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
+ }
+ ));
+ $eventHandler->expects($this->once())
+ ->method('postWriteCallback')
+ ->will($this->returnCallback(
+ function () use ($view, $path, &$wasLockedPost) {
+ $wasLockedPost = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
+ $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
+ }
+ ));
+
+ \OCP\Util::connectHook(
+ Filesystem::CLASSNAME,
+ Filesystem::signal_write,
+ $eventHandler,
+ 'writeCallback'
+ );
+ \OCP\Util::connectHook(
+ Filesystem::CLASSNAME,
+ Filesystem::signal_post_write,
+ $eventHandler,
+ 'postWriteCallback'
+ );
+
+ // beforeMethod locks
+ $view->lockFile($path, ILockingProvider::LOCK_SHARED);
+
+ $this->assertNotEmpty($file->put($this->getStream('test data')));
+
+ // afterMethod unlocks
+ $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
+
+ $this->assertTrue($wasLockedPre, 'File was locked during pre-hooks');
+ $this->assertTrue($wasLockedPost, 'File was locked during post-hooks');
+
+ $this->assertFalse(
+ $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
+ 'File unlocked after put'
+ );
+ $this->assertFalse(
+ $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
+ 'File unlocked after put'
+ );
+ }
+
+ /**
+ * Returns part files in the given path
+ *
+ * @param \OC\Files\View view which root is the current user's "files" folder
+ * @param string $path path for which to list part files
+ *
+ * @return array list of part files
+ */
+ private function listPartFiles(\OC\Files\View $userView = null, $path = '') {
+ if ($userView === null) {
+ $userView = \OC\Files\Filesystem::getView();
+ }
+ $files = [];
+ list($storage, $internalPath) = $userView->resolvePath($path);
+ $realPath = $storage->getSourcePath($internalPath);
+ $dh = opendir($realPath);
+ while (($file = readdir($dh)) !== false) {
+ if (substr($file, strlen($file) - 5, 5) === '.part') {
+ $files[] = $file;
+ }
+ }
+ closedir($dh);
+ return $files;
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\ServiceUnavailable
+ */
+ public function testGetFopenFails() {
+ $view = $this->getMock('\OC\Files\View', ['fopen'], array());
+ $view->expects($this->atLeastOnce())
+ ->method('fopen')
+ ->will($this->returnValue(false));
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OC\Connector\Sabre\File($view, $info);
+
+ $file->get();
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/filesplugin.php b/apps/dav/tests/unit/connector/sabre/filesplugin.php
new file mode 100644
index 00000000000..a91ca7a4ff7
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/filesplugin.php
@@ -0,0 +1,254 @@
+<?php
+
+namespace Tests\Connector\Sabre;
+
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class FilesPlugin extends \Test\TestCase {
+ const GETETAG_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::GETETAG_PROPERTYNAME;
+ const FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::FILEID_PROPERTYNAME;
+ const SIZE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::SIZE_PROPERTYNAME;
+ const PERMISSIONS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::PERMISSIONS_PROPERTYNAME;
+ const LASTMODIFIED_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::LASTMODIFIED_PROPERTYNAME;
+ const DOWNLOADURL_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::DOWNLOADURL_PROPERTYNAME;
+
+ /**
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var \Sabre\DAV\Tree
+ */
+ private $tree;
+
+ /**
+ * @var \OCA\DAV\Connector\Sabre\FilesPlugin
+ */
+ private $plugin;
+
+ /**
+ * @var \OC\Files\View
+ */
+ private $view;
+
+ public function setUp() {
+ parent::setUp();
+ $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->view = $this->getMockBuilder('\OC\Files\View')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->plugin = new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view);
+ $this->plugin->initialize($this->server);
+ }
+
+ private function createTestNode($class) {
+ $node = $this->getMockBuilder($class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(123));
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->returnValue($node));
+
+ $node->expects($this->any())
+ ->method('getFileId')
+ ->will($this->returnValue(123));
+ $node->expects($this->any())
+ ->method('getEtag')
+ ->will($this->returnValue('"abc"'));
+ $node->expects($this->any())
+ ->method('getDavPermissions')
+ ->will($this->returnValue('DWCKMSR'));
+
+ return $node;
+ }
+
+ /**
+ */
+ public function testGetPropertiesForFile() {
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummyPath',
+ array(
+ self::GETETAG_PROPERTYNAME,
+ self::FILEID_PROPERTYNAME,
+ self::SIZE_PROPERTYNAME,
+ self::PERMISSIONS_PROPERTYNAME,
+ self::DOWNLOADURL_PROPERTYNAME,
+ ),
+ 0
+ );
+
+ $node->expects($this->once())
+ ->method('getDirectDownload')
+ ->will($this->returnValue(array('url' => 'http://example.com/')));
+ $node->expects($this->never())
+ ->method('getSize');
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME));
+ $this->assertEquals(123, $propFind->get(self::FILEID_PROPERTYNAME));
+ $this->assertEquals(null, $propFind->get(self::SIZE_PROPERTYNAME));
+ $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
+ $this->assertEquals('http://example.com/', $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
+ $this->assertEquals(array(self::SIZE_PROPERTYNAME), $propFind->get404Properties());
+ }
+
+ public function testGetPublicPermissions() {
+ $this->plugin = new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view, true);
+ $this->plugin->initialize($this->server);
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummyPath',
+ [
+ self::PERMISSIONS_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+ $node->expects($this->any())
+ ->method('getDavPermissions')
+ ->will($this->returnValue('DWCKMSR'));
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('DWCKR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
+ }
+
+ public function testGetPropertiesForDirectory() {
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory');
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummyPath',
+ array(
+ self::GETETAG_PROPERTYNAME,
+ self::FILEID_PROPERTYNAME,
+ self::SIZE_PROPERTYNAME,
+ self::PERMISSIONS_PROPERTYNAME,
+ self::DOWNLOADURL_PROPERTYNAME,
+ ),
+ 0
+ );
+
+ $node->expects($this->never())
+ ->method('getDirectDownload');
+ $node->expects($this->once())
+ ->method('getSize')
+ ->will($this->returnValue(1025));
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME));
+ $this->assertEquals(123, $propFind->get(self::FILEID_PROPERTYNAME));
+ $this->assertEquals(1025, $propFind->get(self::SIZE_PROPERTYNAME));
+ $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
+ $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
+ $this->assertEquals(array(self::DOWNLOADURL_PROPERTYNAME), $propFind->get404Properties());
+ }
+
+ public function testUpdateProps() {
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+
+ $testDate = 'Fri, 13 Feb 2015 00:01:02 GMT';
+
+ $node->expects($this->once())
+ ->method('touch')
+ ->with($testDate);
+
+ $node->expects($this->once())
+ ->method('setEtag')
+ ->with('newetag')
+ ->will($this->returnValue(true));
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch(array(
+ self::GETETAG_PROPERTYNAME => 'newetag',
+ self::LASTMODIFIED_PROPERTYNAME => $testDate
+ ));
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result[self::LASTMODIFIED_PROPERTYNAME]);
+ $this->assertEquals(200, $result[self::GETETAG_PROPERTYNAME]);
+ }
+
+ /**
+ * Testcase from https://github.com/owncloud/core/issues/5251
+ *
+ * |-FolderA
+ * |-text.txt
+ * |-test.txt
+ *
+ * FolderA is an incomming shared folder and there are no delete permissions.
+ * Thus moving /FolderA/test.txt to /test.txt should fail already on that check
+ *
+ * @expectedException \Sabre\DAV\Exception\Forbidden
+ * @expectedExceptionMessage FolderA/test.txt cannot be deleted
+ */
+ public function testMoveSrcNotDeletable() {
+ $fileInfoFolderATestTXT = $this->getMockBuilder('\OCP\Files\FileInfo')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $fileInfoFolderATestTXT->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(false);
+
+ $this->view->expects($this->once())
+ ->method('getFileInfo')
+ ->with('FolderA/test.txt')
+ ->willReturn($fileInfoFolderATestTXT);
+
+ $this->plugin->checkMove('FolderA/test.txt', 'test.txt');
+ }
+
+ public function testMoveSrcDeletable() {
+ $fileInfoFolderATestTXT = $this->getMockBuilder('\OCP\Files\FileInfo')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $fileInfoFolderATestTXT->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ $this->view->expects($this->once())
+ ->method('getFileInfo')
+ ->with('FolderA/test.txt')
+ ->willReturn($fileInfoFolderATestTXT);
+
+ $this->plugin->checkMove('FolderA/test.txt', 'test.txt');
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/node.php b/apps/dav/tests/unit/connector/sabre/node.php
new file mode 100644
index 00000000000..a9610fd84b3
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/node.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * Copyright (c) 2014 Thomas Müller <thomas.mueller@tmit.eu>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace Test\Connector\Sabre;
+
+class Node extends \Test\TestCase {
+ public function davPermissionsProvider() {
+ return array(
+ array(\OCP\Constants::PERMISSION_ALL, 'file', false, false, 'RDNVW'),
+ array(\OCP\Constants::PERMISSION_ALL, 'dir', false, false, 'RDNVCK'),
+ array(\OCP\Constants::PERMISSION_ALL, 'file', true, false, 'SRDNVW'),
+ array(\OCP\Constants::PERMISSION_ALL, 'file', true, true, 'SRMDNVW'),
+ array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_SHARE, 'file', true, false, 'SDNVW'),
+ array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_UPDATE, 'file', false, false, 'RDNV'),
+ array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_DELETE, 'file', false, false, 'RW'),
+ array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, 'file', false, false, 'RDNVW'),
+ array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, 'dir', false, false, 'RDNV'),
+ );
+ }
+
+ /**
+ * @dataProvider davPermissionsProvider
+ */
+ public function testDavPermissions($permissions, $type, $shared, $mounted, $expected) {
+ $info = $this->getMockBuilder('\OC\Files\FileInfo')
+ ->disableOriginalConstructor()
+ ->setMethods(array('getPermissions', 'isShared', 'isMounted', 'getType'))
+ ->getMock();
+ $info->expects($this->any())
+ ->method('getPermissions')
+ ->will($this->returnValue($permissions));
+ $info->expects($this->any())
+ ->method('isShared')
+ ->will($this->returnValue($shared));
+ $info->expects($this->any())
+ ->method('isMounted')
+ ->will($this->returnValue($mounted));
+ $info->expects($this->any())
+ ->method('getType')
+ ->will($this->returnValue($type));
+ $view = $this->getMock('\OC\Files\View');
+
+ $node = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $this->assertEquals($expected, $node->getDavPermissions());
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/objecttree.php b/apps/dav/tests/unit/connector/sabre/objecttree.php
new file mode 100644
index 00000000000..2691385c1c1
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/objecttree.php
@@ -0,0 +1,291 @@
+<?php
+/**
+ * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace Test\OCA\DAV\Connector\Sabre;
+
+
+use OC\Files\FileInfo;
+use OCA\DAV\Connector\Sabre\Directory;
+use OC\Files\Storage\Temporary;
+
+class TestDoubleFileView extends \OC\Files\View {
+
+ public function __construct($updatables, $deletables, $canRename = true) {
+ $this->updatables = $updatables;
+ $this->deletables = $deletables;
+ $this->canRename = $canRename;
+ }
+
+ public function isUpdatable($path) {
+ return $this->updatables[$path];
+ }
+
+ public function isCreatable($path) {
+ return $this->updatables[$path];
+ }
+
+ public function isDeletable($path) {
+ return $this->deletables[$path];
+ }
+
+ public function rename($path1, $path2) {
+ return $this->canRename;
+ }
+
+ public function getRelativePath($path) {
+ return $path;
+ }
+}
+
+class ObjectTree extends \Test\TestCase {
+
+ /**
+ * @dataProvider moveFailedProvider
+ * @expectedException \Sabre\DAV\Exception\Forbidden
+ */
+ public function testMoveFailed($source, $destination, $updatables, $deletables) {
+ $this->moveTest($source, $destination, $updatables, $deletables);
+ }
+
+ /**
+ * @dataProvider moveSuccessProvider
+ */
+ public function testMoveSuccess($source, $destination, $updatables, $deletables) {
+ $this->moveTest($source, $destination, $updatables, $deletables);
+ $this->assertTrue(true);
+ }
+
+ /**
+ * @dataProvider moveFailedInvalidCharsProvider
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath
+ */
+ public function testMoveFailedInvalidChars($source, $destination, $updatables, $deletables) {
+ $this->moveTest($source, $destination, $updatables, $deletables);
+ }
+
+ function moveFailedInvalidCharsProvider() {
+ return array(
+ array('a/b', 'a/*', array('a' => true, 'a/b' => true, 'a/c*' => false), array()),
+ );
+ }
+
+ function moveFailedProvider() {
+ return array(
+ array('a/b', 'a/c', array('a' => false, 'a/b' => false, 'a/c' => false), array()),
+ array('a/b', 'b/b', array('a' => false, 'a/b' => false, 'b' => false, 'b/b' => false), array()),
+ array('a/b', 'b/b', array('a' => false, 'a/b' => true, 'b' => false, 'b/b' => false), array()),
+ array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => false, 'b/b' => false), array()),
+ array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => true, 'b/b' => false), array('a/b' => false)),
+ array('a/b', 'a/c', array('a' => false, 'a/b' => true, 'a/c' => false), array()),
+ );
+ }
+
+ function moveSuccessProvider() {
+ return array(
+ array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => true, 'b/b' => false), array('a/b' => true)),
+ // older files with special chars can still be renamed to valid names
+ array('a/b*', 'b/b', array('a' => true, 'a/b*' => true, 'b' => true, 'b/b' => false), array('a/b*' => true)),
+ );
+ }
+
+ /**
+ * @param $source
+ * @param $destination
+ * @param $updatables
+ */
+ private function moveTest($source, $destination, $updatables, $deletables) {
+ $view = new TestDoubleFileView($updatables, $deletables);
+
+ $info = new FileInfo('', null, null, array(), null);
+
+ $rootDir = new Directory($view, $info);
+ $objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree',
+ array('nodeExists', 'getNodeForPath'),
+ array($rootDir, $view));
+
+ $objectTree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with($this->identicalTo($source))
+ ->will($this->returnValue(false));
+
+ /** @var $objectTree \OCA\DAV\Connector\Sabre\ObjectTree */
+ $mountManager = \OC\Files\Filesystem::getMountManager();
+ $objectTree->init($rootDir, $view, $mountManager);
+ $objectTree->move($source, $destination);
+ }
+
+ /**
+ * @dataProvider nodeForPathProvider
+ */
+ public function testGetNodeForPath(
+ $inputFileName,
+ $fileInfoQueryPath,
+ $outputFileName,
+ $type,
+ $enableChunkingHeader
+ ) {
+
+ if ($enableChunkingHeader) {
+ $_SERVER['HTTP_OC_CHUNKED'] = true;
+ }
+
+ $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mountManager = $this->getMock('\OC\Files\Mount\Manager');
+ $view = $this->getMock('\OC\Files\View');
+ $fileInfo = $this->getMock('\OCP\Files\FileInfo');
+ $fileInfo->expects($this->once())
+ ->method('getType')
+ ->will($this->returnValue($type));
+ $fileInfo->expects($this->once())
+ ->method('getName')
+ ->will($this->returnValue($outputFileName));
+
+ $view->expects($this->once())
+ ->method('getFileInfo')
+ ->with($fileInfoQueryPath)
+ ->will($this->returnValue($fileInfo));
+
+ $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
+ $tree->init($rootNode, $view, $mountManager);
+
+ $node = $tree->getNodeForPath($inputFileName);
+
+ $this->assertNotNull($node);
+ $this->assertEquals($outputFileName, $node->getName());
+
+ if ($type === 'file') {
+ $this->assertTrue($node instanceof \OCA\DAV\Connector\Sabre\File);
+ } else {
+ $this->assertTrue($node instanceof \OCA\DAV\Connector\Sabre\Directory);
+ }
+
+ unset($_SERVER['HTTP_OC_CHUNKED']);
+ }
+
+ function nodeForPathProvider() {
+ return array(
+ // regular file
+ array(
+ 'regularfile.txt',
+ 'regularfile.txt',
+ 'regularfile.txt',
+ 'file',
+ false
+ ),
+ // regular directory
+ array(
+ 'regulardir',
+ 'regulardir',
+ 'regulardir',
+ 'dir',
+ false
+ ),
+ // regular file with chunking
+ array(
+ 'regularfile.txt',
+ 'regularfile.txt',
+ 'regularfile.txt',
+ 'file',
+ true
+ ),
+ // regular directory with chunking
+ array(
+ 'regulardir',
+ 'regulardir',
+ 'regulardir',
+ 'dir',
+ true
+ ),
+ // file with chunky file name
+ array(
+ 'regularfile.txt-chunking-123566789-10-1',
+ 'regularfile.txt',
+ 'regularfile.txt',
+ 'file',
+ true
+ ),
+ // regular file in subdir
+ array(
+ 'subdir/regularfile.txt',
+ 'subdir/regularfile.txt',
+ 'regularfile.txt',
+ 'file',
+ false
+ ),
+ // regular directory in subdir
+ array(
+ 'subdir/regulardir',
+ 'subdir/regulardir',
+ 'regulardir',
+ 'dir',
+ false
+ ),
+ // file with chunky file name in subdir
+ array(
+ 'subdir/regularfile.txt-chunking-123566789-10-1',
+ 'subdir/regularfile.txt',
+ 'regularfile.txt',
+ 'file',
+ true
+ ),
+ );
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath
+ */
+ public function testGetNodeForPathInvalidPath() {
+ $path = '/foo\bar';
+
+
+ $storage = new Temporary([]);
+
+ $view = $this->getMock('\OC\Files\View', ['resolvePath']);
+ $view->expects($this->once())
+ ->method('resolvePath')
+ ->will($this->returnCallback(function($path) use ($storage){
+ return [$storage, ltrim($path, '/')];
+ }));
+
+ $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mountManager = $this->getMock('\OC\Files\Mount\Manager');
+
+ $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
+ $tree->init($rootNode, $view, $mountManager);
+
+ $tree->getNodeForPath($path);
+ }
+
+ public function testGetNodeForPathRoot() {
+ $path = '/';
+
+
+ $storage = new Temporary([]);
+
+ $view = $this->getMock('\OC\Files\View', ['resolvePath']);
+ $view->expects($this->any())
+ ->method('resolvePath')
+ ->will($this->returnCallback(function ($path) use ($storage) {
+ return [$storage, ltrim($path, '/')];
+ }));
+
+ $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mountManager = $this->getMock('\OC\Files\Mount\Manager');
+
+ $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
+ $tree->init($rootNode, $view, $mountManager);
+
+ $this->assertInstanceOf('\Sabre\DAV\INode', $tree->getNodeForPath($path));
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/principal.php b/apps/dav/tests/unit/connector/sabre/principal.php
new file mode 100644
index 00000000000..3c0abeac3f1
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/principal.php
@@ -0,0 +1,250 @@
+<?php
+/**
+ * @author Lukas Reschke
+ * @copyright 2014 Lukas Reschke lukas@owncloud.com
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace Test\Connector\Sabre;
+
+use \Sabre\DAV\PropPatch;
+use OCP\IUserManager;
+use OCP\IConfig;
+
+class Principal extends \Test\TestCase {
+ /** @var IUserManager */
+ private $userManager;
+ /** @var IConfig */
+ private $config;
+ /** @var \OCA\DAV\Connector\Sabre\Principal */
+ private $connector;
+
+ public function setUp() {
+ $this->userManager = $this->getMockBuilder('\OCP\IUserManager')
+ ->disableOriginalConstructor()->getMock();
+ $this->config = $this->getMockBuilder('\OCP\IConfig')
+ ->disableOriginalConstructor()->getMock();
+
+ $this->connector = new \OCA\DAV\Connector\Sabre\Principal($this->config, $this->userManager);
+ parent::setUp();
+ }
+
+ public function testGetPrincipalsByPrefixWithoutPrefix() {
+ $response = $this->connector->getPrincipalsByPrefix('');
+ $this->assertSame([], $response);
+ }
+
+ public function testGetPrincipalsByPrefixWithUsers() {
+ $fooUser = $this->getMockBuilder('\OC\User\User')
+ ->disableOriginalConstructor()->getMock();
+ $fooUser
+ ->expects($this->exactly(3))
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $barUser = $this->getMockBuilder('\OC\User\User')
+ ->disableOriginalConstructor()->getMock();
+ $barUser
+ ->expects($this->exactly(3))
+ ->method('getUID')
+ ->will($this->returnValue('bar'));
+ $this->userManager
+ ->expects($this->once())
+ ->method('search')
+ ->with('')
+ ->will($this->returnValue([$fooUser, $barUser]));
+ $this->config
+ ->expects($this->at(0))
+ ->method('getUserValue')
+ ->with('foo', 'settings', 'email')
+ ->will($this->returnValue(''));
+ $this->config
+ ->expects($this->at(1))
+ ->method('getUserValue')
+ ->with('bar', 'settings', 'email')
+ ->will($this->returnValue('bar@owncloud.org'));
+
+ $expectedResponse = [
+ 0 => [
+ 'uri' => 'principals/foo',
+ '{DAV:}displayname' => 'foo'
+ ],
+ 1 => [
+ 'uri' => 'principals/bar',
+ '{DAV:}displayname' => 'bar',
+ '{http://sabredav.org/ns}email-address' => 'bar@owncloud.org'
+ ]
+ ];
+ $response = $this->connector->getPrincipalsByPrefix('principals');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPrefixEmpty() {
+ $this->userManager
+ ->expects($this->once())
+ ->method('search')
+ ->with('')
+ ->will($this->returnValue([]));
+
+ $response = $this->connector->getPrincipalsByPrefix('principals');
+ $this->assertSame([], $response);
+ }
+
+ public function testGetPrincipalsByPathWithoutMail() {
+ $fooUser = $this->getMockBuilder('\OC\User\User')
+ ->disableOriginalConstructor()->getMock();
+ $fooUser
+ ->expects($this->exactly(3))
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue($fooUser));
+ $this->config
+ ->expects($this->once())
+ ->method('getUserValue')
+ ->with('foo', 'settings', 'email')
+ ->will($this->returnValue(''));
+
+ $expectedResponse = [
+ 'uri' => 'principals/foo',
+ '{DAV:}displayname' => 'foo'
+ ];
+ $response = $this->connector->getPrincipalByPath('principals/foo');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPathWithMail() {
+ $fooUser = $this->getMockBuilder('\OC\User\User')
+ ->disableOriginalConstructor()->getMock();
+ $fooUser
+ ->expects($this->exactly(3))
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue($fooUser));
+ $this->config
+ ->expects($this->once())
+ ->method('getUserValue')
+ ->with('foo', 'settings', 'email')
+ ->will($this->returnValue('foo@owncloud.org'));
+
+ $expectedResponse = [
+ 'uri' => 'principals/foo',
+ '{DAV:}displayname' => 'foo',
+ '{http://sabredav.org/ns}email-address' => 'foo@owncloud.org'
+ ];
+ $response = $this->connector->getPrincipalByPath('principals/foo');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ public function testGetPrincipalsByPathEmpty() {
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue(null));
+
+ $response = $this->connector->getPrincipalByPath('principals/foo');
+ $this->assertSame(null, $response);
+ }
+
+ public function testGetGroupMemberSet() {
+ $fooUser = $this->getMockBuilder('\OC\User\User')
+ ->disableOriginalConstructor()->getMock();
+ $fooUser
+ ->expects($this->exactly(3))
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue($fooUser));
+ $this->config
+ ->expects($this->once())
+ ->method('getUserValue')
+ ->with('foo', 'settings', 'email')
+ ->will($this->returnValue('foo@owncloud.org'));
+
+ $response = $this->connector->getGroupMemberSet('principals/foo');
+ $this->assertSame(['principals/foo'], $response);
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception
+ * @expectedExceptionMessage Principal not found
+ */
+ public function testGetGroupMemberSetEmpty() {
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue(null));
+
+ $this->connector->getGroupMemberSet('principals/foo');
+ }
+
+ public function testGetGroupMembership() {
+ $fooUser = $this->getMockBuilder('\OC\User\User')
+ ->disableOriginalConstructor()->getMock();
+ $fooUser
+ ->expects($this->exactly(3))
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue($fooUser));
+ $this->config
+ ->expects($this->once())
+ ->method('getUserValue')
+ ->with('foo', 'settings', 'email')
+ ->will($this->returnValue('foo@owncloud.org'));
+
+ $expectedResponse = [
+ 'principals/foo/calendar-proxy-read',
+ 'principals/foo/calendar-proxy-write'
+ ];
+ $response = $this->connector->getGroupMembership('principals/foo');
+ $this->assertSame($expectedResponse, $response);
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception
+ * @expectedExceptionMessage Principal not found
+ */
+ public function testGetGroupMembershipEmpty() {
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue(null));
+
+ $this->connector->getGroupMembership('principals/foo');
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception
+ * @expectedExceptionMessage Setting members of the group is not supported yet
+ */
+ public function testSetGroupMembership() {
+ $this->connector->setGroupMemberSet('principals/foo', ['foo']);
+ }
+
+ public function testUpdatePrincipal() {
+ $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch(array())));
+ }
+
+ public function testSearchPrincipals() {
+ $this->assertSame([], $this->connector->searchPrincipals('principals', []));
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/quotaplugin.php b/apps/dav/tests/unit/connector/sabre/quotaplugin.php
new file mode 100644
index 00000000000..5d3364e1f8c
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/quotaplugin.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class Test_OC_Connector_Sabre_QuotaPlugin extends \Test\TestCase {
+
+ /**
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var \OCA\DAV\Connector\Sabre\QuotaPlugin
+ */
+ private $plugin;
+
+ private function init($quota) {
+ $view = $this->buildFileViewMock($quota);
+ $this->server = new \Sabre\DAV\Server();
+ $this->plugin = new \OCA\DAV\Connector\Sabre\QuotaPlugin($view);
+ $this->plugin->initialize($this->server);
+ }
+
+ /**
+ * @dataProvider lengthProvider
+ */
+ public function testLength($expected, $headers) {
+ $this->init(0);
+ $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
+ $length = $this->plugin->getLength();
+ $this->assertEquals($expected, $length);
+ }
+
+ /**
+ * @dataProvider quotaOkayProvider
+ */
+ public function testCheckQuota($quota, $headers) {
+ $this->init($quota);
+
+ $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
+ $result = $this->plugin->checkQuota('');
+ $this->assertTrue($result);
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\InsufficientStorage
+ * @dataProvider quotaExceededProvider
+ */
+ public function testCheckExceededQuota($quota, $headers) {
+ $this->init($quota);
+
+ $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
+ $this->plugin->checkQuota('');
+ }
+
+ public function quotaOkayProvider() {
+ return array(
+ array(1024, array()),
+ array(1024, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
+ array(1024, array('CONTENT-LENGTH' => '512')),
+ array(1024, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
+ // \OCP\Files\FileInfo::SPACE-UNKNOWN = -2
+ array(-2, array()),
+ array(-2, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
+ array(-2, array('CONTENT-LENGTH' => '512')),
+ array(-2, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
+ );
+ }
+
+ public function quotaExceededProvider() {
+ return array(
+ array(1023, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
+ array(511, array('CONTENT-LENGTH' => '512')),
+ array(2047, array('OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024')),
+ );
+ }
+
+ public function lengthProvider() {
+ return array(
+ array(null, array()),
+ array(1024, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
+ array(512, array('CONTENT-LENGTH' => '512')),
+ array(2048, array('OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024')),
+ array(4096, array('OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => '4096')),
+ );
+ }
+
+ private function buildFileViewMock($quota) {
+ // mock filesysten
+ $view = $this->getMock('\OC\Files\View', array('free_space'), array(), '', false);
+ $view->expects($this->any())
+ ->method('free_space')
+ ->with($this->identicalTo(''))
+ ->will($this->returnValue($quota));
+
+ return $view;
+ }
+
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php b/apps/dav/tests/unit/connector/sabre/requesttest/auth.php
new file mode 100644
index 00000000000..41b554d11db
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/auth.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
+
+use Sabre\DAV\Auth\Backend\BackendInterface;
+
+class Auth implements BackendInterface {
+ /**
+ * @var string
+ */
+ private $user;
+
+ /**
+ * @var string
+ */
+ private $password;
+
+ /**
+ * Auth constructor.
+ *
+ * @param string $user
+ * @param string $password
+ */
+ public function __construct($user, $password) {
+ $this->user = $user;
+ $this->password = $password;
+ }
+
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param \Sabre\DAV\Server $server
+ * @param string $realm
+ * @return bool
+ */
+ function authenticate(\Sabre\DAV\Server $server, $realm) {
+ $userSession = \OC::$server->getUserSession();
+ $result = $userSession->login($this->user, $this->password);
+ if ($result) {
+ //we need to pass the user name, which may differ from login name
+ $user = $userSession->getUser()->getUID();
+ \OC_Util::setupFS($user);
+ //trigger creation of user home and /files folder
+ \OC::$server->getUserFolder($user);
+ }
+ return $result;
+ }
+
+ /**
+ * Returns information about the currently logged in username.
+ *
+ * If nobody is currently logged in, this method should return null.
+ *
+ * @return string|null
+ */
+ function getCurrentUser() {
+ return $this->user;
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php
new file mode 100644
index 00000000000..245deff3b31
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
+
+use OCP\AppFramework\Http;
+use OCP\Lock\ILockingProvider;
+
+class DownloadTest extends RequestTest {
+ public function testDownload() {
+ $user = $this->getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'bar');
+
+ $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt');
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ $this->assertEquals(stream_get_contents($response->getBody()), 'bar');
+ }
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked
+ */
+ public function testDownloadWriteLocked() {
+ $user = $this->getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'bar');
+
+ $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE);
+
+ $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd');
+ }
+
+ public function testDownloadReadLocked() {
+ $user = $this->getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'bar');
+
+ $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED);
+
+ $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd');
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ $this->assertEquals(stream_get_contents($response->getBody()), 'bar');
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php
new file mode 100644
index 00000000000..ed1d6046d75
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
+
+use OC\Files\View;
+use Test\Traits\EncryptionTrait;
+
+class EncryptionUploadTest extends UploadTest {
+ use EncryptionTrait;
+
+ protected function setupUser($name, $password) {
+ $this->createUser($name, $password);
+ $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+ $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
+ $this->setupForUser($name, $password);
+ $this->loginWithEncryption($name);
+ return new View('/' . $name . '/files');
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php b/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php
new file mode 100644
index 00000000000..53cd186bbc8
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
+
+use Sabre\DAV\Exception;
+
+class ExceptionPlugin extends \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin {
+ /**
+ * @var \Exception[]
+ */
+ protected $exceptions = [];
+
+ public function logException(\Exception $ex) {
+ $exceptionClass = get_class($ex);
+ if (!isset($this->nonFatalExceptions[$exceptionClass])) {
+ $this->exceptions[] = $ex;
+ }
+ }
+
+ /**
+ * @return \Exception[]
+ */
+ public function getExceptions() {
+ return $this->exceptions;
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php b/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php
new file mode 100644
index 00000000000..d90cf6e19bc
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php
@@ -0,0 +1,129 @@
+<?php
+/**
+ * Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
+
+use OCA\DAV\Connector\Sabre\Server;
+use OCA\DAV\Connector\Sabre\ServerFactory;
+use OC\Files\Mount\MountPoint;
+use OC\Files\Storage\StorageFactory;
+use OC\Files\Storage\Temporary;
+use OC\Files\View;
+use OCP\IUser;
+use Sabre\HTTP\Request;
+use Test\TestCase;
+use Test\Traits\MountProviderTrait;
+use Test\Traits\UserTrait;
+
+abstract class RequestTest extends TestCase {
+ use UserTrait;
+ use MountProviderTrait;
+
+ /**
+ * @var \OCA\DAV\Connector\Sabre\ServerFactory
+ */
+ protected $serverFactory;
+
+ protected function getStream($string) {
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $string);
+ fseek($stream, 0);
+ return $stream;
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->serverFactory = new ServerFactory(
+ \OC::$server->getConfig(),
+ \OC::$server->getLogger(),
+ \OC::$server->getDatabaseConnection(),
+ \OC::$server->getUserSession(),
+ \OC::$server->getMountManager(),
+ \OC::$server->getTagManager(),
+ \OC::$server->getEventDispatcher()
+ );
+ }
+
+ protected function setupUser($name, $password) {
+ $this->createUser($name, $password);
+ $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+ $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
+ $this->loginAsUser($name);
+ return new View('/' . $name . '/files');
+ }
+
+ /**
+ * @param \OC\Files\View $view the view to run the webdav server against
+ * @param string $user
+ * @param string $password
+ * @param string $method
+ * @param string $url
+ * @param resource|string|null $body
+ * @param array|null $headers
+ * @return \Sabre\HTTP\Response
+ */
+ protected function request($view, $user, $password, $method, $url, $body = null, $headers = null) {
+ if (is_string($body)) {
+ $body = $this->getStream($body);
+ }
+ $this->logout();
+ $exceptionPlugin = new ExceptionPlugin('webdav', null);
+ $server = $this->getSabreServer($view, $user, $password, $exceptionPlugin);
+ $request = new Request($method, $url, $headers, $body);
+
+ // since sabre catches all exceptions we need to save them and throw them from outside the sabre server
+
+ $originalServer = $_SERVER;
+
+ if (is_array($headers)) {
+ foreach ($headers as $header => $value) {
+ $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $header))] = $value;
+ }
+ }
+
+ $result = $this->makeRequest($server, $request);
+
+ foreach ($exceptionPlugin->getExceptions() as $exception) {
+ throw $exception;
+ }
+ $_SERVER = $originalServer;
+ return $result;
+ }
+
+ /**
+ * @param Server $server
+ * @param Request $request
+ * @return \Sabre\HTTP\Response
+ */
+ protected function makeRequest(Server $server, Request $request) {
+ $sapi = new Sapi($request);
+ $server->sapi = $sapi;
+ $server->httpRequest = $request;
+ $server->exec();
+ return $sapi->getResponse();
+ }
+
+ /**
+ * @param View $view
+ * @param string $user
+ * @param string $password
+ * @param ExceptionPlugin $exceptionPlugin
+ * @return Server
+ */
+ protected function getSabreServer(View $view, $user, $password, ExceptionPlugin $exceptionPlugin) {
+ $authBackend = new Auth($user, $password);
+
+ $server = $this->serverFactory->createServer('/', 'dummy', $authBackend, function () use ($view) {
+ return $view;
+ });
+ $server->addPlugin($exceptionPlugin);
+
+ return $server;
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php b/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php
new file mode 100644
index 00000000000..3af94010288
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
+
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+
+class Sapi {
+ /**
+ * @var \Sabre\HTTP\Request
+ */
+ private $request;
+
+ /**
+ * @var \Sabre\HTTP\Response
+ */
+ private $response;
+
+ /**
+ * This static method will create a new Request object, based on the
+ * current PHP request.
+ *
+ * @return \Sabre\HTTP\Request
+ */
+ public function getRequest() {
+ return $this->request;
+ }
+
+ public function __construct(Request $request) {
+ $this->request = $request;
+ }
+
+ /**
+ * @param \Sabre\HTTP\Response $response
+ * @return void
+ */
+ public function sendResponse(Response $response) {
+ // we need to copy the body since we close the source stream
+ $copyStream = fopen('php://temp', 'r+');
+ if (is_string($response->getBody())) {
+ fwrite($copyStream, $response->getBody());
+ } else if (is_resource($response->getBody())) {
+ stream_copy_to_stream($response->getBody(), $copyStream);
+ }
+ rewind($copyStream);
+ $this->response = new Response($response->getStatus(), $response->getHeaders(), $copyStream);
+ }
+
+ /**
+ * @return \Sabre\HTTP\Response
+ */
+ public function getResponse() {
+ return $this->response;
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php
new file mode 100644
index 00000000000..9a067f230a3
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
+
+use OC\AppFramework\Http;
+
+class UploadTest extends RequestTest {
+ public function testBasicUpload() {
+ $user = $this->getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $this->assertFalse($view->file_exists('foo.txt'));
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
+
+ $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
+ $this->assertTrue($view->file_exists('foo.txt'));
+ $this->assertEquals('asd', $view->file_get_contents('foo.txt'));
+
+ $info = $view->getFileInfo('foo.txt');
+ $this->assertInstanceOf('\OC\Files\FileInfo', $info);
+ $this->assertEquals(3, $info->getSize());
+ }
+
+ public function testUploadOverWrite() {
+ $user = $this->getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'foobar');
+
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
+
+ $this->assertEquals(Http::STATUS_NO_CONTENT, $response->getStatus());
+ $this->assertEquals('asd', $view->file_get_contents('foo.txt'));
+
+ $info = $view->getFileInfo('foo.txt');
+ $this->assertInstanceOf('\OC\Files\FileInfo', $info);
+ $this->assertEquals(3, $info->getSize());
+ }
+
+ public function testChunkedUpload() {
+ $user = $this->getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $this->assertFalse($view->file_exists('foo.txt'));
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
+
+ $this->assertEquals(201, $response->getStatus());
+ $this->assertFalse($view->file_exists('foo.txt'));
+
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
+
+ $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
+ $this->assertTrue($view->file_exists('foo.txt'));
+
+ $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
+
+ $info = $view->getFileInfo('foo.txt');
+ $this->assertInstanceOf('\OC\Files\FileInfo', $info);
+ $this->assertEquals(6, $info->getSize());
+ }
+
+ public function testChunkedUploadOverWrite() {
+ $user = $this->getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $view->file_put_contents('foo.txt', 'bar');
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
+
+ $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
+ $this->assertEquals('bar', $view->file_get_contents('foo.txt'));
+
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
+
+ $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
+
+ $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
+
+ $info = $view->getFileInfo('foo.txt');
+ $this->assertInstanceOf('\OC\Files\FileInfo', $info);
+ $this->assertEquals(6, $info->getSize());
+ }
+
+ public function testChunkedUploadOutOfOrder() {
+ $user = $this->getUniqueID();
+ $view = $this->setupUser($user, 'pass');
+
+ $this->assertFalse($view->file_exists('foo.txt'));
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
+
+ $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
+ $this->assertFalse($view->file_exists('foo.txt'));
+
+ $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
+
+ $this->assertEquals(201, $response->getStatus());
+ $this->assertTrue($view->file_exists('foo.txt'));
+
+ $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
+
+ $info = $view->getFileInfo('foo.txt');
+ $this->assertInstanceOf('\OC\Files\FileInfo', $info);
+ $this->assertEquals(6, $info->getSize());
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/tagsplugin.php b/apps/dav/tests/unit/connector/sabre/tagsplugin.php
new file mode 100644
index 00000000000..4731e770cfa
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/tagsplugin.php
@@ -0,0 +1,398 @@
+<?php
+
+namespace Tests\Connector\Sabre;
+
+/**
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class TagsPlugin extends \Test\TestCase {
+
+ const TAGS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\TagsPlugin::TAGS_PROPERTYNAME;
+ const FAVORITE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\TagsPlugin::FAVORITE_PROPERTYNAME;
+ const TAG_FAVORITE = \OCA\DAV\Connector\Sabre\TagsPlugin::TAG_FAVORITE;
+
+ /**
+ * @var \Sabre\DAV\Server
+ */
+ private $server;
+
+ /**
+ * @var \Sabre\DAV\ObjectTree
+ */
+ private $tree;
+
+ /**
+ * @var \OCP\ITagManager
+ */
+ private $tagManager;
+
+ /**
+ * @var \OCP\ITags
+ */
+ private $tagger;
+
+ /**
+ * @var \OCA\DAV\Connector\Sabre\TagsPlugin
+ */
+ private $plugin;
+
+ public function setUp() {
+ parent::setUp();
+ $this->server = new \Sabre\DAV\Server();
+ $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->tagger = $this->getMock('\OCP\ITags');
+ $this->tagManager = $this->getMock('\OCP\ITagManager');
+ $this->tagManager->expects($this->any())
+ ->method('load')
+ ->with('files')
+ ->will($this->returnValue($this->tagger));
+ $this->plugin = new \OCA\DAV\Connector\Sabre\TagsPlugin($this->tree, $this->tagManager);
+ $this->plugin->initialize($this->server);
+ }
+
+ /**
+ * @dataProvider tagsGetPropertiesDataProvider
+ */
+ public function testGetProperties($tags, $requestedProperties, $expectedProperties) {
+ $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(123));
+
+ $expectedCallCount = 0;
+ if (count($requestedProperties) > 0) {
+ $expectedCallCount = 1;
+ }
+
+ $this->tagger->expects($this->exactly($expectedCallCount))
+ ->method('getTagsForObjects')
+ ->with($this->equalTo(array(123)))
+ ->will($this->returnValue(array(123 => $tags)));
+
+ $propFind = new \Sabre\DAV\PropFind(
+ '/dummyPath',
+ $requestedProperties,
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $result = $propFind->getResultForMultiStatus();
+
+ $this->assertEmpty($result[404]);
+ unset($result[404]);
+ $this->assertEquals($expectedProperties, $result);
+ }
+
+ /**
+ * @dataProvider tagsGetPropertiesDataProvider
+ */
+ public function testPreloadThenGetProperties($tags, $requestedProperties, $expectedProperties) {
+ $node1 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node1->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(111));
+ $node2 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node2->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(222));
+
+ $expectedCallCount = 0;
+ if (count($requestedProperties) > 0) {
+ // this guarantees that getTagsForObjects
+ // is only called once and then the tags
+ // are cached
+ $expectedCallCount = 1;
+ }
+
+ $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(123));
+ $node->expects($this->exactly($expectedCallCount))
+ ->method('getChildren')
+ ->will($this->returnValue(array($node1, $node2)));
+
+ $this->tagger->expects($this->exactly($expectedCallCount))
+ ->method('getTagsForObjects')
+ ->with($this->equalTo(array(123, 111, 222)))
+ ->will($this->returnValue(
+ array(
+ 111 => $tags,
+ 123 => $tags
+ )
+ ));
+
+ // simulate sabre recursive PROPFIND traversal
+ $propFindRoot = new \Sabre\DAV\PropFind(
+ '/subdir',
+ $requestedProperties,
+ 1
+ );
+ $propFind1 = new \Sabre\DAV\PropFind(
+ '/subdir/test.txt',
+ $requestedProperties,
+ 0
+ );
+ $propFind2 = new \Sabre\DAV\PropFind(
+ '/subdir/test2.txt',
+ $requestedProperties,
+ 0
+ );
+
+ $this->plugin->handleGetProperties(
+ $propFindRoot,
+ $node
+ );
+ $this->plugin->handleGetProperties(
+ $propFind1,
+ $node1
+ );
+ $this->plugin->handleGetProperties(
+ $propFind2,
+ $node2
+ );
+
+ $result = $propFind1->getResultForMultiStatus();
+
+ $this->assertEmpty($result[404]);
+ unset($result[404]);
+ $this->assertEquals($expectedProperties, $result);
+ }
+
+ function tagsGetPropertiesDataProvider() {
+ return array(
+ // request both, receive both
+ array(
+ array('tag1', 'tag2', self::TAG_FAVORITE),
+ array(self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME),
+ array(
+ 200 => array(
+ self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2')),
+ self::FAVORITE_PROPERTYNAME => true,
+ )
+ )
+ ),
+ // request tags alone
+ array(
+ array('tag1', 'tag2', self::TAG_FAVORITE),
+ array(self::TAGS_PROPERTYNAME),
+ array(
+ 200 => array(
+ self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2')),
+ )
+ )
+ ),
+ // request fav alone
+ array(
+ array('tag1', 'tag2', self::TAG_FAVORITE),
+ array(self::FAVORITE_PROPERTYNAME),
+ array(
+ 200 => array(
+ self::FAVORITE_PROPERTYNAME => true,
+ )
+ )
+ ),
+ // request none
+ array(
+ array('tag1', 'tag2', self::TAG_FAVORITE),
+ array(),
+ array(
+ 200 => array()
+ ),
+ ),
+ // request both with none set, receive both
+ array(
+ array(),
+ array(self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME),
+ array(
+ 200 => array(
+ self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array()),
+ self::FAVORITE_PROPERTYNAME => false,
+ )
+ )
+ ),
+ );
+ }
+
+ public function testUpdateTags() {
+ // this test will replace the existing tags "tagremove" with "tag1" and "tag2"
+ // and keep "tagkeep"
+ $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(123));
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->returnValue($node));
+
+ $this->tagger->expects($this->at(0))
+ ->method('getTagsForObjects')
+ ->with($this->equalTo(array(123)))
+ ->will($this->returnValue(array(123 => array('tagkeep', 'tagremove', self::TAG_FAVORITE))));
+
+ // then tag as tag1 and tag2
+ $this->tagger->expects($this->at(1))
+ ->method('tagAs')
+ ->with(123, 'tag1');
+ $this->tagger->expects($this->at(2))
+ ->method('tagAs')
+ ->with(123, 'tag2');
+
+ // it will untag tag3
+ $this->tagger->expects($this->at(3))
+ ->method('unTag')
+ ->with(123, 'tagremove');
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch(array(
+ self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2', 'tagkeep'))
+ ));
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]);
+ $this->assertFalse(isset($result[self::FAVORITE_PROPERTYNAME]));
+ }
+
+ public function testUpdateTagsFromScratch() {
+ $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(123));
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->returnValue($node));
+
+ $this->tagger->expects($this->at(0))
+ ->method('getTagsForObjects')
+ ->with($this->equalTo(array(123)))
+ ->will($this->returnValue(array()));
+
+ // then tag as tag1 and tag2
+ $this->tagger->expects($this->at(1))
+ ->method('tagAs')
+ ->with(123, 'tag1');
+ $this->tagger->expects($this->at(2))
+ ->method('tagAs')
+ ->with(123, 'tag2');
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch(array(
+ self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2', 'tagkeep'))
+ ));
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]);
+ $this->assertFalse(false, isset($result[self::FAVORITE_PROPERTYNAME]));
+ }
+
+ public function testUpdateFav() {
+ // this test will replace the existing tags "tagremove" with "tag1" and "tag2"
+ // and keep "tagkeep"
+ $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue(123));
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/dummypath')
+ ->will($this->returnValue($node));
+
+ // set favorite tag
+ $this->tagger->expects($this->once())
+ ->method('tagAs')
+ ->with(123, self::TAG_FAVORITE);
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch(array(
+ self::FAVORITE_PROPERTYNAME => true
+ ));
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertFalse(false, isset($result[self::TAGS_PROPERTYNAME]));
+ $this->assertEquals(200, isset($result[self::FAVORITE_PROPERTYNAME]));
+
+ // unfavorite now
+ // set favorite tag
+ $this->tagger->expects($this->once())
+ ->method('unTag')
+ ->with(123, self::TAG_FAVORITE);
+
+ // properties to set
+ $propPatch = new \Sabre\DAV\PropPatch(array(
+ self::FAVORITE_PROPERTYNAME => false
+ ));
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ // all requested properties removed, as they were processed already
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertFalse(false, isset($result[self::TAGS_PROPERTYNAME]));
+ $this->assertEquals(200, isset($result[self::FAVORITE_PROPERTYNAME]));
+ }
+
+}
diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml
index 8586c6794f2..9ec6a9c43e7 100644
--- a/apps/files/appinfo/info.xml
+++ b/apps/files/appinfo/info.xml
@@ -12,10 +12,6 @@
<types>
<filesystem/>
</types>
- <remote>
- <files>appinfo/remote.php</files>
- <webdav>appinfo/remote.php</webdav>
- </remote>
<documentation>
<user>user-files</user>
</documentation>
diff --git a/apps/files/appinfo/remote.php b/apps/files/appinfo/remote.php
index 02f7f3c027f..d6a745d028e 100644
--- a/apps/files/appinfo/remote.php
+++ b/apps/files/appinfo/remote.php
@@ -32,7 +32,7 @@ set_time_limit(0);
// Turn off output buffering to prevent memory problems
\OC_Util::obEnd();
-$serverFactory = new \OC\Connector\Sabre\ServerFactory(
+$serverFactory = new \OCA\DAV\Connector\Sabre\ServerFactory(
\OC::$server->getConfig(),
\OC::$server->getLogger(),
\OC::$server->getDatabaseConnection(),
@@ -43,7 +43,7 @@ $serverFactory = new \OC\Connector\Sabre\ServerFactory(
);
// Backends
-$authBackend = new \OC\Connector\Sabre\Auth();
+$authBackend = new \OCA\DAV\Connector\Sabre\Auth();
$requestUri = \OC::$server->getRequest()->getRequestUri();
$server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, function() {
diff --git a/apps/files_sharing/appinfo/info.xml b/apps/files_sharing/appinfo/info.xml
index 28c64cbb1b4..cf69050a9f2 100644
--- a/apps/files_sharing/appinfo/info.xml
+++ b/apps/files_sharing/appinfo/info.xml
@@ -17,6 +17,5 @@ Turning the feature off removes shared files and folders on the server for all s
</types>
<public>
<files>public.php</files>
- <webdav>publicwebdav.php</webdav>
</public>
</info>