diff options
Diffstat (limited to 'apps/dav/lib/Connector/Sabre')
32 files changed, 5412 insertions, 0 deletions
diff --git a/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php b/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php new file mode 100644 index 00000000000..cb061d6a309 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php @@ -0,0 +1,90 @@ +<?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) 2016, 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..b8047e779f5 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Auth.php @@ -0,0 +1,229 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @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 Roeland Jago Douma <rullzer@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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 OC\AppFramework\Http\Request; +use OCP\IRequest; +use OCP\ISession; +use OC\User\Session; +use Sabre\DAV\Auth\Backend\AbstractBasic; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAV\Exception\ServiceUnavailable; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class Auth extends AbstractBasic { + const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND'; + + /** @var ISession */ + private $session; + /** @var Session */ + private $userSession; + /** @var IRequest */ + private $request; + /** @var string */ + private $currentUser; + + /** + * @param ISession $session + * @param Session $userSession + * @param IRequest $request + * @param string $principalPrefix + */ + public function __construct(ISession $session, + Session $userSession, + IRequest $request, + $principalPrefix = 'principals/users/') { + $this->session = $session; + $this->userSession = $userSession; + $this->request = $request; + $this->principalPrefix = $principalPrefix; + } + + /** + * 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 + */ + public function isDavAuthenticated($username) { + return !is_null($this->session->get(self::DAV_AUTHENTICATED)) && + $this->session->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 ($this->userSession->isLoggedIn() && + $this->isDavAuthenticated($this->userSession->getUser()->getUID()) + ) { + \OC_Util::setupFS($this->userSession->getUser()->getUID()); + $this->session->close(); + return true; + } else { + \OC_Util::setUpFS(); //login hooks may need early access to the filesystem + if($this->userSession->login($username, $password)) { + $this->userSession->createSessionToken($this->request, $username, $password); + \OC_Util::setUpFS($this->userSession->getUser()->getUID()); + $this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID()); + $this->session->close(); + return true; + } else { + $this->session->close(); + return false; + } + } + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + * @throws NotAuthenticated + * @throws ServiceUnavailable + */ + function check(RequestInterface $request, ResponseInterface $response) { + try { + $result = $this->auth($request, $response); + return $result; + } catch (NotAuthenticated $e) { + throw $e; + } catch (Exception $e) { + $class = get_class($e); + $msg = $e->getMessage(); + throw new ServiceUnavailable("$class: $msg"); + } + } + + /** + * Checks whether a CSRF check is required on the request + * + * @return bool + */ + private function requiresCSRFCheck() { + // GET requires no check at all + if($this->request->getMethod() === 'GET') { + return false; + } + + // Official ownCloud clients require no checks + if($this->request->isUserAgent([ + Request::USER_AGENT_OWNCLOUD_DESKTOP, + Request::USER_AGENT_OWNCLOUD_ANDROID, + Request::USER_AGENT_OWNCLOUD_IOS, + ])) { + return false; + } + + // If not logged-in no check is required + if(!$this->userSession->isLoggedIn()) { + return false; + } + + // POST always requires a check + if($this->request->getMethod() === 'POST') { + return true; + } + + // If logged-in AND DAV authenticated no check is required + if($this->userSession->isLoggedIn() && + $this->isDavAuthenticated($this->userSession->getUser()->getUID())) { + return false; + } + + return true; + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + * @throws NotAuthenticated + */ + private function auth(RequestInterface $request, ResponseInterface $response) { + $forcedLogout = false; + if(!$this->request->passesCSRFCheck() && + $this->requiresCSRFCheck()) { + // In case of a fail with POST we need to recheck the credentials + if($this->request->getMethod() === 'POST') { + $forcedLogout = true; + } else { + $response->setStatus(401); + throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.'); + } + } + + if($forcedLogout) { + $this->userSession->logout(); + } else { + if (\OC_User::handleApacheAuth() || + //Fix for broken webdav clients + ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) || + //Well behaved clients that only send the cookie are allowed + ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null) + ) { + $user = $this->userSession->getUser()->getUID(); + \OC_Util::setupFS($user); + $this->currentUser = $user; + $this->session->close(); + return [true, $this->principalPrefix . $user]; + } + } + + if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With')))) { + // do not re-authenticate over ajax, use dummy auth name to prevent browser popup + $response->addHeader('WWW-Authenticate','DummyBasic realm="' . $this->realm . '"'); + $response->setStatus(401); + throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); + } + + $data = parent::check($request, $response); + if($data[0] === true) { + $startPos = strrpos($data[1], '/') + 1; + $user = $this->userSession->getUser()->getUID(); + $data[1] = substr_replace($data[1], $user, $startPos); + } + return $data; + } +} diff --git a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php new file mode 100644 index 00000000000..70d19cb7f2a --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php @@ -0,0 +1,80 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, 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/ChecksumList.php b/apps/dav/lib/Connector/Sabre/ChecksumList.php new file mode 100644 index 00000000000..f137222acca --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ChecksumList.php @@ -0,0 +1,71 @@ +<?php +/** + * @author Roeland Jago Douma <rullzer@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Xml\XmlSerializable; +use Sabre\Xml\Element; +use Sabre\Xml\Writer; + +/** + * Checksumlist property + * + * This property contains multiple "checksum" elements, each containing a + * checksum name. + */ +class ChecksumList implements XmlSerializable { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** @var string[] of TYPE:CHECKSUM */ + private $checksums; + + /** + * @param string $checksum + */ + public function __construct($checksum) { + $this->checksums = explode(',', $checksum); + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->checksums as $checksum) { + $writer->writeElement('{' . self::NS_OWNCLOUD . '}checksum', $checksum); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php new file mode 100644 index 00000000000..a8d5f771122 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php @@ -0,0 +1,130 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Comments\ICommentsManager; +use OCP\IUserSession; +use Sabre\DAV\PropFind; +use Sabre\DAV\ServerPlugin; + +class CommentPropertiesPlugin extends ServerPlugin { + + const PROPERTY_NAME_HREF = '{http://owncloud.org/ns}comments-href'; + const PROPERTY_NAME_COUNT = '{http://owncloud.org/ns}comments-count'; + const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread'; + + /** @var \Sabre\DAV\Server */ + protected $server; + + /** @var ICommentsManager */ + private $commentsManager; + + /** @var IUserSession */ + private $userSession; + + public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) { + $this->commentsManager = $commentsManager; + $this->userSession = $userSession; + } + + /** + * 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 + */ + function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('propFind', array($this, 'handleGetProperties')); + } + + /** + * 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 File) && !($node instanceof Directory)) { + return; + } + + $propFind->handle(self::PROPERTY_NAME_COUNT, function() use ($node) { + return $this->commentsManager->getNumberOfCommentsForObject('files', strval($node->getId())); + }); + + $propFind->handle(self::PROPERTY_NAME_HREF, function() use ($node) { + return $this->getCommentsLink($node); + }); + + $propFind->handle(self::PROPERTY_NAME_UNREAD, function() use ($node) { + return $this->getUnreadCount($node); + }); + } + + /** + * returns a reference to the comments node + * + * @param Node $node + * @return mixed|string + */ + public function getCommentsLink(Node $node) { + $href = $this->server->getBaseUri(); + $entryPoint = strrpos($href, '/webdav/'); + if($entryPoint === false) { + // in case we end up somewhere else, unexpectedly. + return null; + } + $href = substr_replace($href, '/dav/', $entryPoint); + $href .= 'comments/files/' . rawurldecode($node->getId()); + return $href; + } + + /** + * returns the number of unread comments for the currently logged in user + * on the given file or directory node + * + * @param Node $node + * @return Int|null + */ + public function getUnreadCount(Node $node) { + $user = $this->userSession->getUser(); + if(is_null($user)) { + return null; + } + + $lastRead = $this->commentsManager->getReadMark('files', strval($node->getId()), $user); + + return $this->commentsManager->getNumberOfCommentsForObject('files', strval($node->getId()), $lastRead); + } + +} diff --git a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php new file mode 100644 index 00000000000..49b6a7b2de7 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php @@ -0,0 +1,57 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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..5946c9910d4 --- /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) 2016, 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/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php new file mode 100644 index 00000000000..f5699b469c3 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php @@ -0,0 +1,72 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Exception\NotFound; +use Sabre\DAV\IFile; +use Sabre\DAV\INode; +use \Sabre\DAV\PropFind; +use \Sabre\DAV\PropPatch; +use Sabre\DAVACL\Exception\NeedPrivileges; +use \Sabre\HTTP\RequestInterface; +use \Sabre\HTTP\ResponseInterface; +use Sabre\HTTP\URLUtil; + +/** + * Class DavAclPlugin is a wrapper around \Sabre\DAVACL\Plugin that returns 404 + * responses in case the resource to a response has been forbidden instead of + * a 403. This is used to prevent enumeration of valid resources. + * + * @see https://github.com/owncloud/core/issues/22578 + * @package OCA\DAV\Connector\Sabre + */ +class DavAclPlugin extends \Sabre\DAVACL\Plugin { + public function __construct() { + $this->hideNodesFromListings = true; + } + + function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) { + $access = parent::checkPrivileges($uri, $privileges, $recursion, false); + if($access === false && $throwExceptions) { + /** @var INode $node */ + $node = $this->server->tree->getNodeForPath($uri); + + switch(get_class($node)) { + case 'OCA\DAV\CardDAV\AddressBook': + $type = 'Addressbook'; + break; + default: + $type = 'Node'; + break; + } + throw new NotFound( + sprintf( + "%s with name '%s' could not be found", + $type, + $node->getName() + ) + ); + } + + return $access; + } +} diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php new file mode 100644 index 00000000000..daa5f29ce79 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -0,0 +1,310 @@ +<?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) 2016, 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\Forbidden; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Connector\Sabre\Exception\FileLocked; +use OCP\Files\ForbiddenException; +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; + + /** + * @var ObjectTree|null + */ + private $tree; + + /** + * Sets up the node, expects a full path name + * + * @param \OC\Files\View $view + * @param \OCP\Files\FileInfo $info + * @param ObjectTree|null $tree + * @param \OCP\Share\IManager $shareManager + */ + public function __construct($view, $info, $tree = null, $shareManager = null) { + parent::__construct($view, $info, $shareManager); + $this->tree = $tree; + } + + /** + * 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 (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } 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 (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } 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, $this->tree, $this->shareManager); + } else { + $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager); + } + if ($this->tree) { + $this->tree->cacheNode($node); + } + 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 (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } 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); + if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) { + $free = \OCP\Files\FileInfo::SPACE_UNLIMITED; + } else { + $free = $storageInfo['free']; + } + $this->quotaInfo = array( + $storageInfo['used'], + $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..b10d5aaab36 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php @@ -0,0 +1,70 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, 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..d16e93bb637 --- /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) 2016, 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..03ced0e81e2 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php @@ -0,0 +1,48 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Owen Winkler <a_github@midnightcircus.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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/Forbidden.php b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php new file mode 100644 index 00000000000..f2467e6b298 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php @@ -0,0 +1,64 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, 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; + +class Forbidden extends \Sabre\DAV\Exception\Forbidden { + + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * @var bool + */ + private $retry; + + /** + * @param string $message + * @param bool $retry + * @param \Exception $previous + */ + public function __construct($message, $retry = false, \Exception $previous = null) { + parent::__construct($message, 0, $previous); + $this->retry = $retry; + } + + /** + * 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/InvalidPath.php b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php new file mode 100644 index 00000000000..7951a0a89b7 --- /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) 2016, 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..99e3c222c75 --- /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) 2016, 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..38bddef8769 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php @@ -0,0 +1,111 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Pierre Jochem <pierrejochem@msn.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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"; + } + + $user = \OC_User::getUser(); + + $exception = [ + 'Message' => $message, + 'Exception' => $exceptionClass, + 'Code' => $ex->getCode(), + 'Trace' => $ex->getTraceAsString(), + 'File' => $ex->getFile(), + 'Line' => $ex->getLine(), + 'User' => $user, + ]; + $this->logger->log($level, 'Exception: ' . json_encode($exception), ['app' => $this->appName]); + } +} diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php new file mode 100644 index 00000000000..6db8740bc13 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php @@ -0,0 +1,156 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, 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\Locks\LockInfo; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Xml\Property\LockDiscovery; +use Sabre\DAV\Xml\Property\SupportedLock; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\DAV\PropFind; +use Sabre\DAV\INode; + +/** + * Class FakeLockerPlugin is a plugin only used when connections come in from + * OS X via Finder. The fake locking plugin does emulate Class 2 WebDAV support + * (locking of files) which allows Finder to access the storage in write mode as + * well. + * + * No real locking is performed, instead the plugin just returns always positive + * responses. + * + * @see https://github.com/owncloud/core/issues/17732 + * @package OCA\DAV\Connector\Sabre + */ +class FakeLockerPlugin extends ServerPlugin { + /** @var \Sabre\DAV\Server */ + private $server; + + /** {@inheritDoc} */ + public function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1); + $this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1); + $server->on('propFind', [$this, 'propFind']); + $server->on('validateTokens', [$this, 'validateTokens']); + } + + /** + * Indicate that we support LOCK and UNLOCK + * + * @param string $path + * @return string[] + */ + public function getHTTPMethods($path) { + return [ + 'LOCK', + 'UNLOCK', + ]; + } + + /** + * Indicate that we support locking + * + * @return integer[] + */ + function getFeatures() { + return [2]; + } + + /** + * Return some dummy response for PROPFIND requests with regard to locking + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + $propFind->handle('{DAV:}supportedlock', function() { + return new SupportedLock(true); + }); + $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) { + return new LockDiscovery([]); + }); + } + + /** + * Mark a locking token always as valid + * + * @param RequestInterface $request + * @param array $conditions + */ + public function validateTokens(RequestInterface $request, &$conditions) { + foreach($conditions as &$fileCondition) { + if(isset($fileCondition['tokens'])) { + foreach($fileCondition['tokens'] as &$token) { + if(isset($token['token'])) { + if(substr($token['token'], 0, 16) === 'opaquelocktoken:') { + $token['validToken'] = true; + } + } + } + } + } + } + + /** + * Fakes a successful LOCK + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function fakeLockProvider(RequestInterface $request, + ResponseInterface $response) { + + $lockInfo = new LockInfo(); + $lockInfo->token = md5($request->getPath()); + $lockInfo->uri = $request->getPath(); + $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY; + $lockInfo->timeout = 1800; + + $body = $this->server->xml->write('{DAV:}prop', [ + '{DAV:}lockdiscovery' => + new LockDiscovery([$lockInfo]) + ]); + + $response->setBody($body); + + return false; + } + + /** + * Fakes a successful LOCK + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function fakeUnlockProvider(RequestInterface $request, + ResponseInterface $response) { + $response->setStatus(204); + $response->setHeader('Content-Length', '0'); + return false; + } +} diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php new file mode 100644 index 00000000000..943e9150e74 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -0,0 +1,567 @@ +<?php +/** + * @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 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 Roeland Jago Douma <rullzer@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Forbidden as DAVForbiddenException; +use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; +use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\Files\EntityTooLargeException; +use OCP\Files\ForbiddenException; +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->getPartFileBasePath($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, $result) = \OC_Helper::streamCopy($data, $target); + fclose($target); + + if ($result === false) { + $expected = -1; + if (isset($_SERVER['CONTENT_LENGTH'])) { + $expected = $_SERVER['CONTENT_LENGTH']; + } + throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )'); + } + + // 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'] === 'PUT') { + $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 (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); + } catch (\Exception $e) { + $partStorage->unlink($internalPartPath); + $this->convertToSabreException($e); + } + } + + // since we skipped the view we need to scan and emit the hooks ourselves + $storage->getUpdater()->update($internalPath); + + try { + $this->changeLock(ILockingProvider::LOCK_SHARED); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + + 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(); + + if (isset($request->server['HTTP_OC_CHECKSUM'])) { + $checksum = trim($request->server['HTTP_OC_CHECKSUM']); + $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); + $this->refreshInfo(); + } else if ($this->getChecksum() !== null && $this->getChecksum() !== '') { + $this->fileView->putFileInfo($this->path, ['checksum' => '']); + $this->refreshInfo(); + } + + } catch (StorageNotAvailableException $e) { + throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage()); + } + + return '"' . $this->info->getEtag() . '"'; + } + + private function getPartFileBasePath($path) { + $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true); + if ($partFileInStorage) { + return $path; + } else { + return md5($path); // will place it in the root of the view with a unique name + } + } + + /** + * @param string $path + */ + 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; + } + + /** + * @param string $path + */ + 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 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 (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); + } 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 (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); + } 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 string + */ + 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::$server->getMimeTypeDetector()->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->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED); + + $this->emitPreHooks($exists, $targetPath); + $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE); + /** @var \OC\Files\Storage\Storage $targetStorage */ + list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); + + if ($needsPartFile) { + // we first assembly the target file as a part file + $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; + /** @var \OC\Files\Storage\Storage $targetStorage */ + list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile); + + + $chunk_handler->file_assemble($partStorage, $partInternalPath); + + // here is the final atomic rename + $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath); + $fileExists = $targetStorage->file_exists($targetInternalPath); + 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->fileView->changeLock($targetPath, 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); + } + + // 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'); + } + } + + // since we skipped the view we need to scan and emit the hooks ourselves + $targetStorage->getUpdater()->update($targetInternalPath); + + $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); + + $this->emitPostHooks($exists, $targetPath); + + // FIXME: should call refreshInfo but can't because $this->path is not the of the final file + $info = $this->fileView->getFileInfo($targetPath); + + if (isset($request->server['HTTP_OC_CHECKSUM'])) { + $checksum = trim($request->server['HTTP_OC_CHECKSUM']); + $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]); + } else if ($info->getChecksum() !== null && $info->getChecksum() !== '') { + $this->fileView->putFileInfo($this->path, ['checksum' => '']); + } + + $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED); + + 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 ForbiddenException) { + // the path for the file was forbidden + throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $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); + } + + /** + * Get the checksum for this file + * + * @return string + */ + public function getChecksum() { + return $this->info->getChecksum(); + } +} diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php new file mode 100644 index 00000000000..8822deb1661 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -0,0 +1,398 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\View; +use OCA\DAV\Upload\FutureFile; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\IFile; +use \Sabre\DAV\PropFind; +use \Sabre\DAV\PropPatch; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Tree; +use \Sabre\HTTP\RequestInterface; +use \Sabre\HTTP\ResponseInterface; +use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; + +class FilesPlugin extends ServerPlugin { + + // namespace + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id'; + const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid'; + const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions'; + const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-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'; + const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id'; + const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name'; + const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums'; + const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint'; + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @var Tree + */ + private $tree; + + /** + * Whether this is public webdav. + * If true, some returned information will be stripped off. + * + * @var bool + */ + private $isPublic; + + /** + * @var View + */ + private $fileView; + + /** + * @var bool + */ + private $downloadAttachment; + + /** + * @var IConfig + */ + private $config; + + /** + * @param Tree $tree + * @param View $view + * @param bool $isPublic + * @param bool $downloadAttachment + */ + public function __construct(Tree $tree, + View $view, + IConfig $config, + $isPublic = false, + $downloadAttachment = true) { + $this->tree = $tree; + $this->fileView = $view; + $this->config = $config; + $this->isPublic = $isPublic; + $this->downloadAttachment = $downloadAttachment; + } + + /** + * 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->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; + $server->protectedProperties[] = self::FILEID_PROPERTYNAME; + $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME; + $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME; + $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME; + $server->protectedProperties[] = self::SIZE_PROPERTYNAME; + $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME; + $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME; + $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME; + $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME; + $server->protectedProperties[] = self::DATA_FINGERPRINT_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('afterMethod:GET', array($this, 'handleDownloadToken')); + $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 Forbidden + * @throws NotFound + */ + function checkMove($source, $destination) { + $sourceNode = $this->tree->getNodeForPath($source); + if ($sourceNode instanceof FutureFile) { + return; + } + list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($source); + list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination); + + if ($sourceDir !== $destinationDir) { + $sourceFileInfo = $this->fileView->getFileInfo($source); + + if ($sourceFileInfo === false) { + throw new NotFound($source . ' does not exist'); + } + + if (!$sourceFileInfo->isDeletable()) { + throw new Forbidden($source . " cannot be deleted"); + } + } + } + + /** + * This sets a cookie to be able to recognize the start of the download + * the content must not be longer than 32 characters and must only contain + * alphanumeric characters + * + * @param RequestInterface $request + * @param ResponseInterface $response + */ + function handleDownloadToken(RequestInterface $request, ResponseInterface $response) { + $queryParams = $request->getQueryParameters(); + + /** + * this sets a cookie to be able to recognize the start of the download + * the content must not be longer than 32 characters and must only contain + * alphanumeric characters + */ + if (isset($queryParams['downloadStartSecret'])) { + $token = $queryParams['downloadStartSecret']; + if (!isset($token[32]) + && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) { + // FIXME: use $response->setHeader() instead + setcookie('ocDownloadStarted', $token, time() + 20, '/'); + } + } + } + + /** + * Add headers to file download + * + * @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; + + // adds a 'Content-Disposition: attachment' header + if ($this->downloadAttachment) { + $response->addHeader('Content-Disposition', 'attachment'); + } + + if ($node instanceof \OCA\DAV\Connector\Sabre\File) { + //Add OC-Checksum header + /** @var $node File */ + $checksum = $node->getChecksum(); + if ($checksum !== null && $checksum !== '') { + $response->addHeader('OC-Checksum', $checksum); + } + } + } + + /** + * Adds all ownCloud-specific properties + * + * @param PropFind $propFind + * @param \Sabre\DAV\INode $node + * @return void + */ + public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) { + + $httpRequest = $this->server->httpRequest; + + if ($node instanceof \OCA\DAV\Connector\Sabre\Node) { + + $propFind->handle(self::FILEID_PROPERTYNAME, function() use ($node) { + return $node->getFileId(); + }); + + $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function() use ($node) { + return $node->getInternalFileId(); + }); + + $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::SHARE_PERMISSIONS_PROPERTYNAME, function() use ($node, $httpRequest) { + return $node->getSharePermissions( + $httpRequest->getRawServerValue('PHP_AUTH_USER') + ); + }); + + $propFind->handle(self::GETETAG_PROPERTYNAME, function() use ($node) { + return $node->getETag(); + }); + + $propFind->handle(self::OWNER_ID_PROPERTYNAME, function() use ($node) { + $owner = $node->getOwner(); + return $owner->getUID(); + }); + $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function() use ($node) { + $owner = $node->getOwner(); + $displayName = $owner->getDisplayName(); + return $displayName; + }); + + $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) { + if ($node->getPath() === '/') { + return $this->config->getSystemValue('data-fingerprint', ''); + } + }); + } + + if ($node instanceof \OCA\DAV\Files\FilesHome) { + $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) { + return $this->config->getSystemValue('data-fingerprint', ''); + }); + } + + if ($node instanceof \OCA\DAV\Connector\Sabre\File) { + $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function() use ($node) { + /** @var $node \OCA\DAV\Connector\Sabre\File */ + try { + $directDownloadUrl = $node->getDirectDownload(); + if (isset($directDownloadUrl['url'])) { + return $directDownloadUrl['url']; + } + } catch (StorageNotAvailableException $e) { + return false; + } + return false; + }); + + $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function() use ($node) { + $checksum = $node->getChecksum(); + if ($checksum === NULL || $checksum === '') { + return null; + } + + return new ChecksumList($checksum); + }); + + } + + 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/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php new file mode 100644 index 00000000000..d4e1cbe3b20 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php @@ -0,0 +1,333 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\View; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\PreconditionFailed; +use Sabre\DAV\Exception\ReportNotSupported; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Tree; +use Sabre\DAV\Xml\Element\Response; +use Sabre\DAV\Xml\Response\MultiStatus; +use Sabre\DAV\PropFind; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\IUserSession; +use OCP\Files\Folder; +use OCP\IGroupManager; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\TagNotFoundException; + +class FilesReportPlugin extends ServerPlugin { + + // namespace + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + const REPORT_NAME = '{http://owncloud.org/ns}filter-files'; + const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag'; + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @var Tree + */ + private $tree; + + /** + * @var View + */ + private $fileView; + + /** + * @var ISystemTagManager + */ + private $tagManager; + + /** + * @var ISystemTagObjectMapper + */ + private $tagMapper; + + /** + * @var IUserSession + */ + private $userSession; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var Folder + */ + private $userFolder; + + /** + * @param Tree $tree + * @param View $view + */ + public function __construct(Tree $tree, + View $view, + ISystemTagManager $tagManager, + ISystemTagObjectMapper $tagMapper, + IUserSession $userSession, + IGroupManager $groupManager, + Folder $userFolder + ) { + $this->tree = $tree; + $this->fileView = $view; + $this->tagManager = $tagManager; + $this->tagMapper = $tagMapper; + $this->userSession = $userSession; + $this->groupManager = $groupManager; + $this->userFolder = $userFolder; + } + + /** + * 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->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; + + $this->server = $server; + $this->server->on('report', array($this, 'onReport')); + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * + * @param string $uri + * @return array + */ + public function getSupportedReportSet($uri) { + return [self::REPORT_NAME]; + } + + /** + * REPORT operations to look for files + * + * @param string $reportName + * @param [] $report + * @param string $uri + * @return bool + * @throws NotFound + * @throws ReportNotSupported + */ + public function onReport($reportName, $report, $uri) { + $reportTargetNode = $this->server->tree->getNodeForPath($uri); + if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) { + throw new ReportNotSupported(); + } + + $ns = '{' . $this::NS_OWNCLOUD . '}'; + $requestedProps = []; + $filterRules = []; + + // parse report properties and gather filter info + foreach ($report as $reportProps) { + $name = $reportProps['name']; + if ($name === $ns . 'filter-rules') { + $filterRules = $reportProps['value']; + } else if ($name === '{DAV:}prop') { + // propfind properties + foreach ($reportProps['value'] as $propVal) { + $requestedProps[] = $propVal['name']; + } + } + } + + if (empty($filterRules)) { + // an empty filter would return all existing files which would be slow + throw new BadRequest('Missing filter-rule block in request'); + } + + // gather all file ids matching filter + try { + $resultFileIds = $this->processFilterRules($filterRules); + } catch (TagNotFoundException $e) { + throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e); + } + + // find sabre nodes by file id, restricted to the root node path + $results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds); + + $responses = $this->prepareResponses($requestedProps, $results); + + $xml = $this->server->xml->write( + '{DAV:}multistatus', + new MultiStatus($responses) + ); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody($xml); + + return false; + } + + /** + * Find file ids matching the given filter rules + * + * @param array $filterRules + * @return array array of unique file id results + * + * @throws TagNotFoundException whenever a tag was not found + */ + protected function processFilterRules($filterRules) { + $ns = '{' . $this::NS_OWNCLOUD . '}'; + $resultFileIds = null; + $systemTagIds = []; + foreach ($filterRules as $filterRule) { + if ($filterRule['name'] === $ns . 'systemtag') { + $systemTagIds[] = $filterRule['value']; + } + } + + // check user permissions, if applicable + if (!$this->isAdmin()) { + // check visibility/permission + $tags = $this->tagManager->getTagsByIds($systemTagIds); + $unknownTagIds = []; + foreach ($tags as $tag) { + if (!$tag->isUserVisible()) { + $unknownTagIds[] = $tag->getId(); + } + } + + if (!empty($unknownTagIds)) { + throw new TagNotFoundException('Tag with ids ' . implode(', ', $unknownTagIds) . ' not found'); + } + } + + // fetch all file ids and intersect them + foreach ($systemTagIds as $systemTagId) { + $fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files'); + + if (empty($fileIds)) { + // This tag has no files, nothing can ever show up + return []; + } + + // first run ? + if ($resultFileIds === null) { + $resultFileIds = $fileIds; + } else { + $resultFileIds = array_intersect($resultFileIds, $fileIds); + } + + if (empty($resultFileIds)) { + // Empty intersection, nothing can show up anymore + return []; + } + } + return $resultFileIds; + } + + /** + * Prepare propfind response for the given nodes + * + * @param string[] $requestedProps requested properties + * @param Node[] nodes nodes for which to fetch and prepare responses + * @return Response[] + */ + public function prepareResponses($requestedProps, $nodes) { + $responses = []; + foreach ($nodes as $node) { + $propFind = new PropFind($node->getPath(), $requestedProps); + + $this->server->getPropertiesByNode($propFind, $node); + // copied from Sabre Server's getPropertiesForPath + $result = $propFind->getResultForMultiStatus(); + $result['href'] = $propFind->getPath(); + + $resourceType = $this->server->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result['href'] .= '/'; + } + + $responses[] = new Response( + rtrim($this->server->getBaseUri(), '/') . $node->getPath(), + $result, + 200 + ); + } + return $responses; + } + + /** + * Find Sabre nodes by file ids + * + * @param Node $rootNode root node for search + * @param array $fileIds file ids + * @return Node[] array of Sabre nodes + */ + public function findNodesByFileIds($rootNode, $fileIds) { + $folder = $this->userFolder; + if (trim($rootNode->getPath(), '/') !== '') { + $folder = $folder->get($rootNode->getPath()); + } + + $results = []; + foreach ($fileIds as $fileId) { + $entry = $folder->getById($fileId); + if ($entry) { + $entry = current($entry); + if ($entry instanceof \OCP\Files\File) { + $results[] = new File($this->fileView, $entry); + } else if ($entry instanceof \OCP\Files\Folder) { + $results[] = new Directory($this->fileView, $entry); + } + } + } + + return $results; + } + + /** + * Returns whether the currently logged in user is an administrator + */ + private function isAdmin() { + $user = $this->userSession->getUser(); + if ($user !== null) { + return $this->groupManager->isAdmin($user->getUID()); + } + return false; + } +} diff --git a/apps/dav/lib/Connector/Sabre/LockPlugin.php b/apps/dav/lib/Connector/Sabre/LockPlugin.php new file mode 100644 index 00000000000..66da39a57c8 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/LockPlugin.php @@ -0,0 +1,84 @@ +<?php +/** + * @author Robin Appelman <icewind@owncloud.com> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, 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\HTTP\RequestInterface; + +class LockPlugin extends ServerPlugin { + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * {@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 can't 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' || isset($_SERVER['HTTP_OC_CHUNKED'])) { + return; + } + try { + $node = $this->server->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' || isset($_SERVER['HTTP_OC_CHUNKED'])) { + return; + } + try { + $node = $this->server->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..6e9a5930b78 --- /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) 2016, 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..ccc035063cd --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Node.php @@ -0,0 +1,348 @@ +<?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) 2016, 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\Mount\MoveableMount; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; + + +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; + + /** + * @var IManager + */ + protected $shareManager; + + /** + * Sets up the node, expects a full path name + * + * @param \OC\Files\View $view + * @param \OCP\Files\FileInfo $info + * @param IManager $shareManager + */ + public function __construct($view, $info, IManager $shareManager = null) { + $this->fileView = $view; + $this->path = $this->fileView->getRelativePath($info->getPath()); + $this->info = $info; + if ($shareManager) { + $this->shareManager = $shareManager; + } else { + $this->shareManager = \OC::$server->getShareManager(); + } + } + + 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 integer + */ + 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 integer + */ + public function getInternalFileId() { + return $this->info->getId(); + } + + /** + * @param string $user + * @return int + */ + public function getSharePermissions($user) { + + // check of we access a federated share + if ($user !== null) { + try { + $share = $this->shareManager->getShareByToken($user); + return $share->getPermissions(); + } catch (ShareNotFound $e) { + // ignore + } + } + + $storage = $this->info->getStorage(); + + $path = $this->info->getInternalPath(); + + if ($storage->instanceOfStorage('\OC\Files\Storage\Shared')) { + /** @var \OC\Files\Storage\Shared $storage */ + $permissions = (int)$storage->getShare()->getPermissions(); + } else { + $permissions = $storage->getPermissions($path); + } + + /* + * We can always share non moveable mount points with DELETE and UPDATE + * Eventually we need to do this properly + */ + $mountpoint = $this->info->getMountPoint(); + if (!($mountpoint instanceof MoveableMount)) { + $mountpointpath = $mountpoint->getMountPoint(); + if (substr($mountpointpath, -1) === '/') { + $mountpointpath = substr($mountpointpath, 0, -1); + } + + if ($mountpointpath === $this->info->getPath()) { + $permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE; + } + } + + /* + * Files can't have create or delete permissions + */ + if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) { + $permissions &= ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE); + } + + return $permissions; + } + + /** + * @return string + */ + 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->isUpdateable()) { + $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; + } + + public function getOwner() { + return $this->info->getOwner(); + } + + 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..f38dfe679c7 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php @@ -0,0 +1,297 @@ +<?php +/** + * @author Björn Schießle <schiessle@owncloud.com> + * @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) 2016, 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\Forbidden; +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\ForbiddenException; +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; + } + + public function cacheNode(Node $node) { + $this->cache[trim($node->getPath(), '/')] = $node; + } + + /** + * Returns the INode object for the requested path + * + * @param string $path + * @return \Sabre\DAV\INode + * @throws InvalidPath + * @throws \Sabre\DAV\Exception\Locked + * @throws \Sabre\DAV\Exception\NotFound + * @throws \Sabre\DAV\Exception\ServiceUnavailable + */ + public function getNodeForPath($path) { + if (!$this->fileView) { + throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup'); + } + + $path = trim($path, '/'); + + if (isset($this->cache[$path])) { + return $this->cache[$path]; + } + + if ($path) { + try { + $this->fileView->verifyPath($path, basename($path)); + } catch (\OCP\Files\InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } + } + + // 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 && $storage->file_exists($internalPath)) { + /** + * @var \OC\Files\Storage\Storage $storage + */ + // get data directly + $data = $storage->getMetaData($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, $this); + } 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->getName() . ', 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 (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } 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 (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } 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..787bcdf469b --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -0,0 +1,234 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @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 Thomas Müller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use Sabre\DAV\Exception; +use \Sabre\DAV\PropPatch; +use Sabre\DAVACL\PrincipalBackend\BackendInterface; +use Sabre\HTTP\URLUtil; + +class Principal implements BackendInterface { + + /** @var IUserManager */ + private $userManager; + + /** @var IGroupManager */ + private $groupManager; + + /** @var string */ + private $principalPrefix; + + /** @var bool */ + private $hasGroups; + + /** + * @param IUserManager $userManager + * @param IGroupManager $groupManager + * @param string $principalPrefix + */ + public function __construct(IUserManager $userManager, + IGroupManager $groupManager, + $principalPrefix = 'principals/users/') { + $this->userManager = $userManager; + $this->groupManager = $groupManager; + $this->principalPrefix = trim($principalPrefix, '/'); + $this->hasGroups = ($principalPrefix === 'principals/users/'); + } + + /** + * 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 === $this->principalPrefix) { + foreach($this->userManager->search('') as $user) { + $principals[] = $this->userToPrincipal($user); + } + } + + 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) = URLUtil::splitPath($path); + + if ($prefix === $this->principalPrefix) { + $user = $this->userManager->get($name); + + if (!is_null($user)) { + return $this->userToPrincipal($user); + } + } + return null; + } + + /** + * Returns the list of members for a group-principal + * + * @param string $principal + * @return string[] + * @throws 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 Exception('Principal not found'); + } + + return [$principal['uri']]; + } + + /** + * Returns the list of groups a principal is a member of + * + * @param string $principal + * @param bool $needGroups + * @return array + * @throws Exception + */ + public function getGroupMembership($principal, $needGroups = false) { + list($prefix, $name) = URLUtil::splitPath($principal); + + if ($prefix === $this->principalPrefix) { + $user = $this->userManager->get($name); + if (!$user) { + throw new Exception('Principal not found'); + } + + if ($this->hasGroups || $needGroups) { + $groups = $this->groupManager->getUserGroups($user); + $groups = array_map(function($group) { + /** @var IGroup $group */ + return 'principals/groups/' . $group->getGID(); + }, $groups); + + return $groups; + } + } + return []; + } + + /** + * 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 string[] $members + * @throws Exception + */ + public function setGroupMemberSet($principal, array $members) { + throw new 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) { + if (substr($uri, 0, 7) === 'mailto:') { + $email = substr($uri, 7); + $users = $this->userManager->getByEmail($email); + if (count($users) === 1) { + return $this->principalPrefix . '/' . $users[0]->getUID(); + } + } + + return ''; + } + + /** + * @param IUser $user + * @return array + */ + protected function userToPrincipal($user) { + $userId = $user->getUID(); + $displayName = $user->getDisplayName(); + $principal = [ + 'uri' => $this->principalPrefix . '/' . $userId, + '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName, + ]; + + $email = $user->getEMailAddress(); + if (!empty($email)) { + $principal['{http://sabredav.org/ns}email-address'] = $email; + return $principal; + } + return $principal; + } + + public function getPrincipalPrefix() { + return $this->principalPrefix; + } + +} diff --git a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php new file mode 100644 index 00000000000..a093c52851c --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php @@ -0,0 +1,146 @@ +<?php +/** + * @author Felix Moeller <mail@felixmoeller.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author scambra <sergio@entrecables.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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 = $this->getFileChunking($info); + // subtract the already uploaded size to see whether + // there is still enough space for the remaining chunks + $length -= $chunkHandler->getCurrentSize(); + // use target file name for free space check in case of shared files + $uri = rtrim($parentUri, '/') . '/' . $info['name']; + } + $freeSpace = $this->getFreeSpace($uri); + if ($freeSpace !== \OCP\Files\FileInfo::SPACE_UNKNOWN && $length > $freeSpace) { + if (isset($chunkHandler)) { + $chunkHandler->cleanup(); + } + throw new \Sabre\DAV\Exception\InsufficientStorage(); + } + } + return true; + } + + public function getFileChunking($info) { + // FIXME: need a factory for better mocking support + return new \OC_FileChunking($info); + } + + 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 $uri + * @return mixed + */ + public function getFreeSpace($uri) { + try { + $freeSpace = $this->view->free_space(ltrim($uri, '/')); + 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..421fc64422d --- /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) 2016, 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..5853370778d --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -0,0 +1,184 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Files\BrowserErrorPagePlugin; +use OCP\Files\Mount\IMountManager; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ILogger; +use OCP\IRequest; +use OCP\ITagManager; +use OCP\IUserSession; +use Sabre\DAV\Auth\Backend\BackendInterface; + +class ServerFactory { + /** @var IConfig */ + private $config; + /** @var ILogger */ + private $logger; + /** @var IDBConnection */ + private $databaseConnection; + /** @var IUserSession */ + private $userSession; + /** @var IMountManager */ + private $mountManager; + /** @var ITagManager */ + private $tagManager; + /** @var IRequest */ + private $request; + + /** + * @param IConfig $config + * @param ILogger $logger + * @param IDBConnection $databaseConnection + * @param IUserSession $userSession + * @param IMountManager $mountManager + * @param ITagManager $tagManager + * @param IRequest $request + */ + public function __construct( + IConfig $config, + ILogger $logger, + IDBConnection $databaseConnection, + IUserSession $userSession, + IMountManager $mountManager, + ITagManager $tagManager, + IRequest $request + ) { + $this->config = $config; + $this->logger = $logger; + $this->databaseConnection = $databaseConnection; + $this->userSession = $userSession; + $this->mountManager = $mountManager; + $this->tagManager = $tagManager; + $this->request = $request; + } + + /** + * @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()); + // Some WebDAV clients do require Class 2 WebDAV support (locking), since + // we do not provide locking we emulate it using a fake locking plugin. + if($this->request->isUserAgent([ + '/WebDAVFS/', + '/Microsoft Office OneNote 2013/', + '/Microsoft-WebDAV-MiniRedir/', + ])) { + $server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin()); + } + + if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) { + $server->addPlugin(new BrowserErrorPagePlugin()); + } + + // wait with registering these until auth is handled and the filesystem is setup + $server->on('beforeMethod', function () use ($server, $objectTree, $viewCallBack) { + // ensure the skeleton is copied + $userFolder = \OC::$server->getUserFolder(); + + /** @var \OC\Files\View $view */ + $view = $viewCallBack($server); + $rootInfo = $view->getFileInfo(''); + + // Create ownCloud Dir + if ($rootInfo->getType() === 'dir') { + $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree); + } 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, + $this->config, + false, + !$this->config->getSystemValue('debug', false) + ) + ); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view)); + + if($this->userSession->isLoggedIn()) { + $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager)); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin( + $objectTree, + $this->userSession, + $userFolder, + \OC::$server->getShareManager() + )); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin(\OC::$server->getCommentsManager(), $this->userSession)); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesReportPlugin( + $objectTree, + $view, + \OC::$server->getSystemTagManager(), + \OC::$server->getSystemTagObjectMapper(), + $this->userSession, + \OC::$server->getGroupManager(), + $userFolder + )); + // 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/ShareTypeList.php b/apps/dav/lib/Connector/Sabre/ShareTypeList.php new file mode 100644 index 00000000000..763586412ad --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ShareTypeList.php @@ -0,0 +1,87 @@ +<?php +/** + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; + +/** + * ShareTypeList property + * + * This property contains multiple "share-type" elements, each containing a share type. + */ +class ShareTypeList implements Element { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * Share types + * + * @var int[] + */ + private $shareTypes; + + /** + * @param int[] $shareTypes + */ + public function __construct($shareTypes) { + $this->shareTypes = $shareTypes; + } + + /** + * Returns the share types + * + * @return int[] + */ + public function getShareTypes() { + return $this->shareTypes; + } + + /** + * The deserialize method is called during xml parsing. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + $shareTypes = []; + + foreach ($reader->parseInnerTree() as $elem) { + if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}share-type') { + $shareTypes[] = (int)$elem['value']; + } + } + return new self($shareTypes); + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + foreach ($this->shareTypes as $shareType) { + $writer->writeElement('{' . self::NS_OWNCLOUD . '}share-type', $shareType); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php new file mode 100644 index 00000000000..c76068969e9 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -0,0 +1,177 @@ +<?php +/** + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\PropFind; +use \Sabre\DAV\PropPatch; +use OCP\IUserSession; +use OCP\Share\IShare; +use OCA\DAV\Connector\Sabre\ShareTypeList; + +/** + * Sabre Plugin to provide share-related properties + */ +class SharesPlugin extends \Sabre\DAV\ServerPlugin { + + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + const SHARETYPES_PROPERTYNAME = '{http://owncloud.org/ns}share-types'; + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @var \OCP\Share\IManager + */ + private $shareManager; + + /** + * @var \Sabre\DAV\Tree + */ + private $tree; + + /** + * @var string + */ + private $userId; + + /** + * @var \OCP\Files\Folder + */ + private $userFolder; + + /** + * @var IShare[] + */ + private $cachedShareTypes; + + /** + * @param \Sabre\DAV\Tree $tree tree + * @param IUserSession $userSession user session + * @param \OCP\Files\Folder $userFolder user home folder + * @param \OCP\Share\IManager $shareManager share manager + */ + public function __construct( + \Sabre\DAV\Tree $tree, + IUserSession $userSession, + \OCP\Files\Folder $userFolder, + \OCP\Share\IManager $shareManager + ) { + $this->tree = $tree; + $this->shareManager = $shareManager; + $this->userFolder = $userFolder; + $this->userId = $userSession->getUser()->getUID(); + $this->cachedShareTypes = []; + } + + /** + * 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 + */ + public function initialize(\Sabre\DAV\Server $server) { + $server->xml->namespacesMap[self::NS_OWNCLOUD] = 'oc'; + $server->xml->elementMap[self::SHARETYPES_PROPERTYNAME] = 'OCA\\DAV\\Connector\\Sabre\\ShareTypeList'; + $server->protectedProperties[] = self::SHARETYPES_PROPERTYNAME; + + $this->server = $server; + $this->server->on('propFind', array($this, 'handleGetProperties')); + } + + /** + * Return a list of share types for outgoing shares + * + * @param \OCP\Files\Node $node file node + * + * @return int[] array of share types + */ + private function getShareTypes(\OCP\Files\Node $node) { + $shareTypes = []; + $requestedShareTypes = [ + \OCP\Share::SHARE_TYPE_USER, + \OCP\Share::SHARE_TYPE_GROUP, + \OCP\Share::SHARE_TYPE_LINK, + \OCP\Share::SHARE_TYPE_REMOTE + ]; + foreach ($requestedShareTypes as $requestedShareType) { + // one of each type is enough to find out about the types + $shares = $this->shareManager->getSharesBy( + $this->userId, + $requestedShareType, + $node, + false, + 1 + ); + if (!empty($shares)) { + $shareTypes[] = $requestedShareType; + } + } + return $shareTypes; + } + + /** + * Adds shares to propfind response + * + * @param PropFind $propFind propfind object + * @param \Sabre\DAV\INode $sabreNode sabre node + */ + public function handleGetProperties( + PropFind $propFind, + \Sabre\DAV\INode $sabreNode + ) { + if (!($sabreNode instanceof \OCA\DAV\Connector\Sabre\Node)) { + return; + } + + // need prefetch ? + if ($sabreNode instanceof \OCA\DAV\Connector\Sabre\Directory + && $propFind->getDepth() !== 0 + && !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) + ) { + $folderNode = $this->userFolder->get($propFind->getPath()); + $children = $folderNode->getDirectoryListing(); + + $this->cachedShareTypes[$folderNode->getId()] = $this->getShareTypes($folderNode); + foreach ($children as $childNode) { + $this->cachedShareTypes[$childNode->getId()] = $this->getShareTypes($childNode); + } + } + + $propFind->handle(self::SHARETYPES_PROPERTYNAME, function() use ($sabreNode) { + if (isset($this->cachedShareTypes[$sabreNode->getId()])) { + $shareTypes = $this->cachedShareTypes[$sabreNode->getId()]; + } else { + $node = $this->userFolder->get($sabreNode->getPath()); + $shareTypes = $this->getShareTypes($node); + } + + return new ShareTypeList($shareTypes); + }); + } +} diff --git a/apps/dav/lib/Connector/Sabre/TagList.php b/apps/dav/lib/Connector/Sabre/TagList.php new file mode 100644 index 00000000000..5c1cd8b4f1d --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/TagList.php @@ -0,0 +1,120 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; + +/** + * TagList property + * + * This property contains multiple "tag" elements, each containing a tag name. + */ +class TagList implements Element { + 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; + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + $tags = []; + + foreach ($reader->parseInnerTree() as $elem) { + if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}tag') { + $tags[] = $elem['value']; + } + } + return new self($tags); + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->tags as $tag) { + $writer->writeElement('{' . self::NS_OWNCLOUD . '}tag', $tag); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php new file mode 100644 index 00000000000..dfc1a2dd95d --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php @@ -0,0 +1,293 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, 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->xml->namespacesMap[self::NS_OWNCLOUD] = 'oc'; + $server->xml->elementMap[self::TAGS_PROPERTYNAME] = 'OCA\\DAV\\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; + }); + } +} |