From 8dc25321d3ca1725b970e86aa72e1f8106e05913 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 18 May 2016 15:28:50 +0200 Subject: Move FederatedFileSharing to PSR-4 --- apps/federatedfilesharing/lib/AddressHandler.php | 184 ++++++ .../lib/AppInfo/Application.php | 88 +++ .../lib/BackgroundJob/UnShare.php | 142 +++++ apps/federatedfilesharing/lib/DiscoveryManager.php | 136 +++++ .../lib/FederatedShareProvider.php | 632 +++++++++++++++++++++ apps/federatedfilesharing/lib/Notifications.php | 184 ++++++ apps/federatedfilesharing/lib/TokenHandler.php | 61 ++ apps/federatedfilesharing/lib/addresshandler.php | 184 ------ apps/federatedfilesharing/lib/discoverymanager.php | 136 ----- .../lib/federatedshareprovider.php | 632 --------------------- apps/federatedfilesharing/lib/notifications.php | 184 ------ apps/federatedfilesharing/lib/tokenhandler.php | 61 -- 12 files changed, 1427 insertions(+), 1197 deletions(-) create mode 100644 apps/federatedfilesharing/lib/AddressHandler.php create mode 100644 apps/federatedfilesharing/lib/AppInfo/Application.php create mode 100644 apps/federatedfilesharing/lib/BackgroundJob/UnShare.php create mode 100644 apps/federatedfilesharing/lib/DiscoveryManager.php create mode 100644 apps/federatedfilesharing/lib/FederatedShareProvider.php create mode 100644 apps/federatedfilesharing/lib/Notifications.php create mode 100644 apps/federatedfilesharing/lib/TokenHandler.php delete mode 100644 apps/federatedfilesharing/lib/addresshandler.php delete mode 100644 apps/federatedfilesharing/lib/discoverymanager.php delete mode 100644 apps/federatedfilesharing/lib/federatedshareprovider.php delete mode 100644 apps/federatedfilesharing/lib/notifications.php delete mode 100644 apps/federatedfilesharing/lib/tokenhandler.php (limited to 'apps/federatedfilesharing/lib') 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 @@ + + * + * @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 + * + */ + +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/AppInfo/Application.php b/apps/federatedfilesharing/lib/AppInfo/Application.php new file mode 100644 index 00000000000..5a213aec8e2 --- /dev/null +++ b/apps/federatedfilesharing/lib/AppInfo/Application.php @@ -0,0 +1,88 @@ + + * + * @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 + * + */ + + +namespace OCA\FederatedFileSharing\AppInfo; + + +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCP\AppFramework\App; + +class Application extends App { + + /** @var FederatedShareProvider */ + protected $federatedShareProvider; + + /** + * register personal and admin settings page + */ + public function registerSettings() { + \OCP\App::registerAdmin('federatedfilesharing', 'settings-admin'); + \OCP\App::registerPersonal('federatedfilesharing', 'settings-personal'); + } + + /** + * get instance of federated share provider + * + * @return FederatedShareProvider + */ + public function getFederatedShareProvider() { + if ($this->federatedShareProvider === null) { + $this->initFederatedShareProvider(); + } + return $this->federatedShareProvider; + } + + /** + * initialize federated share provider + */ + protected function initFederatedShareProvider() { + $addressHandler = new \OCA\FederatedFileSharing\AddressHandler( + \OC::$server->getURLGenerator(), + \OC::$server->getL10N('federatedfilesharing') + ); + $discoveryManager = new \OCA\FederatedFileSharing\DiscoveryManager( + \OC::$server->getMemCacheFactory(), + \OC::$server->getHTTPClientService() + ); + $notifications = new \OCA\FederatedFileSharing\Notifications( + $addressHandler, + \OC::$server->getHTTPClientService(), + $discoveryManager, + \OC::$server->getJobList() + ); + $tokenHandler = new \OCA\FederatedFileSharing\TokenHandler( + \OC::$server->getSecureRandom() + ); + + $this->federatedShareProvider = new \OCA\FederatedFileSharing\FederatedShareProvider( + \OC::$server->getDatabaseConnection(), + $addressHandler, + $notifications, + $tokenHandler, + \OC::$server->getL10N('federatedfilesharing'), + \OC::$server->getLogger(), + \OC::$server->getLazyRootFolder(), + \OC::$server->getConfig() + ); + } + +} diff --git a/apps/federatedfilesharing/lib/BackgroundJob/UnShare.php b/apps/federatedfilesharing/lib/BackgroundJob/UnShare.php new file mode 100644 index 00000000000..b056db4eac7 --- /dev/null +++ b/apps/federatedfilesharing/lib/BackgroundJob/UnShare.php @@ -0,0 +1,142 @@ + + * + * @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 + * + */ + + +namespace OCA\FederatedFileSharing\BackgroundJob; + + +use OC\BackgroundJob\Job; +use OC\BackgroundJob\JobList; +use OCA\FederatedFileSharing\AddressHandler; +use OCA\FederatedFileSharing\DiscoveryManager; +use OCA\FederatedFileSharing\Notifications; +use OCP\BackgroundJob\IJobList; +use OCP\ILogger; + +/** + * Class UnShare + * + * Background job to re-send the un-share notification to the remote server in + * case the server was not available on the first try + * + * @package OCA\FederatedFileSharing\BackgroundJob + */ +class UnShare extends Job { + + /** @var bool */ + private $retainJob = true; + + /** @var Notifications */ + private $notifications; + + /** @var int max number of attempts to send the un-share request */ + private $maxTry = 10; + + /** @var int how much time should be between two tries (12 hours) */ + private $interval = 43200; + + /** + * UnShare constructor. + * + * @param Notifications $notifications + */ + public function __construct(Notifications $notifications = null) { + if ($notifications) { + $this->notifications = $notifications; + } else { + $addressHandler = new AddressHandler( + \OC::$server->getURLGenerator(), + \OC::$server->getL10N('federatedfilesharing') + ); + $discoveryManager = new DiscoveryManager( + \OC::$server->getMemCacheFactory(), + \OC::$server->getHTTPClientService() + ); + $this->notifications = new Notifications( + $addressHandler, + \OC::$server->getHTTPClientService(), + $discoveryManager, + \OC::$server->getJobList() + ); + } + + } + + /** + * run the job, then remove it from the jobList + * + * @param JobList $jobList + * @param ILogger $logger + */ + public function execute($jobList, ILogger $logger = null) { + + if ($this->shouldRun($this->argument)) { + parent::execute($jobList, $logger); + $jobList->remove($this, $this->argument); + if ($this->retainJob) { + $this->reAddJob($jobList, $this->argument); + } + } + } + + protected function run($argument) { + $remote = $argument['remote']; + $id = (int)$argument['id']; + $token = $argument['token']; + $try = (int)$argument['try'] + 1; + + $result = $this->notifications->sendRemoteUnShare($remote, $id, $token, $try); + + if ($result === true || $try > $this->maxTry) { + $this->retainJob = false; + } + } + + /** + * re-add background job with new arguments + * + * @param IJobList $jobList + * @param array $argument + */ + protected function reAddJob(IJobList $jobList, array $argument) { + $jobList->add('OCA\FederatedFileSharing\BackgroundJob\UnShare', + [ + 'remote' => $argument['remote'], + 'id' => $argument['id'], + 'token' => $argument['token'], + 'try' => (int)$argument['try'] + 1, + 'lastRun' => time() + ] + ); + } + + /** + * test if it is time for the next run + * + * @param array $argument + * @return bool + */ + protected function shouldRun(array $argument) { + $lastRun = (int)$argument['lastRun']; + return ((time() - $lastRun) > $this->interval); + } + +} diff --git a/apps/federatedfilesharing/lib/DiscoveryManager.php b/apps/federatedfilesharing/lib/DiscoveryManager.php new file mode 100644 index 00000000000..51ea71195fa --- /dev/null +++ b/apps/federatedfilesharing/lib/DiscoveryManager.php @@ -0,0 +1,136 @@ + + * + * @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 + * + */ + +namespace OCA\FederatedFileSharing; + +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ConnectException; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\ICache; +use OCP\ICacheFactory; + +/** + * Class DiscoveryManager handles the discovery of endpoints used by Federated + * Cloud Sharing. + * + * @package OCA\FederatedFileSharing + */ +class DiscoveryManager { + /** @var ICache */ + private $cache; + /** @var IClient */ + private $client; + + /** + * @param ICacheFactory $cacheFactory + * @param IClientService $clientService + */ + public function __construct(ICacheFactory $cacheFactory, + IClientService $clientService) { + $this->cache = $cacheFactory->create('ocs-discovery'); + $this->client = $clientService->newClient(); + } + + /** + * Returns whether the specified URL includes only safe characters, if not + * returns false + * + * @param string $url + * @return bool + */ + private function isSafeUrl($url) { + return (bool)preg_match('/^[\/\.A-Za-z0-9]+$/', $url); + } + + /** + * Discover the actual data and do some naive caching to ensure that the data + * is not requested multiple times. + * + * If no valid discovery data is found the ownCloud defaults are returned. + * + * @param string $remote + * @return array + */ + private function discover($remote) { + // Check if something is in the cache + if($cacheData = $this->cache->get($remote)) { + return json_decode($cacheData, true); + } + + // Default response body + $discoveredServices = [ + 'webdav' => '/public.php/webdav', + 'share' => '/ocs/v1.php/cloud/shares', + ]; + + // Read the data from the response body + try { + $response = $this->client->get($remote . '/ocs-provider/'); + if($response->getStatusCode() === 200) { + $decodedService = json_decode($response->getBody(), true); + if(is_array($decodedService)) { + $endpoints = [ + 'webdav', + 'share', + ]; + + foreach($endpoints as $endpoint) { + if(isset($decodedService['services']['FEDERATED_SHARING']['endpoints'][$endpoint])) { + $endpointUrl = (string)$decodedService['services']['FEDERATED_SHARING']['endpoints'][$endpoint]; + if($this->isSafeUrl($endpointUrl)) { + $discoveredServices[$endpoint] = $endpointUrl; + } + } + } + } + } + } catch (ClientException $e) { + // Don't throw any exception since exceptions are handled before + } catch (ConnectException $e) { + // Don't throw any exception since exceptions are handled before + } + + // Write into cache + $this->cache->set($remote, json_encode($discoveredServices)); + return $discoveredServices; + } + + /** + * Return the public WebDAV endpoint used by the specified remote + * + * @param string $host + * @return string + */ + public function getWebDavEndpoint($host) { + return $this->discover($host)['webdav']; + } + + /** + * Return the sharing endpoint used by the specified remote + * + * @param string $host + * @return string + */ + public function getShareEndpoint($host) { + return $this->discover($host)['share']; + } +} diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php new file mode 100644 index 00000000000..d014a6219a3 --- /dev/null +++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php @@ -0,0 +1,632 @@ + + * @author Roeland Jago Douma + * @author Thomas Müller + * + * @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 + * + */ + +namespace OCA\FederatedFileSharing; + +use OC\Share20\Share; +use OCP\Files\IRootFolder; +use OCP\IAppConfig; +use OCP\IConfig; +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; + + /** @var IConfig */ + private $config; + + /** + * DefaultShareProvider constructor. + * + * @param IDBConnection $connection + * @param AddressHandler $addressHandler + * @param Notifications $notifications + * @param TokenHandler $tokenHandler + * @param IL10N $l10n + * @param ILogger $logger + * @param IRootFolder $rootFolder + * @param IConfig $config + */ + public function __construct( + IDBConnection $connection, + AddressHandler $addressHandler, + Notifications $notifications, + TokenHandler $tokenHandler, + IL10N $l10n, + ILogger $logger, + IRootFolder $rootFolder, + IConfig $config + ) { + $this->dbConnection = $connection; + $this->addressHandler = $addressHandler; + $this->notifications = $notifications; + $this->tokenHandler = $tokenHandler; + $this->l = $l10n; + $this->logger = $logger; + $this->rootFolder = $rootFolder; + $this->config = $config; + } + + /** + * 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())); + + /* + * Added to fix https://github.com/owncloud/core/issues/22215 + * Can be removed once we get rid of ajax/share.php + */ + $qb->setValue('file_target', $qb->createNamedParameter('')); + + $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())) + ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) + ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) + ->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]; + } + + /** + * A user is deleted from the system + * So clean up the relevant shares. + * + * @param string $uid + * @param int $shareType + */ + public function userDeleted($uid, $shareType) { + //TODO: probabaly a good idea to send unshare info to remote servers + + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))) + ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) + ->execute(); + } + + /** + * This provider does not handle groups + * + * @param string $gid + */ + public function groupDeleted($gid) { + // We don't handle groups here + return; + } + + /** + * This provider does not handle groups + * + * @param string $uid + * @param string $gid + */ + public function userDeletedFromGroup($uid, $gid) { + // We don't handle groups here + return; + } + + /** + * check if users from other ownCloud instances are allowed to mount public links share by this instance + * + * @return bool + */ + public function isOutgoingServer2serverShareEnabled() { + $result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes'); + return ($result === 'yes') ? true : false; + } + + /** + * check if users are allowed to mount public links from other ownClouds + * + * @return bool + */ + public function isIncomingServer2serverShareEnabled() { + $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes'); + return ($result === 'yes') ? true : false; + } +} diff --git a/apps/federatedfilesharing/lib/Notifications.php b/apps/federatedfilesharing/lib/Notifications.php new file mode 100644 index 00000000000..9cdc7760361 --- /dev/null +++ b/apps/federatedfilesharing/lib/Notifications.php @@ -0,0 +1,184 @@ + + * @author Lukas Reschke + * + * @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 + * + */ + + +namespace OCA\FederatedFileSharing; + +use OCP\BackgroundJob\IJobList; +use OCP\Http\Client\IClientService; + +class Notifications { + const RESPONSE_FORMAT = 'json'; // default response format for ocs calls + + /** @var AddressHandler */ + private $addressHandler; + + /** @var IClientService */ + private $httpClientService; + + /** @var DiscoveryManager */ + private $discoveryManager; + + /** @var IJobList */ + private $jobList; + + /** + * @param AddressHandler $addressHandler + * @param IClientService $httpClientService + * @param DiscoveryManager $discoveryManager + * @param IJobList $jobList + */ + public function __construct( + AddressHandler $addressHandler, + IClientService $httpClientService, + DiscoveryManager $discoveryManager, + IJobList $jobList + ) { + $this->addressHandler = $addressHandler; + $this->httpClientService = $httpClientService; + $this->discoveryManager = $discoveryManager; + $this->jobList = $jobList; + } + + /** + * 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; + $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->tryHttpPostToShareEndpoint($url, '', $fields); + $status = json_decode($result['result'], true); + + if ($result['success'] && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200)) { + \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 + * @param int $try how often did we already tried to send the un-share request + * @return bool + */ + public function sendRemoteUnShare($remote, $id, $token, $try = 0) { + $url = rtrim($remote, '/'); + $fields = array('token' => $token, 'format' => 'json'); + $url = $this->addressHandler->removeProtocolFromUrl($url); + $result = $this->tryHttpPostToShareEndpoint($url, '/'.$id.'/unshare', $fields); + $status = json_decode($result['result'], true); + + if ($result['success'] && + ($status['ocs']['meta']['statuscode'] === 100 || + $status['ocs']['meta']['statuscode'] === 200 + ) + ) { + return true; + } elseif ($try === 0) { + // only add new job on first try + $this->jobList->add('OCA\FederatedFileSharing\BackgroundJob\UnShare', + [ + 'remote' => $remote, + 'id' => $id, + 'token' => $token, + 'try' => $try, + 'lastRun' => $this->getTimestamp() + ] + ); + } + + return false; + } + + /** + * return current timestamp + * + * @return int + */ + protected function getTimestamp() { + return time(); + } + + /** + * try http post first with https and then with http as a fallback + * + * @param string $remoteDomain + * @param string $urlSuffix + * @param array $fields post parameters + * @return array + */ + protected function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields) { + $client = $this->httpClientService->newClient(); + $protocol = 'https://'; + $result = [ + 'success' => false, + 'result' => '', + ]; + $try = 0; + + while ($result['success'] === false && $try < 2) { + $endpoint = $this->discoveryManager->getShareEndpoint($protocol . $remoteDomain); + try { + $response = $client->post($protocol . $remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [ + '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 @@ + + * + * @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 + * + */ + + +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/lib/addresshandler.php b/apps/federatedfilesharing/lib/addresshandler.php deleted file mode 100644 index 92768f11b95..00000000000 --- a/apps/federatedfilesharing/lib/addresshandler.php +++ /dev/null @@ -1,184 +0,0 @@ - - * - * @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 - * - */ - -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/discoverymanager.php b/apps/federatedfilesharing/lib/discoverymanager.php deleted file mode 100644 index 51ea71195fa..00000000000 --- a/apps/federatedfilesharing/lib/discoverymanager.php +++ /dev/null @@ -1,136 +0,0 @@ - - * - * @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 - * - */ - -namespace OCA\FederatedFileSharing; - -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ConnectException; -use OCP\Http\Client\IClient; -use OCP\Http\Client\IClientService; -use OCP\ICache; -use OCP\ICacheFactory; - -/** - * Class DiscoveryManager handles the discovery of endpoints used by Federated - * Cloud Sharing. - * - * @package OCA\FederatedFileSharing - */ -class DiscoveryManager { - /** @var ICache */ - private $cache; - /** @var IClient */ - private $client; - - /** - * @param ICacheFactory $cacheFactory - * @param IClientService $clientService - */ - public function __construct(ICacheFactory $cacheFactory, - IClientService $clientService) { - $this->cache = $cacheFactory->create('ocs-discovery'); - $this->client = $clientService->newClient(); - } - - /** - * Returns whether the specified URL includes only safe characters, if not - * returns false - * - * @param string $url - * @return bool - */ - private function isSafeUrl($url) { - return (bool)preg_match('/^[\/\.A-Za-z0-9]+$/', $url); - } - - /** - * Discover the actual data and do some naive caching to ensure that the data - * is not requested multiple times. - * - * If no valid discovery data is found the ownCloud defaults are returned. - * - * @param string $remote - * @return array - */ - private function discover($remote) { - // Check if something is in the cache - if($cacheData = $this->cache->get($remote)) { - return json_decode($cacheData, true); - } - - // Default response body - $discoveredServices = [ - 'webdav' => '/public.php/webdav', - 'share' => '/ocs/v1.php/cloud/shares', - ]; - - // Read the data from the response body - try { - $response = $this->client->get($remote . '/ocs-provider/'); - if($response->getStatusCode() === 200) { - $decodedService = json_decode($response->getBody(), true); - if(is_array($decodedService)) { - $endpoints = [ - 'webdav', - 'share', - ]; - - foreach($endpoints as $endpoint) { - if(isset($decodedService['services']['FEDERATED_SHARING']['endpoints'][$endpoint])) { - $endpointUrl = (string)$decodedService['services']['FEDERATED_SHARING']['endpoints'][$endpoint]; - if($this->isSafeUrl($endpointUrl)) { - $discoveredServices[$endpoint] = $endpointUrl; - } - } - } - } - } - } catch (ClientException $e) { - // Don't throw any exception since exceptions are handled before - } catch (ConnectException $e) { - // Don't throw any exception since exceptions are handled before - } - - // Write into cache - $this->cache->set($remote, json_encode($discoveredServices)); - return $discoveredServices; - } - - /** - * Return the public WebDAV endpoint used by the specified remote - * - * @param string $host - * @return string - */ - public function getWebDavEndpoint($host) { - return $this->discover($host)['webdav']; - } - - /** - * Return the sharing endpoint used by the specified remote - * - * @param string $host - * @return string - */ - public function getShareEndpoint($host) { - return $this->discover($host)['share']; - } -} diff --git a/apps/federatedfilesharing/lib/federatedshareprovider.php b/apps/federatedfilesharing/lib/federatedshareprovider.php deleted file mode 100644 index d014a6219a3..00000000000 --- a/apps/federatedfilesharing/lib/federatedshareprovider.php +++ /dev/null @@ -1,632 +0,0 @@ - - * @author Roeland Jago Douma - * @author Thomas Müller - * - * @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 - * - */ - -namespace OCA\FederatedFileSharing; - -use OC\Share20\Share; -use OCP\Files\IRootFolder; -use OCP\IAppConfig; -use OCP\IConfig; -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; - - /** @var IConfig */ - private $config; - - /** - * DefaultShareProvider constructor. - * - * @param IDBConnection $connection - * @param AddressHandler $addressHandler - * @param Notifications $notifications - * @param TokenHandler $tokenHandler - * @param IL10N $l10n - * @param ILogger $logger - * @param IRootFolder $rootFolder - * @param IConfig $config - */ - public function __construct( - IDBConnection $connection, - AddressHandler $addressHandler, - Notifications $notifications, - TokenHandler $tokenHandler, - IL10N $l10n, - ILogger $logger, - IRootFolder $rootFolder, - IConfig $config - ) { - $this->dbConnection = $connection; - $this->addressHandler = $addressHandler; - $this->notifications = $notifications; - $this->tokenHandler = $tokenHandler; - $this->l = $l10n; - $this->logger = $logger; - $this->rootFolder = $rootFolder; - $this->config = $config; - } - - /** - * 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())); - - /* - * Added to fix https://github.com/owncloud/core/issues/22215 - * Can be removed once we get rid of ajax/share.php - */ - $qb->setValue('file_target', $qb->createNamedParameter('')); - - $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())) - ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) - ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) - ->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]; - } - - /** - * A user is deleted from the system - * So clean up the relevant shares. - * - * @param string $uid - * @param int $shareType - */ - public function userDeleted($uid, $shareType) { - //TODO: probabaly a good idea to send unshare info to remote servers - - $qb = $this->dbConnection->getQueryBuilder(); - - $qb->delete('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))) - ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) - ->execute(); - } - - /** - * This provider does not handle groups - * - * @param string $gid - */ - public function groupDeleted($gid) { - // We don't handle groups here - return; - } - - /** - * This provider does not handle groups - * - * @param string $uid - * @param string $gid - */ - public function userDeletedFromGroup($uid, $gid) { - // We don't handle groups here - return; - } - - /** - * check if users from other ownCloud instances are allowed to mount public links share by this instance - * - * @return bool - */ - public function isOutgoingServer2serverShareEnabled() { - $result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes'); - return ($result === 'yes') ? true : false; - } - - /** - * check if users are allowed to mount public links from other ownClouds - * - * @return bool - */ - public function isIncomingServer2serverShareEnabled() { - $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes'); - return ($result === 'yes') ? true : false; - } -} diff --git a/apps/federatedfilesharing/lib/notifications.php b/apps/federatedfilesharing/lib/notifications.php deleted file mode 100644 index 9cdc7760361..00000000000 --- a/apps/federatedfilesharing/lib/notifications.php +++ /dev/null @@ -1,184 +0,0 @@ - - * @author Lukas Reschke - * - * @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 - * - */ - - -namespace OCA\FederatedFileSharing; - -use OCP\BackgroundJob\IJobList; -use OCP\Http\Client\IClientService; - -class Notifications { - const RESPONSE_FORMAT = 'json'; // default response format for ocs calls - - /** @var AddressHandler */ - private $addressHandler; - - /** @var IClientService */ - private $httpClientService; - - /** @var DiscoveryManager */ - private $discoveryManager; - - /** @var IJobList */ - private $jobList; - - /** - * @param AddressHandler $addressHandler - * @param IClientService $httpClientService - * @param DiscoveryManager $discoveryManager - * @param IJobList $jobList - */ - public function __construct( - AddressHandler $addressHandler, - IClientService $httpClientService, - DiscoveryManager $discoveryManager, - IJobList $jobList - ) { - $this->addressHandler = $addressHandler; - $this->httpClientService = $httpClientService; - $this->discoveryManager = $discoveryManager; - $this->jobList = $jobList; - } - - /** - * 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; - $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->tryHttpPostToShareEndpoint($url, '', $fields); - $status = json_decode($result['result'], true); - - if ($result['success'] && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200)) { - \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 - * @param int $try how often did we already tried to send the un-share request - * @return bool - */ - public function sendRemoteUnShare($remote, $id, $token, $try = 0) { - $url = rtrim($remote, '/'); - $fields = array('token' => $token, 'format' => 'json'); - $url = $this->addressHandler->removeProtocolFromUrl($url); - $result = $this->tryHttpPostToShareEndpoint($url, '/'.$id.'/unshare', $fields); - $status = json_decode($result['result'], true); - - if ($result['success'] && - ($status['ocs']['meta']['statuscode'] === 100 || - $status['ocs']['meta']['statuscode'] === 200 - ) - ) { - return true; - } elseif ($try === 0) { - // only add new job on first try - $this->jobList->add('OCA\FederatedFileSharing\BackgroundJob\UnShare', - [ - 'remote' => $remote, - 'id' => $id, - 'token' => $token, - 'try' => $try, - 'lastRun' => $this->getTimestamp() - ] - ); - } - - return false; - } - - /** - * return current timestamp - * - * @return int - */ - protected function getTimestamp() { - return time(); - } - - /** - * try http post first with https and then with http as a fallback - * - * @param string $remoteDomain - * @param string $urlSuffix - * @param array $fields post parameters - * @return array - */ - protected function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields) { - $client = $this->httpClientService->newClient(); - $protocol = 'https://'; - $result = [ - 'success' => false, - 'result' => '', - ]; - $try = 0; - - while ($result['success'] === false && $try < 2) { - $endpoint = $this->discoveryManager->getShareEndpoint($protocol . $remoteDomain); - try { - $response = $client->post($protocol . $remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [ - '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 deleted file mode 100644 index ec5f73127d6..00000000000 --- a/apps/federatedfilesharing/lib/tokenhandler.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * @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 - * - */ - - -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; - } - -} -- cgit v1.2.3