diff options
Diffstat (limited to 'apps/files_external/lib/Lib')
63 files changed, 2252 insertions, 2456 deletions
diff --git a/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php b/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php index f318ba71a20..c86c88a13d7 100644 --- a/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php +++ b/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php @@ -1,28 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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 OCA\Files_External\Lib\Auth\AuthMechanism; diff --git a/apps/files_external/lib/Lib/Auth/AuthMechanism.php b/apps/files_external/lib/Lib/Auth/AuthMechanism.php index 512a0405fc0..7b0544100fb 100644 --- a/apps/files_external/lib/Lib/Auth/AuthMechanism.php +++ b/apps/files_external/lib/Lib/Auth/AuthMechanism.php @@ -1,32 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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\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; @@ -38,7 +22,7 @@ use OCA\Files_External\Lib\VisibilityTrait; * 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. @@ -51,12 +35,11 @@ use OCA\Files_External\Lib\VisibilityTrait; * - StorageModifierTrait * Object can affect storage mounting */ -class AuthMechanism implements \JsonSerializable { +class AuthMechanism implements \JsonSerializable, IIdentifier, IFrontendDefinition { /** Standard authentication schemes */ public const SCHEME_NULL = 'null'; public const SCHEME_BUILTIN = 'builtin'; public const SCHEME_PASSWORD = 'password'; - public const SCHEME_OAUTH1 = 'oauth1'; public const SCHEME_OAUTH2 = 'oauth2'; public const SCHEME_PUBLICKEY = 'publickey'; public const SCHEME_OPENSTACK = 'openstack'; @@ -91,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(); diff --git a/apps/files_external/lib/Lib/Auth/Builtin.php b/apps/files_external/lib/Lib/Auth/Builtin.php index 0f3d5342c6e..8e12a6daca6 100644 --- a/apps/files_external/lib/Lib/Auth/Builtin.php +++ b/apps/files_external/lib/Lib/Auth/Builtin.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: 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; diff --git a/apps/files_external/lib/Lib/Auth/IUserProvided.php b/apps/files_external/lib/Lib/Auth/IUserProvided.php index 9ed2b76a057..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 7c3ba64302e..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 <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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; /** diff --git a/apps/files_external/lib/Lib/Auth/NullMechanism.php b/apps/files_external/lib/Lib/Auth/NullMechanism.php index e7a30ea613e..8e2e5b656b2 100644 --- a/apps/files_external/lib/Lib/Auth/NullMechanism.php +++ b/apps/files_external/lib/Lib/Auth/NullMechanism.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: 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; 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 d93a09292ad..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 Morris Jobke <hey@morrisjobke.de> - * @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/> - * - */ - -namespace OCA\Files_External\Lib\Auth\OAuth1; - -use OCA\Files_External\Lib\Auth\AuthMechanism; -use OCA\Files_External\Lib\DefinitionParameter; -use OCP\IL10N; - -/** - * 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 17add489776..beaf73c2344 100644 --- a/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php +++ b/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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 OCA\Files_External\Lib\Auth\AuthMechanism; @@ -39,12 +22,14 @@ class OAuth2 extends AuthMechanism { ->setText($l->t('OAuth2')) ->addParameters([ (new DefinitionParameter('configured', 'configured')) - ->setType(DefinitionParameter::VALUE_HIDDEN), + ->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/OpenStackV2.php b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php index 723ecc5068b..3b1c9f123af 100644 --- a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php +++ b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php @@ -1,28 +1,10 @@ <?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> - * @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: 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; @@ -39,7 +21,7 @@ class OpenStackV2 extends AuthMechanism { ->setScheme(self::SCHEME_OPENSTACK) ->setText($l->t('OpenStack v2')) ->addParameters([ - new DefinitionParameter('user', $l->t('Username')), + new DefinitionParameter('user', $l->t('Login')), (new DefinitionParameter('password', $l->t('Password'))) ->setType(DefinitionParameter::VALUE_PASSWORD), new DefinitionParameter('tenant', $l->t('Tenant name')), diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php index 8ef66cc5f11..b5d185fd374 100644 --- a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php +++ b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php @@ -3,30 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Julien Lutran <julien.lutran@corp.ovh.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.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: 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; @@ -43,7 +22,7 @@ class OpenStackV3 extends AuthMechanism { ->setScheme(self::SCHEME_OPENSTACK) ->setText($l->t('OpenStack v3')) ->addParameters([ - new DefinitionParameter('user', $l->t('Username')), + new DefinitionParameter('user', $l->t('Login')), new DefinitionParameter('domain', $l->t('Domain')), (new DefinitionParameter('password', $l->t('Password'))) ->setType(DefinitionParameter::VALUE_PASSWORD), diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php b/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php index 4fa2a07440c..b1d1068e586 100644 --- a/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php +++ b/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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 OCA\Files_External\Lib\Auth\AuthMechanism; @@ -38,7 +21,7 @@ class Rackspace extends AuthMechanism { ->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 1094f778557..916b496b506 100644 --- a/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php +++ b/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php @@ -1,28 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2015, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @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: 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; @@ -38,13 +20,12 @@ use OCP\Security\ICredentialsManager; */ class GlobalAuth extends AuthMechanism { public const CREDENTIALS_IDENTIFIER = 'password::global'; + private const PWD_PLACEHOLDER = '************************'; - /** @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)) { diff --git a/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php b/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php index b8279f5ca61..ce38140b6ee 100644 --- a/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php +++ b/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php @@ -1,32 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2015, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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\DefinitionParameter; use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Listener\StorePasswordListener; @@ -36,6 +18,8 @@ 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; @@ -46,25 +30,23 @@ use OCP\User\Events\UserLoggedInEvent; class LoginCredentials extends AuthMechanism { public const CREDENTIALS_IDENTIFIER = 'password::logincredentials/credentials'; - /** @var ISession */ - protected $session; - - /** @var ICredentialsManager */ - protected $credentialsManager; - - /** @var CredentialsStore */ - private $credentialsStore; - - public function __construct(IL10N $l, ISession $session, ICredentialsManager $credentialsManager, CredentialsStore $credentialsStore, IEventDispatcher $eventDispatcher) { - $this->session = $session; - $this->credentialsManager = $credentialsManager; - $this->credentialsStore = $credentialsStore; - + 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), ]); $eventDispatcher->addServiceListener(UserLoggedInEvent::class, StorePasswordListener::class); @@ -86,7 +68,7 @@ class LoginCredentials extends AuthMechanism { $credentials = [ 'user' => $sessionCredentials->getLoginName(), - 'password' => $sessionCredentials->getPassword() + 'password' => $sessionCredentials->getPassword(), ]; $this->credentialsManager->store($user->getUID(), self::CREDENTIALS_IDENTIFIER, $credentials); @@ -98,13 +80,34 @@ class LoginCredentials extends AuthMechanism { 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'); } $credentials = $this->getCredentials($user); - $storage->setBackendOption('user', $credentials['user']); + $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('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 79be4282014..d4291148e3e 100644 --- a/apps/files_external/lib/Lib/Auth/Password/Password.php +++ b/apps/files_external/lib/Lib/Auth/Password/Password.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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 OCA\Files_External\Lib\Auth\AuthMechanism; @@ -36,9 +19,9 @@ class Password extends AuthMechanism { $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 d6f2d3f844f..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@winzerhof-wurst.at> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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,30 +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 index ff95e225e67..cb7165261ac 100644 --- a/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php +++ b/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php @@ -3,33 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.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: 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; @@ -43,17 +23,15 @@ use OCP\Security\ICredentialsManager; class UserGlobalAuth extends AuthMechanism { private 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::user') ->setVisibility(BackendService::VISIBILITY_DEFAULT) ->setScheme(self::SCHEME_PASSWORD) - ->setText($l->t('Global credentials, user entered')); + ->setText($l->t('Global credentials, manually entered')); } public function saveBackendOptions(IUser $user, $id, $backendOptions) { @@ -62,6 +40,12 @@ class UserGlobalAuth extends AuthMechanism { 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'], @@ -70,7 +54,10 @@ class UserGlobalAuth extends AuthMechanism { $this->credentialsManager->store($user->getUID(), self::CREDENTIALS_IDENTIFIER, $credentials); } - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { if ($user === null) { throw new InsufficientDataForMeaningfulAnswerException('No credentials saved'); } diff --git a/apps/files_external/lib/Lib/Auth/Password/UserProvided.php b/apps/files_external/lib/Lib/Auth/Password/UserProvided.php index d13c2090258..b158392f6eb 100644 --- a/apps/files_external/lib/Lib/Auth/Password/UserProvided.php +++ b/apps/files_external/lib/Lib/Auth/Password/UserProvided.php @@ -1,28 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2015, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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; @@ -41,19 +23,17 @@ use OCP\Security\ICredentialsManager; class UserProvided extends AuthMechanism implements IUserProvided { public 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) @@ -66,13 +46,21 @@ class UserProvided extends AuthMechanism implements IUserProvided { } 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'); } diff --git a/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php b/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php index f3e6c891e73..ad95c743d2d 100644 --- a/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php +++ b/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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 OCA\Files_External\Lib\Auth\AuthMechanism; @@ -37,31 +20,37 @@ use phpseclib\Crypt\RSA as RSACrypt; */ 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('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); } diff --git a/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php b/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php index e7c523f3911..8f58b71d5ac 100644 --- a/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php +++ b/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.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: 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; @@ -36,18 +19,16 @@ use phpseclib\Crypt\RSA as RSACrypt; */ class RSAPrivateKey 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_private') ->setScheme(self::SCHEME_PUBLICKEY) ->setText($l->t('RSA private key')) ->addParameters([ - new DefinitionParameter('user', $l->t('Username')), + new DefinitionParameter('user', $l->t('Login')), (new DefinitionParameter('password', $l->t('Password'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL) ->setType(DefinitionParameter::VALUE_PASSWORD), @@ -55,11 +36,18 @@ class RSAPrivateKey extends AuthMechanism { ]); } - 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); } 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 index 2051a192dad..9210209192a 100644 --- a/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php +++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2018 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: 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; diff --git a/apps/files_external/lib/Lib/Backend/AmazonS3.php b/apps/files_external/lib/Lib/Backend/AmazonS3.php index 5473975f372..464b03b55e0 100644 --- a/apps/files_external/lib/Lib/Backend/AmazonS3.php +++ b/apps/files_external/lib/Lib/Backend/AmazonS3.php @@ -1,31 +1,14 @@ <?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> - * @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\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; @@ -47,14 +30,24 @@ class AmazonS3 extends Backend { ->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 69d41f85ecf..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\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\VisibilityTrait; * 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,7 +43,7 @@ use OCA\Files_External\Lib\VisibilityTrait; * - StorageModifierTrait * Object can affect storage mounting */ -class Backend implements \JsonSerializable { +class Backend implements \JsonSerializable, IIdentifier, IFrontendDefinition { use VisibilityTrait; use FrontendDefinitionTrait; use PriorityTrait; @@ -74,7 +61,7 @@ class Backend implements \JsonSerializable { private $legacyAuthMechanism; /** - * @return string + * @return class-string<IStorage> */ public function getStorageClass() { return $this->storageClass; @@ -119,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(); diff --git a/apps/files_external/lib/Lib/Backend/DAV.php b/apps/files_external/lib/Lib/Backend/DAV.php index 0991c5ac1df..dea9e7c5e77 100644 --- a/apps/files_external/lib/Lib/Backend/DAV.php +++ b/apps/files_external/lib/Lib/Backend/DAV.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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\Auth\AuthMechanism; @@ -44,7 +27,8 @@ class DAV extends Backend { (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 bc35a5d386a..72a8184c9b9 100644 --- a/apps/files_external/lib/Lib/Backend/FTP.php +++ b/apps/files_external/lib/Lib/Backend/FTP.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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\Auth\AuthMechanism; @@ -41,10 +24,13 @@ class FTP extends Backend { ->setText($l->t('FTP')) ->addParameters([ 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 148074a2391..48912c0e49e 100644 --- a/apps/files_external/lib/Lib/Backend/InvalidBackend.php +++ b/apps/files_external/lib/Lib/Backend/InvalidBackend.php @@ -1,28 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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\StorageConfig; @@ -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 */ - public 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,7 +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 d618c756fbf..9c7e5b01bc3 100644 --- a/apps/files_external/lib/Lib/Backend/LegacyBackend.php +++ b/apps/files_external/lib/Lib/Backend/LegacyBackend.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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\Auth\Builtin; @@ -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) diff --git a/apps/files_external/lib/Lib/Backend/Local.php b/apps/files_external/lib/Lib/Backend/Local.php index 67b50e127e9..56940b8e83b 100644 --- a/apps/files_external/lib/Lib/Backend/Local.php +++ b/apps/files_external/lib/Lib/Backend/Local.php @@ -1,34 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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\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) { @@ -46,4 +31,8 @@ class Local extends Backend { ->setLegacyAuthMechanism($legacyAuth) ; } + + 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 876f8709cc3..0c0e2c6d300 100644 --- a/apps/files_external/lib/Lib/Backend/OwnCloud.php +++ b/apps/files_external/lib/Lib/Backend/OwnCloud.php @@ -1,28 +1,10 @@ <?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> - * @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\Auth\AuthMechanism; @@ -42,7 +24,8 @@ class OwnCloud extends Backend { (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 a7f97c6b79a..0926cf7fd93 100644 --- a/apps/files_external/lib/Lib/Backend/SFTP.php +++ b/apps/files_external/lib/Lib/Backend/SFTP.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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\Auth\AuthMechanism; @@ -38,6 +21,8 @@ class SFTP extends Backend { ->setText($l->t('SFTP')) ->addParameters([ 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), ]) diff --git a/apps/files_external/lib/Lib/Backend/SFTP_Key.php b/apps/files_external/lib/Lib/Backend/SFTP_Key.php index 924d6a62ffe..278fae3fba7 100644 --- a/apps/files_external/lib/Lib/Backend/SFTP_Key.php +++ b/apps/files_external/lib/Lib/Backend/SFTP_Key.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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\Auth\AuthMechanism; diff --git a/apps/files_external/lib/Lib/Backend/SMB.php b/apps/files_external/lib/Lib/Backend/SMB.php index 5344bf5f78c..e86ad98880c 100644 --- a/apps/files_external/lib/Lib/Backend/SMB.php +++ b/apps/files_external/lib/Lib/Backend/SMB.php @@ -1,51 +1,36 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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 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\LegacyDependencyCheckPolyfill; +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 ->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')), @@ -56,26 +41,32 @@ class SMB extends Backend { (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 user has no read permissions, comes with a performance penalty")), + ->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_HIDDEN) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + ->setType(DefinitionParameter::VALUE_TEXT) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setFlag(DefinitionParameter::FLAG_HIDDEN), ]) ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) ->addAuthScheme(AuthMechanism::SCHEME_SMB) ->setLegacyAuthMechanism($legacyAuth); } - /** - * @param StorageConfig $storage - * @param IUser $user - */ - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { + 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'), @@ -86,6 +77,43 @@ class SMB extends Backend { 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'); } @@ -93,4 +121,20 @@ class SMB extends 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 439d85164cc..bcb8d0fbf16 100644 --- a/apps/files_external/lib/Lib/Backend/SMB_OC.php +++ b/apps/files_external/lib/Lib/Backend/SMB_OC.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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\Auth\AuthMechanism; @@ -43,10 +26,10 @@ class SMB_OC extends Backend { $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('username_as_share', $l->t('Login as share'))) ->setType(DefinitionParameter::VALUE_BOOLEAN), (new DefinitionParameter('share', $l->t('Share'))) ->setFlag(DefinitionParameter::FLAG_OPTIONAL), @@ -60,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) { diff --git a/apps/files_external/lib/Lib/Backend/Swift.php b/apps/files_external/lib/Lib/Backend/Swift.php index b9d3c9c75ce..37527ba3dbb 100644 --- a/apps/files_external/lib/Lib/Backend/Swift.php +++ b/apps/files_external/lib/Lib/Backend/Swift.php @@ -1,30 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Julien Lutran <julien.lutran@corp.ovh.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 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\Auth\AuthMechanism; diff --git a/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php b/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php index e8f142c01da..0c2e90a243c 100644 --- a/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php +++ b/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.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: 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; diff --git a/apps/files_external/lib/Lib/Config/IBackendProvider.php b/apps/files_external/lib/Lib/Config/IBackendProvider.php index 7757f204c8c..44c460c3138 100644 --- a/apps/files_external/lib/Lib/Config/IBackendProvider.php +++ b/apps/files_external/lib/Lib/Config/IBackendProvider.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: 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; diff --git a/apps/files_external/lib/Lib/DefinitionParameter.php b/apps/files_external/lib/Lib/DefinitionParameter.php index 8415c3214f5..a73dd2df967 100644 --- a/apps/files_external/lib/Lib/DefinitionParameter.php +++ b/apps/files_external/lib/Lib/DefinitionParameter.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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; /** @@ -36,48 +19,45 @@ class DefinitionParameter implements \JsonSerializable { public const VALUE_TEXT = 0; public const VALUE_BOOLEAN = 1; public const VALUE_PASSWORD = 2; - public const VALUE_HIDDEN = 3; /** Flag constants */ public const FLAG_NONE = 0; public const FLAG_OPTIONAL = 1; public const FLAG_USER_PROVIDED = 2; - - /** @var string name of parameter */ - private $name; - - /** @var string human-readable parameter text */ - private $text; + public const FLAG_HIDDEN = 4; /** @var string human-readable parameter tooltip */ - private $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; } @@ -86,7 +66,7 @@ class DefinitionParameter implements \JsonSerializable { * * @return int */ - public function getType() { + public function getType(): int { return $this->type; } @@ -96,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'; @@ -120,7 +116,7 @@ class DefinitionParameter implements \JsonSerializable { /** * @return int */ - public function getFlags() { + public function getFlags(): int { return $this->flags; } @@ -128,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; } @@ -137,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; } @@ -146,7 +142,7 @@ class DefinitionParameter implements \JsonSerializable { * @param int $flag * @return bool */ - public function isFlagSet($flag) { + public function isFlagSet(int $flag): bool { return (bool)($this->flags & $flag); } @@ -168,19 +164,22 @@ class DefinitionParameter implements \JsonSerializable { /** * Serialize into JSON for client-side JS - * - * @return string */ - public function jsonSerialize() { - return [ + public function jsonSerialize(): array { + $result = [ 'value' => $this->getText(), 'flags' => $this->getFlags(), '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); } @@ -191,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 b88a111392a..644132b82bc 100644 --- a/apps/files_external/lib/Lib/DependencyTrait.php +++ b/apps/files_external/lib/Lib/DependencyTrait.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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; /** diff --git a/apps/files_external/lib/Lib/FrontendDefinitionTrait.php b/apps/files_external/lib/Lib/FrontendDefinitionTrait.php index 300abd15db3..0f280d1d486 100644 --- a/apps/files_external/lib/Lib/FrontendDefinitionTrait.php +++ b/apps/files_external/lib/Lib/FrontendDefinitionTrait.php @@ -1,27 +1,10 @@ <?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; /** @@ -30,62 +13,45 @@ namespace OCA\Files_External\Lib; 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; } @@ -93,7 +59,7 @@ trait FrontendDefinitionTrait { /** * @return string[] */ - public function getCustomJs() { + public function getCustomJs(): array { return $this->customJs; } @@ -101,17 +67,15 @@ trait FrontendDefinitionTrait { * @param string $custom * @return self */ - public function addCustomJs($custom) { + public function addCustomJs(string $custom): self { $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; @@ -127,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()) { 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 6bdde976753..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, diff --git a/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php b/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php index 8205dcfd9f1..1e872b35072 100644 --- a/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php +++ b/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php @@ -1,28 +1,10 @@ <?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> - * @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 OCP\Files\StorageNotAvailableException; @@ -39,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 787872511f9..f6311fae83e 100644 --- a/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php +++ b/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php @@ -1,34 +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; +use OCP\Files\Storage\IStorage; + /** * Polyfill for checking dependencies using legacy Storage::checkDependencies() */ trait LegacyDependencyCheckPolyfill { /** - * @return string + * @return class-string<IStorage> */ abstract public function getStorageClass(); @@ -55,7 +42,7 @@ trait LegacyDependencyCheckPolyfill { $module = $key; $message = $value; } - $value = new MissingDependency($module, $this); + $value = new MissingDependency($module); $value->setMessage($message); } $ret[] = $value; diff --git a/apps/files_external/lib/Lib/MissingDependency.php b/apps/files_external/lib/Lib/MissingDependency.php index f91154baec9..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 1570ba15573..2812df6ad6a 100644 --- a/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php +++ b/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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; @@ -31,11 +13,6 @@ use OCP\Files\Notify\INotifyHandler; class SMBNotifyHandler implements INotifyHandler { /** - * @var \Icewind\SMB\INotifyHandler - */ - private $shareNotifyHandler; - - /** * @var string */ private $root; @@ -48,9 +25,11 @@ 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) { diff --git a/apps/files_external/lib/Lib/PersonalMount.php b/apps/files_external/lib/Lib/PersonalMount.php index d8f2aeea56e..d9dbddd1449 100644 --- a/apps/files_external/lib/Lib/PersonalMount.php +++ b/apps/files_external/lib/Lib/PersonalMount.php @@ -1,68 +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> - * @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: 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\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 ExternalMountPoint implements MoveableMount { - /** @var UserStoragesService */ - protected $storagesService; - - /** @var int */ - protected $numericStorageId; - /** * @param UserStoragesService $storagesService * @param int $storageId * @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, + protected UserStoragesService $storagesService, StorageConfig $storageConfig, - $storageId, + /** @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, - $mountId = null + $mountId = null, ) { parent::__construct($storageConfig, $storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId); - $this->storagesService = $storagesService; - $this->numericStorageId = $storageId; } /** @@ -72,7 +49,7 @@ class PersonalMount extends ExternalMountPoint 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]); @@ -87,7 +64,7 @@ class PersonalMount extends ExternalMountPoint 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 12e30de231f..fad2c07e58c 100644 --- a/apps/files_external/lib/Lib/PriorityTrait.php +++ b/apps/files_external/lib/Lib/PriorityTrait.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: 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; @@ -48,13 +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 2b8f2b8613b..8754041b2fa 100644 --- a/apps/files_external/lib/Lib/SessionStorageWrapper.php +++ b/apps/files_external/lib/Lib/SessionStorageWrapper.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: 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\Storage\Wrapper\PermissionsMask; @@ -30,13 +14,12 @@ 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 9ea278d7229..5dc9e114532 100644 --- a/apps/files_external/lib/Lib/Storage/AmazonS3.php +++ b/apps/files_external/lib/Lib/Storage/AmazonS3.php @@ -1,86 +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 Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author enoch <lanxenet@hotmail.com> - * @author Johan Björk <johanimon@gmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Julius Härtl <jus@bitgrid.net> - * @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 Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Storage; -use Aws\Result; use Aws\S3\Exception\S3Exception; -use Aws\S3\S3Client; 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 CappedMemoryCache|Result[] */ - private $objectCache; + /** @var CappedMemoryCache<array|false> */ + private CappedMemoryCache $objectCache; + + /** @var CappedMemoryCache<bool> */ + private CappedMemoryCache $directoryCache; - /** @var CappedMemoryCache|bool[] */ - private $directoryCache; + /** @var CappedMemoryCache<array> */ + private CappedMemoryCache $filesCache; - /** @var CappedMemoryCache|array */ - private $filesCache; + private IMimeTypeDetector $mimeDetector; + private ?bool $versioningEnabled = null; + private ICache $memCache; - public function __construct($parameters) { + 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) { @@ -90,24 +72,24 @@ 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); @@ -116,20 +98,24 @@ class AmazonS3 extends \OC\Files\Storage\Common { unset($this->objectCache[$existingKey]); } } - unset($this->directoryCache[$key], $this->filesCache[$key]); + 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([ 'Bucket' => $this->bucket, 'Key' => $key - ]); + ] + $this->getSSECParameters())->toArray(); } catch (S3Exception $e) { if ($e->getStatusCode() >= 500) { throw $e; @@ -138,6 +124,10 @@ 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]; } @@ -150,77 +140,51 @@ class AmazonS3 extends \OC\Files\Storage\Common { * Implementation from flysystem-aws-s3-v3: * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694 * - * @param $path - * @return bool * @throws \Exception */ - private function doesDirectoryExist($path) { - if (!isset($this->directoryCache[$path])) { + private function doesDirectoryExist(string $path): bool { + if ($path === '.' || $path === '') { + return true; + } + $path = rtrim($path, '/') . '/'; + + 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. - try { - $result = $this->getConnection()->listObjects([ - 'Bucket' => $this->bucket, - 'Prefix' => rtrim($path, '/'), - 'MaxKeys' => 1, - 'Delimiter' => '/', - ]); + $result = $this->getConnection()->listObjectsV2([ + 'Bucket' => $this->bucket, + 'Prefix' => $path, + 'MaxKeys' => 1, + ]); - if ((isset($result['Contents'][0]['Key']) && $result['Contents'][0]['Key'] === rtrim($path, '/') . '/') - || isset($result['CommonPrefixes'])) { - $this->directoryCache[$path] = true; - } else { - $this->directoryCache[$path] = false; - } - } catch (S3Exception $e) { - if ($e->getStatusCode() === 403) { - $this->directoryCache[$path] = false; - } - throw $e; + if (isset($result['Contents'])) { + $this->directoryCache[$path] = true; + return true; } - } - return $this->directoryCache[$path]; - } + // empty directories have their own object + $object = $this->headObject($path); - /** - * 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 a repair step. requires iterating over all users and loading the mount.json from their home - * - * @param array $params - */ - public function updateLegacyId(array $params) { - $oldId = 'amazon::' . $params['key'] . md5($params['secret']); - - // find by old id or bucket - $stmt = \OC::$server->getDatabaseConnection()->prepare( - 'SELECT `numeric_id`, `id` FROM `*PREFIX*storages` WHERE `id` IN (?, ?)' - ); - $stmt->execute([$oldId, $this->id]); - while ($row = $stmt->fetch()) { - $storages[$row['id']] = $row['numeric_id']; + if ($object) { + $this->directoryCache[$path] = true; + return true; + } + } catch (S3Exception $e) { + if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) { + $this->directoryCache[$path] = false; + } + throw $e; } - 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); - } elseif (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([$this->id, $oldId]); - } - // 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') { @@ -232,7 +196,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { } } - public function mkdir($path) { + public function mkdir(string $path): bool { $path = $this->normalizePath($path); if ($this->is_dir($path)) { @@ -244,11 +208,14 @@ class AmazonS3 extends \OC\Files\Storage\Common { 'Bucket' => $this->bucket, 'Key' => $path . '/', 'Body' => '', - 'ContentType' => 'httpd/unix-directory' - ]); + 'ContentType' => FileInfo::MIMETYPE_FOLDER + ] + $this->getSSECParameters()); $this->testTimeout(); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } @@ -257,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)) { @@ -277,18 +244,13 @@ 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 $this->batchDelete(); } - private function batchDelete($path = null) { + 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 ]; @@ -314,91 +276,45 @@ class AmazonS3 extends \OC\Files\Storage\Common { } // we reached the end when the list is no longer truncated } while ($objects['IsTruncated']); + if ($path !== '' && $path !== null) { + $this->deleteObject($path); + } } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } return true; } - public function opendir($path) { - $path = $this->normalizePath($path); - - if ($this->isRoot($path)) { - $path = ''; - } else { - $path .= '/'; - } - + public function opendir(string $path) { try { - $files = []; - $results = $this->getConnection()->getPaginator('ListObjects', [ - 'Bucket' => $this->bucket, - 'Delimiter' => '/', - 'Prefix' => $path, - ]); - - foreach ($results as $result) { - // sub folders - if (is_array($result['CommonPrefixes'])) { - foreach ($result['CommonPrefixes'] as $prefix) { - $directoryName = trim($prefix['Prefix'], '/'); - $files[] = substr($directoryName, strlen($path)); - $this->directoryCache[$directoryName] = true; - } - } - if (is_array($result['Contents'])) { - 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; - - // store this information for later usage - $this->filesCache[$path . $file] = [ - 'ContentLength' => $object['Size'], - 'LastModified' => (string)$object['LastModified'], - ]; - } - } - } - - return IteratorDirectory::wrap($files); + $content = iterator_to_array($this->getDirectoryContent($path)); + return IteratorDirectory::wrap(array_map(function (array $item) { + return $item['name']; + }, $content)); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); return false; } } - public function stat($path) { + public function stat(string $path): array|false { $path = $this->normalizePath($path); - try { - $stat = []; - if ($this->is_dir($path)) { - //folders don't really exist - $stat['size'] = -1; //unknown - $stat['mtime'] = time(); - $cacheEntry = $this->getCache()->get($path); - if ($cacheEntry instanceof CacheEntry && $this->getMountOption('filesystem_check_changes', 1) !== 1) { - $stat['size'] = $cacheEntry->getSize(); - $stat['mtime'] = $cacheEntry->getMTime(); - } - } else { - $stat['size'] = $this->getContentLength($path); - $stat['mtime'] = strtotime($this->getLastModified($path)); + if ($this->is_dir($path)) { + $stat = $this->getDirectoryMetaData($path); + } else { + $object = $this->headObject($path); + if ($object === false) { + return false; } - $stat['atime'] = time(); - - return $stat; - } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); - return false; + $stat = $this->objectToMetaData($object); } + $stat['atime'] = time(); + + return $stat; } /** @@ -406,11 +322,8 @@ class AmazonS3 extends \OC\Files\Storage\Common { * * When the information is already present (e.g. opendir has been called before) * this value is return. Otherwise a headObject is emitted. - * - * @param $path - * @return int|mixed */ - private function getContentLength($path) { + private function getContentLength(string $path): int { if (isset($this->filesCache[$path])) { return (int)$this->filesCache[$path]['ContentLength']; } @@ -428,11 +341,8 @@ class AmazonS3 extends \OC\Files\Storage\Common { * * When the information is already present (e.g. opendir has been called before) * this value is return. Otherwise a headObject is emitted. - * - * @param $path - * @return mixed|string */ - private function getLastModified($path) { + private function getLastModified(string $path): string { if (isset($this->filesCache[$path])) { return $this->filesCache[$path]['LastModified']; } @@ -445,7 +355,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { return 'now'; } - public function is_dir($path) { + public function is_dir(string $path): bool { $path = $this->normalizePath($path); if (isset($this->filesCache[$path])) { @@ -453,14 +363,17 @@ class AmazonS3 extends \OC\Files\Storage\Common { } try { - return $this->isRoot($path) || $this->doesDirectoryExist($path); + return $this->doesDirectoryExist($path); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $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)) { @@ -468,6 +381,9 @@ class AmazonS3 extends \OC\Files\Storage\Common { } try { + if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) { + return 'dir'; + } if (isset($this->filesCache[$path]) || $this->headObject($path)) { return 'file'; } @@ -475,14 +391,17 @@ class AmazonS3 extends \OC\Files\Storage\Common { return 'dir'; } } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $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; @@ -490,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)) { @@ -501,14 +420,17 @@ class AmazonS3 extends \OC\Files\Storage\Common { $this->deleteObject($path); $this->invalidateCache($path); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $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) { @@ -522,16 +444,19 @@ class AmazonS3 extends \OC\Files\Storage\Common { try { return $this->readObject($path); - } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } case 'w': case 'wb': - $tmpFile = \OC::$server->getTempManager()->getTemporaryFile(); + $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': @@ -549,21 +474,21 @@ class AmazonS3 extends \OC\Files\Storage\Common { } else { $ext = ''; } - $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($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) { + public function touch(string $path, ?int $mtime = null): bool { if (is_null($mtime)) { $mtime = time(); } @@ -572,20 +497,25 @@ class AmazonS3 extends \OC\Files\Storage\Common { ]; try { - if (!$this->file_exists($path)) { - $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) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } @@ -593,76 +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([ - '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) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } } else { - $this->remove($path2); + $this->remove($target); try { - $this->getConnection()->copyObject([ - 'Bucket' => $this->bucket, - 'Key' => $path2 . '/', - 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1 . '/') - ]); + $this->mkdir($target); $this->testTimeout(); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $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; } } @@ -670,27 +593,30 @@ class AmazonS3 extends \OC\Files\Storage\Common { return true; } - public function test() { + 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); unlink($tmpFile); return true; } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } } @@ -698,7 +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 48e312ecd7f..944964de7a6 100644 --- a/apps/files_external/lib/Lib/Storage/FTP.php +++ b/apps/files_external/lib/Lib/Storage/FTP.php @@ -1,170 +1,364 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Felix Moeller <mail@felixmoeller.de> - * @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 Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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\CountWrapper; use Icewind\Streams\IteratorDirectory; -use Icewind\Streams\RetryWrapper; +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; + + /** @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 { - $url = $this->constructUrl($path); - $result = unlink($url); - clearstatcache(true, $url); - return $result; + return false; + } + } + + 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 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 fopen($path,$mode) { + + 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(['ftp' => ['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 = ''; - } - $tmpFile = \OC::$server->getTempManager()->getTemporaryFile(); - if ($this->file_exists($path)) { - $this->getFile($path, $tmpFile); + if (!$this->isCreatable(dirname($path))) { + return false; + } + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile(); } - $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 opendir($path) { - $dh = parent::opendir($path); - if (is_resource($dh)) { - $files = []; - while (($file = readdir($dh)) !== false) { - if ($file != '.' && $file != '..' && strpos($file, '#') === false) { - $files[] = $file; - } - } - return IteratorDirectory::wrap($files); - } else { - return false; + 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; } + public function readStream(string $path) { + $stream = fopen('php://temp', 'w+'); + $result = $this->getConnection()->fget($stream, $this->buildPath($path)); + rewind($stream); - public function writeBack($tmpFile, $path) { - $this->uploadFile($tmpFile, $path); - unlink($tmpFile); + if (!$result) { + fclose($stream); + return false; + } + return $stream; } - /** - * check if php-ftp is installed - */ - public static function checkDependencies() { - if (function_exists('ftp_login')) { - return true; + public function touch(string $path, ?int $mtime = null): bool { + if ($this->file_exists($path)) { + return false; } else { - return ['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 501d7e68e1c..12c305de750 100644 --- a/apps/files_external/lib/Lib/Storage/OwnCloud.php +++ b/apps/files_external/lib/Lib/Storage/OwnCloud.php @@ -1,34 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @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 Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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; @@ -39,20 +18,20 @@ use Sabre\DAV\Client; * http://%host/%context/remote.php/webdav/%root * */ -class OwnCloud extends \OC\Files\Storage\DAV implements IDisableEncryptionStorage { +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; - } elseif (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, '/'); @@ -61,24 +40,24 @@ class OwnCloud extends \OC\Files\Storage\DAV implements IDisableEncryptionStorag $host = substr($host, 0, $hostSlashPos); } - if (substr($contextPath, -1) !== '/') { + if (!str_ends_with($contextPath, '/')) { $contextPath .= '/'; } - if (isset($params['root'])) { - $root = '/' . ltrim($params['root'], '/'); + 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($params); + parent::__construct($parameters); } - public function needsPartFile() { + 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 5f3d02dfd0a..a2f5bafcca1 100644 --- a/apps/files_external/lib/Lib/Storage/SFTP.php +++ b/apps/files_external/lib/Lib/Storage/SFTP.php @@ -1,51 +1,30 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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 Roeland Jago Douma <roeland@famdouma.nl> - * @author Ross Nicoll <jrn@jrn.me.uk> - * @author SA <stephen@mthosting.net> - * @author Senorsen <senorsen.zhang@gmail.com> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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 { +class SFTP extends Common { private $host; private $user; private $root; @@ -57,14 +36,19 @@ class SFTP extends \OC\Files\Storage\Common { * @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; } @@ -79,28 +63,25 @@ class SFTP extends \OC\Files\Storage\Common { } } - /** - * {@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']; + if (isset($parameters['public_key_auth'])) { + $this->auth[] = $parameters['public_key_auth']; } - if (isset($params['password']) && $params['password'] !== '') { - $this->auth[] = $params['password']; + if (isset($parameters['password']) && $parameters['password'] !== '') { + $this->auth[] = $parameters['password']; } if ($this->auth === []) { @@ -108,10 +89,14 @@ class SFTP extends \OC\Files\Storage\Common { } $this->root - = isset($params['root']) ? $this->cleanPath($params['root']) : '/'; + = isset($parameters['root']) ? $this->cleanPath($parameters['root']) : '/'; $this->root = '/' . ltrim($this->root, '/'); $this->root = rtrim($this->root, '/') . '/'; + + $this->knownMTimes = new CappedMemoryCache(); + + $this->mimeTypeDetector = Server::get(IMimeTypeDetector::class); } /** @@ -120,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; } @@ -154,10 +139,7 @@ class SFTP extends \OC\Files\Storage\Common { return $this->client; } - /** - * {@inheritdoc} - */ - public function test() { + public function test(): bool { if ( !isset($this->host) || !isset($this->user) @@ -167,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; @@ -182,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)) { @@ -247,10 +208,7 @@ 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)) { @@ -259,7 +217,7 @@ class SFTP extends \OC\Files\Storage\Common { $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]; @@ -273,10 +231,7 @@ class SFTP extends \OC\Files\Storage\Common { return []; } - /** - * {@inheritdoc} - */ - public function mkdir($path) { + public function mkdir(string $path): bool { try { return $this->getConnection()->mkdir($this->absPath($path)); } catch (\Exception $e) { @@ -284,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 @@ -299,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) { @@ -322,17 +271,17 @@ class SFTP extends \OC\Files\Storage\Common { } } - /** - * {@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) { @@ -340,10 +289,7 @@ class SFTP extends \OC\Files\Storage\Common { 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) { @@ -351,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) { @@ -362,27 +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); + $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' => $this->getConnection()]]); + $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(); - $context = stream_context_create(['sftp' => ['session' => $this->getConnection()]]); - return fopen('sftpwrite://' . trim($absPath, '/'), 'w', false, $context); + // 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+': @@ -393,7 +344,7 @@ class SFTP extends \OC\Files\Storage\Common { case 'x+': case 'c': case 'c+': - $context = stream_context_create(['sftp' => ['session' => $this->getConnection()]]); + $context = stream_context_create(['sftp' => ['session' => $connection]]); $handle = fopen($this->constructUrl($path), $mode, false, $context); return RetryWrapper::wrap($handle); } @@ -402,38 +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); } - /** - * {@inheritdoc} - */ - public function rename($source, $target) { + public function rename(string $source, string $target): bool { try { if ($this->file_exists($target)) { $this->unlink($target); @@ -448,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 ['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 index 1a721a0e2d8..7dedbd7035a 100644 --- a/apps/files_external/lib/Lib/Storage/SFTPReadStream.php +++ b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php @@ -3,28 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\Files_External\Lib\Storage; use Icewind\Streams\File; @@ -37,7 +18,7 @@ class SFTPReadStream implements File { /** @var \phpseclib\Net\SFTP */ private $sftp; - /** @var resource */ + /** @var string */ private $handle; /** @var int */ @@ -50,6 +31,8 @@ class SFTPReadStream implements File { 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)) { @@ -61,11 +44,9 @@ class SFTPReadStream implements File { /** * Load the source from the stream context and return the context options * - * @param string $name - * @return array * @throws \BadMethodCallException */ - protected function loadContext($name) { + protected function loadContext(string $name) { $context = stream_context_get_options($this->context); if (isset($context[$name])) { $context = $context[$name]; @@ -77,6 +58,9 @@ class SFTPReadStream implements File { } else { throw new \BadMethodCallException('Invalid context, session not set'); } + if (isset($context['size'])) { + $this->size = $context['size']; + } return $context; } @@ -120,7 +104,25 @@ class SFTPReadStream implements File { } public function stream_seek($offset, $whence = SEEK_SET) { - return false; + 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() { @@ -138,20 +140,23 @@ class SFTPReadStream implements File { $data = substr($this->buffer, 0, $count); $this->buffer = substr($this->buffer, $count); - if ($this->buffer === false) { - $this->buffer = ''; - } $this->readPosition += strlen($data); return $data; } - private function request_chunk($size) { + 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) { @@ -200,8 +205,13 @@ class SFTPReadStream implements File { } 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 index b71dcbb1be5..d64e89b5462 100644 --- a/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php +++ b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php @@ -3,28 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\Files_External\Lib\Storage; use Icewind\Streams\File; @@ -37,7 +18,7 @@ class SFTPWriteStream implements File { /** @var \phpseclib\Net\SFTP */ private $sftp; - /** @var resource */ + /** @var string */ private $handle; /** @var int */ @@ -61,11 +42,9 @@ class SFTPWriteStream implements File { /** * Load the source from the stream context and return the context options * - * @param string $name - * @return array * @throws \BadMethodCallException */ - protected function loadContext($name) { + protected function loadContext(string $name) { $context = stream_context_get_options($this->context); if (isset($context[$name])) { $context = $context[$name]; @@ -181,5 +160,6 @@ class SFTPWriteStream implements File { 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 952f6c08931..8f8750864e1 100644 --- a/apps/files_external/lib/Lib/Storage/SMB.php +++ b/apps/files_external/lib/Lib/Storage/SMB.php @@ -1,37 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jesús Macias <jmacias@solidgear.es> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Juan Pablo Villafañez <jvillafanez@solidgear.es> - * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es> - * @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 Roeland Jago Douma <roeland@famdouma.nl> - * @author Roland Tapken <roland@bitarbeiter.net> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_External\Lib\Storage; @@ -43,6 +15,7 @@ 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; @@ -50,22 +23,24 @@ use Icewind\SMB\IFileInfo; use Icewind\SMB\Native\NativeServer; use Icewind\SMB\Options; use Icewind\SMB\ServerFactory; -use Icewind\SMB\System; +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\ILogger; +use OCP\ITempManager; +use Psr\Log\LoggerInterface; class SMB extends Common implements INotifyStorage { /** @@ -83,91 +58,93 @@ class SMB extends Common implements INotifyStorage { */ protected $root; - /** - * @var \Icewind\SMB\IFileInfo[] - */ - protected $statCache; + /** @var CappedMemoryCache<IFileInfo> */ + protected CappedMemoryCache $statCache; - /** @var ILogger */ + /** @var LoggerInterface */ protected $logger; /** @var bool */ protected $showHidden; + private bool $caseSensitive; + /** @var bool */ protected $checkAcl; - public function __construct($params) { - if (!isset($params['host'])) { + public function __construct(array $parameters) { + if (!isset($parameters['host'])) { throw new \Exception('Invalid configuration, no host provided'); } - if (isset($params['auth'])) { - $auth = $params['auth']; - } elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) { - [$workgroup, $user] = $this->splitUser($params['user']); - $auth = new BasicAuth($user, $workgroup, $params['password']); + 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($params['logger'])) { - $this->logger = $params['logger']; + 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 { - $this->logger = \OC::$server->getLogger(); + $this->logger = \OCP\Server::get(LoggerInterface::class); } $options = new Options(); - if (isset($params['timeout'])) { - $timeout = (int)$params['timeout']; + if (isset($parameters['timeout'])) { + $timeout = (int)$parameters['timeout']; if ($timeout > 0) { $options->setTimeout($timeout); } } - $serverFactory = new ServerFactory($options); - $this->server = $serverFactory->createServer($params['host'], $auth); - $this->share = $this->server->getShare(trim($params['share'], '/')); + $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 = $params['root'] ?? '/'; + $this->root = $parameters['root'] ?? '/'; $this->root = '/' . ltrim($this->root, '/'); $this->root = rtrim($this->root, '/') . '/'; - $this->showHidden = isset($params['show_hidden']) && $params['show_hidden']; - $this->checkAcl = isset($params['check_acl']) && $params['check_acl']; + $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); } - private function splitUser($user) { - if (strpos($user, '/')) { + private function splitUser(string $user): array { + if (str_contains($user, '/')) { return explode('/', $user, 2); - } elseif (strpos($user, '\\')) { + } elseif (str_contains($user, '\\')) { return explode('\\', $user); - } else { - return [null, $user]; } + + return [null, $user]; } - /** - * @return string - */ - public function getId() { + 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->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 ''; } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) { @@ -178,48 +155,55 @@ class SMB extends Common implements INotifyStorage { } /** - * @param string $path - * @return \Icewind\SMB\IFileInfo * @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) { $this->throwUnavailable($e); + } catch (NotFoundException $e) { + throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e); } catch (ForbiddenException $e) { - // with php-smbclient, this exceptions is thrown when the provided password is invalid. + // 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 $e; + throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e); } } /** - * @param \Exception $e * @throws StorageAuthException */ - protected function throwUnavailable(\Exception $e) { - $this->logger->logException($e, ['message' => 'Error while getting file info']); + 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 - * - * @param IFileInfo $file - * @return ACL|null */ private function getACL(IFileInfo $file): ?ACL { - $acls = $file->getAcls(); + 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] = explode('\\', $user); // strip domain + [, $user] = $this->splitUser($user); // strip domain if ($user === $this->server->getAuth()->getUsername()) { return $acl; } @@ -229,17 +213,19 @@ class SMB extends Common implements INotifyStorage { } /** - * @param string $path - * @return \Icewind\SMB\IFileInfo[] + * @return \Generator<IFileInfo> * @throws StorageNotAvailableException */ - protected function getFolderContents($path): iterable { + protected function getFolderContents(string $path): iterable { try { $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; @@ -257,7 +243,7 @@ class SMB extends Common implements INotifyStorage { // 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()); - return false; + continue; } } @@ -268,22 +254,20 @@ class SMB extends Common implements INotifyStorage { yield $file; } } catch (ForbiddenException $e) { - $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]); + $this->logger->debug($e->getMessage(), ['exception' => $e]); } catch (NotFoundException $e) { - $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]); + $this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]); } } } catch (ConnectException $e) { - $this->logger->logException($e, ['message' => 'Error while getting folder content']); - 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(), @@ -301,12 +285,17 @@ 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, $retry = true) { + 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); @@ -315,39 +304,39 @@ class SMB extends Common implements INotifyStorage { } catch (AlreadyExistsException $e) { if ($retry) { $this->remove($target); - $result = $this->share->rename($absoluteSource, $absoluteTarget, false); + $result = $this->share->rename($absoluteSource, $absoluteTarget); } else { - $this->logger->logException($e, ['level' => ILogger::WARN]); + $this->logger->warning($e->getMessage(), ['exception' => $e]); return false; } } catch (InvalidArgumentException $e) { if ($retry) { $this->remove($target); - $result = $this->share->rename($absoluteSource, $absoluteTarget, false); + $result = $this->share->rename($absoluteSource, $absoluteTarget); } else { - $this->logger->logException($e, ['level' => ILogger::WARN]); + $this->logger->warning($e->getMessage(), ['exception' => $e]); return false; } } catch (\Exception $e) { - $this->logger->logException($e, ['level' => ILogger::WARN]); + $this->logger->warning($e->getMessage(), ['exception' => $e]); return false; } unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]); return $result; } - public function stat($path, $retry = true) { + 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 $e; + throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); } } if ($this->remoteIsShare() && $this->isRootDir($path)) { @@ -358,10 +347,8 @@ 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) { @@ -380,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; } @@ -420,48 +398,43 @@ class SMB extends Common implements INotifyStorage { } catch (ForbiddenException $e) { return false; } catch (ConnectException $e) { - $this->logger->logException($e, ['message' => 'Error while deleting file']); - 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|bool + * @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': @@ -482,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 = \OC::$server->getTempManager()->getTemporaryFile($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); @@ -501,24 +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) { - throw new EntityTooLargeException("not enough available space to create file", 0, $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) { - $this->logger->logException($e, ['message' => 'Error while opening file']); - 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 = []; + $this->statCache = new CappedMemoryCache(); $content = $this->share->dir($this->buildPath($path)); foreach ($content as $file) { if ($file->isDirectory()) { @@ -534,12 +512,12 @@ class SMB extends Common implements INotifyStorage { } catch (ForbiddenException $e) { return false; } catch (ConnectException $e) { - $this->logger->logException($e, ['message' => 'Error while removing folder']); - 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, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { try { if (!$this->file_exists($path)) { $fh = $this->share->write($this->buildPath($path)); @@ -548,26 +526,31 @@ class SMB extends Common implements INotifyStorage { } return false; } catch (OutOfSpaceException $e) { - throw new EntityTooLargeException("not enough available space to create file", 0, $e); + throw new EntityTooLargeException('not enough available space to create file', 0, $e); } catch (ConnectException $e) { - $this->logger->logException($e, ['message' => 'Error while creating file']); - 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($path) { - $fileInfo = $this->getFileInfo($path); - if (!$fileInfo) { + 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) { + private function getMetaDataFromFileInfo(IFileInfo $fileInfo): array { $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE; - if (!$fileInfo->isReadOnly()) { + if ( + !$fileInfo->isReadOnly() || $fileInfo->isDirectory() + ) { $permissions += Constants::PERMISSION_DELETE; $permissions += Constants::PERMISSION_UPDATE; if ($fileInfo->isDirectory()) { @@ -579,7 +562,7 @@ class SMB extends Common implements INotifyStorage { if ($fileInfo->isDirectory()) { $data['mimetype'] = 'httpd/unix-directory'; } else { - $data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath()); + $data['mimetype'] = \OCP\Server::get(IMimeTypeDetector::class)->detectPath($fileInfo->getPath()); } $data['mtime'] = $fileInfo->getMTime(); if ($fileInfo->isDirectory()) { @@ -595,7 +578,7 @@ class SMB extends Common implements INotifyStorage { return $data; } - public function opendir($path) { + public function opendir(string $path) { try { $files = $this->getFolderContents($path); } catch (NotFoundException $e) { @@ -604,86 +587,103 @@ class SMB extends Common implements INotifyStorage { return false; } $names = array_map(function ($info) { - /** @var \Icewind\SMB\IFileInfo $info */ + /** @var IFileInfo $info */ return $info->getName(); }, iterator_to_array($files)); return IteratorDirectory::wrap($names); } - public function getDirectoryContent($directory): \Traversable { - $files = $this->getFolderContents($directory); - foreach ($files as $file) { - yield $this->getMetaDataFromFileInfo($file); + public function getDirectoryContent(string $directory): \Traversable { + try { + $files = $this->getFolderContents($directory); + foreach ($files as $file) { + yield $this->getMetaDataFromFileInfo($file); + } + } catch (NotFoundException $e) { + return; + } catch (NotPermittedException $e) { + return; } } - public function filetype($path) { + public function filetype(string $path): string|false { try { return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file'; - } catch (NotFoundException $e) { + } 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) { - $this->logger->logException($e, ['message' => 'Error while creating folder']); - 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 $this->showHidden || !$info->isHidden(); - } catch (NotFoundException $e) { + } 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 ($this->showHidden || !$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 ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly(); - } catch (NotFoundException $e) { + } catch (\OCP\Files\NotFoundException $e) { return false; - } catch (ForbiddenException $e) { + } catch (\OCP\Files\ForbiddenException $e) { return false; } } @@ -691,28 +691,25 @@ class SMB extends Common implements INotifyStorage { /** * check if smbclient is installed */ - public static function checkDependencies() { - return ( - (bool)\OC_Helper::findBinaryPath('smbclient') - || NativeServer::available(new System()) - ) ? 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->logException($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()); @@ -722,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 ba98bceda38..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 <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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; + +abstract class StreamWrapper extends Common { - /** - * @param string $path - * @return string|null - */ - abstract public function constructUrl($path); + abstract public function constructUrl(string $path): ?string; - public function mkdir($path) { + 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,26 +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 f3381117469..e80570f14ba 100644 --- a/apps/files_external/lib/Lib/Storage/Swift.php +++ b/apps/files_external/lib/Lib/Storage/Swift.php @@ -3,60 +3,37 @@ 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 Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Felix Moeller <mail@felixmoeller.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Martin Mattel <martin.mattel@diemattels.at> - * @author Michael Zamot <michael@zamot.io> - * @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 Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tim Dettrick <t.dettrick@uq.edu.au> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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 GuzzleHttp\Psr7\Uri; use Icewind\Streams\CallbackWrapper; use Icewind\Streams\IteratorDirectory; +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\ILogger; +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 \OC\Files\Storage\Common { +class Swift extends Common { /** @var SwiftFactory */ private $connectionFactory; /** - * @var \OpenStack\ObjectStore\v1\Models\Container + * @var Container */ private $container; /** @@ -76,20 +53,19 @@ class Swift extends \OC\Files\Storage\Common { /** @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 - * @return mixed|string - */ - private function normalizePath(string $path) { + private function normalizePath(string $path): string { $path = trim($path, '/'); if (!$path) { @@ -104,28 +80,21 @@ class Swift extends \OC\Files\Storage\Common { public const SUBCONTAINER_FILE = '.subcontainers'; /** - * translate directory path to container name - * - * @param string $path - * @return string - */ - - /** * Fetches an object from the API. * If the object is cached already or a * failed "doesn't exist" response was cached, * that one will be returned. * - * @param string $path - * @return StorageObject|bool object - * or false if the object did not exist - * @throws \OCP\Files\StorageAuthException - * @throws \OCP\Files\StorageNotAvailableException + * @return StorageObject|false object + * or false if the object did not exist + * @throws StorageAuthException + * @throws StorageNotAvailableException */ - private function fetchObject(string $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()->getObject($path); @@ -135,8 +104,8 @@ class Swift extends \OC\Files\Storage\Common { } catch (BadResponseError $e) { // Expected response is "404 Not Found", so only log if it isn't if ($e->getResponse()->getStatusCode() !== 404) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::ERROR, + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, 'app' => 'files_external', ]); } @@ -148,66 +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 \OCP\Files\StorageAuthException - * @throws \OCP\Files\StorageNotAvailableException + * @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']) && empty($params['userid'])) 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 StorageBadConfigException("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.'); } - $user = $params['user']; - $this->id = 'swift::' . $user . md5($params['bucket']); + $user = $parameters['user']; + $this->id = 'swift::' . $user . md5($parameters['bucket']); - $bucketUrl = new Uri($params['bucket']); + $bucketUrl = new Uri($parameters['bucket']); if ($bucketUrl->getHost()) { - $params['bucket'] = basename($bucketUrl->getPath()); - $params['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath())); + $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'; } - $params['autocreate'] = true; + $parameters['autocreate'] = true; - if (isset($params['domain'])) { - $params['user'] = [ - 'name' => $params['user'], - 'password' => $params['password'], + if (isset($parameters['domain'])) { + $parameters['user'] = [ + 'name' => $parameters['user'], + 'password' => $parameters['password'], 'domain' => [ - 'name' => $params['domain'], + 'name' => $parameters['domain'], ] ]; } - $this->params = $params; + $this->params = $parameters; // FIXME: private class... - $this->objectCache = new \OC\Cache\CappedMemoryCache(); + $this->objectCache = new CappedMemoryCache(); $this->connectionFactory = new SwiftFactory( - \OC::$server->getMemCacheFactory()->createDistributed('swift/'), + Server::get(ICacheFactory::class)->createDistributed('swift/'), $this->params, - \OC::$server->getLogger() + Server::get(LoggerInterface::class) ); $this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory); - $this->bucket = $params['bucket']; + $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)) { @@ -228,8 +196,8 @@ class Swift extends \OC\Files\Storage\Common { // with all properties $this->objectCache->remove($path); } catch (BadResponseError $e) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::ERROR, + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, 'app' => 'files_external', ]); return false; @@ -238,7 +206,7 @@ class Swift extends \OC\Files\Storage\Common { return true; } - public function file_exists($path) { + public function file_exists(string $path): bool { $path = $this->normalizePath($path); if ($path !== '.' && $this->is_dir($path)) { @@ -248,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)) { @@ -256,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; } @@ -272,8 +240,8 @@ class Swift extends \OC\Files\Storage\Common { $this->objectStore->deleteObject($path . '/'); $this->objectCache->remove($path . '/'); } catch (BadResponseError $e) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::ERROR, + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, 'app' => 'files_external', ]); return false; @@ -282,7 +250,7 @@ class Swift extends \OC\Files\Storage\Common { return true; } - public function opendir($path) { + public function opendir(string $path) { $path = $this->normalizePath($path); if ($path === '.') { @@ -291,7 +259,7 @@ 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 = []; @@ -310,17 +278,16 @@ class Swift extends \OC\Files\Storage\Common { return IteratorDirectory::wrap($files); } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::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 = ''; } elseif ($this->is_dir($path)) { @@ -333,32 +300,33 @@ class Swift extends \OC\Files\Storage\Common { return false; } } catch (BadResponseError $e) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::ERROR, + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, 'app' => 'files_external', ]); return false; } - $dateTime = $object->lastModified ? \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified) : false; - $mtime = $dateTime ? $dateTime->getTimestamp() : null; - $objectMetadata = $object->getMetadata(); - if (isset($objectMetadata['timestamp'])) { - $mtime = $objectMetadata['timestamp']; + $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 = []; - $stat['size'] = (int)$object->contentLength; - $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)) { @@ -374,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)) { @@ -387,8 +355,8 @@ class Swift extends \OC\Files\Storage\Common { $this->objectCache->remove($path . '/'); } catch (BadResponseError $e) { if ($e->getResponse()->getStatusCode() !== 404) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::ERROR, + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, 'app' => 'files_external', ]); throw $e; @@ -398,7 +366,7 @@ class Swift extends \OC\Files\Storage\Common { return true; } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $path = $this->normalizePath($path); switch ($mode) { @@ -411,8 +379,8 @@ class Swift extends \OC\Files\Storage\Common { try { return $this->objectStore->readObject($path); } catch (BadResponseError $e) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::ERROR, + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, 'app' => 'files_external', ]); return false; @@ -431,7 +399,7 @@ class Swift extends \OC\Files\Storage\Common { } else { $ext = ''; } - $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($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') { @@ -442,13 +410,13 @@ 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(); @@ -466,7 +434,7 @@ class Swift extends \OC\Files\Storage\Common { } return true; } else { - $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path); + $mimeType = $this->mimeDetector->detectPath($path); $this->getContainer()->createObject([ 'name' => $path, 'content' => '', @@ -478,57 +446,57 @@ class Swift extends \OC\Files\Storage\Common { } } - public function copy($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); + public function copy(string $source, string $target): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); - $fileType = $this->filetype($path1); + $fileType = $this->filetype($source); if ($fileType) { // make way - $this->unlink($path2); + $this->unlink($target); } if ($fileType === 'file') { try { - $source = $this->fetchObject($path1); - $source->copy([ - 'destination' => $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 . '/'); + $this->objectCache->remove($target); + $this->objectCache->remove($target . '/'); } catch (BadResponseError $e) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::ERROR, + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, 'app' => 'files_external', ]); return false; } } elseif ($fileType === 'dir') { try { - $source = $this->fetchObject($path1 . '/'); - $source->copy([ - 'destination' => $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 . '/'); + $this->objectCache->remove($target); + $this->objectCache->remove($target . '/'); } catch (BadResponseError $e) { - \OC::$server->getLogger()->logException($e, [ - 'level' => ILogger::ERROR, + 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 { @@ -539,22 +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) { + if ($this->unlink($source) === false) { throw new \Exception('failed to remove original'); - $this->unlink($path2); + $this->unlink($target); return false; } @@ -564,18 +532,18 @@ class Swift extends \OC\Files\Storage\Common { return false; } - public function getId() { + public function getId(): string { return $this->id; } /** * Returns the initialized object store container. * - * @return \OpenStack\ObjectStore\v1\Models\Container - * @throws \OCP\Files\StorageAuthException - * @throws \OCP\Files\StorageNotAvailableException + * @return Container + * @throws StorageAuthException + * @throws StorageNotAvailableException */ - public function getContainer() { + public function getContainer(): Container { if (is_null($this->container)) { $this->container = $this->connectionFactory->getContainer(); @@ -586,15 +554,15 @@ class Swift extends \OC\Files\Storage\Common { return $this->container; } - public function writeBack($tmpFile, $path) { + public function writeBack(string $tmpFile, string $path): void { $fileData = fopen($tmpFile, 'r'); - $this->objectStore->writeObject($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); } @@ -619,7 +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 4e61d89e9a5..2cb82d3790a 100644 --- a/apps/files_external/lib/Lib/StorageConfig.php +++ b/apps/files_external/lib/Lib/StorageConfig.php @@ -1,43 +1,27 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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 Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 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\ResponseDefinitions; /** * External storage configuration + * + * @psalm-import-type Files_ExternalStorageConfig from ResponseDefinitions */ class StorageConfig implements \JsonSerializable { 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; /** @@ -64,7 +48,7 @@ class StorageConfig implements \JsonSerializable { /** * Backend options * - * @var array + * @var array<string, mixed> */ private $backendOptions = []; @@ -99,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 = []; @@ -127,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; } @@ -148,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; } @@ -170,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); } /** @@ -221,7 +205,7 @@ class StorageConfig implements \JsonSerializable { 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; } @@ -262,7 +246,7 @@ class StorageConfig implements \JsonSerializable { } /** - * Sets the mount priotity + * Sets the mount priority * * @param int $priority priority */ @@ -273,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; @@ -282,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)) { @@ -294,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; @@ -303,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)) { @@ -382,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; @@ -397,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(); @@ -428,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 304eadb2254..4062ff1635e 100644 --- a/apps/files_external/lib/Lib/StorageModifierTrait.php +++ b/apps/files_external/lib/Lib/StorageModifierTrait.php @@ -1,29 +1,13 @@ <?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: 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 OCP\Files\Storage\IStorage; use OCP\Files\StorageNotAvailableException; use OCP\IUser; @@ -45,23 +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 dc4ba7b366f..62b26f3edb1 100644 --- a/apps/files_external/lib/Lib/VisibilityTrait.php +++ b/apps/files_external/lib/Lib/VisibilityTrait.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: 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; |