diff options
Diffstat (limited to 'apps/files_sharing/lib/External/Storage.php')
-rw-r--r-- | apps/files_sharing/lib/External/Storage.php | 317 |
1 files changed, 159 insertions, 158 deletions
diff --git a/apps/files_sharing/lib/External/Storage.php b/apps/files_sharing/lib/External/Storage.php index 339770c8601..a9781b91a6c 100644 --- a/apps/files_sharing/lib/External/Storage.php +++ b/apps/files_sharing/lib/External/Storage.php @@ -1,35 +1,11 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_Sharing\External; use GuzzleHttp\Exception\ClientException; @@ -37,66 +13,92 @@ use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use OC\Files\Storage\DAV; use OC\ForbiddenException; +use OC\Share\Share; +use OCA\Files_Sharing\External\Manager as ExternalShareManager; use OCA\Files_Sharing\ISharedStorage; use OCP\AppFramework\Http; use OCP\Constants; use OCP\Federation\ICloudId; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\IScanner; +use OCP\Files\Cache\IWatcher; use OCP\Files\NotFoundException; use OCP\Files\Storage\IDisableEncryptionStorage; +use OCP\Files\Storage\IReliableEtagStorage; +use OCP\Files\Storage\IStorage; use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; - -class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { - /** @var ICloudId */ - private $cloudId; - /** @var string */ - private $mountPoint; - /** @var string */ - private $token; - /** @var \OCP\ICacheFactory */ - private $memcacheFactory; - /** @var \OCP\Http\Client\IClientService */ - private $httpClient; - /** @var bool */ - private $updateChecked = false; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\LocalServerException; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\OCM\Exceptions\OCMArgumentException; +use OCP\OCM\Exceptions\OCMProviderException; +use OCP\OCM\IOCMDiscoveryService; +use OCP\Server; +use OCP\Util; +use Psr\Log\LoggerInterface; + +class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, IReliableEtagStorage { + private ICloudId $cloudId; + private string $mountPoint; + private string $token; + private ICacheFactory $memcacheFactory; + private IClientService $httpClient; + private bool $updateChecked = false; + private ExternalShareManager $manager; + private IConfig $config; /** - * @var \OCA\Files_Sharing\External\Manager + * @param array{HttpClientService: IClientService, manager: ExternalShareManager, cloudId: ICloudId, mountpoint: string, token: string, password: ?string}|array $options */ - private $manager; - public function __construct($options) { - $this->memcacheFactory = \OC::$server->getMemCacheFactory(); + $this->memcacheFactory = Server::get(ICacheFactory::class); $this->httpClient = $options['HttpClientService']; - $this->manager = $options['manager']; $this->cloudId = $options['cloudId']; - $discoveryService = \OC::$server->query(\OCP\OCS\IDiscoveryService::class); + $this->logger = Server::get(LoggerInterface::class); + $discoveryService = Server::get(IOCMDiscoveryService::class); + $this->config = Server::get(IConfig::class); - list($protocol, $remote) = explode('://', $this->cloudId->getRemote()); - if (strpos($remote, '/')) { - list($host, $root) = explode('/', $remote, 2); - } else { - $host = $remote; - $root = ''; + // use default path to webdav if not found on discovery + try { + $ocmProvider = $discoveryService->discover($this->cloudId->getRemote()); + $webDavEndpoint = $ocmProvider->extractProtocolEntry('file', 'webdav'); + $remote = $ocmProvider->getEndPoint(); + } catch (OCMProviderException|OCMArgumentException $e) { + $this->logger->notice('exception while retrieving webdav endpoint', ['exception' => $e]); + $webDavEndpoint = '/public.php/webdav'; + $remote = $this->cloudId->getRemote(); } - $secure = $protocol === 'https'; - $federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING'); - $webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav'; - $root = rtrim($root, '/') . $webDavEndpoint; + + $host = parse_url($remote, PHP_URL_HOST); + $port = parse_url($remote, PHP_URL_PORT); + $host .= ($port === null) ? '' : ':' . $port; // we add port if available + + // in case remote NC is on a sub folder and using deprecated ocm provider + $tmpPath = rtrim(parse_url($this->cloudId->getRemote(), PHP_URL_PATH) ?? '', '/'); + if (!str_starts_with($webDavEndpoint, $tmpPath)) { + $webDavEndpoint = $tmpPath . $webDavEndpoint; + } + $this->mountPoint = $options['mountpoint']; $this->token = $options['token']; - parent::__construct([ - 'secure' => $secure, - 'host' => $host, - 'root' => $root, - 'user' => $options['token'], - 'password' => (string)$options['password'] - ]); + parent::__construct( + [ + 'secure' => ((parse_url($remote, PHP_URL_SCHEME) ?? 'https') === 'https'), + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false), + 'host' => $host, + 'root' => $webDavEndpoint, + 'user' => $options['token'], + 'authType' => \Sabre\DAV\Client::AUTH_BASIC, + 'password' => (string)$options['password'] + ] + ); } - public function getWatcher($path = '', $storage = null) { + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { if (!$storage) { $storage = $this; } @@ -107,66 +109,49 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { return $this->watcher; } - public function getRemoteUser() { + public function getRemoteUser(): string { return $this->cloudId->getUser(); } - public function getRemote() { + public function getRemote(): string { return $this->cloudId->getRemote(); } - public function getMountPoint() { + public function getMountPoint(): string { return $this->mountPoint; } - public function getToken() { + public function getToken(): string { return $this->token; } - public function getPassword() { + public function getPassword(): ?string { return $this->password; } - /** - * @brief get id of the mount point - * @return string - */ - public function getId() { + public function getId(): string { return 'shared::' . md5($this->token . '@' . $this->getRemote()); } - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): ICache { if (is_null($this->cache)) { $this->cache = new Cache($this, $this->cloudId); } return $this->cache; } - /** - * @param string $path - * @param \OC\Files\Storage\Storage $storage - * @return \OCA\Files_Sharing\External\Scanner - */ - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } if (!isset($this->scanner)) { $this->scanner = new Scanner($storage); } + /** @var Scanner */ return $this->scanner; } - /** - * check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @throws \OCP\Files\StorageNotAvailableException - * @throws \OCP\Files\StorageInvalidException - * @return bool - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { // since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage // because of that we only do one check for the entire storage per request if ($this->updateChecked) { @@ -186,7 +171,7 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { } } - public function test() { + public function test(): bool { try { return parent::test(); } catch (StorageInvalidException $e) { @@ -204,13 +189,13 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { * Check whether this storage is permanently or temporarily * unavailable * - * @throws \OCP\Files\StorageNotAvailableException - * @throws \OCP\Files\StorageInvalidException + * @throws StorageNotAvailableException + * @throws StorageInvalidException */ public function checkStorageAvailability() { // see if we can find out why the share is unavailable try { - $this->getShareInfo(); + $this->getShareInfo(0); } catch (NotFoundException $e) { // a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote if ($this->testRemote()) { @@ -219,26 +204,24 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { // we remove the invalid storage $this->manager->removeShare($this->mountPoint); $this->manager->getMountManager()->removeMount($this->mountPoint); - throw new StorageInvalidException(); + throw new StorageInvalidException('Remote share not found', 0, $e); } else { // Nextcloud instance is gone, likely to be a temporary server configuration error - throw new StorageNotAvailableException(); + throw new StorageNotAvailableException('No nextcloud instance found at remote', 0, $e); } } catch (ForbiddenException $e) { // auth error, remove share for now (provide a dialog in the future) $this->manager->removeShare($this->mountPoint); $this->manager->getMountManager()->removeMount($this->mountPoint); - throw new StorageInvalidException(); + throw new StorageInvalidException('Auth error when getting remote share'); } catch (\GuzzleHttp\Exception\ConnectException $e) { - throw new StorageNotAvailableException(); + throw new StorageNotAvailableException('Failed to connect to remote instance', 0, $e); } catch (\GuzzleHttp\Exception\RequestException $e) { - throw new StorageNotAvailableException(); - } catch (\Exception $e) { - throw $e; + throw new StorageNotAvailableException('Error while sending request to remote instance', 0, $e); } } - public function file_exists($path) { + public function file_exists(string $path): bool { if ($path === '') { return true; } else { @@ -247,44 +230,35 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { } /** - * check if the configured remote is a valid federated share provider + * Check if the configured remote is a valid federated share provider * * @return bool */ - protected function testRemote() { + protected function testRemote(): bool { try { - return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php') - || $this->testRemoteUrl($this->getRemote() . '/ocs-provider/') - || $this->testRemoteUrl($this->getRemote() . '/status.php'); + return $this->testRemoteUrl($this->getRemote() . '/ocm-provider/index.php') + || $this->testRemoteUrl($this->getRemote() . '/ocm-provider/') + || $this->testRemoteUrl($this->getRemote() . '/status.php'); } catch (\Exception $e) { return false; } } - /** - * @param string $url - * @return bool - */ - private function testRemoteUrl($url) { + private function testRemoteUrl(string $url): bool { $cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url'); - if ($cache->hasKey($url)) { - return (bool)$cache->get($url); + $cached = $cache->get($url); + if ($cached !== null) { + return (bool)$cached; } $client = $this->httpClient->newClient(); try { - $result = $client->get($url, [ - 'timeout' => 10, - 'connect_timeout' => 10, - ])->getBody(); + $result = $client->get($url, $this->getDefaultRequestOptions())->getBody(); $data = json_decode($result); $returnValue = (is_object($data) && !empty($data->version)); - } catch (ConnectException $e) { - $returnValue = false; - } catch (ClientException $e) { - $returnValue = false; - } catch (RequestException $e) { + } catch (ConnectException|ClientException|RequestException $e) { $returnValue = false; + $this->logger->warning('Failed to test remote URL', ['exception' => $e]); } $cache->set($url, $returnValue, 60 * 60 * 24); @@ -292,12 +266,12 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { } /** - * Whether the remote is an ownCloud/Nextcloud, used since some sharing features are not - * standardized. Let's use this to detect whether to use it. + * Check whether the remote is an ownCloud/Nextcloud. This is needed since some sharing + * features are not standardized. * - * @return bool + * @throws LocalServerException */ - public function remoteIsOwnCloud() { + public function remoteIsOwnCloud(): bool { if (defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) { return false; } @@ -310,27 +284,33 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { * @throws NotFoundException * @throws \Exception */ - public function getShareInfo() { + public function getShareInfo(int $depth = -1) { $remote = $this->getRemote(); $token = $this->getToken(); $password = $this->getPassword(); - // If remote is not an ownCloud do not try to get any share info - if (!$this->remoteIsOwnCloud()) { - return ['status' => 'unsupported']; + try { + // If remote is not an ownCloud do not try to get any share info + if (!$this->remoteIsOwnCloud()) { + return ['status' => 'unsupported']; + } + } catch (LocalServerException $e) { + // throw this to be on the safe side: the share will still be visible + // in the UI in case the failure is intermittent, and the user will + // be able to decide whether to remove it if it's really gone + throw new StorageNotAvailableException(); } $url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token; // TODO: DI - $client = \OC::$server->getHTTPClientService()->newClient(); + $client = Server::get(IClientService::class)->newClient(); try { - $response = $client->post($url, [ - 'body' => ['password' => $password], - 'timeout' => 10, - 'connect_timeout' => 10, - ]); + $response = $client->post($url, array_merge($this->getDefaultRequestOptions(), [ + 'body' => ['password' => $password, 'depth' => $depth], + ])); } catch (\GuzzleHttp\Exception\RequestException $e) { + $this->logger->warning('Failed to fetch share info', ['exception' => $e]); if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) { throw new ForbiddenException(); } @@ -346,25 +326,34 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { return json_decode($response->getBody(), true); } - public function getOwner($path) { + public function getOwner(string $path): string|false { return $this->cloudId->getDisplayId(); } - public function isSharable($path) { - if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) { + public function isSharable(string $path): bool { + if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) { return false; } - return ($this->getPermissions($path) & Constants::PERMISSION_SHARE); + return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); } - public function getPermissions($path) { + public function getPermissions(string $path): int { $response = $this->propfind($path); + if ($response === false) { + return 0; + } + + $ocsPermissions = $response['{http://open-collaboration-services.org/ns}share-permissions'] ?? null; + $ocmPermissions = $response['{http://open-cloud-mesh.org/ns}share-permissions'] ?? null; + $ocPermissions = $response['{http://owncloud.org/ns}permissions'] ?? null; // old federated sharing permissions - if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) { - $permissions = $response['{http://open-collaboration-services.org/ns}share-permissions']; - } elseif (isset($response['{http://open-cloud-mesh.org/ns}share-permissions'])) { + if ($ocsPermissions !== null) { + $permissions = (int)$ocsPermissions; + } elseif ($ocmPermissions !== null) { // permissions provided by the OCM API - $permissions = $this->ocmPermissions2ncPermissions($response['{http://open-collaboration-services.org/ns}share-permissions'], $path); + $permissions = $this->ocmPermissions2ncPermissions($ocmPermissions, $path); + } elseif ($ocPermissions !== null) { + return $this->parsePermissions($ocPermissions); } else { // use default permission if remote server doesn't provide the share permissions $permissions = $this->getDefaultPermissions($path); @@ -373,18 +362,18 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { return $permissions; } - public function needsPartFile() { + public function needsPartFile(): bool { return false; } /** - * translate OCM Permissions to Nextcloud permissions + * Translate OCM Permissions to Nextcloud permissions * * @param string $ocmPermissions json encoded OCM permissions * @param string $path path to file * @return int */ - protected function ocmPermissions2ncPermissions($ocmPermissions, $path) { + protected function ocmPermissions2ncPermissions(string $ocmPermissions, string $path): int { try { $ocmPermissions = json_decode($ocmPermissions); $ncPermissions = 0; @@ -411,12 +400,9 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { } /** - * calculate default permissions in case no permissions are provided - * - * @param $path - * @return int + * Calculate the default permissions in case no permissions are provided */ - protected function getDefaultPermissions($path) { + protected function getDefaultPermissions(string $path): int { if ($this->is_dir($path)) { $permissions = Constants::PERMISSION_ALL; } else { @@ -425,4 +411,19 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage { return $permissions; } + + public function free_space(string $path): int|float|false { + return parent::free_space(''); + } + + private function getDefaultRequestOptions(): array { + $options = [ + 'timeout' => 10, + 'connect_timeout' => 10, + ]; + if ($this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates')) { + $options['verify'] = false; + } + return $options; + } } |