diff options
41 files changed, 2267 insertions, 68 deletions
diff --git a/.gitignore b/.gitignore index 2e42105ad83..d8c57c25180 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ !/apps/dav !/apps/files !/apps/federation +!/apps/federatedfilesharing !/apps/encryption !/apps/files_external !/apps/files_sharing diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index f035d19d862..4f3a93dbf8b 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -14,6 +14,12 @@ <files>appinfo/v1/webdav.php</files> <webdav>appinfo/v1/webdav.php</webdav> <dav>appinfo/v2/remote.php</dav> + <!-- carddav endpoints as used before ownCloud 9.0 --> + <contacts>appinfo/v1/carddav.php</contacts> + <carddav>appinfo/v1/carddav.php</carddav> + <!-- caldav endpoints as used before ownCloud 9.0 --> + <calendar>appinfo/v1/caldav.php</calendar> + <caldav>appinfo/v1/caldav.php</caldav> </remote> <public> <webdav>appinfo/v1/publicwebdav.php</webdav> diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php new file mode 100644 index 00000000000..f860ced3877 --- /dev/null +++ b/apps/dav/appinfo/v1/caldav.php @@ -0,0 +1,69 @@ +<?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/> + * + */ + +// Backends +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Connector\Sabre\Auth; +use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; +use OCA\DAV\Connector\Sabre\MaintenancePlugin; +use OCA\DAV\Connector\Sabre\Principal; +use Sabre\CalDAV\CalendarRoot; + +$authBackend = new Auth( + \OC::$server->getSession(), + \OC::$server->getUserSession(), + 'principals/' +); +$principalBackend = new Principal( + \OC::$server->getUserManager(), + \OC::$server->getGroupManager(), + 'principals/' +); +$db = \OC::$server->getDatabaseConnection(); +$calDavBackend = new CalDavBackend($db, $principalBackend); + +// Root nodes +$principalCollection = new \Sabre\CalDAV\Principal\Collection($principalBackend); +$principalCollection->disableListing = true; // Disable listing + +$addressBookRoot = new CalendarRoot($principalBackend, $calDavBackend); +$addressBookRoot->disableListing = true; // Disable listing + +$nodes = array( + $principalCollection, + $addressBookRoot, +); + +// Fire up server +$server = new \Sabre\DAV\Server($nodes); +$server->httpRequest->setUrl(\OC::$server->getRequest()->getRequestUri()); +$server->setBaseUri($baseuri); + +// Add plugins +$server->addPlugin(new MaintenancePlugin()); +$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, 'ownCloud')); +$server->addPlugin(new \Sabre\CalDAV\Plugin()); +$server->addPlugin(new \Sabre\DAVACL\Plugin()); +$server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); +$server->addPlugin(new ExceptionLoggerPlugin('caldav', \OC::$server->getLogger())); + +// And off we go! +$server->exec(); diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php new file mode 100644 index 00000000000..e0c79c75b72 --- /dev/null +++ b/apps/dav/appinfo/v1/carddav.php @@ -0,0 +1,70 @@ +<?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/> + * + */ + +// Backends +use OCA\DAV\CardDAV\AddressBookRoot; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Connector\Sabre\AppEnabledPlugin; +use OCA\DAV\Connector\Sabre\Auth; +use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; +use OCA\DAV\Connector\Sabre\MaintenancePlugin; +use OCA\DAV\Connector\Sabre\Principal; +use Sabre\CardDAV\Plugin; + +$authBackend = new Auth( + \OC::$server->getSession(), + \OC::$server->getUserSession(), + 'principals/' +); +$principalBackend = new Principal( + \OC::$server->getUserManager(), + \OC::$server->getGroupManager(), + 'principals/' +); +$db = \OC::$server->getDatabaseConnection(); +$cardDavBackend = new CardDavBackend($db, $principalBackend); + +// Root nodes +$principalCollection = new \Sabre\CalDAV\Principal\Collection($principalBackend); +$principalCollection->disableListing = true; // Disable listing + +$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend); +$addressBookRoot->disableListing = true; // Disable listing + +$nodes = array( + $principalCollection, + $addressBookRoot, +); + +// Fire up server +$server = new \Sabre\DAV\Server($nodes); +$server->httpRequest->setUrl(\OC::$server->getRequest()->getRequestUri()); +$server->setBaseUri($baseuri); +// Add plugins +$server->addPlugin(new MaintenancePlugin()); +$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, 'ownCloud')); +$server->addPlugin(new Plugin()); +$server->addPlugin(new \Sabre\DAVACL\Plugin()); +$server->addPlugin(new \Sabre\CardDAV\VCFExportPlugin()); +$server->addPlugin(new ExceptionLoggerPlugin('carddav', \OC::$server->getLogger())); + +// And off we go! +$server->exec(); diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php index d75c3526bdd..3d3e51e84bc 100644 --- a/apps/dav/appinfo/v1/webdav.php +++ b/apps/dav/appinfo/v1/webdav.php @@ -40,7 +40,8 @@ $serverFactory = new \OCA\DAV\Connector\Sabre\ServerFactory( // Backends $authBackend = new \OCA\DAV\Connector\Sabre\Auth( \OC::$server->getSession(), - \OC::$server->getUserSession() + \OC::$server->getUserSession(), + 'principals/' ); $requestUri = \OC::$server->getRequest()->getRequestUri(); diff --git a/apps/dav/lib/carddav/addressbookroot.php b/apps/dav/lib/carddav/addressbookroot.php index 2680135dec2..99c36c2e767 100644 --- a/apps/dav/lib/carddav/addressbookroot.php +++ b/apps/dav/lib/carddav/addressbookroot.php @@ -40,6 +40,9 @@ class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { function getName() { + if ($this->principalPrefix === 'principals') { + return parent::getName(); + } // Grabbing all the components of the principal path. $parts = explode('/', $this->principalPrefix); diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php index 2c1960fbb9d..c4f29b39d0d 100644 --- a/apps/dav/lib/carddav/carddavbackend.php +++ b/apps/dav/lib/carddav/carddavbackend.php @@ -93,11 +93,11 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return array */ function getAddressBooksForUser($principalUri) { + $principalUri = $this->convertPrincipal($principalUri, true); $query = $this->db->getQueryBuilder(); $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) ->from('addressbooks') - ->where($query->expr()->eq('principaluri', $query->createParameter('principaluri'))) - ->setParameter('principaluri', $principalUri); + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); $addressBooks = []; @@ -106,7 +106,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $addressBooks[$row['id']] = [ 'id' => $row['id'], 'uri' => $row['uri'], - 'principaluri' => $row['principaluri'], + 'principaluri' => $this->convertPrincipal($row['principaluri'], false), '{DAV:}displayname' => $row['displayname'], '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], @@ -921,4 +921,15 @@ class CardDavBackend implements BackendInterface, SyncSupport { public function applyShareAcl($addressBookId, $acl) { return $this->sharingBackend->applyShareAcl($addressBookId, $acl); } + + private function convertPrincipal($principalUri, $toV2) { + if ($this->principalBackend->getPrincipalPrefix() === 'principals') { + list(, $name) = URLUtil::splitPath($principalUri); + if ($toV2 === true) { + return "principals/users/$name"; + } + return "principals/$name"; + } + return $principalUri; + } } diff --git a/apps/dav/lib/connector/sabre/auth.php b/apps/dav/lib/connector/sabre/auth.php index cc679e44dbe..a046e078482 100644 --- a/apps/dav/lib/connector/sabre/auth.php +++ b/apps/dav/lib/connector/sabre/auth.php @@ -49,12 +49,14 @@ class Auth extends AbstractBasic { /** * @param ISession $session * @param IUserSession $userSession + * @param string $principalPrefix */ public function __construct(ISession $session, - IUserSession $userSession) { + IUserSession $userSession, + $principalPrefix = 'principals/users/') { $this->session = $session; $this->userSession = $userSession; - $this->principalPrefix = 'principals/users/'; + $this->principalPrefix = $principalPrefix; } /** diff --git a/apps/dav/lib/connector/sabre/principal.php b/apps/dav/lib/connector/sabre/principal.php index 5f02d1271df..4f26390e3cc 100644 --- a/apps/dav/lib/connector/sabre/principal.php +++ b/apps/dav/lib/connector/sabre/principal.php @@ -46,12 +46,20 @@ class Principal implements BackendInterface { /** @var IGroupManager */ private $groupManager; + /** @var string */ + private $principalPrefix; + /** * @param IUserManager $userManager + * @param IGroupManager $groupManager + * @param string $principalPrefix */ - public function __construct(IUserManager $userManager, IGroupManager $groupManager) { + public function __construct(IUserManager $userManager, + IGroupManager $groupManager, + $principalPrefix = 'principals/users/') { $this->userManager = $userManager; $this->groupManager = $groupManager; + $this->principalPrefix = trim($principalPrefix, '/'); } /** @@ -70,7 +78,7 @@ class Principal implements BackendInterface { public function getPrincipalsByPrefix($prefixPath) { $principals = []; - if ($prefixPath === 'principals/users') { + if ($prefixPath === $this->principalPrefix) { foreach($this->userManager->search('') as $user) { $principals[] = $this->userToPrincipal($user); } @@ -88,20 +96,15 @@ class Principal implements BackendInterface { * @return array */ public function getPrincipalByPath($path) { - $elements = explode('/', $path); - if ($elements[0] !== 'principals') { - return null; - } - if ($elements[1] !== 'users') { - return null; - } - $name = $elements[2]; - $user = $this->userManager->get($name); + list($prefix, $name) = URLUtil::splitPath($path); - if (!is_null($user)) { - return $this->userToPrincipal($user); - } + if ($prefix === $this->principalPrefix) { + $user = $this->userManager->get($name); + if (!is_null($user)) { + return $this->userToPrincipal($user); + } + } return null; } @@ -132,7 +135,7 @@ class Principal implements BackendInterface { public function getGroupMembership($principal) { list($prefix, $name) = URLUtil::splitPath($principal); - if ($prefix === 'principals/users') { + if ($prefix === $this->principalPrefix) { $user = $this->userManager->get($name); if (!$user) { throw new Exception('Principal not found'); @@ -141,11 +144,9 @@ class Principal implements BackendInterface { $groups = $this->groupManager->getUserGroups($user); $groups = array_map(function($group) { /** @var IGroup $group */ - return 'principals/groups/' . $group->getGID(); + return $this->principalPrefix . '/' . $group->getGID(); }, $groups); - $groups[]= 'principals/users/'.$name.'/calendar-proxy-read'; - $groups[]= 'principals/users/'.$name.'/calendar-proxy-write'; return $groups; } return []; @@ -200,7 +201,7 @@ class Principal implements BackendInterface { $userId = $user->getUID(); $displayName = $user->getDisplayName(); $principal = [ - 'uri' => "principals/users/$userId", + 'uri' => $this->principalPrefix . '/' . $userId, '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName, ]; @@ -212,4 +213,8 @@ class Principal implements BackendInterface { return $principal; } + public function getPrincipalPrefix() { + return $this->principalPrefix; + } + } diff --git a/apps/dav/tests/unit/connector/sabre/principal.php b/apps/dav/tests/unit/connector/sabre/principal.php index d6bc7cd405f..07bfd5d263b 100644 --- a/apps/dav/tests/unit/connector/sabre/principal.php +++ b/apps/dav/tests/unit/connector/sabre/principal.php @@ -211,10 +211,7 @@ class Principal extends TestCase { ->method('getUserGroups') ->willReturn([]); - $expectedResponse = [ - 'principals/users/foo/calendar-proxy-read', - 'principals/users/foo/calendar-proxy-write' - ]; + $expectedResponse = []; $response = $this->connector->getGroupMembership('principals/users/foo'); $this->assertSame($expectedResponse, $response); } diff --git a/apps/federatedfilesharing/appinfo/app.php b/apps/federatedfilesharing/appinfo/app.php new file mode 100644 index 00000000000..804ab69759c --- /dev/null +++ b/apps/federatedfilesharing/appinfo/app.php @@ -0,0 +1,27 @@ +<?php +/** + * @author Björn Schießle <schiessle@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\FederatedFileSharing\AppInfo; + +use OCP\AppFramework\App; + +new App('federatedfilesharing'); + diff --git a/apps/federatedfilesharing/appinfo/info.xml b/apps/federatedfilesharing/appinfo/info.xml new file mode 100644 index 00000000000..d88ea2640e1 --- /dev/null +++ b/apps/federatedfilesharing/appinfo/info.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<info> + <id>federatedfilesharing</id> + <name>Federated File Sharing</name> + <description>Provide federated file sharing across ownCloud servers</description> + <licence>AGPL</licence> + <author>Bjoern Schiessle, Roeland Jago Douma</author> + <version>0.1.0</version> + <namespace>FederatedFileSharing</namespace> + <category>other</category> + <dependencies> + <owncloud min-version="9.0" max-version="9.0" /> + </dependencies> +</info> diff --git a/apps/federatedfilesharing/lib/addresshandler.php b/apps/federatedfilesharing/lib/addresshandler.php new file mode 100644 index 00000000000..92768f11b95 --- /dev/null +++ b/apps/federatedfilesharing/lib/addresshandler.php @@ -0,0 +1,184 @@ +<?php +/** + * @author Björn Schießle <schiessle@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\FederatedFileSharing; +use OC\HintException; +use OCP\IL10N; +use OCP\IURLGenerator; + +/** + * Class AddressHandler - parse, modify and construct federated sharing addresses + * + * @package OCA\FederatedFileSharing + */ +class AddressHandler { + + /** @var IL10N */ + private $l; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** + * AddressHandler constructor. + * + * @param IURLGenerator $urlGenerator + * @param IL10N $il10n + */ + public function __construct( + IURLGenerator $urlGenerator, + IL10N $il10n + ) { + $this->l = $il10n; + $this->urlGenerator = $urlGenerator; + } + + /** + * split user and remote from federated cloud id + * + * @param string $address federated share address + * @return array [user, remoteURL] + * @throws HintException + */ + public function splitUserRemote($address) { + if (strpos($address, '@') === false) { + $hint = $this->l->t('Invalid Federated Cloud ID'); + throw new HintException('Invalid Federated Cloud ID', $hint); + } + + // Find the first character that is not allowed in user names + $id = str_replace('\\', '/', $address); + $posSlash = strpos($id, '/'); + $posColon = strpos($id, ':'); + + if ($posSlash === false && $posColon === false) { + $invalidPos = strlen($id); + } else if ($posSlash === false) { + $invalidPos = $posColon; + } else if ($posColon === false) { + $invalidPos = $posSlash; + } else { + $invalidPos = min($posSlash, $posColon); + } + + // Find the last @ before $invalidPos + $pos = $lastAtPos = 0; + while ($lastAtPos !== false && $lastAtPos <= $invalidPos) { + $pos = $lastAtPos; + $lastAtPos = strpos($id, '@', $pos + 1); + } + + if ($pos !== false) { + $user = substr($id, 0, $pos); + $remote = substr($id, $pos + 1); + $remote = $this->fixRemoteURL($remote); + if (!empty($user) && !empty($remote)) { + return array($user, $remote); + } + } + + $hint = $this->l->t('Invalid Federated Cloud ID'); + throw new HintException('Invalid Federated Cloud ID', $hint); + } + + /** + * generate remote URL part of federated ID + * + * @return string url of the current server + */ + public function generateRemoteURL() { + $url = $this->urlGenerator->getAbsoluteURL('/'); + return $url; + } + + /** + * check if two federated cloud IDs refer to the same user + * + * @param string $user1 + * @param string $server1 + * @param string $user2 + * @param string $server2 + * @return bool true if both users and servers are the same + */ + public function compareAddresses($user1, $server1, $user2, $server2) { + $normalizedServer1 = strtolower($this->removeProtocolFromUrl($server1)); + $normalizedServer2 = strtolower($this->removeProtocolFromUrl($server2)); + + if (rtrim($normalizedServer1, '/') === rtrim($normalizedServer2, '/')) { + // FIXME this should be a method in the user management instead + \OCP\Util::emitHook( + '\OCA\Files_Sharing\API\Server2Server', + 'preLoginNameUsedAsUserName', + array('uid' => &$user1) + ); + \OCP\Util::emitHook( + '\OCA\Files_Sharing\API\Server2Server', + 'preLoginNameUsedAsUserName', + array('uid' => &$user2) + ); + + if ($user1 === $user2) { + return true; + } + } + + return false; + } + + /** + * remove protocol from URL + * + * @param string $url + * @return string + */ + public function removeProtocolFromUrl($url) { + if (strpos($url, 'https://') === 0) { + return substr($url, strlen('https://')); + } else if (strpos($url, 'http://') === 0) { + return substr($url, strlen('http://')); + } + + return $url; + } + + /** + * Strips away a potential file names and trailing slashes: + * - http://localhost + * - http://localhost/ + * - http://localhost/index.php + * - http://localhost/index.php/s/{shareToken} + * + * all return: http://localhost + * + * @param string $remote + * @return string + */ + protected function fixRemoteURL($remote) { + $remote = str_replace('\\', '/', $remote); + if ($fileNamePosition = strpos($remote, '/index.php')) { + $remote = substr($remote, 0, $fileNamePosition); + } + $remote = rtrim($remote, '/'); + + return $remote; + } + +} diff --git a/apps/federatedfilesharing/lib/federatedshareprovider.php b/apps/federatedfilesharing/lib/federatedshareprovider.php new file mode 100644 index 00000000000..05a9432a32f --- /dev/null +++ b/apps/federatedfilesharing/lib/federatedshareprovider.php @@ -0,0 +1,556 @@ +<?php +/** + * @author Björn Schießle <schiessle@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\FederatedFileSharing; + +use OC\Share20\Share; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\ILogger; +use OCP\Share\IShare; +use OCP\Share\IShareProvider; +use OC\Share20\Exception\InvalidShare; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Files\NotFoundException; +use OCP\IDBConnection; +use OCP\Files\Node; + +/** + * Class FederatedShareProvider + * + * @package OCA\FederatedFileSharing + */ +class FederatedShareProvider implements IShareProvider { + + const SHARE_TYPE_REMOTE = 6; + + /** @var IDBConnection */ + private $dbConnection; + + /** @var AddressHandler */ + private $addressHandler; + + /** @var Notifications */ + private $notifications; + + /** @var TokenHandler */ + private $tokenHandler; + + /** @var IL10N */ + private $l; + + /** @var ILogger */ + private $logger; + + /** @var IRootFolder */ + private $rootFolder; + + /** + * DefaultShareProvider constructor. + * + * @param IDBConnection $connection + * @param AddressHandler $addressHandler + * @param Notifications $notifications + * @param TokenHandler $tokenHandler + * @param IL10N $l10n + * @param ILogger $logger + * @param IRootFolder $rootFolder + */ + public function __construct( + IDBConnection $connection, + AddressHandler $addressHandler, + Notifications $notifications, + TokenHandler $tokenHandler, + IL10N $l10n, + ILogger $logger, + IRootFolder $rootFolder + ) { + $this->dbConnection = $connection; + $this->addressHandler = $addressHandler; + $this->notifications = $notifications; + $this->tokenHandler = $tokenHandler; + $this->l = $l10n; + $this->logger = $logger; + $this->rootFolder = $rootFolder; + } + + /** + * Return the identifier of this provider. + * + * @return string Containing only [a-zA-Z0-9] + */ + public function identifier() { + return 'ocFederatedSharing'; + } + + /** + * Share a path + * + * @param IShare $share + * @return IShare The share object + * @throws ShareNotFound + * @throws \Exception + */ + public function create(IShare $share) { + + $shareWith = $share->getSharedWith(); + $itemSource = $share->getNodeId(); + $itemType = $share->getNodeType(); + $uidOwner = $share->getShareOwner(); + $permissions = $share->getPermissions(); + $sharedBy = $share->getSharedBy(); + + /* + * Check if file is not already shared with the remote user + */ + $alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0); + if (!empty($alreadyShared)) { + $message = 'Sharing %s failed, because this item is already shared with %s'; + $message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); + $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + + // don't allow federated shares if source and target server are the same + list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); + $currentServer = $this->addressHandler->generateRemoteURL(); + $currentUser = $sharedBy; + if ($this->addressHandler->compareAddresses($user, $remote, $currentUser, $currentServer)) { + $message = 'Not allowed to create a federated share with the same user.'; + $message_t = $this->l->t('Not allowed to create a federated share with the same user'); + $this->logger->debug($message, ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + $token = $this->tokenHandler->generateToken(); + + $shareWith = $user . '@' . $remote; + + $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $send = $this->notifications->sendRemoteShare( + $token, + $shareWith, + $share->getNode()->getName(), + $shareId, + $share->getSharedBy() + ); + + $data = $this->getRawShare($shareId); + $share = $this->createShare($data); + + if ($send === false) { + $this->delete($share); + $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable.', + [$share->getNode()->getName(), $shareWith]); + throw new \Exception($message_t); + } + + return $share; + } + + /** + * add share to the database and return the ID + * + * @param int $itemSource + * @param string $itemType + * @param string $shareWith + * @param string $sharedBy + * @param string $uidOwner + * @param int $permissions + * @param string $token + * @return int + */ + private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert('share') + ->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)) + ->setValue('item_type', $qb->createNamedParameter($itemType)) + ->setValue('item_source', $qb->createNamedParameter($itemSource)) + ->setValue('file_source', $qb->createNamedParameter($itemSource)) + ->setValue('share_with', $qb->createNamedParameter($shareWith)) + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) + ->setValue('permissions', $qb->createNamedParameter($permissions)) + ->setValue('token', $qb->createNamedParameter($token)) + ->setValue('stime', $qb->createNamedParameter(time())); + + $qb->execute(); + $id = $qb->getLastInsertId(); + + return (int)$id; + } + + /** + * Update a share + * + * @param IShare $share + * @return IShare The share object + */ + public function update(IShare $share) { + /* + * We allow updating the permissions of federated shares + */ + $qb = $this->dbConnection->getQueryBuilder(); + $qb->update('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) + ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->execute(); + + return $share; + } + + /** + * @inheritdoc + */ + public function move(IShare $share, $recipient) { + /* + * This function does nothing yet as it is just for outgoing + * federated shares. + */ + return $share; + } + + /** + * Get all children of this share + * + * @param IShare $parent + * @return IShare[] + */ + public function getChildren(IShare $parent) { + $children = []; + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) + ->orderBy('id'); + + $cursor = $qb->execute(); + while($data = $cursor->fetch()) { + $children[] = $this->createShare($data); + } + $cursor->closeCursor(); + + return $children; + } + + /** + * Delete a share + * + * @param IShare $share + */ + public function delete(IShare $share) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))); + $qb->execute(); + + list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith()); + $this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken()); + } + + /** + * @inheritdoc + */ + public function deleteFromSelf(IShare $share, $recipient) { + // nothing to do here. Technically deleteFromSelf in the context of federated + // shares is a umount of a external storage. This is handled here + // apps/files_sharing/lib/external/manager.php + // TODO move this code over to this app + return; + } + + /** + * @inheritdoc + */ + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); + + /** + * Reshares for this user are shares where they are the owner. + */ + if ($reshares === false) { + //Special case for old shares created via the web UI + $or1 = $qb->expr()->andX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->isNull('uid_initiator') + ); + + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), + $or1 + ) + ); + } else { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } + + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + + $qb->setFirstResult($offset); + $qb->orderBy('id'); + + $cursor = $qb->execute(); + $shares = []; + while($data = $cursor->fetch()) { + $shares[] = $this->createShare($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getShareById($id, $recipientId = null) { + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound(); + } + + try { + $share = $this->createShare($data); + } catch (InvalidShare $e) { + throw new ShareNotFound(); + } + + return $share; + } + + /** + * Get shares for a given path + * + * @param \OCP\Files\Node $path + * @return IShare[] + */ + public function getSharesByPath(Node $path) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) + ->execute(); + + $shares = []; + while($data = $cursor->fetch()) { + $shares[] = $this->createShare($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getSharedWith($userId, $shareType, $node, $limit, $offset) { + /** @var IShare[] $shares */ + $shares = []; + + //Get shares directly with this user + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + // Order by id + $qb->orderBy('id'); + + // Set limit and offset + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + $qb->setFirstResult($offset); + + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); + + // Filter by node if provided + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + $cursor = $qb->execute(); + + while($data = $cursor->fetch()) { + $shares[] = $this->createShare($data); + } + $cursor->closeCursor(); + + + return $shares; + } + + /** + * Get a share by token + * + * @param string $token + * @return IShare + * @throws ShareNotFound + */ + public function getShareByToken($token) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->execute(); + + $data = $cursor->fetch(); + + if ($data === false) { + throw new ShareNotFound(); + } + + try { + $share = $this->createShare($data); + } catch (InvalidShare $e) { + throw new ShareNotFound(); + } + + return $share; + } + + /** + * get database row of a give share + * + * @param $id + * @return array + * @throws ShareNotFound + */ + private function getRawShare($id) { + + // Now fetch the inserted share and create a complete share object + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound; + } + + return $data; + } + + /** + * Create a share object from an database row + * + * @param array $data + * @return IShare + * @throws InvalidShare + * @throws ShareNotFound + */ + private function createShare($data) { + + $share = new Share($this->rootFolder); + $share->setId((int)$data['id']) + ->setShareType((int)$data['share_type']) + ->setPermissions((int)$data['permissions']) + ->setTarget($data['file_target']) + ->setMailSend((bool)$data['mail_send']) + ->setToken($data['token']); + + $shareTime = new \DateTime(); + $shareTime->setTimestamp((int)$data['stime']); + $share->setShareTime($shareTime); + $share->setSharedWith($data['share_with']); + + if ($data['uid_initiator'] !== null) { + $share->setShareOwner($data['uid_owner']); + $share->setSharedBy($data['uid_initiator']); + } else { + //OLD SHARE + $share->setSharedBy($data['uid_owner']); + $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); + + $owner = $path->getOwner(); + $share->setShareOwner($owner->getUID()); + } + + $share->setNodeId((int)$data['file_source']); + $share->setNodeType($data['item_type']); + + $share->setProviderId($this->identifier()); + + return $share; + } + + /** + * Get the node with file $id for $user + * + * @param string $userId + * @param int $id + * @return \OCP\Files\File|\OCP\Files\Folder + * @throws InvalidShare + */ + private function getNode($userId, $id) { + try { + $userFolder = $this->rootFolder->getUserFolder($userId); + } catch (NotFoundException $e) { + throw new InvalidShare(); + } + + $nodes = $userFolder->getById($id); + + if (empty($nodes)) { + throw new InvalidShare(); + } + + return $nodes[0]; + } + +} diff --git a/apps/federatedfilesharing/lib/notifications.php b/apps/federatedfilesharing/lib/notifications.php new file mode 100644 index 00000000000..d778ac87828 --- /dev/null +++ b/apps/federatedfilesharing/lib/notifications.php @@ -0,0 +1,144 @@ +<?php +/** + * @author Björn Schießle <schiessle@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\FederatedFileSharing; + + +use OCP\Http\Client\IClientService; + +class Notifications { + + const BASE_PATH_TO_SHARE_API = '/ocs/v1.php/cloud/shares'; + const RESPONSE_FORMAT = 'json'; // default response format for ocs calls + + /** @var AddressHandler */ + private $addressHandler; + + /** @var IClientService */ + private $httpClientService; + + /** + * Notifications constructor. + * + * @param AddressHandler $addressHandler + * @param IClientService $httpClientService + */ + public function __construct( + AddressHandler $addressHandler, + IClientService $httpClientService + ) { + $this->addressHandler = $addressHandler; + $this->httpClientService = $httpClientService; + } + + /** + * send server-to-server share to remote server + * + * @param string $token + * @param string $shareWith + * @param string $name + * @param int $remote_id + * @param string $owner + * @return bool + */ + public function sendRemoteShare($token, $shareWith, $name, $remote_id, $owner) { + + list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); + + if ($user && $remote) { + $url = $remote . self::BASE_PATH_TO_SHARE_API . '?format=' . self::RESPONSE_FORMAT; + $local = $this->addressHandler->generateRemoteURL(); + + $fields = array( + 'shareWith' => $user, + 'token' => $token, + 'name' => $name, + 'remoteId' => $remote_id, + 'owner' => $owner, + 'remote' => $local, + ); + + $url = $this->addressHandler->removeProtocolFromUrl($url); + $result = $this->tryHttpPost($url, $fields); + $status = json_decode($result['result'], true); + + if ($result['success'] && $status['ocs']['meta']['statuscode'] === 100) { + \OC_Hook::emit('OCP\Share', 'federated_share_added', ['server' => $remote]); + return true; + } + + } + + return false; + } + + /** + * send server-to-server unshare to remote server + * + * @param string $remote url + * @param int $id share id + * @param string $token + * @return bool + */ + public function sendRemoteUnShare($remote, $id, $token) { + $url = rtrim($remote, '/') . self::BASE_PATH_TO_SHARE_API . '/' . $id . '/unshare?format=' . self::RESPONSE_FORMAT; + $fields = array('token' => $token, 'format' => 'json'); + $url = $this->addressHandler->removeProtocolFromUrl($url); + $result = $this->tryHttpPost($url, $fields); + $status = json_decode($result['result'], true); + + return ($result['success'] && $status['ocs']['meta']['statuscode'] === 100); + } + + /** + * try http post first with https and then with http as a fallback + * + * @param string $url + * @param array $fields post parameters + * @return array + */ + private function tryHttpPost($url, array $fields) { + $client = $this->httpClientService->newClient(); + $protocol = 'https://'; + $result = [ + 'success' => false, + 'result' => '', + ]; + $try = 0; + while ($result['success'] === false && $try < 2) { + try { + $response = $client->post($protocol . $url, [ + 'body' => $fields + ]); + $result['result'] = $response->getBody(); + $result['success'] = true; + break; + } catch (\Exception $e) { + $try++; + $protocol = 'http://'; + } + } + + return $result; + } + +} diff --git a/apps/federatedfilesharing/lib/tokenhandler.php b/apps/federatedfilesharing/lib/tokenhandler.php new file mode 100644 index 00000000000..ec5f73127d6 --- /dev/null +++ b/apps/federatedfilesharing/lib/tokenhandler.php @@ -0,0 +1,61 @@ +<?php +/** + * @author Björn Schießle <schiessle@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\FederatedFileSharing; + + +use OCP\Security\ISecureRandom; + +/** + * Class TokenHandler + * + * @package OCA\FederatedFileSharing + */ +class TokenHandler { + + const TOKEN_LENGTH = 15; + + /** @var ISecureRandom */ + private $secureRandom; + + /** + * TokenHandler constructor. + * + * @param ISecureRandom $secureRandom + */ + public function __construct(ISecureRandom $secureRandom) { + $this->secureRandom = $secureRandom; + } + + /** + * generate to token used to authenticate federated shares + * + * @return string + */ + public function generateToken() { + $token = $this->secureRandom->generate( + self::TOKEN_LENGTH, + ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); + return $token; + } + +} diff --git a/apps/federatedfilesharing/tests/addresshandlertest.php b/apps/federatedfilesharing/tests/addresshandlertest.php new file mode 100644 index 00000000000..b1c23dc75bf --- /dev/null +++ b/apps/federatedfilesharing/tests/addresshandlertest.php @@ -0,0 +1,198 @@ +<?php +/** + * @author Björn Schießle <schiessle@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\FederatedFileSharing\Tests; + + +use OCA\FederatedFileSharing\AddressHandler; +use OCP\IL10N; +use OCP\IURLGenerator; +use Test\TestCase; + +class AddressHandlerTest extends TestCase { + + /** @var AddressHandler */ + private $addressHandler; + + /** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject */ + private $urlGenerator; + + /** @var IL10N | \PHPUnit_Framework_MockObject_MockObject */ + private $il10n; + + public function setUp() { + parent::setUp(); + + $this->urlGenerator = $this->getMock('OCP\IURLGenerator'); + $this->il10n = $this->getMock('OCP\IL10N'); + + $this->addressHandler = new AddressHandler($this->urlGenerator, $this->il10n); + } + + public function dataTestSplitUserRemote() { + $userPrefix = ['user@name', 'username']; + $protocols = ['', 'http://', 'https://']; + $remotes = [ + 'localhost', + 'local.host', + 'dev.local.host', + 'dev.local.host/path', + 'dev.local.host/at@inpath', + '127.0.0.1', + '::1', + '::192.0.2.128', + '::192.0.2.128/at@inpath', + ]; + + $testCases = []; + foreach ($userPrefix as $user) { + foreach ($remotes as $remote) { + foreach ($protocols as $protocol) { + $baseUrl = $user . '@' . $protocol . $remote; + + $testCases[] = [$baseUrl, $user, $protocol . $remote]; + $testCases[] = [$baseUrl . '/', $user, $protocol . $remote]; + $testCases[] = [$baseUrl . '/index.php', $user, $protocol . $remote]; + $testCases[] = [$baseUrl . '/index.php/s/token', $user, $protocol . $remote]; + } + } + } + return $testCases; + } + + /** + * @dataProvider dataTestSplitUserRemote + * + * @param string $remote + * @param string $expectedUser + * @param string $expectedUrl + */ + public function testSplitUserRemote($remote, $expectedUser, $expectedUrl) { + list($remoteUser, $remoteUrl) = $this->addressHandler->splitUserRemote($remote); + $this->assertSame($expectedUser, $remoteUser); + $this->assertSame($expectedUrl, $remoteUrl); + } + + public function dataTestSplitUserRemoteError() { + return array( + // Invalid path + array('user@'), + + // Invalid user + array('@server'), + array('us/er@server'), + array('us:er@server'), + + // Invalid splitting + array('user'), + array(''), + array('us/erserver'), + array('us:erserver'), + ); + } + + /** + * @dataProvider dataTestSplitUserRemoteError + * + * @param string $id + * @expectedException \OC\HintException + */ + public function testSplitUserRemoteError($id) { + $this->addressHandler->splitUserRemote($id); + } + + /** + * @dataProvider dataTestCompareAddresses + * + * @param string $user1 + * @param string $server1 + * @param string $user2 + * @param string $server2 + * @param bool $expected + */ + public function testCompareAddresses($user1, $server1, $user2, $server2, $expected) { + $this->assertSame($expected, + $this->addressHandler->compareAddresses($user1, $server1, $user2, $server2) + ); + } + + public function dataTestCompareAddresses() { + return [ + ['user1', 'http://server1', 'user1', 'http://server1', true], + ['user1', 'https://server1', 'user1', 'http://server1', true], + ['user1', 'http://serVer1', 'user1', 'http://server1', true], + ['user1', 'http://server1/', 'user1', 'http://server1', true], + ['user1', 'server1', 'user1', 'http://server1', true], + ['user1', 'http://server1', 'user1', 'http://server2', false], + ['user1', 'https://server1', 'user1', 'http://server2', false], + ['user1', 'http://serVer1', 'user1', 'http://serer2', false], + ['user1', 'http://server1/', 'user1', 'http://server2', false], + ['user1', 'server1', 'user1', 'http://server2', false], + ['user1', 'http://server1', 'user2', 'http://server1', false], + ['user1', 'https://server1', 'user2', 'http://server1', false], + ['user1', 'http://serVer1', 'user2', 'http://server1', false], + ['user1', 'http://server1/', 'user2', 'http://server1', false], + ['user1', 'server1', 'user2', 'http://server1', false], + ]; + } + + /** + * @dataProvider dataTestRemoveProtocolFromUrl + * + * @param string $url + * @param string $expectedResult + */ + public function testRemoveProtocolFromUrl($url, $expectedResult) { + $result = $this->addressHandler->removeProtocolFromUrl($url); + $this->assertSame($expectedResult, $result); + } + + public function dataTestRemoveProtocolFromUrl() { + return [ + ['http://owncloud.org', 'owncloud.org'], + ['https://owncloud.org', 'owncloud.org'], + ['owncloud.org', 'owncloud.org'], + ]; + } + + /** + * @dataProvider dataTestFixRemoteUrl + * + * @param string $url + * @param string $expected + */ + public function testFixRemoteUrl($url, $expected) { + $this->assertSame($expected, + $this->invokePrivate($this->addressHandler, 'fixRemoteURL', [$url]) + ); + } + + public function dataTestFixRemoteUrl() { + return [ + ['http://localhost', 'http://localhost'], + ['http://localhost/', 'http://localhost'], + ['http://localhost/index.php', 'http://localhost'], + ['http://localhost/index.php/s/AShareToken', 'http://localhost'], + ]; + } + +} diff --git a/apps/federatedfilesharing/tests/federatedshareprovidertest.php b/apps/federatedfilesharing/tests/federatedshareprovidertest.php new file mode 100644 index 00000000000..a7ff02e7697 --- /dev/null +++ b/apps/federatedfilesharing/tests/federatedshareprovidertest.php @@ -0,0 +1,447 @@ +<?php + +namespace OCA\FederatedFileSharing\Tests; + + +use OCA\FederatedFileSharing\AddressHandler; +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\FederatedFileSharing\Notifications; +use OCA\FederatedFileSharing\TokenHandler; +use OCP\Files\IRootFolder; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\ILogger; +use OCP\Share\IManager; +use Test\TestCase; + +/** + * Class FederatedShareProviderTest + * + * @package OCA\FederatedFileSharing\Tests + * @group DB + */ +class FederatedShareProviderTest extends TestCase { + + /** @var IDBConnection */ + protected $connection; + /** @var AddressHandler | \PHPUnit_Framework_MockObject_MockObject */ + protected $addressHandler; + /** @var Notifications | \PHPUnit_Framework_MockObject_MockObject */ + protected $notifications; + /** @var TokenHandler */ + protected $tokenHandler; + /** @var IL10N */ + protected $l; + /** @var ILogger */ + protected $logger; + /** @var IRootFolder | \PHPUnit_Framework_MockObject_MockObject */ + protected $rootFolder; + + /** @var IManager */ + protected $shareManager; + /** @var FederatedShareProvider */ + protected $provider; + + + public function setUp() { + parent::setUp(); + + $this->connection = \OC::$server->getDatabaseConnection(); + $this->notifications = $this->getMockBuilder('OCA\FederatedFileSharing\Notifications') + ->disableOriginalConstructor() + ->getMock(); + $this->tokenHandler = $this->getMockBuilder('OCA\FederatedFileSharing\TokenHandler') + ->disableOriginalConstructor() + ->getMock(); + $this->l = $this->getMock('OCP\IL10N'); + $this->l->method('t') + ->will($this->returnCallback(function($text, $parameters = []) { + return vsprintf($text, $parameters); + })); + $this->logger = $this->getMock('OCP\ILogger'); + $this->rootFolder = $this->getMock('OCP\Files\IRootFolder'); + $this->addressHandler = new AddressHandler(\OC::$server->getURLGenerator(), $this->l); + + $this->provider = new FederatedShareProvider( + $this->connection, + $this->addressHandler, + $this->notifications, + $this->tokenHandler, + $this->l, + $this->logger, + $this->rootFolder + ); + + $this->shareManager = \OC::$server->getShareManager(); + } + + public function tearDown() { + $this->connection->getQueryBuilder()->delete('share')->execute(); + + return parent::tearDown(); + } + + public function testCreate() { + $share = $this->shareManager->newShare(); + + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $share->setSharedWith('user@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + + $this->tokenHandler->method('generateToken')->willReturn('token'); + + $this->notifications->expects($this->once()) + ->method('sendRemoteShare') + ->with( + $this->equalTo('token'), + $this->equalTo('user@server.com'), + $this->equalTo('myFile'), + $this->anything(), + 'sharedBy' + )->willReturn(true); + + $this->rootFolder->expects($this->never())->method($this->anything()); + + $share = $this->provider->create($share); + + $qb = $this->connection->getQueryBuilder(); + $stmt = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) + ->execute(); + + $data = $stmt->fetch(); + $stmt->closeCursor(); + + $expected = [ + 'share_type' => \OCP\Share::SHARE_TYPE_REMOTE, + 'share_with' => 'user@server.com', + 'uid_owner' => 'shareOwner', + 'uid_initiator' => 'sharedBy', + 'item_type' => 'file', + 'item_source' => 42, + 'file_source' => 42, + 'permissions' => 19, + 'accepted' => 0, + 'token' => 'token', + ]; + $this->assertArraySubset($expected, $data); + + $this->assertEquals($data['id'], $share->getId()); + $this->assertEquals(\OCP\Share::SHARE_TYPE_REMOTE, $share->getShareType()); + $this->assertEquals('user@server.com', $share->getSharedWith()); + $this->assertEquals('sharedBy', $share->getSharedBy()); + $this->assertEquals('shareOwner', $share->getShareOwner()); + $this->assertEquals('file', $share->getNodeType()); + $this->assertEquals(42, $share->getNodeId()); + $this->assertEquals(19, $share->getPermissions()); + $this->assertEquals('token', $share->getToken()); + } + + public function testCreateCouldNotFindServer() { + $share = $this->shareManager->newShare(); + + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $share->setSharedWith('user@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + + $this->tokenHandler->method('generateToken')->willReturn('token'); + + $this->notifications->expects($this->once()) + ->method('sendRemoteShare') + ->with( + $this->equalTo('token'), + $this->equalTo('user@server.com'), + $this->equalTo('myFile'), + $this->anything(), + 'sharedBy' + )->willReturn(false); + + $this->rootFolder->expects($this->once()) + ->method('getUserFolder') + ->with('shareOwner') + ->will($this->returnSelf()); + $this->rootFolder->method('getById') + ->with('42') + ->willReturn([$node]); + + try { + $share = $this->provider->create($share); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals('Sharing myFile failed, could not find user@server.com, maybe the server is currently unreachable.', $e->getMessage()); + } + + $qb = $this->connection->getQueryBuilder(); + $stmt = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) + ->execute(); + + $data = $stmt->fetch(); + $stmt->closeCursor(); + + $this->assertFalse($data); + } + + public function testCreateShareWithSelf() { + $share = $this->shareManager->newShare(); + + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $shareWith = 'sharedBy@' . $this->addressHandler->generateRemoteURL(); + + $share->setSharedWith($shareWith) + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + + $this->rootFolder->expects($this->never())->method($this->anything()); + + try { + $share = $this->provider->create($share); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals('Not allowed to create a federated share with the same user', $e->getMessage()); + } + + $qb = $this->connection->getQueryBuilder(); + $stmt = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) + ->execute(); + + $data = $stmt->fetch(); + $stmt->closeCursor(); + + $this->assertFalse($data); + } + + public function testCreateAlreadyShared() { + $share = $this->shareManager->newShare(); + + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $share->setSharedWith('user@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + + $this->tokenHandler->method('generateToken')->willReturn('token'); + + $this->notifications->expects($this->once()) + ->method('sendRemoteShare') + ->with( + $this->equalTo('token'), + $this->equalTo('user@server.com'), + $this->equalTo('myFile'), + $this->anything(), + 'sharedBy' + )->willReturn(true); + + $this->rootFolder->expects($this->never())->method($this->anything()); + + $this->provider->create($share); + + try { + $this->provider->create($share); + } catch (\Exception $e) { + $this->assertEquals('Sharing myFile failed, because this item is already shared with user@server.com', $e->getMessage()); + } + } + + public function testUpdate() { + $share = $this->shareManager->newShare(); + + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $share->setSharedWith('user@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + + $this->tokenHandler->method('generateToken')->willReturn('token'); + + $this->notifications->expects($this->once()) + ->method('sendRemoteShare') + ->with( + $this->equalTo('token'), + $this->equalTo('user@server.com'), + $this->equalTo('myFile'), + $this->anything(), + 'sharedBy' + )->willReturn(true); + + $this->rootFolder->expects($this->never())->method($this->anything()); + + $share = $this->provider->create($share); + + $share->setPermissions(1); + $this->provider->update($share); + + $share = $this->provider->getShareById($share->getId()); + + $this->assertEquals(1, $share->getPermissions()); + } + + public function testGetSharedBy() { + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $this->tokenHandler->method('generateToken')->willReturn('token'); + $this->notifications + ->method('sendRemoteShare') + ->willReturn(true); + + $this->rootFolder->expects($this->never())->method($this->anything()); + + $share = $this->shareManager->newShare(); + $share->setSharedWith('user@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + $this->provider->create($share); + + $share2 = $this->shareManager->newShare(); + $share2->setSharedWith('user2@server.com') + ->setSharedBy('sharedBy2') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + $this->provider->create($share2); + + $shares = $this->provider->getSharesBy('sharedBy', \OCP\Share::SHARE_TYPE_REMOTE, null, false, -1, 0); + + $this->assertCount(1, $shares); + $this->assertEquals('user@server.com', $shares[0]->getSharedWith()); + $this->assertEquals('sharedBy', $shares[0]->getSharedBy()); + } + + public function testGetSharedByWithNode() { + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $this->tokenHandler->method('generateToken')->willReturn('token'); + $this->notifications + ->method('sendRemoteShare') + ->willReturn(true); + + $this->rootFolder->expects($this->never())->method($this->anything()); + + $share = $this->shareManager->newShare(); + $share->setSharedWith('user@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + $this->provider->create($share); + + $node2 = $this->getMock('\OCP\Files\File'); + $node2->method('getId')->willReturn(43); + $node2->method('getName')->willReturn('myOtherFile'); + + $share2 = $this->shareManager->newShare(); + $share2->setSharedWith('user@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node2); + $this->provider->create($share2); + + $shares = $this->provider->getSharesBy('sharedBy', \OCP\Share::SHARE_TYPE_REMOTE, $node2, false, -1, 0); + + $this->assertCount(1, $shares); + $this->assertEquals(43, $shares[0]->getNodeId()); + } + + public function testGetSharedByWithReshares() { + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $this->tokenHandler->method('generateToken')->willReturn('token'); + $this->notifications + ->method('sendRemoteShare') + ->willReturn(true); + + $this->rootFolder->expects($this->never())->method($this->anything()); + + $share = $this->shareManager->newShare(); + $share->setSharedWith('user@server.com') + ->setSharedBy('shareOwner') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + $this->provider->create($share); + + $share2 = $this->shareManager->newShare(); + $share2->setSharedWith('user2@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + $this->provider->create($share2); + + $shares = $this->provider->getSharesBy('shareOwner', \OCP\Share::SHARE_TYPE_REMOTE, null, true, -1, 0); + + $this->assertCount(2, $shares); + } + + public function testGetSharedByWithLimit() { + $node = $this->getMock('\OCP\Files\File'); + $node->method('getId')->willReturn(42); + $node->method('getName')->willReturn('myFile'); + + $this->tokenHandler->method('generateToken')->willReturn('token'); + $this->notifications + ->method('sendRemoteShare') + ->willReturn(true); + + $this->rootFolder->expects($this->never())->method($this->anything()); + + $share = $this->shareManager->newShare(); + $share->setSharedWith('user@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + $this->provider->create($share); + + $share2 = $this->shareManager->newShare(); + $share2->setSharedWith('user2@server.com') + ->setSharedBy('sharedBy') + ->setShareOwner('shareOwner') + ->setPermissions(19) + ->setNode($node); + $this->provider->create($share2); + + $shares = $this->provider->getSharesBy('shareOwner', \OCP\Share::SHARE_TYPE_REMOTE, null, true, 1, 1); + + $this->assertCount(1, $shares); + $this->assertEquals('user2@server.com', $shares[0]->getSharedWith()); + } +} diff --git a/apps/federatedfilesharing/tests/tokenhandlertest.php b/apps/federatedfilesharing/tests/tokenhandlertest.php new file mode 100644 index 00000000000..4ff428d4a46 --- /dev/null +++ b/apps/federatedfilesharing/tests/tokenhandlertest.php @@ -0,0 +1,62 @@ +<?php +/** + * @author Björn Schießle <schiessle@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\FederatedFileSharing\Tests; + + +use OCA\FederatedFileSharing\TokenHandler; +use OCP\Security\ISecureRandom; +use Test\TestCase; + +class TokenHandlerTest extends TestCase { + + /** @var TokenHandler */ + private $tokenHandler; + + /** @var ISecureRandom | \PHPUnit_Framework_MockObject_MockObject */ + private $secureRandom; + + /** @var int */ + private $expectedTokenLength = 15; + + public function setUp() { + parent::setUp(); + + $this->secureRandom = $this->getMock('OCP\Security\ISecureRandom'); + + $this->tokenHandler = new TokenHandler($this->secureRandom); + } + + public function testGenerateToken() { + + $this->secureRandom->expects($this->once())->method('generate') + ->with( + $this->expectedTokenLength, + ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS + ) + ->willReturn(true); + + $this->assertTrue($this->tokenHandler->generateToken()); + + } + +} diff --git a/apps/files_external/appinfo/register_command.php b/apps/files_external/appinfo/register_command.php index 5f6f42bf8c1..927ce9869f9 100644 --- a/apps/files_external/appinfo/register_command.php +++ b/apps/files_external/appinfo/register_command.php @@ -29,6 +29,7 @@ use OCA\Files_External\Command\Export; use OCA\Files_External\Command\Delete; use OCA\Files_External\Command\Create; use OCA\Files_External\Command\Backends; +use OCA\Files_External\Command\Verify; $userManager = OC::$server->getUserManager(); $userSession = OC::$server->getUserSession(); @@ -51,3 +52,4 @@ $application->add(new Export($globalStorageService, $userStorageService, $userSe $application->add(new Delete($globalStorageService, $userStorageService, $userSession, $userManager)); $application->add(new Create($globalStorageService, $userStorageService, $userManager, $userSession, $backendService)); $application->add(new Backends($backendService)); +$application->add(new Verify($globalStorageService)); diff --git a/apps/files_external/command/verify.php b/apps/files_external/command/verify.php new file mode 100644 index 00000000000..f985cb401af --- /dev/null +++ b/apps/files_external/command/verify.php @@ -0,0 +1,145 @@ +<?php +/** + * @author Robin Appelman <icewind@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\Files_External\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_external\Lib\StorageConfig; +use OCA\Files_external\NotFoundException; +use OCA\Files_external\Service\GlobalStoragesService; +use OCP\Files\StorageNotAvailableException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Verify extends Base { + /** + * @var GlobalStoragesService + */ + protected $globalService; + + function __construct(GlobalStoragesService $globalService) { + parent::__construct(); + $this->globalService = $globalService; + } + + protected function configure() { + $this + ->setName('files_external:verify') + ->setDescription('Verify mount configuration') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'The id of the mount to check' + )->addOption( + 'config', + 'c', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Additional config option to set before checking in key=value pairs, required for certain auth backends such as login credentails' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $mountId = $input->getArgument('mount_id'); + $configInput = $input->getOption('config'); + + try { + $mount = $this->globalService->getStorage($mountId); + } catch (NotFoundException $e) { + $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>'); + return 404; + } + + $this->updateStorageStatus($mount, $configInput, $output); + + $this->writeArrayInOutputFormat($input, $output, [ + 'status' => StorageNotAvailableException::getStateCodeName($mount->getStatus()), + 'code' => $mount->getStatus(), + 'message' => $mount->getStatusMessage() + ]); + } + + private function manipulateStorageConfig(StorageConfig $storage) { + /** @var AuthMechanism */ + $authMechanism = $storage->getAuthMechanism(); + $authMechanism->manipulateStorageConfig($storage); + /** @var Backend */ + $backend = $storage->getBackend(); + $backend->manipulateStorageConfig($storage); + } + + private function updateStorageStatus(StorageConfig &$storage, $configInput, OutputInterface $output) { + try { + try { + $this->manipulateStorageConfig($storage); + } catch (InsufficientDataForMeaningfulAnswerException $e) { + if (count($configInput) === 0) { // extra config options might solve the error + throw $e; + } + } + + foreach ($configInput as $configOption) { + if (!strpos($configOption, '=')) { + $output->writeln('<error>Invalid mount configuration option "' . $configOption . '"</error>'); + return; + } + list($key, $value) = explode('=', $configOption, 2); + $storage->setBackendOption($key, $value); + } + + /** @var Backend */ + $backend = $storage->getBackend(); + // update status (can be time-consuming) + $storage->setStatus( + \OC_Mount_Config::getBackendStatus( + $backend->getStorageClass(), + $storage->getBackendOptions(), + false + ) + ); + } catch (InsufficientDataForMeaningfulAnswerException $e) { + $status = $e->getCode() ? $e->getCode() : StorageNotAvailableException::STATUS_INDETERMINATE; + $storage->setStatus( + $status, + $e->getMessage() + ); + } catch (StorageNotAvailableException $e) { + $storage->setStatus( + $e->getCode(), + $e->getMessage() + ); + } catch (\Exception $e) { + // FIXME: convert storage exceptions to StorageNotAvailableException + $storage->setStatus( + StorageNotAvailableException::STATUS_ERROR, + get_class($e) . ': ' . $e->getMessage() + ); + } + } +} diff --git a/apps/files_sharing/api/share20ocs.php b/apps/files_sharing/api/share20ocs.php index c03243d1d41..67a94aaf8aa 100644 --- a/apps/files_sharing/api/share20ocs.php +++ b/apps/files_sharing/api/share20ocs.php @@ -26,6 +26,7 @@ use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUser; use OCP\Files\IRootFolder; +use OCP\Share; use OCP\Share\IManager; use OCP\Share\Exceptions\ShareNotFound; @@ -164,8 +165,15 @@ class Share20OCS { } if ($share === null) { - //For now federated shares are handled by the old endpoint. - return \OCA\Files_Sharing\API\Local::getShare(['id' => $id]); + if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { + return new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); + } + + try { + $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id); + } catch (ShareNotFound $e) { + return new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); + } } if ($this->canAccessShare($share)) { @@ -195,7 +203,15 @@ class Share20OCS { // Could not find the share as internal share... maybe it is a federated share if ($share === null) { - return \OCA\Files_Sharing\API\Local::deleteShare(['id' => $id]); + if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { + return new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); + } + + try { + $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id); + } catch (ShareNotFound $e) { + return new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); + } } if (!$this->canAccessShare($share)) { @@ -313,8 +329,12 @@ class Share20OCS { } } else if ($shareType === \OCP\Share::SHARE_TYPE_REMOTE) { - //fixme Remote shares are handled by old code path for now - return \OCA\Files_Sharing\API\Local::createShare([]); + if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { + return new \OC_OCS_Result(null, 403, 'Sharing '.$path.' failed, because the backend does not allow shares from type '.$shareType); + } + + $share->setSharedWith($shareWith); + $share->setPermissions($permissions); } else { return new \OC_OCS_Result(null, 400, "unknown share type"); } @@ -368,11 +388,12 @@ class Share20OCS { /** @var \OCP\Share\IShare[] $shares */ $shares = []; foreach ($nodes as $node) { - $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_USER, $node, false, -1, 0)); + $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_USER, $node, false, -1, 0)); $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_GROUP, $node, false, -1, 0)); - $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_LINK, $node, false, -1, 0)); - //TODO: Add federated shares - + $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_LINK, $node, false, -1, 0)); + if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { + $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_REMOTE, $node, false, -1, 0)); + } } $formatted = []; @@ -427,10 +448,14 @@ class Share20OCS { $userShares = $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_USER, $path, $reshares, -1, 0); $groupShares = $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_GROUP, $path, $reshares, -1, 0); $linkShares = $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_LINK, $path, $reshares, -1, 0); - //TODO: Add federated shares - $shares = array_merge($userShares, $groupShares, $linkShares); + if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { + $federatedShares = $this->shareManager->getSharesBy($this->currentUser->getUID(), \OCP\Share::SHARE_TYPE_REMOTE, $path, $reshares, -1, 0); + $shares = array_merge($shares, $federatedShares); + } + + $formatted = []; foreach ($shares as $share) { $formatted[] = $this->formatShare($share); @@ -456,7 +481,15 @@ class Share20OCS { // Could not find the share as internal share... maybe it is a federated share if ($share === null) { - return \OCA\Files_Sharing\API\Local::updateShare(['id' => $id]); + if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { + return new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); + } + + try { + $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id); + } catch (ShareNotFound $e) { + return new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); + } } if (!$this->canAccessShare($share)) { diff --git a/apps/files_sharing/appinfo/info.xml b/apps/files_sharing/appinfo/info.xml index 17826be47b4..29ae15e4722 100644 --- a/apps/files_sharing/appinfo/info.xml +++ b/apps/files_sharing/appinfo/info.xml @@ -10,7 +10,7 @@ Turning the feature off removes shared files and folders on the server for all s <licence>AGPL</licence> <author>Michael Gapczynski, Bjoern Schiessle</author> <default_enable/> - <version>0.9.0</version> + <version>0.9.1</version> <types> <filesystem/> </types> diff --git a/apps/files_sharing/appinfo/update.php b/apps/files_sharing/appinfo/update.php index d754a95705c..ced227a107b 100644 --- a/apps/files_sharing/appinfo/update.php +++ b/apps/files_sharing/appinfo/update.php @@ -25,7 +25,7 @@ use OCA\Files_Sharing\Migration; $installedVersion = \OC::$server->getConfig()->getAppValue('files_sharing', 'installed_version'); // Migration OC8.2 -> OC9 -if (version_compare($installedVersion, '0.9.0', '<')) { +if (version_compare($installedVersion, '0.9.1', '<')) { $m = new Migration(\OC::$server->getDatabaseConnection()); $m->removeReShares(); $m->updateInitiatorInfo(); diff --git a/apps/files_sharing/lib/migration.php b/apps/files_sharing/lib/migration.php index 90e0dead480..e7346385510 100644 --- a/apps/files_sharing/lib/migration.php +++ b/apps/files_sharing/lib/migration.php @@ -142,7 +142,8 @@ class Migration { [ \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP, - \OCP\Share::SHARE_TYPE_LINK + \OCP\Share::SHARE_TYPE_LINK, + \OCP\Share::SHARE_TYPE_REMOTE, ], Connection::PARAM_INT_ARRAY ) @@ -185,7 +186,8 @@ class Migration { [ \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP, - \OCP\Share::SHARE_TYPE_LINK + \OCP\Share::SHARE_TYPE_LINK, + \OCP\Share::SHARE_TYPE_REMOTE, ], Connection::PARAM_INT_ARRAY ) diff --git a/apps/files_sharing/tests/api/share20ocstest.php b/apps/files_sharing/tests/api/share20ocstest.php index 97abdca7ac3..a1094ce4b22 100644 --- a/apps/files_sharing/tests/api/share20ocstest.php +++ b/apps/files_sharing/tests/api/share20ocstest.php @@ -97,10 +97,17 @@ class Share20OCSTest extends \Test\TestCase { public function testDeleteShareShareNotFound() { $this->shareManager - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('getShareById') - ->with('ocinternal:42') - ->will($this->throwException(new \OCP\Share\Exceptions\ShareNotFound())); + ->will($this->returnCallback(function($id) { + if ($id === 'ocinternal:42' || $id === 'ocFederatedSharing:42') { + throw new \OCP\Share\Exceptions\ShareNotFound(); + } else { + throw new \Exception(); + } + })); + + $this->shareManager->method('outgoingServer2ServerSharesAllowed')->willReturn(true); $expected = new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); $this->assertEquals($expected, $this->ocs->deleteShare(42)); diff --git a/apps/files_sharing/tests/migrationtest.php b/apps/files_sharing/tests/migrationtest.php index e1c047e0342..8a40b76a642 100644 --- a/apps/files_sharing/tests/migrationtest.php +++ b/apps/files_sharing/tests/migrationtest.php @@ -209,6 +209,23 @@ class MigrationTest extends TestCase { $this->assertSame(1, $query->execute() ); + $parent = $query->getLastInsertId(); + // third re-share, should be attached to the first user share after migration + $query->setParameter('share_type', \OCP\Share::SHARE_TYPE_REMOTE) + ->setParameter('share_with', 'user@server.com') + ->setParameter('uid_owner', 'user3') + ->setParameter('uid_initiator', '') + ->setParameter('parent', $parent) + ->setParameter('item_type', 'file') + ->setParameter('item_source', '2') + ->setParameter('item_target', '/2') + ->setParameter('file_source', 2) + ->setParameter('file_target', '/foobar') + ->setParameter('permissions', 31) + ->setParameter('stime', time()); + $this->assertSame(1, + $query->execute() + ); } public function testRemoveReShares() { @@ -221,7 +238,7 @@ class MigrationTest extends TestCase { $query = $this->connection->getQueryBuilder(); $query->select('*')->from($this->table)->orderBy('id'); $result = $query->execute()->fetchAll(); - $this->assertSame(8, count($result)); + $this->assertSame(9, count($result)); // shares which shouldn't be modified for ($i = 0; $i < 4; $i++) { @@ -238,7 +255,7 @@ class MigrationTest extends TestCase { $this->assertEmpty($result[5]['uid_initiator']); $this->assertNull($result[5]['parent']); // flatted re-shares - for($i = 6; $i < 8; $i++) { + for($i = 6; $i < 9; $i++) { $this->assertSame('owner2', $result[$i]['uid_owner']); $user = 'user' . ($i - 5); $this->assertSame($user, $result[$i]['uid_initiator']); diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 3b8633b872a..af177b713dd 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -284,6 +284,7 @@ Feature: provisioning And apps returned are | comments | | dav | + | federatedfilesharing | | files | | files_sharing | | files_trashbin | diff --git a/core/js/update.js b/core/js/update.js index 1626b6f2c49..77ac1bb20ff 100644 --- a/core/js/update.js +++ b/core/js/update.js @@ -60,12 +60,16 @@ updateEventSource.listen('failure', function(message) { $(window).off('beforeunload.inprogress'); $('<span>').addClass('error').append(message).append('<br />').appendTo($el); - $('<span>') - .addClass('bold') - .append(t('core', 'The update was unsuccessful. ' + - 'Please report this issue to the ' + - '<a href="https://github.com/owncloud/core/issues" target="_blank">ownCloud community</a>.')) - .appendTo($el); + var span = $('<span>') + .addClass('bold'); + if(message === 'Exception: Updates between multiple major versions and downgrades are unsupported.') { + span.append(t('core', 'The update was unsuccessful. For more information <a href="{url}">check our forum post</a> covering this issue.', {'url': 'https://forum.owncloud.org/viewtopic.php?f=17&t=32087'})); + } else { + span.append(t('core', 'The update was unsuccessful. ' + + 'Please report this issue to the ' + + '<a href="https://github.com/owncloud/core/issues" target="_blank">ownCloud community</a>.')); + } + span.appendTo($el); }); updateEventSource.listen('done', function() { $(window).off('beforeunload.inprogress'); diff --git a/core/shipped.json b/core/shipped.json index 069bb210ba7..b74f2f28c47 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -8,6 +8,7 @@ "enterprise_key", "external", "federation", + "federatedfilesharing", "files", "files_antivirus", "files_drop", @@ -39,6 +40,7 @@ ], "alwaysEnabled": [ "files", - "dav" + "dav", + "federatedfilesharing" ] } diff --git a/lib/private/files/config/usermountcache.php b/lib/private/files/config/usermountcache.php index a2da3e9f528..35f40353190 100644 --- a/lib/private/files/config/usermountcache.php +++ b/lib/private/files/config/usermountcache.php @@ -129,7 +129,7 @@ class UserMountCache implements IUserMountCache { 'root_id' => $mount->getRootId(), 'user_id' => $mount->getUser()->getUID(), 'mount_point' => $mount->getMountPoint() - ]); + ], ['root_id', 'user_id']); } private function setMountPoint(ICachedMountInfo $mount) { diff --git a/lib/private/files/node/file.php b/lib/private/files/node/file.php index cf163b9b763..f8279c00b8c 100644 --- a/lib/private/files/node/file.php +++ b/lib/private/files/node/file.php @@ -169,6 +169,6 @@ class File extends Node implements \OCP\Files\File { * @inheritdoc */ public function getChecksum() { - return $this->fileInfo->getChecksum(); + return $this->getFileInfo()->getChecksum(); } } diff --git a/lib/private/share20/manager.php b/lib/private/share20/manager.php index 33085410e1d..7cd44a7cb37 100644 --- a/lib/private/share20/manager.php +++ b/lib/private/share20/manager.php @@ -166,6 +166,10 @@ class Manager implements IManager { if ($share->getSharedWith() !== null) { throw new \InvalidArgumentException('SharedWith should be empty'); } + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_REMOTE) { + if ($share->getSharedWith() === null) { + throw new \InvalidArgumentException('SharedWith should not be empty'); + } } else { // We can't handle other types yet throw new \InvalidArgumentException('unkown share type'); @@ -1068,4 +1072,11 @@ class Manager implements IManager { return false; } + /** + * @inheritdoc + */ + public function outgoingServer2ServerSharesAllowed() { + return $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes'; + } + } diff --git a/lib/private/share20/providerfactory.php b/lib/private/share20/providerfactory.php index 64147355596..bb440d2e01f 100644 --- a/lib/private/share20/providerfactory.php +++ b/lib/private/share20/providerfactory.php @@ -20,6 +20,10 @@ */ namespace OC\Share20; +use OCA\FederatedFileSharing\AddressHandler; +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\FederatedFileSharing\Notifications; +use OCA\FederatedFileSharing\TokenHandler; use OCP\Share\IProviderFactory; use OC\Share20\Exception\ProviderException; use OCP\IServerContainer; @@ -35,6 +39,8 @@ class ProviderFactory implements IProviderFactory { private $serverContainer; /** @var DefaultShareProvider */ private $defaultProvider = null; + /** @var FederatedShareProvider */ + private $federatedProvider = null; /** * IProviderFactory constructor. @@ -63,28 +69,88 @@ class ProviderFactory implements IProviderFactory { } /** + * Create the federated share provider + * + * @return FederatedShareProvider + */ + protected function federatedShareProvider() { + if ($this->federatedProvider === null) { + /* + * Check if the app is enabled + */ + $appManager = $this->serverContainer->getAppManager(); + if (!$appManager->isEnabledForUser('federatedfilesharing')) { + return null; + } + + /* + * TODO: add factory to federated sharing app + */ + $l = $this->serverContainer->getL10N('federatedfilessharing'); + $addressHandler = new AddressHandler( + $this->serverContainer->getURLGenerator(), + $l + ); + $notifications = new Notifications( + $addressHandler, + $this->serverContainer->getHTTPClientService() + ); + $tokenHandler = new TokenHandler( + $this->serverContainer->getSecureRandom() + ); + + $this->federatedProvider = new FederatedShareProvider( + $this->serverContainer->getDatabaseConnection(), + $addressHandler, + $notifications, + $tokenHandler, + $l, + $this->serverContainer->getLogger(), + $this->serverContainer->getRootFolder() + ); + } + + return $this->federatedProvider; + } + + /** * @inheritdoc */ public function getProvider($id) { + $provider = null; if ($id === 'ocinternal') { - return $this->defaultShareProvider(); + $provider = $this->defaultShareProvider(); + } else if ($id === 'ocFederatedSharing') { + $provider = $this->federatedShareProvider(); } - throw new ProviderException('No provider with id .' . $id . ' found.'); + if ($provider === null) { + throw new ProviderException('No provider with id .' . $id . ' found.'); + } + + return $provider; } /** * @inheritdoc */ public function getProviderForType($shareType) { + $provider = null; + //FIXME we should not report type 2 if ($shareType === \OCP\Share::SHARE_TYPE_USER || $shareType === 2 || $shareType === \OCP\Share::SHARE_TYPE_GROUP || $shareType === \OCP\Share::SHARE_TYPE_LINK) { - return $this->defaultShareProvider(); + $provider = $this->defaultShareProvider(); + } else if ($shareType === \OCP\Share::SHARE_TYPE_REMOTE) { + $provider = $this->federatedShareProvider(); + } + + if ($provider === null) { + throw new ProviderException('No share provider for share type ' . $shareType); } - throw new ProviderException('No share provider for share type ' . $shareType); + return $provider; } } diff --git a/lib/public/files/file.php b/lib/public/files/file.php index 3acf24b9277..1550f92682b 100644 --- a/lib/public/files/file.php +++ b/lib/public/files/file.php @@ -84,4 +84,14 @@ interface File extends Node { * @since 6.0.0 */ public function hash($type, $raw = false); + + /** + * Get the stored checksum for this file + * + * @return string + * @since 9.0.0 + * @throws InvalidPathException + * @throws NotFoundException + */ + public function getChecksum(); } diff --git a/lib/public/files/storagenotavailableexception.php b/lib/public/files/storagenotavailableexception.php index f9ac79d66ec..dd3915a2f6a 100644 --- a/lib/public/files/storagenotavailableexception.php +++ b/lib/public/files/storagenotavailableexception.php @@ -58,4 +58,30 @@ class StorageNotAvailableException extends HintException { $l = \OC::$server->getL10N('core'); parent::__construct($message, $l->t('Storage not available'), $code, $previous); } + + /** + * Get the name for a status code + * + * @param int $code + * @return string + * @since 9.0.0 + */ + public static function getStateCodeName($code) { + switch ($code) { + case self::STATUS_SUCCESS: + return 'ok'; + case self::STATUS_ERROR: + return 'error'; + case self::STATUS_INDETERMINATE: + return 'indeterminate'; + case self::STATUS_UNAUTHORIZED: + return 'unauthorized'; + case self::STATUS_TIMEOUT: + return 'timeout'; + case self::STATUS_NETWORK_ERROR: + return 'network error'; + default: + return 'unknown'; + } + } } diff --git a/lib/public/share/imanager.php b/lib/public/share/imanager.php index 4d79f97c31a..f926104dde0 100644 --- a/lib/public/share/imanager.php +++ b/lib/public/share/imanager.php @@ -229,4 +229,11 @@ interface IManager { */ public function sharingDisabledForUser($userId); + /** + * Check if outgoing server2server shares are allowed + * @return bool + * @since 9.0.0 + */ + public function outgoingServer2ServerSharesAllowed(); + } diff --git a/tests/enable_all.php b/tests/enable_all.php index 6f2d1fa8717..afea5c89665 100644 --- a/tests/enable_all.php +++ b/tests/enable_all.php @@ -23,3 +23,4 @@ enableApp('user_ldap'); enableApp('files_versions'); enableApp('provisioning_api'); enableApp('federation'); +enableApp('federatedfilesharing'); diff --git a/tests/lib/app.php b/tests/lib/app.php index 389c9e5d2cf..3fb42ea2382 100644 --- a/tests/lib/app.php +++ b/tests/lib/app.php @@ -312,6 +312,7 @@ class Test_App extends \Test\TestCase { 'appforgroup1', 'appforgroup12', 'dav', + 'federatedfilesharing', ), false ), @@ -325,6 +326,7 @@ class Test_App extends \Test\TestCase { 'appforgroup12', 'appforgroup2', 'dav', + 'federatedfilesharing', ), false ), @@ -339,6 +341,7 @@ class Test_App extends \Test\TestCase { 'appforgroup12', 'appforgroup2', 'dav', + 'federatedfilesharing', ), false ), @@ -353,6 +356,7 @@ class Test_App extends \Test\TestCase { 'appforgroup12', 'appforgroup2', 'dav', + 'federatedfilesharing', ), false, ), @@ -367,6 +371,7 @@ class Test_App extends \Test\TestCase { 'appforgroup12', 'appforgroup2', 'dav', + 'federatedfilesharing', ), true, ), @@ -444,11 +449,11 @@ class Test_App extends \Test\TestCase { ); $apps = \OC_App::getEnabledApps(true); - $this->assertEquals(array('files', 'app3', 'dav'), $apps); + $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing',), $apps); // mock should not be called again here $apps = \OC_App::getEnabledApps(false); - $this->assertEquals(array('files', 'app3', 'dav'), $apps); + $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing',), $apps); $this->restoreAppConfig(); \OC_User::setUserId(null); diff --git a/tests/lib/app/manager.php b/tests/lib/app/manager.php index f82f1049ce3..ee9b1f308ea 100644 --- a/tests/lib/app/manager.php +++ b/tests/lib/app/manager.php @@ -282,7 +282,7 @@ class Manager extends TestCase { $this->appConfig->setValue('test1', 'enabled', 'yes'); $this->appConfig->setValue('test2', 'enabled', 'no'); $this->appConfig->setValue('test3', 'enabled', '["foo"]'); - $this->assertEquals(['dav', 'files', 'test1', 'test3'], $this->manager->getInstalledApps()); + $this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3'], $this->manager->getInstalledApps()); } public function testGetAppsForUser() { @@ -296,7 +296,7 @@ class Manager extends TestCase { $this->appConfig->setValue('test2', 'enabled', 'no'); $this->appConfig->setValue('test3', 'enabled', '["foo"]'); $this->appConfig->setValue('test4', 'enabled', '["asd"]'); - $this->assertEquals(['dav', 'files', 'test1', 'test3'], $this->manager->getEnabledAppsForUser($user)); + $this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3'], $this->manager->getEnabledAppsForUser($user)); } public function testGetAppsNeedingUpgrade() { @@ -308,6 +308,7 @@ class Manager extends TestCase { $appInfos = [ 'dav' => ['id' => 'dav'], 'files' => ['id' => 'files'], + 'federatedfilesharing' => ['id' => 'federatedfilesharing'], 'test1' => ['id' => 'test1', 'version' => '1.0.1', 'requiremax' => '9.0.0'], 'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'], 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], @@ -348,6 +349,7 @@ class Manager extends TestCase { $appInfos = [ 'dav' => ['id' => 'dav'], 'files' => ['id' => 'files'], + 'federatedfilesharing' => ['id' => 'federatedfilesharing'], 'test1' => ['id' => 'test1', 'version' => '1.0.1', 'requiremax' => '8.0.0'], 'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'], 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], diff --git a/tests/phpunit-autotest.xml b/tests/phpunit-autotest.xml index 499c69bc9d1..7ef7c12df82 100644 --- a/tests/phpunit-autotest.xml +++ b/tests/phpunit-autotest.xml @@ -17,7 +17,7 @@ <filter> <!-- whitelist processUncoveredFilesFromWhitelist="true" --> <whitelist> - <directory suffix=".php">..</directory> + <directory suffix=".php">../apps/federatedfilesharing/</directory> <exclude> <directory suffix=".php">../3rdparty</directory> <directory suffix=".php">../apps/files/l10n</directory> |