diff options
Diffstat (limited to 'apps/files_external/lib/Lib')
64 files changed, 3387 insertions, 2644 deletions
diff --git a/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php b/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php index dbf14b76da8..c86c88a13d7 100644 --- a/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php +++ b/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php @@ -1,37 +1,21 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\AmazonS3; -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; /** * Amazon S3 access key authentication */ class AccessKey extends AuthMechanism { - - const SCHEME_AMAZONS3_ACCESSKEY = 'amazons3_accesskey'; + public const SCHEME_AMAZONS3_ACCESSKEY = 'amazons3_accesskey'; public function __construct(IL10N $l) { $this @@ -39,10 +23,9 @@ class AccessKey extends AuthMechanism { ->setScheme(self::SCHEME_AMAZONS3_ACCESSKEY) ->setText($l->t('Access key')) ->addParameters([ - (new DefinitionParameter('key', $l->t('Access key'))), + new DefinitionParameter('key', $l->t('Access key')), (new DefinitionParameter('secret', $l->t('Secret key'))) ->setType(DefinitionParameter::VALUE_PASSWORD), ]); } - } diff --git a/apps/files_external/lib/Lib/Auth/AuthMechanism.php b/apps/files_external/lib/Lib/Auth/AuthMechanism.php index 28d1dde4375..7b0544100fb 100644 --- a/apps/files_external/lib/Lib/Auth/AuthMechanism.php +++ b/apps/files_external/lib/Lib/Auth/AuthMechanism.php @@ -1,34 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\VisibilityTrait; -use \OCA\Files_External\Lib\IdentifierTrait; -use \OCA\Files_External\Lib\FrontendDefinitionTrait; -use \OCA\Files_External\Lib\StorageModifierTrait; +use OCA\Files_External\Lib\FrontendDefinitionTrait; +use OCA\Files_External\Lib\IdentifierTrait; +use OCA\Files_External\Lib\IFrontendDefinition; +use OCA\Files_External\Lib\IIdentifier; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Lib\StorageModifierTrait; +use OCA\Files_External\Lib\VisibilityTrait; /** * Authentication mechanism @@ -37,7 +22,7 @@ use \OCA\Files_External\Lib\StorageModifierTrait; * such as \OCP\IDB for database operations. This allows an authentication * mechanism to perform advanced operations based on provided information. * - * An authenication scheme defines the parameter interface, common to the + * An authentication scheme defines the parameter interface, common to the * storage implementation, the backend and the authentication mechanism. * A storage implementation expects parameters according to the authentication * scheme, which are provided from the authentication mechanism. @@ -50,16 +35,15 @@ use \OCA\Files_External\Lib\StorageModifierTrait; * - StorageModifierTrait * Object can affect storage mounting */ -class AuthMechanism implements \JsonSerializable { - +class AuthMechanism implements \JsonSerializable, IIdentifier, IFrontendDefinition { /** Standard authentication schemes */ - const SCHEME_NULL = 'null'; - const SCHEME_BUILTIN = 'builtin'; - const SCHEME_PASSWORD = 'password'; - const SCHEME_OAUTH1 = 'oauth1'; - const SCHEME_OAUTH2 = 'oauth2'; - const SCHEME_PUBLICKEY = 'publickey'; - const SCHEME_OPENSTACK = 'openstack'; + public const SCHEME_NULL = 'null'; + public const SCHEME_BUILTIN = 'builtin'; + public const SCHEME_PASSWORD = 'password'; + public const SCHEME_OAUTH2 = 'oauth2'; + public const SCHEME_PUBLICKEY = 'publickey'; + public const SCHEME_OPENSTACK = 'openstack'; + public const SCHEME_SMB = 'smb'; use VisibilityTrait; use FrontendDefinitionTrait; @@ -90,10 +74,8 @@ class AuthMechanism implements \JsonSerializable { /** * Serialize into JSON for client-side JS - * - * @return array */ - public function jsonSerialize() { + public function jsonSerialize(): array { $data = $this->jsonSerializeDefinition(); $data += $this->jsonSerializeIdentifier(); @@ -118,5 +100,4 @@ class AuthMechanism implements \JsonSerializable { return $this->validateStorageDefinition($storage); } - } diff --git a/apps/files_external/lib/Lib/Auth/Builtin.php b/apps/files_external/lib/Lib/Auth/Builtin.php index d39f459c660..8e12a6daca6 100644 --- a/apps/files_external/lib/Lib/Auth/Builtin.php +++ b/apps/files_external/lib/Lib/Auth/Builtin.php @@ -1,34 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth; -use \OCP\IL10N; +use OCP\IL10N; /** * Builtin authentication mechanism, for legacy backends */ class Builtin extends AuthMechanism { - public function __construct(IL10N $l) { $this ->setIdentifier('builtin::builtin') @@ -36,5 +20,4 @@ class Builtin extends AuthMechanism { ->setText($l->t('Builtin')) ; } - } diff --git a/apps/files_external/lib/Lib/Auth/IUserProvided.php b/apps/files_external/lib/Lib/Auth/IUserProvided.php index ed4abaf6573..2350d7f6db4 100644 --- a/apps/files_external/lib/Lib/Auth/IUserProvided.php +++ b/apps/files_external/lib/Lib/Auth/IUserProvided.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin Appelman <robin@icewind.nl> - * - * @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 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth; use OCP\IUser; diff --git a/apps/files_external/lib/Lib/Auth/InvalidAuth.php b/apps/files_external/lib/Lib/Auth/InvalidAuth.php index e22a75adc17..2af24f1ea07 100644 --- a/apps/files_external/lib/Lib/Auth/InvalidAuth.php +++ b/apps/files_external/lib/Lib/Auth/InvalidAuth.php @@ -1,25 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @author Vincent Petry <pvince81@owncloud.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth; /** @@ -41,5 +26,4 @@ class InvalidAuth extends AuthMechanism { ->setText('Unknown auth mechanism backend ' . $invalidId) ; } - } diff --git a/apps/files_external/lib/Lib/Auth/NullMechanism.php b/apps/files_external/lib/Lib/Auth/NullMechanism.php index f0d741e0124..8e2e5b656b2 100644 --- a/apps/files_external/lib/Lib/Auth/NullMechanism.php +++ b/apps/files_external/lib/Lib/Auth/NullMechanism.php @@ -1,34 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth; -use \OCP\IL10N; +use OCP\IL10N; /** * Null authentication mechanism */ class NullMechanism extends AuthMechanism { - public function __construct(IL10N $l) { $this ->setIdentifier('null::null') @@ -36,5 +20,4 @@ class NullMechanism extends AuthMechanism { ->setText($l->t('None')) ; } - } diff --git a/apps/files_external/lib/Lib/Auth/OAuth1/OAuth1.php b/apps/files_external/lib/Lib/Auth/OAuth1/OAuth1.php deleted file mode 100644 index 2602d5de41b..00000000000 --- a/apps/files_external/lib/Lib/Auth/OAuth1/OAuth1.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\OAuth1; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; - -/** - * OAuth1 authentication - */ -class OAuth1 extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('oauth1::oauth1') - ->setScheme(self::SCHEME_OAUTH1) - ->setText($l->t('OAuth1')) - ->addParameters([ - (new DefinitionParameter('configured', 'configured')) - ->setType(DefinitionParameter::VALUE_HIDDEN), - (new DefinitionParameter('app_key', $l->t('App key'))), - (new DefinitionParameter('app_secret', $l->t('App secret'))) - ->setType(DefinitionParameter::VALUE_PASSWORD), - (new DefinitionParameter('token', 'token')) - ->setType(DefinitionParameter::VALUE_HIDDEN), - (new DefinitionParameter('token_secret', 'token_secret')) - ->setType(DefinitionParameter::VALUE_HIDDEN), - ]) - ->addCustomJs('oauth1') - ; - } - -} diff --git a/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php b/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php index 325efb0e1a0..beaf73c2344 100644 --- a/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php +++ b/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php @@ -1,36 +1,20 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\OAuth2; -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; /** * OAuth2 authentication */ class OAuth2 extends AuthMechanism { - public function __construct(IL10N $l) { $this ->setIdentifier('oauth2::oauth2') @@ -38,15 +22,16 @@ class OAuth2 extends AuthMechanism { ->setText($l->t('OAuth2')) ->addParameters([ (new DefinitionParameter('configured', 'configured')) - ->setType(DefinitionParameter::VALUE_HIDDEN), - (new DefinitionParameter('client_id', $l->t('Client ID'))), + ->setType(DefinitionParameter::VALUE_TEXT) + ->setFlag(DefinitionParameter::FLAG_HIDDEN), + new DefinitionParameter('client_id', $l->t('Client ID')), (new DefinitionParameter('client_secret', $l->t('Client secret'))) ->setType(DefinitionParameter::VALUE_PASSWORD), (new DefinitionParameter('token', 'token')) - ->setType(DefinitionParameter::VALUE_HIDDEN), + ->setType(DefinitionParameter::VALUE_PASSWORD) + ->setFlag(DefinitionParameter::FLAG_HIDDEN), ]) ->addCustomJs('oauth2') ; } - } diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStack.php b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStack.php deleted file mode 100644 index 17c2c8bd146..00000000000 --- a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStack.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\OpenStack; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; - -/** - * OpenStack Keystone authentication - */ -class OpenStack extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('openstack::openstack') - ->setScheme(self::SCHEME_OPENSTACK) - ->setText($l->t('OpenStack')) - ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))), - (new DefinitionParameter('password', $l->t('Password'))) - ->setType(DefinitionParameter::VALUE_PASSWORD), - (new DefinitionParameter('tenant', $l->t('Tenant name'))), - (new DefinitionParameter('url', $l->t('Identity endpoint URL'))), - ]) - ; - } - -} diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php new file mode 100644 index 00000000000..3b1c9f123af --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php @@ -0,0 +1,32 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Auth\OpenStack; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +/** + * OpenStack Keystone authentication + */ +class OpenStackV2 extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('openstack::openstack') + ->setScheme(self::SCHEME_OPENSTACK) + ->setText($l->t('OpenStack v2')) + ->addParameters([ + new DefinitionParameter('user', $l->t('Login')), + (new DefinitionParameter('password', $l->t('Password'))) + ->setType(DefinitionParameter::VALUE_PASSWORD), + new DefinitionParameter('tenant', $l->t('Tenant name')), + new DefinitionParameter('url', $l->t('Identity endpoint URL')), + ]) + ; + } +} diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php new file mode 100644 index 00000000000..b5d185fd374 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Auth\OpenStack; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +/** + * OpenStack Keystone authentication + */ +class OpenStackV3 extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('openstack::openstackv3') + ->setScheme(self::SCHEME_OPENSTACK) + ->setText($l->t('OpenStack v3')) + ->addParameters([ + new DefinitionParameter('user', $l->t('Login')), + new DefinitionParameter('domain', $l->t('Domain')), + (new DefinitionParameter('password', $l->t('Password'))) + ->setType(DefinitionParameter::VALUE_PASSWORD), + new DefinitionParameter('tenant', $l->t('Tenant name')), + new DefinitionParameter('url', $l->t('Identity endpoint URL')) + ]) + ; + } +} diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php b/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php index caff8e26ae3..b1d1068e586 100644 --- a/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php +++ b/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php @@ -1,47 +1,30 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\OpenStack; -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; /** * Rackspace authentication */ class Rackspace extends AuthMechanism { - public function __construct(IL10N $l) { $this ->setIdentifier('openstack::rackspace') ->setScheme(self::SCHEME_OPENSTACK) ->setText($l->t('Rackspace')) ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))), + new DefinitionParameter('user', $l->t('Login')), (new DefinitionParameter('key', $l->t('API key'))) ->setType(DefinitionParameter::VALUE_PASSWORD), ]) ; } - } diff --git a/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php b/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php index 3c5798ccee7..916b496b506 100644 --- a/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php +++ b/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php @@ -1,50 +1,31 @@ <?php + /** - * @copyright Copyright (c) 2015, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @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: 2015 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\Password; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\BackendService; use OCP\IL10N; use OCP\IUser; -use OCA\Files_External\Lib\Auth\AuthMechanism; -use OCA\Files_External\Lib\StorageConfig; use OCP\Security\ICredentialsManager; -use OCP\Files\Storage; -use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; /** * Global Username and Password */ class GlobalAuth extends AuthMechanism { + public const CREDENTIALS_IDENTIFIER = 'password::global'; + private const PWD_PLACEHOLDER = '************************'; - const CREDENTIALS_IDENTIFIER = 'password::global'; - - /** @var ICredentialsManager */ - protected $credentialsManager; - - public function __construct(IL10N $l, ICredentialsManager $credentialsManager) { - $this->credentialsManager = $credentialsManager; - + public function __construct( + IL10N $l, + protected ICredentialsManager $credentialsManager, + ) { $this ->setIdentifier('password::global') ->setVisibility(BackendService::VISIBILITY_DEFAULT) @@ -60,18 +41,28 @@ class GlobalAuth extends AuthMechanism { 'password' => '' ]; } else { + $auth['password'] = self::PWD_PLACEHOLDER; return $auth; } } public function saveAuth($uid, $user, $password) { + // Use old password if it has not changed. + if ($password === self::PWD_PLACEHOLDER) { + $auth = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER); + $password = $auth['password']; + } + $this->credentialsManager->store($uid, self::CREDENTIALS_IDENTIFIER, [ 'user' => $user, 'password' => $password ]); } - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { if ($storage->getType() === StorageConfig::MOUNT_TYPE_ADMIN) { $uid = ''; } elseif (is_null($user)) { @@ -86,5 +77,4 @@ class GlobalAuth extends AuthMechanism { $storage->setBackendOption('password', $credentials['password']); } } - } diff --git a/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php b/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php index 80703b52a9a..ce38140b6ee 100644 --- a/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php +++ b/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php @@ -1,93 +1,113 @@ <?php + /** - * @copyright Copyright (c) 2015, ownCloud, Inc. - * - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2015 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\Password; -use \OCP\IL10N; -use \OCP\IUser; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Lib\StorageConfig; -use \OCP\ISession; -use \OCP\Security\ICredentialsManager; -use \OCP\Files\Storage; -use \OCA\Files_External\Lib\SessionStorageWrapper; -use \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Listener\StorePasswordListener; +use OCP\Authentication\Exceptions\CredentialsUnavailableException; +use OCP\Authentication\LoginCredentials\IStore as CredentialsStore; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IL10N; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserBackend; +use OCP\LDAP\ILDAPProviderFactory; +use OCP\Security\ICredentialsManager; +use OCP\User\Events\PasswordUpdatedEvent; +use OCP\User\Events\UserLoggedInEvent; /** * Username and password from login credentials, saved in DB */ class LoginCredentials extends AuthMechanism { + public const CREDENTIALS_IDENTIFIER = 'password::logincredentials/credentials'; - const CREDENTIALS_IDENTIFIER = 'password::logincredentials/credentials'; - - /** @var ISession */ - protected $session; - - /** @var ICredentialsManager */ - protected $credentialsManager; - - public function __construct(IL10N $l, ISession $session, ICredentialsManager $credentialsManager) { - $this->session = $session; - $this->credentialsManager = $credentialsManager; - + public function __construct( + IL10N $l, + protected ISession $session, + protected ICredentialsManager $credentialsManager, + private CredentialsStore $credentialsStore, + IEventDispatcher $eventDispatcher, + private ILDAPProviderFactory $ldapFactory, + ) { $this ->setIdentifier('password::logincredentials') ->setScheme(self::SCHEME_PASSWORD) ->setText($l->t('Log-in credentials, save in database')) ->addParameters([ - ]) - ; + (new DefinitionParameter('password', $l->t('Password'))) + ->setType(DefinitionParameter::VALUE_PASSWORD) + ->setFlag(DefinitionParameter::FLAG_HIDDEN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + ]); - \OCP\Util::connectHook('OC_User', 'post_login', $this, 'authenticate'); + $eventDispatcher->addServiceListener(UserLoggedInEvent::class, StorePasswordListener::class); + $eventDispatcher->addServiceListener(PasswordUpdatedEvent::class, StorePasswordListener::class); } - /** - * Hook listener on post login - * - * @param array $params - */ - public function authenticate(array $params) { - $userId = $params['uid']; - $credentials = [ - 'user' => $this->session->get('loginname'), - 'password' => $params['password'] - ]; - $this->credentialsManager->store($userId, self::CREDENTIALS_IDENTIFIER, $credentials); + private function getCredentials(IUser $user): array { + $credentials = $this->credentialsManager->retrieve($user->getUID(), self::CREDENTIALS_IDENTIFIER); + + if (is_null($credentials)) { + // nothing saved in db, try to get it from the session and save it + try { + $sessionCredentials = $this->credentialsStore->getLoginCredentials(); + + if ($sessionCredentials->getUID() !== $user->getUID()) { + // Can't take the credentials from the session as they are not the same user + throw new CredentialsUnavailableException(); + } + + $credentials = [ + 'user' => $sessionCredentials->getLoginName(), + 'password' => $sessionCredentials->getPassword(), + ]; + + $this->credentialsManager->store($user->getUID(), self::CREDENTIALS_IDENTIFIER, $credentials); + } catch (CredentialsUnavailableException $e) { + throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved'); + } + } + + return $credentials; } - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { if (!isset($user)) { throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved'); } - $uid = $user->getUID(); - $credentials = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER); + $credentials = $this->getCredentials($user); - if (!isset($credentials)) { - throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved'); + $loginKey = $storage->getBackendOption('login_ldap_attr'); + if ($loginKey) { + $backend = $user->getBackend(); + if ($backend instanceof IUserBackend && $backend->getBackendName() === 'LDAP') { + $value = $this->getLdapPropertyForUser($user, $loginKey); + if ($value === null) { + throw new InsufficientDataForMeaningfulAnswerException('Custom ldap attribute not set for user ' . $user->getUID()); + } + $storage->setBackendOption('user', $value); + } else { + throw new InsufficientDataForMeaningfulAnswerException('Custom ldap attribute configured but user ' . $user->getUID() . ' is not an ldap user'); + } + } else { + $storage->setBackendOption('user', $credentials['user']); } - - $storage->setBackendOption('user', $credentials['user']); $storage->setBackendOption('password', $credentials['password']); } + private function getLdapPropertyForUser(IUser $user, string $property): ?string { + return $this->ldapFactory->getLDAPProvider()->getUserAttribute($user->getUID(), $property); + } } diff --git a/apps/files_external/lib/Lib/Auth/Password/Password.php b/apps/files_external/lib/Lib/Auth/Password/Password.php index cf8d6b9f458..d4291148e3e 100644 --- a/apps/files_external/lib/Lib/Auth/Password/Password.php +++ b/apps/files_external/lib/Lib/Auth/Password/Password.php @@ -1,46 +1,29 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\Password; -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; /** * Basic password authentication mechanism */ class Password extends AuthMechanism { - public function __construct(IL10N $l) { $this ->setIdentifier('password::password') ->setScheme(self::SCHEME_PASSWORD) - ->setText($l->t('Username and password')) + ->setText($l->t('Login and password')) ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))), + new DefinitionParameter('user', $l->t('Login')), (new DefinitionParameter('password', $l->t('Password'))) ->setType(DefinitionParameter::VALUE_PASSWORD), ]); } - } diff --git a/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php b/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php index 9f1ad9e4a32..8f161073771 100644 --- a/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php +++ b/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php @@ -1,36 +1,21 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Vincent Petry <pvince81@owncloud.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\Password; use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; use OCA\Files_External\Lib\SessionStorageWrapper; use OCA\Files_External\Lib\StorageConfig; use OCP\Authentication\Exceptions\CredentialsUnavailableException; use OCP\Authentication\LoginCredentials\IStore as CredentialsStore; -use OCP\Files\Storage; +use OCP\Files\Storage\IStorage; +use OCP\Files\StorageAuthException; use OCP\IL10N; use OCP\IUser; @@ -39,31 +24,44 @@ use OCP\IUser; */ class SessionCredentials extends AuthMechanism { - /** @var CredentialsStore */ - private $credentialsStore; - - public function __construct(IL10N $l, CredentialsStore $credentialsStore) { - $this->credentialsStore = $credentialsStore; - + public function __construct( + IL10N $l, + private CredentialsStore $credentialsStore, + ) { $this->setIdentifier('password::sessioncredentials') ->setScheme(self::SCHEME_PASSWORD) ->setText($l->t('Log-in credentials, save in session')) - ->addParameters([]); + ->addParameters([ + (new DefinitionParameter('password', $l->t('Password'))) + ->setType(DefinitionParameter::VALUE_PASSWORD) + ->setFlag(DefinitionParameter::FLAG_HIDDEN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + ]); } - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { try { $credentials = $this->credentialsStore->getLoginCredentials(); } catch (CredentialsUnavailableException $e) { throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved'); } + if ($user === null) { + throw new StorageAuthException('Session unavailable'); + } + + if ($credentials->getUID() !== $user->getUID()) { + throw new StorageAuthException('Session credentials for storage owner not available'); + } + $storage->setBackendOption('user', $credentials->getLoginName()); $storage->setBackendOption('password', $credentials->getPassword()); } - public function wrapStorage(Storage $storage) { + public function wrapStorage(IStorage $storage): IStorage { return new SessionStorageWrapper(['storage' => $storage]); } - } diff --git a/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php b/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php new file mode 100644 index 00000000000..cb7165261ac --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Auth\Password; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCP\IL10N; +use OCP\IUser; +use OCP\Security\ICredentialsManager; + +/** + * User provided Global Username and Password + */ +class UserGlobalAuth extends AuthMechanism { + private const CREDENTIALS_IDENTIFIER = 'password::global'; + + public function __construct( + IL10N $l, + protected ICredentialsManager $credentialsManager, + ) { + $this + ->setIdentifier('password::global::user') + ->setVisibility(BackendService::VISIBILITY_DEFAULT) + ->setScheme(self::SCHEME_PASSWORD) + ->setText($l->t('Global credentials, manually entered')); + } + + public function saveBackendOptions(IUser $user, $id, $backendOptions) { + // backendOptions are set when invoked via Files app + // but they are not set when invoked via ext storage settings + if (!isset($backendOptions['user']) && !isset($backendOptions['password'])) { + return; + } + + if ($backendOptions['password'] === DefinitionParameter::UNMODIFIED_PLACEHOLDER) { + $oldCredentials = $this->credentialsManager->retrieve($user->getUID(), self::CREDENTIALS_IDENTIFIER); + $backendOptions['password'] = $oldCredentials['password']; + } + + // make sure we're not setting any unexpected keys + $credentials = [ + 'user' => $backendOptions['user'], + 'password' => $backendOptions['password'], + ]; + $this->credentialsManager->store($user->getUID(), self::CREDENTIALS_IDENTIFIER, $credentials); + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + if ($user === null) { + throw new InsufficientDataForMeaningfulAnswerException('No credentials saved'); + } + + $uid = $user->getUID(); + $credentials = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER); + + if (is_array($credentials)) { + $storage->setBackendOption('user', $credentials['user']); + $storage->setBackendOption('password', $credentials['password']); + } + } +} diff --git a/apps/files_external/lib/Lib/Auth/Password/UserProvided.php b/apps/files_external/lib/Lib/Auth/Password/UserProvided.php index 7eb2f63e7d3..b158392f6eb 100644 --- a/apps/files_external/lib/Lib/Auth/Password/UserProvided.php +++ b/apps/files_external/lib/Lib/Auth/Password/UserProvided.php @@ -1,58 +1,39 @@ <?php + /** - * @copyright Copyright (c) 2015, ownCloud, Inc. - * - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2015 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\Password; +use OCA\Files_External\Lib\Auth\AuthMechanism; use OCA\Files_External\Lib\Auth\IUserProvided; use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\BackendService; use OCP\IL10N; use OCP\IUser; -use OCA\Files_External\Lib\Auth\AuthMechanism; -use OCA\Files_External\Lib\StorageConfig; use OCP\Security\ICredentialsManager; -use OCP\Files\Storage; -use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; /** * User provided Username and Password */ class UserProvided extends AuthMechanism implements IUserProvided { + public const CREDENTIALS_IDENTIFIER_PREFIX = 'password::userprovided/'; - const CREDENTIALS_IDENTIFIER_PREFIX = 'password::userprovided/'; - - /** @var ICredentialsManager */ - protected $credentialsManager; - - public function __construct(IL10N $l, ICredentialsManager $credentialsManager) { - $this->credentialsManager = $credentialsManager; - + public function __construct( + IL10N $l, + protected ICredentialsManager $credentialsManager, + ) { $this ->setIdentifier('password::userprovided') ->setVisibility(BackendService::VISIBILITY_ADMIN) ->setScheme(self::SCHEME_PASSWORD) - ->setText($l->t('User entered, store in database')) + ->setText($l->t('Manually entered, store in database')) ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))) + (new DefinitionParameter('user', $l->t('Login'))) ->setFlag(DefinitionParameter::FLAG_USER_PROVIDED), (new DefinitionParameter('password', $l->t('Password'))) ->setType(DefinitionParameter::VALUE_PASSWORD) @@ -64,14 +45,22 @@ class UserProvided extends AuthMechanism implements IUserProvided { return self::CREDENTIALS_IDENTIFIER_PREFIX . $storageId; } - public function saveBackendOptions(IUser $user, $id, array $options) { - $this->credentialsManager->store($user->getUID(), $this->getCredentialsIdentifier($id), [ + public function saveBackendOptions(IUser $user, $mountId, array $options) { + if ($options['password'] === DefinitionParameter::UNMODIFIED_PLACEHOLDER) { + $oldCredentials = $this->credentialsManager->retrieve($user->getUID(), $this->getCredentialsIdentifier($mountId)); + $options['password'] = $oldCredentials['password']; + } + + $this->credentialsManager->store($user->getUID(), $this->getCredentialsIdentifier($mountId), [ 'user' => $options['user'], // explicitly copy the fields we want instead of just passing the entire $options array 'password' => $options['password'] // this way we prevent users from being able to modify any other field ]); } - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { if (!isset($user)) { throw new InsufficientDataForMeaningfulAnswerException('No credentials saved'); } @@ -85,5 +74,4 @@ class UserProvided extends AuthMechanism implements IUserProvided { $storage->setBackendOption('user', $credentials['user']); $storage->setBackendOption('password', $credentials['password']); } - } diff --git a/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php b/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php index 425a601d7e9..ad95c743d2d 100644 --- a/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php +++ b/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php @@ -1,66 +1,56 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth\PublicKey; -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Lib\StorageConfig; -use \OCP\IConfig; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\StorageConfig; +use OCP\IConfig; +use OCP\IL10N; use OCP\IUser; -use \phpseclib\Crypt\RSA as RSACrypt; +use phpseclib\Crypt\RSA as RSACrypt; /** * RSA public key authentication */ class RSA extends AuthMechanism { - /** @var IConfig */ - private $config; - - public function __construct(IL10N $l, IConfig $config) { - $this->config = $config; - + public function __construct( + IL10N $l, + private IConfig $config, + ) { $this ->setIdentifier('publickey::rsa') ->setScheme(self::SCHEME_PUBLICKEY) ->setText($l->t('RSA public key')) ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))), - (new DefinitionParameter('public_key', $l->t('Public key'))), + new DefinitionParameter('user', $l->t('Login')), + new DefinitionParameter('public_key', $l->t('Public key')), (new DefinitionParameter('private_key', 'private_key')) - ->setType(DefinitionParameter::VALUE_HIDDEN), + ->setType(DefinitionParameter::VALUE_PASSWORD) + ->setFlag(DefinitionParameter::FLAG_HIDDEN), ]) ->addCustomJs('public_key') ; } - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { $auth = new RSACrypt(); $auth->setPassword($this->config->getSystemValue('secret', '')); if (!$auth->loadKey($storage->getBackendOption('private_key'))) { - throw new \RuntimeException('unable to load private key'); + // Add fallback routine for a time where secret was not enforced to be exists + $auth->setPassword(''); + if (!$auth->loadKey($storage->getBackendOption('private_key'))) { + throw new \RuntimeException('unable to load private key'); + } } $storage->setBackendOption('public_key_auth', $auth); } @@ -82,5 +72,4 @@ class RSA extends AuthMechanism { return $rsa->createKey($keyLength); } - } diff --git a/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php b/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php new file mode 100644 index 00000000000..8f58b71d5ac --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php @@ -0,0 +1,54 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Auth\PublicKey; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\StorageConfig; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IUser; +use phpseclib\Crypt\RSA as RSACrypt; + +/** + * RSA public key authentication + */ +class RSAPrivateKey extends AuthMechanism { + + public function __construct( + IL10N $l, + private IConfig $config, + ) { + $this + ->setIdentifier('publickey::rsa_private') + ->setScheme(self::SCHEME_PUBLICKEY) + ->setText($l->t('RSA private key')) + ->addParameters([ + new DefinitionParameter('user', $l->t('Login')), + (new DefinitionParameter('password', $l->t('Password'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setType(DefinitionParameter::VALUE_PASSWORD), + new DefinitionParameter('private_key', $l->t('Private key')), + ]); + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + $auth = new RSACrypt(); + $auth->setPassword($this->config->getSystemValue('secret', '')); + if (!$auth->loadKey($storage->getBackendOption('private_key'))) { + // Add fallback routine for a time where secret was not enforced to be exists + $auth->setPassword(''); + if (!$auth->loadKey($storage->getBackendOption('private_key'))) { + throw new \RuntimeException('unable to load private key'); + } + } + $storage->setBackendOption('public_key_auth', $auth); + } +} diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php new file mode 100644 index 00000000000..26671110294 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php @@ -0,0 +1,35 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Lib\Auth\SMB; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\Authentication\LoginCredentials\IStore; +use OCP\IL10N; + +class KerberosApacheAuth extends AuthMechanism { + public function __construct( + IL10N $l, + private IStore $credentialsStore, + ) { + $realm = new DefinitionParameter('default_realm', 'Default realm'); + $realm + ->setType(DefinitionParameter::VALUE_TEXT) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setTooltip($l->t('Kerberos default realm, defaults to "WORKGROUP"')); + $this + ->setIdentifier('smb::kerberosapache') + ->setScheme(self::SCHEME_SMB) + ->setText($l->t('Kerberos ticket Apache mode')) + ->addParameter($realm); + } + + public function getCredentialsStore(): IStore { + return $this->credentialsStore; + } +} diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php new file mode 100644 index 00000000000..9210209192a --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php @@ -0,0 +1,19 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Auth\SMB; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCP\IL10N; + +class KerberosAuth extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('smb::kerberos') + ->setScheme(self::SCHEME_SMB) + ->setText($l->t('Kerberos ticket')); + } +} diff --git a/apps/files_external/lib/Lib/Backend/AmazonS3.php b/apps/files_external/lib/Lib/Backend/AmazonS3.php index 5625805601c..464b03b55e0 100644 --- a/apps/files_external/lib/Lib/Backend/AmazonS3.php +++ b/apps/files_external/lib/Lib/Backend/AmazonS3.php @@ -1,38 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\AmazonS3\AccessKey; +use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCP\IL10N; class AmazonS3 extends Backend { - use LegacyDependencyCheckPolyfill; public function __construct(IL10N $l, AccessKey $legacyAuth) { @@ -42,23 +23,32 @@ class AmazonS3 extends Backend { ->setStorageClass('\OCA\Files_External\Lib\Storage\AmazonS3') ->setText($l->t('Amazon S3')) ->addParameters([ - (new DefinitionParameter('bucket', $l->t('Bucket'))), + new DefinitionParameter('bucket', $l->t('Bucket')), (new DefinitionParameter('hostname', $l->t('Hostname'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('port', $l->t('Port'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('region', $l->t('Region'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('storageClass', $l->t('Storage Class'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('use_ssl', $l->t('Enable SSL'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setDefaultValue(true), (new DefinitionParameter('use_path_style', $l->t('Enable Path Style'))) ->setType(DefinitionParameter::VALUE_BOOLEAN), (new DefinitionParameter('legacy_auth', $l->t('Legacy (v2) authentication'))) ->setType(DefinitionParameter::VALUE_BOOLEAN), + (new DefinitionParameter('useMultipartCopy', $l->t('Enable multipart copy'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setDefaultValue(true), + (new DefinitionParameter('sse_c_key', $l->t('SSE-C encryption key'))) + ->setType(DefinitionParameter::VALUE_PASSWORD) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), ]) ->addAuthScheme(AccessKey::SCHEME_AMAZONS3_ACCESSKEY) + ->addAuthScheme(AuthMechanism::SCHEME_NULL) ->setLegacyAuthMechanism($legacyAuth) ; } - } diff --git a/apps/files_external/lib/Lib/Backend/Backend.php b/apps/files_external/lib/Lib/Backend/Backend.php index d00c1802b4b..f7500ee24a4 100644 --- a/apps/files_external/lib/Lib/Backend/Backend.php +++ b/apps/files_external/lib/Lib/Backend/Backend.php @@ -1,36 +1,23 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\VisibilityTrait; -use \OCA\Files_External\Lib\FrontendDefinitionTrait; -use \OCA\Files_External\Lib\PriorityTrait; -use \OCA\Files_External\Lib\DependencyTrait; -use \OCA\Files_External\Lib\StorageModifierTrait; -use \OCA\Files_External\Lib\IdentifierTrait; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DependencyTrait; +use OCA\Files_External\Lib\FrontendDefinitionTrait; +use OCA\Files_External\Lib\IdentifierTrait; +use OCA\Files_External\Lib\IFrontendDefinition; +use OCA\Files_External\Lib\IIdentifier; +use OCA\Files_External\Lib\PriorityTrait; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Lib\StorageModifierTrait; +use OCA\Files_External\Lib\VisibilityTrait; +use OCP\Files\Storage\IStorage; /** * Storage backend @@ -39,7 +26,7 @@ use \OCA\Files_External\Lib\Auth\AuthMechanism; * such as \OCP\IDB for database operations. This allows a backend * to perform advanced operations based on provided information. * - * An authenication scheme defines the parameter interface, common to the + * An authentication scheme defines the parameter interface, common to the * storage implementation, the backend and the authentication mechanism. * A storage implementation expects parameters according to the authentication * scheme, which are provided from the authentication mechanism. @@ -56,8 +43,7 @@ use \OCA\Files_External\Lib\Auth\AuthMechanism; * - StorageModifierTrait * Object can affect storage mounting */ -class Backend implements \JsonSerializable { - +class Backend implements \JsonSerializable, IIdentifier, IFrontendDefinition { use VisibilityTrait; use FrontendDefinitionTrait; use PriorityTrait; @@ -75,7 +61,7 @@ class Backend implements \JsonSerializable { private $legacyAuthMechanism; /** - * @return string + * @return class-string<IStorage> */ public function getStorageClass() { return $this->storageClass; @@ -120,29 +106,23 @@ class Backend implements \JsonSerializable { return $this->legacyAuthMechanism; } - /** - * @param AuthMechanism $authMechanism - * @return self - */ - public function setLegacyAuthMechanism(AuthMechanism $authMechanism) { + public function setLegacyAuthMechanism(AuthMechanism $authMechanism): self { $this->legacyAuthMechanism = $authMechanism; return $this; } /** * @param callable $callback dynamic auth mechanism selection - * @return self */ - public function setLegacyAuthMechanismCallback(callable $callback) { + public function setLegacyAuthMechanismCallback(callable $callback): self { $this->legacyAuthMechanism = $callback; + return $this; } /** * Serialize into JSON for client-side JS - * - * @return array */ - public function jsonSerialize() { + public function jsonSerialize(): array { $data = $this->jsonSerializeDefinition(); $data += $this->jsonSerializeIdentifier(); @@ -162,6 +142,4 @@ class Backend implements \JsonSerializable { public function validateStorage(StorageConfig $storage) { return $this->validateStorageDefinition($storage); } - } - diff --git a/apps/files_external/lib/Lib/Backend/DAV.php b/apps/files_external/lib/Lib/Backend/DAV.php index a8053f81d36..dea9e7c5e77 100644 --- a/apps/files_external/lib/Lib/Backend/DAV.php +++ b/apps/files_external/lib/Lib/Backend/DAV.php @@ -1,38 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCP\IL10N; class DAV extends Backend { - use LegacyDependencyCheckPolyfill; public function __construct(IL10N $l, Password $legacyAuth) { @@ -42,15 +23,15 @@ class DAV extends Backend { ->setStorageClass('\OC\Files\Storage\DAV') ->setText($l->t('WebDAV')) ->addParameters([ - (new DefinitionParameter('host', $l->t('URL'))), + new DefinitionParameter('host', $l->t('URL')), (new DefinitionParameter('root', $l->t('Remote subfolder'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('secure', $l->t('Secure https://'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setDefaultValue(true), ]) ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) ->setLegacyAuthMechanism($legacyAuth) ; } - } diff --git a/apps/files_external/lib/Lib/Backend/FTP.php b/apps/files_external/lib/Lib/Backend/FTP.php index edb8884f595..72a8184c9b9 100644 --- a/apps/files_external/lib/Lib/Backend/FTP.php +++ b/apps/files_external/lib/Lib/Backend/FTP.php @@ -1,38 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCP\IL10N; class FTP extends Backend { - use LegacyDependencyCheckPolyfill; public function __construct(IL10N $l, Password $legacyAuth) { @@ -42,15 +23,17 @@ class FTP extends Backend { ->setStorageClass('\OCA\Files_External\Lib\Storage\FTP') ->setText($l->t('FTP')) ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), + new DefinitionParameter('host', $l->t('Host')), + (new DefinitionParameter('port', $l->t('Port'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('root', $l->t('Remote subfolder'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('secure', $l->t('Secure ftps://'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setDefaultValue(true), ]) ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) ->setLegacyAuthMechanism($legacyAuth) ; } - } diff --git a/apps/files_external/lib/Lib/Backend/InvalidBackend.php b/apps/files_external/lib/Lib/Backend/InvalidBackend.php index 7b3eb26aec9..48912c0e49e 100644 --- a/apps/files_external/lib/Lib/Backend/InvalidBackend.php +++ b/apps/files_external/lib/Lib/Backend/InvalidBackend.php @@ -1,30 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Vincent Petry <pvince81@owncloud.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use OCA\Files_External\Lib\Storage\InvalidStorage; use OCA\Files_External\Lib\StorageConfig; use OCP\Files\StorageNotAvailableException; use OCP\IUser; @@ -35,21 +17,19 @@ use OCP\IUser; */ class InvalidBackend extends Backend { - /** @var string Invalid backend id */ - private $invalidId; - /** * Constructs a new InvalidBackend with the id of the invalid backend * for display purposes * * @param string $invalidId id of the backend that did not exist */ - function __construct($invalidId) { - $this->invalidId = $invalidId; + public function __construct( + private $invalidId, + ) { $this - ->setIdentifier($invalidId) + ->setIdentifier($this->invalidId) ->setStorageClass('\OC\Files\Storage\FailedStorage') - ->setText('Unknown storage backend ' . $invalidId); + ->setText('Unknown storage backend ' . $this->invalidId); } /** @@ -61,8 +41,10 @@ class InvalidBackend extends Backend { return $this->invalidId; } - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { $storage->setBackendOption('exception', new \Exception('Unknown storage backend "' . $this->invalidId . '"', StorageNotAvailableException::STATUS_ERROR)); } } - diff --git a/apps/files_external/lib/Lib/Backend/LegacyBackend.php b/apps/files_external/lib/Lib/Backend/LegacyBackend.php index 4d3aa3e7e93..9c7e5b01bc3 100644 --- a/apps/files_external/lib/Lib/Backend/LegacyBackend.php +++ b/apps/files_external/lib/Lib/Backend/LegacyBackend.php @@ -1,38 +1,21 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\Auth\Builtin; -use \OCA\Files_External\Lib\MissingDependency; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCA\Files_External\Lib\Auth\Builtin; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCA\Files_External\Lib\MissingDependency; /** - * Legacy compatibility for OC_Mount_Config::registerBackend() + * Legacy compatibility for OCA\Files_External\MountConfig::registerBackend() */ class LegacyBackend extends Backend { - use LegacyDependencyCheckPolyfill { LegacyDependencyCheckPolyfill::checkDependencies as doCheckDependencies; } @@ -62,18 +45,14 @@ class LegacyBackend extends Backend { $placeholder = substr($placeholder, 1); } switch ($placeholder[0]) { - case '!': - $type = DefinitionParameter::VALUE_BOOLEAN; - $placeholder = substr($placeholder, 1); - break; - case '*': - $type = DefinitionParameter::VALUE_PASSWORD; - $placeholder = substr($placeholder, 1); - break; - case '#': - $type = DefinitionParameter::VALUE_HIDDEN; - $placeholder = substr($placeholder, 1); - break; + case '!': + $type = DefinitionParameter::VALUE_BOOLEAN; + $placeholder = substr($placeholder, 1); + break; + case '*': + $type = DefinitionParameter::VALUE_PASSWORD; + $placeholder = substr($placeholder, 1); + break; } $this->addParameter((new DefinitionParameter($name, $placeholder)) ->setType($type) @@ -101,5 +80,4 @@ class LegacyBackend extends Backend { } return []; } - } diff --git a/apps/files_external/lib/Lib/Backend/Local.php b/apps/files_external/lib/Lib/Backend/Local.php index 298b0af1c7a..56940b8e83b 100644 --- a/apps/files_external/lib/Lib/Backend/Local.php +++ b/apps/files_external/lib/Lib/Backend/Local.php @@ -1,36 +1,21 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\Auth\NullMechanism; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\NullMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCP\IL10N; +use OCP\IUser; class Local extends Backend { - public function __construct(IL10N $l, NullMechanism $legacyAuth) { $this ->setIdentifier('local') @@ -38,7 +23,7 @@ class Local extends Backend { ->setStorageClass('\OC\Files\Storage\Local') ->setText($l->t('Local')) ->addParameters([ - (new DefinitionParameter('datadir', $l->t('Location'))), + new DefinitionParameter('datadir', $l->t('Location')), ]) ->setAllowedVisibility(BackendService::VISIBILITY_ADMIN) ->setPriority(BackendService::PRIORITY_DEFAULT + 50) @@ -47,4 +32,7 @@ class Local extends Backend { ; } + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null): void { + $storage->setBackendOption('isExternal', true); + } } diff --git a/apps/files_external/lib/Lib/Backend/OwnCloud.php b/apps/files_external/lib/Lib/Backend/OwnCloud.php index 0d3ffb30b40..0c0e2c6d300 100644 --- a/apps/files_external/lib/Lib/Backend/OwnCloud.php +++ b/apps/files_external/lib/Lib/Backend/OwnCloud.php @@ -1,38 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; - -use \OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; class OwnCloud extends Backend { - public function __construct(IL10N $l, Password $legacyAuth) { $this ->setIdentifier('owncloud') @@ -40,15 +20,15 @@ class OwnCloud extends Backend { ->setStorageClass('\OCA\Files_External\Lib\Storage\OwnCloud') ->setText($l->t('Nextcloud')) ->addParameters([ - (new DefinitionParameter('host', $l->t('URL'))), + new DefinitionParameter('host', $l->t('URL')), (new DefinitionParameter('root', $l->t('Remote subfolder'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('secure', $l->t('Secure https://'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setDefaultValue(true), ]) ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) ->setLegacyAuthMechanism($legacyAuth) ; } - } diff --git a/apps/files_external/lib/Lib/Backend/SFTP.php b/apps/files_external/lib/Lib/Backend/SFTP.php index c9b31272999..0926cf7fd93 100644 --- a/apps/files_external/lib/Lib/Backend/SFTP.php +++ b/apps/files_external/lib/Lib/Backend/SFTP.php @@ -1,34 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; class SFTP extends Backend { - public function __construct(IL10N $l, Password $legacyAuth) { $this ->setIdentifier('sftp') @@ -36,7 +20,9 @@ class SFTP extends Backend { ->setStorageClass('\OCA\Files_External\Lib\Storage\SFTP') ->setText($l->t('SFTP')) ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), + new DefinitionParameter('host', $l->t('Host')), + (new DefinitionParameter('port', $l->t('Port'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('root', $l->t('Root'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), ]) @@ -45,5 +31,4 @@ class SFTP extends Backend { ->setLegacyAuthMechanism($legacyAuth) ; } - } diff --git a/apps/files_external/lib/Lib/Backend/SFTP_Key.php b/apps/files_external/lib/Lib/Backend/SFTP_Key.php index 80ac1ceecc4..278fae3fba7 100644 --- a/apps/files_external/lib/Lib/Backend/SFTP_Key.php +++ b/apps/files_external/lib/Lib/Backend/SFTP_Key.php @@ -1,44 +1,25 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\Auth\PublicKey\RSA; -use \OCA\Files_External\Lib\Backend\SFTP; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\PublicKey\RSA; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; class SFTP_Key extends Backend { - public function __construct(IL10N $l, RSA $legacyAuth, SFTP $sftpBackend) { $this ->setIdentifier('\OC\Files\Storage\SFTP_Key') ->setStorageClass('\OCA\Files_External\Lib\Storage\SFTP') ->setText($l->t('SFTP with secret key login')) ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), + new DefinitionParameter('host', $l->t('Host')), (new DefinitionParameter('root', $l->t('Remote subfolder'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), ]) @@ -47,5 +28,4 @@ class SFTP_Key extends Backend { ->deprecateTo($sftpBackend) ; } - } diff --git a/apps/files_external/lib/Lib/Backend/SMB.php b/apps/files_external/lib/Lib/Backend/SMB.php index c4aadbd13f6..e86ad98880c 100644 --- a/apps/files_external/lib/Lib/Backend/SMB.php +++ b/apps/files_external/lib/Lib/Backend/SMB.php @@ -1,70 +1,140 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\Password\Password; +use Icewind\SMB\BasicAuth; +use Icewind\SMB\KerberosAuth; +use Icewind\SMB\KerberosTicket; +use Icewind\SMB\Native\NativeServer; +use Icewind\SMB\Wrapped\Server; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\Auth\SMB\KerberosApacheAuth as KerberosApacheAuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\MissingDependency; +use OCA\Files_External\Lib\Storage\SystemBridge; +use OCA\Files_External\Lib\StorageConfig; +use OCP\IL10N; use OCP\IUser; class SMB extends Backend { - - use LegacyDependencyCheckPolyfill; - public function __construct(IL10N $l, Password $legacyAuth) { $this ->setIdentifier('smb') - ->addIdentifierAlias('\OC\Files\Storage\SMB') // legacy compat + ->addIdentifierAlias('\OC\Files\Storage\SMB')// legacy compat ->setStorageClass('\OCA\Files_External\Lib\Storage\SMB') - ->setText($l->t('SMB / CIFS')) + ->setText($l->t('SMB/CIFS')) ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), - (new DefinitionParameter('share', $l->t('Share'))), + new DefinitionParameter('host', $l->t('Host')), + new DefinitionParameter('share', $l->t('Share')), (new DefinitionParameter('root', $l->t('Remote subfolder'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), (new DefinitionParameter('domain', $l->t('Domain'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('show_hidden', $l->t('Show hidden files'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('case_sensitive', $l->t('Case sensitive file system'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setDefaultValue(true) + ->setTooltip($l->t('Disabling it will allow to use a case insensitive file system, but comes with a performance penalty')), + (new DefinitionParameter('check_acl', $l->t('Verify ACL access when listing files'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setTooltip($l->t("Check the ACL's of each file or folder inside a directory to filter out items where the account has no read permissions, comes with a performance penalty")), + (new DefinitionParameter('timeout', $l->t('Timeout'))) + ->setType(DefinitionParameter::VALUE_TEXT) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setFlag(DefinitionParameter::FLAG_HIDDEN), ]) ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) - ->setLegacyAuthMechanism($legacyAuth) - ; + ->addAuthScheme(AuthMechanism::SCHEME_SMB) + ->setLegacyAuthMechanism($legacyAuth); } - /** - * @param StorageConfig $storage - * @param IUser $user - */ - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { - $user = $storage->getBackendOption('user'); - if ($domain = $storage->getBackendOption('domain')) { - $storage->setBackendOption('user', $domain.'\\'.$user); + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null): void { + $auth = $storage->getAuthMechanism(); + if ($auth->getScheme() === AuthMechanism::SCHEME_PASSWORD) { + if (!is_string($storage->getBackendOption('user')) || !is_string($storage->getBackendOption('password'))) { + throw new \InvalidArgumentException('user or password is not set'); + } + + $smbAuth = new BasicAuth( + $storage->getBackendOption('user'), + $storage->getBackendOption('domain'), + $storage->getBackendOption('password') + ); + } else { + switch ($auth->getIdentifier()) { + case 'smb::kerberos': + $smbAuth = new KerberosAuth(); + break; + case 'smb::kerberosapache': + if (!$auth instanceof KerberosApacheAuthMechanism) { + throw new \InvalidArgumentException('invalid authentication backend'); + } + $credentialsStore = $auth->getCredentialsStore(); + $kerbAuth = new KerberosAuth(); + $kerbAuth->setTicket(KerberosTicket::fromEnv()); + // check if a kerberos ticket is available, else fallback to session credentials + if ($kerbAuth->getTicket()?->isValid()) { + $smbAuth = $kerbAuth; + } else { + try { + $credentials = $credentialsStore->getLoginCredentials(); + $loginName = $credentials->getLoginName(); + $pass = $credentials->getPassword(); + preg_match('/(.*)@(.*)/', $loginName, $matches); + $realm = $storage->getBackendOption('default_realm'); + if (empty($realm)) { + $realm = 'WORKGROUP'; + } + if (count($matches) === 0) { + $username = $loginName; + $workgroup = $realm; + } else { + [, $username, $workgroup] = $matches; + } + $smbAuth = new BasicAuth( + $username, + $workgroup, + $pass + ); + } catch (\Exception) { + throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved'); + } + } + + break; + default: + throw new \InvalidArgumentException('unknown authentication backend'); + } } + + $storage->setBackendOption('auth', $smbAuth); } + public function checkDependencies(): array { + $system = \OCP\Server::get(SystemBridge::class); + if (NativeServer::available($system)) { + return []; + } elseif (Server::available($system)) { + $missing = new MissingDependency('php-smbclient'); + $missing->setOptional(true); + $missing->setMessage('The php-smbclient library provides improved compatibility and performance for SMB storages.'); + return [$missing]; + } else { + $missing = new MissingDependency('php-smbclient'); + $missing->setMessage('Either the php-smbclient library (preferred) or the smbclient binary is required for SMB storages.'); + return [$missing, new MissingDependency('smbclient')]; + } + } } diff --git a/apps/files_external/lib/Lib/Backend/SMB_OC.php b/apps/files_external/lib/Lib/Backend/SMB_OC.php index 76126fd649f..bcb8d0fbf16 100644 --- a/apps/files_external/lib/Lib/Backend/SMB_OC.php +++ b/apps/files_external/lib/Lib/Backend/SMB_OC.php @@ -1,53 +1,35 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\Auth\Password\SessionCredentials; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; -use \OCA\Files_External\Lib\Backend\SMB; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\Password\SessionCredentials; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCP\IL10N; use OCP\IUser; /** * Deprecated SMB_OC class - use SMB with the password::sessioncredentials auth mechanism */ class SMB_OC extends Backend { - use LegacyDependencyCheckPolyfill; public function __construct(IL10N $l, SessionCredentials $legacyAuth, SMB $smbBackend) { $this ->setIdentifier('\OC\Files\Storage\SMB_OC') ->setStorageClass('\OCA\Files_External\Lib\Storage\SMB') - ->setText($l->t('SMB / CIFS using OC login')) + ->setText($l->t('SMB/CIFS using OC login')) ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), - (new DefinitionParameter('username_as_share', $l->t('Username as share'))) + new DefinitionParameter('host', $l->t('Host')), + (new DefinitionParameter('username_as_share', $l->t('Login as share'))) ->setType(DefinitionParameter::VALUE_BOOLEAN), (new DefinitionParameter('share', $l->t('Share'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), @@ -61,7 +43,10 @@ class SMB_OC extends Backend { ; } - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { $username_as_share = ($storage->getBackendOption('username_as_share') === true); if ($username_as_share) { @@ -69,5 +54,4 @@ class SMB_OC extends Backend { $storage->setBackendOption('share', $share); } } - } diff --git a/apps/files_external/lib/Lib/Backend/Swift.php b/apps/files_external/lib/Lib/Backend/Swift.php index 52ed66b95ae..37527ba3dbb 100644 --- a/apps/files_external/lib/Lib/Backend/Swift.php +++ b/apps/files_external/lib/Lib/Backend/Swift.php @@ -1,41 +1,23 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\Auth\OpenStack\OpenStack; -use \OCA\Files_External\Lib\Auth\OpenStack\Rackspace; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\OpenStack\OpenStackV2; +use OCA\Files_External\Lib\Auth\OpenStack\Rackspace; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCP\IL10N; class Swift extends Backend { - use LegacyDependencyCheckPolyfill; - public function __construct(IL10N $l, OpenStack $openstackAuth, Rackspace $rackspaceAuth) { + public function __construct(IL10N $l, OpenStackV2 $openstackAuth, Rackspace $rackspaceAuth) { $this ->setIdentifier('swift') ->addIdentifierAlias('\OC\Files\Storage\Swift') // legacy compat @@ -44,14 +26,13 @@ class Swift extends Backend { ->addParameters([ (new DefinitionParameter('service_name', $l->t('Service name'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('region', $l->t('Region'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('bucket', $l->t('Bucket'))), + new DefinitionParameter('region', $l->t('Region')), + new DefinitionParameter('bucket', $l->t('Bucket')), (new DefinitionParameter('timeout', $l->t('Request timeout (seconds)'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), ]) ->addAuthScheme(AuthMechanism::SCHEME_OPENSTACK) - ->setLegacyAuthMechanismCallback(function(array $params) use ($openstackAuth, $rackspaceAuth) { + ->setLegacyAuthMechanismCallback(function (array $params) use ($openstackAuth, $rackspaceAuth) { if (isset($params['options']['key']) && $params['options']['key']) { return $rackspaceAuth; } @@ -59,5 +40,4 @@ class Swift extends Backend { }) ; } - } diff --git a/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php b/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php index 251b04eada0..0c2e90a243c 100644 --- a/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php +++ b/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php @@ -1,28 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Config; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\AuthMechanism; /** * Provider of external storage auth mechanisms @@ -35,5 +20,4 @@ interface IAuthMechanismProvider { * @return AuthMechanism[] */ public function getAuthMechanisms(); - } diff --git a/apps/files_external/lib/Lib/Config/IBackendProvider.php b/apps/files_external/lib/Lib/Config/IBackendProvider.php index 178e5e5d2b6..44c460c3138 100644 --- a/apps/files_external/lib/Lib/Config/IBackendProvider.php +++ b/apps/files_external/lib/Lib/Config/IBackendProvider.php @@ -1,28 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Config; -use \OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\Backend\Backend; /** * Provider of external storage backends @@ -35,5 +20,4 @@ interface IBackendProvider { * @return Backend[] */ public function getBackends(); - } diff --git a/apps/files_external/lib/Lib/DefinitionParameter.php b/apps/files_external/lib/Lib/DefinitionParameter.php index c1108fccce7..a73dd2df967 100644 --- a/apps/files_external/lib/Lib/DefinitionParameter.php +++ b/apps/files_external/lib/Lib/DefinitionParameter.php @@ -1,76 +1,63 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; /** * Parameter for an external storage definition */ class DefinitionParameter implements \JsonSerializable { + // placeholder value for password fields, when the client updates a storage configuration + // placeholder values are ignored and the field is left unmodified + public const UNMODIFIED_PLACEHOLDER = '__unmodified__'; /** Value constants */ - const VALUE_TEXT = 0; - const VALUE_BOOLEAN = 1; - const VALUE_PASSWORD = 2; - const VALUE_HIDDEN = 3; + public const VALUE_TEXT = 0; + public const VALUE_BOOLEAN = 1; + public const VALUE_PASSWORD = 2; /** Flag constants */ - const FLAG_NONE = 0; - const FLAG_OPTIONAL = 1; - const FLAG_USER_PROVIDED = 2; + public const FLAG_NONE = 0; + public const FLAG_OPTIONAL = 1; + public const FLAG_USER_PROVIDED = 2; + public const FLAG_HIDDEN = 4; - /** @var string name of parameter */ - private $name; - - /** @var string human-readable parameter text */ - private $text; + /** @var string human-readable parameter tooltip */ + private string $tooltip = ''; /** @var int value type, see self::VALUE_* constants */ - private $type = self::VALUE_TEXT; + private int $type = self::VALUE_TEXT; /** @var int flags, see self::FLAG_* constants */ - private $flags = self::FLAG_NONE; + private int $flags = self::FLAG_NONE; /** - * @param string $name - * @param string $text + * @param string $name parameter name + * @param string $text parameter description + * @param mixed $defaultValue default value */ - public function __construct($name, $text) { - $this->name = $name; - $this->text = $text; + public function __construct( + private string $name, + private string $text, + private $defaultValue = null, + ) { } /** * @return string */ - public function getName() { + public function getName(): string { return $this->name; } /** * @return string */ - public function getText() { + public function getText(): string { return $this->text; } @@ -79,7 +66,7 @@ class DefinitionParameter implements \JsonSerializable { * * @return int */ - public function getType() { + public function getType(): int { return $this->type; } @@ -89,15 +76,31 @@ class DefinitionParameter implements \JsonSerializable { * @param int $type * @return self */ - public function setType($type) { + public function setType(int $type) { $this->type = $type; return $this; } /** + * @return mixed default value + */ + public function getDefaultValue() { + return $this->defaultValue; + } + + /** + * @param mixed $defaultValue default value + * @return self + */ + public function setDefaultValue($defaultValue) { + $this->defaultValue = $defaultValue; + return $this; + } + + /** * @return string */ - public function getTypeName() { + public function getTypeName(): string { switch ($this->type) { case self::VALUE_BOOLEAN: return 'boolean'; @@ -113,7 +116,7 @@ class DefinitionParameter implements \JsonSerializable { /** * @return int */ - public function getFlags() { + public function getFlags(): int { return $this->flags; } @@ -121,7 +124,7 @@ class DefinitionParameter implements \JsonSerializable { * @param int $flags * @return self */ - public function setFlags($flags) { + public function setFlags(int $flags) { $this->flags = $flags; return $this; } @@ -130,7 +133,7 @@ class DefinitionParameter implements \JsonSerializable { * @param int $flag * @return self */ - public function setFlag($flag) { + public function setFlag(int $flag) { $this->flags |= $flag; return $this; } @@ -139,24 +142,44 @@ class DefinitionParameter implements \JsonSerializable { * @param int $flag * @return bool */ - public function isFlagSet($flag) { + public function isFlagSet(int $flag): bool { return (bool)($this->flags & $flag); } /** - * Serialize into JSON for client-side JS - * * @return string */ - public function jsonSerialize() { - return [ + public function getTooltip(): string { + return $this->tooltip; + } + + /** + * @param string $tooltip + * @return self + */ + public function setTooltip(string $tooltip) { + $this->tooltip = $tooltip; + return $this; + } + + /** + * Serialize into JSON for client-side JS + */ + public function jsonSerialize(): array { + $result = [ 'value' => $this->getText(), 'flags' => $this->getFlags(), - 'type' => $this->getType() + 'type' => $this->getType(), + 'tooltip' => $this->getTooltip(), ]; + $defaultValue = $this->getDefaultValue(); + if ($defaultValue) { + $result['defaultValue'] = $defaultValue; + } + return $result; } - public function isOptional() { + public function isOptional(): bool { return $this->isFlagSet(self::FLAG_OPTIONAL) || $this->isFlagSet(self::FLAG_USER_PROVIDED); } @@ -167,7 +190,7 @@ class DefinitionParameter implements \JsonSerializable { * @param mixed $value Value to check * @return bool success */ - public function validateValue(&$value) { + public function validateValue(&$value): bool { switch ($this->getType()) { case self::VALUE_BOOLEAN: if (!is_bool($value)) { diff --git a/apps/files_external/lib/Lib/DependencyTrait.php b/apps/files_external/lib/Lib/DependencyTrait.php index bae2d277a59..644132b82bc 100644 --- a/apps/files_external/lib/Lib/DependencyTrait.php +++ b/apps/files_external/lib/Lib/DependencyTrait.php @@ -1,29 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCA\Files_External\Lib\MissingDependency; - /** * Trait for objects that have dependencies for use */ @@ -37,6 +20,4 @@ trait DependencyTrait { public function checkDependencies() { return []; // no dependencies by default } - } - diff --git a/apps/files_external/lib/Lib/FrontendDefinitionTrait.php b/apps/files_external/lib/Lib/FrontendDefinitionTrait.php index 25b3cb4fc29..0f280d1d486 100644 --- a/apps/files_external/lib/Lib/FrontendDefinitionTrait.php +++ b/apps/files_external/lib/Lib/FrontendDefinitionTrait.php @@ -1,94 +1,57 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\StorageConfig; - /** * Trait for objects that have a frontend representation */ trait FrontendDefinitionTrait { /** @var string human-readable mechanism name */ - private $text; + private string $text = ''; - /** @var DefinitionParameter[] parameters for mechanism */ - private $parameters = []; + /** @var array<string, DefinitionParameter> parameters for mechanism */ + private array $parameters = []; /** @var string[] custom JS */ - private $customJs = []; + private array $customJs = []; - /** - * @return string - */ - public function getText() { + public function getText(): string { return $this->text; } - /** - * @param string $text - * @return $this - */ - public function setText($text) { + public function setText(string $text): self { $this->text = $text; return $this; } - /** - * @param FrontendDefinitionTrait $a - * @param FrontendDefinitionTrait $b - * @return int - */ - public static function lexicalCompare(FrontendDefinitionTrait $a, FrontendDefinitionTrait $b) { + public static function lexicalCompare(IFrontendDefinition $a, IFrontendDefinition $b): int { return strcmp($a->getText(), $b->getText()); } /** - * @return DefinitionParameter[] + * @return array<string, DefinitionParameter> */ - public function getParameters() { + public function getParameters(): array { return $this->parameters; } /** - * @param DefinitionParameter[] $parameters - * @return self + * @param list<DefinitionParameter> $parameters */ - public function addParameters(array $parameters) { + public function addParameters(array $parameters): self { foreach ($parameters as $parameter) { $this->addParameter($parameter); } return $this; } - /** - * @param DefinitionParameter $parameter - * @return self - */ - public function addParameter(DefinitionParameter $parameter) { + public function addParameter(DefinitionParameter $parameter): self { $this->parameters[$parameter->getName()] = $parameter; return $this; } @@ -96,7 +59,7 @@ trait FrontendDefinitionTrait { /** * @return string[] */ - public function getCustomJs() { + public function getCustomJs(): array { return $this->customJs; } @@ -104,27 +67,15 @@ trait FrontendDefinitionTrait { * @param string $custom * @return self */ - public function addCustomJs($custom) { + public function addCustomJs(string $custom): self { $this->customJs[] = $custom; return $this; } /** - * @param string $custom - * @return self - * @deprecated 9.1.0, use addCustomJs() instead - */ - public function setCustomJs($custom) { - $this->customJs = [$custom]; - return $this; - } - - /** * Serialize into JSON for client-side JS - * - * @return array */ - public function jsonSerializeDefinition() { + public function jsonSerializeDefinition(): array { $configuration = []; foreach ($this->getParameters() as $parameter) { $configuration[$parameter->getName()] = $parameter; @@ -140,11 +91,8 @@ trait FrontendDefinitionTrait { /** * Check if parameters are satisfied in a StorageConfig - * - * @param StorageConfig $storage - * @return bool */ - public function validateStorageDefinition(StorageConfig $storage) { + public function validateStorageDefinition(StorageConfig $storage): bool { foreach ($this->getParameters() as $name => $parameter) { $value = $storage->getBackendOption($name); if (!is_null($value) || !$parameter->isOptional()) { @@ -156,5 +104,4 @@ trait FrontendDefinitionTrait { } return true; } - } diff --git a/apps/files_external/lib/Lib/IFrontendDefinition.php b/apps/files_external/lib/Lib/IFrontendDefinition.php new file mode 100644 index 00000000000..c8b06a1c30b --- /dev/null +++ b/apps/files_external/lib/Lib/IFrontendDefinition.php @@ -0,0 +1,43 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib; + +interface IFrontendDefinition { + + public function getText(): string; + + public function setText(string $text): self; + + /** + * @return array<string, DefinitionParameter> + */ + public function getParameters(): array; + + /** + * @param list<DefinitionParameter> $parameters + */ + public function addParameters(array $parameters): self; + + public function addParameter(DefinitionParameter $parameter): self; + + /** + * @return string[] + */ + public function getCustomJs(): array; + + public function addCustomJs(string $custom): self; + + /** + * Serialize into JSON for client-side JS + */ + public function jsonSerializeDefinition(): array; + + /** + * Check if parameters are satisfied in a StorageConfig + */ + public function validateStorageDefinition(StorageConfig $storage): bool; +} diff --git a/apps/files_external/lib/Lib/IIdentifier.php b/apps/files_external/lib/Lib/IIdentifier.php new file mode 100644 index 00000000000..0677409a3cf --- /dev/null +++ b/apps/files_external/lib/Lib/IIdentifier.php @@ -0,0 +1,14 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib; + +interface IIdentifier { + + public function getIdentifier(): string; + + public function setIdentifier(string $identifier): self; +} diff --git a/apps/files_external/lib/Lib/IdentifierTrait.php b/apps/files_external/lib/Lib/IdentifierTrait.php index a9f553dea89..f5ffde32307 100644 --- a/apps/files_external/lib/Lib/IdentifierTrait.php +++ b/apps/files_external/lib/Lib/IdentifierTrait.php @@ -1,26 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; /** @@ -29,27 +13,17 @@ namespace OCA\Files_External\Lib; */ trait IdentifierTrait { - /** @var string */ - protected $identifier; + protected string $identifier = ''; /** @var string[] */ - protected $identifierAliases = []; + protected array $identifierAliases = []; + protected ?IIdentifier $deprecateTo = null; - /** @var IdentifierTrait */ - protected $deprecateTo = null; - - /** - * @return string - */ - public function getIdentifier() { + public function getIdentifier(): string { return $this->identifier; } - /** - * @param string $identifier - * @return $this - */ - public function setIdentifier($identifier) { + public function setIdentifier(string $identifier): self { $this->identifier = $identifier; $this->identifierAliases[] = $identifier; return $this; @@ -58,39 +32,25 @@ trait IdentifierTrait { /** * @return string[] */ - public function getIdentifierAliases() { + public function getIdentifierAliases(): array { return $this->identifierAliases; } - /** - * @param string $alias - * @return $this - */ - public function addIdentifierAlias($alias) { + public function addIdentifierAlias(string $alias): self { $this->identifierAliases[] = $alias; return $this; } - /** - * @return object|null - */ - public function getDeprecateTo() { + public function getDeprecateTo(): ?IIdentifier { return $this->deprecateTo; } - /** - * @param object $destinationObject - * @return self - */ - public function deprecateTo($destinationObject) { + public function deprecateTo(IIdentifier $destinationObject): self { $this->deprecateTo = $destinationObject; return $this; } - /** - * @return array - */ - public function jsonSerializeIdentifier() { + public function jsonSerializeIdentifier(): array { $data = [ 'identifier' => $this->identifier, 'identifierAliases' => $this->identifierAliases, @@ -100,5 +60,4 @@ trait IdentifierTrait { } return $data; } - } diff --git a/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php b/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php index dc0799042e5..1e872b35072 100644 --- a/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php +++ b/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php @@ -1,30 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCP\Files\StorageNotAvailableException; +use OCP\Files\StorageNotAvailableException; /** * Authentication mechanism or backend has insufficient data @@ -38,7 +21,7 @@ class InsufficientDataForMeaningfulAnswerException extends StorageNotAvailableEx * @param \Exception|null $previous * @since 6.0.0 */ - public function __construct($message = '', $code = self::STATUS_INDETERMINATE, \Exception $previous = null) { + public function __construct($message = '', $code = self::STATUS_INDETERMINATE, ?\Exception $previous = null) { parent::__construct($message, $code, $previous); } } diff --git a/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php b/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php index 4db4e6d74de..f6311fae83e 100644 --- a/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php +++ b/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php @@ -1,28 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCA\Files_External\Lib\MissingDependency; +use OCP\Files\Storage\IStorage; /** * Polyfill for checking dependencies using legacy Storage::checkDependencies() @@ -30,7 +15,7 @@ use \OCA\Files_External\Lib\MissingDependency; trait LegacyDependencyCheckPolyfill { /** - * @return string + * @return class-string<IStorage> */ abstract public function getStorageClass(); @@ -57,7 +42,7 @@ trait LegacyDependencyCheckPolyfill { $module = $key; $message = $value; } - $value = new MissingDependency($module, $this); + $value = new MissingDependency($module); $value->setMessage($message); } $ret[] = $value; @@ -66,6 +51,4 @@ trait LegacyDependencyCheckPolyfill { return $ret; } - } - diff --git a/apps/files_external/lib/Lib/MissingDependency.php b/apps/files_external/lib/Lib/MissingDependency.php index 056f57638eb..c2da7fcadbf 100644 --- a/apps/files_external/lib/Lib/MissingDependency.php +++ b/apps/files_external/lib/Lib/MissingDependency.php @@ -1,25 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; /** @@ -27,30 +12,23 @@ namespace OCA\Files_External\Lib; */ class MissingDependency { - /** @var string */ - private $dependency; - /** @var string|null Custom message */ - private $message = null; + private ?string $message = null; + private bool $optional = false; /** * @param string $dependency */ - public function __construct($dependency) { - $this->dependency = $dependency; + public function __construct( + private readonly string $dependency, + ) { } - /** - * @return string - */ - public function getDependency() { + public function getDependency(): string { return $this->dependency; } - /** - * @return string|null - */ - public function getMessage() { + public function getMessage(): ?string { return $this->message; } @@ -62,4 +40,12 @@ class MissingDependency { $this->message = $message; return $this; } + + public function isOptional(): bool { + return $this->optional; + } + + public function setOptional(bool $optional): void { + $this->optional = $optional; + } } diff --git a/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php b/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php index 9ac74b32ad8..2812df6ad6a 100644 --- a/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php +++ b/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\Files_External\Lib\Notify; use OC\Files\Notify\Change; @@ -30,11 +13,6 @@ use OCP\Files\Notify\INotifyHandler; class SMBNotifyHandler implements INotifyHandler { /** - * @var \Icewind\SMB\INotifyHandler - */ - private $shareNotifyHandler; - - /** * @var string */ private $root; @@ -47,15 +25,17 @@ class SMBNotifyHandler implements INotifyHandler { * @param \Icewind\SMB\INotifyHandler $shareNotifyHandler * @param string $root */ - public function __construct(\Icewind\SMB\INotifyHandler $shareNotifyHandler, $root) { - $this->shareNotifyHandler = $shareNotifyHandler; - $this->root = $root; + public function __construct( + private \Icewind\SMB\INotifyHandler $shareNotifyHandler, + $root, + ) { + $this->root = str_replace('\\', '/', $root); } private function relativePath($fullPath) { if ($fullPath === $this->root) { return ''; - } else if (substr($fullPath, 0, strlen($this->root)) === $this->root) { + } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) { return substr($fullPath, strlen($this->root)); } else { return null; diff --git a/apps/files_external/lib/Lib/PersonalMount.php b/apps/files_external/lib/Lib/PersonalMount.php index c54ed0a79f3..d9dbddd1449 100644 --- a/apps/files_external/lib/Lib/PersonalMount.php +++ b/apps/files_external/lib/Lib/PersonalMount.php @@ -1,64 +1,45 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use OC\Files\Mount\MountPoint; use OC\Files\Mount\MoveableMount; +use OCA\Files_External\Config\ExternalMountPoint; use OCA\Files_External\Service\UserStoragesService; +use OCP\Files\Storage\IStorage; +use OCP\Files\Storage\IStorageFactory; /** * Person mount points can be moved by the user */ -class PersonalMount extends MountPoint implements MoveableMount { - /** @var UserStoragesService */ - protected $storagesService; - - /** @var int */ - protected $numericStorageId; - +class PersonalMount extends ExternalMountPoint implements MoveableMount { /** * @param UserStoragesService $storagesService * @param int $storageId - * @param \OCP\Files\Storage $storage + * @param IStorage $storage * @param string $mountpoint * @param array $arguments (optional) configuration for the storage backend - * @param \OCP\Files\Storage\IStorageFactory $loader + * @param IStorageFactory $loader * @param array $mountOptions mount specific options + * @param int $externalStorageId */ public function __construct( - UserStoragesService $storagesService, - $storageId, + protected UserStoragesService $storagesService, + StorageConfig $storageConfig, + /** @var int id of the external storage (mount) (not the numeric id of the resulting storage!) */ + protected $numericExternalStorageId, $storage, $mountpoint, $arguments = null, $loader = null, - $mountOptions = null + $mountOptions = null, + $mountId = null, ) { - parent::__construct($storage, $mountpoint, $arguments, $loader, $mountOptions); - $this->storagesService = $storagesService; - $this->numericStorageId = $storageId; + parent::__construct($storageConfig, $storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId); } /** @@ -68,7 +49,7 @@ class PersonalMount extends MountPoint implements MoveableMount { * @return bool */ public function moveMount($target) { - $storage = $this->storagesService->getStorage($this->numericStorageId); + $storage = $this->storagesService->getStorage($this->numericExternalStorageId); // remove "/$user/files" prefix $targetParts = explode('/', trim($target, '/'), 3); $storage->setMountPoint($targetParts[2]); @@ -83,7 +64,7 @@ class PersonalMount extends MountPoint implements MoveableMount { * @return bool */ public function removeMount() { - $this->storagesService->removeStorage($this->numericStorageId); + $this->storagesService->removeStorage($this->numericExternalStorageId); return true; } } diff --git a/apps/files_external/lib/Lib/PriorityTrait.php b/apps/files_external/lib/Lib/PriorityTrait.php index d060dbe2f9d..fad2c07e58c 100644 --- a/apps/files_external/lib/Lib/PriorityTrait.php +++ b/apps/files_external/lib/Lib/PriorityTrait.php @@ -1,28 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\BackendService; /** * Trait to implement priority mechanics for a configuration class @@ -47,15 +32,4 @@ trait PriorityTrait { $this->priority = $priority; return $this; } - - /** - * @param PriorityTrait $a - * @param PriorityTrait $b - * @return int - */ - public static function priorityCompare(PriorityTrait $a, PriorityTrait $b) { - return ($a->getPriority() - $b->getPriority()); - } - } - diff --git a/apps/files_external/lib/Lib/SessionStorageWrapper.php b/apps/files_external/lib/Lib/SessionStorageWrapper.php index ba0f68b4e11..8754041b2fa 100644 --- a/apps/files_external/lib/Lib/SessionStorageWrapper.php +++ b/apps/files_external/lib/Lib/SessionStorageWrapper.php @@ -1,44 +1,25 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCP\Files\Storage; -use \OC\Files\Storage\Wrapper\PermissionsMask; -use \OCP\Constants; +use OC\Files\Storage\Wrapper\PermissionsMask; +use OCP\Constants; /** * Wrap Storage in PermissionsMask for session ephemeral use */ class SessionStorageWrapper extends PermissionsMask { - /** - * @param array $arguments ['storage' => $storage] + * @param array $parameters ['storage' => $storage] */ - public function __construct($arguments) { + public function __construct(array $parameters) { // disable sharing permission - $arguments['mask'] = Constants::PERMISSION_ALL & ~Constants::PERMISSION_SHARE; - parent::__construct($arguments); + $parameters['mask'] = Constants::PERMISSION_ALL & ~Constants::PERMISSION_SHARE; + parent::__construct($parameters); } - } - diff --git a/apps/files_external/lib/Lib/Storage/AmazonS3.php b/apps/files_external/lib/Lib/Storage/AmazonS3.php index 7c536443a0e..5dc9e114532 100644 --- a/apps/files_external/lib/Lib/Storage/AmazonS3.php +++ b/apps/files_external/lib/Lib/Storage/AmazonS3.php @@ -1,77 +1,68 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author André Gaul <gaul@web-yard.de> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christian Berendt <berendt@b1-systems.de> - * @author Christopher T. Johnson <ctjctj@gmail.com> - * @author Johan Björk <johanimon@gmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Martin Mattel <martin.mattel@diemattels.at> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.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_External\Lib\Storage; -use Aws\Result; -use Aws\S3\S3Client; use Aws\S3\Exception\S3Exception; use Icewind\Streams\CallbackWrapper; +use Icewind\Streams\CountWrapper; use Icewind\Streams\IteratorDirectory; -use OC\Cache\CappedMemoryCache; +use OC\Files\Cache\CacheEntry; use OC\Files\ObjectStore\S3ConnectionTrait; use OC\Files\ObjectStore\S3ObjectTrait; +use OC\Files\Storage\Common; +use OCP\Cache\CappedMemoryCache; use OCP\Constants; - -class AmazonS3 extends \OC\Files\Storage\Common { +use OCP\Files\FileInfo; +use OCP\Files\IMimeTypeDetector; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\ITempManager; +use OCP\Server; +use Psr\Log\LoggerInterface; + +class AmazonS3 extends Common { use S3ConnectionTrait; use S3ObjectTrait; - public function needsPartFile() { + private LoggerInterface $logger; + + public function needsPartFile(): bool { return false; } - /** - * @var int in seconds - */ - private $rescanDelay = 10; + /** @var CappedMemoryCache<array|false> */ + private CappedMemoryCache $objectCache; - /** @var CappedMemoryCache|Result[] */ - private $objectCache; + /** @var CappedMemoryCache<bool> */ + private CappedMemoryCache $directoryCache; - public function __construct($parameters) { + /** @var CappedMemoryCache<array> */ + private CappedMemoryCache $filesCache; + + private IMimeTypeDetector $mimeDetector; + private ?bool $versioningEnabled = null; + private ICache $memCache; + + public function __construct(array $parameters) { parent::__construct($parameters); $this->parseParams($parameters); + $this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']); $this->objectCache = new CappedMemoryCache(); + $this->directoryCache = new CappedMemoryCache(); + $this->filesCache = new CappedMemoryCache(); + $this->mimeDetector = Server::get(IMimeTypeDetector::class); + /** @var ICacheFactory $cacheFactory */ + $cacheFactory = Server::get(ICacheFactory::class); + $this->memCache = $cacheFactory->createLocal('s3-external'); + $this->logger = Server::get(LoggerInterface::class); } - /** - * @param string $path - * @return string correctly encoded path - */ - private function normalizePath($path) { + private function normalizePath(string $path): string { $path = trim($path, '/'); if (!$path) { @@ -81,43 +72,50 @@ class AmazonS3 extends \OC\Files\Storage\Common { return $path; } - private function isRoot($path) { + private function isRoot(string $path): bool { return $path === '.'; } - private function cleanKey($path) { + private function cleanKey(string $path): string { if ($this->isRoot($path)) { return '/'; } return $path; } - private function clearCache() { + private function clearCache(): void { $this->objectCache = new CappedMemoryCache(); + $this->directoryCache = new CappedMemoryCache(); + $this->filesCache = new CappedMemoryCache(); } - private function invalidateCache($key) { + private function invalidateCache(string $key): void { unset($this->objectCache[$key]); $keys = array_keys($this->objectCache->getData()); $keyLength = strlen($key); foreach ($keys as $existingKey) { - if (substr($existingKey, 0, $keyLength) === $keys) { + if (substr($existingKey, 0, $keyLength) === $key) { unset($this->objectCache[$existingKey]); } } + unset($this->filesCache[$key]); + $keys = array_keys($this->directoryCache->getData()); + $keyLength = strlen($key); + foreach ($keys as $existingKey) { + if (substr($existingKey, 0, $keyLength) === $key) { + unset($this->directoryCache[$existingKey]); + } + } + unset($this->directoryCache[$key]); } - /** - * @param $key - * @return Result|boolean - */ - private function headObject($key) { + private function headObject(string $key): array|false { if (!isset($this->objectCache[$key])) { try { - $this->objectCache[$key] = $this->getConnection()->headObject(array( + $this->objectCache[$key] = $this->getConnection()->headObject([ 'Bucket' => $this->bucket, 'Key' => $key - )); + ] + $this->getSSECParameters())->toArray(); } catch (S3Exception $e) { if ($e->getStatusCode() >= 500) { throw $e; @@ -126,59 +124,79 @@ class AmazonS3 extends \OC\Files\Storage\Common { } } + if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]['Key'])) { + /** @psalm-suppress InvalidArgument Psalm doesn't understand nested arrays well */ + $this->objectCache[$key]['Key'] = $key; + } return $this->objectCache[$key]; } /** - * Updates old storage ids (v0.2.1 and older) that are based on key and secret to new ones based on the bucket name. - * TODO Do this in an update.php. requires iterating over all users and loading the mount.json from their home + * Return true if directory exists + * + * There are no folders in s3. A folder like structure could be archived + * by prefixing files with the folder name. * - * @param array $params + * Implementation from flysystem-aws-s3-v3: + * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694 + * + * @throws \Exception */ - public function updateLegacyId(array $params) { - $oldId = 'amazon::' . $params['key'] . md5($params['secret']); + private function doesDirectoryExist(string $path): bool { + if ($path === '.' || $path === '') { + return true; + } + $path = rtrim($path, '/') . '/'; - // find by old id or bucket - $stmt = \OC::$server->getDatabaseConnection()->prepare( - 'SELECT `numeric_id`, `id` FROM `*PREFIX*storages` WHERE `id` IN (?, ?)' - ); - $stmt->execute(array($oldId, $this->id)); - while ($row = $stmt->fetch()) { - $storages[$row['id']] = $row['numeric_id']; + if (isset($this->directoryCache[$path])) { + return $this->directoryCache[$path]; } + try { + // Maybe this isn't an actual key, but a prefix. + // Do a prefix listing of objects to determine. + $result = $this->getConnection()->listObjectsV2([ + 'Bucket' => $this->bucket, + 'Prefix' => $path, + 'MaxKeys' => 1, + ]); + + if (isset($result['Contents'])) { + $this->directoryCache[$path] = true; + return true; + } - if (isset($storages[$this->id]) && isset($storages[$oldId])) { - // if both ids exist, delete the old storage and corresponding filecache entries - \OC\Files\Cache\Storage::remove($oldId); - } else if (isset($storages[$oldId])) { - // if only the old id exists do an update - $stmt = \OC::$server->getDatabaseConnection()->prepare( - 'UPDATE `*PREFIX*storages` SET `id` = ? WHERE `id` = ?' - ); - $stmt->execute(array($this->id, $oldId)); + // empty directories have their own object + $object = $this->headObject($path); + + if ($object) { + $this->directoryCache[$path] = true; + return true; + } + } catch (S3Exception $e) { + if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) { + $this->directoryCache[$path] = false; + } + throw $e; } - // only the bucket based id may exist, do nothing + + + $this->directoryCache[$path] = false; + return false; } - /** - * Remove a file or folder - * - * @param string $path - * @return bool - */ - protected function remove($path) { + protected function remove(string $path): bool { // remember fileType to reduce http calls $fileType = $this->filetype($path); if ($fileType === 'dir') { return $this->rmdir($path); - } else if ($fileType === 'file') { + } elseif ($fileType === 'file') { return $this->unlink($path); } else { return false; } } - public function mkdir($path) { + public function mkdir(string $path): bool { $path = $this->normalizePath($path); if ($this->is_dir($path)) { @@ -186,15 +204,18 @@ class AmazonS3 extends \OC\Files\Storage\Common { } try { - $this->getConnection()->putObject(array( + $this->getConnection()->putObject([ 'Bucket' => $this->bucket, 'Key' => $path . '/', 'Body' => '', - 'ContentType' => 'httpd/unix-directory' - )); + 'ContentType' => FileInfo::MIMETYPE_FOLDER + ] + $this->getSSECParameters()); $this->testTimeout(); } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } @@ -203,12 +224,12 @@ class AmazonS3 extends \OC\Files\Storage\Common { return true; } - public function file_exists($path) { + public function file_exists(string $path): bool { return $this->filetype($path) !== false; } - public function rmdir($path) { + public function rmdir(string $path): bool { $path = $this->normalizePath($path); if ($this->isRoot($path)) { @@ -223,128 +244,136 @@ class AmazonS3 extends \OC\Files\Storage\Common { return $this->batchDelete($path); } - protected function clearBucket() { + protected function clearBucket(): bool { $this->clearCache(); - try { - $this->getConnection()->clearBucket($this->bucket); - return true; - // clearBucket() is not working with Ceph, so if it fails we try the slower approach - } catch (\Exception $e) { - return $this->batchDelete(); - } - return false; + return $this->batchDelete(); } - private function batchDelete($path = null) { - $params = array( + private function batchDelete(?string $path = null): bool { + // TODO explore using https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.S3.BatchDelete.html + $params = [ 'Bucket' => $this->bucket - ); + ]; if ($path !== null) { $params['Prefix'] = $path . '/'; } try { + $connection = $this->getConnection(); // Since there are no real directories on S3, we need // to delete all objects prefixed with the path. do { // instead of the iterator, manually loop over the list ... - $objects = $this->getConnection()->listObjects($params); + $objects = $connection->listObjects($params); // ... so we can delete the files in batches - $this->getConnection()->deleteObjects(array( - 'Bucket' => $this->bucket, - 'Objects' => $objects['Contents'] - )); - $this->testTimeout(); + if (isset($objects['Contents'])) { + $connection->deleteObjects([ + 'Bucket' => $this->bucket, + 'Delete' => [ + 'Objects' => $objects['Contents'] + ] + ]); + $this->testTimeout(); + } // we reached the end when the list is no longer truncated } while ($objects['IsTruncated']); + if ($path !== '' && $path !== null) { + $this->deleteObject($path); + } } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } return true; } - public function opendir($path) { + public function opendir(string $path) { + try { + $content = iterator_to_array($this->getDirectoryContent($path)); + return IteratorDirectory::wrap(array_map(function (array $item) { + return $item['name']; + }, $content)); + } catch (S3Exception $e) { + return false; + } + } + + public function stat(string $path): array|false { $path = $this->normalizePath($path); - if ($this->isRoot($path)) { - $path = ''; + if ($this->is_dir($path)) { + $stat = $this->getDirectoryMetaData($path); } else { - $path .= '/'; + $object = $this->headObject($path); + if ($object === false) { + return false; + } + $stat = $this->objectToMetaData($object); } + $stat['atime'] = time(); - try { - $files = array(); - $results = $this->getConnection()->getPaginator('ListObjects', [ - 'Bucket' => $this->bucket, - 'Delimiter' => '/', - 'Prefix' => $path, - ]); + return $stat; + } - foreach ($results as $result) { - // sub folders - if (is_array($result['CommonPrefixes'])) { - foreach ($result['CommonPrefixes'] as $prefix) { - $files[] = substr(trim($prefix['Prefix'], '/'), strlen($path)); - } - } - foreach ($result['Contents'] as $object) { - if (isset($object['Key']) && $object['Key'] === $path) { - // it's the directory itself, skip - continue; - } - $file = basename( - isset($object['Key']) ? $object['Key'] : $object['Prefix'] - ); - $files[] = $file; - } - } + /** + * Return content length for object + * + * When the information is already present (e.g. opendir has been called before) + * this value is return. Otherwise a headObject is emitted. + */ + private function getContentLength(string $path): int { + if (isset($this->filesCache[$path])) { + return (int)$this->filesCache[$path]['ContentLength']; + } - return IteratorDirectory::wrap($files); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; + $result = $this->headObject($path); + if (isset($result['ContentLength'])) { + return (int)$result['ContentLength']; } + + return 0; } - public function stat($path) { - $path = $this->normalizePath($path); + /** + * Return last modified for object + * + * When the information is already present (e.g. opendir has been called before) + * this value is return. Otherwise a headObject is emitted. + */ + private function getLastModified(string $path): string { + if (isset($this->filesCache[$path])) { + return $this->filesCache[$path]['LastModified']; + } - try { - $stat = array(); - if ($this->is_dir($path)) { - //folders don't really exist - $stat['size'] = -1; //unknown - $stat['mtime'] = time() - $this->rescanDelay * 1000; - } else { - $result = $this->headObject($path); + $result = $this->headObject($path); + if (isset($result['LastModified'])) { + return $result['LastModified']; + } - $stat['size'] = $result['ContentLength'] ? $result['ContentLength'] : 0; - if (isset($result['Metadata']['lastmodified'])) { - $stat['mtime'] = strtotime($result['Metadata']['lastmodified']); - } else { - $stat['mtime'] = strtotime($result['LastModified']); - } - } - $stat['atime'] = time(); + return 'now'; + } - return $stat; - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + public function is_dir(string $path): bool { + $path = $this->normalizePath($path); + + if (isset($this->filesCache[$path])) { return false; } - } - public function is_dir($path) { - $path = $this->normalizePath($path); try { - return $this->isRoot($path) || $this->headObject($path . '/'); + return $this->doesDirectoryExist($path); } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } } - public function filetype($path) { + public function filetype(string $path): string|false { $path = $this->normalizePath($path); if ($this->isRoot($path)) { @@ -352,21 +381,27 @@ class AmazonS3 extends \OC\Files\Storage\Common { } try { - if ($this->headObject($path)) { + if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) { + return 'dir'; + } + if (isset($this->filesCache[$path]) || $this->headObject($path)) { return 'file'; } - if ($this->headObject($path . '/')) { + if ($this->doesDirectoryExist($path)) { return 'dir'; } } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } return false; } - public function getPermissions($path) { + public function getPermissions(string $path): int { $type = $this->filetype($path); if (!$type) { return 0; @@ -374,7 +409,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; } - public function unlink($path) { + public function unlink(string $path): bool { $path = $this->normalizePath($path); if ($this->is_dir($path)) { @@ -385,31 +420,43 @@ class AmazonS3 extends \OC\Files\Storage\Common { $this->deleteObject($path); $this->invalidateCache($path); } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } return true; } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $path = $this->normalizePath($path); switch ($mode) { case 'r': case 'rb': + // Don't try to fetch empty files + $stat = $this->stat($path); + if (is_array($stat) && isset($stat['size']) && $stat['size'] === 0) { + return fopen('php://memory', $mode); + } + try { return $this->readObject($path); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } case 'w': case 'wb': - $tmpFile = \OCP\Files::tmpFile(); + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile(); $handle = fopen($tmpFile, 'w'); - return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void { $this->writeBack($tmpFile, $path); }); case 'a': @@ -427,24 +474,21 @@ class AmazonS3 extends \OC\Files\Storage\Common { } else { $ext = ''; } - $tmpFile = \OCP\Files::tmpFile($ext); + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile($ext); if ($this->file_exists($path)) { $source = $this->readObject($path); file_put_contents($tmpFile, $source); } $handle = fopen($tmpFile, $mode); - return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void { $this->writeBack($tmpFile, $path); }); } return false; } - public function touch($path, $mtime = null) { - $path = $this->normalizePath($path); - - $metadata = array(); + public function touch(string $path, ?int $mtime = null): bool { if (is_null($mtime)) { $mtime = time(); } @@ -452,34 +496,26 @@ class AmazonS3 extends \OC\Files\Storage\Common { 'lastmodified' => gmdate(\DateTime::RFC1123, $mtime) ]; - $fileType = $this->filetype($path); try { - if ($fileType !== false) { - if ($fileType === 'dir' && !$this->isRoot($path)) { - $path .= '/'; - } - $this->getConnection()->copyObject([ - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey($path), - 'Metadata' => $metadata, - 'CopySource' => $this->bucket . '/' . $path, - 'MetadataDirective' => 'REPLACE', - ]); - $this->testTimeout(); - } else { - $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path); - $this->getConnection()->putObject([ - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey($path), - 'Metadata' => $metadata, - 'Body' => '', - 'ContentType' => $mimeType, - 'MetadataDirective' => 'REPLACE', - ]); - $this->testTimeout(); + if ($this->file_exists($path)) { + return false; } + + $mimeType = $this->mimeDetector->detectPath($path); + $this->getConnection()->putObject([ + 'Bucket' => $this->bucket, + 'Key' => $this->cleanKey($path), + 'Metadata' => $metadata, + 'Body' => '', + 'ContentType' => $mimeType, + 'MetadataDirective' => 'REPLACE', + ] + $this->getSSECParameters()); + $this->testTimeout(); } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } @@ -487,78 +523,69 @@ class AmazonS3 extends \OC\Files\Storage\Common { return true; } - public function copy($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); + public function copy(string $source, string $target, ?bool $isFile = null): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); - if ($this->is_file($path1)) { + if ($isFile === true || $this->is_file($source)) { try { - $this->getConnection()->copyObject(array( - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey($path2), - 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1) - )); + $this->copyObject($source, $target, [ + 'StorageClass' => $this->storageClass, + ]); $this->testTimeout(); } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } } else { - $this->remove($path2); + $this->remove($target); try { - $this->getConnection()->copyObject(array( - 'Bucket' => $this->bucket, - 'Key' => $path2 . '/', - 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1 . '/') - )); + $this->mkdir($target); $this->testTimeout(); } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } - $dh = $this->opendir($path1); - if (is_resource($dh)) { - while (($file = readdir($dh)) !== false) { - if (\OC\Files\Filesystem::isIgnoredDir($file)) { - continue; - } - - $source = $path1 . '/' . $file; - $target = $path2 . '/' . $file; - $this->copy($source, $target); - } + foreach ($this->getDirectoryContent($source) as $item) { + $childSource = $source . '/' . $item['name']; + $childTarget = $target . '/' . $item['name']; + $this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER); } } - $this->invalidateCache($path2); + $this->invalidateCache($target); return true; } - public function rename($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); + public function rename(string $source, string $target): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); - if ($this->is_file($path1)) { - - if ($this->copy($path1, $path2) === false) { + if ($this->is_file($source)) { + if ($this->copy($source, $target) === false) { return false; } - if ($this->unlink($path1) === false) { - $this->unlink($path2); + if ($this->unlink($source) === false) { + $this->unlink($target); return false; } } else { - - if ($this->copy($path1, $path2) === false) { + if ($this->copy($source, $target) === false) { return false; } - if ($this->rmdir($path1) === false) { - $this->rmdir($path2); + if ($this->rmdir($source) === false) { + $this->rmdir($target); return false; } } @@ -566,31 +593,30 @@ class AmazonS3 extends \OC\Files\Storage\Common { return true; } - public function test() { - $test = $this->getConnection()->getBucketAcl(array( - 'Bucket' => $this->bucket, - )); - if (isset($test) && !is_null($test->getPath('Owner/ID'))) { - return true; - } - return false; + public function test(): bool { + $this->getConnection()->headBucket([ + 'Bucket' => $this->bucket + ]); + return true; } - public function getId() { + public function getId(): string { return $this->id; } - public function writeBack($tmpFile, $path) { + public function writeBack(string $tmpFile, string $path): bool { try { $source = fopen($tmpFile, 'r'); - $this->writeObject($path, $source); + $this->writeObject($path, $source, $this->mimeDetector->detectPath($path)); $this->invalidateCache($path); - fclose($source); unlink($tmpFile); return true; } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } } @@ -598,8 +624,137 @@ class AmazonS3 extends \OC\Files\Storage\Common { /** * check if curl is installed */ - public static function checkDependencies() { + public static function checkDependencies(): bool { return true; } + public function getDirectoryContent(string $directory): \Traversable { + $path = $this->normalizePath($directory); + + if ($this->isRoot($path)) { + $path = ''; + } else { + $path .= '/'; + } + + $results = $this->getConnection()->getPaginator('ListObjectsV2', [ + 'Bucket' => $this->bucket, + 'Delimiter' => '/', + 'Prefix' => $path, + ]); + + foreach ($results as $result) { + // sub folders + if (is_array($result['CommonPrefixes'])) { + foreach ($result['CommonPrefixes'] as $prefix) { + $dir = $this->getDirectoryMetaData($prefix['Prefix']); + if ($dir) { + yield $dir; + } + } + } + if (is_array($result['Contents'])) { + foreach ($result['Contents'] as $object) { + $this->objectCache[$object['Key']] = $object; + if ($object['Key'] !== $path) { + yield $this->objectToMetaData($object); + } + } + } + } + } + + private function objectToMetaData(array $object): array { + return [ + 'name' => basename($object['Key']), + 'mimetype' => $this->mimeDetector->detectPath($object['Key']), + 'mtime' => strtotime($object['LastModified']), + 'storage_mtime' => strtotime($object['LastModified']), + 'etag' => trim($object['ETag'], '"'), + 'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, + 'size' => (int)($object['Size'] ?? $object['ContentLength']), + ]; + } + + private function getDirectoryMetaData(string $path): ?array { + $path = trim($path, '/'); + // when versioning is enabled, delete markers are returned as part of CommonPrefixes + // resulting in "ghost" folders, verify that each folder actually exists + if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) { + return null; + } + $cacheEntry = $this->getCache()->get($path); + if ($cacheEntry instanceof CacheEntry) { + return $cacheEntry->getData(); + } else { + return [ + 'name' => basename($path), + 'mimetype' => FileInfo::MIMETYPE_FOLDER, + 'mtime' => time(), + 'storage_mtime' => time(), + 'etag' => uniqid(), + 'permissions' => Constants::PERMISSION_ALL, + 'size' => -1, + ]; + } + } + + public function versioningEnabled(): bool { + if ($this->versioningEnabled === null) { + $cached = $this->memCache->get('versioning-enabled::' . $this->getBucket()); + if ($cached === null) { + $this->versioningEnabled = $this->getVersioningStatusFromBucket(); + $this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60); + } else { + $this->versioningEnabled = $cached; + } + } + return $this->versioningEnabled; + } + + protected function getVersioningStatusFromBucket(): bool { + try { + $result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]); + return $result->get('Status') === 'Enabled'; + } catch (S3Exception $s3Exception) { + // This is needed for compatibility with Storj gateway which does not support versioning yet + if ($s3Exception->getAwsErrorCode() === 'NotImplemented' || $s3Exception->getAwsErrorCode() === 'AccessDenied') { + return false; + } + throw $s3Exception; + } + } + + public function hasUpdated(string $path, int $time): bool { + // for files we can get the proper mtime + if ($path !== '' && $object = $this->headObject($path)) { + $stat = $this->objectToMetaData($object); + return $stat['mtime'] > $time; + } else { + // for directories, the only real option we have is to do a prefix listing and iterate over all objects + // however, since this is just as expensive as just re-scanning the directory, we can simply return true + // and have the scanner figure out if anything has actually changed + return true; + } + } + + public function writeStream(string $path, $stream, ?int $size = null): int { + if ($size === null) { + $size = 0; + // track the number of bytes read from the input stream to return as the number of written bytes. + $stream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size): void { + $size = $writtenSize; + }); + } + + if (!is_resource($stream)) { + throw new \InvalidArgumentException('Invalid stream provided'); + } + + $path = $this->normalizePath($path); + $this->writeObject($path, $stream, $this->mimeDetector->detectPath($path)); + $this->invalidateCache($path); + + return $size; + } } diff --git a/apps/files_external/lib/Lib/Storage/FTP.php b/apps/files_external/lib/Lib/Storage/FTP.php index 427a076ffe1..944964de7a6 100644 --- a/apps/files_external/lib/Lib/Storage/FTP.php +++ b/apps/files_external/lib/Lib/Storage/FTP.php @@ -1,159 +1,364 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Felix Moeller <mail@felixmoeller.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Senorsen <senorsen.zhang@gmail.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.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: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Storage; use Icewind\Streams\CallbackWrapper; -use Icewind\Streams\RetryWrapper; +use Icewind\Streams\CountWrapper; +use Icewind\Streams\IteratorDirectory; +use OC\Files\Storage\Common; +use OC\Files\Storage\PolyFill\CopyDirectory; +use OCP\Constants; +use OCP\Files\FileInfo; +use OCP\Files\IMimeTypeDetector; +use OCP\Files\StorageNotAvailableException; +use OCP\ITempManager; +use OCP\Server; +use Psr\Log\LoggerInterface; -class FTP extends StreamWrapper{ - private $password; - private $user; +class FTP extends Common { + use CopyDirectory; + + private $root; private $host; + private $password; + private $username; private $secure; - private $root; + private $port; + private $utf8Mode; - private static $tempFiles=array(); + /** @var FtpConnection|null */ + private $connection; - public function __construct($params) { - if (isset($params['host']) && isset($params['user']) && isset($params['password'])) { - $this->host=$params['host']; - $this->user=$params['user']; - $this->password=$params['password']; - if (isset($params['secure'])) { - $this->secure = $params['secure']; + public function __construct(array $parameters) { + if (isset($parameters['host']) && isset($parameters['user']) && isset($parameters['password'])) { + $this->host = $parameters['host']; + $this->username = $parameters['user']; + $this->password = $parameters['password']; + if (isset($parameters['secure'])) { + if (is_string($parameters['secure'])) { + $this->secure = ($parameters['secure'] === 'true'); + } else { + $this->secure = (bool)$parameters['secure']; + } } else { $this->secure = false; } - $this->root=isset($params['root'])?$params['root']:'/'; - if ( ! $this->root || $this->root[0]!=='/') { - $this->root='/'.$this->root; + $this->root = isset($parameters['root']) ? '/' . ltrim($parameters['root']) : '/'; + $this->port = $parameters['port'] ?? 21; + $this->utf8Mode = isset($parameters['utf8']) && $parameters['utf8']; + } else { + throw new \Exception('Creating ' . self::class . ' storage failed, required parameters not set'); + } + } + + public function __destruct() { + $this->connection = null; + } + + protected function getConnection(): FtpConnection { + if (!$this->connection) { + try { + $this->connection = new FtpConnection( + $this->secure, + $this->host, + $this->port, + $this->username, + $this->password + ); + } catch (\Exception $e) { + throw new StorageNotAvailableException('Failed to create ftp connection', 0, $e); } - if (substr($this->root, -1) !== '/') { - $this->root .= '/'; + if ($this->utf8Mode) { + if (!$this->connection->setUtf8Mode()) { + throw new StorageNotAvailableException('Could not set UTF-8 mode'); + } } + } + + return $this->connection; + } + + public function getId(): string { + return 'ftp::' . $this->username . '@' . $this->host . '/' . $this->root; + } + + protected function buildPath(string $path): string { + return rtrim($this->root . '/' . $path, '/'); + } + + public static function checkDependencies(): array|bool { + if (function_exists('ftp_login')) { + return true; } else { - throw new \Exception('Creating FTP storage failed'); + return ['ftp']; } - } - public function getId(){ - return 'ftp::' . $this->user . '@' . $this->host . '/' . $this->root; + public function filemtime(string $path): int|false { + $result = $this->getConnection()->mdtm($this->buildPath($path)); + + if ($result === -1) { + if ($this->is_dir($path)) { + $list = $this->getConnection()->mlsd($this->buildPath($path)); + if (!$list) { + Server::get(LoggerInterface::class)->warning("Unable to get last modified date for ftp folder ($path), failed to list folder contents"); + return time(); + } + $currentDir = current(array_filter($list, function ($item) { + return $item['type'] === 'cdir'; + })); + if ($currentDir) { + [$modify] = explode('.', $currentDir['modify'] ?? '', 2); + $time = \DateTime::createFromFormat('YmdHis', $modify); + if ($time === false) { + throw new \Exception("Invalid date format for directory: $currentDir"); + } + return $time->getTimestamp(); + } else { + Server::get(LoggerInterface::class)->warning("Unable to get last modified date for ftp folder ($path), folder contents doesn't include current folder"); + return time(); + } + } else { + return false; + } + } else { + return $result; + } } - /** - * construct the ftp url - * @param string $path - * @return string - */ - public function constructUrl($path) { - $url='ftp'; - if ($this->secure) { - $url.='s'; + public function filesize(string $path): false|int|float { + $result = $this->getConnection()->size($this->buildPath($path)); + if ($result === -1) { + return false; + } else { + return $result; } - $url.='://'.urlencode($this->user).':'.urlencode($this->password).'@'.$this->host.$this->root.$path; - return $url; } - /** - * Unlinks file or directory - * @param string $path - */ - public function unlink($path) { + public function rmdir(string $path): bool { if ($this->is_dir($path)) { - return $this->rmdir($path); + $result = $this->getConnection()->rmdir($this->buildPath($path)); + // recursive rmdir support depends on the ftp server + if ($result) { + return $result; + } else { + return $this->recursiveRmDir($path); + } + } elseif ($this->is_file($path)) { + return $this->unlink($path); + } else { + return false; } - else { - $url = $this->constructUrl($path); - $result = unlink($url); - clearstatcache(true, $url); - return $result; + } + + private function recursiveRmDir(string $path): bool { + $contents = $this->getDirectoryContent($path); + $result = true; + foreach ($contents as $content) { + if ($content['mimetype'] === FileInfo::MIMETYPE_FOLDER) { + $result = $result && $this->recursiveRmDir($path . '/' . $content['name']); + } else { + $result = $result && $this->getConnection()->delete($this->buildPath($path . '/' . $content['name'])); + } + } + $result = $result && $this->getConnection()->rmdir($this->buildPath($path)); + + return $result; + } + + public function test(): bool { + try { + return $this->getConnection()->systype() !== false; + } catch (\Exception $e) { + return false; + } + } + + public function stat(string $path): array|false { + if (!$this->file_exists($path)) { + return false; } + return [ + 'mtime' => $this->filemtime($path), + 'size' => $this->filesize($path), + ]; } - public function fopen($path,$mode) { - switch($mode) { + + public function file_exists(string $path): bool { + if ($path === '' || $path === '.' || $path === '/') { + return true; + } + return $this->filetype($path) !== false; + } + + public function unlink(string $path): bool { + switch ($this->filetype($path)) { + case 'dir': + return $this->rmdir($path); + case 'file': + return $this->getConnection()->delete($this->buildPath($path)); + default: + return false; + } + } + + public function opendir(string $path) { + $files = $this->getConnection()->nlist($this->buildPath($path)); + return IteratorDirectory::wrap($files); + } + + public function mkdir(string $path): bool { + if ($this->is_dir($path)) { + return false; + } + return $this->getConnection()->mkdir($this->buildPath($path)) !== false; + } + + public function is_dir(string $path): bool { + if ($path === '') { + return true; + } + if ($this->getConnection()->chdir($this->buildPath($path)) === true) { + $this->getConnection()->chdir('/'); + return true; + } else { + return false; + } + } + + public function is_file(string $path): bool { + return $this->filesize($path) !== false; + } + + public function filetype(string $path): string|false { + if ($this->is_dir($path)) { + return 'dir'; + } elseif ($this->is_file($path)) { + return 'file'; + } else { + return false; + } + } + + public function fopen(string $path, string $mode) { + $useExisting = true; + switch ($mode) { case 'r': case 'rb': + return $this->readStream($path); case 'w': + case 'w+': case 'wb': + case 'wb+': + $useExisting = false; + // no break case 'a': case 'ab': - //these are supported by the wrapper - $context = stream_context_create(array('ftp' => array('overwrite' => true))); - $handle = fopen($this->constructUrl($path), $mode, false, $context); - return RetryWrapper::wrap($handle); case 'r+': - case 'w+': - case 'wb+': case 'a+': case 'x': case 'x+': case 'c': case 'c+': //emulate these - if (strrpos($path, '.')!==false) { - $ext=substr($path, strrpos($path, '.')); + if ($useExisting and $this->file_exists($path)) { + if (!$this->isUpdatable($path)) { + return false; + } + $tmpFile = $this->getCachedFile($path); } else { - $ext=''; + if (!$this->isCreatable(dirname($path))) { + return false; + } + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile(); } - $tmpFile=\OCP\Files::tmpFile($ext); - if ($this->file_exists($path)) { - $this->getFile($path, $tmpFile); - } - $handle = fopen($tmpFile, $mode); - return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { - $this->writeBack($tmpFile, $path); + $source = fopen($tmpFile, $mode); + return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $path): void { + $this->writeStream($path, fopen($tmpFile, 'r')); + unlink($tmpFile); }); } return false; } - public function writeBack($tmpFile, $path) { - $this->uploadFile($tmpFile, $path); - unlink($tmpFile); + public function writeStream(string $path, $stream, ?int $size = null): int { + if ($size === null) { + $stream = CountWrapper::wrap($stream, function ($writtenSize) use (&$size): void { + $size = $writtenSize; + }); + } + + $this->getConnection()->fput($this->buildPath($path), $stream); + fclose($stream); + + return $size; } - /** - * check if php-ftp is installed - */ - public static function checkDependencies() { - if (function_exists('ftp_login')) { - return(true); + public function readStream(string $path) { + $stream = fopen('php://temp', 'w+'); + $result = $this->getConnection()->fget($stream, $this->buildPath($path)); + rewind($stream); + + if (!$result) { + fclose($stream); + return false; + } + return $stream; + } + + public function touch(string $path, ?int $mtime = null): bool { + if ($this->file_exists($path)) { + return false; } else { - return array('ftp'); + $this->file_put_contents($path, ''); + return true; } } + public function rename(string $source, string $target): bool { + $this->unlink($target); + return $this->getConnection()->rename($this->buildPath($source), $this->buildPath($target)); + } + + public function getDirectoryContent(string $directory): \Traversable { + $files = $this->getConnection()->mlsd($this->buildPath($directory)); + $mimeTypeDetector = Server::get(IMimeTypeDetector::class); + + foreach ($files as $file) { + $name = $file['name']; + if ($file['type'] === 'cdir' || $file['type'] === 'pdir') { + continue; + } + $permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; + $isDir = $file['type'] === 'dir'; + if ($isDir) { + $permissions += Constants::PERMISSION_CREATE; + } + + $data = []; + $data['mimetype'] = $isDir ? FileInfo::MIMETYPE_FOLDER : $mimeTypeDetector->detectPath($name); + + // strip fractional seconds + [$modify] = explode('.', $file['modify'], 2); + $mtime = \DateTime::createFromFormat('YmdGis', $modify); + $data['mtime'] = $mtime === false ? time() : $mtime->getTimestamp(); + if ($isDir) { + $data['size'] = -1; //unknown + } elseif (isset($file['size'])) { + $data['size'] = $file['size']; + } else { + $data['size'] = $this->filesize($directory . '/' . $name); + } + $data['etag'] = uniqid(); + $data['storage_mtime'] = $data['mtime']; + $data['permissions'] = $permissions; + $data['name'] = $name; + + yield $data; + } + } } diff --git a/apps/files_external/lib/Lib/Storage/FtpConnection.php b/apps/files_external/lib/Lib/Storage/FtpConnection.php new file mode 100644 index 00000000000..a064bf9b100 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/FtpConnection.php @@ -0,0 +1,222 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Lib\Storage; + +/** + * Low level wrapper around the ftp functions that smooths over some difference between servers + */ +class FtpConnection { + private \FTP\Connection $connection; + + public function __construct(bool $secure, string $hostname, int $port, string $username, string $password) { + if ($secure) { + $connection = ftp_ssl_connect($hostname, $port); + } else { + $connection = ftp_connect($hostname, $port); + } + + if ($connection === false) { + throw new \Exception('Failed to connect to ftp'); + } + + if (ftp_login($connection, $username, $password) === false) { + throw new \Exception('Failed to connect to login to ftp'); + } + + ftp_pasv($connection, true); + $this->connection = $connection; + } + + public function __destruct() { + ftp_close($this->connection); + } + + public function setUtf8Mode(): bool { + $response = ftp_raw($this->connection, 'OPTS UTF8 ON'); + return substr($response[0], 0, 3) === '200'; + } + + public function fput(string $path, $handle) { + return @ftp_fput($this->connection, $path, $handle, FTP_BINARY); + } + + public function fget($handle, string $path) { + return @ftp_fget($this->connection, $handle, $path, FTP_BINARY); + } + + public function mkdir(string $path) { + return @ftp_mkdir($this->connection, $path); + } + + public function chdir(string $path) { + return @ftp_chdir($this->connection, $path); + } + + public function delete(string $path) { + return @ftp_delete($this->connection, $path); + } + + public function rmdir(string $path) { + return @ftp_rmdir($this->connection, $path); + } + + public function rename(string $source, string $target) { + return @ftp_rename($this->connection, $source, $target); + } + + public function mdtm(string $path): int { + $result = @ftp_mdtm($this->connection, $path); + + // filezilla doesn't like empty path with mdtm + if ($result === -1 && $path === '') { + $result = @ftp_mdtm($this->connection, '/'); + } + return $result; + } + + public function size(string $path) { + return @ftp_size($this->connection, $path); + } + + public function systype() { + return @ftp_systype($this->connection); + } + + public function nlist(string $path) { + $files = @ftp_nlist($this->connection, $path); + return array_map(function ($name) { + if (str_contains($name, '/')) { + $name = basename($name); + } + return $name; + }, $files); + } + + public function mlsd(string $path) { + $files = @ftp_mlsd($this->connection, $path); + + if ($files !== false) { + return array_map(function ($file) { + if (str_contains($file['name'], '/')) { + $file['name'] = basename($file['name']); + } + return $file; + }, $files); + } else { + // not all servers support mlsd, in those cases we parse the raw list ourselves + $rawList = @ftp_rawlist($this->connection, '-aln ' . $path); + if ($rawList === false) { + return false; + } + return $this->parseRawList($rawList, $path); + } + } + + // rawlist parsing logic is based on the ftp implementation from https://github.com/thephpleague/flysystem + private function parseRawList(array $rawList, string $directory): array { + return array_map(function ($item) use ($directory) { + return $this->parseRawListItem($item, $directory); + }, $rawList); + } + + private function parseRawListItem(string $item, string $directory): array { + $isWindows = preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item); + + return $isWindows ? $this->parseWindowsItem($item, $directory) : $this->parseUnixItem($item, $directory); + } + + private function parseUnixItem(string $item, string $directory): array { + $item = preg_replace('#\s+#', ' ', $item, 7); + + if (count(explode(' ', $item, 9)) !== 9) { + throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); + } + + [$permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $time, $name] = explode(' ', $item, 9); + if ($name === '.') { + $type = 'cdir'; + } elseif ($name === '..') { + $type = 'pdir'; + } else { + $type = substr($permissions, 0, 1) === 'd' ? 'dir' : 'file'; + } + + $parsedDate = (new \DateTime()) + ->setTimestamp(strtotime("$month $day $time")); + $tomorrow = (new \DateTime())->add(new \DateInterval('P1D')); + + // since the provided date doesn't include the year, we either set it to the correct year + // or when the date would otherwise be in the future (by more then 1 day to account for timezone errors) + // we use last year + if ($parsedDate > $tomorrow) { + $parsedDate = $parsedDate->sub(new \DateInterval('P1Y')); + } + + $formattedDate = $parsedDate + ->format('YmdHis'); + + return [ + 'type' => $type, + 'name' => $name, + 'modify' => $formattedDate, + 'perm' => $this->normalizePermissions($permissions), + 'size' => (int)$size, + ]; + } + + private function normalizePermissions(string $permissions) { + $isDir = substr($permissions, 0, 1) === 'd'; + // remove the type identifier and only use owner permissions + $permissions = substr($permissions, 1, 4); + + // map the string rights to the ftp counterparts + $filePermissionsMap = ['r' => 'r', 'w' => 'fadfw']; + $dirPermissionsMap = ['r' => 'e', 'w' => 'flcdmp']; + + $map = $isDir ? $dirPermissionsMap : $filePermissionsMap; + + return array_reduce(str_split($permissions), function ($ftpPermissions, $permission) use ($map) { + if (isset($map[$permission])) { + $ftpPermissions .= $map[$permission]; + } + return $ftpPermissions; + }, ''); + } + + private function parseWindowsItem(string $item, string $directory): array { + $item = preg_replace('#\s+#', ' ', trim($item), 3); + + if (count(explode(' ', $item, 4)) !== 4) { + throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); + } + + [$date, $time, $size, $name] = explode(' ', $item, 4); + + // Check for the correct date/time format + $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i'; + $formattedDate = \DateTime::createFromFormat($format, $date . $time)->format('YmdGis'); + + if ($name === '.') { + $type = 'cdir'; + } elseif ($name === '..') { + $type = 'pdir'; + } else { + $type = ($size === '<DIR>') ? 'dir' : 'file'; + } + + return [ + 'type' => $type, + 'name' => $name, + 'modify' => $formattedDate, + 'perm' => ($type === 'file') ? 'adfrw' : 'flcdmpe', + 'size' => (int)$size, + ]; + } +} diff --git a/apps/files_external/lib/Lib/Storage/OwnCloud.php b/apps/files_external/lib/Lib/Storage/OwnCloud.php index d846fa811a0..12c305de750 100644 --- a/apps/files_external/lib/Lib/Storage/OwnCloud.php +++ b/apps/files_external/lib/Lib/Storage/OwnCloud.php @@ -1,30 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Vincent Petry <pvince81@owncloud.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Storage; + +use OC\Files\Storage\DAV; +use OCP\Files\Storage\IDisableEncryptionStorage; use Sabre\DAV\Client; /** @@ -34,46 +18,46 @@ use Sabre\DAV\Client; * http://%host/%context/remote.php/webdav/%root * */ -class OwnCloud extends \OC\Files\Storage\DAV{ - const OC_URL_SUFFIX = 'remote.php/webdav'; +class OwnCloud extends DAV implements IDisableEncryptionStorage { + public const OC_URL_SUFFIX = 'remote.php/webdav'; - public function __construct($params) { + public function __construct(array $parameters) { // extract context path from host if specified // (owncloud install path on host) - $host = $params['host']; + $host = $parameters['host']; // strip protocol - if (substr($host, 0, 8) === "https://") { + if (substr($host, 0, 8) === 'https://') { $host = substr($host, 8); - $params['secure'] = true; - } else if (substr($host, 0, 7) === "http://") { + $parameters['secure'] = true; + } elseif (substr($host, 0, 7) === 'http://') { $host = substr($host, 7); - $params['secure'] = false; + $parameters['secure'] = false; } $contextPath = ''; $hostSlashPos = strpos($host, '/'); - if ($hostSlashPos !== false){ + if ($hostSlashPos !== false) { $contextPath = substr($host, $hostSlashPos); $host = substr($host, 0, $hostSlashPos); } - if (substr($contextPath, -1) !== '/'){ + if (!str_ends_with($contextPath, '/')) { $contextPath .= '/'; } - if (isset($params['root'])){ - $root = $params['root']; - if (substr($root, 0, 1) !== '/'){ - $root = '/' . $root; - } - } - else{ + if (isset($parameters['root'])) { + $root = '/' . ltrim($parameters['root'], '/'); + } else { $root = '/'; } - $params['host'] = $host; - $params['root'] = $contextPath . self::OC_URL_SUFFIX . $root; - $params['authType'] = Client::AUTH_BASIC; + $parameters['host'] = $host; + $parameters['root'] = $contextPath . self::OC_URL_SUFFIX . $root; + $parameters['authType'] = Client::AUTH_BASIC; + + parent::__construct($parameters); + } - parent::__construct($params); + public function needsPartFile(): bool { + return false; } } diff --git a/apps/files_external/lib/Lib/Storage/SFTP.php b/apps/files_external/lib/Lib/Storage/SFTP.php index 22162fa61cf..a2f5bafcca1 100644 --- a/apps/files_external/lib/Lib/Storage/SFTP.php +++ b/apps/files_external/lib/Lib/Storage/SFTP.php @@ -1,115 +1,102 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author hkjolhede <hkjolhede@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lennart Rosam <lennart.rosam@medien-systempartner.de> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Ross Nicoll <jrn@jrn.me.uk> - * @author SA <stephen@mthosting.net> - * @author Senorsen <senorsen.zhang@gmail.com> - * @author Vincent Petry <pvince81@owncloud.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_External\Lib\Storage; + +use Icewind\Streams\CallbackWrapper; +use Icewind\Streams\CountWrapper; use Icewind\Streams\IteratorDirectory; use Icewind\Streams\RetryWrapper; +use OC\Files\Storage\Common; +use OC\Files\View; +use OCP\Cache\CappedMemoryCache; +use OCP\Constants; +use OCP\Files\FileInfo; +use OCP\Files\IMimeTypeDetector; +use OCP\Server; use phpseclib\Net\SFTP\Stream; /** -* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to -* provide access to SFTP servers. -*/ -class SFTP extends \OC\Files\Storage\Common { + * Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to + * provide access to SFTP servers. + */ +class SFTP extends Common { private $host; private $user; private $root; private $port = 22; - private $auth; + private $auth = []; /** * @var \phpseclib\Net\SFTP */ protected $client; + private CappedMemoryCache $knownMTimes; + + private IMimeTypeDetector $mimeTypeDetector; + + public const COPY_CHUNK_SIZE = 8 * 1024 * 1024; /** * @param string $host protocol://server:port * @return array [$server, $port] */ - private function splitHost($host) { + private function splitHost(string $host): array { $input = $host; - if (strpos($host, '://') === false) { + if (!str_contains($host, '://')) { // add a protocol to fix parse_url behavior with ipv6 $host = 'http://' . $host; } $parsed = parse_url($host); - if(is_array($parsed) && isset($parsed['port'])) { + if (is_array($parsed) && isset($parsed['port'])) { return [$parsed['host'], $parsed['port']]; - } else if (is_array($parsed)) { + } elseif (is_array($parsed)) { return [$parsed['host'], 22]; } else { return [$input, 22]; } } - /** - * {@inheritdoc} - */ - public function __construct($params) { + public function __construct(array $parameters) { // Register sftp:// Stream::register(); - $parsedHost = $this->splitHost($params['host']); + $parsedHost = $this->splitHost($parameters['host']); $this->host = $parsedHost[0]; $this->port = $parsedHost[1]; - if (!isset($params['user'])) { + if (!isset($parameters['user'])) { throw new \UnexpectedValueException('no authentication parameters specified'); } - $this->user = $params['user']; + $this->user = $parameters['user']; - if (isset($params['public_key_auth'])) { - $this->auth = $params['public_key_auth']; - } elseif (isset($params['password'])) { - $this->auth = $params['password']; - } else { + if (isset($parameters['public_key_auth'])) { + $this->auth[] = $parameters['public_key_auth']; + } + if (isset($parameters['password']) && $parameters['password'] !== '') { + $this->auth[] = $parameters['password']; + } + + if ($this->auth === []) { throw new \UnexpectedValueException('no authentication parameters specified'); } $this->root - = isset($params['root']) ? $this->cleanPath($params['root']) : '/'; + = isset($parameters['root']) ? $this->cleanPath($parameters['root']) : '/'; - if ($this->root[0] !== '/') { - $this->root = '/' . $this->root; - } + $this->root = '/' . ltrim($this->root, '/'); + $this->root = rtrim($this->root, '/') . '/'; - if (substr($this->root, -1, 1) !== '/') { - $this->root .= '/'; - } + $this->knownMTimes = new CappedMemoryCache(); + + $this->mimeTypeDetector = Server::get(IMimeTypeDetector::class); } /** @@ -118,7 +105,7 @@ class SFTP extends \OC\Files\Storage\Common { * @return \phpseclib\Net\SFTP connected client instance * @throws \Exception when the connection failed */ - public function getConnection() { + public function getConnection(): \phpseclib\Net\SFTP { if (!is_null($this->client)) { return $this->client; } @@ -137,16 +124,22 @@ class SFTP extends \OC\Files\Storage\Common { $this->writeHostKeys($hostKeys); } - if (!$this->client->login($this->user, $this->auth)) { + $login = false; + foreach ($this->auth as $auth) { + /** @psalm-suppress TooManyArguments */ + $login = $this->client->login($this->user, $auth); + if ($login === true) { + break; + } + } + + if ($login === false) { throw new \Exception('Login failed'); } return $this->client; } - /** - * {@inheritdoc} - */ - public function test() { + public function test(): bool { if ( !isset($this->host) || !isset($this->user) @@ -156,10 +149,7 @@ class SFTP extends \OC\Files\Storage\Common { return $this->getConnection()->nlist() !== false; } - /** - * {@inheritdoc} - */ - public function getId(){ + public function getId(): string { $id = 'sftp::' . $this->user . '@' . $this->host; if ($this->port !== 22) { $id .= ':' . $this->port; @@ -171,56 +161,38 @@ class SFTP extends \OC\Files\Storage\Common { return $id; } - /** - * @return string - */ - public function getHost() { + public function getHost(): string { return $this->host; } - /** - * @return string - */ - public function getRoot() { + public function getRoot(): string { return $this->root; } - /** - * @return mixed - */ - public function getUser() { + public function getUser(): string { return $this->user; } - /** - * @param string $path - * @return string - */ - private function absPath($path) { + private function absPath(string $path): string { return $this->root . $this->cleanPath($path); } - /** - * @return string|false - */ - private function hostKeysPath() { + private function hostKeysPath(): string|false { try { - $storage_view = \OCP\Files::getStorage('files_external'); - if ($storage_view) { - return \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . - $storage_view->getAbsolutePath('') . - 'ssh_hostKeys'; + $userId = \OC_User::getUser(); + if ($userId === false) { + return false; } + + $view = new View('/' . $userId . '/files_external'); + + return $view->getLocalFile('ssh_hostKeys'); } catch (\Exception $e) { } return false; } - /** - * @param $keys - * @return bool - */ - protected function writeHostKeys($keys) { + protected function writeHostKeys(array $keys): bool { try { $keyPath = $this->hostKeysPath(); if ($keyPath && file_exists($keyPath)) { @@ -236,19 +208,16 @@ class SFTP extends \OC\Files\Storage\Common { return false; } - /** - * @return array - */ - protected function readHostKeys() { + protected function readHostKeys(): array { try { $keyPath = $this->hostKeysPath(); if (file_exists($keyPath)) { - $hosts = array(); - $keys = array(); + $hosts = []; + $keys = []; $lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($lines) { foreach ($lines as $line) { - $hostKeyArray = explode("::", $line, 2); + $hostKeyArray = explode('::', $line, 2); if (count($hostKeyArray) === 2) { $hosts[] = $hostKeyArray[0]; $keys[] = $hostKeyArray[1]; @@ -259,13 +228,10 @@ class SFTP extends \OC\Files\Storage\Common { } } catch (\Exception $e) { } - return array(); + return []; } - /** - * {@inheritdoc} - */ - public function mkdir($path) { + public function mkdir(string $path): bool { try { return $this->getConnection()->mkdir($this->absPath($path)); } catch (\Exception $e) { @@ -273,10 +239,7 @@ class SFTP extends \OC\Files\Storage\Common { } } - /** - * {@inheritdoc} - */ - public function rmdir($path) { + public function rmdir(string $path): bool { try { $result = $this->getConnection()->delete($this->absPath($path), true); // workaround: stray stat cache entry when deleting empty folders @@ -288,10 +251,7 @@ class SFTP extends \OC\Files\Storage\Common { } } - /** - * {@inheritdoc} - */ - public function opendir($path) { + public function opendir(string $path) { try { $list = $this->getConnection()->nlist($this->absPath($path)); if ($list === false) { @@ -299,41 +259,37 @@ class SFTP extends \OC\Files\Storage\Common { } $id = md5('sftp:' . $path); - $dirStream = array(); - foreach($list as $file) { + $dirStream = []; + foreach ($list as $file) { if ($file !== '.' && $file !== '..') { $dirStream[] = $file; } } return IteratorDirectory::wrap($dirStream); - } catch(\Exception $e) { + } catch (\Exception $e) { return false; } } - /** - * {@inheritdoc} - */ - public function filetype($path) { + public function filetype(string $path): string|false { try { $stat = $this->getConnection()->stat($this->absPath($path)); - if ((int) $stat['type'] === NET_SFTP_TYPE_REGULAR) { + if (!is_array($stat) || !array_key_exists('type', $stat)) { + return false; + } + if ((int)$stat['type'] === NET_SFTP_TYPE_REGULAR) { return 'file'; } - if ((int) $stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + if ((int)$stat['type'] === NET_SFTP_TYPE_DIRECTORY) { return 'dir'; } } catch (\Exception $e) { - } return false; } - /** - * {@inheritdoc} - */ - public function file_exists($path) { + public function file_exists(string $path): bool { try { return $this->getConnection()->stat($this->absPath($path)) !== false; } catch (\Exception $e) { @@ -341,10 +297,7 @@ class SFTP extends \OC\Files\Storage\Common { } } - /** - * {@inheritdoc} - */ - public function unlink($path) { + public function unlink(string $path): bool { try { return $this->getConnection()->delete($this->absPath($path), true); } catch (\Exception $e) { @@ -352,20 +305,35 @@ class SFTP extends \OC\Files\Storage\Common { } } - /** - * {@inheritdoc} - */ - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { + $path = $this->cleanPath($path); try { $absPath = $this->absPath($path); - switch($mode) { + $connection = $this->getConnection(); + switch ($mode) { case 'r': case 'rb': - if ( !$this->file_exists($path)) { + $stat = $this->stat($path); + if (!$stat) { return false; } + SFTPReadStream::register(); + $context = stream_context_create(['sftp' => ['session' => $connection, 'size' => $stat['size']]]); + $handle = fopen('sftpread://' . trim($absPath, '/'), 'r', false, $context); + return RetryWrapper::wrap($handle); case 'w': case 'wb': + SFTPWriteStream::register(); + // the SFTPWriteStream doesn't go through the "normal" methods so it doesn't clear the stat cache. + $connection->_remove_from_stat_cache($absPath); + $context = stream_context_create(['sftp' => ['session' => $connection]]); + $fh = fopen('sftpwrite://' . trim($absPath, '/'), 'w', false, $context); + if ($fh) { + $fh = CallbackWrapper::wrap($fh, null, null, function () use ($path): void { + $this->knownMTimes->set($path, time()); + }); + } + return $fh; case 'a': case 'ab': case 'r+': @@ -376,7 +344,7 @@ class SFTP extends \OC\Files\Storage\Common { case 'x+': case 'c': case 'c+': - $context = stream_context_create(array('sftp' => array('session' => $this->getConnection()))); + $context = stream_context_create(['sftp' => ['session' => $connection]]); $handle = fopen($this->constructUrl($path), $mode, false, $context); return RetryWrapper::wrap($handle); } @@ -385,47 +353,29 @@ class SFTP extends \OC\Files\Storage\Common { return false; } - /** - * {@inheritdoc} - */ - public function touch($path, $mtime=null) { + public function touch(string $path, ?int $mtime = null): bool { try { if (!is_null($mtime)) { return false; } if (!$this->file_exists($path)) { - $this->getConnection()->put($this->absPath($path), ''); + return $this->getConnection()->put($this->absPath($path), ''); } else { return false; } } catch (\Exception $e) { return false; } - return true; } /** - * @param string $path - * @param string $target * @throws \Exception */ - public function getFile($path, $target) { + public function getFile(string $path, string $target): void { $this->getConnection()->get($path, $target); } - /** - * @param string $path - * @param string $target - * @throws \Exception - */ - public function uploadFile($path, $target) { - $this->getConnection()->put($target, $path, NET_SFTP_LOCAL_FILE); - } - - /** - * {@inheritdoc} - */ - public function rename($source, $target) { + public function rename(string $source, string $target): bool { try { if ($this->file_exists($target)) { $this->unlink($target); @@ -440,30 +390,134 @@ class SFTP extends \OC\Files\Storage\Common { } /** - * {@inheritdoc} + * @return array{mtime: int, size: int, ctime: int}|false */ - public function stat($path) { + public function stat(string $path): array|false { try { + $path = $this->cleanPath($path); $stat = $this->getConnection()->stat($this->absPath($path)); - $mtime = $stat ? $stat['mtime'] : -1; - $size = $stat ? $stat['size'] : 0; + $mtime = isset($stat['mtime']) ? (int)$stat['mtime'] : -1; + $size = isset($stat['size']) ? (int)$stat['size'] : 0; + + // the mtime can't be less than when we last touched it + if ($knownMTime = $this->knownMTimes->get($path)) { + $mtime = max($mtime, $knownMTime); + } - return array('mtime' => $mtime, 'size' => $size, 'ctime' => -1); + return [ + 'mtime' => $mtime, + 'size' => $size, + 'ctime' => -1 + ]; } catch (\Exception $e) { return false; } } - /** - * @param string $path - * @return string - */ - public function constructUrl($path) { + public function constructUrl(string $path): string { // Do not pass the password here. We want to use the Net_SFTP object // supplied via stream context or fail. We only supply username and // hostname because this might show up in logs (they are not used). $url = 'sftp://' . urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path; return $url; } + + public function file_put_contents(string $path, mixed $data): int|float|false { + /** @psalm-suppress InternalMethod */ + $result = $this->getConnection()->put($this->absPath($path), $data); + if ($result) { + return strlen($data); + } else { + return false; + } + } + + public function writeStream(string $path, $stream, ?int $size = null): int { + if ($size === null) { + $stream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size): void { + $size = $writtenSize; + }); + if (!$stream) { + throw new \Exception('Failed to wrap stream'); + } + } + /** @psalm-suppress InternalMethod */ + $result = $this->getConnection()->put($this->absPath($path), $stream); + fclose($stream); + if ($result) { + if ($size === null) { + throw new \Exception('Failed to get written size from sftp storage wrapper'); + } + return $size; + } else { + throw new \Exception('Failed to write steam to sftp storage'); + } + } + + public function copy(string $source, string $target): bool { + if ($this->is_dir($source) || $this->is_dir($target)) { + return parent::copy($source, $target); + } else { + $absSource = $this->absPath($source); + $absTarget = $this->absPath($target); + + $connection = $this->getConnection(); + $size = $connection->size($absSource); + if ($size === false) { + return false; + } + for ($i = 0; $i < $size; $i += self::COPY_CHUNK_SIZE) { + /** @psalm-suppress InvalidArgument */ + $chunk = $connection->get($absSource, false, $i, self::COPY_CHUNK_SIZE); + if ($chunk === false) { + return false; + } + /** @psalm-suppress InternalMethod */ + if (!$connection->put($absTarget, $chunk, \phpseclib\Net\SFTP::SOURCE_STRING, $i)) { + return false; + } + } + return true; + } + } + + public function getPermissions(string $path): int { + $stat = $this->getConnection()->stat($this->absPath($path)); + if (!$stat) { + return 0; + } + if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + return Constants::PERMISSION_ALL; + } else { + return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; + } + } + + public function getMetaData(string $path): ?array { + $stat = $this->getConnection()->stat($this->absPath($path)); + if (!$stat) { + return null; + } + + if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + $stat['permissions'] = Constants::PERMISSION_ALL; + } else { + $stat['permissions'] = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; + } + + if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + $stat['size'] = -1; + $stat['mimetype'] = FileInfo::MIMETYPE_FOLDER; + } else { + $stat['mimetype'] = $this->mimeTypeDetector->detectPath($path); + } + + $stat['etag'] = $this->getETag($path); + $stat['storage_mtime'] = $stat['mtime']; + $stat['name'] = basename($path); + + $keys = ['size', 'mtime', 'mimetype', 'etag', 'storage_mtime', 'permissions', 'name']; + return array_intersect_key($stat, array_flip($keys)); + } } diff --git a/apps/files_external/lib/Lib/Storage/SFTPReadStream.php b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php new file mode 100644 index 00000000000..7dedbd7035a --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php @@ -0,0 +1,217 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Storage; + +use Icewind\Streams\File; +use phpseclib\Net\SSH2; + +class SFTPReadStream implements File { + /** @var resource */ + public $context; + + /** @var \phpseclib\Net\SFTP */ + private $sftp; + + /** @var string */ + private $handle; + + /** @var int */ + private $internalPosition = 0; + + /** @var int */ + private $readPosition = 0; + + /** @var bool */ + private $eof = false; + + private $buffer = ''; + private bool $pendingRead = false; + private int $size = 0; + + public static function register($protocol = 'sftpread') { + if (in_array($protocol, stream_get_wrappers(), true)) { + return false; + } + return stream_wrapper_register($protocol, get_called_class()); + } + + /** + * Load the source from the stream context and return the context options + * + * @throws \BadMethodCallException + */ + protected function loadContext(string $name) { + $context = stream_context_get_options($this->context); + if (isset($context[$name])) { + $context = $context[$name]; + } else { + throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); + } + if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) { + $this->sftp = $context['session']; + } else { + throw new \BadMethodCallException('Invalid context, session not set'); + } + if (isset($context['size'])) { + $this->size = $context['size']; + } + return $context; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + [, $path] = explode('://', $path); + $path = '/' . ltrim($path); + $path = str_replace('//', '/', $path); + + $this->loadContext('sftp'); + + if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) { + return false; + } + + $remote_file = $this->sftp->_realpath($path); + if ($remote_file === false) { + return false; + } + + $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0); + if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->sftp->_get_sftp_packet(); + switch ($this->sftp->packet_type) { + case NET_SFTP_HANDLE: + $this->handle = substr($response, 4); + break; + case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + $this->sftp->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + $this->request_chunk(256 * 1024); + + return true; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + switch ($whence) { + case SEEK_SET: + $this->seekTo($offset); + break; + case SEEK_CUR: + $this->seekTo($this->readPosition + $offset); + break; + case SEEK_END: + $this->seekTo($this->size + $offset); + break; + } + return true; + } + + private function seekTo(int $offset): void { + $this->internalPosition = $offset; + $this->readPosition = $offset; + $this->buffer = ''; + $this->request_chunk(256 * 1024); + } + + public function stream_tell() { + return $this->readPosition; + } + + public function stream_read($count) { + if (!$this->eof && strlen($this->buffer) < $count) { + $chunk = $this->read_chunk(); + $this->buffer .= $chunk; + if (!$this->eof) { + $this->request_chunk(256 * 1024); + } + } + + $data = substr($this->buffer, 0, $count); + $this->buffer = substr($this->buffer, $count); + $this->readPosition += strlen($data); + + return $data; + } + + private function request_chunk(int $size) { + if ($this->pendingRead) { + $this->sftp->_get_sftp_packet(); + } + + $packet = pack('Na*N3', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size); + $this->pendingRead = true; + return $this->sftp->_send_sftp_packet(NET_SFTP_READ, $packet); + } + + private function read_chunk() { + $this->pendingRead = false; + $response = $this->sftp->_get_sftp_packet(); + + switch ($this->sftp->packet_type) { + case NET_SFTP_DATA: + $temp = substr($response, 4); + $len = strlen($temp); + $this->internalPosition += $len; + return $temp; + case NET_SFTP_STATUS: + [1 => $status] = unpack('N', substr($response, 0, 4)); + if ($status == NET_SFTP_STATUS_EOF) { + $this->eof = true; + } + return ''; + default: + return ''; + } + } + + public function stream_write($data) { + return false; + } + + public function stream_set_option($option, $arg1, $arg2) { + return false; + } + + public function stream_truncate($size) { + return false; + } + + public function stream_stat() { + return false; + } + + public function stream_lock($operation) { + return false; + } + + public function stream_flush() { + return false; + } + + public function stream_eof() { + return $this->eof; + } + + public function stream_close() { + // we still have a read request incoming that needs to be handled before we can close + if ($this->pendingRead) { + $this->sftp->_get_sftp_packet(); + } + if (!$this->sftp->_close_handle($this->handle)) { + return false; + } + return true; + } +} diff --git a/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php new file mode 100644 index 00000000000..d64e89b5462 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php @@ -0,0 +1,165 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Storage; + +use Icewind\Streams\File; +use phpseclib\Net\SSH2; + +class SFTPWriteStream implements File { + /** @var resource */ + public $context; + + /** @var \phpseclib\Net\SFTP */ + private $sftp; + + /** @var string */ + private $handle; + + /** @var int */ + private $internalPosition = 0; + + /** @var int */ + private $writePosition = 0; + + /** @var bool */ + private $eof = false; + + private $buffer = ''; + + public static function register($protocol = 'sftpwrite') { + if (in_array($protocol, stream_get_wrappers(), true)) { + return false; + } + return stream_wrapper_register($protocol, get_called_class()); + } + + /** + * Load the source from the stream context and return the context options + * + * @throws \BadMethodCallException + */ + protected function loadContext(string $name) { + $context = stream_context_get_options($this->context); + if (isset($context[$name])) { + $context = $context[$name]; + } else { + throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); + } + if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) { + $this->sftp = $context['session']; + } else { + throw new \BadMethodCallException('Invalid context, session not set'); + } + return $context; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + [, $path] = explode('://', $path); + $path = '/' . ltrim($path); + $path = str_replace('//', '/', $path); + + $this->loadContext('sftp'); + + if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) { + return false; + } + + $remote_file = $this->sftp->_realpath($path); + if ($remote_file === false) { + return false; + } + + $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_TRUNCATE, 0); + if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->sftp->_get_sftp_packet(); + switch ($this->sftp->packet_type) { + case NET_SFTP_HANDLE: + $this->handle = substr($response, 4); + break; + case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + $this->sftp->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + return true; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + return false; + } + + public function stream_tell() { + return $this->writePosition; + } + + public function stream_read($count) { + return false; + } + + public function stream_write($data) { + $written = strlen($data); + $this->writePosition += $written; + + $this->buffer .= $data; + + if (strlen($this->buffer) > 64 * 1024) { + if (!$this->stream_flush()) { + return false; + } + } + + return $written; + } + + public function stream_set_option($option, $arg1, $arg2) { + return false; + } + + public function stream_truncate($size) { + return false; + } + + public function stream_stat() { + return false; + } + + public function stream_lock($operation) { + return false; + } + + public function stream_flush() { + $size = strlen($this->buffer); + $packet = pack('Na*N3a*', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size, $this->buffer); + if (!$this->sftp->_send_sftp_packet(NET_SFTP_WRITE, $packet)) { + return false; + } + $this->internalPosition += $size; + $this->buffer = ''; + + return $this->sftp->_read_put_responses(1); + } + + public function stream_eof() { + return $this->eof; + } + + public function stream_close() { + $this->stream_flush(); + if (!$this->sftp->_close_handle($this->handle)) { + return false; + } + return true; + } +} diff --git a/apps/files_external/lib/Lib/Storage/SMB.php b/apps/files_external/lib/Lib/Storage/SMB.php index d8bbe8c4718..8f8750864e1 100644 --- a/apps/files_external/lib/Lib/Storage/SMB.php +++ b/apps/files_external/lib/Lib/Storage/SMB.php @@ -1,67 +1,55 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Jesús Macias <jmacias@solidgear.es> - * @author Joas Schilling <coding@schilljs.com> - * @author Juan Pablo Villafañez <jvillafanez@solidgear.es> - * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.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_External\Lib\Storage; +use Icewind\SMB\ACL; +use Icewind\SMB\BasicAuth; use Icewind\SMB\Exception\AlreadyExistsException; use Icewind\SMB\Exception\ConnectException; use Icewind\SMB\Exception\Exception; use Icewind\SMB\Exception\ForbiddenException; +use Icewind\SMB\Exception\InvalidArgumentException; +use Icewind\SMB\Exception\InvalidTypeException; use Icewind\SMB\Exception\NotFoundException; +use Icewind\SMB\Exception\OutOfSpaceException; +use Icewind\SMB\Exception\TimedOutException; use Icewind\SMB\IFileInfo; -use Icewind\SMB\NativeServer; -use Icewind\SMB\Server; +use Icewind\SMB\Native\NativeServer; +use Icewind\SMB\Options; +use Icewind\SMB\ServerFactory; +use Icewind\SMB\Wrapped\Server; use Icewind\Streams\CallbackWrapper; use Icewind\Streams\IteratorDirectory; -use OC\Cache\CappedMemoryCache; use OC\Files\Filesystem; use OC\Files\Storage\Common; use OCA\Files_External\Lib\Notify\SMBNotifyHandler; +use OCP\Cache\CappedMemoryCache; +use OCP\Constants; +use OCP\Files\EntityTooLargeException; +use OCP\Files\IMimeTypeDetector; use OCP\Files\Notify\IChange; use OCP\Files\Notify\IRenameChange; +use OCP\Files\NotPermittedException; use OCP\Files\Storage\INotifyStorage; +use OCP\Files\StorageAuthException; use OCP\Files\StorageNotAvailableException; -use OCP\Util; +use OCP\ITempManager; +use Psr\Log\LoggerInterface; class SMB extends Common implements INotifyStorage { /** - * @var \Icewind\SMB\Server + * @var \Icewind\SMB\IServer */ protected $server; /** - * @var \Icewind\SMB\Share + * @var \Icewind\SMB\IShare */ protected $share; @@ -70,56 +58,96 @@ class SMB extends Common implements INotifyStorage { */ protected $root; - /** - * @var \Icewind\SMB\FileInfo[] - */ - protected $statCache; + /** @var CappedMemoryCache<IFileInfo> */ + protected CappedMemoryCache $statCache; - public function __construct($params) { - if (isset($params['host']) && isset($params['user']) && isset($params['password']) && isset($params['share'])) { - if (Server::NativeAvailable()) { - $this->server = new NativeServer($params['host'], $params['user'], $params['password']); - } else { - $this->server = new Server($params['host'], $params['user'], $params['password']); - } - $this->share = $this->server->getShare(trim($params['share'], '/')); + /** @var LoggerInterface */ + protected $logger; - $this->root = isset($params['root']) ? $params['root'] : '/'; - if (!$this->root || $this->root[0] !== '/') { - $this->root = '/' . $this->root; - } - if (substr($this->root, -1, 1) !== '/') { - $this->root .= '/'; + /** @var bool */ + protected $showHidden; + + private bool $caseSensitive; + + /** @var bool */ + protected $checkAcl; + + public function __construct(array $parameters) { + if (!isset($parameters['host'])) { + throw new \Exception('Invalid configuration, no host provided'); + } + + if (isset($parameters['auth'])) { + $auth = $parameters['auth']; + } elseif (isset($parameters['user']) && isset($parameters['password']) && isset($parameters['share'])) { + [$workgroup, $user] = $this->splitUser($parameters['user']); + $auth = new BasicAuth($user, $workgroup, $parameters['password']); + } else { + throw new \Exception('Invalid configuration, no credentials provided'); + } + + if (isset($parameters['logger'])) { + if (!$parameters['logger'] instanceof LoggerInterface) { + throw new \Exception( + 'Invalid logger. Got ' + . get_class($parameters['logger']) + . ' Expected ' . LoggerInterface::class + ); } + $this->logger = $parameters['logger']; } else { - throw new \Exception('Invalid configuration'); + $this->logger = \OCP\Server::get(LoggerInterface::class); + } + + $options = new Options(); + if (isset($parameters['timeout'])) { + $timeout = (int)$parameters['timeout']; + if ($timeout > 0) { + $options->setTimeout($timeout); + } } + $system = \OCP\Server::get(SystemBridge::class); + $serverFactory = new ServerFactory($options, $system); + $this->server = $serverFactory->createServer($parameters['host'], $auth); + $this->share = $this->server->getShare(trim($parameters['share'], '/')); + + $this->root = $parameters['root'] ?? '/'; + $this->root = '/' . ltrim($this->root, '/'); + $this->root = rtrim($this->root, '/') . '/'; + + $this->showHidden = isset($parameters['show_hidden']) && $parameters['show_hidden']; + $this->caseSensitive = (bool)($parameters['case_sensitive'] ?? true); + $this->checkAcl = isset($parameters['check_acl']) && $parameters['check_acl']; + $this->statCache = new CappedMemoryCache(); - parent::__construct($params); + parent::__construct($parameters); } - /** - * @return string - */ - public function getId() { + private function splitUser(string $user): array { + if (str_contains($user, '/')) { + return explode('/', $user, 2); + } elseif (str_contains($user, '\\')) { + return explode('\\', $user); + } + + return [null, $user]; + } + + public function getId(): string { // FIXME: double slash to keep compatible with the old storage ids, // failure to do so will lead to creation of a new storage id and // loss of shares from the storage - return 'smb::' . $this->server->getUser() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root; + return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root; } - /** - * @param string $path - * @return string - */ - protected function buildPath($path) { + protected function buildPath(string $path): string { return Filesystem::normalizePath($this->root . '/' . $path, true, false, true); } - protected function relativePath($fullPath) { + protected function relativePath(string $fullPath): ?string { if ($fullPath === $this->root) { return ''; - } else if (substr($fullPath, 0, strlen($this->root)) === $this->root) { + } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) { return substr($fullPath, strlen($this->root)); } else { return null; @@ -127,47 +155,119 @@ class SMB extends Common implements INotifyStorage { } /** - * @param string $path - * @return \Icewind\SMB\IFileInfo - * @throws StorageNotAvailableException + * @throws StorageAuthException + * @throws \OCP\Files\NotFoundException + * @throws \OCP\Files\ForbiddenException */ - protected function getFileInfo($path) { + protected function getFileInfo(string $path): IFileInfo { try { $path = $this->buildPath($path); - if (!isset($this->statCache[$path])) { - $this->statCache[$path] = $this->share->stat($path); + $cached = $this->statCache[$path] ?? null; + if ($cached instanceof IFileInfo) { + return $cached; + } else { + $stat = $this->share->stat($path); + $this->statCache[$path] = $stat; + return $stat; } - return $this->statCache[$path]; } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + $this->throwUnavailable($e); + } catch (NotFoundException $e) { + throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e); + } catch (ForbiddenException $e) { + // with php-smbclient, this exception is thrown when the provided password is invalid. + // Possible is also ForbiddenException with a different error code, so we check it. + if ($e->getCode() === 1) { + $this->throwUnavailable($e); + } + throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e); + } + } + + /** + * @throws StorageAuthException + */ + protected function throwUnavailable(\Exception $e): never { + $this->logger->error('Error while getting file info', ['exception' => $e]); + throw new StorageAuthException($e->getMessage(), $e); + } + + /** + * get the acl from fileinfo that is relevant for the configured user + */ + private function getACL(IFileInfo $file): ?ACL { + try { + $acls = $file->getAcls(); + } catch (Exception $e) { + $this->logger->warning('Error while getting file acls', ['exception' => $e]); + return null; + } + foreach ($acls as $user => $acl) { + [, $user] = $this->splitUser($user); // strip domain + if ($user === $this->server->getAuth()->getUsername()) { + return $acl; + } } + + return null; } /** - * @param string $path - * @return \Icewind\SMB\IFileInfo[] + * @return \Generator<IFileInfo> * @throws StorageNotAvailableException */ - protected function getFolderContents($path) { + protected function getFolderContents(string $path): iterable { try { - $path = $this->buildPath($path); - $files = $this->share->dir($path); + $path = ltrim($this->buildPath($path), '/'); + try { + $files = $this->share->dir($path); + } catch (ForbiddenException $e) { + $this->logger->critical($e->getMessage(), ['exception' => $e]); + throw new NotPermittedException(); + } catch (InvalidTypeException $e) { + return; + } foreach ($files as $file) { $this->statCache[$path . '/' . $file->getName()] = $file; } - return array_filter($files, function (IFileInfo $file) { - return !$file->isHidden(); - }); + + foreach ($files as $file) { + try { + // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch + // so we trigger the below exceptions where applicable + $hide = $file->isHidden() && !$this->showHidden; + + if ($this->checkAcl && $acl = $this->getACL($file)) { + // if there is no explicit deny, we assume it's allowed + // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder + // additionally, it's better to have false negatives here then false positives + if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) { + $this->logger->debug('Hiding non readable entry ' . $file->getName()); + continue; + } + } + + if ($hide) { + $this->logger->debug('hiding hidden file ' . $file->getName()); + } + if (!$hide) { + yield $file; + } + } catch (ForbiddenException $e) { + $this->logger->debug($e->getMessage(), ['exception' => $e]); + } catch (NotFoundException $e) { + $this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]); + } + } } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + $this->logger->error('Error while getting folder content', ['exception' => $e]); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); + } catch (NotFoundException $e) { + throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e); } } - /** - * @param \Icewind\SMB\IFileInfo $info - * @return array - */ - protected function formatInfo($info) { + protected function formatInfo(IFileInfo $info): array { $result = [ 'size' => $info->getSize(), 'mtime' => $info->getMTime(), @@ -185,35 +285,59 @@ class SMB extends Common implements INotifyStorage { * * @param string $source the old name of the path * @param string $target the new name of the path - * @return bool true if the rename is successful, false otherwise */ - public function rename($source, $target) { + public function rename(string $source, string $target, bool $retry = true): bool { if ($this->isRootDir($source) || $this->isRootDir($target)) { return false; } + if ($this->caseSensitive === false + && mb_strtolower($target) === mb_strtolower($source) + ) { + // Forbid changing case only on case-insensitive file system + return false; + } $absoluteSource = $this->buildPath($source); $absoluteTarget = $this->buildPath($target); try { $result = $this->share->rename($absoluteSource, $absoluteTarget); } catch (AlreadyExistsException $e) { - $this->remove($target); - $result = $this->share->rename($absoluteSource, $absoluteTarget); + if ($retry) { + $this->remove($target); + $result = $this->share->rename($absoluteSource, $absoluteTarget); + } else { + $this->logger->warning($e->getMessage(), ['exception' => $e]); + return false; + } + } catch (InvalidArgumentException $e) { + if ($retry) { + $this->remove($target); + $result = $this->share->rename($absoluteSource, $absoluteTarget); + } else { + $this->logger->warning($e->getMessage(), ['exception' => $e]); + return false; + } } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e, ['level' => Util::WARN]); + $this->logger->warning($e->getMessage(), ['exception' => $e]); return false; } unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]); return $result; } - public function stat($path) { + public function stat(string $path, bool $retry = true): array|false { try { $result = $this->formatInfo($this->getFileInfo($path)); - } catch (ForbiddenException $e) { + } catch (\OCP\Files\ForbiddenException $e) { return false; - } catch (NotFoundException $e) { + } catch (\OCP\Files\NotFoundException $e) { return false; + } catch (TimedOutException $e) { + if ($retry) { + return $this->stat($path, false); + } else { + throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + } } if ($this->remoteIsShare() && $this->isRootDir($path)) { $result['mtime'] = $this->shareMTime(); @@ -223,15 +347,19 @@ class SMB extends Common implements INotifyStorage { /** * get the best guess for the modification time of the share - * - * @return int */ - private function shareMTime() { + private function shareMTime(): int { $highestMTime = 0; $files = $this->share->dir($this->root); foreach ($files as $fileInfo) { - if ($fileInfo->getMTime() > $highestMTime) { - $highestMTime = $fileInfo->getMTime(); + try { + if ($fileInfo->getMTime() > $highestMTime) { + $highestMTime = $fileInfo->getMTime(); + } + } catch (NotFoundException $e) { + // Ignore this, can happen on unavailable DFS shares + } catch (ForbiddenException $e) { + // Ignore this too - it's a symlink } } return $highestMTime; @@ -239,28 +367,19 @@ class SMB extends Common implements INotifyStorage { /** * Check if the path is our root dir (not the smb one) - * - * @param string $path the path - * @return bool */ - private function isRootDir($path) { + private function isRootDir(string $path): bool { return $path === '' || $path === '/' || $path === '.'; } /** * Check if our root points to a smb share - * - * @return bool true if our root points to a share false otherwise */ - private function remoteIsShare() { + private function remoteIsShare(): bool { return $this->share->getName() && (!$this->root || $this->root === '/'); } - /** - * @param string $path - * @return bool - */ - public function unlink($path) { + public function unlink(string $path): bool { if ($this->isRootDir($path)) { return false; } @@ -279,47 +398,43 @@ class SMB extends Common implements INotifyStorage { } catch (ForbiddenException $e) { return false; } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + $this->logger->error('Error while deleting file', ['exception' => $e]); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); } } /** * check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @return bool */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { if (!$path and $this->root === '/') { // mtime doesn't work for shares, but giving the nature of the backend, // doing a full update is still just fast enough return true; } else { $actualTime = $this->filemtime($path); - return $actualTime > $time; + return $actualTime > $time || $actualTime === 0; } } /** - * @param string $path - * @param string $mode * @return resource|false */ - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $fullPath = $this->buildPath($path); try { switch ($mode) { case 'r': case 'rb': if (!$this->file_exists($path)) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file doesn\'t exist.'); return false; } return $this->share->read($fullPath); case 'w': case 'wb': $source = $this->share->write($fullPath); - return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) { + return CallBackWrapper::wrap($source, null, null, function () use ($fullPath): void { unset($this->statCache[$fullPath]); }); case 'a': @@ -340,18 +455,20 @@ class SMB extends Common implements INotifyStorage { } if ($this->file_exists($path)) { if (!$this->isUpdatable($path)) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file not updatable.'); return false; } $tmpFile = $this->getCachedFile($path); } else { if (!$this->isCreatable(dirname($path))) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', parent directory not writable.'); return false; } - $tmpFile = \OCP\Files::tmpFile($ext); + $tmpFile = \OCP\Server::get(ITempManager::class)->getTemporaryFile($ext); } $source = fopen($tmpFile, $mode); $share = $this->share; - return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) { + return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share): void { unset($this->statCache[$fullPath]); $share->put($tmpFile, $fullPath); unlink($tmpFile); @@ -359,21 +476,27 @@ class SMB extends Common implements INotifyStorage { } return false; } catch (NotFoundException $e) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', not found.', ['exception' => $e]); return false; } catch (ForbiddenException $e) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', forbidden.', ['exception' => $e]); return false; + } catch (OutOfSpaceException $e) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', out of space.', ['exception' => $e]); + throw new EntityTooLargeException('not enough available space to create file', 0, $e); } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + $this->logger->error('Error while opening file ' . $path . ' on ' . $this->getId(), ['exception' => $e]); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); } } - public function rmdir($path) { + public function rmdir(string $path): bool { if ($this->isRootDir($path)) { return false; } try { - $this->statCache = array(); + $this->statCache = new CappedMemoryCache(); $content = $this->share->dir($this->buildPath($path)); foreach ($content as $file) { if ($file->isDirectory()) { @@ -389,11 +512,12 @@ class SMB extends Common implements INotifyStorage { } catch (ForbiddenException $e) { return false; } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + $this->logger->error('Error while removing folder', ['exception' => $e]); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); } } - public function touch($path, $time = null) { + public function touch(string $path, ?int $mtime = null): bool { try { if (!$this->file_exists($path)) { $fh = $this->share->write($this->buildPath($path)); @@ -401,92 +525,165 @@ class SMB extends Common implements INotifyStorage { return true; } return false; + } catch (OutOfSpaceException $e) { + throw new EntityTooLargeException('not enough available space to create file', 0, $e); } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + $this->logger->error('Error while creating file', ['exception' => $e]); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); + } + } + + public function getMetaData(string $path): ?array { + try { + $fileInfo = $this->getFileInfo($path); + } catch (\OCP\Files\NotFoundException $e) { + return null; + } catch (\OCP\Files\ForbiddenException $e) { + return null; + } + + return $this->getMetaDataFromFileInfo($fileInfo); + } + + private function getMetaDataFromFileInfo(IFileInfo $fileInfo): array { + $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE; + + if ( + !$fileInfo->isReadOnly() || $fileInfo->isDirectory() + ) { + $permissions += Constants::PERMISSION_DELETE; + $permissions += Constants::PERMISSION_UPDATE; + if ($fileInfo->isDirectory()) { + $permissions += Constants::PERMISSION_CREATE; + } + } + + $data = []; + if ($fileInfo->isDirectory()) { + $data['mimetype'] = 'httpd/unix-directory'; + } else { + $data['mimetype'] = \OCP\Server::get(IMimeTypeDetector::class)->detectPath($fileInfo->getPath()); + } + $data['mtime'] = $fileInfo->getMTime(); + if ($fileInfo->isDirectory()) { + $data['size'] = -1; //unknown + } else { + $data['size'] = $fileInfo->getSize(); } + $data['etag'] = $this->getETag($fileInfo->getPath()); + $data['storage_mtime'] = $data['mtime']; + $data['permissions'] = $permissions; + $data['name'] = $fileInfo->getName(); + + return $data; } - public function opendir($path) { + public function opendir(string $path) { try { $files = $this->getFolderContents($path); } catch (NotFoundException $e) { return false; - } catch (ForbiddenException $e) { + } catch (NotPermittedException $e) { return false; } $names = array_map(function ($info) { - /** @var \Icewind\SMB\IFileInfo $info */ + /** @var IFileInfo $info */ return $info->getName(); - }, $files); + }, iterator_to_array($files)); return IteratorDirectory::wrap($names); } - public function filetype($path) { + public function getDirectoryContent(string $directory): \Traversable { try { - return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file'; + $files = $this->getFolderContents($directory); + foreach ($files as $file) { + yield $this->getMetaDataFromFileInfo($file); + } } catch (NotFoundException $e) { + return; + } catch (NotPermittedException $e) { + return; + } + } + + public function filetype(string $path): string|false { + try { + return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file'; + } catch (\OCP\Files\NotFoundException $e) { return false; - } catch (ForbiddenException $e) { + } catch (\OCP\Files\ForbiddenException $e) { return false; } } - public function mkdir($path) { + public function mkdir(string $path): bool { $path = $this->buildPath($path); try { $this->share->mkdir($path); return true; } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + $this->logger->error('Error while creating folder', ['exception' => $e]); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); } catch (Exception $e) { return false; } } - public function file_exists($path) { + public function file_exists(string $path): bool { try { + // Case sensitive filesystem doesn't matter for root directory + if ($this->caseSensitive === false && $path !== '') { + $filename = basename($path); + $siblings = $this->getDirectoryContent(dirname($path)); + foreach ($siblings as $sibling) { + if ($sibling['name'] === $filename) { + return true; + } + } + return false; + } $this->getFileInfo($path); return true; - } catch (NotFoundException $e) { + } catch (\OCP\Files\NotFoundException $e) { return false; - } catch (ForbiddenException $e) { + } catch (\OCP\Files\ForbiddenException $e) { return false; } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); } } - public function isReadable($path) { + public function isReadable(string $path): bool { try { $info = $this->getFileInfo($path); - return !$info->isHidden(); - } catch (NotFoundException $e) { + return $this->showHidden || !$info->isHidden(); + } catch (\OCP\Files\NotFoundException $e) { return false; - } catch (ForbiddenException $e) { + } catch (\OCP\Files\ForbiddenException $e) { return false; } } - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { try { $info = $this->getFileInfo($path); // following windows behaviour for read-only folders: they can be written into // (https://support.microsoft.com/en-us/kb/326549 - "cause" section) - return !$info->isHidden() && (!$info->isReadOnly() || $this->is_dir($path)); - } catch (NotFoundException $e) { + return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory()); + } catch (\OCP\Files\NotFoundException $e) { return false; - } catch (ForbiddenException $e) { + } catch (\OCP\Files\ForbiddenException $e) { return false; } } - public function isDeletable($path) { + public function isDeletable(string $path): bool { try { $info = $this->getFileInfo($path); - return !$info->isHidden() && !$info->isReadOnly(); - } catch (NotFoundException $e) { + return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly(); + } catch (\OCP\Files\NotFoundException $e) { return false; - } catch (ForbiddenException $e) { + } catch (\OCP\Files\ForbiddenException $e) { return false; } } @@ -494,27 +691,25 @@ class SMB extends Common implements INotifyStorage { /** * check if smbclient is installed */ - public static function checkDependencies() { - return ( - (bool)\OC_Helper::findBinaryPath('smbclient') - || Server::NativeAvailable() - ) ? true : ['smbclient']; + public static function checkDependencies(): array|bool { + $system = \OCP\Server::get(SystemBridge::class); + return Server::available($system) || NativeServer::available($system) ?: ['smbclient']; } - /** - * Test a storage for availability - * - * @return bool - */ - public function test() { + public function test(): bool { try { return parent::test(); + } catch (StorageAuthException $e) { + return false; + } catch (ForbiddenException $e) { + return false; } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); return false; } } - public function listen($path, callable $callback) { + public function listen(string $path, callable $callback): void { $this->notify($path)->listen(function (IChange $change) use ($callback) { if ($change instanceof IRenameChange) { return $callback($change->getType(), $change->getPath(), $change->getTargetPath()); @@ -524,7 +719,7 @@ class SMB extends Common implements INotifyStorage { }); } - public function notify($path) { + public function notify(string $path): SMBNotifyHandler { $path = '/' . ltrim($path, '/'); $shareNotifyHandler = $this->share->notify($this->buildPath($path)); return new SMBNotifyHandler($shareNotifyHandler, $this->root); diff --git a/apps/files_external/lib/Lib/Storage/StreamWrapper.php b/apps/files_external/lib/Lib/Storage/StreamWrapper.php index 5b440910060..1272b9d4d8a 100644 --- a/apps/files_external/lib/Lib/Storage/StreamWrapper.php +++ b/apps/files_external/lib/Lib/Storage/StreamWrapper.php @@ -1,46 +1,23 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.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: 2020-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Storage; -abstract class StreamWrapper extends \OC\Files\Storage\Common { +use OC\Files\Storage\Common; - /** - * @param string $path - * @return string|null - */ - abstract public function constructUrl($path); +abstract class StreamWrapper extends Common { - public function mkdir($path) { + abstract public function constructUrl(string $path): ?string; + + public function mkdir(string $path): bool { return mkdir($this->constructUrl($path)); } - public function rmdir($path) { + public function rmdir(string $path): bool { if ($this->is_dir($path) && $this->isDeletable($path)) { $dh = $this->opendir($path); if (!is_resource($dh)) { @@ -62,19 +39,19 @@ abstract class StreamWrapper extends \OC\Files\Storage\Common { } } - public function opendir($path) { + public function opendir(string $path) { return opendir($this->constructUrl($path)); } - public function filetype($path) { + public function filetype(string $path): string|false { return @filetype($this->constructUrl($path)); } - public function file_exists($path) { + public function file_exists(string $path): bool { return file_exists($this->constructUrl($path)); } - public function unlink($path) { + public function unlink(string $path): bool { $url = $this->constructUrl($path); $success = unlink($url); // normally unlink() is supposed to do this implicitly, @@ -83,11 +60,11 @@ abstract class StreamWrapper extends \OC\Files\Storage\Common { return $success; } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { return fopen($this->constructUrl($path), $mode); } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { if ($this->file_exists($path)) { if (is_null($mtime)) { $fh = $this->fopen($path, 'a'); @@ -104,27 +81,19 @@ abstract class StreamWrapper extends \OC\Files\Storage\Common { } } - /** - * @param string $path - * @param string $target - */ - public function getFile($path, $target) { + public function getFile(string $path, string $target): bool { return copy($this->constructUrl($path), $target); } - /** - * @param string $target - */ - public function uploadFile($path, $target) { + public function uploadFile(string $path, string $target): bool { return copy($path, $this->constructUrl($target)); } - public function rename($path1, $path2) { - return rename($this->constructUrl($path1), $this->constructUrl($path2)); + public function rename(string $source, string $target): bool { + return rename($this->constructUrl($source), $this->constructUrl($target)); } - public function stat($path) { + public function stat(string $path): array|false { return stat($this->constructUrl($path)); } - } diff --git a/apps/files_external/lib/Lib/Storage/Swift.php b/apps/files_external/lib/Lib/Storage/Swift.php index f07087e903d..e80570f14ba 100644 --- a/apps/files_external/lib/Lib/Storage/Swift.php +++ b/apps/files_external/lib/Lib/Storage/Swift.php @@ -1,71 +1,42 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Benjamin Liles <benliles@arch.tamu.edu> - * @author Christian Berendt <berendt@b1-systems.de> - * @author Christopher Bartz <bartz@dkrz.de> - * @author Daniel Tosello <tosello.daniel@gmail.com> - * @author Felix Moeller <mail@felixmoeller.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Martin Mattel <martin.mattel@diemattels.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tim Dettrick <t.dettrick@uq.edu.au> - * @author Vincent Petry <pvince81@owncloud.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Storage; -use Guzzle\Http\Url; -use Guzzle\Http\Exception\ClientErrorResponseException; +use GuzzleHttp\Psr7\Uri; use Icewind\Streams\CallbackWrapper; use Icewind\Streams\IteratorDirectory; -use Icewind\Streams\RetryWrapper; -use OpenCloud; -use OpenCloud\Common\Exceptions; -use OpenCloud\OpenStack; -use OpenCloud\Rackspace; -use OpenCloud\ObjectStore\Resource\DataObject; -use OpenCloud\ObjectStore\Exception; - -class Swift extends \OC\Files\Storage\Common { - +use OC\Files\Filesystem; +use OC\Files\ObjectStore\SwiftFactory; +use OC\Files\Storage\Common; +use OCP\Cache\CappedMemoryCache; +use OCP\Files\IMimeTypeDetector; +use OCP\Files\StorageAuthException; +use OCP\Files\StorageBadConfigException; +use OCP\Files\StorageNotAvailableException; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\ITempManager; +use OCP\Server; +use OpenStack\Common\Error\BadResponseError; +use OpenStack\ObjectStore\v1\Models\Container; +use OpenStack\ObjectStore\v1\Models\StorageObject; +use Psr\Log\LoggerInterface; + +class Swift extends Common { + /** @var SwiftFactory */ + private $connectionFactory; /** - * @var \OpenCloud\ObjectStore\Service - */ - private $connection; - /** - * @var \OpenCloud\ObjectStore\Resource\Container + * @var Container */ private $container; /** - * @var \OpenCloud\OpenStack - */ - private $anchor; - /** * @var string */ private $bucket; @@ -76,21 +47,25 @@ class Swift extends \OC\Files\Storage\Common { */ private $params; - /** @var string */ + /** @var string */ private $id; + /** @var \OC\Files\ObjectStore\Swift */ + private $objectStore; + + /** @var IMimeTypeDetector */ + private $mimeDetector; + /** * Key value cache mapping path to data object. Maps path to * \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject for existing * paths and path to false for not existing paths. - * @var \OCP\ICache + * + * @var ICache */ private $objectCache; - /** - * @param string $path - */ - private function normalizePath($path) { + private function normalizePath(string $path): string { $path = trim($path, '/'); if (!$path) { @@ -102,14 +77,7 @@ class Swift extends \OC\Files\Storage\Common { return $path; } - const SUBCONTAINER_FILE = '.subcontainers'; - - /** - * translate directory path to container name - * - * @param string $path - * @return string - */ + public const SUBCONTAINER_FILE = '.subcontainers'; /** * Fetches an object from the API. @@ -117,29 +85,31 @@ class Swift extends \OC\Files\Storage\Common { * failed "doesn't exist" response was cached, * that one will be returned. * - * @param string $path - * @return \OpenCloud\ObjectStore\Resource\DataObject|bool object - * or false if the object did not exist + * @return StorageObject|false object + * or false if the object did not exist + * @throws StorageAuthException + * @throws StorageNotAvailableException */ - private function fetchObject($path) { - if ($this->objectCache->hasKey($path)) { + private function fetchObject(string $path): StorageObject|false { + $cached = $this->objectCache->get($path); + if ($cached !== null) { // might be "false" if object did not exist from last check - return $this->objectCache->get($path); + return $cached; } try { - $object = $this->getContainer()->getPartialObject($path); + $object = $this->getContainer()->getObject($path); + $object->retrieve(); $this->objectCache->set($path, $object); return $object; - } catch (ClientErrorResponseException $e) { - // this exception happens when the object does not exist, which - // is expected in most cases - $this->objectCache->set($path, false); - return false; - } catch (ClientErrorResponseException $e) { + } catch (BadResponseError $e) { // Expected response is "404 Not Found", so only log if it isn't if ($e->getResponse()->getStatusCode() !== 404) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); } + $this->objectCache->set($path, false); return false; } } @@ -147,46 +117,65 @@ class Swift extends \OC\Files\Storage\Common { /** * Returns whether the given path exists. * - * @param string $path - * * @return bool true if the object exist, false otherwise + * @throws StorageAuthException + * @throws StorageNotAvailableException */ - private function doesObjectExist($path) { + private function doesObjectExist(string $path): bool { return $this->fetchObject($path) !== false; } - public function __construct($params) { - if ((empty($params['key']) and empty($params['password'])) - or empty($params['user']) or empty($params['bucket']) - or empty($params['region']) + public function __construct(array $parameters) { + if ((empty($parameters['key']) and empty($parameters['password'])) + or (empty($parameters['user']) && empty($parameters['userid'])) or empty($parameters['bucket']) + or empty($parameters['region']) ) { - throw new \Exception("API Key or password, Username, Bucket and Region have to be configured."); + throw new StorageBadConfigException('API Key or password, Login, Bucket and Region have to be configured.'); } - $this->id = 'swift::' . $params['user'] . md5($params['bucket']); + $user = $parameters['user']; + $this->id = 'swift::' . $user . md5($parameters['bucket']); - $bucketUrl = Url::factory($params['bucket']); - if ($bucketUrl->isAbsolute()) { - $this->bucket = end(($bucketUrl->getPathSegments())); - $params['endpoint_url'] = $bucketUrl->addPath('..')->normalizePath(); - } else { - $this->bucket = $params['bucket']; + $bucketUrl = new Uri($parameters['bucket']); + if ($bucketUrl->getHost()) { + $parameters['bucket'] = basename($bucketUrl->getPath()); + $parameters['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath())); } - if (empty($params['url'])) { - $params['url'] = 'https://identity.api.rackspacecloud.com/v2.0/'; + if (empty($parameters['url'])) { + $parameters['url'] = 'https://identity.api.rackspacecloud.com/v2.0/'; } - if (empty($params['service_name'])) { - $params['service_name'] = 'cloudFiles'; + if (empty($parameters['service_name'])) { + $parameters['service_name'] = 'cloudFiles'; } - $this->params = $params; + $parameters['autocreate'] = true; + + if (isset($parameters['domain'])) { + $parameters['user'] = [ + 'name' => $parameters['user'], + 'password' => $parameters['password'], + 'domain' => [ + 'name' => $parameters['domain'], + ] + ]; + } + + $this->params = $parameters; // FIXME: private class... - $this->objectCache = new \OC\Cache\CappedMemoryCache(); + $this->objectCache = new CappedMemoryCache(); + $this->connectionFactory = new SwiftFactory( + Server::get(ICacheFactory::class)->createDistributed('swift/'), + $this->params, + Server::get(LoggerInterface::class) + ); + $this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory); + $this->bucket = $parameters['bucket']; + $this->mimeDetector = Server::get(IMimeTypeDetector::class); } - public function mkdir($path) { + public function mkdir(string $path): bool { $path = $this->normalizePath($path); if ($this->is_dir($path)) { @@ -198,22 +187,26 @@ class Swift extends \OC\Files\Storage\Common { } try { - $customHeaders = array('content-type' => 'httpd/unix-directory'); - $metadataHeaders = DataObject::stockHeaders(array()); - $allHeaders = $customHeaders + $metadataHeaders; - $this->getContainer()->uploadObject($path, '', $allHeaders); + $this->getContainer()->createObject([ + 'name' => $path, + 'content' => '', + 'headers' => ['content-type' => 'httpd/unix-directory'] + ]); // invalidate so that the next access gets the real object // with all properties $this->objectCache->remove($path); - } catch (Exceptions\CreateUpdateError $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); return false; } return true; } - public function file_exists($path) { + public function file_exists(string $path): bool { $path = $this->normalizePath($path); if ($path !== '.' && $this->is_dir($path)) { @@ -223,7 +216,7 @@ class Swift extends \OC\Files\Storage\Common { return $this->doesObjectExist($path); } - public function rmdir($path) { + public function rmdir(string $path): bool { $path = $this->normalizePath($path); if (!$this->is_dir($path) || !$this->isDeletable($path)) { @@ -231,8 +224,8 @@ class Swift extends \OC\Files\Storage\Common { } $dh = $this->opendir($path); - while ($file = readdir($dh)) { - if (\OC\Files\Filesystem::isIgnoredDir($file)) { + while (($file = readdir($dh)) !== false) { + if (Filesystem::isIgnoredDir($file)) { continue; } @@ -244,17 +237,20 @@ class Swift extends \OC\Files\Storage\Common { } try { - $this->getContainer()->dataObject()->setName($path . '/')->delete(); + $this->objectStore->deleteObject($path . '/'); $this->objectCache->remove($path . '/'); - } catch (Exceptions\DeleteError $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); return false; } return true; } - public function opendir($path) { + public function opendir(string $path) { $path = $this->normalizePath($path); if ($path === '.') { @@ -263,19 +259,18 @@ class Swift extends \OC\Files\Storage\Common { $path .= '/'; } - $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of # + // $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of # try { - $files = array(); - /** @var OpenCloud\Common\Collection $objects */ - $objects = $this->getContainer()->objectList(array( + $files = []; + $objects = $this->getContainer()->listObjects([ 'prefix' => $path, 'delimiter' => '/' - )); + ]); - /** @var OpenCloud\ObjectStore\Resource\DataObject $object */ + /** @var StorageObject $object */ foreach ($objects as $object) { - $file = basename($object->getName()); + $file = basename($object->name); if ($file !== basename($path) && $file !== '.') { $files[] = $file; } @@ -283,56 +278,55 @@ class Swift extends \OC\Files\Storage\Common { return IteratorDirectory::wrap($files); } catch (\Exception $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); return false; } - } - public function stat($path) { + public function stat(string $path): array|false { $path = $this->normalizePath($path); - if ($path === '.') { $path = ''; - } else if ($this->is_dir($path)) { + } elseif ($this->is_dir($path)) { $path .= '/'; } try { - /** @var DataObject $object */ $object = $this->fetchObject($path); if (!$object) { return false; } - } catch (ClientErrorResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); return false; } - $dateTime = \DateTime::createFromFormat(\DateTime::RFC1123, $object->getLastModified()); - if ($dateTime !== false) { - $mtime = $dateTime->getTimestamp(); - } else { - $mtime = null; - } - $objectMetadata = $object->getMetadata(); - $metaTimestamp = $objectMetadata->getProperty('timestamp'); - if (isset($metaTimestamp)) { - $mtime = $metaTimestamp; + $mtime = null; + if (!empty($object->lastModified)) { + $dateTime = \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified); + if ($dateTime !== false) { + $mtime = $dateTime->getTimestamp(); + } } - if (!empty($mtime)) { - $mtime = floor($mtime); + if (is_numeric($object->getMetadata()['timestamp'] ?? null)) { + $mtime = (float)$object->getMetadata()['timestamp']; } - $stat = array(); - $stat['size'] = (int)$object->getContentLength(); - $stat['mtime'] = $mtime; - $stat['atime'] = time(); - return $stat; + return [ + 'size' => (int)$object->contentLength, + 'mtime' => isset($mtime) ? (int)floor($mtime) : null, + 'atime' => time(), + ]; } - public function filetype($path) { + public function filetype(string $path) { $path = $this->normalizePath($path); if ($path !== '.' && $this->doesObjectExist($path)) { @@ -348,7 +342,7 @@ class Swift extends \OC\Files\Storage\Common { } } - public function unlink($path) { + public function unlink(string $path): bool { $path = $this->normalizePath($path); if ($this->is_dir($path)) { @@ -356,20 +350,23 @@ class Swift extends \OC\Files\Storage\Common { } try { - $this->getContainer()->dataObject()->setName($path)->delete(); + $this->objectStore->deleteObject($path); $this->objectCache->remove($path); $this->objectCache->remove($path . '/'); - } catch (ClientErrorResponseException $e) { + } catch (BadResponseError $e) { if ($e->getResponse()->getStatusCode() !== 404) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + throw $e; } - return false; } return true; } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $path = $this->normalizePath($path); switch ($mode) { @@ -380,21 +377,12 @@ class Swift extends \OC\Files\Storage\Common { case 'r': case 'rb': try { - $c = $this->getContainer(); - $streamFactory = new \Guzzle\Stream\PhpStreamRequestFactory(); - /** @var \OpenCloud\Common\Http\Client $client */ - $client = $c->getClient(); - $streamInterface = $streamFactory->fromRequest($client->get($c->getUrl($path))); - $streamInterface->rewind(); - $stream = $streamInterface->getStream(); - stream_context_set_option($stream, 'swift','content', $streamInterface); - if(!strrpos($streamInterface - ->getMetaData('wrapper_data')[0], '404 Not Found')) { - return RetryWrapper::wrap($stream); - } - return false; - } catch (\Guzzle\Http\Exception\BadResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + return $this->objectStore->readObject($path); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); return false; } case 'w': @@ -411,7 +399,7 @@ class Swift extends \OC\Files\Storage\Common { } else { $ext = ''; } - $tmpFile = \OCP\Files::tmpFile($ext); + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile($ext); // Fetch existing file if required if ($mode[0] !== 'w' && $this->file_exists($path)) { if ($mode[0] === 'x') { @@ -422,89 +410,95 @@ class Swift extends \OC\Files\Storage\Common { file_put_contents($tmpFile, $source); } $handle = fopen($tmpFile, $mode); - return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void { $this->writeBack($tmpFile, $path); }); } } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { $path = $this->normalizePath($path); if (is_null($mtime)) { $mtime = time(); } - $metadata = array('timestamp' => $mtime); + $metadata = ['timestamp' => (string)$mtime]; if ($this->file_exists($path)) { if ($this->is_dir($path) && $path !== '.') { $path .= '/'; } $object = $this->fetchObject($path); - if ($object->saveMetadata($metadata)) { + if ($object->mergeMetadata($metadata)) { // invalidate target object to force repopulation on fetch $this->objectCache->remove($path); } return true; } else { - $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path); - $customHeaders = array('content-type' => $mimeType); - $metadataHeaders = DataObject::stockHeaders($metadata); - $allHeaders = $customHeaders + $metadataHeaders; - $this->getContainer()->uploadObject($path, '', $allHeaders); + $mimeType = $this->mimeDetector->detectPath($path); + $this->getContainer()->createObject([ + 'name' => $path, + 'content' => '', + 'headers' => ['content-type' => 'httpd/unix-directory'] + ]); // invalidate target object to force repopulation on fetch $this->objectCache->remove($path); return true; } } - public function copy($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); - - $fileType = $this->filetype($path1); - if ($fileType === 'file') { + public function copy(string $source, string $target): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); + $fileType = $this->filetype($source); + if ($fileType) { // make way - $this->unlink($path2); + $this->unlink($target); + } + if ($fileType === 'file') { try { - $source = $this->fetchObject($path1); - $source->copy($this->bucket . '/' . $path2); + $sourceObject = $this->fetchObject($source); + $sourceObject->copy([ + 'destination' => $this->bucket . '/' . $target + ]); // invalidate target object to force repopulation on fetch - $this->objectCache->remove($path2); - $this->objectCache->remove($path2 . '/'); - } catch (ClientErrorResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + $this->objectCache->remove($target); + $this->objectCache->remove($target . '/'); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); return false; } - - } else if ($fileType === 'dir') { - - // make way - $this->unlink($path2); - + } elseif ($fileType === 'dir') { try { - $source = $this->fetchObject($path1 . '/'); - $source->copy($this->bucket . '/' . $path2 . '/'); + $sourceObject = $this->fetchObject($source . '/'); + $sourceObject->copy([ + 'destination' => $this->bucket . '/' . $target . '/' + ]); // invalidate target object to force repopulation on fetch - $this->objectCache->remove($path2); - $this->objectCache->remove($path2 . '/'); - } catch (ClientErrorResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + $this->objectCache->remove($target); + $this->objectCache->remove($target . '/'); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); return false; } - $dh = $this->opendir($path1); - while ($file = readdir($dh)) { - if (\OC\Files\Filesystem::isIgnoredDir($file)) { + $dh = $this->opendir($source); + while (($file = readdir($dh)) !== false) { + if (Filesystem::isIgnoredDir($file)) { continue; } - $source = $path1 . '/' . $file; - $target = $path2 . '/' . $file; + $source = $source . '/' . $file; + $target = $target . '/' . $file; $this->copy($source, $target); } - } else { //file does not exist return false; @@ -513,21 +507,22 @@ class Swift extends \OC\Files\Storage\Common { return true; } - public function rename($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); + public function rename(string $source, string $target): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); - $fileType = $this->filetype($path1); + $fileType = $this->filetype($source); if ($fileType === 'dir' || $fileType === 'file') { // copy - if ($this->copy($path1, $path2) === false) { + if ($this->copy($source, $target) === false) { return false; } // cleanup - if ($this->unlink($path1) === false) { - $this->unlink($path2); + if ($this->unlink($source) === false) { + throw new \Exception('failed to remove original'); + $this->unlink($target); return false; } @@ -537,97 +532,43 @@ class Swift extends \OC\Files\Storage\Common { return false; } - public function getId() { + public function getId(): string { return $this->id; } /** - * Returns the connection - * - * @return OpenCloud\ObjectStore\Service connected client - * @throws \Exception if connection could not be made - */ - public function getConnection() { - if (!is_null($this->connection)) { - return $this->connection; - } - - $settings = array( - 'username' => $this->params['user'], - ); - - if (!empty($this->params['password'])) { - $settings['password'] = $this->params['password']; - } else if (!empty($this->params['key'])) { - $settings['apiKey'] = $this->params['key']; - } - - if (!empty($this->params['tenant'])) { - $settings['tenantName'] = $this->params['tenant']; - } - - if (!empty($this->params['timeout'])) { - $settings['timeout'] = $this->params['timeout']; - } - - if (isset($settings['apiKey'])) { - $this->anchor = new Rackspace($this->params['url'], $settings); - } else { - $this->anchor = new OpenStack($this->params['url'], $settings); - } - - $connection = $this->anchor->objectStoreService($this->params['service_name'], $this->params['region']); - - if (!empty($this->params['endpoint_url'])) { - $endpoint = $connection->getEndpoint(); - $endpoint->setPublicUrl($this->params['endpoint_url']); - $endpoint->setPrivateUrl($this->params['endpoint_url']); - $connection->setEndpoint($endpoint); - } - - $this->connection = $connection; - - return $this->connection; - } - - /** * Returns the initialized object store container. * - * @return OpenCloud\ObjectStore\Resource\Container + * @return Container + * @throws StorageAuthException + * @throws StorageNotAvailableException */ - public function getContainer() { - if (!is_null($this->container)) { - return $this->container; - } + public function getContainer(): Container { + if (is_null($this->container)) { + $this->container = $this->connectionFactory->getContainer(); - try { - $this->container = $this->getConnection()->getContainer($this->bucket); - } catch (ClientErrorResponseException $e) { - $this->container = $this->getConnection()->createContainer($this->bucket); - } - - if (!$this->file_exists('.')) { - $this->mkdir('.'); + if (!$this->file_exists('.')) { + $this->mkdir('.'); + } } - return $this->container; } - public function writeBack($tmpFile, $path) { + public function writeBack(string $tmpFile, string $path): void { $fileData = fopen($tmpFile, 'r'); - $this->getContainer()->uploadObject($path, $fileData); + $this->objectStore->writeObject($path, $fileData, $this->mimeDetector->detectPath($path)); // invalidate target object to force repopulation on fetch $this->objectCache->remove($path); unlink($tmpFile); } - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { if ($this->is_file($path)) { return parent::hasUpdated($path, $time); } $path = $this->normalizePath($path); $dh = $this->opendir($path); - $content = array(); + $content = []; while (($file = readdir($dh)) !== false) { $content[] = $file; } @@ -646,8 +587,7 @@ class Swift extends \OC\Files\Storage\Common { /** * check if curl is installed */ - public static function checkDependencies() { + public static function checkDependencies(): bool { return true; } - } diff --git a/apps/files_external/lib/Lib/Storage/SystemBridge.php b/apps/files_external/lib/Lib/Storage/SystemBridge.php new file mode 100644 index 00000000000..80449b2744b --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/SystemBridge.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Lib\Storage; + +use Icewind\SMB\System; +use OCP\IBinaryFinder; + +/** + * Bridge the NC and SMB binary finding logic + */ +class SystemBridge extends System { + public function __construct( + private IBinaryFinder $binaryFinder, + ) { + } + + protected function getBinaryPath(string $binary): ?string { + $path = $this->binaryFinder->findBinaryPath($binary); + return $path !== false ? $path : null; + } +} diff --git a/apps/files_external/lib/Lib/StorageConfig.php b/apps/files_external/lib/Lib/StorageConfig.php index 093e6840a19..2cb82d3790a 100644 --- a/apps/files_external/lib/Lib/StorageConfig.php +++ b/apps/files_external/lib/Lib/StorageConfig.php @@ -1,42 +1,28 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Jesús Macias <jmacias@solidgear.es> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Vincent Petry <pvince81@owncloud.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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; +use OC\Files\Filesystem; +use OCA\Files_External\Lib\Auth\AuthMechanism; use OCA\Files_External\Lib\Auth\IUserProvided; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\ResponseDefinitions; /** * External storage configuration + * + * @psalm-import-type Files_ExternalStorageConfig from ResponseDefinitions */ class StorageConfig implements \JsonSerializable { - const MOUNT_TYPE_ADMIN = 1; - const MOUNT_TYPE_PERSONAl = 2; + public const MOUNT_TYPE_ADMIN = 1; + public const MOUNT_TYPE_PERSONAL = 2; + /** @deprecated use MOUNT_TYPE_PERSONAL (full uppercase) instead */ + public const MOUNT_TYPE_PERSONAl = 2; /** * Storage config id @@ -62,7 +48,7 @@ class StorageConfig implements \JsonSerializable { /** * Backend options * - * @var array + * @var array<string, mixed> */ private $backendOptions = []; @@ -97,21 +83,21 @@ class StorageConfig implements \JsonSerializable { /** * List of users who have access to this storage * - * @var array + * @var list<string> */ private $applicableUsers = []; /** * List of groups that have access to this storage * - * @var array + * @var list<string> */ private $applicableGroups = []; /** * Mount-specific options * - * @var array + * @var array<string, mixed> */ private $mountOptions = []; @@ -125,10 +111,10 @@ class StorageConfig implements \JsonSerializable { /** * Creates a storage config * - * @param int|null $id config id or null for a new config + * @param int|string $id config id or null for a new config */ public function __construct($id = null) { - $this->id = $id; + $this->id = $id ?? -1; $this->mountOptions['enable_sharing'] = false; } @@ -146,7 +132,7 @@ class StorageConfig implements \JsonSerializable { * * @param int $id configuration id */ - public function setId($id) { + public function setId(int $id): void { $this->id = $id; } @@ -168,7 +154,7 @@ class StorageConfig implements \JsonSerializable { * @param string $mountPoint path */ public function setMountPoint($mountPoint) { - $this->mountPoint = \OC\Files\Filesystem::normalizePath($mountPoint); + $this->mountPoint = Filesystem::normalizePath($mountPoint); } /** @@ -182,7 +168,7 @@ class StorageConfig implements \JsonSerializable { * @param Backend $backend */ public function setBackend(Backend $backend) { - $this->backend= $backend; + $this->backend = $backend; } /** @@ -214,12 +200,12 @@ class StorageConfig implements \JsonSerializable { * @param array $backendOptions backend options */ public function setBackendOptions($backendOptions) { - if($this->getBackend() instanceof Backend) { + if ($this->getBackend() instanceof Backend) { $parameters = $this->getBackend()->getParameters(); - foreach($backendOptions as $key => $value) { - if(isset($parameters[$key])) { + foreach ($backendOptions as $key => $value) { + if (isset($parameters[$key])) { switch ($parameters[$key]->getType()) { - case \OCA\Files_External\Lib\DefinitionParameter::VALUE_BOOLEAN: + case DefinitionParameter::VALUE_BOOLEAN: $value = (bool)$value; break; } @@ -260,7 +246,7 @@ class StorageConfig implements \JsonSerializable { } /** - * Sets the mount priotity + * Sets the mount priority * * @param int $priority priority */ @@ -271,7 +257,7 @@ class StorageConfig implements \JsonSerializable { /** * Returns the users for which to mount this storage * - * @return array applicable users + * @return list<string> applicable users */ public function getApplicableUsers() { return $this->applicableUsers; @@ -280,7 +266,7 @@ class StorageConfig implements \JsonSerializable { /** * Sets the users for which to mount this storage * - * @param array|null $applicableUsers applicable users + * @param list<string>|null $applicableUsers applicable users */ public function setApplicableUsers($applicableUsers) { if (is_null($applicableUsers)) { @@ -292,7 +278,7 @@ class StorageConfig implements \JsonSerializable { /** * Returns the groups for which to mount this storage * - * @return array applicable groups + * @return list<string> applicable groups */ public function getApplicableGroups() { return $this->applicableGroups; @@ -301,7 +287,7 @@ class StorageConfig implements \JsonSerializable { /** * Sets the groups for which to mount this storage * - * @param array|null $applicableGroups applicable groups + * @param list<string>|null $applicableGroups applicable groups */ public function setApplicableGroups($applicableGroups) { if (is_null($applicableGroups)) { @@ -380,14 +366,14 @@ class StorageConfig implements \JsonSerializable { } /** - * @return int self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAl + * @return int self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL */ public function getType() { return $this->type; } /** - * @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAl + * @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL */ public function setType($type) { $this->type = $type; @@ -395,14 +381,19 @@ class StorageConfig implements \JsonSerializable { /** * Serialize config to JSON - * - * @return array + * @return Files_ExternalStorageConfig */ - public function jsonSerialize() { + public function jsonSerialize(bool $obfuscate = false): array { $result = []; if (!is_null($this->id)) { $result['id'] = $this->id; } + + // obfuscate sensitive data if requested + if ($obfuscate) { + $this->formatStorageForUI(); + } + $result['mountPoint'] = $this->mountPoint; $result['backend'] = $this->backend->getIdentifier(); $result['authMechanism'] = $this->authMechanism->getIdentifier(); @@ -426,7 +417,22 @@ class StorageConfig implements \JsonSerializable { $result['statusMessage'] = $this->statusMessage; } $result['userProvided'] = $this->authMechanism instanceof IUserProvided; - $result['type'] = ($this->getType() === self::MOUNT_TYPE_PERSONAl) ? 'personal': 'system'; + $result['type'] = ($this->getType() === self::MOUNT_TYPE_PERSONAL) ? 'personal': 'system'; return $result; } + + protected function formatStorageForUI(): void { + /** @var DefinitionParameter[] $parameters */ + $parameters = array_merge($this->getBackend()->getParameters(), $this->getAuthMechanism()->getParameters()); + + $options = $this->getBackendOptions(); + foreach ($options as $key => $value) { + foreach ($parameters as $parameter) { + if ($parameter->getName() === $key && $parameter->getType() === DefinitionParameter::VALUE_PASSWORD) { + $this->setBackendOption($key, DefinitionParameter::UNMODIFIED_PLACEHOLDER); + break; + } + } + } + } } diff --git a/apps/files_external/lib/Lib/StorageModifierTrait.php b/apps/files_external/lib/Lib/StorageModifierTrait.php index 3327869e1a3..4062ff1635e 100644 --- a/apps/files_external/lib/Lib/StorageModifierTrait.php +++ b/apps/files_external/lib/Lib/StorageModifierTrait.php @@ -1,32 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCP\IUser; -use \OCP\Files\Storage; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; -use \OCP\Files\StorageNotAvailableException; +use OCP\Files\Storage\IStorage; +use OCP\Files\StorageNotAvailableException; +use OCP\IUser; /** * Trait for objects that can modify StorageConfigs and wrap Storages @@ -46,25 +29,22 @@ trait StorageModifierTrait { /** * Modify a StorageConfig parameters * - * @param StorageConfig $storage - * @param IUser $user User the storage is being used as + * @param StorageConfig &$storage + * @param ?IUser $user User the storage is being used as + * @return void * @throws InsufficientDataForMeaningfulAnswerException * @throws StorageNotAvailableException */ - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { } /** - * Wrap a Storage if necessary + * Wrap a storage if necessary * - * @param Storage $storage - * @return Storage * @throws InsufficientDataForMeaningfulAnswerException * @throws StorageNotAvailableException */ - public function wrapStorage(Storage $storage) { + public function wrapStorage(IStorage $storage): IStorage { return $storage; } - } - diff --git a/apps/files_external/lib/Lib/VisibilityTrait.php b/apps/files_external/lib/Lib/VisibilityTrait.php index 7d886de04dd..62b26f3edb1 100644 --- a/apps/files_external/lib/Lib/VisibilityTrait.php +++ b/apps/files_external/lib/Lib/VisibilityTrait.php @@ -1,28 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\BackendService; /** * Trait to implement visibility mechanics for a configuration class @@ -133,5 +118,4 @@ trait VisibilityTrait { public function removeAllowedVisibility($allowedVisibility) { return $this->setAllowedVisibility($this->allowedVisibility & ~$allowedVisibility); } - } |