diff options
Diffstat (limited to 'apps/files_external/lib')
163 files changed, 12950 insertions, 6546 deletions
diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php new file mode 100644 index 00000000000..a6c2aff947b --- /dev/null +++ b/apps/files_external/lib/AppInfo/Application.php @@ -0,0 +1,156 @@ +<?php + +/** + * 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\AppInfo; + +use OCA\Files\Event\LoadAdditionalScriptsEvent; +use OCA\Files_External\Config\ConfigAdapter; +use OCA\Files_External\Config\UserPlaceholderHandler; +use OCA\Files_External\ConfigLexicon; +use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey; +use OCA\Files_External\Lib\Auth\Builtin; +use OCA\Files_External\Lib\Auth\NullMechanism; +use OCA\Files_External\Lib\Auth\OAuth2\OAuth2; +use OCA\Files_External\Lib\Auth\OpenStack\OpenStackV2; +use OCA\Files_External\Lib\Auth\OpenStack\OpenStackV3; +use OCA\Files_External\Lib\Auth\OpenStack\Rackspace; +use OCA\Files_External\Lib\Auth\Password\GlobalAuth; +use OCA\Files_External\Lib\Auth\Password\LoginCredentials; +use OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\Auth\Password\SessionCredentials; +use OCA\Files_External\Lib\Auth\Password\UserGlobalAuth; +use OCA\Files_External\Lib\Auth\Password\UserProvided; +use OCA\Files_External\Lib\Auth\PublicKey\RSA; +use OCA\Files_External\Lib\Auth\PublicKey\RSAPrivateKey; +use OCA\Files_External\Lib\Auth\SMB\KerberosApacheAuth; +use OCA\Files_External\Lib\Auth\SMB\KerberosAuth; +use OCA\Files_External\Lib\Backend\AmazonS3; +use OCA\Files_External\Lib\Backend\DAV; +use OCA\Files_External\Lib\Backend\FTP; +use OCA\Files_External\Lib\Backend\Local; +use OCA\Files_External\Lib\Backend\OwnCloud; +use OCA\Files_External\Lib\Backend\SFTP; +use OCA\Files_External\Lib\Backend\SFTP_Key; +use OCA\Files_External\Lib\Backend\SMB; +use OCA\Files_External\Lib\Backend\SMB_OC; +use OCA\Files_External\Lib\Backend\Swift; +use OCA\Files_External\Lib\Config\IAuthMechanismProvider; +use OCA\Files_External\Lib\Config\IBackendProvider; +use OCA\Files_External\Listener\GroupDeletedListener; +use OCA\Files_External\Listener\LoadAdditionalListener; +use OCA\Files_External\Listener\UserDeletedListener; +use OCA\Files_External\Service\BackendService; +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\QueryException; +use OCP\Files\Config\IMountProviderCollection; +use OCP\Group\Events\GroupDeletedEvent; +use OCP\User\Events\UserDeletedEvent; + +/** + * @package OCA\Files_External\AppInfo + */ +class Application extends App implements IBackendProvider, IAuthMechanismProvider, IBootstrap { + public const APP_ID = 'files_external'; + + /** + * Application constructor. + * + * @throws QueryException + */ + public function __construct(array $urlParams = []) { + parent::__construct(self::APP_ID, $urlParams); + } + + public function register(IRegistrationContext $context): void { + $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); + $context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class); + $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); + $context->registerConfigLexicon(ConfigLexicon::class); + } + + public function boot(IBootContext $context): void { + $context->injectFn(function (IMountProviderCollection $mountProviderCollection, ConfigAdapter $configAdapter): void { + $mountProviderCollection->registerProvider($configAdapter); + }); + $context->injectFn(function (BackendService $backendService, UserPlaceholderHandler $userConfigHandler): void { + $backendService->registerBackendProvider($this); + $backendService->registerAuthMechanismProvider($this); + $backendService->registerConfigHandler('user', function () use ($userConfigHandler) { + return $userConfigHandler; + }); + }); + + // force-load auth mechanisms since some will register hooks + // TODO: obsolete these and use the TokenProvider to get the user's password from the session + $this->getAuthMechanisms(); + } + + /** + * @{inheritdoc} + */ + public function getBackends() { + $container = $this->getContainer(); + + $backends = [ + $container->get(Local::class), + $container->get(FTP::class), + $container->get(DAV::class), + $container->get(OwnCloud::class), + $container->get(SFTP::class), + $container->get(AmazonS3::class), + $container->get(Swift::class), + $container->get(SFTP_Key::class), + $container->get(SMB::class), + $container->get(SMB_OC::class), + ]; + + return $backends; + } + + /** + * @{inheritdoc} + */ + public function getAuthMechanisms() { + $container = $this->getContainer(); + + return [ + // AuthMechanism::SCHEME_NULL mechanism + $container->get(NullMechanism::class), + + // AuthMechanism::SCHEME_BUILTIN mechanism + $container->get(Builtin::class), + + // AuthMechanism::SCHEME_PASSWORD mechanisms + $container->get(Password::class), + $container->get(SessionCredentials::class), + $container->get(LoginCredentials::class), + $container->get(UserProvided::class), + $container->get(GlobalAuth::class), + $container->get(UserGlobalAuth::class), + + // AuthMechanism::SCHEME_OAUTH2 mechanisms + $container->get(OAuth2::class), + + // AuthMechanism::SCHEME_PUBLICKEY mechanisms + $container->get(RSA::class), + $container->get(RSAPrivateKey::class), + + // AuthMechanism::SCHEME_OPENSTACK mechanisms + $container->get(OpenStackV2::class), + $container->get(OpenStackV3::class), + $container->get(Rackspace::class), + + // Specialized mechanisms + $container->get(AccessKey::class), + $container->get(KerberosAuth::class), + $container->get(KerberosApacheAuth::class), + ]; + } +} diff --git a/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php b/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php new file mode 100644 index 00000000000..90a5ae17ab2 --- /dev/null +++ b/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php @@ -0,0 +1,47 @@ +<?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\BackgroundJob; + +use OCA\Files_External\Lib\Auth\Password\LoginCredentials; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Security\ICredentialsManager; + +class CredentialsCleanup extends TimedJob { + public function __construct( + ITimeFactory $time, + private ICredentialsManager $credentialsManager, + private UserGlobalStoragesService $userGlobalStoragesService, + private IUserManager $userManager, + ) { + parent::__construct($time); + + // run every day + $this->setInterval(24 * 60 * 60); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); + } + + protected function run($argument) { + $this->userManager->callForSeenUsers(function (IUser $user): void { + $storages = $this->userGlobalStoragesService->getAllStoragesForUser($user); + + $usesLoginCredentials = array_reduce($storages, function (bool $uses, StorageConfig $storage) { + return $uses || $storage->getAuthMechanism() instanceof LoginCredentials; + }, false); + + if (!$usesLoginCredentials) { + $this->credentialsManager->delete($user->getUID(), LoginCredentials::CREDENTIALS_IDENTIFIER); + } + }); + } +} diff --git a/apps/files_external/lib/Command/Applicable.php b/apps/files_external/lib/Command/Applicable.php new file mode 100644 index 00000000000..4d5e264bfaf --- /dev/null +++ b/apps/files_external/lib/Command/Applicable.php @@ -0,0 +1,124 @@ +<?php + +/** + * 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\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\AppFramework\Http; +use OCP\IGroupManager; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Applicable extends Base { + public function __construct( + protected GlobalStoragesService $globalService, + private IUserManager $userManager, + private IGroupManager $groupManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:applicable') + ->setDescription('Manage applicable users and groups for a mount') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'The id of the mount to edit' + )->addOption( + 'add-user', + '', + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'user to add as applicable' + )->addOption( + 'remove-user', + '', + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'user to remove as applicable' + )->addOption( + 'add-group', + '', + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'group to add as applicable' + )->addOption( + 'remove-group', + '', + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'group to remove as applicable' + )->addOption( + 'remove-all', + '', + InputOption::VALUE_NONE, + 'Set the mount to be globally applicable' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $mountId = $input->getArgument('mount_id'); + try { + $mount = $this->globalService->getStorage($mountId); + } catch (NotFoundException $e) { + $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts</error>'); + return Http::STATUS_NOT_FOUND; + } + + if ($mount->getType() === StorageConfig::MOUNT_TYPE_PERSONAL) { + $output->writeln('<error>Can\'t change applicables on personal mounts</error>'); + return self::FAILURE; + } + + $addUsers = $input->getOption('add-user'); + $removeUsers = $input->getOption('remove-user'); + $addGroups = $input->getOption('add-group'); + $removeGroups = $input->getOption('remove-group'); + + $applicableUsers = $mount->getApplicableUsers(); + $applicableGroups = $mount->getApplicableGroups(); + + if ((count($addUsers) + count($removeUsers) + count($addGroups) + count($removeGroups) > 0) || $input->getOption('remove-all')) { + foreach ($addUsers as $addUser) { + if (!$this->userManager->userExists($addUser)) { + $output->writeln('<error>User "' . $addUser . '" not found</error>'); + return Http::STATUS_NOT_FOUND; + } + } + foreach ($addGroups as $addGroup) { + if (!$this->groupManager->groupExists($addGroup)) { + $output->writeln('<error>Group "' . $addGroup . '" not found</error>'); + return Http::STATUS_NOT_FOUND; + } + } + + if ($input->getOption('remove-all')) { + $applicableUsers = []; + $applicableGroups = []; + } else { + $applicableUsers = array_unique(array_merge($applicableUsers, $addUsers)); + $applicableUsers = array_values(array_diff($applicableUsers, $removeUsers)); + $applicableGroups = array_unique(array_merge($applicableGroups, $addGroups)); + $applicableGroups = array_values(array_diff($applicableGroups, $removeGroups)); + } + $mount->setApplicableUsers($applicableUsers); + $mount->setApplicableGroups($applicableGroups); + $this->globalService->updateStorage($mount); + } + + $this->writeArrayInOutputFormat($input, $output, [ + 'users' => $applicableUsers, + 'groups' => $applicableGroups + ]); + return self::SUCCESS; + } +} diff --git a/apps/files_external/lib/Command/Backends.php b/apps/files_external/lib/Command/Backends.php new file mode 100644 index 00000000000..7fab0477adf --- /dev/null +++ b/apps/files_external/lib/Command/Backends.php @@ -0,0 +1,104 @@ +<?php + +/** + * 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\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Service\BackendService; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Backends extends Base { + public function __construct( + private BackendService $backendService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:backends') + ->setDescription('Show available authentication and storage backends') + ->addArgument( + 'type', + InputArgument::OPTIONAL, + 'only show backends of a certain type. Possible values are "authentication" or "storage"' + )->addArgument( + 'backend', + InputArgument::OPTIONAL, + 'only show information of a specific backend' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $authBackends = $this->backendService->getAuthMechanisms(); + $storageBackends = $this->backendService->getBackends(); + + $data = [ + 'authentication' => array_map([$this, 'serializeAuthBackend'], $authBackends), + 'storage' => array_map([$this, 'serializeAuthBackend'], $storageBackends) + ]; + + $type = $input->getArgument('type'); + $backend = $input->getArgument('backend'); + if ($type) { + if (!isset($data[$type])) { + $output->writeln('<error>Invalid type "' . $type . '". Possible values are "authentication" or "storage"</error>'); + return self::FAILURE; + } + $data = $data[$type]; + + if ($backend) { + if (!isset($data[$backend])) { + $output->writeln('<error>Unknown backend "' . $backend . '" of type "' . $type . '"</error>'); + return self::FAILURE; + } + $data = $data[$backend]; + } + } + + $this->writeArrayInOutputFormat($input, $output, $data); + return self::SUCCESS; + } + + private function serializeAuthBackend(\JsonSerializable $backend): array { + $data = $backend->jsonSerialize(); + $result = [ + 'name' => $data['name'], + 'identifier' => $data['identifier'], + 'configuration' => $this->formatConfiguration($data['configuration']) + ]; + if ($backend instanceof Backend) { + $result['storage_class'] = $backend->getStorageClass(); + $authBackends = $this->backendService->getAuthMechanismsByScheme(array_keys($backend->getAuthSchemes())); + $result['supported_authentication_backends'] = array_keys($authBackends); + $authConfig = array_map(function (AuthMechanism $auth) { + return $this->serializeAuthBackend($auth)['configuration']; + }, $authBackends); + $result['authentication_configuration'] = array_combine(array_keys($authBackends), $authConfig); + } + return $result; + } + + /** + * @param DefinitionParameter[] $parameters + * @return string[] + */ + private function formatConfiguration(array $parameters): array { + $configuration = array_filter($parameters, function (DefinitionParameter $parameter) { + return $parameter->isFlagSet(DefinitionParameter::FLAG_HIDDEN); + }); + return array_map(function (DefinitionParameter $parameter) { + return $parameter->getTypeName(); + }, $configuration); + } +} diff --git a/apps/files_external/lib/Command/Config.php b/apps/files_external/lib/Command/Config.php new file mode 100644 index 00000000000..883b4a2f3e7 --- /dev/null +++ b/apps/files_external/lib/Command/Config.php @@ -0,0 +1,96 @@ +<?php + +/** + * 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\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\AppFramework\Http; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Config extends Base { + public function __construct( + protected GlobalStoragesService $globalService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:config') + ->setDescription('Manage backend configuration for a mount') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'The id of the mount to edit' + )->addArgument( + 'key', + InputArgument::REQUIRED, + 'key of the config option to set/get' + )->addArgument( + 'value', + InputArgument::OPTIONAL, + 'value to set the config option to, when no value is provided the existing value will be printed' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $mountId = $input->getArgument('mount_id'); + $key = $input->getArgument('key'); + try { + $mount = $this->globalService->getStorage($mountId); + } catch (NotFoundException $e) { + $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>'); + return Http::STATUS_NOT_FOUND; + } + + $value = $input->getArgument('value'); + if ($value !== null) { + $this->setOption($mount, $key, $value, $output); + } else { + $this->getOption($mount, $key, $output); + } + return self::SUCCESS; + } + + /** + * @param string $key + */ + protected function getOption(StorageConfig $mount, $key, OutputInterface $output): void { + if ($key === 'mountpoint' || $key === 'mount_point') { + $value = $mount->getMountPoint(); + } else { + $value = $mount->getBackendOption($key); + } + if (!is_string($value) && json_decode(json_encode($value)) === $value) { // show bools and objects correctly + $value = json_encode($value); + } + $output->writeln((string)$value); + } + + /** + * @param string $key + * @param string $value + */ + protected function setOption(StorageConfig $mount, $key, $value, OutputInterface $output): void { + $decoded = json_decode($value, true); + if (!is_null($decoded) && json_encode($decoded) === $value) { + $value = $decoded; + } + if ($key === 'mountpoint' || $key === 'mount_point') { + $mount->setMountPoint($value); + } else { + $mount->setBackendOption($key, $value); + } + $this->globalService->updateStorage($mount); + } +} diff --git a/apps/files_external/lib/Command/Create.php b/apps/files_external/lib/Command/Create.php new file mode 100644 index 00000000000..3307015518a --- /dev/null +++ b/apps/files_external/lib/Command/Create.php @@ -0,0 +1,184 @@ +<?php + +/** + * 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\Command; + +use OC\Core\Command\Base; +use OC\Files\Filesystem; +use OC\User\NoUserException; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\GlobalStoragesService; +use OCA\Files_External\Service\StoragesService; +use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\Http; +use OCP\IUserManager; +use OCP\IUserSession; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Create extends Base { + public function __construct( + private GlobalStoragesService $globalService, + private UserStoragesService $userService, + private IUserManager $userManager, + private IUserSession $userSession, + private BackendService $backendService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:create') + ->setDescription('Create a new mount configuration') + ->addOption( + 'user', + '', + InputOption::VALUE_OPTIONAL, + 'user to add the mount configuration for, if not set the mount will be added as system mount' + ) + ->addArgument( + 'mount_point', + InputArgument::REQUIRED, + 'mount point for the new mount' + ) + ->addArgument( + 'storage_backend', + InputArgument::REQUIRED, + 'storage backend identifier for the new mount, see `occ files_external:backends` for possible values' + ) + ->addArgument( + 'authentication_backend', + InputArgument::REQUIRED, + 'authentication backend identifier for the new mount, see `occ files_external:backends` for possible values' + ) + ->addOption( + 'config', + 'c', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Mount configuration option in key=value format' + ) + ->addOption( + 'dry', + '', + InputOption::VALUE_NONE, + 'Don\'t save the created mount, only list the new mount' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getOption('user'); + $mountPoint = $input->getArgument('mount_point'); + $storageIdentifier = $input->getArgument('storage_backend'); + $authIdentifier = $input->getArgument('authentication_backend'); + $configInput = $input->getOption('config'); + + $storageBackend = $this->backendService->getBackend($storageIdentifier); + $authBackend = $this->backendService->getAuthMechanism($authIdentifier); + + if (!Filesystem::isValidPath($mountPoint)) { + $output->writeln('<error>Invalid mountpoint "' . $mountPoint . '"</error>'); + return self::FAILURE; + } + if (is_null($storageBackend)) { + $output->writeln('<error>Storage backend with identifier "' . $storageIdentifier . '" not found (see `occ files_external:backends` for possible values)</error>'); + return Http::STATUS_NOT_FOUND; + } + if (is_null($authBackend)) { + $output->writeln('<error>Authentication backend with identifier "' . $authIdentifier . '" not found (see `occ files_external:backends` for possible values)</error>'); + return Http::STATUS_NOT_FOUND; + } + $supportedSchemes = array_keys($storageBackend->getAuthSchemes()); + if (!in_array($authBackend->getScheme(), $supportedSchemes)) { + $output->writeln('<error>Authentication backend "' . $authIdentifier . '" not valid for storage backend "' . $storageIdentifier . '" (see `occ files_external:backends storage ' . $storageIdentifier . '` for possible values)</error>'); + return self::FAILURE; + } + + $config = []; + foreach ($configInput as $configOption) { + if (!str_contains($configOption, '=')) { + $output->writeln('<error>Invalid mount configuration option "' . $configOption . '"</error>'); + return self::FAILURE; + } + [$key, $value] = explode('=', $configOption, 2); + if (!$this->validateParam($key, $value, $storageBackend, $authBackend)) { + $output->writeln('<error>Unknown configuration for backends "' . $key . '"</error>'); + return self::FAILURE; + } + $config[$key] = $value; + } + + $mount = new StorageConfig(); + $mount->setMountPoint($mountPoint); + $mount->setBackend($storageBackend); + $mount->setAuthMechanism($authBackend); + $mount->setBackendOptions($config); + + if ($user) { + if (!$this->userManager->userExists($user)) { + $output->writeln('<error>User "' . $user . '" not found</error>'); + return self::FAILURE; + } + $mount->setApplicableUsers([$user]); + } + + if ($input->getOption('dry')) { + $this->showMount($user, $mount, $input, $output); + } else { + $this->getStorageService($user)->addStorage($mount); + if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN) { + $output->writeln('<info>Storage created with id ' . $mount->getId() . '</info>'); + } else { + $output->writeln((string)$mount->getId()); + } + } + return self::SUCCESS; + } + + private function validateParam(string $key, &$value, Backend $storageBackend, AuthMechanism $authBackend): bool { + $params = array_merge($storageBackend->getParameters(), $authBackend->getParameters()); + foreach ($params as $param) { + /** @var DefinitionParameter $param */ + if ($param->getName() === $key) { + if ($param->getType() === DefinitionParameter::VALUE_BOOLEAN) { + $value = ($value === 'true'); + } + return true; + } + } + return false; + } + + private function showMount(string $user, StorageConfig $mount, InputInterface $input, OutputInterface $output): void { + $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager); + $listInput = new ArrayInput([], $listCommand->getDefinition()); + $listInput->setOption('output', $input->getOption('output')); + $listInput->setOption('show-password', true); + $listCommand->listMounts($user, [$mount], $listInput, $output); + } + + protected function getStorageService(string $userId): StoragesService { + if (empty($userId)) { + return $this->globalService; + } + + $user = $this->userManager->get($userId); + if (is_null($user)) { + throw new NoUserException("user $userId not found"); + } + $this->userSession->setUser($user); + return $this->userService; + } +} diff --git a/apps/files_external/lib/Command/Delete.php b/apps/files_external/lib/Command/Delete.php new file mode 100644 index 00000000000..9f121250f7d --- /dev/null +++ b/apps/files_external/lib/Command/Delete.php @@ -0,0 +1,82 @@ +<?php + +/** + * 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\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\GlobalStoragesService; +use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\Http; +use OCP\IUserManager; +use OCP\IUserSession; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class Delete extends Base { + public function __construct( + protected GlobalStoragesService $globalService, + protected UserStoragesService $userService, + protected IUserSession $userSession, + protected IUserManager $userManager, + protected QuestionHelper $questionHelper, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:delete') + ->setDescription('Delete an external mount') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'The id of the mount to edit' + )->addOption( + 'yes', + 'y', + InputOption::VALUE_NONE, + 'Skip confirmation' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $mountId = $input->getArgument('mount_id'); + try { + $mount = $this->globalService->getStorage($mountId); + } catch (NotFoundException $e) { + $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>'); + return Http::STATUS_NOT_FOUND; + } + + $noConfirm = $input->getOption('yes'); + + if (!$noConfirm) { + $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager); + $listInput = new ArrayInput([], $listCommand->getDefinition()); + $listInput->setOption('output', $input->getOption('output')); + $listCommand->listMounts(null, [$mount], $listInput, $output); + + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Delete this mount? [y/N] ', false); + + if (!$questionHelper->ask($input, $output, $question)) { + return self::FAILURE; + } + } + + $this->globalService->removeStorage($mountId); + return self::SUCCESS; + } +} diff --git a/apps/files_external/lib/Command/Dependencies.php b/apps/files_external/lib/Command/Dependencies.php new file mode 100644 index 00000000000..d2db8a8c9af --- /dev/null +++ b/apps/files_external/lib/Command/Dependencies.php @@ -0,0 +1,57 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\Service\BackendService; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Dependencies extends Base { + public function __construct( + private readonly BackendService $backendService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:dependencies') + ->setDescription('Show information about the backend dependencies'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $storageBackends = $this->backendService->getBackends(); + + $anyMissing = false; + + foreach ($storageBackends as $backend) { + if ($backend->getDeprecateTo() !== null) { + continue; + } + $missingDependencies = $backend->checkDependencies(); + if ($missingDependencies) { + $anyMissing = true; + $output->writeln($backend->getText() . ':'); + foreach ($missingDependencies as $missingDependency) { + if ($missingDependency->getMessage()) { + $output->writeln(" - <comment>{$missingDependency->getDependency()}</comment>: {$missingDependency->getMessage()}"); + } else { + $output->writeln(" - <comment>{$missingDependency->getDependency()}</comment>"); + } + } + } + } + + if (!$anyMissing) { + $output->writeln('<info>All dependencies are met</info>'); + } + + return self::SUCCESS; + } +} diff --git a/apps/files_external/lib/Command/Export.php b/apps/files_external/lib/Command/Export.php new file mode 100644 index 00000000000..59484d0e788 --- /dev/null +++ b/apps/files_external/lib/Command/Export.php @@ -0,0 +1,44 @@ +<?php + +/** + * 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\Command; + +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Export extends ListCommand { + protected function configure(): void { + $this + ->setName('files_external:export') + ->setDescription('Export mount configurations') + ->addArgument( + 'user_id', + InputArgument::OPTIONAL, + 'user id to export the personal mounts for, if no user is provided admin mounts will be exported' + )->addOption( + 'all', + 'a', + InputOption::VALUE_NONE, + 'show both system wide mounts and all personal mounts' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager); + $listInput = new ArrayInput([], $listCommand->getDefinition()); + $listInput->setArgument('user_id', $input->getArgument('user_id')); + $listInput->setOption('all', $input->getOption('all')); + $listInput->setOption('output', 'json_pretty'); + $listInput->setOption('show-password', true); + $listInput->setOption('full', true); + $listCommand->execute($listInput, $output); + return self::SUCCESS; + } +} diff --git a/apps/files_external/lib/Command/Import.php b/apps/files_external/lib/Command/Import.php new file mode 100644 index 00000000000..a9ed76fbe40 --- /dev/null +++ b/apps/files_external/lib/Command/Import.php @@ -0,0 +1,179 @@ +<?php + +/** + * 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\Command; + +use OC\Core\Command\Base; +use OC\User\NoUserException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\GlobalStoragesService; +use OCA\Files_External\Service\ImportLegacyStoragesService; +use OCA\Files_External\Service\StoragesService; +use OCA\Files_External\Service\UserStoragesService; +use OCP\IUserManager; +use OCP\IUserSession; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Import extends Base { + public function __construct( + private GlobalStoragesService $globalService, + private UserStoragesService $userService, + private IUserSession $userSession, + private IUserManager $userManager, + private ImportLegacyStoragesService $importLegacyStorageService, + private BackendService $backendService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:import') + ->setDescription('Import mount configurations') + ->addOption( + 'user', + '', + InputOption::VALUE_OPTIONAL, + 'user to add the mount configurations for, if not set the mount will be added as system mount' + ) + ->addArgument( + 'path', + InputArgument::REQUIRED, + 'path to a json file containing the mounts to import, use "-" to read from stdin' + ) + ->addOption( + 'dry', + '', + InputOption::VALUE_NONE, + 'Don\'t save the imported mounts, only list the new mounts' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getOption('user'); + $path = $input->getArgument('path'); + if ($path === '-') { + $json = file_get_contents('php://stdin'); + } else { + if (!file_exists($path)) { + $output->writeln('<error>File not found: ' . $path . '</error>'); + return self::FAILURE; + } + $json = file_get_contents($path); + } + if (!is_string($json) || strlen($json) < 2) { + $output->writeln('<error>Error while reading json</error>'); + return self::FAILURE; + } + $data = json_decode($json, true); + if (!is_array($data)) { + $output->writeln('<error>Error while parsing json</error>'); + return self::FAILURE; + } + + $isLegacy = isset($data['user']) || isset($data['group']); + if ($isLegacy) { + $this->importLegacyStorageService->setData($data); + $mounts = $this->importLegacyStorageService->getAllStorages(); + foreach ($mounts as $mount) { + if ($mount->getBackendOption('password') === false) { + $output->writeln('<error>Failed to decrypt password</error>'); + return self::FAILURE; + } + } + } else { + if (!isset($data[0])) { //normalize to an array of mounts + $data = [$data]; + } + $mounts = array_map([$this, 'parseData'], $data); + } + + if ($user) { + // ensure applicables are correct for personal mounts + foreach ($mounts as $mount) { + $mount->setApplicableGroups([]); + $mount->setApplicableUsers([$user]); + } + } + + $storageService = $this->getStorageService($user); + + $existingMounts = $storageService->getAllStorages(); + + foreach ($mounts as $mount) { + foreach ($existingMounts as $existingMount) { + if ( + $existingMount->getMountPoint() === $mount->getMountPoint() + && $existingMount->getApplicableGroups() === $mount->getApplicableGroups() + && $existingMount->getApplicableUsers() === $mount->getApplicableUsers() + && $existingMount->getBackendOptions() === $mount->getBackendOptions() + ) { + $output->writeln('<error>Duplicate mount (' . $mount->getMountPoint() . ')</error>'); + return self::FAILURE; + } + } + } + + if ($input->getOption('dry')) { + if (count($mounts) === 0) { + $output->writeln('<error>No mounts to be imported</error>'); + return self::FAILURE; + } + $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager); + $listInput = new ArrayInput([], $listCommand->getDefinition()); + $listInput->setOption('output', $input->getOption('output')); + $listInput->setOption('show-password', true); + $listCommand->listMounts($user, $mounts, $listInput, $output); + } else { + foreach ($mounts as $mount) { + $storageService->addStorage($mount); + } + } + return self::SUCCESS; + } + + private function parseData(array $data): StorageConfig { + $mount = new StorageConfig($data['mount_id']); + $mount->setMountPoint($data['mount_point']); + $mount->setBackend($this->getBackendByClass($data['storage'])); + $authBackend = $this->backendService->getAuthMechanism($data['authentication_type']); + $mount->setAuthMechanism($authBackend); + $mount->setBackendOptions($data['configuration']); + $mount->setMountOptions($data['options']); + $mount->setApplicableUsers($data['applicable_users'] ?? []); + $mount->setApplicableGroups($data['applicable_groups'] ?? []); + return $mount; + } + + private function getBackendByClass(string $className) { + $backends = $this->backendService->getBackends(); + foreach ($backends as $backend) { + if ($backend->getStorageClass() === $className) { + return $backend; + } + } + } + + protected function getStorageService(string $userId): StoragesService { + if (empty($userId)) { + return $this->globalService; + } + + $user = $this->userManager->get($userId); + if (is_null($user)) { + throw new NoUserException("user $userId not found"); + } + $this->userSession->setUser($user); + return $this->userService; + } +} diff --git a/apps/files_external/lib/Command/ListCommand.php b/apps/files_external/lib/Command/ListCommand.php new file mode 100644 index 00000000000..350916b6c2c --- /dev/null +++ b/apps/files_external/lib/Command/ListCommand.php @@ -0,0 +1,239 @@ +<?php + +/** + * 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\Command; + +use OC\Core\Command\Base; +use OC\User\NoUserException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\GlobalStoragesService; +use OCA\Files_External\Service\StoragesService; +use OCA\Files_External\Service\UserStoragesService; +use OCP\IUserManager; +use OCP\IUserSession; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ListCommand extends Base { + public const ALL = -1; + + public function __construct( + protected GlobalStoragesService $globalService, + protected UserStoragesService $userService, + protected IUserSession $userSession, + protected IUserManager $userManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:list') + ->setDescription('List configured admin or personal mounts') + ->addArgument( + 'user_id', + InputArgument::OPTIONAL, + 'user id to list the personal mounts for, if no user is provided admin mounts will be listed' + )->addOption( + 'show-password', + '', + InputOption::VALUE_NONE, + 'show passwords and secrets' + )->addOption( + 'full', + null, + InputOption::VALUE_NONE, + 'don\'t truncate long values in table output' + )->addOption( + 'all', + 'a', + InputOption::VALUE_NONE, + 'show both system wide mounts and all personal mounts' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + /** @var StorageConfig[] $mounts */ + if ($input->getOption('all')) { + $mounts = $this->globalService->getStorageForAllUsers(); + $userId = self::ALL; + } else { + $userId = (string)$input->getArgument('user_id'); + $storageService = $this->getStorageService($userId); + $mounts = $storageService->getAllStorages(); + } + + $this->listMounts($userId, $mounts, $input, $output); + return self::SUCCESS; + } + + /** + * @param ?string|ListCommand::ALL $userId + * @param StorageConfig[] $mounts + */ + public function listMounts($userId, array $mounts, InputInterface $input, OutputInterface $output): void { + $outputType = $input->getOption('output'); + if (count($mounts) === 0) { + if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) { + $output->writeln('[]'); + } else { + if ($userId === self::ALL) { + $output->writeln('<info>No mounts configured</info>'); + } elseif ($userId) { + $output->writeln("<info>No mounts configured by $userId</info>"); + } else { + $output->writeln('<info>No admin mounts configured</info>'); + } + } + return; + } + + $headers = ['Mount ID', 'Mount Point', 'Storage', 'Authentication Type', 'Configuration', 'Options']; + + if (!$userId || $userId === self::ALL) { + $headers[] = 'Applicable Users'; + $headers[] = 'Applicable Groups'; + } + if ($userId === self::ALL) { + $headers[] = 'Type'; + } + + if (!$input->getOption('show-password')) { + $hideKeys = ['key', 'bucket', 'secret', 'password', 'refresh_token', 'token', 'client_secret', 'public_key', 'private_key']; + foreach ($mounts as $mount) { + $config = $mount->getBackendOptions(); + foreach ($config as $key => $value) { + if (in_array($key, $hideKeys)) { + $mount->setBackendOption($key, '***REMOVED SENSITIVE VALUE***'); + } + } + } + } + + if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) { + $keys = array_map(function ($header) { + return strtolower(str_replace(' ', '_', $header)); + }, $headers); + + $pairs = array_map(function (StorageConfig $config) use ($keys, $userId) { + $values = [ + $config->getId(), + $config->getMountPoint(), + $config->getBackend()->getStorageClass(), + $config->getAuthMechanism()->getIdentifier(), + $config->getBackendOptions(), + $config->getMountOptions() + ]; + if (!$userId || $userId === self::ALL) { + $values[] = $config->getApplicableUsers(); + $values[] = $config->getApplicableGroups(); + } + if ($userId === self::ALL) { + $values[] = $config->getType() === StorageConfig::MOUNT_TYPE_ADMIN ? 'admin' : 'personal'; + } + + return array_combine($keys, $values); + }, $mounts); + if ($outputType === self::OUTPUT_FORMAT_JSON) { + $output->writeln(json_encode(array_values($pairs))); + } else { + $output->writeln(json_encode(array_values($pairs), JSON_PRETTY_PRINT)); + } + } else { + $full = $input->getOption('full'); + $defaultMountOptions = [ + 'encrypt' => true, + 'previews' => true, + 'filesystem_check_changes' => 1, + 'enable_sharing' => false, + 'encoding_compatibility' => false, + 'readonly' => false, + ]; + $rows = array_map(function (StorageConfig $config) use ($userId, $defaultMountOptions, $full) { + $storageConfig = $config->getBackendOptions(); + $keys = array_keys($storageConfig); + $values = array_values($storageConfig); + + if (!$full) { + $values = array_map(function ($value) { + if (is_string($value) && strlen($value) > 32) { + return substr($value, 0, 6) . '...' . substr($value, -6, 6); + } else { + return $value; + } + }, $values); + } + + $configStrings = array_map(function ($key, $value) { + return $key . ': ' . json_encode($value); + }, $keys, $values); + $configString = implode(', ', $configStrings); + + $mountOptions = $config->getMountOptions(); + // hide defaults + foreach ($mountOptions as $key => $value) { + if ($value === $defaultMountOptions[$key]) { + unset($mountOptions[$key]); + } + } + $keys = array_keys($mountOptions); + $values = array_values($mountOptions); + + $optionsStrings = array_map(function ($key, $value) { + return $key . ': ' . json_encode($value); + }, $keys, $values); + $optionsString = implode(', ', $optionsStrings); + + $values = [ + $config->getId(), + $config->getMountPoint(), + $config->getBackend()->getText(), + $config->getAuthMechanism()->getText(), + $configString, + $optionsString + ]; + + if (!$userId || $userId === self::ALL) { + $applicableUsers = implode(', ', $config->getApplicableUsers()); + $applicableGroups = implode(', ', $config->getApplicableGroups()); + if ($applicableUsers === '' && $applicableGroups === '') { + $applicableUsers = 'All'; + } + $values[] = $applicableUsers; + $values[] = $applicableGroups; + } + if ($userId === self::ALL) { + $values[] = $config->getType() === StorageConfig::MOUNT_TYPE_ADMIN ? 'Admin' : 'Personal'; + } + + return $values; + }, $mounts); + + $table = new Table($output); + $table->setHeaders($headers); + $table->setRows($rows); + $table->render(); + } + } + + protected function getStorageService(string $userId): StoragesService { + if (empty($userId)) { + return $this->globalService; + } + + $user = $this->userManager->get($userId); + if (is_null($user)) { + throw new NoUserException("user $userId not found"); + } + $this->userSession->setUser($user); + return $this->userService; + } +} diff --git a/apps/files_external/lib/Command/Notify.php b/apps/files_external/lib/Command/Notify.php new file mode 100644 index 00000000000..0982aa5598b --- /dev/null +++ b/apps/files_external/lib/Command/Notify.php @@ -0,0 +1,253 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Command; + +use Doctrine\DBAL\Exception\DriverException; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\Notify\IChange; +use OCP\Files\Notify\INotifyHandler; +use OCP\Files\Notify\IRenameChange; +use OCP\Files\Storage\INotifyStorage; +use OCP\Files\Storage\IStorage; +use OCP\IDBConnection; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Notify extends StorageAuthBase { + public function __construct( + private IDBConnection $connection, + private LoggerInterface $logger, + GlobalStoragesService $globalService, + IUserManager $userManager, + ) { + parent::__construct($globalService, $userManager); + } + + protected function configure(): void { + $this + ->setName('files_external:notify') + ->setDescription('Listen for active update notifications for a configured external mount') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'the mount id of the mount to listen to' + )->addOption( + 'user', + 'u', + InputOption::VALUE_REQUIRED, + 'The username for the remote mount (required only for some mount configuration that don\'t store credentials)' + )->addOption( + 'password', + 'p', + InputOption::VALUE_REQUIRED, + 'The password for the remote mount (required only for some mount configuration that don\'t store credentials)' + )->addOption( + 'path', + '', + InputOption::VALUE_REQUIRED, + 'The directory in the storage to listen for updates in', + '/' + )->addOption( + 'no-self-check', + '', + InputOption::VALUE_NONE, + 'Disable self check on startup' + )->addOption( + 'dry-run', + '', + InputOption::VALUE_NONE, + 'Don\'t make any changes, only log detected changes' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + [$mount, $storage] = $this->createStorage($input, $output); + if ($storage === null) { + return self::FAILURE; + } + + if (!$storage instanceof INotifyStorage) { + $output->writeln('<error>Mount of type "' . $mount->getBackend()->getText() . '" does not support active update notifications</error>'); + return self::FAILURE; + } + + $dryRun = $input->getOption('dry-run'); + if ($dryRun && $output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { + $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); + } + + $path = trim($input->getOption('path'), '/'); + $notifyHandler = $storage->notify($path); + + if (!$input->getOption('no-self-check')) { + $this->selfTest($storage, $notifyHandler, $output); + } + + $notifyHandler->listen(function (IChange $change) use ($mount, $output, $dryRun): void { + $this->logUpdate($change, $output); + if ($change instanceof IRenameChange) { + $this->markParentAsOutdated($mount->getId(), $change->getTargetPath(), $output, $dryRun); + } + $this->markParentAsOutdated($mount->getId(), $change->getPath(), $output, $dryRun); + }); + return self::SUCCESS; + } + + private function markParentAsOutdated($mountId, $path, OutputInterface $output, bool $dryRun): void { + $parent = ltrim(dirname($path), '/'); + if ($parent === '.') { + $parent = ''; + } + + try { + $storages = $this->getStorageIds($mountId, $parent); + } catch (DriverException $ex) { + $this->logger->warning('Error while trying to find correct storage ids.', ['exception' => $ex]); + $this->connection = $this->reconnectToDatabase($this->connection, $output); + $output->writeln('<info>Needed to reconnect to the database</info>'); + $storages = $this->getStorageIds($mountId, $path); + } + if (count($storages) === 0) { + $output->writeln(" no users found with access to '$parent', skipping", OutputInterface::VERBOSITY_VERBOSE); + return; + } + + $users = array_map(function (array $storage) { + return $storage['user_id']; + }, $storages); + + $output->writeln(" marking '$parent' as outdated for " . implode(', ', $users), OutputInterface::VERBOSITY_VERBOSE); + + $storageIds = array_map(function (array $storage) { + return intval($storage['storage_id']); + }, $storages); + $storageIds = array_values(array_unique($storageIds)); + + if ($dryRun) { + $output->writeln(' dry-run: skipping database write'); + } else { + $result = $this->updateParent($storageIds, $parent); + if ($result === 0) { + //TODO: Find existing parent further up the tree in the database and register that folder instead. + $this->logger->info('Failed updating parent for "' . $path . '" while trying to register change. It may not exist in the filecache.'); + } + } + } + + private function logUpdate(IChange $change, OutputInterface $output): void { + $text = match ($change->getType()) { + INotifyStorage::NOTIFY_ADDED => 'added', + INotifyStorage::NOTIFY_MODIFIED => 'modified', + INotifyStorage::NOTIFY_REMOVED => 'removed', + INotifyStorage::NOTIFY_RENAMED => 'renamed', + default => '', + }; + + if ($text === '') { + return; + } + + $text .= ' ' . $change->getPath(); + if ($change instanceof IRenameChange) { + $text .= ' to ' . $change->getTargetPath(); + } + + $output->writeln($text, OutputInterface::VERBOSITY_VERBOSE); + } + + private function getStorageIds(int $mountId, string $path): array { + $pathHash = md5(trim(\OC_Util::normalizeUnicode($path), '/')); + $qb = $this->connection->getQueryBuilder(); + return $qb + ->select('storage_id', 'user_id') + ->from('mounts', 'm') + ->innerJoin('m', 'filecache', 'f', $qb->expr()->eq('m.storage_id', 'f.storage')) + ->where($qb->expr()->eq('mount_id', $qb->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR))) + ->execute() + ->fetchAll(); + } + + private function updateParent(array $storageIds, string $parent): int { + $pathHash = md5(trim(\OC_Util::normalizeUnicode($parent), '/')); + $qb = $this->connection->getQueryBuilder(); + return $qb + ->update('filecache') + ->set('size', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->in('storage', $qb->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY, ':storage_ids'))) + ->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR))) + ->executeStatement(); + } + + private function reconnectToDatabase(IDBConnection $connection, OutputInterface $output): IDBConnection { + try { + $connection->close(); + } catch (\Exception $ex) { + $this->logger->warning('Error while disconnecting from DB', ['exception' => $ex]); + $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>"); + } + $connected = false; + while (!$connected) { + try { + $connected = $connection->connect(); + } catch (\Exception $ex) { + $this->logger->warning('Error while re-connecting to database', ['exception' => $ex]); + $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>"); + sleep(60); + } + } + return $connection; + } + + + private function selfTest(IStorage $storage, INotifyHandler $notifyHandler, OutputInterface $output): void { + usleep(100 * 1000); //give time for the notify to start + if (!$storage->file_put_contents('/.nc_test_file.txt', 'test content')) { + $output->writeln('Failed to create test file for self-test'); + return; + } + $storage->mkdir('/.nc_test_folder'); + $storage->file_put_contents('/.nc_test_folder/subfile.txt', 'test content'); + + usleep(100 * 1000); //time for all changes to be processed + $changes = $notifyHandler->getChanges(); + + $storage->unlink('/.nc_test_file.txt'); + $storage->unlink('/.nc_test_folder/subfile.txt'); + $storage->rmdir('/.nc_test_folder'); + + usleep(100 * 1000); //time for all changes to be processed + $notifyHandler->getChanges(); // flush + + $foundRootChange = false; + $foundSubfolderChange = false; + + foreach ($changes as $change) { + if ($change->getPath() === '/.nc_test_file.txt' || $change->getPath() === '.nc_test_file.txt') { + $foundRootChange = true; + } elseif ($change->getPath() === '/.nc_test_folder/subfile.txt' || $change->getPath() === '.nc_test_folder/subfile.txt') { + $foundSubfolderChange = true; + } + } + + if ($foundRootChange && $foundSubfolderChange) { + $output->writeln('<info>Self-test successful</info>', OutputInterface::VERBOSITY_VERBOSE); + } elseif ($foundRootChange) { + $output->writeln('<error>Error while running self-test, change is subfolder not detected</error>'); + } else { + $output->writeln('<error>Error while running self-test, no changes detected</error>'); + } + } +} diff --git a/apps/files_external/lib/Command/Option.php b/apps/files_external/lib/Command/Option.php new file mode 100644 index 00000000000..3fda3fcb3cf --- /dev/null +++ b/apps/files_external/lib/Command/Option.php @@ -0,0 +1,57 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Command; + +use OCA\Files_External\Lib\StorageConfig; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; + +class Option extends Config { + protected function configure(): void { + $this + ->setName('files_external:option') + ->setDescription('Manage mount options for a mount') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'The id of the mount to edit' + )->addArgument( + 'key', + InputArgument::REQUIRED, + 'key of the mount option to set/get' + )->addArgument( + 'value', + InputArgument::OPTIONAL, + 'value to set the mount option to, when no value is provided the existing value will be printed' + ); + } + + /** + * @param string $key + */ + protected function getOption(StorageConfig $mount, $key, OutputInterface $output): void { + $value = $mount->getMountOption($key); + if (!is_string($value)) { // show bools and objects correctly + $value = json_encode($value); + } + $output->writeln((string)$value); + } + + /** + * @param string $key + * @param string $value + */ + protected function setOption(StorageConfig $mount, $key, $value, OutputInterface $output): void { + $decoded = json_decode($value, true); + if (!is_null($decoded)) { + $value = $decoded; + } + $mount->setMountOption($key, $value); + $this->globalService->updateStorage($mount); + } +} diff --git a/apps/files_external/lib/Command/Scan.php b/apps/files_external/lib/Command/Scan.php new file mode 100644 index 00000000000..75d98878baa --- /dev/null +++ b/apps/files_external/lib/Command/Scan.php @@ -0,0 +1,166 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Command; + +use OC\Files\Cache\Scanner; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\IUserManager; +use OCP\Lock\LockedException; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Scan extends StorageAuthBase { + protected float $execTime = 0; + protected int $foldersCounter = 0; + protected int $filesCounter = 0; + + public function __construct( + GlobalStoragesService $globalService, + IUserManager $userManager, + ) { + parent::__construct($globalService, $userManager); + } + + protected function configure(): void { + $this + ->setName('files_external:scan') + ->setDescription('Scan an external storage for changed files') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'the mount id of the mount to scan' + )->addOption( + 'user', + 'u', + InputOption::VALUE_REQUIRED, + 'The username for the remote mount (required only for some mount configuration that don\'t store credentials)' + )->addOption( + 'password', + 'p', + InputOption::VALUE_REQUIRED, + 'The password for the remote mount (required only for some mount configuration that don\'t store credentials)' + )->addOption( + 'path', + '', + InputOption::VALUE_OPTIONAL, + 'The path in the storage to scan', + '' + )->addOption( + 'unscanned', + '', + InputOption::VALUE_NONE, + 'only scan files which are marked as not fully scanned' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + [, $storage] = $this->createStorage($input, $output); + if ($storage === null) { + return 1; + } + + $path = $input->getOption('path'); + + $this->execTime = -microtime(true); + + /** @var Scanner $scanner */ + $scanner = $storage->getScanner(); + + $scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function (string $path) use ($output): void { + $output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); + ++$this->filesCounter; + $this->abortIfInterrupted(); + }); + + $scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function (string $path) use ($output): void { + $output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); + ++$this->foldersCounter; + $this->abortIfInterrupted(); + }); + + try { + if ($input->getOption('unscanned')) { + if ($path !== '') { + $output->writeln('<error>--unscanned is mutually exclusive with --path</error>'); + return 1; + } + $scanner->backgroundScan(); + } else { + $scanner->scan($path); + } + } catch (LockedException $e) { + if (is_string($e->getReadablePath()) && str_starts_with($e->getReadablePath(), 'scanner::')) { + if ($e->getReadablePath() === 'scanner::') { + $output->writeln('<error>Another process is already scanning this storage</error>'); + } else { + $output->writeln('<error>Another process is already scanning \'' . substr($e->getReadablePath(), strlen('scanner::')) . '\' in this storage</error>'); + } + } else { + throw $e; + } + } + + $this->presentStats($output); + + return 0; + } + + /** + * @param OutputInterface $output + */ + protected function presentStats(OutputInterface $output): void { + // Stop the timer + $this->execTime += microtime(true); + + $headers = [ + 'Folders', 'Files', 'Elapsed time' + ]; + + $this->showSummary($headers, [], $output); + } + + /** + * Shows a summary of operations + * + * @param string[] $headers + * @param string[] $rows + * @param OutputInterface $output + */ + protected function showSummary(array $headers, array $rows, OutputInterface $output): void { + $niceDate = $this->formatExecTime(); + if (!$rows) { + $rows = [ + $this->foldersCounter, + $this->filesCounter, + $niceDate, + ]; + } + $table = new Table($output); + $table + ->setHeaders($headers) + ->setRows([$rows]); + $table->render(); + } + + + /** + * Formats microtime into a human readable format + * + * @return string + */ + protected function formatExecTime(): string { + $secs = round($this->execTime); + # convert seconds into HH:MM:SS form + return sprintf('%02d:%02d:%02d', ($secs / 3600), ($secs / 60 % 60), $secs % 60); + } +} diff --git a/apps/files_external/lib/Command/StorageAuthBase.php b/apps/files_external/lib/Command/StorageAuthBase.php new file mode 100644 index 00000000000..6f830a08a60 --- /dev/null +++ b/apps/files_external/lib/Command/StorageAuthBase.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\Files\Storage\IStorage; +use OCP\Files\StorageNotAvailableException; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +abstract class StorageAuthBase extends Base { + public function __construct( + protected GlobalStoragesService $globalService, + protected IUserManager $userManager, + ) { + parent::__construct(); + } + + private function getUserOption(InputInterface $input): ?string { + if ($input->getOption('user')) { + return (string)$input->getOption('user'); + } + + return $_ENV['NOTIFY_USER'] ?? $_SERVER['NOTIFY_USER'] ?? null; + } + + private function getPasswordOption(InputInterface $input): ?string { + if ($input->getOption('password')) { + return (string)$input->getOption('password'); + } + + return $_ENV['NOTIFY_PASSWORD'] ?? $_SERVER['NOTIFY_PASSWORD'] ?? null; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return array + * @psalm-return array{0: StorageConfig, 1: IStorage}|array{0: null, 1: null} + */ + protected function createStorage(InputInterface $input, OutputInterface $output): array { + try { + /** @var StorageConfig|null $mount */ + $mount = $this->globalService->getStorage((int)$input->getArgument('mount_id')); + } catch (NotFoundException $e) { + $output->writeln('<error>Mount not found</error>'); + return [null, null]; + } + if (is_null($mount)) { + $output->writeln('<error>Mount not found</error>'); + return [null, null]; + } + $noAuth = false; + + $userOption = $this->getUserOption($input); + $passwordOption = $this->getPasswordOption($input); + + // if only the user is provided, we get the user object to pass along to the auth backend + // this allows using saved user credentials + $user = ($userOption && !$passwordOption) ? $this->userManager->get($userOption) : null; + + try { + $authBackend = $mount->getAuthMechanism(); + $authBackend->manipulateStorageConfig($mount, $user); + } catch (InsufficientDataForMeaningfulAnswerException $e) { + $noAuth = true; + } catch (StorageNotAvailableException $e) { + $noAuth = true; + } + + if ($userOption) { + $mount->setBackendOption('user', $userOption); + } + if ($passwordOption) { + $mount->setBackendOption('password', $passwordOption); + } + + try { + $backend = $mount->getBackend(); + $backend->manipulateStorageConfig($mount, $user); + } catch (InsufficientDataForMeaningfulAnswerException $e) { + $noAuth = true; + } catch (StorageNotAvailableException $e) { + $noAuth = true; + } + + try { + $class = $mount->getBackend()->getStorageClass(); + /** @var IStorage $storage */ + $storage = new $class($mount->getBackendOptions()); + if (!$storage->test()) { + throw new \Exception(); + } + return [$mount, $storage]; + } catch (\Exception $e) { + $output->writeln('<error>Error while trying to create storage</error>'); + if ($noAuth) { + $output->writeln('<error>Username and/or password required</error>'); + } + return [null, null]; + } + } +} diff --git a/apps/files_external/lib/Command/Verify.php b/apps/files_external/lib/Command/Verify.php new file mode 100644 index 00000000000..ecebbe0f7e6 --- /dev/null +++ b/apps/files_external/lib/Command/Verify.php @@ -0,0 +1,121 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\MountConfig; +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\AppFramework\Http; +use OCP\Files\StorageNotAvailableException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Verify extends Base { + public function __construct( + protected GlobalStoragesService $globalService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:verify') + ->setDescription('Verify mount configuration') + ->addArgument( + 'mount_id', + InputArgument::REQUIRED, + 'The id of the mount to check' + )->addOption( + 'config', + 'c', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Additional config option to set before checking in key=value pairs, required for certain auth backends such as login credentails' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $mountId = $input->getArgument('mount_id'); + $configInput = $input->getOption('config'); + + try { + $mount = $this->globalService->getStorage($mountId); + } catch (NotFoundException $e) { + $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>'); + return Http::STATUS_NOT_FOUND; + } + + $this->updateStorageStatus($mount, $configInput, $output); + + $this->writeArrayInOutputFormat($input, $output, [ + 'status' => StorageNotAvailableException::getStateCodeName($mount->getStatus()), + 'code' => $mount->getStatus(), + 'message' => $mount->getStatusMessage() + ]); + return self::SUCCESS; + } + + private function manipulateStorageConfig(StorageConfig $storage): void { + $authMechanism = $storage->getAuthMechanism(); + $authMechanism->manipulateStorageConfig($storage); + $backend = $storage->getBackend(); + $backend->manipulateStorageConfig($storage); + } + + private function updateStorageStatus(StorageConfig &$storage, $configInput, OutputInterface $output): void { + try { + try { + $this->manipulateStorageConfig($storage); + } catch (InsufficientDataForMeaningfulAnswerException $e) { + if (count($configInput) === 0) { // extra config options might solve the error + throw $e; + } + } + + foreach ($configInput as $configOption) { + if (!strpos($configOption, '=')) { + $output->writeln('<error>Invalid mount configuration option "' . $configOption . '"</error>'); + return; + } + [$key, $value] = explode('=', $configOption, 2); + $storage->setBackendOption($key, $value); + } + + $backend = $storage->getBackend(); + // update status (can be time-consuming) + $storage->setStatus( + MountConfig::getBackendStatus( + $backend->getStorageClass(), + $storage->getBackendOptions(), + ) + ); + } catch (InsufficientDataForMeaningfulAnswerException $e) { + $status = $e->getCode() ?: StorageNotAvailableException::STATUS_INDETERMINATE; + $storage->setStatus( + $status, + $e->getMessage() + ); + } catch (StorageNotAvailableException $e) { + $storage->setStatus( + $e->getCode(), + $e->getMessage() + ); + } catch (\Exception $e) { + // FIXME: convert storage exceptions to StorageNotAvailableException + $storage->setStatus( + StorageNotAvailableException::STATUS_ERROR, + get_class($e) . ': ' . $e->getMessage() + ); + } + } +} diff --git a/apps/files_external/lib/Config/ConfigAdapter.php b/apps/files_external/lib/Config/ConfigAdapter.php new file mode 100644 index 00000000000..a46c0fd5c66 --- /dev/null +++ b/apps/files_external/lib/Config/ConfigAdapter.php @@ -0,0 +1,171 @@ +<?php + +/** + * 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\Config; + +use OC\Files\Cache\Storage; +use OC\Files\Storage\FailedStorage; +use OC\Files\Storage\Wrapper\Availability; +use OC\Files\Storage\Wrapper\KnownMtime; +use OCA\Files_External\Lib\PersonalMount; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\MountConfig; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\QueryException; +use OCP\Files\Config\IMountProvider; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\ObjectStore\IObjectStore; +use OCP\Files\Storage\IConstructableStorage; +use OCP\Files\Storage\IStorage; +use OCP\Files\Storage\IStorageFactory; +use OCP\Files\StorageNotAvailableException; +use OCP\IUser; +use OCP\Server; +use Psr\Clock\ClockInterface; +use Psr\Log\LoggerInterface; + +/** + * Make the old files_external config work with the new public mount config api + */ +class ConfigAdapter implements IMountProvider { + public function __construct( + private UserStoragesService $userStoragesService, + private UserGlobalStoragesService $userGlobalStoragesService, + private ClockInterface $clock, + ) { + } + + /** + * @param class-string $class + * @return class-string<IObjectStore> + * @throws \InvalidArgumentException + * @psalm-taint-escape callable + */ + private function validateObjectStoreClassString(string $class): string { + if (!\is_subclass_of($class, IObjectStore::class)) { + throw new \InvalidArgumentException('Invalid object store'); + } + return $class; + } + + /** + * Process storage ready for mounting + * + * @throws QueryException + */ + private function prepareStorageConfig(StorageConfig &$storage, IUser $user): void { + foreach ($storage->getBackendOptions() as $option => $value) { + $storage->setBackendOption($option, MountConfig::substitutePlaceholdersInConfig($value, $user->getUID())); + } + + $objectStore = $storage->getBackendOption('objectstore'); + if ($objectStore) { + $objectClass = $this->validateObjectStoreClassString($objectStore['class']); + $storage->setBackendOption('objectstore', new $objectClass($objectStore)); + } + + $storage->getAuthMechanism()->manipulateStorageConfig($storage, $user); + $storage->getBackend()->manipulateStorageConfig($storage, $user); + } + + /** + * Construct the storage implementation + * + * @param StorageConfig $storageConfig + */ + private function constructStorage(StorageConfig $storageConfig): IStorage { + $class = $storageConfig->getBackend()->getStorageClass(); + if (!is_a($class, IConstructableStorage::class, true)) { + Server::get(LoggerInterface::class)->warning('Building a storage not implementing IConstructableStorage is deprecated since 31.0.0', ['class' => $class]); + } + $storage = new $class($storageConfig->getBackendOptions()); + + // auth mechanism should fire first + $storage = $storageConfig->getBackend()->wrapStorage($storage); + $storage = $storageConfig->getAuthMechanism()->wrapStorage($storage); + + return $storage; + } + + /** + * Get all mountpoints applicable for the user + * + * @return IMountPoint[] + */ + public function getMountsForUser(IUser $user, IStorageFactory $loader) { + $this->userStoragesService->setUser($user); + $this->userGlobalStoragesService->setUser($user); + + $storageConfigs = $this->userGlobalStoragesService->getAllStoragesForUser(); + + $storages = array_map(function (StorageConfig $storageConfig) use ($user) { + try { + $this->prepareStorageConfig($storageConfig, $user); + return $this->constructStorage($storageConfig); + } catch (\Exception $e) { + // propagate exception into filesystem + return new FailedStorage(['exception' => $e]); + } + }, $storageConfigs); + + + Storage::getGlobalCache()->loadForStorageIds(array_map(function (IStorage $storage) { + return $storage->getId(); + }, $storages)); + + $availableStorages = array_map(function (IStorage $storage, StorageConfig $storageConfig): IStorage { + try { + $availability = $storage->getAvailability(); + if (!$availability['available'] && !Availability::shouldRecheck($availability)) { + $storage = new FailedStorage([ + 'exception' => new StorageNotAvailableException('Storage with mount id ' . $storageConfig->getId() . ' is not available') + ]); + } + } catch (\Exception $e) { + // propagate exception into filesystem + $storage = new FailedStorage(['exception' => $e]); + } + return $storage; + }, $storages, $storageConfigs); + + $mounts = array_map(function (StorageConfig $storageConfig, IStorage $storage) use ($user, $loader) { + $storage->setOwner($user->getUID()); + if ($storageConfig->getType() === StorageConfig::MOUNT_TYPE_PERSONAL) { + return new PersonalMount( + $this->userStoragesService, + $storageConfig, + $storageConfig->getId(), + new KnownMtime([ + 'storage' => $storage, + 'clock' => $this->clock, + ]), + '/' . $user->getUID() . '/files' . $storageConfig->getMountPoint(), + null, + $loader, + $storageConfig->getMountOptions(), + $storageConfig->getId() + ); + } else { + return new SystemMountPoint( + $storageConfig, + $storage, + '/' . $user->getUID() . '/files' . $storageConfig->getMountPoint(), + null, + $loader, + $storageConfig->getMountOptions(), + $storageConfig->getId() + ); + } + }, $storageConfigs, $availableStorages); + + $this->userStoragesService->resetUser(); + $this->userGlobalStoragesService->resetUser(); + + return $mounts; + } +} diff --git a/apps/files_external/lib/Config/ExternalMountPoint.php b/apps/files_external/lib/Config/ExternalMountPoint.php new file mode 100644 index 00000000000..97569ed2913 --- /dev/null +++ b/apps/files_external/lib/Config/ExternalMountPoint.php @@ -0,0 +1,34 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Config; + +use OC\Files\Mount\MountPoint; +use OCA\Files_External\Lib\Auth\Password\SessionCredentials; +use OCA\Files_External\Lib\StorageConfig; + +class ExternalMountPoint extends MountPoint { + + public function __construct( + protected StorageConfig $storageConfig, + $storage, + $mountpoint, + $arguments = null, + $loader = null, + $mountOptions = null, + $mountId = null, + ) { + parent::__construct($storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId, ConfigAdapter::class); + } + + public function getMountType() { + return ($this->storageConfig->getAuthMechanism() instanceof SessionCredentials) ? 'external-session' : 'external'; + } + + public function getStorageConfig(): StorageConfig { + return $this->storageConfig; + } +} diff --git a/apps/files_external/lib/Config/IConfigHandler.php b/apps/files_external/lib/Config/IConfigHandler.php new file mode 100644 index 00000000000..9e8283cc58b --- /dev/null +++ b/apps/files_external/lib/Config/IConfigHandler.php @@ -0,0 +1,22 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Config; + +/** + * Interface IConfigHandler + * + * @package OCA\Files_External\Config + * @since 16.0.0 + */ +interface IConfigHandler { + /** + * @param mixed $optionValue + * @return mixed the same type as $optionValue + * @since 16.0.0 + */ + public function handle($optionValue); +} diff --git a/apps/files_external/lib/Config/SimpleSubstitutionTrait.php b/apps/files_external/lib/Config/SimpleSubstitutionTrait.php new file mode 100644 index 00000000000..85a76054fa8 --- /dev/null +++ b/apps/files_external/lib/Config/SimpleSubstitutionTrait.php @@ -0,0 +1,69 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Config; + +/** + * Trait SimpleSubstitutionTrait + * + * @package OCA\Files_External\Config + * @since 16.0.0 + */ +trait SimpleSubstitutionTrait { + /** + * @var string the placeholder without $ prefix + * @since 16.0.0 + */ + protected $placeholder; + + /** @var string */ + protected $sanitizedPlaceholder; + + /** + * @param mixed $optionValue + * @param string $replacement + * @return mixed + * @since 16.0.0 + */ + private function processInput($optionValue, string $replacement) { + $this->checkPlaceholder(); + if (is_array($optionValue)) { + foreach ($optionValue as &$value) { + $value = $this->substituteIfString($value, $replacement); + } + } else { + $optionValue = $this->substituteIfString($optionValue, $replacement); + } + return $optionValue; + } + + /** + * @throws \RuntimeException + */ + protected function checkPlaceholder(): void { + $this->sanitizedPlaceholder = trim(strtolower($this->placeholder)); + if (!(bool)\preg_match('/^[a-z0-9]*$/', $this->sanitizedPlaceholder)) { + throw new \RuntimeException(sprintf( + 'Invalid placeholder %s, only [a-z0-9] are allowed', $this->sanitizedPlaceholder + )); + } + if ($this->sanitizedPlaceholder === '') { + throw new \RuntimeException('Invalid empty placeholder'); + } + } + + /** + * @param mixed $value + * @param string $replacement + * @return mixed + */ + protected function substituteIfString($value, string $replacement) { + if (is_string($value)) { + return str_ireplace('$' . $this->sanitizedPlaceholder, $replacement, $value); + } + return $value; + } +} diff --git a/apps/files_external/lib/Config/SystemMountPoint.php b/apps/files_external/lib/Config/SystemMountPoint.php new file mode 100644 index 00000000000..af0bf792140 --- /dev/null +++ b/apps/files_external/lib/Config/SystemMountPoint.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Config; + +use OCP\Files\Mount\IShareOwnerlessMount; +use OCP\Files\Mount\ISystemMountPoint; + +class SystemMountPoint extends ExternalMountPoint implements ISystemMountPoint, IShareOwnerlessMount { +} diff --git a/apps/files_external/lib/Config/UserContext.php b/apps/files_external/lib/Config/UserContext.php new file mode 100644 index 00000000000..fb5c79a9329 --- /dev/null +++ b/apps/files_external/lib/Config/UserContext.php @@ -0,0 +1,61 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Config; + +use OCP\IRequest; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; + +class UserContext { + + /** @var string */ + private $userId; + + public function __construct( + private IUserSession $session, + private ShareManager $shareManager, + private IRequest $request, + private IUserManager $userManager, + ) { + } + + public function getSession(): IUserSession { + return $this->session; + } + + public function setUserId(string $userId): void { + $this->userId = $userId; + } + + protected function getUserId(): ?string { + if ($this->userId !== null) { + return $this->userId; + } + if ($this->session->getUser() !== null) { + return $this->session->getUser()->getUID(); + } + try { + $shareToken = $this->request->getParam('token'); + $share = $this->shareManager->getShareByToken($shareToken); + return $share->getShareOwner(); + } catch (ShareNotFound $e) { + } + + return null; + } + + protected function getUser(): ?IUser { + $userId = $this->getUserId(); + if ($userId !== null) { + return $this->userManager->get($userId); + } + return null; + } +} diff --git a/apps/files_external/lib/Config/UserPlaceholderHandler.php b/apps/files_external/lib/Config/UserPlaceholderHandler.php new file mode 100644 index 00000000000..d158e6923c1 --- /dev/null +++ b/apps/files_external/lib/Config/UserPlaceholderHandler.php @@ -0,0 +1,25 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Config; + +class UserPlaceholderHandler extends UserContext implements IConfigHandler { + use SimpleSubstitutionTrait; + + /** + * @param mixed $optionValue + * @return mixed the same type as $optionValue + * @since 16.0.0 + */ + public function handle($optionValue) { + $this->placeholder = 'user'; + $uid = $this->getUserId(); + if ($uid === null) { + return $optionValue; + } + return $this->processInput($optionValue, $uid); + } +} diff --git a/apps/files_external/lib/ConfigLexicon.php b/apps/files_external/lib/ConfigLexicon.php new file mode 100644 index 00000000000..154f76c1989 --- /dev/null +++ b/apps/files_external/lib/ConfigLexicon.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External; + +use OCP\Config\Lexicon\Entry; +use OCP\Config\Lexicon\ILexicon; +use OCP\Config\Lexicon\Strictness; +use OCP\Config\ValueType; + +/** + * Config Lexicon for files_sharing. + * + * Please Add & Manage your Config Keys in that file and keep the Lexicon up to date! + * + * {@see ILexicon} + */ +class ConfigLexicon implements ILexicon { + public const ALLOW_USER_MOUNTING = 'allow_user_mounting'; + public const USER_MOUNTING_BACKENDS = 'user_mounting_backends'; + + public function getStrictness(): Strictness { + return Strictness::NOTICE; + } + + public function getAppConfigs(): array { + return [ + new Entry(self::ALLOW_USER_MOUNTING, ValueType::BOOL, false, 'allow users to mount their own external filesystems', true), + new Entry(self::USER_MOUNTING_BACKENDS, ValueType::STRING, '', 'list of mounting backends available for users', true), + ]; + } + + public function getUserConfigs(): array { + return []; + } +} diff --git a/apps/files_external/lib/Controller/AjaxController.php b/apps/files_external/lib/Controller/AjaxController.php new file mode 100644 index 00000000000..5cee6422530 --- /dev/null +++ b/apps/files_external/lib/Controller/AjaxController.php @@ -0,0 +1,107 @@ +<?php + +/** + * 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\Controller; + +use OCA\Files_External\Lib\Auth\Password\GlobalAuth; +use OCA\Files_External\Lib\Auth\PublicKey\RSA; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUserSession; + +class AjaxController extends Controller { + /** + * @param string $appName + * @param IRequest $request + * @param RSA $rsaMechanism + * @param GlobalAuth $globalAuth + * @param IUserSession $userSession + * @param IGroupManager $groupManager + */ + public function __construct( + $appName, + IRequest $request, + private RSA $rsaMechanism, + private GlobalAuth $globalAuth, + private IUserSession $userSession, + private IGroupManager $groupManager, + private IL10N $l10n, + ) { + parent::__construct($appName, $request); + } + + /** + * @param int $keyLength + * @return array + */ + private function generateSshKeys($keyLength) { + $key = $this->rsaMechanism->createKey($keyLength); + // Replace the placeholder label with a more meaningful one + $key['publickey'] = str_replace('phpseclib-generated-key', gethostname(), $key['publickey']); + + return $key; + } + + /** + * Generates an SSH public/private key pair. + * + * @param int $keyLength + */ + #[NoAdminRequired] + public function getSshKeys($keyLength = 1024) { + $key = $this->generateSshKeys($keyLength); + return new JSONResponse([ + 'data' => [ + 'private_key' => $key['privatekey'], + 'public_key' => $key['publickey'] + ], + 'status' => 'success', + ]); + } + + /** + * @param string $uid + * @param string $user + * @param string $password + * @return JSONResponse + */ + #[NoAdminRequired] + #[PasswordConfirmationRequired(strict: true)] + public function saveGlobalCredentials($uid, $user, $password): JSONResponse { + $currentUser = $this->userSession->getUser(); + if ($currentUser === null) { + return new JSONResponse([ + 'status' => 'error', + 'message' => $this->l10n->t('You are not logged in'), + ], Http::STATUS_UNAUTHORIZED); + } + + // Non-admins can only edit their own credentials + // Admin can edit global credentials + $allowedToEdit = $uid === '' + ? $this->groupManager->isAdmin($currentUser->getUID()) + : $currentUser->getUID() === $uid; + + if ($allowedToEdit) { + $this->globalAuth->saveAuth($uid, $user, $password); + return new JSONResponse([ + 'status' => 'success', + ]); + } + + return new JSONResponse([ + 'status' => 'success', + 'message' => $this->l10n->t('Permission denied'), + ], Http::STATUS_FORBIDDEN); + } +} diff --git a/apps/files_external/lib/Controller/ApiController.php b/apps/files_external/lib/Controller/ApiController.php new file mode 100644 index 00000000000..49547357e6b --- /dev/null +++ b/apps/files_external/lib/Controller/ApiController.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +/** + * 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\Controller; + +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\ResponseDefinitions; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\Constants; +use OCP\IRequest; + +/** + * @psalm-import-type Files_ExternalMount from ResponseDefinitions + */ +class ApiController extends OCSController { + + public function __construct( + string $appName, + IRequest $request, + private UserGlobalStoragesService $userGlobalStoragesService, + private UserStoragesService $userStoragesService, + ) { + parent::__construct($appName, $request); + } + + /** + * Formats the given mount config to a mount entry. + * + * @param string $mountPoint mount point name, relative to the data dir + * @param StorageConfig $mountConfig mount config to format + * + * @return Files_ExternalMount + */ + private function formatMount(string $mountPoint, StorageConfig $mountConfig): array { + // split path from mount point + $path = \dirname($mountPoint); + if ($path === '.' || $path === '/') { + $path = ''; + } + + $isSystemMount = $mountConfig->getType() === StorageConfig::MOUNT_TYPE_ADMIN; + + $permissions = Constants::PERMISSION_READ; + // personal mounts can be deleted + if (!$isSystemMount) { + $permissions |= Constants::PERMISSION_DELETE; + } + + $entry = [ + 'id' => $mountConfig->getId(), + 'type' => 'dir', + 'name' => basename($mountPoint), + 'path' => $path, + 'permissions' => $permissions, + 'scope' => $isSystemMount ? 'system' : 'personal', + 'backend' => $mountConfig->getBackend()->getText(), + 'class' => $mountConfig->getBackend()->getIdentifier(), + 'config' => $mountConfig->jsonSerialize(true), + ]; + return $entry; + } + + /** + * Get the mount points visible for this user + * + * @return DataResponse<Http::STATUS_OK, list<Files_ExternalMount>, array{}> + * + * 200: User mounts returned + */ + #[NoAdminRequired] + public function getUserMounts(): DataResponse { + $entries = []; + $mountPoints = []; + + foreach ($this->userGlobalStoragesService->getStorages() as $storage) { + $mountPoint = $storage->getMountPoint(); + $mountPoints[$mountPoint] = $storage; + } + + foreach ($this->userStoragesService->getStorages() as $storage) { + $mountPoint = $storage->getMountPoint(); + $mountPoints[$mountPoint] = $storage; + } + foreach ($mountPoints as $mountPoint => $mount) { + $entries[] = $this->formatMount($mountPoint, $mount); + } + + return new DataResponse($entries); + } +} diff --git a/apps/files_external/lib/Controller/GlobalStoragesController.php b/apps/files_external/lib/Controller/GlobalStoragesController.php new file mode 100644 index 00000000000..e7274c9cfb6 --- /dev/null +++ b/apps/files_external/lib/Controller/GlobalStoragesController.php @@ -0,0 +1,189 @@ +<?php + +/** + * 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\Controller; + +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Global storages controller + */ +class GlobalStoragesController extends StoragesController { + /** + * Creates a new global storages controller. + * + * @param string $AppName application name + * @param IRequest $request request object + * @param IL10N $l10n l10n service + * @param GlobalStoragesService $globalStoragesService storage service + * @param LoggerInterface $logger + * @param IUserSession $userSession + * @param IGroupManager $groupManager + * @param IConfig $config + */ + public function __construct( + $AppName, + IRequest $request, + IL10N $l10n, + GlobalStoragesService $globalStoragesService, + LoggerInterface $logger, + IUserSession $userSession, + IGroupManager $groupManager, + IConfig $config, + ) { + parent::__construct( + $AppName, + $request, + $l10n, + $globalStoragesService, + $logger, + $userSession, + $groupManager, + $config + ); + } + + /** + * Create an external storage entry. + * + * @param string $mountPoint storage mount point + * @param string $backend backend identifier + * @param string $authMechanism authentication mechanism identifier + * @param array $backendOptions backend-specific options + * @param array $mountOptions mount-specific options + * @param array $applicableUsers users for which to mount the storage + * @param array $applicableGroups groups for which to mount the storage + * @param int $priority priority + * + * @return DataResponse + */ + #[PasswordConfirmationRequired(strict: true)] + public function create( + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions, + $applicableUsers, + $applicableGroups, + $priority, + ) { + $canCreateNewLocalStorage = $this->config->getSystemValue('files_external_allow_create_new_local', true); + if (!$canCreateNewLocalStorage && $backend === 'local') { + return new DataResponse( + [ + 'message' => $this->l10n->t('Forbidden to manage local mounts') + ], + Http::STATUS_FORBIDDEN + ); + } + + $newStorage = $this->createStorage( + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions, + $applicableUsers, + $applicableGroups, + $priority + ); + if ($newStorage instanceof DataResponse) { + return $newStorage; + } + + $response = $this->validate($newStorage); + if (!empty($response)) { + return $response; + } + + $newStorage = $this->service->addStorage($newStorage); + + $this->updateStorageStatus($newStorage); + + return new DataResponse( + $newStorage->jsonSerialize(true), + Http::STATUS_CREATED + ); + } + + /** + * Update an external storage entry. + * + * @param int $id storage id + * @param string $mountPoint storage mount point + * @param string $backend backend identifier + * @param string $authMechanism authentication mechanism identifier + * @param array $backendOptions backend-specific options + * @param array $mountOptions mount-specific options + * @param array $applicableUsers users for which to mount the storage + * @param array $applicableGroups groups for which to mount the storage + * @param int $priority priority + * + * @return DataResponse + */ + #[PasswordConfirmationRequired(strict: true)] + public function update( + $id, + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions, + $applicableUsers, + $applicableGroups, + $priority, + ) { + $storage = $this->createStorage( + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions, + $applicableUsers, + $applicableGroups, + $priority + ); + if ($storage instanceof DataResponse) { + return $storage; + } + $storage->setId($id); + + $response = $this->validate($storage); + if (!empty($response)) { + return $response; + } + + try { + $storage = $this->service->updateStorage($storage); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id]) + ], + Http::STATUS_NOT_FOUND + ); + } + + $this->updateStorageStatus($storage); + + return new DataResponse( + $storage->jsonSerialize(true), + Http::STATUS_OK + ); + } +} diff --git a/apps/files_external/lib/Controller/StoragesController.php b/apps/files_external/lib/Controller/StoragesController.php new file mode 100644 index 00000000000..df3a4528054 --- /dev/null +++ b/apps/files_external/lib/Controller/StoragesController.php @@ -0,0 +1,317 @@ +<?php + +/** + * 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\Controller; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\MountConfig; +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\StoragesService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Base class for storages controllers + */ +abstract class StoragesController extends Controller { + /** + * Creates a new storages controller. + * + * @param string $AppName application name + * @param IRequest $request request object + * @param IL10N $l10n l10n service + * @param StoragesService $storagesService storage service + * @param LoggerInterface $logger + */ + public function __construct( + $AppName, + IRequest $request, + protected IL10N $l10n, + protected StoragesService $service, + protected LoggerInterface $logger, + protected IUserSession $userSession, + protected IGroupManager $groupManager, + protected IConfig $config, + ) { + parent::__construct($AppName, $request); + } + + /** + * Create a storage from its parameters + * + * @param string $mountPoint storage mount point + * @param string $backend backend identifier + * @param string $authMechanism authentication mechanism identifier + * @param array $backendOptions backend-specific options + * @param array|null $mountOptions mount-specific options + * @param array|null $applicableUsers users for which to mount the storage + * @param array|null $applicableGroups groups for which to mount the storage + * @param int|null $priority priority + * + * @return StorageConfig|DataResponse + */ + protected function createStorage( + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions = null, + $applicableUsers = null, + $applicableGroups = null, + $priority = null, + ) { + $canCreateNewLocalStorage = $this->config->getSystemValue('files_external_allow_create_new_local', true); + if (!$canCreateNewLocalStorage && $backend === 'local') { + return new DataResponse( + [ + 'message' => $this->l10n->t('Forbidden to manage local mounts') + ], + Http::STATUS_FORBIDDEN + ); + } + + try { + return $this->service->createStorage( + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions, + $applicableUsers, + $applicableGroups, + $priority + ); + } catch (\InvalidArgumentException $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + return new DataResponse( + [ + 'message' => $this->l10n->t('Invalid backend or authentication mechanism class') + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + } + + /** + * Validate storage config + * + * @param StorageConfig $storage storage config + *1 + * @return DataResponse|null returns response in case of validation error + */ + protected function validate(StorageConfig $storage) { + $mountPoint = $storage->getMountPoint(); + if ($mountPoint === '') { + return new DataResponse( + [ + 'message' => $this->l10n->t('Invalid mount point'), + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + if ($storage->getBackendOption('objectstore')) { + // objectstore must not be sent from client side + return new DataResponse( + [ + 'message' => $this->l10n->t('Objectstore forbidden'), + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + /** @var Backend */ + $backend = $storage->getBackend(); + /** @var AuthMechanism */ + $authMechanism = $storage->getAuthMechanism(); + if ($backend->checkDependencies()) { + // invalid backend + return new DataResponse( + [ + 'message' => $this->l10n->t('Invalid storage backend "%s"', [ + $backend->getIdentifier(), + ]), + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + if (!$backend->isVisibleFor($this->service->getVisibilityType())) { + // not permitted to use backend + return new DataResponse( + [ + 'message' => $this->l10n->t('Not permitted to use backend "%s"', [ + $backend->getIdentifier(), + ]), + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + if (!$authMechanism->isVisibleFor($this->service->getVisibilityType())) { + // not permitted to use auth mechanism + return new DataResponse( + [ + 'message' => $this->l10n->t('Not permitted to use authentication mechanism "%s"', [ + $authMechanism->getIdentifier(), + ]), + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + if (!$backend->validateStorage($storage)) { + // unsatisfied parameters + return new DataResponse( + [ + 'message' => $this->l10n->t('Unsatisfied backend parameters'), + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + if (!$authMechanism->validateStorage($storage)) { + // unsatisfied parameters + return new DataResponse( + [ + 'message' => $this->l10n->t('Unsatisfied authentication mechanism parameters'), + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + return null; + } + + protected function manipulateStorageConfig(StorageConfig $storage) { + /** @var AuthMechanism */ + $authMechanism = $storage->getAuthMechanism(); + $authMechanism->manipulateStorageConfig($storage); + /** @var Backend */ + $backend = $storage->getBackend(); + $backend->manipulateStorageConfig($storage); + } + + /** + * Check whether the given storage is available / valid. + * + * Note that this operation can be time consuming depending + * on whether the remote storage is available or not. + * + * @param StorageConfig $storage storage configuration + */ + protected function updateStorageStatus(StorageConfig &$storage) { + try { + $this->manipulateStorageConfig($storage); + + /** @var Backend */ + $backend = $storage->getBackend(); + // update status (can be time-consuming) + $storage->setStatus( + MountConfig::getBackendStatus( + $backend->getStorageClass(), + $storage->getBackendOptions(), + ) + ); + } catch (InsufficientDataForMeaningfulAnswerException $e) { + $status = $e->getCode() ?: StorageNotAvailableException::STATUS_INDETERMINATE; + $storage->setStatus( + (int)$status, + $this->l10n->t('Insufficient data: %s', [$e->getMessage()]) + ); + } catch (StorageNotAvailableException $e) { + $storage->setStatus( + (int)$e->getCode(), + $this->l10n->t('%s', [$e->getMessage()]) + ); + } catch (\Exception $e) { + // FIXME: convert storage exceptions to StorageNotAvailableException + $storage->setStatus( + StorageNotAvailableException::STATUS_ERROR, + get_class($e) . ': ' . $e->getMessage() + ); + } + } + + /** + * Get all storage entries + * + * @return DataResponse + */ + public function index() { + $storages = array_map(static fn ($storage) => $storage->jsonSerialize(true), $this->service->getStorages()); + + return new DataResponse( + $storages, + Http::STATUS_OK + ); + } + + /** + * Get an external storage entry. + * + * @param int $id storage id + * + * @return DataResponse + */ + public function show(int $id) { + try { + $storage = $this->service->getStorage($id); + + $this->updateStorageStatus($storage); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id]), + ], + Http::STATUS_NOT_FOUND + ); + } + + $data = $storage->jsonSerialize(true); + $isAdmin = $this->groupManager->isAdmin($this->userSession->getUser()->getUID()); + $data['can_edit'] = $storage->getType() === StorageConfig::MOUNT_TYPE_PERSONAL || $isAdmin; + + return new DataResponse( + $data, + Http::STATUS_OK + ); + } + + /** + * Deletes the storage with the given id. + * + * @param int $id storage id + * + * @return DataResponse + */ + #[PasswordConfirmationRequired(strict: true)] + public function destroy(int $id) { + try { + $this->service->removeStorage($id); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id]), + ], + Http::STATUS_NOT_FOUND + ); + } + + return new DataResponse([], Http::STATUS_NO_CONTENT); + } +} diff --git a/apps/files_external/lib/Controller/UserGlobalStoragesController.php b/apps/files_external/lib/Controller/UserGlobalStoragesController.php new file mode 100644 index 00000000000..88a9f936401 --- /dev/null +++ b/apps/files_external/lib/Controller/UserGlobalStoragesController.php @@ -0,0 +1,193 @@ +<?php + +/** + * 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\Controller; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\IUserProvided; +use OCA\Files_External\Lib\Auth\Password\UserGlobalAuth; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * User global storages controller + */ +class UserGlobalStoragesController extends StoragesController { + /** + * Creates a new user global storages controller. + * + * @param string $AppName application name + * @param IRequest $request request object + * @param IL10N $l10n l10n service + * @param UserGlobalStoragesService $userGlobalStoragesService storage service + * @param LoggerInterface $logger + * @param IUserSession $userSession + * @param IGroupManager $groupManager + */ + public function __construct( + $AppName, + IRequest $request, + IL10N $l10n, + UserGlobalStoragesService $userGlobalStoragesService, + LoggerInterface $logger, + IUserSession $userSession, + IGroupManager $groupManager, + IConfig $config, + ) { + parent::__construct( + $AppName, + $request, + $l10n, + $userGlobalStoragesService, + $logger, + $userSession, + $groupManager, + $config + ); + } + + /** + * Get all storage entries + * + * @return DataResponse + */ + #[NoAdminRequired] + public function index() { + /** @var UserGlobalStoragesService */ + $service = $this->service; + $storages = array_map(function ($storage) { + // remove configuration data, this must be kept private + $this->sanitizeStorage($storage); + return $storage->jsonSerialize(true); + }, $service->getUniqueStorages()); + + return new DataResponse( + $storages, + Http::STATUS_OK + ); + } + + protected function manipulateStorageConfig(StorageConfig $storage) { + /** @var AuthMechanism */ + $authMechanism = $storage->getAuthMechanism(); + $authMechanism->manipulateStorageConfig($storage, $this->userSession->getUser()); + /** @var Backend */ + $backend = $storage->getBackend(); + $backend->manipulateStorageConfig($storage, $this->userSession->getUser()); + } + + /** + * Get an external storage entry. + * + * @param int $id storage id + * @return DataResponse + */ + #[NoAdminRequired] + public function show($id) { + try { + $storage = $this->service->getStorage($id); + + $this->updateStorageStatus($storage); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id]) + ], + Http::STATUS_NOT_FOUND + ); + } + + $this->sanitizeStorage($storage); + + $data = $storage->jsonSerialize(true); + $isAdmin = $this->groupManager->isAdmin($this->userSession->getUser()->getUID()); + $data['can_edit'] = $storage->getType() === StorageConfig::MOUNT_TYPE_PERSONAL || $isAdmin; + + return new DataResponse( + $data, + Http::STATUS_OK + ); + } + + /** + * Update an external storage entry. + * Only allows setting user provided backend fields + * + * @param int $id storage id + * @param array $backendOptions backend-specific options + * + * @return DataResponse + */ + #[NoAdminRequired] + #[PasswordConfirmationRequired(strict: true)] + public function update( + $id, + $backendOptions, + ) { + try { + $storage = $this->service->getStorage($id); + $authMechanism = $storage->getAuthMechanism(); + if ($authMechanism instanceof IUserProvided || $authMechanism instanceof UserGlobalAuth) { + $authMechanism->saveBackendOptions($this->userSession->getUser(), $id, $backendOptions); + $authMechanism->manipulateStorageConfig($storage, $this->userSession->getUser()); + } else { + return new DataResponse( + [ + 'message' => $this->l10n->t('Storage with ID "%d" is not editable by non-admins', [$id]) + ], + Http::STATUS_FORBIDDEN + ); + } + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id]) + ], + Http::STATUS_NOT_FOUND + ); + } + + $this->updateStorageStatus($storage); + $this->sanitizeStorage($storage); + + return new DataResponse( + $storage->jsonSerialize(true), + Http::STATUS_OK + ); + } + + /** + * Remove sensitive data from a StorageConfig before returning it to the user + * + * @param StorageConfig $storage + */ + protected function sanitizeStorage(StorageConfig $storage) { + $storage->setBackendOptions([]); + $storage->setMountOptions([]); + + if ($storage->getAuthMechanism() instanceof IUserProvided) { + try { + $storage->getAuthMechanism()->manipulateStorageConfig($storage, $this->userSession->getUser()); + } catch (InsufficientDataForMeaningfulAnswerException $e) { + // not configured yet + } + } + } +} diff --git a/apps/files_external/lib/Controller/UserStoragesController.php b/apps/files_external/lib/Controller/UserStoragesController.php new file mode 100644 index 00000000000..7b564d57f7e --- /dev/null +++ b/apps/files_external/lib/Controller/UserStoragesController.php @@ -0,0 +1,214 @@ +<?php + +/** + * 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\Controller; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\NotFoundException; +use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * User storages controller + */ +class UserStoragesController extends StoragesController { + /** + * Creates a new user storages controller. + * + * @param string $AppName application name + * @param IRequest $request request object + * @param IL10N $l10n l10n service + * @param UserStoragesService $userStoragesService storage service + * @param LoggerInterface $logger + * @param IUserSession $userSession + * @param IGroupManager $groupManager + */ + public function __construct( + $AppName, + IRequest $request, + IL10N $l10n, + UserStoragesService $userStoragesService, + LoggerInterface $logger, + IUserSession $userSession, + IGroupManager $groupManager, + IConfig $config, + ) { + parent::__construct( + $AppName, + $request, + $l10n, + $userStoragesService, + $logger, + $userSession, + $groupManager, + $config + ); + } + + protected function manipulateStorageConfig(StorageConfig $storage) { + /** @var AuthMechanism */ + $authMechanism = $storage->getAuthMechanism(); + $authMechanism->manipulateStorageConfig($storage, $this->userSession->getUser()); + /** @var Backend */ + $backend = $storage->getBackend(); + $backend->manipulateStorageConfig($storage, $this->userSession->getUser()); + } + + /** + * Get all storage entries + * + * @return DataResponse + */ + #[NoAdminRequired] + public function index() { + return parent::index(); + } + + /** + * Return storage + * + * {@inheritdoc} + */ + #[NoAdminRequired] + public function show(int $id) { + return parent::show($id); + } + + /** + * Create an external storage entry. + * + * @param string $mountPoint storage mount point + * @param string $backend backend identifier + * @param string $authMechanism authentication mechanism identifier + * @param array $backendOptions backend-specific options + * @param array $mountOptions backend-specific mount options + * + * @return DataResponse + */ + #[NoAdminRequired] + #[PasswordConfirmationRequired(strict: true)] + public function create( + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions, + ) { + $canCreateNewLocalStorage = $this->config->getSystemValue('files_external_allow_create_new_local', true); + if (!$canCreateNewLocalStorage && $backend === 'local') { + return new DataResponse( + [ + 'message' => $this->l10n->t('Forbidden to manage local mounts') + ], + Http::STATUS_FORBIDDEN + ); + } + $newStorage = $this->createStorage( + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions + ); + if ($newStorage instanceof DataResponse) { + return $newStorage; + } + + $response = $this->validate($newStorage); + if (!empty($response)) { + return $response; + } + + $newStorage = $this->service->addStorage($newStorage); + $this->updateStorageStatus($newStorage); + + return new DataResponse( + $newStorage->jsonSerialize(true), + Http::STATUS_CREATED + ); + } + + /** + * Update an external storage entry. + * + * @param int $id storage id + * @param string $mountPoint storage mount point + * @param string $backend backend identifier + * @param string $authMechanism authentication mechanism identifier + * @param array $backendOptions backend-specific options + * @param array $mountOptions backend-specific mount options + * + * @return DataResponse + */ + #[NoAdminRequired] + #[PasswordConfirmationRequired(strict: true)] + public function update( + $id, + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions, + ) { + $storage = $this->createStorage( + $mountPoint, + $backend, + $authMechanism, + $backendOptions, + $mountOptions + ); + if ($storage instanceof DataResponse) { + return $storage; + } + $storage->setId($id); + + $response = $this->validate($storage); + if (!empty($response)) { + return $response; + } + + try { + $storage = $this->service->updateStorage($storage); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id]) + ], + Http::STATUS_NOT_FOUND + ); + } + + $this->updateStorageStatus($storage); + + return new DataResponse( + $storage->jsonSerialize(true), + Http::STATUS_OK + ); + } + + /** + * Delete storage + * + * {@inheritdoc} + */ + #[NoAdminRequired] + #[PasswordConfirmationRequired(strict: true)] + public function destroy(int $id) { + return parent::destroy($id); + } +} diff --git a/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php b/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php new file mode 100644 index 00000000000..c86c88a13d7 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php @@ -0,0 +1,31 @@ +<?php + +/** + * 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; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +/** + * Amazon S3 access key authentication + */ +class AccessKey extends AuthMechanism { + public const SCHEME_AMAZONS3_ACCESSKEY = 'amazons3_accesskey'; + + public function __construct(IL10N $l) { + $this + ->setIdentifier('amazons3::accesskey') + ->setScheme(self::SCHEME_AMAZONS3_ACCESSKEY) + ->setText($l->t('Access key')) + ->addParameters([ + new DefinitionParameter('key', $l->t('Access key')), + (new DefinitionParameter('secret', $l->t('Secret key'))) + ->setType(DefinitionParameter::VALUE_PASSWORD), + ]); + } +} diff --git a/apps/files_external/lib/auth/authmechanism.php b/apps/files_external/lib/Lib/Auth/AuthMechanism.php index 68d6f023487..7b0544100fb 100644 --- a/apps/files_external/lib/auth/authmechanism.php +++ b/apps/files_external/lib/Lib/Auth/AuthMechanism.php @@ -1,32 +1,19 @@ <?php + /** - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Auth; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\VisibilityTrait; -use \OCA\Files_External\Lib\IdentifierTrait; -use \OCA\Files_External\Lib\FrontendDefinitionTrait; -use \OCA\Files_External\Lib\StorageModifierTrait; +use OCA\Files_External\Lib\FrontendDefinitionTrait; +use OCA\Files_External\Lib\IdentifierTrait; +use OCA\Files_External\Lib\IFrontendDefinition; +use OCA\Files_External\Lib\IIdentifier; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Lib\StorageModifierTrait; +use OCA\Files_External\Lib\VisibilityTrait; /** * Authentication mechanism @@ -35,7 +22,7 @@ use \OCA\Files_External\Lib\StorageModifierTrait; * such as \OCP\IDB for database operations. This allows an authentication * mechanism to perform advanced operations based on provided information. * - * An authenication scheme defines the parameter interface, common to the + * An authentication scheme defines the parameter interface, common to the * storage implementation, the backend and the authentication mechanism. * A storage implementation expects parameters according to the authentication * scheme, which are provided from the authentication mechanism. @@ -48,16 +35,15 @@ use \OCA\Files_External\Lib\StorageModifierTrait; * - StorageModifierTrait * Object can affect storage mounting */ -class AuthMechanism implements \JsonSerializable { - +class AuthMechanism implements \JsonSerializable, IIdentifier, IFrontendDefinition { /** Standard authentication schemes */ - const SCHEME_NULL = 'null'; - const SCHEME_BUILTIN = 'builtin'; - const SCHEME_PASSWORD = 'password'; - const SCHEME_OAUTH1 = 'oauth1'; - const SCHEME_OAUTH2 = 'oauth2'; - const SCHEME_PUBLICKEY = 'publickey'; - const SCHEME_OPENSTACK = 'openstack'; + public const SCHEME_NULL = 'null'; + public const SCHEME_BUILTIN = 'builtin'; + public const SCHEME_PASSWORD = 'password'; + public const SCHEME_OAUTH2 = 'oauth2'; + public const SCHEME_PUBLICKEY = 'publickey'; + public const SCHEME_OPENSTACK = 'openstack'; + public const SCHEME_SMB = 'smb'; use VisibilityTrait; use FrontendDefinitionTrait; @@ -79,7 +65,7 @@ class AuthMechanism implements \JsonSerializable { /** * @param string $scheme - * @return self + * @return $this */ public function setScheme($scheme) { $this->scheme = $scheme; @@ -88,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(); @@ -116,5 +100,4 @@ class AuthMechanism implements \JsonSerializable { return $this->validateStorageDefinition($storage); } - } diff --git a/apps/files_external/lib/Lib/Auth/Builtin.php b/apps/files_external/lib/Lib/Auth/Builtin.php new file mode 100644 index 00000000000..8e12a6daca6 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/Builtin.php @@ -0,0 +1,23 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Auth; + +use OCP\IL10N; + +/** + * Builtin authentication mechanism, for legacy backends + */ +class Builtin extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('builtin::builtin') + ->setScheme(self::SCHEME_BUILTIN) + ->setText($l->t('Builtin')) + ; + } +} diff --git a/apps/files_external/lib/Lib/Auth/IUserProvided.php b/apps/files_external/lib/Lib/Auth/IUserProvided.php new file mode 100644 index 00000000000..2350d7f6db4 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/IUserProvided.php @@ -0,0 +1,21 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Auth; + +use OCP\IUser; + +/** + * For auth mechanisms where the user needs to provide credentials + */ +interface IUserProvided { + /** + * @param IUser $user the user for which to save the user provided options + * @param int $mountId the mount id to save the options for + * @param array $options the user provided options + */ + public function saveBackendOptions(IUser $user, $mountId, array $options); +} diff --git a/apps/files_external/lib/Lib/Auth/InvalidAuth.php b/apps/files_external/lib/Lib/Auth/InvalidAuth.php new file mode 100644 index 00000000000..2af24f1ea07 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/InvalidAuth.php @@ -0,0 +1,29 @@ +<?php + +/** + * 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; + +/** + * Invalid authentication representing an auth mechanism + * that could not be resolved0 + */ +class InvalidAuth extends AuthMechanism { + + /** + * Constructs a new InvalidAuth with the id of the invalid auth + * for display purposes + * + * @param string $invalidId invalid id + */ + public function __construct($invalidId) { + $this + ->setIdentifier($invalidId) + ->setScheme(self::SCHEME_NULL) + ->setText('Unknown auth mechanism backend ' . $invalidId) + ; + } +} diff --git a/apps/files_external/lib/Lib/Auth/NullMechanism.php b/apps/files_external/lib/Lib/Auth/NullMechanism.php new file mode 100644 index 00000000000..8e2e5b656b2 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/NullMechanism.php @@ -0,0 +1,23 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Auth; + +use OCP\IL10N; + +/** + * Null authentication mechanism + */ +class NullMechanism extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('null::null') + ->setScheme(self::SCHEME_NULL) + ->setText($l->t('None')) + ; + } +} diff --git a/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php b/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php new file mode 100644 index 00000000000..beaf73c2344 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php @@ -0,0 +1,37 @@ +<?php + +/** + * 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; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +/** + * OAuth2 authentication + */ +class OAuth2 extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('oauth2::oauth2') + ->setScheme(self::SCHEME_OAUTH2) + ->setText($l->t('OAuth2')) + ->addParameters([ + (new DefinitionParameter('configured', 'configured')) + ->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_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 new file mode 100644 index 00000000000..3b1c9f123af --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php @@ -0,0 +1,32 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Auth\OpenStack; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +/** + * OpenStack Keystone authentication + */ +class OpenStackV2 extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('openstack::openstack') + ->setScheme(self::SCHEME_OPENSTACK) + ->setText($l->t('OpenStack v2')) + ->addParameters([ + new DefinitionParameter('user', $l->t('Login')), + (new DefinitionParameter('password', $l->t('Password'))) + ->setType(DefinitionParameter::VALUE_PASSWORD), + new DefinitionParameter('tenant', $l->t('Tenant name')), + new DefinitionParameter('url', $l->t('Identity endpoint URL')), + ]) + ; + } +} diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php new file mode 100644 index 00000000000..b5d185fd374 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Auth\OpenStack; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +/** + * OpenStack Keystone authentication + */ +class OpenStackV3 extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('openstack::openstackv3') + ->setScheme(self::SCHEME_OPENSTACK) + ->setText($l->t('OpenStack v3')) + ->addParameters([ + new DefinitionParameter('user', $l->t('Login')), + new DefinitionParameter('domain', $l->t('Domain')), + (new DefinitionParameter('password', $l->t('Password'))) + ->setType(DefinitionParameter::VALUE_PASSWORD), + new DefinitionParameter('tenant', $l->t('Tenant name')), + new DefinitionParameter('url', $l->t('Identity endpoint URL')) + ]) + ; + } +} diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php b/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php new file mode 100644 index 00000000000..b1d1068e586 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php @@ -0,0 +1,30 @@ +<?php + +/** + * 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; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +/** + * Rackspace authentication + */ +class Rackspace extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('openstack::rackspace') + ->setScheme(self::SCHEME_OPENSTACK) + ->setText($l->t('Rackspace')) + ->addParameters([ + new DefinitionParameter('user', $l->t('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 new file mode 100644 index 00000000000..916b496b506 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php @@ -0,0 +1,80 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2015 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Auth\Password; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCP\IL10N; +use OCP\IUser; +use OCP\Security\ICredentialsManager; + +/** + * Global Username and Password + */ +class GlobalAuth extends AuthMechanism { + public const CREDENTIALS_IDENTIFIER = 'password::global'; + private const PWD_PLACEHOLDER = '************************'; + + public function __construct( + IL10N $l, + protected ICredentialsManager $credentialsManager, + ) { + $this + ->setIdentifier('password::global') + ->setVisibility(BackendService::VISIBILITY_DEFAULT) + ->setScheme(self::SCHEME_PASSWORD) + ->setText($l->t('Global credentials')); + } + + public function getAuth($uid) { + $auth = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER); + if (!is_array($auth)) { + return [ + 'user' => '', + '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 + ]); + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + if ($storage->getType() === StorageConfig::MOUNT_TYPE_ADMIN) { + $uid = ''; + } elseif (is_null($user)) { + throw new InsufficientDataForMeaningfulAnswerException('No credentials saved'); + } else { + $uid = $user->getUID(); + } + $credentials = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER); + + if (is_array($credentials)) { + $storage->setBackendOption('user', $credentials['user']); + $storage->setBackendOption('password', $credentials['password']); + } + } +} diff --git a/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php b/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php new file mode 100644 index 00000000000..ce38140b6ee --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php @@ -0,0 +1,113 @@ +<?php + +/** + * 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; +use OCP\Authentication\Exceptions\CredentialsUnavailableException; +use OCP\Authentication\LoginCredentials\IStore as CredentialsStore; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IL10N; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserBackend; +use OCP\LDAP\ILDAPProviderFactory; +use OCP\Security\ICredentialsManager; +use OCP\User\Events\PasswordUpdatedEvent; +use OCP\User\Events\UserLoggedInEvent; + +/** + * Username and password from login credentials, saved in DB + */ +class LoginCredentials extends AuthMechanism { + public const CREDENTIALS_IDENTIFIER = 'password::logincredentials/credentials'; + + 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); + $eventDispatcher->addServiceListener(PasswordUpdatedEvent::class, StorePasswordListener::class); + } + + private function getCredentials(IUser $user): array { + $credentials = $this->credentialsManager->retrieve($user->getUID(), self::CREDENTIALS_IDENTIFIER); + + if (is_null($credentials)) { + // nothing saved in db, try to get it from the session and save it + try { + $sessionCredentials = $this->credentialsStore->getLoginCredentials(); + + if ($sessionCredentials->getUID() !== $user->getUID()) { + // Can't take the credentials from the session as they are not the same user + throw new CredentialsUnavailableException(); + } + + $credentials = [ + 'user' => $sessionCredentials->getLoginName(), + 'password' => $sessionCredentials->getPassword(), + ]; + + $this->credentialsManager->store($user->getUID(), self::CREDENTIALS_IDENTIFIER, $credentials); + } catch (CredentialsUnavailableException $e) { + throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved'); + } + } + + return $credentials; + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + if (!isset($user)) { + throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved'); + } + $credentials = $this->getCredentials($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 new file mode 100644 index 00000000000..d4291148e3e --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/Password/Password.php @@ -0,0 +1,29 @@ +<?php + +/** + * 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; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +/** + * Basic password authentication mechanism + */ +class Password extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('password::password') + ->setScheme(self::SCHEME_PASSWORD) + ->setText($l->t('Login and password')) + ->addParameters([ + 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 new file mode 100644 index 00000000000..8f161073771 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php @@ -0,0 +1,67 @@ +<?php + +/** + * 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\IStorage; +use OCP\Files\StorageAuthException; +use OCP\IL10N; +use OCP\IUser; + +/** + * Username and password from login credentials, saved in session + */ +class SessionCredentials extends AuthMechanism { + + 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([ + (new DefinitionParameter('password', $l->t('Password'))) + ->setType(DefinitionParameter::VALUE_PASSWORD) + ->setFlag(DefinitionParameter::FLAG_HIDDEN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + ]); + } + + /** + * @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(IStorage $storage): IStorage { + return new SessionStorageWrapper(['storage' => $storage]); + } +} diff --git a/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php b/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php new file mode 100644 index 00000000000..cb7165261ac --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Auth\Password; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCP\IL10N; +use OCP\IUser; +use OCP\Security\ICredentialsManager; + +/** + * User provided Global Username and Password + */ +class UserGlobalAuth extends AuthMechanism { + private const CREDENTIALS_IDENTIFIER = 'password::global'; + + public function __construct( + IL10N $l, + protected ICredentialsManager $credentialsManager, + ) { + $this + ->setIdentifier('password::global::user') + ->setVisibility(BackendService::VISIBILITY_DEFAULT) + ->setScheme(self::SCHEME_PASSWORD) + ->setText($l->t('Global credentials, manually entered')); + } + + public function saveBackendOptions(IUser $user, $id, $backendOptions) { + // backendOptions are set when invoked via Files app + // but they are not set when invoked via ext storage settings + if (!isset($backendOptions['user']) && !isset($backendOptions['password'])) { + return; + } + + if ($backendOptions['password'] === DefinitionParameter::UNMODIFIED_PLACEHOLDER) { + $oldCredentials = $this->credentialsManager->retrieve($user->getUID(), self::CREDENTIALS_IDENTIFIER); + $backendOptions['password'] = $oldCredentials['password']; + } + + // make sure we're not setting any unexpected keys + $credentials = [ + 'user' => $backendOptions['user'], + 'password' => $backendOptions['password'], + ]; + $this->credentialsManager->store($user->getUID(), self::CREDENTIALS_IDENTIFIER, $credentials); + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + if ($user === null) { + throw new InsufficientDataForMeaningfulAnswerException('No credentials saved'); + } + + $uid = $user->getUID(); + $credentials = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER); + + if (is_array($credentials)) { + $storage->setBackendOption('user', $credentials['user']); + $storage->setBackendOption('password', $credentials['password']); + } + } +} diff --git a/apps/files_external/lib/Lib/Auth/Password/UserProvided.php b/apps/files_external/lib/Lib/Auth/Password/UserProvided.php new file mode 100644 index 00000000000..b158392f6eb --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/Password/UserProvided.php @@ -0,0 +1,77 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2015 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Auth\Password; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\IUserProvided; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCP\IL10N; +use OCP\IUser; +use OCP\Security\ICredentialsManager; + +/** + * User provided Username and Password + */ +class UserProvided extends AuthMechanism implements IUserProvided { + public const CREDENTIALS_IDENTIFIER_PREFIX = 'password::userprovided/'; + + public function __construct( + IL10N $l, + protected ICredentialsManager $credentialsManager, + ) { + $this + ->setIdentifier('password::userprovided') + ->setVisibility(BackendService::VISIBILITY_ADMIN) + ->setScheme(self::SCHEME_PASSWORD) + ->setText($l->t('Manually entered, store in database')) + ->addParameters([ + (new DefinitionParameter('user', $l->t('Login'))) + ->setFlag(DefinitionParameter::FLAG_USER_PROVIDED), + (new DefinitionParameter('password', $l->t('Password'))) + ->setType(DefinitionParameter::VALUE_PASSWORD) + ->setFlag(DefinitionParameter::FLAG_USER_PROVIDED), + ]); + } + + private function getCredentialsIdentifier($storageId) { + return self::CREDENTIALS_IDENTIFIER_PREFIX . $storageId; + } + + 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 + ]); + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + if (!isset($user)) { + throw new InsufficientDataForMeaningfulAnswerException('No credentials saved'); + } + $uid = $user->getUID(); + $credentials = $this->credentialsManager->retrieve($uid, $this->getCredentialsIdentifier($storage->getId())); + + if (!isset($credentials)) { + throw new InsufficientDataForMeaningfulAnswerException('No credentials saved'); + } + + $storage->setBackendOption('user', $credentials['user']); + $storage->setBackendOption('password', $credentials['password']); + } +} diff --git a/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php b/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php new file mode 100644 index 00000000000..ad95c743d2d --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php @@ -0,0 +1,75 @@ +<?php + +/** + * 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; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\StorageConfig; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IUser; +use phpseclib\Crypt\RSA as RSACrypt; + +/** + * RSA public key authentication + */ +class RSA extends AuthMechanism { + + 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('Login')), + new DefinitionParameter('public_key', $l->t('Public key')), + (new DefinitionParameter('private_key', 'private_key')) + ->setType(DefinitionParameter::VALUE_PASSWORD) + ->setFlag(DefinitionParameter::FLAG_HIDDEN), + ]) + ->addCustomJs('public_key') + ; + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + $auth = new RSACrypt(); + $auth->setPassword($this->config->getSystemValue('secret', '')); + if (!$auth->loadKey($storage->getBackendOption('private_key'))) { + // Add fallback routine for a time where secret was not enforced to be exists + $auth->setPassword(''); + if (!$auth->loadKey($storage->getBackendOption('private_key'))) { + throw new \RuntimeException('unable to load private key'); + } + } + $storage->setBackendOption('public_key_auth', $auth); + } + + /** + * Generate a keypair + * + * @param int $keyLenth + * @return array ['privatekey' => $privateKey, 'publickey' => $publicKey] + */ + public function createKey($keyLength) { + $rsa = new RSACrypt(); + $rsa->setPublicKeyFormat(RSACrypt::PUBLIC_FORMAT_OPENSSH); + $rsa->setPassword($this->config->getSystemValue('secret', '')); + + if ($keyLength !== 1024 && $keyLength !== 2048 && $keyLength !== 4096) { + $keyLength = 1024; + } + + return $rsa->createKey($keyLength); + } +} diff --git a/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php b/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php new file mode 100644 index 00000000000..8f58b71d5ac --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php @@ -0,0 +1,54 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Auth\PublicKey; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\StorageConfig; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IUser; +use phpseclib\Crypt\RSA as RSACrypt; + +/** + * RSA public key authentication + */ +class RSAPrivateKey extends AuthMechanism { + + public function __construct( + IL10N $l, + private IConfig $config, + ) { + $this + ->setIdentifier('publickey::rsa_private') + ->setScheme(self::SCHEME_PUBLICKEY) + ->setText($l->t('RSA private key')) + ->addParameters([ + new DefinitionParameter('user', $l->t('Login')), + (new DefinitionParameter('password', $l->t('Password'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setType(DefinitionParameter::VALUE_PASSWORD), + new DefinitionParameter('private_key', $l->t('Private key')), + ]); + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + $auth = new RSACrypt(); + $auth->setPassword($this->config->getSystemValue('secret', '')); + if (!$auth->loadKey($storage->getBackendOption('private_key'))) { + // Add fallback routine for a time where secret was not enforced to be exists + $auth->setPassword(''); + if (!$auth->loadKey($storage->getBackendOption('private_key'))) { + throw new \RuntimeException('unable to load private key'); + } + } + $storage->setBackendOption('public_key_auth', $auth); + } +} diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php new file mode 100644 index 00000000000..26671110294 --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php @@ -0,0 +1,35 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Lib\Auth\SMB; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\Authentication\LoginCredentials\IStore; +use OCP\IL10N; + +class KerberosApacheAuth extends AuthMechanism { + public function __construct( + IL10N $l, + private IStore $credentialsStore, + ) { + $realm = new DefinitionParameter('default_realm', 'Default realm'); + $realm + ->setType(DefinitionParameter::VALUE_TEXT) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setTooltip($l->t('Kerberos default realm, defaults to "WORKGROUP"')); + $this + ->setIdentifier('smb::kerberosapache') + ->setScheme(self::SCHEME_SMB) + ->setText($l->t('Kerberos ticket Apache mode')) + ->addParameter($realm); + } + + public function getCredentialsStore(): IStore { + return $this->credentialsStore; + } +} diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php new file mode 100644 index 00000000000..9210209192a --- /dev/null +++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php @@ -0,0 +1,19 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Auth\SMB; + +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCP\IL10N; + +class KerberosAuth extends AuthMechanism { + public function __construct(IL10N $l) { + $this + ->setIdentifier('smb::kerberos') + ->setScheme(self::SCHEME_SMB) + ->setText($l->t('Kerberos ticket')); + } +} diff --git a/apps/files_external/lib/Lib/Backend/AmazonS3.php b/apps/files_external/lib/Lib/Backend/AmazonS3.php new file mode 100644 index 00000000000..464b03b55e0 --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/AmazonS3.php @@ -0,0 +1,54 @@ +<?php + +/** + * 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; + +class AmazonS3 extends Backend { + use LegacyDependencyCheckPolyfill; + + public function __construct(IL10N $l, AccessKey $legacyAuth) { + $this + ->setIdentifier('amazons3') + ->addIdentifierAlias('\OC\Files\Storage\AmazonS3') // legacy compat + ->setStorageClass('\OCA\Files_External\Lib\Storage\AmazonS3') + ->setText($l->t('Amazon S3')) + ->addParameters([ + new DefinitionParameter('bucket', $l->t('Bucket')), + (new DefinitionParameter('hostname', $l->t('Hostname'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('port', $l->t('Port'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('region', $l->t('Region'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('storageClass', $l->t('Storage Class'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('use_ssl', $l->t('Enable SSL'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->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/backend/backend.php b/apps/files_external/lib/Lib/Backend/Backend.php index 8fb84b0e835..f7500ee24a4 100644 --- a/apps/files_external/lib/backend/backend.php +++ b/apps/files_external/lib/Lib/Backend/Backend.php @@ -1,34 +1,23 @@ <?php + /** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\VisibilityTrait; -use \OCA\Files_External\Lib\FrontendDefinitionTrait; -use \OCA\Files_External\Lib\PriorityTrait; -use \OCA\Files_External\Lib\DependencyTrait; -use \OCA\Files_External\Lib\StorageModifierTrait; -use \OCA\Files_External\Lib\IdentifierTrait; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\DependencyTrait; +use OCA\Files_External\Lib\FrontendDefinitionTrait; +use OCA\Files_External\Lib\IdentifierTrait; +use OCA\Files_External\Lib\IFrontendDefinition; +use OCA\Files_External\Lib\IIdentifier; +use OCA\Files_External\Lib\PriorityTrait; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Lib\StorageModifierTrait; +use OCA\Files_External\Lib\VisibilityTrait; +use OCP\Files\Storage\IStorage; /** * Storage backend @@ -37,7 +26,7 @@ use \OCA\Files_External\Lib\Auth\AuthMechanism; * such as \OCP\IDB for database operations. This allows a backend * to perform advanced operations based on provided information. * - * An authenication scheme defines the parameter interface, common to the + * An authentication scheme defines the parameter interface, common to the * storage implementation, the backend and the authentication mechanism. * A storage implementation expects parameters according to the authentication * scheme, which are provided from the authentication mechanism. @@ -54,8 +43,7 @@ use \OCA\Files_External\Lib\Auth\AuthMechanism; * - StorageModifierTrait * Object can affect storage mounting */ -class Backend implements \JsonSerializable { - +class Backend implements \JsonSerializable, IIdentifier, IFrontendDefinition { use VisibilityTrait; use FrontendDefinitionTrait; use PriorityTrait; @@ -73,7 +61,7 @@ class Backend implements \JsonSerializable { private $legacyAuthMechanism; /** - * @return string + * @return class-string<IStorage> */ public function getStorageClass() { return $this->storageClass; @@ -81,7 +69,7 @@ class Backend implements \JsonSerializable { /** * @param string $class - * @return self + * @return $this */ public function setStorageClass($class) { $this->storageClass = $class; @@ -118,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(); @@ -160,6 +142,4 @@ class Backend implements \JsonSerializable { public function validateStorage(StorageConfig $storage) { return $this->validateStorageDefinition($storage); } - } - diff --git a/apps/files_external/lib/Lib/Backend/DAV.php b/apps/files_external/lib/Lib/Backend/DAV.php new file mode 100644 index 00000000000..dea9e7c5e77 --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/DAV.php @@ -0,0 +1,37 @@ +<?php + +/** + * 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\Password\Password; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCP\IL10N; + +class DAV extends Backend { + use LegacyDependencyCheckPolyfill; + + public function __construct(IL10N $l, Password $legacyAuth) { + $this + ->setIdentifier('dav') + ->addIdentifierAlias('\OC\Files\Storage\DAV') // legacy compat + ->setStorageClass('\OC\Files\Storage\DAV') + ->setText($l->t('WebDAV')) + ->addParameters([ + new DefinitionParameter('host', $l->t('URL')), + (new DefinitionParameter('root', $l->t('Remote subfolder'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('secure', $l->t('Secure https://'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->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 new file mode 100644 index 00000000000..72a8184c9b9 --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/FTP.php @@ -0,0 +1,39 @@ +<?php + +/** + * 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\Password\Password; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCP\IL10N; + +class FTP extends Backend { + use LegacyDependencyCheckPolyfill; + + public function __construct(IL10N $l, Password $legacyAuth) { + $this + ->setIdentifier('ftp') + ->addIdentifierAlias('\OC\Files\Storage\FTP') // legacy compat + ->setStorageClass('\OCA\Files_External\Lib\Storage\FTP') + ->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) + ->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 new file mode 100644 index 00000000000..48912c0e49e --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/InvalidBackend.php @@ -0,0 +1,50 @@ +<?php + +/** + * 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; +use OCP\Files\StorageNotAvailableException; +use OCP\IUser; + +/** + * Invalid storage backend representing a backend + * that could not be resolved + */ +class InvalidBackend extends Backend { + + /** + * 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( + private $invalidId, + ) { + $this + ->setIdentifier($this->invalidId) + ->setStorageClass('\OC\Files\Storage\FailedStorage') + ->setText('Unknown storage backend ' . $this->invalidId); + } + + /** + * Returns the invalid backend id + * + * @return string invalid backend id + */ + public function getInvalidId() { + return $this->invalidId; + } + + /** + * @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/backend/legacybackend.php b/apps/files_external/lib/Lib/Backend/LegacyBackend.php index 084758ff78a..9c7e5b01bc3 100644 --- a/apps/files_external/lib/backend/legacybackend.php +++ b/apps/files_external/lib/Lib/Backend/LegacyBackend.php @@ -1,37 +1,21 @@ <?php + /** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\Auth\Builtin; -use \OCA\Files_External\Lib\MissingDependency; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCA\Files_External\Lib\Auth\Builtin; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCA\Files_External\Lib\MissingDependency; /** - * Legacy compatibility for OC_Mount_Config::registerBackend() + * Legacy compatibility for OCA\Files_External\MountConfig::registerBackend() */ class LegacyBackend extends Backend { - use LegacyDependencyCheckPolyfill { LegacyDependencyCheckPolyfill::checkDependencies as doCheckDependencies; } @@ -61,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) @@ -84,7 +64,7 @@ class LegacyBackend extends Backend { $this->setPriority($definition['priority']); } if (isset($definition['custom'])) { - $this->setCustomJs($definition['custom']); + $this->addCustomJs($definition['custom']); } if (isset($definition['has_dependencies']) && $definition['has_dependencies']) { $this->hasDependencies = true; @@ -100,5 +80,4 @@ class LegacyBackend extends Backend { } return []; } - } diff --git a/apps/files_external/lib/Lib/Backend/Local.php b/apps/files_external/lib/Lib/Backend/Local.php new file mode 100644 index 00000000000..56940b8e83b --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/Local.php @@ -0,0 +1,38 @@ +<?php + +/** + * 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) { + $this + ->setIdentifier('local') + ->addIdentifierAlias('\OC\Files\Storage\Local') // legacy compat + ->setStorageClass('\OC\Files\Storage\Local') + ->setText($l->t('Local')) + ->addParameters([ + new DefinitionParameter('datadir', $l->t('Location')), + ]) + ->setAllowedVisibility(BackendService::VISIBILITY_ADMIN) + ->setPriority(BackendService::PRIORITY_DEFAULT + 50) + ->addAuthScheme(AuthMechanism::SCHEME_NULL) + ->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 new file mode 100644 index 00000000000..0c0e2c6d300 --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/OwnCloud.php @@ -0,0 +1,34 @@ +<?php + +/** + * 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\Auth\Password\Password; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +class OwnCloud extends Backend { + public function __construct(IL10N $l, Password $legacyAuth) { + $this + ->setIdentifier('owncloud') + ->addIdentifierAlias('\OC\Files\Storage\OwnCloud') // legacy compat + ->setStorageClass('\OCA\Files_External\Lib\Storage\OwnCloud') + ->setText($l->t('Nextcloud')) + ->addParameters([ + new DefinitionParameter('host', $l->t('URL')), + (new DefinitionParameter('root', $l->t('Remote subfolder'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('secure', $l->t('Secure https://'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->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 new file mode 100644 index 00000000000..0926cf7fd93 --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/SFTP.php @@ -0,0 +1,34 @@ +<?php + +/** + * 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\Auth\Password\Password; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +class SFTP extends Backend { + public function __construct(IL10N $l, Password $legacyAuth) { + $this + ->setIdentifier('sftp') + ->addIdentifierAlias('\OC\Files\Storage\SFTP') // legacy compat + ->setStorageClass('\OCA\Files_External\Lib\Storage\SFTP') + ->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), + ]) + ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) + ->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY) + ->setLegacyAuthMechanism($legacyAuth) + ; + } +} diff --git a/apps/files_external/lib/Lib/Backend/SFTP_Key.php b/apps/files_external/lib/Lib/Backend/SFTP_Key.php new file mode 100644 index 00000000000..278fae3fba7 --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/SFTP_Key.php @@ -0,0 +1,31 @@ +<?php + +/** + * 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\PublicKey\RSA; +use OCA\Files_External\Lib\DefinitionParameter; +use OCP\IL10N; + +class SFTP_Key extends Backend { + public function __construct(IL10N $l, RSA $legacyAuth, SFTP $sftpBackend) { + $this + ->setIdentifier('\OC\Files\Storage\SFTP_Key') + ->setStorageClass('\OCA\Files_External\Lib\Storage\SFTP') + ->setText($l->t('SFTP with secret key login')) + ->addParameters([ + new DefinitionParameter('host', $l->t('Host')), + (new DefinitionParameter('root', $l->t('Remote subfolder'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + ]) + ->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY) + ->setLegacyAuthMechanism($legacyAuth) + ->deprecateTo($sftpBackend) + ; + } +} diff --git a/apps/files_external/lib/Lib/Backend/SMB.php b/apps/files_external/lib/Lib/Backend/SMB.php new file mode 100644 index 00000000000..e86ad98880c --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/SMB.php @@ -0,0 +1,140 @@ +<?php + +/** + * 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\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 { + 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')) + ->addParameters([ + new DefinitionParameter('host', $l->t('Host')), + new DefinitionParameter('share', $l->t('Share')), + (new DefinitionParameter('root', $l->t('Remote subfolder'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('domain', $l->t('Domain'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('show_hidden', $l->t('Show hidden files'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('case_sensitive', $l->t('Case sensitive file system'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setDefaultValue(true) + ->setTooltip($l->t('Disabling it will allow to use a case insensitive file system, but comes with a performance penalty')), + (new DefinitionParameter('check_acl', $l->t('Verify ACL access when listing files'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setTooltip($l->t("Check the ACL's of each file or folder inside a directory to filter out items where the account has no read permissions, comes with a performance penalty")), + (new DefinitionParameter('timeout', $l->t('Timeout'))) + ->setType(DefinitionParameter::VALUE_TEXT) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL) + ->setFlag(DefinitionParameter::FLAG_HIDDEN), + ]) + ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) + ->addAuthScheme(AuthMechanism::SCHEME_SMB) + ->setLegacyAuthMechanism($legacyAuth); + } + + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null): void { + $auth = $storage->getAuthMechanism(); + if ($auth->getScheme() === AuthMechanism::SCHEME_PASSWORD) { + if (!is_string($storage->getBackendOption('user')) || !is_string($storage->getBackendOption('password'))) { + throw new \InvalidArgumentException('user or password is not set'); + } + + $smbAuth = new BasicAuth( + $storage->getBackendOption('user'), + $storage->getBackendOption('domain'), + $storage->getBackendOption('password') + ); + } else { + switch ($auth->getIdentifier()) { + case 'smb::kerberos': + $smbAuth = new KerberosAuth(); + break; + case 'smb::kerberosapache': + if (!$auth instanceof KerberosApacheAuthMechanism) { + throw new \InvalidArgumentException('invalid authentication backend'); + } + $credentialsStore = $auth->getCredentialsStore(); + $kerbAuth = new KerberosAuth(); + $kerbAuth->setTicket(KerberosTicket::fromEnv()); + // check if a kerberos ticket is available, else fallback to session credentials + if ($kerbAuth->getTicket()?->isValid()) { + $smbAuth = $kerbAuth; + } else { + try { + $credentials = $credentialsStore->getLoginCredentials(); + $loginName = $credentials->getLoginName(); + $pass = $credentials->getPassword(); + preg_match('/(.*)@(.*)/', $loginName, $matches); + $realm = $storage->getBackendOption('default_realm'); + if (empty($realm)) { + $realm = 'WORKGROUP'; + } + if (count($matches) === 0) { + $username = $loginName; + $workgroup = $realm; + } else { + [, $username, $workgroup] = $matches; + } + $smbAuth = new BasicAuth( + $username, + $workgroup, + $pass + ); + } catch (\Exception) { + throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved'); + } + } + + break; + default: + throw new \InvalidArgumentException('unknown authentication backend'); + } + } + + $storage->setBackendOption('auth', $smbAuth); + } + + public function checkDependencies(): array { + $system = \OCP\Server::get(SystemBridge::class); + if (NativeServer::available($system)) { + return []; + } elseif (Server::available($system)) { + $missing = new MissingDependency('php-smbclient'); + $missing->setOptional(true); + $missing->setMessage('The php-smbclient library provides improved compatibility and performance for SMB storages.'); + return [$missing]; + } else { + $missing = new MissingDependency('php-smbclient'); + $missing->setMessage('Either the php-smbclient library (preferred) or the smbclient binary is required for SMB storages.'); + return [$missing, new MissingDependency('smbclient')]; + } + } +} diff --git a/apps/files_external/lib/Lib/Backend/SMB_OC.php b/apps/files_external/lib/Lib/Backend/SMB_OC.php new file mode 100644 index 00000000000..bcb8d0fbf16 --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/SMB_OC.php @@ -0,0 +1,57 @@ +<?php + +/** + * 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\Password\SessionCredentials; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\Service\BackendService; +use OCP\IL10N; +use OCP\IUser; + +/** + * Deprecated SMB_OC class - use SMB with the password::sessioncredentials auth mechanism + */ +class SMB_OC extends Backend { + use LegacyDependencyCheckPolyfill; + + public function __construct(IL10N $l, SessionCredentials $legacyAuth, SMB $smbBackend) { + $this + ->setIdentifier('\OC\Files\Storage\SMB_OC') + ->setStorageClass('\OCA\Files_External\Lib\Storage\SMB') + ->setText($l->t('SMB/CIFS using OC login')) + ->addParameters([ + new DefinitionParameter('host', $l->t('Host')), + (new DefinitionParameter('username_as_share', $l->t('Login as share'))) + ->setType(DefinitionParameter::VALUE_BOOLEAN), + (new DefinitionParameter('share', $l->t('Share'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + (new DefinitionParameter('root', $l->t('Remote subfolder'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + ]) + ->setPriority(BackendService::PRIORITY_DEFAULT - 10) + ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) + ->setLegacyAuthMechanism($legacyAuth) + ->deprecateTo($smbBackend) + ; + } + + /** + * @return void + */ + public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) { + $username_as_share = ($storage->getBackendOption('username_as_share') === true); + + if ($username_as_share) { + $share = '/' . $storage->getBackendOption('user'); + $storage->setBackendOption('share', $share); + } + } +} diff --git a/apps/files_external/lib/Lib/Backend/Swift.php b/apps/files_external/lib/Lib/Backend/Swift.php new file mode 100644 index 00000000000..37527ba3dbb --- /dev/null +++ b/apps/files_external/lib/Lib/Backend/Swift.php @@ -0,0 +1,43 @@ +<?php + +/** + * 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\OpenStack\OpenStackV2; +use OCA\Files_External\Lib\Auth\OpenStack\Rackspace; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; +use OCP\IL10N; + +class Swift extends Backend { + use LegacyDependencyCheckPolyfill; + + public function __construct(IL10N $l, OpenStackV2 $openstackAuth, Rackspace $rackspaceAuth) { + $this + ->setIdentifier('swift') + ->addIdentifierAlias('\OC\Files\Storage\Swift') // legacy compat + ->setStorageClass('\OCA\Files_External\Lib\Storage\Swift') + ->setText($l->t('OpenStack Object Storage')) + ->addParameters([ + (new DefinitionParameter('service_name', $l->t('Service name'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + new DefinitionParameter('region', $l->t('Region')), + new DefinitionParameter('bucket', $l->t('Bucket')), + (new DefinitionParameter('timeout', $l->t('Request timeout (seconds)'))) + ->setFlag(DefinitionParameter::FLAG_OPTIONAL), + ]) + ->addAuthScheme(AuthMechanism::SCHEME_OPENSTACK) + ->setLegacyAuthMechanismCallback(function (array $params) use ($openstackAuth, $rackspaceAuth) { + if (isset($params['options']['key']) && $params['options']['key']) { + return $rackspaceAuth; + } + return $openstackAuth; + }) + ; + } +} diff --git a/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php b/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php new file mode 100644 index 00000000000..0c2e90a243c --- /dev/null +++ b/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php @@ -0,0 +1,23 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Config; + +use OCA\Files_External\Lib\Auth\AuthMechanism; + +/** + * Provider of external storage auth mechanisms + * @since 9.1.0 + */ +interface IAuthMechanismProvider { + + /** + * @since 9.1.0 + * @return AuthMechanism[] + */ + public function getAuthMechanisms(); +} diff --git a/apps/files_external/lib/Lib/Config/IBackendProvider.php b/apps/files_external/lib/Lib/Config/IBackendProvider.php new file mode 100644 index 00000000000..44c460c3138 --- /dev/null +++ b/apps/files_external/lib/Lib/Config/IBackendProvider.php @@ -0,0 +1,23 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib\Config; + +use OCA\Files_External\Lib\Backend\Backend; + +/** + * Provider of external storage backends + * @since 9.1.0 + */ +interface IBackendProvider { + + /** + * @since 9.1.0 + * @return Backend[] + */ + public function getBackends(); +} diff --git a/apps/files_external/lib/Lib/DefinitionParameter.php b/apps/files_external/lib/Lib/DefinitionParameter.php new file mode 100644 index 00000000000..a73dd2df967 --- /dev/null +++ b/apps/files_external/lib/Lib/DefinitionParameter.php @@ -0,0 +1,217 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib; + +/** + * Parameter for an external storage definition + */ +class DefinitionParameter implements \JsonSerializable { + // placeholder value for password fields, when the client updates a storage configuration + // placeholder values are ignored and the field is left unmodified + public const UNMODIFIED_PLACEHOLDER = '__unmodified__'; + + /** Value constants */ + public const VALUE_TEXT = 0; + public const VALUE_BOOLEAN = 1; + public const VALUE_PASSWORD = 2; + + /** Flag constants */ + public const FLAG_NONE = 0; + public const FLAG_OPTIONAL = 1; + public const FLAG_USER_PROVIDED = 2; + public const FLAG_HIDDEN = 4; + + /** @var string human-readable parameter tooltip */ + private string $tooltip = ''; + + /** @var int value type, see self::VALUE_* constants */ + private int $type = self::VALUE_TEXT; + + /** @var int flags, see self::FLAG_* constants */ + private int $flags = self::FLAG_NONE; + + /** + * @param string $name parameter name + * @param string $text parameter description + * @param mixed $defaultValue default value + */ + public function __construct( + private string $name, + private string $text, + private $defaultValue = null, + ) { + } + + /** + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * @return string + */ + public function getText(): string { + return $this->text; + } + + /** + * Get value type + * + * @return int + */ + public function getType(): int { + return $this->type; + } + + /** + * Set value type + * + * @param int $type + * @return self + */ + 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(): string { + switch ($this->type) { + case self::VALUE_BOOLEAN: + return 'boolean'; + case self::VALUE_TEXT: + return 'text'; + case self::VALUE_PASSWORD: + return 'password'; + default: + return 'unknown'; + } + } + + /** + * @return int + */ + public function getFlags(): int { + return $this->flags; + } + + /** + * @param int $flags + * @return self + */ + public function setFlags(int $flags) { + $this->flags = $flags; + return $this; + } + + /** + * @param int $flag + * @return self + */ + public function setFlag(int $flag) { + $this->flags |= $flag; + return $this; + } + + /** + * @param int $flag + * @return bool + */ + public function isFlagSet(int $flag): bool { + return (bool)($this->flags & $flag); + } + + /** + * @return string + */ + public function getTooltip(): string { + return $this->tooltip; + } + + /** + * @param string $tooltip + * @return self + */ + public function setTooltip(string $tooltip) { + $this->tooltip = $tooltip; + return $this; + } + + /** + * Serialize into JSON for client-side JS + */ + public function jsonSerialize(): array { + $result = [ + 'value' => $this->getText(), + 'flags' => $this->getFlags(), + 'type' => $this->getType(), + 'tooltip' => $this->getTooltip(), + ]; + $defaultValue = $this->getDefaultValue(); + if ($defaultValue) { + $result['defaultValue'] = $defaultValue; + } + return $result; + } + + public function isOptional(): bool { + return $this->isFlagSet(self::FLAG_OPTIONAL) || $this->isFlagSet(self::FLAG_USER_PROVIDED); + } + + /** + * Validate a parameter value against this + * Convert type as necessary + * + * @param mixed $value Value to check + * @return bool success + */ + public function validateValue(&$value): bool { + switch ($this->getType()) { + case self::VALUE_BOOLEAN: + if (!is_bool($value)) { + switch ($value) { + case 'true': + $value = true; + break; + case 'false': + $value = false; + break; + default: + return false; + } + } + break; + default: + if (!$value && !$this->isOptional()) { + return false; + } + break; + } + return true; + } +} diff --git a/apps/files_external/lib/Lib/DependencyTrait.php b/apps/files_external/lib/Lib/DependencyTrait.php new file mode 100644 index 00000000000..644132b82bc --- /dev/null +++ b/apps/files_external/lib/Lib/DependencyTrait.php @@ -0,0 +1,23 @@ +<?php + +/** + * 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; + +/** + * Trait for objects that have dependencies for use + */ +trait DependencyTrait { + + /** + * Check if object is valid for use + * + * @return MissingDependency[] Unsatisfied dependencies + */ + public function checkDependencies() { + return []; // no dependencies by default + } +} diff --git a/apps/files_external/lib/Lib/FrontendDefinitionTrait.php b/apps/files_external/lib/Lib/FrontendDefinitionTrait.php new file mode 100644 index 00000000000..0f280d1d486 --- /dev/null +++ b/apps/files_external/lib/Lib/FrontendDefinitionTrait.php @@ -0,0 +1,107 @@ +<?php + +/** + * 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; + +/** + * Trait for objects that have a frontend representation + */ +trait FrontendDefinitionTrait { + + /** @var string human-readable mechanism name */ + private string $text = ''; + + /** @var array<string, DefinitionParameter> parameters for mechanism */ + private array $parameters = []; + + /** @var string[] custom JS */ + private array $customJs = []; + + public function getText(): string { + return $this->text; + } + + public function setText(string $text): self { + $this->text = $text; + return $this; + } + + public static function lexicalCompare(IFrontendDefinition $a, IFrontendDefinition $b): int { + return strcmp($a->getText(), $b->getText()); + } + + /** + * @return array<string, DefinitionParameter> + */ + public function getParameters(): array { + return $this->parameters; + } + + /** + * @param list<DefinitionParameter> $parameters + */ + public function addParameters(array $parameters): self { + foreach ($parameters as $parameter) { + $this->addParameter($parameter); + } + return $this; + } + + public function addParameter(DefinitionParameter $parameter): self { + $this->parameters[$parameter->getName()] = $parameter; + return $this; + } + + /** + * @return string[] + */ + public function getCustomJs(): array { + return $this->customJs; + } + + /** + * @param string $custom + * @return self + */ + public function addCustomJs(string $custom): self { + $this->customJs[] = $custom; + return $this; + } + + /** + * Serialize into JSON for client-side JS + */ + public function jsonSerializeDefinition(): array { + $configuration = []; + foreach ($this->getParameters() as $parameter) { + $configuration[$parameter->getName()] = $parameter; + } + + $data = [ + 'name' => $this->getText(), + 'configuration' => $configuration, + 'custom' => $this->getCustomJs(), + ]; + return $data; + } + + /** + * Check if parameters are satisfied in a StorageConfig + */ + public function validateStorageDefinition(StorageConfig $storage): bool { + foreach ($this->getParameters() as $name => $parameter) { + $value = $storage->getBackendOption($name); + if (!is_null($value) || !$parameter->isOptional()) { + if (!$parameter->validateValue($value)) { + return false; + } + $storage->setBackendOption($name, $value); + } + } + return true; + } +} diff --git a/apps/files_external/lib/Lib/IFrontendDefinition.php b/apps/files_external/lib/Lib/IFrontendDefinition.php new file mode 100644 index 00000000000..c8b06a1c30b --- /dev/null +++ b/apps/files_external/lib/Lib/IFrontendDefinition.php @@ -0,0 +1,43 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib; + +interface IFrontendDefinition { + + public function getText(): string; + + public function setText(string $text): self; + + /** + * @return array<string, DefinitionParameter> + */ + public function getParameters(): array; + + /** + * @param list<DefinitionParameter> $parameters + */ + public function addParameters(array $parameters): self; + + public function addParameter(DefinitionParameter $parameter): self; + + /** + * @return string[] + */ + public function getCustomJs(): array; + + public function addCustomJs(string $custom): self; + + /** + * Serialize into JSON for client-side JS + */ + public function jsonSerializeDefinition(): array; + + /** + * Check if parameters are satisfied in a StorageConfig + */ + public function validateStorageDefinition(StorageConfig $storage): bool; +} diff --git a/apps/files_external/lib/Lib/IIdentifier.php b/apps/files_external/lib/Lib/IIdentifier.php new file mode 100644 index 00000000000..0677409a3cf --- /dev/null +++ b/apps/files_external/lib/Lib/IIdentifier.php @@ -0,0 +1,14 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib; + +interface IIdentifier { + + public function getIdentifier(): string; + + public function setIdentifier(string $identifier): self; +} diff --git a/apps/files_external/lib/Lib/IdentifierTrait.php b/apps/files_external/lib/Lib/IdentifierTrait.php new file mode 100644 index 00000000000..f5ffde32307 --- /dev/null +++ b/apps/files_external/lib/Lib/IdentifierTrait.php @@ -0,0 +1,63 @@ +<?php + +/** + * 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; + +/** + * Trait for objects requiring an identifier (and/or identifier aliases) + * Also supports deprecation to a different object, linking the objects + */ +trait IdentifierTrait { + + protected string $identifier = ''; + + /** @var string[] */ + protected array $identifierAliases = []; + protected ?IIdentifier $deprecateTo = null; + + public function getIdentifier(): string { + return $this->identifier; + } + + public function setIdentifier(string $identifier): self { + $this->identifier = $identifier; + $this->identifierAliases[] = $identifier; + return $this; + } + + /** + * @return string[] + */ + public function getIdentifierAliases(): array { + return $this->identifierAliases; + } + + public function addIdentifierAlias(string $alias): self { + $this->identifierAliases[] = $alias; + return $this; + } + + public function getDeprecateTo(): ?IIdentifier { + return $this->deprecateTo; + } + + public function deprecateTo(IIdentifier $destinationObject): self { + $this->deprecateTo = $destinationObject; + return $this; + } + + public function jsonSerializeIdentifier(): array { + $data = [ + 'identifier' => $this->identifier, + 'identifierAliases' => $this->identifierAliases, + ]; + if ($this->deprecateTo) { + $data['deprecateTo'] = $this->deprecateTo->getIdentifier(); + } + return $data; + } +} diff --git a/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php b/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php new file mode 100644 index 00000000000..1e872b35072 --- /dev/null +++ b/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php @@ -0,0 +1,27 @@ +<?php + +/** + * 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; + +/** + * Authentication mechanism or backend has insufficient data + */ +class InsufficientDataForMeaningfulAnswerException extends StorageNotAvailableException { + /** + * StorageNotAvailableException constructor. + * + * @param string $message + * @param int $code + * @param \Exception|null $previous + * @since 6.0.0 + */ + public function __construct($message = '', $code = self::STATUS_INDETERMINATE, ?\Exception $previous = null) { + parent::__construct($message, $code, $previous); + } +} diff --git a/apps/files_external/lib/legacydependencycheckpolyfill.php b/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php index 7d6c0c4b45b..f6311fae83e 100644 --- a/apps/files_external/lib/legacydependencycheckpolyfill.php +++ b/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php @@ -1,27 +1,13 @@ <?php + /** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCA\Files_External\Lib\MissingDependency; +use OCP\Files\Storage\IStorage; /** * Polyfill for checking dependencies using legacy Storage::checkDependencies() @@ -29,7 +15,7 @@ use \OCA\Files_External\Lib\MissingDependency; trait LegacyDependencyCheckPolyfill { /** - * @return string + * @return class-string<IStorage> */ abstract public function getStorageClass(); @@ -56,7 +42,7 @@ trait LegacyDependencyCheckPolyfill { $module = $key; $message = $value; } - $value = new MissingDependency($module, $this); + $value = new MissingDependency($module); $value->setMessage($message); } $ret[] = $value; @@ -65,6 +51,4 @@ trait LegacyDependencyCheckPolyfill { return $ret; } - } - diff --git a/apps/files_external/lib/Lib/MissingDependency.php b/apps/files_external/lib/Lib/MissingDependency.php new file mode 100644 index 00000000000..c2da7fcadbf --- /dev/null +++ b/apps/files_external/lib/Lib/MissingDependency.php @@ -0,0 +1,51 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib; + +/** + * External storage backend dependency + */ +class MissingDependency { + + /** @var string|null Custom message */ + private ?string $message = null; + private bool $optional = false; + + /** + * @param string $dependency + */ + public function __construct( + private readonly string $dependency, + ) { + } + + public function getDependency(): string { + return $this->dependency; + } + + public function getMessage(): ?string { + return $this->message; + } + + /** + * @param string $message + * @return self + */ + public function setMessage($message) { + $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 new file mode 100644 index 00000000000..2812df6ad6a --- /dev/null +++ b/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php @@ -0,0 +1,130 @@ +<?php + +/** + * 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; +use OC\Files\Notify\RenameChange; +use OCP\Files\Notify\IChange; +use OCP\Files\Notify\INotifyHandler; + +class SMBNotifyHandler implements INotifyHandler { + /** + * @var string + */ + private $root; + + private $oldRenamePath = null; + + /** + * SMBNotifyHandler constructor. + * + * @param \Icewind\SMB\INotifyHandler $shareNotifyHandler + * @param string $root + */ + public function __construct( + private \Icewind\SMB\INotifyHandler $shareNotifyHandler, + $root, + ) { + $this->root = str_replace('\\', '/', $root); + } + + private function relativePath($fullPath) { + if ($fullPath === $this->root) { + return ''; + } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) { + return substr($fullPath, strlen($this->root)); + } else { + return null; + } + } + + public function listen(callable $callback) { + $oldRenamePath = null; + $this->shareNotifyHandler->listen(function (\Icewind\SMB\Change $shareChange) use ($callback) { + $change = $this->mapChange($shareChange); + if (!is_null($change)) { + return $callback($change); + } else { + return true; + } + }); + } + + /** + * Get all changes detected since the start of the notify process or the last call to getChanges + * + * @return IChange[] + */ + public function getChanges() { + $shareChanges = $this->shareNotifyHandler->getChanges(); + $changes = []; + foreach ($shareChanges as $shareChange) { + $change = $this->mapChange($shareChange); + if ($change) { + $changes[] = $change; + } + } + return $changes; + } + + /** + * Stop listening for changes + * + * Note that any pending changes will be discarded + */ + public function stop() { + $this->shareNotifyHandler->stop(); + } + + /** + * @param \Icewind\SMB\Change $change + * @return IChange|null + */ + private function mapChange(\Icewind\SMB\Change $change) { + $path = $this->relativePath($change->getPath()); + if (is_null($path)) { + return null; + } + if ($change->getCode() === \Icewind\SMB\INotifyHandler::NOTIFY_RENAMED_OLD) { + $this->oldRenamePath = $path; + return null; + } + $type = $this->mapNotifyType($change->getCode()); + if (is_null($type)) { + return null; + } + if ($type === IChange::RENAMED) { + if (!is_null($this->oldRenamePath)) { + $result = new RenameChange($type, $this->oldRenamePath, $path); + $this->oldRenamePath = null; + } else { + $result = null; + } + } else { + $result = new Change($type, $path); + } + return $result; + } + + private function mapNotifyType($smbType) { + switch ($smbType) { + case \Icewind\SMB\INotifyHandler::NOTIFY_ADDED: + return IChange::ADDED; + case \Icewind\SMB\INotifyHandler::NOTIFY_REMOVED: + return IChange::REMOVED; + case \Icewind\SMB\INotifyHandler::NOTIFY_MODIFIED: + case \Icewind\SMB\INotifyHandler::NOTIFY_ADDED_STREAM: + case \Icewind\SMB\INotifyHandler::NOTIFY_MODIFIED_STREAM: + case \Icewind\SMB\INotifyHandler::NOTIFY_REMOVED_STREAM: + return IChange::MODIFIED; + case \Icewind\SMB\INotifyHandler::NOTIFY_RENAMED_NEW: + return IChange::RENAMED; + default: + return null; + } + } +} diff --git a/apps/files_external/lib/Lib/PersonalMount.php b/apps/files_external/lib/Lib/PersonalMount.php new file mode 100644 index 00000000000..d9dbddd1449 --- /dev/null +++ b/apps/files_external/lib/Lib/PersonalMount.php @@ -0,0 +1,70 @@ +<?php + +/** + * 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 { + /** + * @param UserStoragesService $storagesService + * @param int $storageId + * @param IStorage $storage + * @param string $mountpoint + * @param array $arguments (optional) configuration for the storage backend + * @param IStorageFactory $loader + * @param array $mountOptions mount specific options + * @param int $externalStorageId + */ + public function __construct( + protected UserStoragesService $storagesService, + StorageConfig $storageConfig, + /** @var int id of the external storage (mount) (not the numeric id of the resulting storage!) */ + protected $numericExternalStorageId, + $storage, + $mountpoint, + $arguments = null, + $loader = null, + $mountOptions = null, + $mountId = null, + ) { + parent::__construct($storageConfig, $storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId); + } + + /** + * Move the mount point to $target + * + * @param string $target the target mount point + * @return bool + */ + public function moveMount($target) { + $storage = $this->storagesService->getStorage($this->numericExternalStorageId); + // remove "/$user/files" prefix + $targetParts = explode('/', trim($target, '/'), 3); + $storage->setMountPoint($targetParts[2]); + $this->storagesService->updateStorage($storage); + $this->setMountPoint($target); + return true; + } + + /** + * Remove the mount points + * + * @return bool + */ + public function removeMount() { + $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 new file mode 100644 index 00000000000..fad2c07e58c --- /dev/null +++ b/apps/files_external/lib/Lib/PriorityTrait.php @@ -0,0 +1,35 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Lib; + +use OCA\Files_External\Service\BackendService; + +/** + * Trait to implement priority mechanics for a configuration class + */ +trait PriorityTrait { + + /** @var int initial priority */ + protected $priority = BackendService::PRIORITY_DEFAULT; + + /** + * @return int + */ + public function getPriority() { + return $this->priority; + } + + /** + * @param int $priority + * @return self + */ + public function setPriority($priority) { + $this->priority = $priority; + return $this; + } +} diff --git a/apps/files_external/lib/Lib/SessionStorageWrapper.php b/apps/files_external/lib/Lib/SessionStorageWrapper.php new file mode 100644 index 00000000000..8754041b2fa --- /dev/null +++ b/apps/files_external/lib/Lib/SessionStorageWrapper.php @@ -0,0 +1,25 @@ +<?php + +/** + * 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; +use OCP\Constants; + +/** + * Wrap Storage in PermissionsMask for session ephemeral use + */ +class SessionStorageWrapper extends PermissionsMask { + /** + * @param array $parameters ['storage' => $storage] + */ + public function __construct(array $parameters) { + // disable sharing permission + $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 new file mode 100644 index 00000000000..5dc9e114532 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/AmazonS3.php @@ -0,0 +1,760 @@ +<?php + +/** + * 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\S3\Exception\S3Exception; +use Icewind\Streams\CallbackWrapper; +use Icewind\Streams\CountWrapper; +use Icewind\Streams\IteratorDirectory; +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; +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; + + private LoggerInterface $logger; + + public function needsPartFile(): bool { + return false; + } + + /** @var CappedMemoryCache<array|false> */ + private CappedMemoryCache $objectCache; + + /** @var CappedMemoryCache<bool> */ + private CappedMemoryCache $directoryCache; + + /** @var CappedMemoryCache<array> */ + private CappedMemoryCache $filesCache; + + private IMimeTypeDetector $mimeDetector; + private ?bool $versioningEnabled = null; + private ICache $memCache; + + public function __construct(array $parameters) { + parent::__construct($parameters); + $this->parseParams($parameters); + $this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']); + $this->objectCache = new CappedMemoryCache(); + $this->directoryCache = new CappedMemoryCache(); + $this->filesCache = new CappedMemoryCache(); + $this->mimeDetector = Server::get(IMimeTypeDetector::class); + /** @var ICacheFactory $cacheFactory */ + $cacheFactory = Server::get(ICacheFactory::class); + $this->memCache = $cacheFactory->createLocal('s3-external'); + $this->logger = Server::get(LoggerInterface::class); + } + + private function normalizePath(string $path): string { + $path = trim($path, '/'); + + if (!$path) { + $path = '.'; + } + + return $path; + } + + private function isRoot(string $path): bool { + return $path === '.'; + } + + private function cleanKey(string $path): string { + if ($this->isRoot($path)) { + return '/'; + } + return $path; + } + + private function clearCache(): void { + $this->objectCache = new CappedMemoryCache(); + $this->directoryCache = new CappedMemoryCache(); + $this->filesCache = new CappedMemoryCache(); + } + + private function invalidateCache(string $key): void { + unset($this->objectCache[$key]); + $keys = array_keys($this->objectCache->getData()); + $keyLength = strlen($key); + foreach ($keys as $existingKey) { + if (substr($existingKey, 0, $keyLength) === $key) { + unset($this->objectCache[$existingKey]); + } + } + unset($this->filesCache[$key]); + $keys = array_keys($this->directoryCache->getData()); + $keyLength = strlen($key); + foreach ($keys as $existingKey) { + if (substr($existingKey, 0, $keyLength) === $key) { + unset($this->directoryCache[$existingKey]); + } + } + unset($this->directoryCache[$key]); + } + + 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; + } + $this->objectCache[$key] = false; + } + } + + 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]; + } + + /** + * Return true if directory exists + * + * There are no folders in s3. A folder like structure could be archived + * by prefixing files with the folder name. + * + * Implementation from flysystem-aws-s3-v3: + * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694 + * + * @throws \Exception + */ + 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. + $result = $this->getConnection()->listObjectsV2([ + 'Bucket' => $this->bucket, + 'Prefix' => $path, + 'MaxKeys' => 1, + ]); + + if (isset($result['Contents'])) { + $this->directoryCache[$path] = true; + return true; + } + + // empty directories have their own object + $object = $this->headObject($path); + + if ($object) { + $this->directoryCache[$path] = true; + return true; + } + } catch (S3Exception $e) { + if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) { + $this->directoryCache[$path] = false; + } + throw $e; + } + + + $this->directoryCache[$path] = false; + return false; + } + + protected function remove(string $path): bool { + // remember fileType to reduce http calls + $fileType = $this->filetype($path); + if ($fileType === 'dir') { + return $this->rmdir($path); + } elseif ($fileType === 'file') { + return $this->unlink($path); + } else { + return false; + } + } + + public function mkdir(string $path): bool { + $path = $this->normalizePath($path); + + if ($this->is_dir($path)) { + return false; + } + + try { + $this->getConnection()->putObject([ + 'Bucket' => $this->bucket, + 'Key' => $path . '/', + 'Body' => '', + 'ContentType' => FileInfo::MIMETYPE_FOLDER + ] + $this->getSSECParameters()); + $this->testTimeout(); + } catch (S3Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + + $this->invalidateCache($path); + + return true; + } + + public function file_exists(string $path): bool { + return $this->filetype($path) !== false; + } + + + public function rmdir(string $path): bool { + $path = $this->normalizePath($path); + + if ($this->isRoot($path)) { + return $this->clearBucket(); + } + + if (!$this->file_exists($path)) { + return false; + } + + $this->invalidateCache($path); + return $this->batchDelete($path); + } + + protected function clearBucket(): bool { + $this->clearCache(); + return $this->batchDelete(); + } + + private function batchDelete(?string $path = null): bool { + // TODO explore using https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.S3.BatchDelete.html + $params = [ + 'Bucket' => $this->bucket + ]; + if ($path !== null) { + $params['Prefix'] = $path . '/'; + } + try { + $connection = $this->getConnection(); + // Since there are no real directories on S3, we need + // to delete all objects prefixed with the path. + do { + // instead of the iterator, manually loop over the list ... + $objects = $connection->listObjects($params); + // ... so we can delete the files in batches + if (isset($objects['Contents'])) { + $connection->deleteObjects([ + 'Bucket' => $this->bucket, + 'Delete' => [ + 'Objects' => $objects['Contents'] + ] + ]); + $this->testTimeout(); + } + // we reached the end when the list is no longer truncated + } while ($objects['IsTruncated']); + if ($path !== '' && $path !== null) { + $this->deleteObject($path); + } + } catch (S3Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + return true; + } + + public function opendir(string $path) { + try { + $content = iterator_to_array($this->getDirectoryContent($path)); + return IteratorDirectory::wrap(array_map(function (array $item) { + return $item['name']; + }, $content)); + } catch (S3Exception $e) { + return false; + } + } + + public function stat(string $path): array|false { + $path = $this->normalizePath($path); + + if ($this->is_dir($path)) { + $stat = $this->getDirectoryMetaData($path); + } else { + $object = $this->headObject($path); + if ($object === false) { + return false; + } + $stat = $this->objectToMetaData($object); + } + $stat['atime'] = time(); + + return $stat; + } + + /** + * Return content length for object + * + * When the information is already present (e.g. opendir has been called before) + * this value is return. Otherwise a headObject is emitted. + */ + private function getContentLength(string $path): int { + if (isset($this->filesCache[$path])) { + return (int)$this->filesCache[$path]['ContentLength']; + } + + $result = $this->headObject($path); + if (isset($result['ContentLength'])) { + return (int)$result['ContentLength']; + } + + return 0; + } + + /** + * Return last modified for object + * + * When the information is already present (e.g. opendir has been called before) + * this value is return. Otherwise a headObject is emitted. + */ + private function getLastModified(string $path): string { + if (isset($this->filesCache[$path])) { + return $this->filesCache[$path]['LastModified']; + } + + $result = $this->headObject($path); + if (isset($result['LastModified'])) { + return $result['LastModified']; + } + + return 'now'; + } + + public function is_dir(string $path): bool { + $path = $this->normalizePath($path); + + if (isset($this->filesCache[$path])) { + return false; + } + + try { + return $this->doesDirectoryExist($path); + } catch (S3Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + } + + public function filetype(string $path): string|false { + $path = $this->normalizePath($path); + + if ($this->isRoot($path)) { + return 'dir'; + } + + try { + if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) { + return 'dir'; + } + if (isset($this->filesCache[$path]) || $this->headObject($path)) { + return 'file'; + } + if ($this->doesDirectoryExist($path)) { + return 'dir'; + } + } catch (S3Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + + return false; + } + + public function getPermissions(string $path): int { + $type = $this->filetype($path); + if (!$type) { + return 0; + } + return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; + } + + public function unlink(string $path): bool { + $path = $this->normalizePath($path); + + if ($this->is_dir($path)) { + return $this->rmdir($path); + } + + try { + $this->deleteObject($path); + $this->invalidateCache($path); + } catch (S3Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + + return true; + } + + public function fopen(string $path, string $mode) { + $path = $this->normalizePath($path); + + switch ($mode) { + case 'r': + case 'rb': + // Don't try to fetch empty files + $stat = $this->stat($path); + if (is_array($stat) && isset($stat['size']) && $stat['size'] === 0) { + return fopen('php://memory', $mode); + } + + try { + return $this->readObject($path); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + case 'w': + case 'wb': + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile(); + + $handle = fopen($tmpFile, 'w'); + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void { + $this->writeBack($tmpFile, $path); + }); + case 'a': + case 'ab': + case 'r+': + case 'w+': + case 'wb+': + case 'a+': + case 'x': + case 'x+': + case 'c': + case 'c+': + if (strrpos($path, '.') !== false) { + $ext = substr($path, strrpos($path, '.')); + } else { + $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): void { + $this->writeBack($tmpFile, $path); + }); + } + return false; + } + + public function touch(string $path, ?int $mtime = null): bool { + if (is_null($mtime)) { + $mtime = time(); + } + $metadata = [ + 'lastmodified' => gmdate(\DateTime::RFC1123, $mtime) + ]; + + try { + 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) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + + $this->invalidateCache($path); + return true; + } + + public function copy(string $source, string $target, ?bool $isFile = null): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); + + if ($isFile === true || $this->is_file($source)) { + try { + $this->copyObject($source, $target, [ + 'StorageClass' => $this->storageClass, + ]); + $this->testTimeout(); + } catch (S3Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + } else { + $this->remove($target); + + try { + $this->mkdir($target); + $this->testTimeout(); + } catch (S3Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + + 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($target); + + return true; + } + + public function rename(string $source, string $target): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); + + if ($this->is_file($source)) { + if ($this->copy($source, $target) === false) { + return false; + } + + if ($this->unlink($source) === false) { + $this->unlink($target); + return false; + } + } else { + if ($this->copy($source, $target) === false) { + return false; + } + + if ($this->rmdir($source) === false) { + $this->rmdir($target); + return false; + } + } + + return true; + } + + public function test(): bool { + $this->getConnection()->headBucket([ + 'Bucket' => $this->bucket + ]); + return true; + } + + public function getId(): string { + return $this->id; + } + + public function writeBack(string $tmpFile, string $path): bool { + try { + $source = fopen($tmpFile, 'r'); + $this->writeObject($path, $source, $this->mimeDetector->detectPath($path)); + $this->invalidateCache($path); + + unlink($tmpFile); + return true; + } catch (S3Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return false; + } + } + + /** + * check if curl is installed + */ + 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 new file mode 100644 index 00000000000..944964de7a6 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/FTP.php @@ -0,0 +1,364 @@ +<?php + +/** + * 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 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 Common { + use CopyDirectory; + + private $root; + private $host; + private $password; + private $username; + private $secure; + private $port; + private $utf8Mode; + + /** @var FtpConnection|null */ + private $connection; + + 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($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 ($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 { + return ['ftp']; + } + } + + 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; + } + } + + public function filesize(string $path): false|int|float { + $result = $this->getConnection()->size($this->buildPath($path)); + if ($result === -1) { + return false; + } else { + return $result; + } + } + + public function rmdir(string $path): bool { + if ($this->is_dir($path)) { + $result = $this->getConnection()->rmdir($this->buildPath($path)); + // recursive rmdir support depends on the ftp server + if ($result) { + return $result; + } else { + return $this->recursiveRmDir($path); + } + } elseif ($this->is_file($path)) { + return $this->unlink($path); + } else { + return false; + } + } + + 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 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': + case 'r+': + case 'a+': + case 'x': + case 'x+': + case 'c': + case 'c+': + //emulate these + if ($useExisting and $this->file_exists($path)) { + if (!$this->isUpdatable($path)) { + return false; + } + $tmpFile = $this->getCachedFile($path); + } else { + if (!$this->isCreatable(dirname($path))) { + return false; + } + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile(); + } + $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 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); + + if (!$result) { + fclose($stream); + return false; + } + return $stream; + } + + public function touch(string $path, ?int $mtime = null): bool { + if ($this->file_exists($path)) { + return false; + } else { + $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 new file mode 100644 index 00000000000..12c305de750 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/OwnCloud.php @@ -0,0 +1,63 @@ +<?php + +/** + * 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; + +/** + * Nextcloud backend for external storage based on DAV backend. + * + * The Nextcloud URL consists of three parts: + * http://%host/%context/remote.php/webdav/%root + * + */ +class OwnCloud extends DAV implements IDisableEncryptionStorage { + public const OC_URL_SUFFIX = 'remote.php/webdav'; + + public function __construct(array $parameters) { + // extract context path from host if specified + // (owncloud install path on host) + $host = $parameters['host']; + // strip protocol + if (substr($host, 0, 8) === 'https://') { + $host = substr($host, 8); + $parameters['secure'] = true; + } elseif (substr($host, 0, 7) === 'http://') { + $host = substr($host, 7); + $parameters['secure'] = false; + } + $contextPath = ''; + $hostSlashPos = strpos($host, '/'); + if ($hostSlashPos !== false) { + $contextPath = substr($host, $hostSlashPos); + $host = substr($host, 0, $hostSlashPos); + } + + if (!str_ends_with($contextPath, '/')) { + $contextPath .= '/'; + } + + if (isset($parameters['root'])) { + $root = '/' . ltrim($parameters['root'], '/'); + } else { + $root = '/'; + } + + $parameters['host'] = $host; + $parameters['root'] = $contextPath . self::OC_URL_SUFFIX . $root; + $parameters['authType'] = Client::AUTH_BASIC; + + parent::__construct($parameters); + } + + 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 new file mode 100644 index 00000000000..a2f5bafcca1 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/SFTP.php @@ -0,0 +1,523 @@ +<?php + +/** + * 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 Common { + private $host; + private $user; + private $root; + private $port = 22; + + private $auth = []; + + /** + * @var \phpseclib\Net\SFTP + */ + protected $client; + private CappedMemoryCache $knownMTimes; + + private IMimeTypeDetector $mimeTypeDetector; + + public const COPY_CHUNK_SIZE = 8 * 1024 * 1024; + + /** + * @param string $host protocol://server:port + * @return array [$server, $port] + */ + private function splitHost(string $host): array { + $input = $host; + if (!str_contains($host, '://')) { + // add a protocol to fix parse_url behavior with ipv6 + $host = 'http://' . $host; + } + + $parsed = parse_url($host); + if (is_array($parsed) && isset($parsed['port'])) { + return [$parsed['host'], $parsed['port']]; + } elseif (is_array($parsed)) { + return [$parsed['host'], 22]; + } else { + return [$input, 22]; + } + } + + public function __construct(array $parameters) { + // Register sftp:// + Stream::register(); + + $parsedHost = $this->splitHost($parameters['host']); + + $this->host = $parsedHost[0]; + $this->port = $parsedHost[1]; + + if (!isset($parameters['user'])) { + throw new \UnexpectedValueException('no authentication parameters specified'); + } + $this->user = $parameters['user']; + + if (isset($parameters['public_key_auth'])) { + $this->auth[] = $parameters['public_key_auth']; + } + if (isset($parameters['password']) && $parameters['password'] !== '') { + $this->auth[] = $parameters['password']; + } + + if ($this->auth === []) { + throw new \UnexpectedValueException('no authentication parameters specified'); + } + + $this->root + = isset($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); + } + + /** + * Returns the connection. + * + * @return \phpseclib\Net\SFTP connected client instance + * @throws \Exception when the connection failed + */ + public function getConnection(): \phpseclib\Net\SFTP { + if (!is_null($this->client)) { + return $this->client; + } + + $hostKeys = $this->readHostKeys(); + $this->client = new \phpseclib\Net\SFTP($this->host, $this->port); + + // The SSH Host Key MUST be verified before login(). + $currentHostKey = $this->client->getServerPublicHostKey(); + if (array_key_exists($this->host, $hostKeys)) { + if ($hostKeys[$this->host] !== $currentHostKey) { + throw new \Exception('Host public key does not match known key'); + } + } else { + $hostKeys[$this->host] = $currentHostKey; + $this->writeHostKeys($hostKeys); + } + + $login = false; + foreach ($this->auth as $auth) { + /** @psalm-suppress TooManyArguments */ + $login = $this->client->login($this->user, $auth); + if ($login === true) { + break; + } + } + + if ($login === false) { + throw new \Exception('Login failed'); + } + return $this->client; + } + + public function test(): bool { + if ( + !isset($this->host) + || !isset($this->user) + ) { + return false; + } + return $this->getConnection()->nlist() !== false; + } + + public function getId(): string { + $id = 'sftp::' . $this->user . '@' . $this->host; + if ($this->port !== 22) { + $id .= ':' . $this->port; + } + // note: this will double the root slash, + // we should not change it to keep compatible with + // old storage ids + $id .= '/' . $this->root; + return $id; + } + + public function getHost(): string { + return $this->host; + } + + public function getRoot(): string { + return $this->root; + } + + public function getUser(): string { + return $this->user; + } + + private function absPath(string $path): string { + return $this->root . $this->cleanPath($path); + } + + private function hostKeysPath(): string|false { + try { + $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; + } + + protected function writeHostKeys(array $keys): bool { + try { + $keyPath = $this->hostKeysPath(); + if ($keyPath && file_exists($keyPath)) { + $fp = fopen($keyPath, 'w'); + foreach ($keys as $host => $key) { + fwrite($fp, $host . '::' . $key . "\n"); + } + fclose($fp); + return true; + } + } catch (\Exception $e) { + } + return false; + } + + protected function readHostKeys(): array { + try { + $keyPath = $this->hostKeysPath(); + if (file_exists($keyPath)) { + $hosts = []; + $keys = []; + $lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if ($lines) { + foreach ($lines as $line) { + $hostKeyArray = explode('::', $line, 2); + if (count($hostKeyArray) === 2) { + $hosts[] = $hostKeyArray[0]; + $keys[] = $hostKeyArray[1]; + } + } + return array_combine($hosts, $keys); + } + } + } catch (\Exception $e) { + } + return []; + } + + public function mkdir(string $path): bool { + try { + return $this->getConnection()->mkdir($this->absPath($path)); + } catch (\Exception $e) { + return false; + } + } + + public function rmdir(string $path): bool { + try { + $result = $this->getConnection()->delete($this->absPath($path), true); + // workaround: stray stat cache entry when deleting empty folders + // see https://github.com/phpseclib/phpseclib/issues/706 + $this->getConnection()->clearStatCache(); + return $result; + } catch (\Exception $e) { + return false; + } + } + + public function opendir(string $path) { + try { + $list = $this->getConnection()->nlist($this->absPath($path)); + if ($list === false) { + return false; + } + + $id = md5('sftp:' . $path); + $dirStream = []; + foreach ($list as $file) { + if ($file !== '.' && $file !== '..') { + $dirStream[] = $file; + } + } + return IteratorDirectory::wrap($dirStream); + } catch (\Exception $e) { + return false; + } + } + + public function filetype(string $path): string|false { + try { + $stat = $this->getConnection()->stat($this->absPath($path)); + 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) { + return 'dir'; + } + } catch (\Exception $e) { + } + return false; + } + + public function file_exists(string $path): bool { + try { + return $this->getConnection()->stat($this->absPath($path)) !== false; + } catch (\Exception $e) { + return false; + } + } + + public function unlink(string $path): bool { + try { + return $this->getConnection()->delete($this->absPath($path), true); + } catch (\Exception $e) { + return false; + } + } + + 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': + $stat = $this->stat($path); + if (!$stat) { + return false; + } + SFTPReadStream::register(); + $context = stream_context_create(['sftp' => ['session' => $connection, 'size' => $stat['size']]]); + $handle = fopen('sftpread://' . trim($absPath, '/'), 'r', false, $context); + return RetryWrapper::wrap($handle); + case 'w': + case 'wb': + SFTPWriteStream::register(); + // the SFTPWriteStream doesn't go through the "normal" methods so it doesn't clear the stat cache. + $connection->_remove_from_stat_cache($absPath); + $context = stream_context_create(['sftp' => ['session' => $connection]]); + $fh = fopen('sftpwrite://' . trim($absPath, '/'), 'w', false, $context); + if ($fh) { + $fh = CallbackWrapper::wrap($fh, null, null, function () use ($path): void { + $this->knownMTimes->set($path, time()); + }); + } + return $fh; + case 'a': + case 'ab': + case 'r+': + case 'w+': + case 'wb+': + case 'a+': + case 'x': + case 'x+': + case 'c': + case 'c+': + $context = stream_context_create(['sftp' => ['session' => $connection]]); + $handle = fopen($this->constructUrl($path), $mode, false, $context); + return RetryWrapper::wrap($handle); + } + } catch (\Exception $e) { + } + return false; + } + + public function touch(string $path, ?int $mtime = null): bool { + try { + if (!is_null($mtime)) { + return false; + } + if (!$this->file_exists($path)) { + return $this->getConnection()->put($this->absPath($path), ''); + } else { + return false; + } + } catch (\Exception $e) { + return false; + } + } + + /** + * @throws \Exception + */ + public function getFile(string $path, string $target): void { + $this->getConnection()->get($path, $target); + } + + public function rename(string $source, string $target): bool { + try { + if ($this->file_exists($target)) { + $this->unlink($target); + } + return $this->getConnection()->rename( + $this->absPath($source), + $this->absPath($target) + ); + } catch (\Exception $e) { + return false; + } + } + + /** + * @return array{mtime: int, size: int, ctime: int}|false + */ + public function stat(string $path): array|false { + try { + $path = $this->cleanPath($path); + $stat = $this->getConnection()->stat($this->absPath($path)); + + $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 + ]; + } catch (\Exception $e) { + return false; + } + } + + public function constructUrl(string $path): string { + // Do not pass the password here. We want to use the Net_SFTP object + // supplied via stream context or fail. We only supply username and + // hostname because this might show up in logs (they are not used). + $url = 'sftp://' . urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path; + return $url; + } + + public function file_put_contents(string $path, mixed $data): int|float|false { + /** @psalm-suppress InternalMethod */ + $result = $this->getConnection()->put($this->absPath($path), $data); + if ($result) { + return strlen($data); + } else { + return false; + } + } + + public function writeStream(string $path, $stream, ?int $size = null): int { + if ($size === null) { + $stream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size): void { + $size = $writtenSize; + }); + if (!$stream) { + throw new \Exception('Failed to wrap stream'); + } + } + /** @psalm-suppress InternalMethod */ + $result = $this->getConnection()->put($this->absPath($path), $stream); + fclose($stream); + if ($result) { + if ($size === null) { + throw new \Exception('Failed to get written size from sftp storage wrapper'); + } + return $size; + } else { + throw new \Exception('Failed to write steam to sftp storage'); + } + } + + public function copy(string $source, string $target): bool { + if ($this->is_dir($source) || $this->is_dir($target)) { + return parent::copy($source, $target); + } else { + $absSource = $this->absPath($source); + $absTarget = $this->absPath($target); + + $connection = $this->getConnection(); + $size = $connection->size($absSource); + if ($size === false) { + return false; + } + for ($i = 0; $i < $size; $i += self::COPY_CHUNK_SIZE) { + /** @psalm-suppress InvalidArgument */ + $chunk = $connection->get($absSource, false, $i, self::COPY_CHUNK_SIZE); + if ($chunk === false) { + return false; + } + /** @psalm-suppress InternalMethod */ + if (!$connection->put($absTarget, $chunk, \phpseclib\Net\SFTP::SOURCE_STRING, $i)) { + return false; + } + } + return true; + } + } + + public function getPermissions(string $path): int { + $stat = $this->getConnection()->stat($this->absPath($path)); + if (!$stat) { + return 0; + } + if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + return Constants::PERMISSION_ALL; + } else { + return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; + } + } + + public function getMetaData(string $path): ?array { + $stat = $this->getConnection()->stat($this->absPath($path)); + if (!$stat) { + return null; + } + + if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + $stat['permissions'] = Constants::PERMISSION_ALL; + } else { + $stat['permissions'] = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; + } + + if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + $stat['size'] = -1; + $stat['mimetype'] = FileInfo::MIMETYPE_FOLDER; + } else { + $stat['mimetype'] = $this->mimeTypeDetector->detectPath($path); + } + + $stat['etag'] = $this->getETag($path); + $stat['storage_mtime'] = $stat['mtime']; + $stat['name'] = basename($path); + + $keys = ['size', 'mtime', 'mimetype', 'etag', 'storage_mtime', 'permissions', 'name']; + return array_intersect_key($stat, array_flip($keys)); + } +} diff --git a/apps/files_external/lib/Lib/Storage/SFTPReadStream.php b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php new file mode 100644 index 00000000000..7dedbd7035a --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php @@ -0,0 +1,217 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Storage; + +use Icewind\Streams\File; +use phpseclib\Net\SSH2; + +class SFTPReadStream implements File { + /** @var resource */ + public $context; + + /** @var \phpseclib\Net\SFTP */ + private $sftp; + + /** @var string */ + private $handle; + + /** @var int */ + private $internalPosition = 0; + + /** @var int */ + private $readPosition = 0; + + /** @var bool */ + private $eof = false; + + private $buffer = ''; + private bool $pendingRead = false; + private int $size = 0; + + public static function register($protocol = 'sftpread') { + if (in_array($protocol, stream_get_wrappers(), true)) { + return false; + } + return stream_wrapper_register($protocol, get_called_class()); + } + + /** + * Load the source from the stream context and return the context options + * + * @throws \BadMethodCallException + */ + protected function loadContext(string $name) { + $context = stream_context_get_options($this->context); + if (isset($context[$name])) { + $context = $context[$name]; + } else { + throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); + } + if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) { + $this->sftp = $context['session']; + } else { + throw new \BadMethodCallException('Invalid context, session not set'); + } + if (isset($context['size'])) { + $this->size = $context['size']; + } + return $context; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + [, $path] = explode('://', $path); + $path = '/' . ltrim($path); + $path = str_replace('//', '/', $path); + + $this->loadContext('sftp'); + + if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) { + return false; + } + + $remote_file = $this->sftp->_realpath($path); + if ($remote_file === false) { + return false; + } + + $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0); + if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->sftp->_get_sftp_packet(); + switch ($this->sftp->packet_type) { + case NET_SFTP_HANDLE: + $this->handle = substr($response, 4); + break; + case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + $this->sftp->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + $this->request_chunk(256 * 1024); + + return true; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + switch ($whence) { + case SEEK_SET: + $this->seekTo($offset); + break; + case SEEK_CUR: + $this->seekTo($this->readPosition + $offset); + break; + case SEEK_END: + $this->seekTo($this->size + $offset); + break; + } + return true; + } + + private function seekTo(int $offset): void { + $this->internalPosition = $offset; + $this->readPosition = $offset; + $this->buffer = ''; + $this->request_chunk(256 * 1024); + } + + public function stream_tell() { + return $this->readPosition; + } + + public function stream_read($count) { + if (!$this->eof && strlen($this->buffer) < $count) { + $chunk = $this->read_chunk(); + $this->buffer .= $chunk; + if (!$this->eof) { + $this->request_chunk(256 * 1024); + } + } + + $data = substr($this->buffer, 0, $count); + $this->buffer = substr($this->buffer, $count); + $this->readPosition += strlen($data); + + return $data; + } + + private function request_chunk(int $size) { + if ($this->pendingRead) { + $this->sftp->_get_sftp_packet(); + } + + $packet = pack('Na*N3', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size); + $this->pendingRead = true; + return $this->sftp->_send_sftp_packet(NET_SFTP_READ, $packet); + } + + private function read_chunk() { + $this->pendingRead = false; + $response = $this->sftp->_get_sftp_packet(); + + switch ($this->sftp->packet_type) { + case NET_SFTP_DATA: + $temp = substr($response, 4); + $len = strlen($temp); + $this->internalPosition += $len; + return $temp; + case NET_SFTP_STATUS: + [1 => $status] = unpack('N', substr($response, 0, 4)); + if ($status == NET_SFTP_STATUS_EOF) { + $this->eof = true; + } + return ''; + default: + return ''; + } + } + + public function stream_write($data) { + return false; + } + + public function stream_set_option($option, $arg1, $arg2) { + return false; + } + + public function stream_truncate($size) { + return false; + } + + public function stream_stat() { + return false; + } + + public function stream_lock($operation) { + return false; + } + + public function stream_flush() { + return false; + } + + public function stream_eof() { + return $this->eof; + } + + public function stream_close() { + // we still have a read request incoming that needs to be handled before we can close + if ($this->pendingRead) { + $this->sftp->_get_sftp_packet(); + } + if (!$this->sftp->_close_handle($this->handle)) { + return false; + } + return true; + } +} diff --git a/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php new file mode 100644 index 00000000000..d64e89b5462 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php @@ -0,0 +1,165 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Lib\Storage; + +use Icewind\Streams\File; +use phpseclib\Net\SSH2; + +class SFTPWriteStream implements File { + /** @var resource */ + public $context; + + /** @var \phpseclib\Net\SFTP */ + private $sftp; + + /** @var string */ + private $handle; + + /** @var int */ + private $internalPosition = 0; + + /** @var int */ + private $writePosition = 0; + + /** @var bool */ + private $eof = false; + + private $buffer = ''; + + public static function register($protocol = 'sftpwrite') { + if (in_array($protocol, stream_get_wrappers(), true)) { + return false; + } + return stream_wrapper_register($protocol, get_called_class()); + } + + /** + * Load the source from the stream context and return the context options + * + * @throws \BadMethodCallException + */ + protected function loadContext(string $name) { + $context = stream_context_get_options($this->context); + if (isset($context[$name])) { + $context = $context[$name]; + } else { + throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); + } + if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) { + $this->sftp = $context['session']; + } else { + throw new \BadMethodCallException('Invalid context, session not set'); + } + return $context; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + [, $path] = explode('://', $path); + $path = '/' . ltrim($path); + $path = str_replace('//', '/', $path); + + $this->loadContext('sftp'); + + if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) { + return false; + } + + $remote_file = $this->sftp->_realpath($path); + if ($remote_file === false) { + return false; + } + + $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_TRUNCATE, 0); + if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->sftp->_get_sftp_packet(); + switch ($this->sftp->packet_type) { + case NET_SFTP_HANDLE: + $this->handle = substr($response, 4); + break; + case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + $this->sftp->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + return true; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + return false; + } + + public function stream_tell() { + return $this->writePosition; + } + + public function stream_read($count) { + return false; + } + + public function stream_write($data) { + $written = strlen($data); + $this->writePosition += $written; + + $this->buffer .= $data; + + if (strlen($this->buffer) > 64 * 1024) { + if (!$this->stream_flush()) { + return false; + } + } + + return $written; + } + + public function stream_set_option($option, $arg1, $arg2) { + return false; + } + + public function stream_truncate($size) { + return false; + } + + public function stream_stat() { + return false; + } + + public function stream_lock($operation) { + return false; + } + + public function stream_flush() { + $size = strlen($this->buffer); + $packet = pack('Na*N3a*', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size, $this->buffer); + if (!$this->sftp->_send_sftp_packet(NET_SFTP_WRITE, $packet)) { + return false; + } + $this->internalPosition += $size; + $this->buffer = ''; + + return $this->sftp->_read_put_responses(1); + } + + public function stream_eof() { + return $this->eof; + } + + public function stream_close() { + $this->stream_flush(); + if (!$this->sftp->_close_handle($this->handle)) { + return false; + } + return true; + } +} diff --git a/apps/files_external/lib/Lib/Storage/SMB.php b/apps/files_external/lib/Lib/Storage/SMB.php new file mode 100644 index 00000000000..8f8750864e1 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/SMB.php @@ -0,0 +1,727 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\Files_External\Lib\Storage; + +use Icewind\SMB\ACL; +use Icewind\SMB\BasicAuth; +use Icewind\SMB\Exception\AlreadyExistsException; +use Icewind\SMB\Exception\ConnectException; +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\ForbiddenException; +use Icewind\SMB\Exception\InvalidArgumentException; +use Icewind\SMB\Exception\InvalidTypeException; +use Icewind\SMB\Exception\NotFoundException; +use Icewind\SMB\Exception\OutOfSpaceException; +use Icewind\SMB\Exception\TimedOutException; +use Icewind\SMB\IFileInfo; +use Icewind\SMB\Native\NativeServer; +use Icewind\SMB\Options; +use Icewind\SMB\ServerFactory; +use Icewind\SMB\Wrapped\Server; +use Icewind\Streams\CallbackWrapper; +use Icewind\Streams\IteratorDirectory; +use OC\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\ITempManager; +use Psr\Log\LoggerInterface; + +class SMB extends Common implements INotifyStorage { + /** + * @var \Icewind\SMB\IServer + */ + protected $server; + + /** + * @var \Icewind\SMB\IShare + */ + protected $share; + + /** + * @var string + */ + protected $root; + + /** @var CappedMemoryCache<IFileInfo> */ + protected CappedMemoryCache $statCache; + + /** @var LoggerInterface */ + protected $logger; + + /** @var bool */ + protected $showHidden; + + private bool $caseSensitive; + + /** @var bool */ + protected $checkAcl; + + public function __construct(array $parameters) { + if (!isset($parameters['host'])) { + throw new \Exception('Invalid configuration, no host provided'); + } + + if (isset($parameters['auth'])) { + $auth = $parameters['auth']; + } elseif (isset($parameters['user']) && isset($parameters['password']) && isset($parameters['share'])) { + [$workgroup, $user] = $this->splitUser($parameters['user']); + $auth = new BasicAuth($user, $workgroup, $parameters['password']); + } else { + throw new \Exception('Invalid configuration, no credentials provided'); + } + + if (isset($parameters['logger'])) { + if (!$parameters['logger'] instanceof LoggerInterface) { + throw new \Exception( + 'Invalid logger. Got ' + . get_class($parameters['logger']) + . ' Expected ' . LoggerInterface::class + ); + } + $this->logger = $parameters['logger']; + } else { + $this->logger = \OCP\Server::get(LoggerInterface::class); + } + + $options = new Options(); + if (isset($parameters['timeout'])) { + $timeout = (int)$parameters['timeout']; + if ($timeout > 0) { + $options->setTimeout($timeout); + } + } + $system = \OCP\Server::get(SystemBridge::class); + $serverFactory = new ServerFactory($options, $system); + $this->server = $serverFactory->createServer($parameters['host'], $auth); + $this->share = $this->server->getShare(trim($parameters['share'], '/')); + + $this->root = $parameters['root'] ?? '/'; + $this->root = '/' . ltrim($this->root, '/'); + $this->root = rtrim($this->root, '/') . '/'; + + $this->showHidden = isset($parameters['show_hidden']) && $parameters['show_hidden']; + $this->caseSensitive = (bool)($parameters['case_sensitive'] ?? true); + $this->checkAcl = isset($parameters['check_acl']) && $parameters['check_acl']; + + $this->statCache = new CappedMemoryCache(); + parent::__construct($parameters); + } + + private function splitUser(string $user): array { + if (str_contains($user, '/')) { + return explode('/', $user, 2); + } elseif (str_contains($user, '\\')) { + return explode('\\', $user); + } + + return [null, $user]; + } + + public function getId(): string { + // FIXME: double slash to keep compatible with the old storage ids, + // failure to do so will lead to creation of a new storage id and + // loss of shares from the storage + return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root; + } + + protected function buildPath(string $path): string { + return Filesystem::normalizePath($this->root . '/' . $path, true, false, true); + } + + protected function relativePath(string $fullPath): ?string { + if ($fullPath === $this->root) { + return ''; + } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) { + return substr($fullPath, strlen($this->root)); + } else { + return null; + } + } + + /** + * @throws StorageAuthException + * @throws \OCP\Files\NotFoundException + * @throws \OCP\Files\ForbiddenException + */ + protected function getFileInfo(string $path): IFileInfo { + try { + $path = $this->buildPath($path); + $cached = $this->statCache[$path] ?? null; + if ($cached instanceof IFileInfo) { + return $cached; + } else { + $stat = $this->share->stat($path); + $this->statCache[$path] = $stat; + return $stat; + } + } 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 exception is thrown when the provided password is invalid. + // Possible is also ForbiddenException with a different error code, so we check it. + if ($e->getCode() === 1) { + $this->throwUnavailable($e); + } + throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e); + } + } + + /** + * @throws StorageAuthException + */ + protected function throwUnavailable(\Exception $e): never { + $this->logger->error('Error while getting file info', ['exception' => $e]); + throw new StorageAuthException($e->getMessage(), $e); + } + + /** + * get the acl from fileinfo that is relevant for the configured user + */ + private function getACL(IFileInfo $file): ?ACL { + try { + $acls = $file->getAcls(); + } catch (Exception $e) { + $this->logger->warning('Error while getting file acls', ['exception' => $e]); + return null; + } + foreach ($acls as $user => $acl) { + [, $user] = $this->splitUser($user); // strip domain + if ($user === $this->server->getAuth()->getUsername()) { + return $acl; + } + } + + return null; + } + + /** + * @return \Generator<IFileInfo> + * @throws StorageNotAvailableException + */ + 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; + } + + foreach ($files as $file) { + try { + // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch + // so we trigger the below exceptions where applicable + $hide = $file->isHidden() && !$this->showHidden; + + if ($this->checkAcl && $acl = $this->getACL($file)) { + // if there is no explicit deny, we assume it's allowed + // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder + // additionally, it's better to have false negatives here then false positives + if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) { + $this->logger->debug('Hiding non readable entry ' . $file->getName()); + continue; + } + } + + if ($hide) { + $this->logger->debug('hiding hidden file ' . $file->getName()); + } + if (!$hide) { + yield $file; + } + } catch (ForbiddenException $e) { + $this->logger->debug($e->getMessage(), ['exception' => $e]); + } catch (NotFoundException $e) { + $this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]); + } + } + } catch (ConnectException $e) { + $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); + } + } + + protected function formatInfo(IFileInfo $info): array { + $result = [ + 'size' => $info->getSize(), + 'mtime' => $info->getMTime(), + ]; + if ($info->isDirectory()) { + $result['type'] = 'dir'; + } else { + $result['type'] = 'file'; + } + return $result; + } + + /** + * Rename the files. If the source or the target is the root, the rename won't happen. + * + * @param string $source the old name of the path + * @param string $target the new name of the path + */ + public function rename(string $source, string $target, bool $retry = true): bool { + if ($this->isRootDir($source) || $this->isRootDir($target)) { + return false; + } + if ($this->caseSensitive === false + && mb_strtolower($target) === mb_strtolower($source) + ) { + // Forbid changing case only on case-insensitive file system + return false; + } + + $absoluteSource = $this->buildPath($source); + $absoluteTarget = $this->buildPath($target); + try { + $result = $this->share->rename($absoluteSource, $absoluteTarget); + } catch (AlreadyExistsException $e) { + if ($retry) { + $this->remove($target); + $result = $this->share->rename($absoluteSource, $absoluteTarget); + } else { + $this->logger->warning($e->getMessage(), ['exception' => $e]); + return false; + } + } catch (InvalidArgumentException $e) { + if ($retry) { + $this->remove($target); + $result = $this->share->rename($absoluteSource, $absoluteTarget); + } else { + $this->logger->warning($e->getMessage(), ['exception' => $e]); + return false; + } + } catch (\Exception $e) { + $this->logger->warning($e->getMessage(), ['exception' => $e]); + return false; + } + unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]); + return $result; + } + + public function stat(string $path, bool $retry = true): array|false { + try { + $result = $this->formatInfo($this->getFileInfo($path)); + } catch (\OCP\Files\ForbiddenException $e) { + return false; + } catch (\OCP\Files\NotFoundException $e) { + return false; + } catch (TimedOutException $e) { + if ($retry) { + return $this->stat($path, false); + } else { + throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); + } + } + if ($this->remoteIsShare() && $this->isRootDir($path)) { + $result['mtime'] = $this->shareMTime(); + } + return $result; + } + + /** + * get the best guess for the modification time of the share + */ + private function shareMTime(): int { + $highestMTime = 0; + $files = $this->share->dir($this->root); + foreach ($files as $fileInfo) { + try { + if ($fileInfo->getMTime() > $highestMTime) { + $highestMTime = $fileInfo->getMTime(); + } + } catch (NotFoundException $e) { + // Ignore this, can happen on unavailable DFS shares + } catch (ForbiddenException $e) { + // Ignore this too - it's a symlink + } + } + return $highestMTime; + } + + /** + * Check if the path is our root dir (not the smb one) + */ + private function isRootDir(string $path): bool { + return $path === '' || $path === '/' || $path === '.'; + } + + /** + * Check if our root points to a smb share + */ + private function remoteIsShare(): bool { + return $this->share->getName() && (!$this->root || $this->root === '/'); + } + + public function unlink(string $path): bool { + if ($this->isRootDir($path)) { + return false; + } + + try { + if ($this->is_dir($path)) { + return $this->rmdir($path); + } else { + $path = $this->buildPath($path); + unset($this->statCache[$path]); + $this->share->del($path); + return true; + } + } catch (NotFoundException $e) { + return false; + } catch (ForbiddenException $e) { + return false; + } catch (ConnectException $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 + */ + 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 || $actualTime === 0; + } + } + + /** + * @return resource|false + */ + 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): void { + unset($this->statCache[$fullPath]); + }); + case 'a': + case 'ab': + 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, '.')); + } else { + $ext = ''; + } + if ($this->file_exists($path)) { + if (!$this->isUpdatable($path)) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file not updatable.'); + return false; + } + $tmpFile = $this->getCachedFile($path); + } else { + if (!$this->isCreatable(dirname($path))) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', parent directory not writable.'); + return false; + } + $tmpFile = \OCP\Server::get(ITempManager::class)->getTemporaryFile($ext); + } + $source = fopen($tmpFile, $mode); + $share = $this->share; + return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share): void { + unset($this->statCache[$fullPath]); + $share->put($tmpFile, $fullPath); + unlink($tmpFile); + }); + } + return false; + } catch (NotFoundException $e) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', not found.', ['exception' => $e]); + return false; + } catch (ForbiddenException $e) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', forbidden.', ['exception' => $e]); + return false; + } catch (OutOfSpaceException $e) { + $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', out of space.', ['exception' => $e]); + throw new EntityTooLargeException('not enough available space to create file', 0, $e); + } catch (ConnectException $e) { + $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(string $path): bool { + if ($this->isRootDir($path)) { + return false; + } + + try { + $this->statCache = new CappedMemoryCache(); + $content = $this->share->dir($this->buildPath($path)); + foreach ($content as $file) { + if ($file->isDirectory()) { + $this->rmdir($path . '/' . $file->getName()); + } else { + $this->share->del($file->getPath()); + } + } + $this->share->rmdir($this->buildPath($path)); + return true; + } catch (NotFoundException $e) { + return false; + } catch (ForbiddenException $e) { + return false; + } catch (ConnectException $e) { + $this->logger->error('Error while removing folder', ['exception' => $e]); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); + } + } + + public function touch(string $path, ?int $mtime = null): bool { + try { + if (!$this->file_exists($path)) { + $fh = $this->share->write($this->buildPath($path)); + fclose($fh); + return true; + } + return false; + } catch (OutOfSpaceException $e) { + throw new EntityTooLargeException('not enough available space to create file', 0, $e); + } catch (ConnectException $e) { + $this->logger->error('Error while creating file', ['exception' => $e]); + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); + } + } + + public function getMetaData(string $path): ?array { + try { + $fileInfo = $this->getFileInfo($path); + } catch (\OCP\Files\NotFoundException $e) { + return null; + } catch (\OCP\Files\ForbiddenException $e) { + return null; + } + + return $this->getMetaDataFromFileInfo($fileInfo); + } + + private function getMetaDataFromFileInfo(IFileInfo $fileInfo): array { + $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE; + + if ( + !$fileInfo->isReadOnly() || $fileInfo->isDirectory() + ) { + $permissions += Constants::PERMISSION_DELETE; + $permissions += Constants::PERMISSION_UPDATE; + if ($fileInfo->isDirectory()) { + $permissions += Constants::PERMISSION_CREATE; + } + } + + $data = []; + if ($fileInfo->isDirectory()) { + $data['mimetype'] = 'httpd/unix-directory'; + } else { + $data['mimetype'] = \OCP\Server::get(IMimeTypeDetector::class)->detectPath($fileInfo->getPath()); + } + $data['mtime'] = $fileInfo->getMTime(); + if ($fileInfo->isDirectory()) { + $data['size'] = -1; //unknown + } else { + $data['size'] = $fileInfo->getSize(); + } + $data['etag'] = $this->getETag($fileInfo->getPath()); + $data['storage_mtime'] = $data['mtime']; + $data['permissions'] = $permissions; + $data['name'] = $fileInfo->getName(); + + return $data; + } + + public function opendir(string $path) { + try { + $files = $this->getFolderContents($path); + } catch (NotFoundException $e) { + return false; + } catch (NotPermittedException $e) { + return false; + } + $names = array_map(function ($info) { + /** @var IFileInfo $info */ + return $info->getName(); + }, iterator_to_array($files)); + return IteratorDirectory::wrap($names); + } + + 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(string $path): string|false { + try { + return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file'; + } catch (\OCP\Files\NotFoundException $e) { + return false; + } catch (\OCP\Files\ForbiddenException $e) { + return false; + } + } + + public function mkdir(string $path): bool { + $path = $this->buildPath($path); + try { + $this->share->mkdir($path); + return true; + } catch (ConnectException $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(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 (\OCP\Files\NotFoundException $e) { + return false; + } catch (\OCP\Files\ForbiddenException $e) { + return false; + } catch (ConnectException $e) { + throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); + } + } + + public function isReadable(string $path): bool { + try { + $info = $this->getFileInfo($path); + return $this->showHidden || !$info->isHidden(); + } catch (\OCP\Files\NotFoundException $e) { + return false; + } catch (\OCP\Files\ForbiddenException $e) { + return false; + } + } + + 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() || $info->isDirectory()); + } catch (\OCP\Files\NotFoundException $e) { + return false; + } catch (\OCP\Files\ForbiddenException $e) { + return false; + } + } + + public function isDeletable(string $path): bool { + try { + $info = $this->getFileInfo($path); + return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly(); + } catch (\OCP\Files\NotFoundException $e) { + return false; + } catch (\OCP\Files\ForbiddenException $e) { + return false; + } + } + + /** + * check if smbclient is installed + */ + public static function checkDependencies(): array|bool { + $system = \OCP\Server::get(SystemBridge::class); + return Server::available($system) || NativeServer::available($system) ?: ['smbclient']; + } + + public function test(): bool { + try { + return parent::test(); + } catch (StorageAuthException $e) { + return false; + } catch (ForbiddenException $e) { + return false; + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + return false; + } + } + + public function listen(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()); + } else { + return $callback($change->getType(), $change->getPath()); + } + }); + } + + 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 new file mode 100644 index 00000000000..1272b9d4d8a --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/StreamWrapper.php @@ -0,0 +1,99 @@ +<?php + +/** + * 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; + +use OC\Files\Storage\Common; + +abstract class StreamWrapper extends Common { + + abstract public function constructUrl(string $path): ?string; + + public function mkdir(string $path): bool { + return mkdir($this->constructUrl($path)); + } + + public function rmdir(string $path): bool { + if ($this->is_dir($path) && $this->isDeletable($path)) { + $dh = $this->opendir($path); + if (!is_resource($dh)) { + return false; + } + while (($file = readdir($dh)) !== false) { + if ($this->is_dir($path . '/' . $file)) { + $this->rmdir($path . '/' . $file); + } else { + $this->unlink($path . '/' . $file); + } + } + $url = $this->constructUrl($path); + $success = rmdir($url); + clearstatcache(false, $url); + return $success; + } else { + return false; + } + } + + public function opendir(string $path) { + return opendir($this->constructUrl($path)); + } + + public function filetype(string $path): string|false { + return @filetype($this->constructUrl($path)); + } + + public function file_exists(string $path): bool { + return file_exists($this->constructUrl($path)); + } + + public function unlink(string $path): bool { + $url = $this->constructUrl($path); + $success = unlink($url); + // normally unlink() is supposed to do this implicitly, + // but doing it anyway just to be sure + clearstatcache(false, $url); + return $success; + } + + public function fopen(string $path, string $mode) { + return fopen($this->constructUrl($path), $mode); + } + + public function touch(string $path, ?int $mtime = null): bool { + if ($this->file_exists($path)) { + if (is_null($mtime)) { + $fh = $this->fopen($path, 'a'); + fwrite($fh, ''); + fclose($fh); + + return true; + } else { + return false; //not supported + } + } else { + $this->file_put_contents($path, ''); + return true; + } + } + + public function getFile(string $path, string $target): bool { + return copy($this->constructUrl($path), $target); + } + + public function uploadFile(string $path, string $target): bool { + return copy($path, $this->constructUrl($target)); + } + + public function rename(string $source, string $target): bool { + return rename($this->constructUrl($source), $this->constructUrl($target)); + } + + 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 new file mode 100644 index 00000000000..e80570f14ba --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/Swift.php @@ -0,0 +1,593 @@ +<?php + +declare(strict_types=1); + +/** + * 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\Files\StorageNotAvailableException; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\ITempManager; +use OCP\Server; +use OpenStack\Common\Error\BadResponseError; +use OpenStack\ObjectStore\v1\Models\Container; +use OpenStack\ObjectStore\v1\Models\StorageObject; +use Psr\Log\LoggerInterface; + +class Swift extends Common { + /** @var SwiftFactory */ + private $connectionFactory; + /** + * @var Container + */ + private $container; + /** + * @var string + */ + private $bucket; + /** + * Connection parameters + * + * @var array + */ + private $params; + + /** @var string */ + private $id; + + /** @var \OC\Files\ObjectStore\Swift */ + private $objectStore; + + /** @var IMimeTypeDetector */ + private $mimeDetector; + + /** + * Key value cache mapping path to data object. Maps path to + * \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject for existing + * paths and path to false for not existing paths. + * + * @var ICache + */ + private $objectCache; + + private function normalizePath(string $path): string { + $path = trim($path, '/'); + + if (!$path) { + $path = '.'; + } + + $path = str_replace('#', '%23', $path); + + return $path; + } + + public const SUBCONTAINER_FILE = '.subcontainers'; + + /** + * 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. + * + * @return StorageObject|false object + * or false if the object did not exist + * @throws StorageAuthException + * @throws StorageNotAvailableException + */ + 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 $cached; + } + try { + $object = $this->getContainer()->getObject($path); + $object->retrieve(); + $this->objectCache->set($path, $object); + return $object; + } catch (BadResponseError $e) { + // Expected response is "404 Not Found", so only log if it isn't + if ($e->getResponse()->getStatusCode() !== 404) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + } + $this->objectCache->set($path, false); + return false; + } + } + + /** + * Returns whether the given path exists. + * + * @return bool true if the object exist, false otherwise + * @throws StorageAuthException + * @throws StorageNotAvailableException + */ + private function doesObjectExist(string $path): bool { + return $this->fetchObject($path) !== false; + } + + 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, Login, Bucket and Region have to be configured.'); + } + + $user = $parameters['user']; + $this->id = 'swift::' . $user . md5($parameters['bucket']); + + $bucketUrl = new Uri($parameters['bucket']); + if ($bucketUrl->getHost()) { + $parameters['bucket'] = basename($bucketUrl->getPath()); + $parameters['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath())); + } + + if (empty($parameters['url'])) { + $parameters['url'] = 'https://identity.api.rackspacecloud.com/v2.0/'; + } + + if (empty($parameters['service_name'])) { + $parameters['service_name'] = 'cloudFiles'; + } + + $parameters['autocreate'] = true; + + if (isset($parameters['domain'])) { + $parameters['user'] = [ + 'name' => $parameters['user'], + 'password' => $parameters['password'], + 'domain' => [ + 'name' => $parameters['domain'], + ] + ]; + } + + $this->params = $parameters; + // FIXME: private class... + $this->objectCache = new CappedMemoryCache(); + $this->connectionFactory = new SwiftFactory( + Server::get(ICacheFactory::class)->createDistributed('swift/'), + $this->params, + Server::get(LoggerInterface::class) + ); + $this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory); + $this->bucket = $parameters['bucket']; + $this->mimeDetector = Server::get(IMimeTypeDetector::class); + } + + public function mkdir(string $path): bool { + $path = $this->normalizePath($path); + + if ($this->is_dir($path)) { + return false; + } + + if ($path !== '.') { + $path .= '/'; + } + + try { + $this->getContainer()->createObject([ + 'name' => $path, + 'content' => '', + 'headers' => ['content-type' => 'httpd/unix-directory'] + ]); + // invalidate so that the next access gets the real object + // with all properties + $this->objectCache->remove($path); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + return false; + } + + return true; + } + + public function file_exists(string $path): bool { + $path = $this->normalizePath($path); + + if ($path !== '.' && $this->is_dir($path)) { + $path .= '/'; + } + + return $this->doesObjectExist($path); + } + + public function rmdir(string $path): bool { + $path = $this->normalizePath($path); + + if (!$this->is_dir($path) || !$this->isDeletable($path)) { + return false; + } + + $dh = $this->opendir($path); + while (($file = readdir($dh)) !== false) { + if (Filesystem::isIgnoredDir($file)) { + continue; + } + + if ($this->is_dir($path . '/' . $file)) { + $this->rmdir($path . '/' . $file); + } else { + $this->unlink($path . '/' . $file); + } + } + + try { + $this->objectStore->deleteObject($path . '/'); + $this->objectCache->remove($path . '/'); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + return false; + } + + return true; + } + + public function opendir(string $path) { + $path = $this->normalizePath($path); + + if ($path === '.') { + $path = ''; + } else { + $path .= '/'; + } + + // $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of # + + try { + $files = []; + $objects = $this->getContainer()->listObjects([ + 'prefix' => $path, + 'delimiter' => '/' + ]); + + /** @var StorageObject $object */ + foreach ($objects as $object) { + $file = basename($object->name); + if ($file !== basename($path) && $file !== '.') { + $files[] = $file; + } + } + + return IteratorDirectory::wrap($files); + } catch (\Exception $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + return false; + } + } + + public function stat(string $path): array|false { + $path = $this->normalizePath($path); + if ($path === '.') { + $path = ''; + } elseif ($this->is_dir($path)) { + $path .= '/'; + } + + try { + $object = $this->fetchObject($path); + if (!$object) { + return false; + } + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + return false; + } + + $mtime = null; + if (!empty($object->lastModified)) { + $dateTime = \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified); + if ($dateTime !== false) { + $mtime = $dateTime->getTimestamp(); + } + } + + if (is_numeric($object->getMetadata()['timestamp'] ?? null)) { + $mtime = (float)$object->getMetadata()['timestamp']; + } + + return [ + 'size' => (int)$object->contentLength, + 'mtime' => isset($mtime) ? (int)floor($mtime) : null, + 'atime' => time(), + ]; + } + + public function filetype(string $path) { + $path = $this->normalizePath($path); + + if ($path !== '.' && $this->doesObjectExist($path)) { + return 'file'; + } + + if ($path !== '.') { + $path .= '/'; + } + + if ($this->doesObjectExist($path)) { + return 'dir'; + } + } + + public function unlink(string $path): bool { + $path = $this->normalizePath($path); + + if ($this->is_dir($path)) { + return $this->rmdir($path); + } + + try { + $this->objectStore->deleteObject($path); + $this->objectCache->remove($path); + $this->objectCache->remove($path . '/'); + } catch (BadResponseError $e) { + if ($e->getResponse()->getStatusCode() !== 404) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + throw $e; + } + } + + return true; + } + + public function fopen(string $path, string $mode) { + $path = $this->normalizePath($path); + + switch ($mode) { + case 'a': + case 'ab': + case 'a+': + return false; + case 'r': + case 'rb': + try { + return $this->objectStore->readObject($path); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + return false; + } + case 'w': + case 'wb': + case 'r+': + case 'w+': + case 'wb+': + case 'x': + case 'x+': + case 'c': + case 'c+': + if (strrpos($path, '.') !== false) { + $ext = substr($path, strrpos($path, '.')); + } else { + $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') { + // File cannot already exist + return false; + } + $source = $this->fopen($path, 'r'); + file_put_contents($tmpFile, $source); + } + $handle = fopen($tmpFile, $mode); + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void { + $this->writeBack($tmpFile, $path); + }); + } + } + + public function touch(string $path, ?int $mtime = null): bool { + $path = $this->normalizePath($path); + if (is_null($mtime)) { + $mtime = time(); + } + $metadata = ['timestamp' => (string)$mtime]; + if ($this->file_exists($path)) { + if ($this->is_dir($path) && $path !== '.') { + $path .= '/'; + } + + $object = $this->fetchObject($path); + if ($object->mergeMetadata($metadata)) { + // invalidate target object to force repopulation on fetch + $this->objectCache->remove($path); + } + return true; + } else { + $mimeType = $this->mimeDetector->detectPath($path); + $this->getContainer()->createObject([ + 'name' => $path, + 'content' => '', + 'headers' => ['content-type' => 'httpd/unix-directory'] + ]); + // invalidate target object to force repopulation on fetch + $this->objectCache->remove($path); + return true; + } + } + + public function copy(string $source, string $target): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); + + $fileType = $this->filetype($source); + if ($fileType) { + // make way + $this->unlink($target); + } + + if ($fileType === 'file') { + try { + $sourceObject = $this->fetchObject($source); + $sourceObject->copy([ + 'destination' => $this->bucket . '/' . $target + ]); + // invalidate target object to force repopulation on fetch + $this->objectCache->remove($target); + $this->objectCache->remove($target . '/'); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + return false; + } + } elseif ($fileType === 'dir') { + try { + $sourceObject = $this->fetchObject($source . '/'); + $sourceObject->copy([ + 'destination' => $this->bucket . '/' . $target . '/' + ]); + // invalidate target object to force repopulation on fetch + $this->objectCache->remove($target); + $this->objectCache->remove($target . '/'); + } catch (BadResponseError $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'exception' => $e, + 'app' => 'files_external', + ]); + return false; + } + + $dh = $this->opendir($source); + while (($file = readdir($dh)) !== false) { + if (Filesystem::isIgnoredDir($file)) { + continue; + } + + $source = $source . '/' . $file; + $target = $target . '/' . $file; + $this->copy($source, $target); + } + } else { + //file does not exist + return false; + } + + return true; + } + + public function rename(string $source, string $target): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); + + $fileType = $this->filetype($source); + + if ($fileType === 'dir' || $fileType === 'file') { + // copy + if ($this->copy($source, $target) === false) { + return false; + } + + // cleanup + if ($this->unlink($source) === false) { + throw new \Exception('failed to remove original'); + $this->unlink($target); + return false; + } + + return true; + } + + return false; + } + + public function getId(): string { + return $this->id; + } + + /** + * Returns the initialized object store container. + * + * @return Container + * @throws StorageAuthException + * @throws StorageNotAvailableException + */ + public function getContainer(): Container { + if (is_null($this->container)) { + $this->container = $this->connectionFactory->getContainer(); + + if (!$this->file_exists('.')) { + $this->mkdir('.'); + } + } + return $this->container; + } + + public function writeBack(string $tmpFile, string $path): void { + $fileData = fopen($tmpFile, 'r'); + $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(string $path, int $time): bool { + if ($this->is_file($path)) { + return parent::hasUpdated($path, $time); + } + $path = $this->normalizePath($path); + $dh = $this->opendir($path); + $content = []; + while (($file = readdir($dh)) !== false) { + $content[] = $file; + } + if ($path === '.') { + $path = ''; + } + $cachedContent = $this->getCache()->getFolderContents($path); + $cachedNames = array_map(function ($content) { + return $content['name']; + }, $cachedContent); + sort($cachedNames); + sort($content); + return $cachedNames !== $content; + } + + /** + * check if curl is installed + */ + 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/storageconfig.php b/apps/files_external/lib/Lib/StorageConfig.php index 590a5f53249..2cb82d3790a 100644 --- a/apps/files_external/lib/storageconfig.php +++ b/apps/files_external/lib/Lib/StorageConfig.php @@ -1,40 +1,28 @@ <?php + /** - * @author Jesús Macias <jmacias@solidgear.es> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * 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; -namespace OCA\Files_external\Lib; - +use OC\Files\Filesystem; +use OCA\Files_External\Lib\Auth\AuthMechanism; use OCA\Files_External\Lib\Auth\IUserProvided; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\ResponseDefinitions; /** * External storage configuration + * + * @psalm-import-type Files_ExternalStorageConfig from ResponseDefinitions */ class StorageConfig implements \JsonSerializable { - const MOUNT_TYPE_ADMIN = 1; - const MOUNT_TYPE_PERSONAl = 2; + public const MOUNT_TYPE_ADMIN = 1; + public const MOUNT_TYPE_PERSONAL = 2; + /** @deprecated use MOUNT_TYPE_PERSONAL (full uppercase) instead */ + public const MOUNT_TYPE_PERSONAl = 2; /** * Storage config id @@ -60,7 +48,7 @@ class StorageConfig implements \JsonSerializable { /** * Backend options * - * @var array + * @var array<string, mixed> */ private $backendOptions = []; @@ -95,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 = []; @@ -123,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; } @@ -144,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; } @@ -166,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); } /** @@ -180,7 +168,7 @@ class StorageConfig implements \JsonSerializable { * @param Backend $backend */ public function setBackend(Backend $backend) { - $this->backend= $backend; + $this->backend = $backend; } /** @@ -212,12 +200,12 @@ class StorageConfig implements \JsonSerializable { * @param array $backendOptions backend options */ public function setBackendOptions($backendOptions) { - if($this->getBackend() instanceof Backend) { + if ($this->getBackend() instanceof Backend) { $parameters = $this->getBackend()->getParameters(); - foreach($backendOptions as $key => $value) { - if(isset($parameters[$key])) { + foreach ($backendOptions as $key => $value) { + if (isset($parameters[$key])) { switch ($parameters[$key]->getType()) { - case \OCA\Files_External\Lib\DefinitionParameter::VALUE_BOOLEAN: + case DefinitionParameter::VALUE_BOOLEAN: $value = (bool)$value; break; } @@ -258,7 +246,7 @@ class StorageConfig implements \JsonSerializable { } /** - * Sets the mount priotity + * Sets the mount priority * * @param int $priority priority */ @@ -269,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; @@ -278,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)) { @@ -290,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; @@ -299,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)) { @@ -378,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; @@ -393,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(); @@ -424,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 new file mode 100644 index 00000000000..4062ff1635e --- /dev/null +++ b/apps/files_external/lib/Lib/StorageModifierTrait.php @@ -0,0 +1,50 @@ +<?php + +/** + * 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; +use OCP\Files\StorageNotAvailableException; +use OCP\IUser; + +/** + * Trait for objects that can modify StorageConfigs and wrap Storages + * + * When a storage implementation is being prepared for use, the StorageConfig + * is passed through manipulateStorageConfig() to update any parameters as + * necessary. After the storage implementation has been constructed, it is + * passed through wrapStorage(), potentially replacing the implementation with + * a wrapped storage that changes its behaviour. + * + * Certain configuration options need to be set before the implementation is + * constructed, while others are retrieved directly from the storage + * implementation and so need a wrapper to be modified. + */ +trait StorageModifierTrait { + + /** + * Modify a StorageConfig parameters + * + * @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) { + } + + /** + * Wrap a storage if necessary + * + * @throws InsufficientDataForMeaningfulAnswerException + * @throws StorageNotAvailableException + */ + public function wrapStorage(IStorage $storage): IStorage { + return $storage; + } +} diff --git a/apps/files_external/lib/visibilitytrait.php b/apps/files_external/lib/Lib/VisibilityTrait.php index 916c8e69d9c..62b26f3edb1 100644 --- a/apps/files_external/lib/visibilitytrait.php +++ b/apps/files_external/lib/Lib/VisibilityTrait.php @@ -1,27 +1,13 @@ <?php + /** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Lib; -use \OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\BackendService; /** * Trait to implement visibility mechanics for a configuration class @@ -132,5 +118,4 @@ trait VisibilityTrait { public function removeAllowedVisibility($allowedVisibility) { return $this->setAllowedVisibility($this->allowedVisibility & ~$allowedVisibility); } - } diff --git a/apps/files_external/lib/Listener/GroupDeletedListener.php b/apps/files_external/lib/Listener/GroupDeletedListener.php new file mode 100644 index 00000000000..244b3b2371f --- /dev/null +++ b/apps/files_external/lib/Listener/GroupDeletedListener.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Listener; + +use OCA\Files_External\Service\DBConfigService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Group\Events\GroupDeletedEvent; + +/** @template-implements IEventListener<GroupDeletedEvent> */ +class GroupDeletedListener implements IEventListener { + public function __construct( + private DBConfigService $config, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof GroupDeletedEvent) { + return; + } + $this->config->modifyMountsOnGroupDelete($event->getGroup()->getGID()); + } +} diff --git a/apps/files_external/lib/Listener/LoadAdditionalListener.php b/apps/files_external/lib/Listener/LoadAdditionalListener.php new file mode 100644 index 00000000000..6ba917759c3 --- /dev/null +++ b/apps/files_external/lib/Listener/LoadAdditionalListener.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Listener; + +use OCA\Files\Event\LoadAdditionalScriptsEvent; +use OCA\Files_External\AppInfo\Application; +use OCA\Files_External\ConfigLexicon; +use OCP\AppFramework\Services\IInitialState; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IAppConfig; +use OCP\Util; + +/** + * @template-implements IEventListener<LoadAdditionalScriptsEvent> + */ +class LoadAdditionalListener implements IEventListener { + + public function __construct( + private readonly IAppConfig $appConfig, + private IInitialState $initialState, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof LoadAdditionalScriptsEvent)) { + return; + } + + $allowUserMounting = $this->appConfig->getValueBool('files_external', ConfigLexicon::ALLOW_USER_MOUNTING); + $this->initialState->provideInitialState('allowUserMounting', $allowUserMounting); + + Util::addInitScript(Application::APP_ID, 'init'); + } +} diff --git a/apps/files_external/lib/Listener/StorePasswordListener.php b/apps/files_external/lib/Listener/StorePasswordListener.php new file mode 100644 index 00000000000..8580176b014 --- /dev/null +++ b/apps/files_external/lib/Listener/StorePasswordListener.php @@ -0,0 +1,57 @@ +<?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\Listener; + +use OCA\Files_External\Lib\Auth\Password\LoginCredentials; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Security\ICredentialsManager; +use OCP\User\Events\PasswordUpdatedEvent; +use OCP\User\Events\UserLoggedInEvent; + +/** @template-implements IEventListener<PasswordUpdatedEvent|UserLoggedInEvent> */ +class StorePasswordListener implements IEventListener { + public function __construct( + private ICredentialsManager $credentialsManager, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof UserLoggedInEvent && !$event instanceof PasswordUpdatedEvent) { + return; + } + + if ($event instanceof UserLoggedInEvent && $event->isTokenLogin()) { + return; + } + + $storedCredentials = $this->credentialsManager->retrieve($event->getUser()->getUID(), LoginCredentials::CREDENTIALS_IDENTIFIER); + + if (!$storedCredentials) { + return; + } + + $newCredentials = $storedCredentials; + $shouldUpdate = false; + + if (($storedCredentials['password'] ?? null) !== $event->getPassword() && $event->getPassword() !== null) { + $shouldUpdate = true; + $newCredentials['password'] = $event->getPassword(); + } + + if ($event instanceof UserLoggedInEvent && ($storedCredentials['user'] ?? null) !== $event->getLoginName()) { + $shouldUpdate = true; + $newCredentials['user'] = $event->getLoginName(); + } + + if ($shouldUpdate) { + $this->credentialsManager->store($event->getUser()->getUID(), LoginCredentials::CREDENTIALS_IDENTIFIER, $newCredentials); + } + } +} diff --git a/apps/files_external/lib/Listener/UserDeletedListener.php b/apps/files_external/lib/Listener/UserDeletedListener.php new file mode 100644 index 00000000000..337fd12f311 --- /dev/null +++ b/apps/files_external/lib/Listener/UserDeletedListener.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Listener; + +use OCA\Files_External\Service\DBConfigService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\UserDeletedEvent; + +/** @template-implements IEventListener<UserDeletedEvent> */ +class UserDeletedListener implements IEventListener { + public function __construct( + private DBConfigService $config, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof UserDeletedEvent) { + return; + } + $this->config->modifyMountsOnUserDelete($event->getUser()->getUID()); + } +} diff --git a/apps/files_external/lib/Migration/DummyUserSession.php b/apps/files_external/lib/Migration/DummyUserSession.php new file mode 100644 index 00000000000..1ebf0e1ec4f --- /dev/null +++ b/apps/files_external/lib/Migration/DummyUserSession.php @@ -0,0 +1,57 @@ +<?php + +/** + * 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\Migration; + +use OCP\IUser; +use OCP\IUserSession; + +class DummyUserSession implements IUserSession { + + private ?IUser $user = null; + + public function login($uid, $password) { + } + + public function logout() { + } + + public function setUser($user) { + $this->user = $user; + } + + public function setVolatileActiveUser(?IUser $user): void { + $this->user = $user; + } + + public function getUser() { + return $this->user; + } + + public function isLoggedIn() { + return !is_null($this->user); + } + + /** + * get getImpersonatingUserID + * + * @return string|null + * @since 17.0.0 + */ + public function getImpersonatingUserID() : ?string { + return null; + } + + /** + * set setImpersonatingUserID + * + * @since 17.0.0 + */ + public function setImpersonatingUserID(bool $useCurrentUser = true): void { + //no OP + } +} diff --git a/apps/files_external/lib/Migration/Version1011Date20200630192246.php b/apps/files_external/lib/Migration/Version1011Date20200630192246.php new file mode 100644 index 00000000000..c87b1cfbc8b --- /dev/null +++ b/apps/files_external/lib/Migration/Version1011Date20200630192246.php @@ -0,0 +1,136 @@ +<?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\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1011Date20200630192246 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('external_mounts')) { + $table = $schema->createTable('external_mounts'); + $table->addColumn('mount_id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('mount_point', Types::STRING, [ + 'notnull' => true, + 'length' => 128, + ]); + $table->addColumn('storage_backend', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('auth_backend', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('priority', Types::INTEGER, [ + 'notnull' => true, + 'length' => 4, + 'default' => 100, + ]); + $table->addColumn('type', Types::INTEGER, [ + 'notnull' => true, + 'length' => 4, + ]); + $table->setPrimaryKey(['mount_id']); + } + + if (!$schema->hasTable('external_applicable')) { + $table = $schema->createTable('external_applicable'); + $table->addColumn('applicable_id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('mount_id', Types::BIGINT, [ + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('type', Types::INTEGER, [ + 'notnull' => true, + 'length' => 4, + ]); + $table->addColumn('value', Types::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + $table->setPrimaryKey(['applicable_id']); + $table->addIndex(['mount_id'], 'applicable_mount'); + $table->addUniqueIndex(['type', 'value', 'mount_id'], 'applicable_type_value_mount'); + } + + if (!$schema->hasTable('external_config')) { + $table = $schema->createTable('external_config'); + $table->addColumn('config_id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('mount_id', Types::BIGINT, [ + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('key', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('value', Types::STRING, [ + 'notnull' => false, + 'length' => 4000, + ]); + $table->setPrimaryKey(['config_id']); + $table->addUniqueIndex(['mount_id', 'key'], 'config_mount_key'); + } else { + $table = $schema->getTable('external_config'); + $table->changeColumn('value', [ + 'notnull' => false, + 'length' => 4000, + ]); + } + + if (!$schema->hasTable('external_options')) { + $table = $schema->createTable('external_options'); + $table->addColumn('option_id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('mount_id', Types::BIGINT, [ + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('key', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('value', Types::STRING, [ + 'notnull' => true, + 'length' => 256, + ]); + $table->setPrimaryKey(['option_id']); + $table->addUniqueIndex(['mount_id', 'key'], 'option_mount_key'); + } + return $schema; + } +} diff --git a/apps/files_external/lib/Migration/Version1015Date20211104103506.php b/apps/files_external/lib/Migration/Version1015Date20211104103506.php new file mode 100644 index 00000000000..6027c795cdf --- /dev/null +++ b/apps/files_external/lib/Migration/Version1015Date20211104103506.php @@ -0,0 +1,90 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Migration; + +use Closure; +use OC\Files\Cache\Storage; +use OCP\DB\Exception; +use OCP\DB\IResult; +use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; +use Psr\Log\LoggerInterface; + +class Version1015Date20211104103506 extends SimpleMigrationStep { + + public function __construct( + private IDBConnection $connection, + private LoggerInterface $logger, + ) { + } + + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + $qb = $this->connection->getQueryBuilder(); + $qb->update('storages') + ->set('id', $qb->createParameter('newId')) + ->where($qb->expr()->eq('id', $qb->createParameter('oldId'))); + + $mounts = $this->getS3Mounts(); + if (!$mounts instanceof IResult) { + throw new \Exception('Could not fetch existing mounts for migration'); + } + + while ($mount = $mounts->fetch()) { + $config = $this->getStorageConfig((int)$mount['mount_id']); + $hostname = $config['hostname']; + $bucket = $config['bucket']; + $key = $config['key']; + $oldId = Storage::adjustStorageId('amazon::' . $bucket); + $newId = Storage::adjustStorageId('amazon::external::' . md5($hostname . ':' . $bucket . ':' . $key)); + try { + $qb->setParameter('oldId', $oldId); + $qb->setParameter('newId', $newId); + $qb->execute(); + $this->logger->info('Migrated s3 storage id for mount with id ' . $mount['mount_id'] . ' to ' . $newId); + } catch (Exception $e) { + $this->logger->error('Failed to migrate external s3 storage id for mount with id ' . $mount['mount_id'], [ + 'exception' => $e + ]); + } + } + return null; + } + + /** + * @throws Exception + * @return IResult|int + */ + private function getS3Mounts() { + $qb = $this->connection->getQueryBuilder(); + $qb->select('m.mount_id') + ->selectAlias('c.value', 'bucket') + ->from('external_mounts', 'm') + ->innerJoin('m', 'external_config', 'c', 'c.mount_id = m.mount_id') + ->where($qb->expr()->eq('m.storage_backend', $qb->createPositionalParameter('amazons3'))) + ->andWhere($qb->expr()->eq('c.key', $qb->createPositionalParameter('bucket'))); + return $qb->execute(); + } + + /** + * @throws Exception + */ + private function getStorageConfig(int $mountId): array { + $qb = $this->connection->getQueryBuilder(); + $qb->select('key', 'value') + ->from('external_config') + ->where($qb->expr()->eq('mount_id', $qb->createPositionalParameter($mountId))); + $config = []; + foreach ($qb->execute()->fetchAll() as $row) { + $config[$row['key']] = $row['value']; + } + return $config; + } +} diff --git a/apps/files_external/lib/Migration/Version1016Date20220324154536.php b/apps/files_external/lib/Migration/Version1016Date20220324154536.php new file mode 100644 index 00000000000..fb2cccfdd80 --- /dev/null +++ b/apps/files_external/lib/Migration/Version1016Date20220324154536.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1016Date20220324154536 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('external_config'); + $column = $table->getColumn('value'); + + if ($column->getLength() > 4000) { + $column->setLength(4000); + return $schema; + } + + return null; + } +} diff --git a/apps/files_external/lib/Migration/Version22000Date20210216084416.php b/apps/files_external/lib/Migration/Version22000Date20210216084416.php new file mode 100644 index 00000000000..c4878e602c0 --- /dev/null +++ b/apps/files_external/lib/Migration/Version22000Date20210216084416.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Auto-generated migration step: Please modify to your needs! + */ +class Version22000Date20210216084416 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('external_applicable'); + if ($table->hasIndex('applicable_type_value')) { + $table->dropIndex('applicable_type_value'); + } + + $table = $schema->getTable('external_config'); + if ($table->hasIndex('config_mount')) { + $table->dropIndex('config_mount'); + } + + $table = $schema->getTable('external_options'); + if ($table->hasIndex('option_mount')) { + $table->dropIndex('option_mount'); + } + + return $schema; + } +} diff --git a/apps/files_external/lib/MountConfig.php b/apps/files_external/lib/MountConfig.php new file mode 100644 index 00000000000..5637ee71ec1 --- /dev/null +++ b/apps/files_external/lib/MountConfig.php @@ -0,0 +1,250 @@ +<?php + +/** + * 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; + +use OC\Files\Storage\Common; +use OCA\Files_External\Config\IConfigHandler; +use OCA\Files_External\Config\UserContext; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\GlobalStoragesService; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\QueryException; +use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; +use OCP\IL10N; +use OCP\Security\ISecureRandom; +use OCP\Server; +use OCP\Util; +use phpseclib\Crypt\AES; +use Psr\Log\LoggerInterface; + +/** + * Class to configure mount.json globally and for users + */ +class MountConfig { + // TODO: make this class non-static and give it a proper namespace + + public const MOUNT_TYPE_GLOBAL = 'global'; + public const MOUNT_TYPE_GROUP = 'group'; + public const MOUNT_TYPE_USER = 'user'; + public const MOUNT_TYPE_PERSONAL = 'personal'; + + // whether to skip backend test (for unit tests, as this static class is not mockable) + public static $skipTest = false; + + public function __construct( + private UserGlobalStoragesService $userGlobalStorageService, + private UserStoragesService $userStorageService, + private GlobalStoragesService $globalStorageService, + ) { + } + + /** + * @param mixed $input + * @param string|null $userId + * @return mixed + * @throws QueryException + * @since 16.0.0 + */ + public static function substitutePlaceholdersInConfig($input, ?string $userId = null) { + /** @var BackendService $backendService */ + $backendService = Server::get(BackendService::class); + /** @var IConfigHandler[] $handlers */ + $handlers = $backendService->getConfigHandlers(); + foreach ($handlers as $handler) { + if ($handler instanceof UserContext && $userId !== null) { + $handler->setUserId($userId); + } + $input = $handler->handle($input); + } + return $input; + } + + /** + * Test connecting using the given backend configuration + * + * @param string $class backend class name + * @param array $options backend configuration options + * @param boolean $isPersonal + * @return int see self::STATUS_* + * @throws \Exception + */ + public static function getBackendStatus($class, $options) { + if (self::$skipTest) { + return StorageNotAvailableException::STATUS_SUCCESS; + } + foreach ($options as $key => &$option) { + if ($key === 'password') { + // no replacements in passwords + continue; + } + $option = self::substitutePlaceholdersInConfig($option); + } + if (class_exists($class)) { + try { + /** @var Common $storage */ + $storage = new $class($options); + + try { + $result = $storage->test(); + $storage->setAvailability($result); + if ($result) { + return StorageNotAvailableException::STATUS_SUCCESS; + } + } catch (\Exception $e) { + $storage->setAvailability(false); + throw $e; + } + } catch (\Exception $exception) { + Server::get(LoggerInterface::class)->error($exception->getMessage(), ['exception' => $exception, 'app' => 'files_external']); + throw $exception; + } + } + return StorageNotAvailableException::STATUS_ERROR; + } + + /** + * Get backend dependency message + * TODO: move into AppFramework along with templates + * + * @param Backend[] $backends + */ + public static function dependencyMessage(array $backends): string { + $l = Util::getL10N('files_external'); + $message = ''; + $dependencyGroups = []; + + foreach ($backends as $backend) { + foreach ($backend->checkDependencies() as $dependency) { + $dependencyMessage = $dependency->getMessage(); + if ($dependencyMessage !== null) { + $message .= '<p>' . $dependencyMessage . '</p>'; + } else { + $dependencyGroups[$dependency->getDependency()][] = $backend; + } + } + } + + foreach ($dependencyGroups as $module => $dependants) { + $backends = implode(', ', array_map(function (Backend $backend): string { + return '"' . $backend->getText() . '"'; + }, $dependants)); + $message .= '<p>' . MountConfig::getSingleDependencyMessage($l, $module, $backends) . '</p>'; + } + + return $message; + } + + /** + * Returns a dependency missing message + */ + private static function getSingleDependencyMessage(IL10N $l, string $module, string $backend): string { + switch (strtolower($module)) { + case 'curl': + return $l->t('The cURL support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', [$backend]); + case 'ftp': + return $l->t('The FTP support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', [$backend]); + default: + return $l->t('"%1$s" is not installed. Mounting of %2$s is not possible. Please ask your system administrator to install it.', [$module, $backend]); + } + } + + /** + * Encrypt passwords in the given config options + * + * @param array $options mount options + * @return array updated options + */ + public static function encryptPasswords($options) { + if (isset($options['password'])) { + $options['password_encrypted'] = self::encryptPassword($options['password']); + // do not unset the password, we want to keep the keys order + // on load... because that's how the UI currently works + $options['password'] = ''; + } + return $options; + } + + /** + * Decrypt passwords in the given config options + * + * @param array $options mount options + * @return array updated options + */ + public static function decryptPasswords($options) { + // note: legacy options might still have the unencrypted password in the "password" field + if (isset($options['password_encrypted'])) { + $options['password'] = self::decryptPassword($options['password_encrypted']); + unset($options['password_encrypted']); + } + return $options; + } + + /** + * Encrypt a single password + * + * @param string $password plain text password + * @return string encrypted password + */ + private static function encryptPassword($password) { + $cipher = self::getCipher(); + $iv = Server::get(ISecureRandom::class)->generate(16); + $cipher->setIV($iv); + return base64_encode($iv . $cipher->encrypt($password)); + } + + /** + * Decrypts a single password + * + * @param string $encryptedPassword encrypted password + * @return string plain text password + */ + private static function decryptPassword($encryptedPassword) { + $cipher = self::getCipher(); + $binaryPassword = base64_decode($encryptedPassword); + $iv = substr($binaryPassword, 0, 16); + $cipher->setIV($iv); + $binaryPassword = substr($binaryPassword, 16); + return $cipher->decrypt($binaryPassword); + } + + /** + * Returns the encryption cipher + * + * @return AES + */ + private static function getCipher() { + $cipher = new AES(AES::MODE_CBC); + $cipher->setKey(Server::get(IConfig::class)->getSystemValue('passwordsalt', null)); + return $cipher; + } + + /** + * Computes a hash based on the given configuration. + * This is mostly used to find out whether configurations + * are the same. + * + * @param array $config + * @return string + */ + public static function makeConfigHash($config) { + $data = json_encode( + [ + 'c' => $config['backend'], + 'a' => $config['authMechanism'], + 'm' => $config['mountpoint'], + 'o' => $config['options'], + 'p' => $config['priority'] ?? -1, + 'mo' => $config['mountOptions'] ?? [], + ] + ); + return hash('md5', $data); + } +} diff --git a/apps/files_external/lib/NotFoundException.php b/apps/files_external/lib/NotFoundException.php new file mode 100644 index 00000000000..411a2212513 --- /dev/null +++ b/apps/files_external/lib/NotFoundException.php @@ -0,0 +1,14 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External; + +/** + * Storage is not found + */ +class NotFoundException extends \Exception { +} diff --git a/apps/files_external/lib/ResponseDefinitions.php b/apps/files_external/lib/ResponseDefinitions.php new file mode 100644 index 00000000000..26a0965f1fc --- /dev/null +++ b/apps/files_external/lib/ResponseDefinitions.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External; + +/** + * @psalm-type Files_ExternalStorageConfig = array{ + * applicableGroups?: list<string>, + * applicableUsers?: list<string>, + * authMechanism: string, + * backend: string, + * backendOptions: array<string, mixed>, + * id?: int, + * mountOptions?: array<string, mixed>, + * mountPoint: string, + * priority?: int, + * status?: int, + * statusMessage?: string, + * type: 'personal'|'system', + * userProvided: bool, + * } + * + * @psalm-type Files_ExternalMount = array{ + * name: string, + * path: string, + * type: 'dir', + * backend: string, + * scope: 'system'|'personal', + * permissions: int, + * id: int, + * class: string, + * config: Files_ExternalStorageConfig, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/files_external/lib/Service/BackendService.php b/apps/files_external/lib/Service/BackendService.php new file mode 100644 index 00000000000..3a688ee66e6 --- /dev/null +++ b/apps/files_external/lib/Service/BackendService.php @@ -0,0 +1,342 @@ +<?php + +/** + * 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\Service; + +use OCA\Files_External\Config\IConfigHandler; +use OCA\Files_External\ConfigLexicon; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\Config\IAuthMechanismProvider; +use OCA\Files_External\Lib\Config\IBackendProvider; +use OCA\Files_External\Lib\MissingDependency; +use OCP\EventDispatcher\GenericEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; +use OCP\Server; + +/** + * Service class to manage backend definitions + */ +class BackendService { + + /** Visibility constants for VisibilityTrait */ + public const VISIBILITY_NONE = 0; + public const VISIBILITY_PERSONAL = 1; + public const VISIBILITY_ADMIN = 2; + //const VISIBILITY_ALIENS = 4; + + public const VISIBILITY_DEFAULT = 3; // PERSONAL | ADMIN + + /** Priority constants for PriorityTrait */ + public const PRIORITY_DEFAULT = 100; + + /** @var bool */ + private $userMountingAllowed = true; + + /** @var string[] */ + private $userMountingBackends = []; + + /** @var Backend[] */ + private $backends = []; + + /** @var IBackendProvider[] */ + private $backendProviders = []; + + /** @var AuthMechanism[] */ + private $authMechanisms = []; + + /** @var IAuthMechanismProvider[] */ + private $authMechanismProviders = []; + + /** @var callable[] */ + private $configHandlerLoaders = []; + + private $configHandlers = []; + + public function __construct( + protected IAppConfig $appConfig, + ) { + // Load config values + $this->userMountingAllowed = $appConfig->getValueBool('files_external', ConfigLexicon::ALLOW_USER_MOUNTING); + $this->userMountingBackends = explode(',', $appConfig->getValueString('files_external', ConfigLexicon::USER_MOUNTING_BACKENDS)); + + // if no backend is in the list an empty string is in the array and user mounting is disabled + if ($this->userMountingBackends === ['']) { + $this->userMountingAllowed = false; + } + } + + /** + * Register a backend provider + * + * @since 9.1.0 + * @param IBackendProvider $provider + */ + public function registerBackendProvider(IBackendProvider $provider) { + $this->backendProviders[] = $provider; + } + + private function callForRegistrations() { + static $eventSent = false; + if (!$eventSent) { + Server::get(IEventDispatcher::class)->dispatch( + 'OCA\\Files_External::loadAdditionalBackends', + new GenericEvent() + ); + $eventSent = true; + } + } + + private function loadBackendProviders() { + $this->callForRegistrations(); + foreach ($this->backendProviders as $provider) { + $this->registerBackends($provider->getBackends()); + } + $this->backendProviders = []; + } + + /** + * Register an auth mechanism provider + * + * @since 9.1.0 + * @param IAuthMechanismProvider $provider + */ + public function registerAuthMechanismProvider(IAuthMechanismProvider $provider) { + $this->authMechanismProviders[] = $provider; + } + + private function loadAuthMechanismProviders() { + $this->callForRegistrations(); + foreach ($this->authMechanismProviders as $provider) { + $this->registerAuthMechanisms($provider->getAuthMechanisms()); + } + $this->authMechanismProviders = []; + } + + /** + * Register a backend + * + * @deprecated 9.1.0 use registerBackendProvider() + * @param Backend $backend + */ + public function registerBackend(Backend $backend) { + if (!$this->isAllowedUserBackend($backend)) { + $backend->removeVisibility(BackendService::VISIBILITY_PERSONAL); + } + foreach ($backend->getIdentifierAliases() as $alias) { + $this->backends[$alias] = $backend; + } + } + + /** + * @deprecated 9.1.0 use registerBackendProvider() + * @param Backend[] $backends + */ + public function registerBackends(array $backends) { + foreach ($backends as $backend) { + $this->registerBackend($backend); + } + } + /** + * Register an authentication mechanism + * + * @deprecated 9.1.0 use registerAuthMechanismProvider() + * @param AuthMechanism $authMech + */ + public function registerAuthMechanism(AuthMechanism $authMech) { + if (!$this->isAllowedAuthMechanism($authMech)) { + $authMech->removeVisibility(BackendService::VISIBILITY_PERSONAL); + } + foreach ($authMech->getIdentifierAliases() as $alias) { + $this->authMechanisms[$alias] = $authMech; + } + } + + /** + * @deprecated 9.1.0 use registerAuthMechanismProvider() + * @param AuthMechanism[] $mechanisms + */ + public function registerAuthMechanisms(array $mechanisms) { + foreach ($mechanisms as $mechanism) { + $this->registerAuthMechanism($mechanism); + } + } + + /** + * Get all backends + * + * @return Backend[] + */ + public function getBackends() { + $this->loadBackendProviders(); + // only return real identifiers, no aliases + $backends = []; + foreach ($this->backends as $backend) { + $backends[$backend->getIdentifier()] = $backend; + } + return $backends; + } + + /** + * Get all available backends + * + * @return Backend[] + */ + public function getAvailableBackends() { + return array_filter($this->getBackends(), function ($backend) { + $missing = array_filter($backend->checkDependencies(), fn (MissingDependency $dependency) => !$dependency->isOptional()); + return count($missing) === 0; + }); + } + + /** + * @param string $identifier + * @return Backend|null + */ + public function getBackend($identifier) { + $this->loadBackendProviders(); + if (isset($this->backends[$identifier])) { + return $this->backends[$identifier]; + } + return null; + } + + /** + * Get all authentication mechanisms + * + * @return AuthMechanism[] + */ + public function getAuthMechanisms() { + $this->loadAuthMechanismProviders(); + // only return real identifiers, no aliases + $mechanisms = []; + foreach ($this->authMechanisms as $mechanism) { + $mechanisms[$mechanism->getIdentifier()] = $mechanism; + } + return $mechanisms; + } + + /** + * Get all authentication mechanisms for schemes + * + * @param string[] $schemes + * @return AuthMechanism[] + */ + public function getAuthMechanismsByScheme(array $schemes) { + return array_filter($this->getAuthMechanisms(), function ($authMech) use ($schemes) { + return in_array($authMech->getScheme(), $schemes, true); + }); + } + + /** + * @param string $identifier + * @return AuthMechanism|null + */ + public function getAuthMechanism($identifier) { + $this->loadAuthMechanismProviders(); + if (isset($this->authMechanisms[$identifier])) { + return $this->authMechanisms[$identifier]; + } + return null; + } + + /** + * @return bool + */ + public function isUserMountingAllowed() { + return $this->userMountingAllowed; + } + + /** + * Check a backend if a user is allowed to mount it + * + * @param Backend $backend + * @return bool + */ + protected function isAllowedUserBackend(Backend $backend) { + if ($this->userMountingAllowed + && array_intersect($backend->getIdentifierAliases(), $this->userMountingBackends) + ) { + return true; + } + return false; + } + + /** + * Check an authentication mechanism if a user is allowed to use it + * + * @param AuthMechanism $authMechanism + * @return bool + */ + protected function isAllowedAuthMechanism(AuthMechanism $authMechanism) { + return true; // not implemented + } + + /** + * registers a configuration handler + * + * The function of the provided $placeholder is mostly to act a sorting + * criteria, so longer placeholders are replaced first. This avoids + * "$user" overwriting parts of "$userMail" and "$userLang", for example. + * The provided value should not contain the $ prefix, only a-z0-9 are + * allowed. Upper case letters are lower cased, the replacement is case- + * insensitive. + * + * The configHandlerLoader should just instantiate the handler on demand. + * For now all handlers are instantiated when a mount is loaded, independent + * of whether the placeholder is present or not. This may change in future. + * + * @since 16.0.0 + */ + public function registerConfigHandler(string $placeholder, callable $configHandlerLoader) { + $placeholder = trim(strtolower($placeholder)); + if (!(bool)\preg_match('/^[a-z0-9]*$/', $placeholder)) { + throw new \RuntimeException(sprintf( + 'Invalid placeholder %s, only [a-z0-9] are allowed', $placeholder + )); + } + if ($placeholder === '') { + throw new \RuntimeException('Invalid empty placeholder'); + } + if (isset($this->configHandlerLoaders[$placeholder]) || isset($this->configHandlers[$placeholder])) { + throw new \RuntimeException(sprintf('A handler is already registered for %s', $placeholder)); + } + $this->configHandlerLoaders[$placeholder] = $configHandlerLoader; + } + + protected function loadConfigHandlers():void { + $this->callForRegistrations(); + $newLoaded = false; + foreach ($this->configHandlerLoaders as $placeholder => $loader) { + $handler = $loader(); + if (!$handler instanceof IConfigHandler) { + throw new \RuntimeException(sprintf( + 'Handler for %s is not an instance of IConfigHandler', $placeholder + )); + } + $this->configHandlers[$placeholder] = $handler; + $newLoaded = true; + } + $this->configHandlerLoaders = []; + if ($newLoaded) { + // ensure those with longest placeholders come first, + // to avoid substring matches + uksort($this->configHandlers, function ($phA, $phB) { + return strlen($phB) <=> strlen($phA); + }); + } + } + + /** + * @since 16.0.0 + */ + public function getConfigHandlers() { + $this->loadConfigHandlers(); + return $this->configHandlers; + } +} diff --git a/apps/files_external/lib/Service/DBConfigService.php b/apps/files_external/lib/Service/DBConfigService.php new file mode 100644 index 00000000000..41ec4512d70 --- /dev/null +++ b/apps/files_external/lib/Service/DBConfigService.php @@ -0,0 +1,501 @@ +<?php + +/** + * 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\Service; + +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Security\ICrypto; + +/** + * Stores the mount config in the database + */ +class DBConfigService { + 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; + + public const APPLICABLE_TYPE_GLOBAL = 1; + public const APPLICABLE_TYPE_GROUP = 2; + public const APPLICABLE_TYPE_USER = 3; + + public function __construct( + private IDBConnection $connection, + private ICrypto $crypto, + ) { + } + + public function getMountById(int $mountId): ?array { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type']) + ->from('external_mounts', 'm') + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))); + $mounts = $this->getMountsFromQuery($query); + if (count($mounts) > 0) { + return $mounts[0]; + } else { + return null; + } + } + + /** + * Get all configured mounts + * + * @return array + */ + public function getAllMounts() { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type']) + ->from('external_mounts'); + return $this->getMountsFromQuery($query); + } + + public function getMountsForUser($userId, $groupIds) { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type']) + ->from('external_mounts', 'm') + ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id')) + ->where($builder->expr()->orX( + $builder->expr()->andX( // global mounts + $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)), + $builder->expr()->isNull('a.value') + ), + $builder->expr()->andX( // mounts for user + $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_USER, IQueryBuilder::PARAM_INT)), + $builder->expr()->eq('a.value', $builder->createNamedParameter($userId)) + ), + $builder->expr()->andX( // mounts for group + $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GROUP, IQueryBuilder::PARAM_INT)), + $builder->expr()->in('a.value', $builder->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY)) + ) + )); + + return $this->getMountsFromQuery($query); + } + + public function modifyMountsOnUserDelete(string $uid): void { + $this->modifyMountsOnDelete($uid, self::APPLICABLE_TYPE_USER); + } + + public function modifyMountsOnGroupDelete(string $gid): void { + $this->modifyMountsOnDelete($gid, self::APPLICABLE_TYPE_GROUP); + } + + protected function modifyMountsOnDelete(string $applicableId, int $applicableType): void { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select(['a.mount_id', $builder->func()->count('a.mount_id', 'count')]) + ->from('external_applicable', 'a') + ->leftJoin('a', 'external_applicable', 'b', $builder->expr()->eq('a.mount_id', 'b.mount_id')) + ->where($builder->expr()->andX( + $builder->expr()->eq('b.type', $builder->createNamedParameter($applicableType, IQueryBuilder::PARAM_INT)), + $builder->expr()->eq('b.value', $builder->createNamedParameter($applicableId)) + ) + ) + ->groupBy(['a.mount_id']); + $stmt = $query->executeQuery(); + $result = $stmt->fetchAll(); + $stmt->closeCursor(); + + foreach ($result as $row) { + if ((int)$row['count'] > 1) { + $this->removeApplicable($row['mount_id'], $applicableType, $applicableId); + } else { + $this->removeMount($row['mount_id']); + } + } + } + + /** + * Get admin defined mounts + * + * @return array + */ + public function getAdminMounts() { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type']) + ->from('external_mounts') + ->where($builder->expr()->eq('type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT))); + return $this->getMountsFromQuery($query); + } + + protected function getForQuery(IQueryBuilder $builder, $type, $value) { + $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type']) + ->from('external_mounts', 'm') + ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id')) + ->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT))); + + if (is_null($value)) { + $query = $query->andWhere($builder->expr()->isNull('a.value')); + } else { + $query = $query->andWhere($builder->expr()->eq('a.value', $builder->createNamedParameter($value))); + } + + return $query; + } + + /** + * Get mounts by applicable + * + * @param int $type any of the self::APPLICABLE_TYPE_ constants + * @param string|null $value user_id, group_id or null for global mounts + * @return array + */ + public function getMountsFor($type, $value) { + $builder = $this->connection->getQueryBuilder(); + $query = $this->getForQuery($builder, $type, $value); + + return $this->getMountsFromQuery($query); + } + + /** + * Get admin defined mounts by applicable + * + * @param int $type any of the self::APPLICABLE_TYPE_ constants + * @param string|null $value user_id, group_id or null for global mounts + * @return array + */ + public function getAdminMountsFor($type, $value) { + $builder = $this->connection->getQueryBuilder(); + $query = $this->getForQuery($builder, $type, $value); + $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT))); + + return $this->getMountsFromQuery($query); + } + + /** + * Get admin defined mounts for multiple applicable + * + * @param int $type any of the self::APPLICABLE_TYPE_ constants + * @param string[] $values user_ids or group_ids + * @return array + */ + public function getAdminMountsForMultiple($type, array $values) { + $builder = $this->connection->getQueryBuilder(); + $params = array_map(function ($value) use ($builder) { + return $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR); + }, $values); + + $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type']) + ->from('external_mounts', 'm') + ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id')) + ->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT))) + ->andWhere($builder->expr()->in('a.value', $params)); + $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT))); + + return $this->getMountsFromQuery($query); + } + + /** + * Get user defined mounts by applicable + * + * @param int $type any of the self::APPLICABLE_TYPE_ constants + * @param string|null $value user_id, group_id or null for global mounts + * @return array + */ + public function getUserMountsFor($type, $value) { + $builder = $this->connection->getQueryBuilder(); + $query = $this->getForQuery($builder, $type, $value); + $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_PERSONAL, IQueryBuilder::PARAM_INT))); + + return $this->getMountsFromQuery($query); + } + + /** + * Add a mount to the database + * + * @param string $mountPoint + * @param string $storageBackend + * @param string $authBackend + * @param int $priority + * @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL + * @return int the id of the new mount + */ + public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type) { + if (!$priority) { + $priority = 100; + } + $builder = $this->connection->getQueryBuilder(); + $query = $builder->insert('external_mounts') + ->values([ + 'mount_point' => $builder->createNamedParameter($mountPoint, IQueryBuilder::PARAM_STR), + 'storage_backend' => $builder->createNamedParameter($storageBackend, IQueryBuilder::PARAM_STR), + 'auth_backend' => $builder->createNamedParameter($authBackend, IQueryBuilder::PARAM_STR), + 'priority' => $builder->createNamedParameter($priority, IQueryBuilder::PARAM_INT), + 'type' => $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT) + ]); + $query->executeStatement(); + return $query->getLastInsertId(); + } + + /** + * Remove a mount from the database + * + * @param int $mountId + */ + public function removeMount($mountId) { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->delete('external_mounts') + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + + $builder = $this->connection->getQueryBuilder(); + $query = $builder->delete('external_applicable') + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + + $builder = $this->connection->getQueryBuilder(); + $query = $builder->delete('external_config') + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + + $builder = $this->connection->getQueryBuilder(); + $query = $builder->delete('external_options') + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + } + + /** + * @param int $mountId + * @param string $newMountPoint + */ + public function setMountPoint($mountId, $newMountPoint) { + $builder = $this->connection->getQueryBuilder(); + + $query = $builder->update('external_mounts') + ->set('mount_point', $builder->createNamedParameter($newMountPoint)) + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))); + + $query->executeStatement(); + } + + /** + * @param int $mountId + * @param string $newAuthBackend + */ + public function setAuthBackend($mountId, $newAuthBackend) { + $builder = $this->connection->getQueryBuilder(); + + $query = $builder->update('external_mounts') + ->set('auth_backend', $builder->createNamedParameter($newAuthBackend)) + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))); + + $query->executeStatement(); + } + + /** + * @param int $mountId + * @param string $key + * @param string $value + */ + public function setConfig($mountId, $key, $value) { + if ($key === 'password') { + $value = $this->encryptValue($value); + } + + try { + $builder = $this->connection->getQueryBuilder(); + $builder->insert('external_config') + ->setValue('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)) + ->setValue('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR)) + ->setValue('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR)) + ->execute(); + } catch (UniqueConstraintViolationException $e) { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->update('external_config') + ->set('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR)) + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))) + ->andWhere($builder->expr()->eq('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR))); + $query->executeStatement(); + } + } + + /** + * @param int $mountId + * @param string $key + * @param string $value + */ + public function setOption($mountId, $key, $value) { + try { + $builder = $this->connection->getQueryBuilder(); + $builder->insert('external_options') + ->setValue('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)) + ->setValue('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR)) + ->setValue('value', $builder->createNamedParameter(json_encode($value), IQueryBuilder::PARAM_STR)) + ->execute(); + } catch (UniqueConstraintViolationException $e) { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->update('external_options') + ->set('value', $builder->createNamedParameter(json_encode($value), IQueryBuilder::PARAM_STR)) + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))) + ->andWhere($builder->expr()->eq('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR))); + $query->executeStatement(); + } + } + + public function addApplicable($mountId, $type, $value) { + try { + $builder = $this->connection->getQueryBuilder(); + $builder->insert('external_applicable') + ->setValue('mount_id', $builder->createNamedParameter($mountId)) + ->setValue('type', $builder->createNamedParameter($type)) + ->setValue('value', $builder->createNamedParameter($value)) + ->execute(); + } catch (UniqueConstraintViolationException $e) { + // applicable exists already + } + } + + public function removeApplicable($mountId, $type, $value) { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->delete('external_applicable') + ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))) + ->andWhere($builder->expr()->eq('type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT))); + + if (is_null($value)) { + $query = $query->andWhere($builder->expr()->isNull('value')); + } else { + $query = $query->andWhere($builder->expr()->eq('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR))); + } + + $query->executeStatement(); + } + + private function getMountsFromQuery(IQueryBuilder $query) { + $result = $query->executeQuery(); + $mounts = $result->fetchAll(); + $uniqueMounts = []; + foreach ($mounts as $mount) { + $id = $mount['mount_id']; + if (!isset($uniqueMounts[$id])) { + $uniqueMounts[$id] = $mount; + } + } + $uniqueMounts = array_values($uniqueMounts); + + $mountIds = array_map(function ($mount) { + return $mount['mount_id']; + }, $uniqueMounts); + $mountIds = array_values(array_unique($mountIds)); + + $applicable = $this->getApplicableForMounts($mountIds); + $config = $this->getConfigForMounts($mountIds); + $options = $this->getOptionsForMounts($mountIds); + + return array_map(function ($mount, $applicable, $config, $options) { + $mount['type'] = (int)$mount['type']; + $mount['priority'] = (int)$mount['priority']; + $mount['applicable'] = $applicable; + $mount['config'] = $config; + $mount['options'] = $options; + return $mount; + }, $uniqueMounts, $applicable, $config, $options); + } + + /** + * Get mount options from a table grouped by mount id + * + * @param string $table + * @param string[] $fields + * @param int[] $mountIds + * @return array [$mountId => [['field1' => $value1, ...], ...], ...] + */ + private function selectForMounts($table, array $fields, array $mountIds) { + if (count($mountIds) === 0) { + return []; + } + $builder = $this->connection->getQueryBuilder(); + $fields[] = 'mount_id'; + $placeHolders = array_map(function ($id) use ($builder) { + return $builder->createPositionalParameter($id, IQueryBuilder::PARAM_INT); + }, $mountIds); + $query = $builder->select($fields) + ->from($table) + ->where($builder->expr()->in('mount_id', $placeHolders)); + + $result = $query->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + $result = []; + foreach ($mountIds as $mountId) { + $result[$mountId] = []; + } + foreach ($rows as $row) { + if (isset($row['type'])) { + $row['type'] = (int)$row['type']; + } + $result[$row['mount_id']][] = $row; + } + return $result; + } + + /** + * @param int[] $mountIds + * @return array [$id => [['type' => $type, 'value' => $value], ...], ...] + */ + public function getApplicableForMounts($mountIds) { + return $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds); + } + + /** + * @param int[] $mountIds + * @return array [$id => ['key1' => $value1, ...], ...] + */ + public function getConfigForMounts($mountIds) { + $mountConfigs = $this->selectForMounts('external_config', ['key', 'value'], $mountIds); + return array_map([$this, 'createKeyValueMap'], $mountConfigs); + } + + /** + * @param int[] $mountIds + * @return array [$id => ['key1' => $value1, ...], ...] + */ + public function getOptionsForMounts($mountIds) { + $mountOptions = $this->selectForMounts('external_options', ['key', 'value'], $mountIds); + $optionsMap = array_map([$this, 'createKeyValueMap'], $mountOptions); + return array_map(function (array $options) { + return array_map(function ($option) { + return json_decode($option); + }, $options); + }, $optionsMap); + } + + /** + * @param array $keyValuePairs [['key'=>$key, 'value=>$value], ...] + * @return array ['key1' => $value1, ...] + */ + private function createKeyValueMap(array $keyValuePairs) { + $decryptedPairts = array_map(function ($pair) { + if ($pair['key'] === 'password') { + $pair['value'] = $this->decryptValue($pair['value']); + } + return $pair; + }, $keyValuePairs); + $keys = array_map(function ($pair) { + return $pair['key']; + }, $decryptedPairts); + $values = array_map(function ($pair) { + return $pair['value']; + }, $decryptedPairts); + + return array_combine($keys, $values); + } + + private function encryptValue($value) { + return $this->crypto->encrypt($value); + } + + private function decryptValue($value) { + try { + return $this->crypto->decrypt($value); + } catch (\Exception $e) { + return $value; + } + } +} diff --git a/apps/files_external/lib/Service/GlobalStoragesService.php b/apps/files_external/lib/Service/GlobalStoragesService.php new file mode 100644 index 00000000000..5b1a9f41e48 --- /dev/null +++ b/apps/files_external/lib/Service/GlobalStoragesService.php @@ -0,0 +1,165 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Service; + +use OC\Files\Filesystem; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\MountConfig; + +/** + * Service class to manage global external storage + */ +class GlobalStoragesService extends StoragesService { + /** + * Triggers $signal for all applicable users of the given + * storage + * + * @param StorageConfig $storage storage data + * @param string $signal signal to trigger + */ + protected function triggerHooks(StorageConfig $storage, $signal) { + // FIXME: Use as expression in empty once PHP 5.4 support is dropped + $applicableUsers = $storage->getApplicableUsers(); + $applicableGroups = $storage->getApplicableGroups(); + if (empty($applicableUsers) && empty($applicableGroups)) { + // raise for user "all" + $this->triggerApplicableHooks( + $signal, + $storage->getMountPoint(), + MountConfig::MOUNT_TYPE_USER, + ['all'] + ); + return; + } + + $this->triggerApplicableHooks( + $signal, + $storage->getMountPoint(), + MountConfig::MOUNT_TYPE_USER, + $applicableUsers + ); + $this->triggerApplicableHooks( + $signal, + $storage->getMountPoint(), + MountConfig::MOUNT_TYPE_GROUP, + $applicableGroups + ); + } + + /** + * Triggers signal_create_mount or signal_delete_mount to + * accommodate for additions/deletions in applicableUsers + * and applicableGroups fields. + * + * @param StorageConfig $oldStorage old storage config + * @param StorageConfig $newStorage new storage config + */ + protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { + // if mount point changed, it's like a deletion + creation + if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) { + $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount); + $this->triggerHooks($newStorage, Filesystem::signal_create_mount); + return; + } + + $userAdditions = array_diff($newStorage->getApplicableUsers(), $oldStorage->getApplicableUsers()); + $userDeletions = array_diff($oldStorage->getApplicableUsers(), $newStorage->getApplicableUsers()); + $groupAdditions = array_diff($newStorage->getApplicableGroups(), $oldStorage->getApplicableGroups()); + $groupDeletions = array_diff($oldStorage->getApplicableGroups(), $newStorage->getApplicableGroups()); + + // FIXME: Use as expression in empty once PHP 5.4 support is dropped + // if no applicable were set, raise a signal for "all" + $oldApplicableUsers = $oldStorage->getApplicableUsers(); + $oldApplicableGroups = $oldStorage->getApplicableGroups(); + if (empty($oldApplicableUsers) && empty($oldApplicableGroups)) { + $this->triggerApplicableHooks( + Filesystem::signal_delete_mount, + $oldStorage->getMountPoint(), + MountConfig::MOUNT_TYPE_USER, + ['all'] + ); + } + + // trigger delete for removed users + $this->triggerApplicableHooks( + Filesystem::signal_delete_mount, + $oldStorage->getMountPoint(), + MountConfig::MOUNT_TYPE_USER, + $userDeletions + ); + + // trigger delete for removed groups + $this->triggerApplicableHooks( + Filesystem::signal_delete_mount, + $oldStorage->getMountPoint(), + MountConfig::MOUNT_TYPE_GROUP, + $groupDeletions + ); + + // and now add the new users + $this->triggerApplicableHooks( + Filesystem::signal_create_mount, + $newStorage->getMountPoint(), + MountConfig::MOUNT_TYPE_USER, + $userAdditions + ); + + // and now add the new groups + $this->triggerApplicableHooks( + Filesystem::signal_create_mount, + $newStorage->getMountPoint(), + MountConfig::MOUNT_TYPE_GROUP, + $groupAdditions + ); + + // FIXME: Use as expression in empty once PHP 5.4 support is dropped + // if no applicable, raise a signal for "all" + $newApplicableUsers = $newStorage->getApplicableUsers(); + $newApplicableGroups = $newStorage->getApplicableGroups(); + if (empty($newApplicableUsers) && empty($newApplicableGroups)) { + $this->triggerApplicableHooks( + Filesystem::signal_create_mount, + $newStorage->getMountPoint(), + MountConfig::MOUNT_TYPE_USER, + ['all'] + ); + } + } + + /** + * Get the visibility type for this controller, used in validation + * + * @return int BackendService::VISIBILITY_* constants + */ + public function getVisibilityType() { + return BackendService::VISIBILITY_ADMIN; + } + + protected function isApplicable(StorageConfig $config) { + return true; + } + + /** + * Get all configured admin and personal mounts + * + * @return StorageConfig[] map of storage id to storage config + */ + public function getStorageForAllUsers() { + $mounts = $this->dbConfig->getAllMounts(); + $configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts); + $configs = array_filter($configs, function ($config) { + return $config instanceof StorageConfig; + }); + + $keys = array_map(function (StorageConfig $config) { + return $config->getId(); + }, $configs); + + return array_combine($keys, $configs); + } +} diff --git a/apps/files_external/lib/Service/ImportLegacyStoragesService.php b/apps/files_external/lib/Service/ImportLegacyStoragesService.php new file mode 100644 index 00000000000..7d9840e9f5e --- /dev/null +++ b/apps/files_external/lib/Service/ImportLegacyStoragesService.php @@ -0,0 +1,31 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Service; + +class ImportLegacyStoragesService extends LegacyStoragesService { + private $data; + + /** + * @param BackendService $backendService + */ + public function __construct(BackendService $backendService) { + $this->backendService = $backendService; + } + + public function setData($data) { + $this->data = $data; + } + + /** + * Read legacy config data + * + * @return array list of mount configs + */ + protected function readLegacyConfig() { + return $this->data; + } +} diff --git a/apps/files_external/lib/Service/LegacyStoragesService.php b/apps/files_external/lib/Service/LegacyStoragesService.php new file mode 100644 index 00000000000..9f199a89b3f --- /dev/null +++ b/apps/files_external/lib/Service/LegacyStoragesService.php @@ -0,0 +1,193 @@ +<?php + +/** + * 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\Service; + +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\MountConfig; +use OCP\Server; +use Psr\Log\LoggerInterface; + +/** + * Read mount config from legacy mount.json + */ +abstract class LegacyStoragesService { + /** @var BackendService */ + protected $backendService; + + /** + * Read legacy config data + * + * @return array list of mount configs + */ + abstract protected function readLegacyConfig(); + + /** + * Copy legacy storage options into the given storage config object. + * + * @param StorageConfig $storageConfig storage config to populate + * @param string $mountType mount type + * @param string $applicable applicable user or group + * @param array $storageOptions legacy storage options + * + * @return StorageConfig populated storage config + */ + protected function populateStorageConfigWithLegacyOptions( + &$storageConfig, + $mountType, + $applicable, + $storageOptions, + ) { + $backend = $this->backendService->getBackend($storageOptions['backend']); + if (!$backend) { + throw new \UnexpectedValueException('Invalid backend ' . $storageOptions['backend']); + } + $storageConfig->setBackend($backend); + if (isset($storageOptions['authMechanism']) && $storageOptions['authMechanism'] !== 'builtin::builtin') { + $authMechanism = $this->backendService->getAuthMechanism($storageOptions['authMechanism']); + } else { + $authMechanism = $backend->getLegacyAuthMechanism($storageOptions); + $storageOptions['authMechanism'] = 'null'; // to make error handling easier + } + if (!$authMechanism) { + throw new \UnexpectedValueException('Invalid authentication mechanism ' . $storageOptions['authMechanism']); + } + $storageConfig->setAuthMechanism($authMechanism); + $storageConfig->setBackendOptions($storageOptions['options']); + if (isset($storageOptions['mountOptions'])) { + $storageConfig->setMountOptions($storageOptions['mountOptions']); + } + if (!isset($storageOptions['priority'])) { + $storageOptions['priority'] = $backend->getPriority(); + } + $storageConfig->setPriority($storageOptions['priority']); + if ($mountType === MountConfig::MOUNT_TYPE_USER) { + $applicableUsers = $storageConfig->getApplicableUsers(); + if ($applicable !== 'all') { + $applicableUsers[] = $applicable; + $storageConfig->setApplicableUsers($applicableUsers); + } + } elseif ($mountType === MountConfig::MOUNT_TYPE_GROUP) { + $applicableGroups = $storageConfig->getApplicableGroups(); + $applicableGroups[] = $applicable; + $storageConfig->setApplicableGroups($applicableGroups); + } + return $storageConfig; + } + + /** + * Read the external storage config + * + * @return StorageConfig[] map of storage id to storage config + */ + public function getAllStorages() { + $mountPoints = $this->readLegacyConfig(); + /** + * Here is the how the horribly messy mount point array looks like + * from the mount.json file: + * + * $storageOptions = $mountPoints[$mountType][$applicable][$mountPath] + * + * - $mountType is either "user" or "group" + * - $applicable is the name of a user or group (or the current user for personal mounts) + * - $mountPath is the mount point path (where the storage must be mounted) + * - $storageOptions is a map of storage options: + * - "priority": storage priority + * - "backend": backend identifier + * - "class": LEGACY backend class name + * - "options": backend-specific options + * - "authMechanism": authentication mechanism identifier + * - "mountOptions": mount-specific options (ex: disable previews, scanner, etc) + */ + // group by storage id + /** @var StorageConfig[] $storages */ + $storages = []; + // for storages without id (legacy), group by config hash for + // later processing + $storagesWithConfigHash = []; + foreach ($mountPoints as $mountType => $applicables) { + foreach ($applicables as $applicable => $mountPaths) { + foreach ($mountPaths as $rootMountPath => $storageOptions) { + $currentStorage = null; + /** + * Flag whether the config that was read already has an id. + * If not, it will use a config hash instead and generate + * a proper id later + * + * @var boolean + */ + $hasId = false; + // the root mount point is in the format "/$user/files/the/mount/point" + // we remove the "/$user/files" prefix + $parts = explode('/', ltrim($rootMountPath, '/'), 3); + if (count($parts) < 3) { + // something went wrong, skip + Server::get(LoggerInterface::class)->error('Could not parse mount point "' . $rootMountPath . '"', ['app' => 'files_external']); + continue; + } + $relativeMountPath = rtrim($parts[2], '/'); + // note: we cannot do this after the loop because the decrypted config + // options might be needed for the config hash + $storageOptions['options'] = MountConfig::decryptPasswords($storageOptions['options']); + if (!isset($storageOptions['backend'])) { + $storageOptions['backend'] = $storageOptions['class']; // legacy compat + } + if (!isset($storageOptions['authMechanism'])) { + $storageOptions['authMechanism'] = null; // ensure config hash works + } + if (isset($storageOptions['id'])) { + $configId = (int)$storageOptions['id']; + if (isset($storages[$configId])) { + $currentStorage = $storages[$configId]; + } + $hasId = true; + } else { + // missing id in legacy config, need to generate + // but at this point we don't know the max-id, so use + // first group it by config hash + $storageOptions['mountpoint'] = $rootMountPath; + $configId = MountConfig::makeConfigHash($storageOptions); + if (isset($storagesWithConfigHash[$configId])) { + $currentStorage = $storagesWithConfigHash[$configId]; + } + } + if (is_null($currentStorage)) { + // create new + $currentStorage = new StorageConfig($configId); + $currentStorage->setMountPoint($relativeMountPath); + } + try { + $this->populateStorageConfigWithLegacyOptions( + $currentStorage, + $mountType, + $applicable, + $storageOptions + ); + if ($hasId) { + $storages[$configId] = $currentStorage; + } else { + $storagesWithConfigHash[$configId] = $currentStorage; + } + } catch (\UnexpectedValueException $e) { + // don't die if a storage backend doesn't exist + Server::get(LoggerInterface::class)->error('Could not load storage.', [ + 'app' => 'files_external', + 'exception' => $e, + ]); + } + } + } + } + + // convert parameter values + foreach ($storages as $storage) { + $storage->getBackend()->validateStorageDefinition($storage); + $storage->getAuthMechanism()->validateStorageDefinition($storage); + } + return $storages; + } +} diff --git a/apps/files_external/lib/Service/StoragesService.php b/apps/files_external/lib/Service/StoragesService.php new file mode 100644 index 00000000000..a12a8fc245a --- /dev/null +++ b/apps/files_external/lib/Service/StoragesService.php @@ -0,0 +1,476 @@ +<?php + +/** + * 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\Service; + +use OC\Files\Cache\Storage; +use OC\Files\Filesystem; +use OCA\Files_External\Lib\Auth\AuthMechanism; +use OCA\Files_External\Lib\Auth\InvalidAuth; +use OCA\Files_External\Lib\Backend\Backend; +use OCA\Files_External\Lib\Backend\InvalidBackend; +use OCA\Files_External\Lib\DefinitionParameter; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\NotFoundException; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Config\IUserMountCache; +use OCP\Files\Events\InvalidateMountCacheEvent; +use OCP\Files\StorageNotAvailableException; +use OCP\Server; +use OCP\Util; +use Psr\Log\LoggerInterface; + +/** + * Service class to manage external storage + */ +abstract class StoragesService { + + /** + * @param BackendService $backendService + * @param DBConfigService $dbConfig + * @param IUserMountCache $userMountCache + * @param IEventDispatcher $eventDispatcher + */ + public function __construct( + protected BackendService $backendService, + protected DBConfigService $dbConfig, + protected IUserMountCache $userMountCache, + protected IEventDispatcher $eventDispatcher, + ) { + } + + protected function readDBConfig() { + return $this->dbConfig->getAdminMounts(); + } + + protected function getStorageConfigFromDBMount(array $mount) { + $applicableUsers = array_filter($mount['applicable'], function ($applicable) { + return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_USER; + }); + $applicableUsers = array_map(function ($applicable) { + return $applicable['value']; + }, $applicableUsers); + + $applicableGroups = array_filter($mount['applicable'], function ($applicable) { + return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_GROUP; + }); + $applicableGroups = array_map(function ($applicable) { + return $applicable['value']; + }, $applicableGroups); + + try { + $config = $this->createStorage( + $mount['mount_point'], + $mount['storage_backend'], + $mount['auth_backend'], + $mount['config'], + $mount['options'], + array_values($applicableUsers), + array_values($applicableGroups), + $mount['priority'] + ); + $config->setType($mount['type']); + $config->setId((int)$mount['mount_id']); + return $config; + } catch (\UnexpectedValueException $e) { + // don't die if a storage backend doesn't exist + Server::get(LoggerInterface::class)->error('Could not load storage.', [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return null; + } catch (\InvalidArgumentException $e) { + Server::get(LoggerInterface::class)->error('Could not load storage.', [ + 'app' => 'files_external', + 'exception' => $e, + ]); + return null; + } + } + + /** + * Read the external storage config + * + * @return array map of storage id to storage config + */ + protected function readConfig() { + $mounts = $this->readDBConfig(); + $configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts); + $configs = array_filter($configs, function ($config) { + return $config instanceof StorageConfig; + }); + + $keys = array_map(function (StorageConfig $config) { + return $config->getId(); + }, $configs); + + return array_combine($keys, $configs); + } + + /** + * Get a storage with status + * + * @param int $id storage id + * + * @return StorageConfig + * @throws NotFoundException if the storage with the given id was not found + */ + public function getStorage(int $id) { + $mount = $this->dbConfig->getMountById($id); + + if (!is_array($mount)) { + throw new NotFoundException('Storage with ID "' . $id . '" not found'); + } + + $config = $this->getStorageConfigFromDBMount($mount); + if ($this->isApplicable($config)) { + return $config; + } else { + throw new NotFoundException('Storage with ID "' . $id . '" not found'); + } + } + + /** + * Check whether this storage service should provide access to a storage + * + * @param StorageConfig $config + * @return bool + */ + abstract protected function isApplicable(StorageConfig $config); + + /** + * Gets all storages, valid or not + * + * @return StorageConfig[] array of storage configs + */ + public function getAllStorages() { + return $this->readConfig(); + } + + /** + * Gets all valid storages + * + * @return StorageConfig[] + */ + public function getStorages() { + return array_filter($this->getAllStorages(), [$this, 'validateStorage']); + } + + /** + * Validate storage + * FIXME: De-duplicate with StoragesController::validate() + * + * @param StorageConfig $storage + * @return bool + */ + protected function validateStorage(StorageConfig $storage) { + /** @var Backend */ + $backend = $storage->getBackend(); + /** @var AuthMechanism */ + $authMechanism = $storage->getAuthMechanism(); + + if (!$backend->isVisibleFor($this->getVisibilityType())) { + // not permitted to use backend + return false; + } + if (!$authMechanism->isVisibleFor($this->getVisibilityType())) { + // not permitted to use auth mechanism + return false; + } + + return true; + } + + /** + * Get the visibility type for this controller, used in validation + * + * @return int BackendService::VISIBILITY_* constants + */ + abstract public function getVisibilityType(); + + /** + * @return integer + */ + protected function getType() { + return DBConfigService::MOUNT_TYPE_ADMIN; + } + + /** + * Add new storage to the configuration + * + * @param StorageConfig $newStorage storage attributes + * + * @return StorageConfig storage config, with added id + */ + public function addStorage(StorageConfig $newStorage) { + $allStorages = $this->readConfig(); + + $configId = $this->dbConfig->addMount( + $newStorage->getMountPoint(), + $newStorage->getBackend()->getIdentifier(), + $newStorage->getAuthMechanism()->getIdentifier(), + $newStorage->getPriority(), + $this->getType() + ); + + $newStorage->setId($configId); + + foreach ($newStorage->getApplicableUsers() as $user) { + $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_USER, $user); + } + foreach ($newStorage->getApplicableGroups() as $group) { + $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GROUP, $group); + } + foreach ($newStorage->getBackendOptions() as $key => $value) { + $this->dbConfig->setConfig($configId, $key, $value); + } + foreach ($newStorage->getMountOptions() as $key => $value) { + $this->dbConfig->setOption($configId, $key, $value); + } + + if (count($newStorage->getApplicableUsers()) === 0 && count($newStorage->getApplicableGroups()) === 0) { + $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GLOBAL, null); + } + + // add new storage + $allStorages[$configId] = $newStorage; + + $this->triggerHooks($newStorage, Filesystem::signal_create_mount); + + $newStorage->setStatus(StorageNotAvailableException::STATUS_SUCCESS); + return $newStorage; + } + + /** + * Create a storage from its parameters + * + * @param string $mountPoint storage mount point + * @param string $backendIdentifier backend identifier + * @param string $authMechanismIdentifier authentication mechanism identifier + * @param array $backendOptions backend-specific options + * @param array|null $mountOptions mount-specific options + * @param array|null $applicableUsers users for which to mount the storage + * @param array|null $applicableGroups groups for which to mount the storage + * @param int|null $priority priority + * + * @return StorageConfig + */ + public function createStorage( + $mountPoint, + $backendIdentifier, + $authMechanismIdentifier, + $backendOptions, + $mountOptions = null, + $applicableUsers = null, + $applicableGroups = null, + $priority = null, + ) { + $backend = $this->backendService->getBackend($backendIdentifier); + if (!$backend) { + $backend = new InvalidBackend($backendIdentifier); + } + $authMechanism = $this->backendService->getAuthMechanism($authMechanismIdentifier); + if (!$authMechanism) { + $authMechanism = new InvalidAuth($authMechanismIdentifier); + } + $newStorage = new StorageConfig(); + $newStorage->setMountPoint($mountPoint); + $newStorage->setBackend($backend); + $newStorage->setAuthMechanism($authMechanism); + $newStorage->setBackendOptions($backendOptions); + if (isset($mountOptions)) { + $newStorage->setMountOptions($mountOptions); + } + if (isset($applicableUsers)) { + $newStorage->setApplicableUsers($applicableUsers); + } + if (isset($applicableGroups)) { + $newStorage->setApplicableGroups($applicableGroups); + } + if (isset($priority)) { + $newStorage->setPriority($priority); + } + + return $newStorage; + } + + /** + * Triggers the given hook signal for all the applicables given + * + * @param string $signal signal + * @param string $mountPoint hook mount point param + * @param string $mountType hook mount type param + * @param array $applicableArray array of applicable users/groups for which to trigger the hook + */ + protected function triggerApplicableHooks($signal, $mountPoint, $mountType, $applicableArray): void { + $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent(null)); + foreach ($applicableArray as $applicable) { + Util::emitHook( + Filesystem::CLASSNAME, + $signal, + [ + Filesystem::signal_param_path => $mountPoint, + Filesystem::signal_param_mount_type => $mountType, + Filesystem::signal_param_users => $applicable, + ] + ); + } + } + + /** + * Triggers $signal for all applicable users of the given + * storage + * + * @param StorageConfig $storage storage data + * @param string $signal signal to trigger + */ + abstract protected function triggerHooks(StorageConfig $storage, $signal); + + /** + * Triggers signal_create_mount or signal_delete_mount to + * accommodate for additions/deletions in applicableUsers + * and applicableGroups fields. + * + * @param StorageConfig $oldStorage old storage data + * @param StorageConfig $newStorage new storage data + */ + abstract protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage); + + /** + * Update storage to the configuration + * + * @param StorageConfig $updatedStorage storage attributes + * + * @return StorageConfig storage config + * @throws NotFoundException if the given storage does not exist in the config + */ + public function updateStorage(StorageConfig $updatedStorage) { + $id = $updatedStorage->getId(); + + $existingMount = $this->dbConfig->getMountById($id); + + if (!is_array($existingMount)) { + throw new NotFoundException('Storage with ID "' . $id . '" not found while updating storage'); + } + + $oldStorage = $this->getStorageConfigFromDBMount($existingMount); + + if ($oldStorage->getBackend() instanceof InvalidBackend) { + throw new NotFoundException('Storage with id "' . $id . '" cannot be edited due to missing backend'); + } + + $removedUsers = array_diff($oldStorage->getApplicableUsers(), $updatedStorage->getApplicableUsers()); + $removedGroups = array_diff($oldStorage->getApplicableGroups(), $updatedStorage->getApplicableGroups()); + $addedUsers = array_diff($updatedStorage->getApplicableUsers(), $oldStorage->getApplicableUsers()); + $addedGroups = array_diff($updatedStorage->getApplicableGroups(), $oldStorage->getApplicableGroups()); + + $oldUserCount = count($oldStorage->getApplicableUsers()); + $oldGroupCount = count($oldStorage->getApplicableGroups()); + $newUserCount = count($updatedStorage->getApplicableUsers()); + $newGroupCount = count($updatedStorage->getApplicableGroups()); + $wasGlobal = ($oldUserCount + $oldGroupCount) === 0; + $isGlobal = ($newUserCount + $newGroupCount) === 0; + + foreach ($removedUsers as $user) { + $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user); + } + foreach ($removedGroups as $group) { + $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group); + } + foreach ($addedUsers as $user) { + $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user); + } + foreach ($addedGroups as $group) { + $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group); + } + + if ($wasGlobal && !$isGlobal) { + $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null); + } elseif (!$wasGlobal && $isGlobal) { + $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null); + } + + $changedConfig = array_diff_assoc($updatedStorage->getBackendOptions(), $oldStorage->getBackendOptions()); + $changedOptions = array_diff_assoc($updatedStorage->getMountOptions(), $oldStorage->getMountOptions()); + + foreach ($changedConfig as $key => $value) { + if ($value !== DefinitionParameter::UNMODIFIED_PLACEHOLDER) { + $this->dbConfig->setConfig($id, $key, $value); + } + } + foreach ($changedOptions as $key => $value) { + $this->dbConfig->setOption($id, $key, $value); + } + + if ($updatedStorage->getMountPoint() !== $oldStorage->getMountPoint()) { + $this->dbConfig->setMountPoint($id, $updatedStorage->getMountPoint()); + } + + if ($updatedStorage->getAuthMechanism()->getIdentifier() !== $oldStorage->getAuthMechanism()->getIdentifier()) { + $this->dbConfig->setAuthBackend($id, $updatedStorage->getAuthMechanism()->getIdentifier()); + } + + $this->triggerChangeHooks($oldStorage, $updatedStorage); + + if (($wasGlobal && !$isGlobal) || count($removedGroups) > 0) { // to expensive to properly handle these on the fly + $this->userMountCache->remoteStorageMounts($this->getStorageId($updatedStorage)); + } else { + $storageId = $this->getStorageId($updatedStorage); + foreach ($removedUsers as $userId) { + $this->userMountCache->removeUserStorageMount($storageId, $userId); + } + } + + return $this->getStorage($id); + } + + /** + * Delete the storage with the given id. + * + * @param int $id storage id + * + * @throws NotFoundException if no storage was found with the given id + */ + public function removeStorage(int $id) { + $existingMount = $this->dbConfig->getMountById($id); + + if (!is_array($existingMount)) { + throw new NotFoundException('Storage with ID "' . $id . '" not found'); + } + + $this->dbConfig->removeMount($id); + + $deletedStorage = $this->getStorageConfigFromDBMount($existingMount); + $this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount); + + // delete oc_storages entries and oc_filecache + Storage::cleanByMountId($id); + } + + /** + * Construct the storage implementation + * + * @param StorageConfig $storageConfig + * @return int + */ + private function getStorageId(StorageConfig $storageConfig) { + try { + $class = $storageConfig->getBackend()->getStorageClass(); + /** @var \OC\Files\Storage\Storage $storage */ + $storage = new $class($storageConfig->getBackendOptions()); + + // auth mechanism should fire first + $storage = $storageConfig->getBackend()->wrapStorage($storage); + $storage = $storageConfig->getAuthMechanism()->wrapStorage($storage); + + /** @var \OC\Files\Storage\Storage $storage */ + return $storage->getStorageCache()->getNumericId(); + } catch (\Exception $e) { + return -1; + } + } +} diff --git a/apps/files_external/lib/Service/UserGlobalStoragesService.php b/apps/files_external/lib/Service/UserGlobalStoragesService.php new file mode 100644 index 00000000000..aaa59c85d62 --- /dev/null +++ b/apps/files_external/lib/Service/UserGlobalStoragesService.php @@ -0,0 +1,187 @@ +<?php + +/** + * 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\Service; + +use OCA\Files_External\Lib\StorageConfig; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Config\IUserMountCache; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; + +/** + * Service class to read global storages applicable to the user + * Read-only access available, attempting to write will throw DomainException + */ +class UserGlobalStoragesService extends GlobalStoragesService { + use UserTrait; + + /** + * @param BackendService $backendService + * @param DBConfigService $dbConfig + * @param IUserSession $userSession + * @param IGroupManager $groupManager + * @param IUserMountCache $userMountCache + * @param IEventDispatcher $eventDispatcher + */ + public function __construct( + BackendService $backendService, + DBConfigService $dbConfig, + IUserSession $userSession, + protected IGroupManager $groupManager, + IUserMountCache $userMountCache, + IEventDispatcher $eventDispatcher, + ) { + parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher); + $this->userSession = $userSession; + } + + /** + * Replace config hash ID with real IDs, for migrating legacy storages + * + * @param StorageConfig[] $storages Storages with real IDs + * @param StorageConfig[] $storagesWithConfigHash Storages with config hash IDs + */ + protected function setRealStorageIds(array &$storages, array $storagesWithConfigHash) { + // as a read-only view, storage IDs don't need to be real + foreach ($storagesWithConfigHash as $storage) { + $storages[$storage->getId()] = $storage; + } + } + + protected function readDBConfig() { + $userMounts = $this->dbConfig->getAdminMountsFor(DBConfigService::APPLICABLE_TYPE_USER, $this->getUser()->getUID()); + $globalMounts = $this->dbConfig->getAdminMountsFor(DBConfigService::APPLICABLE_TYPE_GLOBAL, null); + $groups = $this->groupManager->getUserGroupIds($this->getUser()); + if (count($groups) !== 0) { + $groupMounts = $this->dbConfig->getAdminMountsForMultiple(DBConfigService::APPLICABLE_TYPE_GROUP, $groups); + } else { + $groupMounts = []; + } + return array_merge($userMounts, $groupMounts, $globalMounts); + } + + public function addStorage(StorageConfig $newStorage) { + throw new \DomainException('UserGlobalStoragesService writing disallowed'); + } + + public function updateStorage(StorageConfig $updatedStorage) { + throw new \DomainException('UserGlobalStoragesService writing disallowed'); + } + + /** + * @param integer $id + */ + public function removeStorage($id) { + throw new \DomainException('UserGlobalStoragesService writing disallowed'); + } + + /** + * Get unique storages, in case two are defined with the same mountpoint + * Higher priority storages take precedence + * + * @return StorageConfig[] + */ + public function getUniqueStorages() { + $storages = $this->getStorages(); + + $storagesByMountpoint = []; + foreach ($storages as $storage) { + $storagesByMountpoint[$storage->getMountPoint()][] = $storage; + } + + $result = []; + foreach ($storagesByMountpoint as $storageList) { + $storage = array_reduce($storageList, function ($carry, $item) { + if (isset($carry)) { + $carryPriorityType = $this->getPriorityType($carry); + $itemPriorityType = $this->getPriorityType($item); + if ($carryPriorityType > $itemPriorityType) { + return $carry; + } elseif ($carryPriorityType === $itemPriorityType) { + if ($carry->getPriority() > $item->getPriority()) { + return $carry; + } + } + } + return $item; + }); + $result[$storage->getID()] = $storage; + } + + return $result; + } + + /** + * Get a priority 'type', where a bigger number means higher priority + * user applicable > group applicable > 'all' + * + * @param StorageConfig $storage + * @return int + */ + protected function getPriorityType(StorageConfig $storage) { + $applicableUsers = $storage->getApplicableUsers(); + $applicableGroups = $storage->getApplicableGroups(); + + if ($applicableUsers && $applicableUsers[0] !== 'all') { + return 2; + } + if ($applicableGroups) { + return 1; + } + return 0; + } + + protected function isApplicable(StorageConfig $config) { + $applicableUsers = $config->getApplicableUsers(); + $applicableGroups = $config->getApplicableGroups(); + + if (count($applicableUsers) === 0 && count($applicableGroups) === 0) { + return true; + } + if (in_array($this->getUser()->getUID(), $applicableUsers, true)) { + return true; + } + $groupIds = $this->groupManager->getUserGroupIds($this->getUser()); + foreach ($groupIds as $groupId) { + if (in_array($groupId, $applicableGroups, true)) { + return true; + } + } + return false; + } + + + /** + * Gets all storages for the user, admin, personal, global, etc + * + * @param IUser|null $user user to get the storages for, if not set the currently logged in user will be used + * @return StorageConfig[] array of storage configs + */ + public function getAllStoragesForUser(?IUser $user = null) { + if (is_null($user)) { + $user = $this->getUser(); + } + if (is_null($user)) { + return []; + } + $groupIds = $this->groupManager->getUserGroupIds($user); + $mounts = $this->dbConfig->getMountsForUser($user->getUID(), $groupIds); + $configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts); + $configs = array_filter($configs, function ($config) { + return $config instanceof StorageConfig; + }); + + $keys = array_map(function (StorageConfig $config) { + return $config->getId(); + }, $configs); + + $storages = array_combine($keys, $configs); + return array_filter($storages, [$this, 'validateStorage']); + } +} diff --git a/apps/files_external/lib/Service/UserStoragesService.php b/apps/files_external/lib/Service/UserStoragesService.php new file mode 100644 index 00000000000..9d4192734b6 --- /dev/null +++ b/apps/files_external/lib/Service/UserStoragesService.php @@ -0,0 +1,134 @@ +<?php + +/** + * 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\Service; + +use OC\Files\Filesystem; +use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\MountConfig; +use OCA\Files_External\NotFoundException; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Config\IUserMountCache; +use OCP\IUserSession; + +/** + * Service class to manage user external storage + * (aka personal storages) + */ +class UserStoragesService extends StoragesService { + use UserTrait; + + /** + * Create a user storages service + * + * @param BackendService $backendService + * @param DBConfigService $dbConfig + * @param IUserSession $userSession user session + * @param IUserMountCache $userMountCache + * @param IEventDispatcher $eventDispatcher + */ + public function __construct( + BackendService $backendService, + DBConfigService $dbConfig, + IUserSession $userSession, + IUserMountCache $userMountCache, + IEventDispatcher $eventDispatcher, + ) { + $this->userSession = $userSession; + parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher); + } + + protected function readDBConfig() { + return $this->dbConfig->getUserMountsFor(DBConfigService::APPLICABLE_TYPE_USER, $this->getUser()->getUID()); + } + + /** + * Triggers $signal for all applicable users of the given + * storage + * + * @param StorageConfig $storage storage data + * @param string $signal signal to trigger + */ + protected function triggerHooks(StorageConfig $storage, $signal) { + $user = $this->getUser()->getUID(); + + // trigger hook for the current user + $this->triggerApplicableHooks( + $signal, + $storage->getMountPoint(), + MountConfig::MOUNT_TYPE_USER, + [$user] + ); + } + + /** + * Triggers signal_create_mount or signal_delete_mount to + * accommodate for additions/deletions in applicableUsers + * and applicableGroups fields. + * + * @param StorageConfig $oldStorage old storage data + * @param StorageConfig $newStorage new storage data + */ + protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { + // if mount point changed, it's like a deletion + creation + if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) { + $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount); + $this->triggerHooks($newStorage, Filesystem::signal_create_mount); + } + } + + protected function getType() { + return DBConfigService::MOUNT_TYPE_PERSONAL; + } + + /** + * Add new storage to the configuration + * + * @param StorageConfig $newStorage storage attributes + * + * @return StorageConfig storage config, with added id + */ + public function addStorage(StorageConfig $newStorage) { + $newStorage->setApplicableUsers([$this->getUser()->getUID()]); + return parent::addStorage($newStorage); + } + + /** + * Update storage to the configuration + * + * @param StorageConfig $updatedStorage storage attributes + * + * @return StorageConfig storage config + * @throws NotFoundException if the given storage does not exist in the config + */ + public function updateStorage(StorageConfig $updatedStorage) { + // verify ownership through $this->isApplicable() and otherwise throws an exception + $this->getStorage($updatedStorage->getId()); + + $updatedStorage->setApplicableUsers([$this->getUser()->getUID()]); + return parent::updateStorage($updatedStorage); + } + + /** + * Get the visibility type for this controller, used in validation + * + * @return int BackendService::VISIBILITY_* constants + */ + public function getVisibilityType() { + return BackendService::VISIBILITY_PERSONAL; + } + + protected function isApplicable(StorageConfig $config) { + return ($config->getApplicableUsers() === [$this->getUser()->getUID()]) && $config->getType() === StorageConfig::MOUNT_TYPE_PERSONAL; + } + + public function removeStorage($id) { + // verify ownership through $this->isApplicable() and otherwise throws an exception + $this->getStorage($id); + parent::removeStorage($id); + } +} diff --git a/apps/files_external/lib/Service/UserTrait.php b/apps/files_external/lib/Service/UserTrait.php new file mode 100644 index 00000000000..679066283a5 --- /dev/null +++ b/apps/files_external/lib/Service/UserTrait.php @@ -0,0 +1,59 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Service; + +use OCP\IUser; +use OCP\IUserSession; + +/** + * Trait for getting user information in a service + */ +trait UserTrait { + + /** @var IUserSession */ + protected $userSession; + + /** + * User override + * + * @var IUser|null + */ + private $user = null; + + /** + * @return IUser|null + */ + protected function getUser() { + if ($this->user) { + return $this->user; + } + return $this->userSession->getUser(); + } + + /** + * Override the user from the session + * Unset with ->resetUser() when finished! + * + * @param IUser $user + * @return self + */ + public function setUser(IUser $user) { + $this->user = $user; + return $this; + } + + /** + * Reset the user override + * + * @return self + */ + public function resetUser() { + $this->user = null; + return $this; + } +} diff --git a/apps/files_external/lib/Settings/Admin.php b/apps/files_external/lib/Settings/Admin.php new file mode 100644 index 00000000000..9af0f3c61c1 --- /dev/null +++ b/apps/files_external/lib/Settings/Admin.php @@ -0,0 +1,63 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Settings; + +use OCA\Files_External\Lib\Auth\Password\GlobalAuth; +use OCA\Files_External\MountConfig; +use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Encryption\IManager; +use OCP\Settings\ISettings; + +class Admin implements ISettings { + + public function __construct( + private IManager $encryptionManager, + private GlobalStoragesService $globalStoragesService, + private BackendService $backendService, + private GlobalAuth $globalAuth, + ) { + } + + /** + * @return TemplateResponse + */ + public function getForm() { + $parameters = [ + 'encryptionEnabled' => $this->encryptionManager->isEnabled(), + 'visibilityType' => BackendService::VISIBILITY_ADMIN, + 'storages' => $this->globalStoragesService->getStorages(), + 'backends' => $this->backendService->getAvailableBackends(), + 'authMechanisms' => $this->backendService->getAuthMechanisms(), + 'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()), + 'allowUserMounting' => $this->backendService->isUserMountingAllowed(), + 'globalCredentials' => $this->globalAuth->getAuth(''), + 'globalCredentialsUid' => '', + ]; + + return new TemplateResponse('files_external', 'settings', $parameters, ''); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'externalstorages'; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + */ + public function getPriority() { + return 40; + } +} diff --git a/apps/files_external/lib/Settings/Personal.php b/apps/files_external/lib/Settings/Personal.php new file mode 100644 index 00000000000..8478badb842 --- /dev/null +++ b/apps/files_external/lib/Settings/Personal.php @@ -0,0 +1,67 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Settings; + +use OCA\Files_External\Lib\Auth\Password\GlobalAuth; +use OCA\Files_External\MountConfig; +use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Encryption\IManager; +use OCP\IUserSession; +use OCP\Settings\ISettings; + +class Personal implements ISettings { + + public function __construct( + private IManager $encryptionManager, + private UserGlobalStoragesService $userGlobalStoragesService, + private BackendService $backendService, + private GlobalAuth $globalAuth, + private IUserSession $userSession, + ) { + } + + /** + * @return TemplateResponse + */ + public function getForm() { + $uid = $this->userSession->getUser()->getUID(); + + $parameters = [ + 'encryptionEnabled' => $this->encryptionManager->isEnabled(), + 'visibilityType' => BackendService::VISIBILITY_PERSONAL, + 'storages' => $this->userGlobalStoragesService->getStorages(), + 'backends' => $this->backendService->getAvailableBackends(), + 'authMechanisms' => $this->backendService->getAuthMechanisms(), + 'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()), + 'allowUserMounting' => $this->backendService->isUserMountingAllowed(), + 'globalCredentials' => $this->globalAuth->getAuth($uid), + 'globalCredentialsUid' => $uid, + ]; + + return new TemplateResponse('files_external', 'settings', $parameters, ''); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'externalstorages'; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + */ + public function getPriority() { + return 40; + } +} diff --git a/apps/files_external/lib/Settings/PersonalSection.php b/apps/files_external/lib/Settings/PersonalSection.php new file mode 100644 index 00000000000..c6eb1c6b889 --- /dev/null +++ b/apps/files_external/lib/Settings/PersonalSection.php @@ -0,0 +1,25 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Settings; + +use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserSession; + +class PersonalSection extends Section { + public function __construct( + IURLGenerator $url, + IL10N $l, + private IUserSession $userSession, + private UserGlobalStoragesService $userGlobalStoragesService, + private BackendService $backendService, + ) { + parent::__construct($url, $l); + } +} diff --git a/apps/files_external/lib/Settings/Section.php b/apps/files_external/lib/Settings/Section.php new file mode 100644 index 00000000000..cf3b73472d7 --- /dev/null +++ b/apps/files_external/lib/Settings/Section.php @@ -0,0 +1,61 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_External\Settings; + +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\Settings\IIconSection; + +class Section implements IIconSection { + /** + * @param IURLGenerator $url + * @param IL10N $l + */ + public function __construct( + private IURLGenerator $url, + private IL10N $l, + ) { + } + + /** + * returns the ID of the section. It is supposed to be a lower case string, + * e.g. 'ldap' + * + * @returns string + */ + public function getID() { + return 'externalstorages'; + } + + /** + * returns the translated name as it should be displayed, e.g. 'LDAP / AD + * integration'. Use the L10N service to translate it. + * + * @return string + */ + public function getName() { + return $this->l->t('External storage'); + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the settings navigation. The sections are arranged in ascending order of + * the priority values. It is required to return a value between 0 and 99. + * + * E.g.: 70 + */ + public function getPriority() { + return 10; + } + + /** + * {@inheritdoc} + */ + public function getIcon() { + return $this->url->imagePath('files_external', 'app-dark.svg'); + } +} diff --git a/apps/files_external/lib/amazons3.php b/apps/files_external/lib/amazons3.php deleted file mode 100644 index cb2082ee38b..00000000000 --- a/apps/files_external/lib/amazons3.php +++ /dev/null @@ -1,634 +0,0 @@ -<?php -/** - * @author André Gaul <gaul@web-yard.de> - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christian Berendt <berendt@b1-systems.de> - * @author Christopher T. Johnson <ctjctj@gmail.com> - * @author Johan Björk <johanimon@gmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Martin Mattel <martin.mattel@diemattels.at> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -set_include_path(get_include_path() . PATH_SEPARATOR . - \OC_App::getAppPath('files_external') . '/3rdparty/aws-sdk-php'); -require 'aws-autoloader.php'; - -use Aws\S3\S3Client; -use Aws\S3\Exception\S3Exception; -use Icewind\Streams\IteratorDirectory; - -class AmazonS3 extends \OC\Files\Storage\Common { - - /** - * @var \Aws\S3\S3Client - */ - private $connection; - /** - * @var string - */ - private $bucket; - /** - * @var array - */ - private static $tmpFiles = array(); - /** - * @var array - */ - private $params; - /** - * @var bool - */ - private $test = false; - /** - * @var int - */ - private $timeout = 15; - /** - * @var int in seconds - */ - private $rescanDelay = 10; - - /** - * @param string $path - * @return string correctly encoded path - */ - private function normalizePath($path) { - $path = trim($path, '/'); - - if (!$path) { - $path = '.'; - } - - return $path; - } - - /** - * when running the tests wait to let the buckets catch up - */ - private function testTimeout() { - if ($this->test) { - sleep($this->timeout); - } - } - - private function isRoot($path) { - return $path === '.'; - } - - private function cleanKey($path) { - if ($this->isRoot($path)) { - return '/'; - } - return $path; - } - - public function __construct($params) { - if (empty($params['key']) || empty($params['secret']) || empty($params['bucket'])) { - throw new \Exception("Access Key, Secret and Bucket have to be configured."); - } - - $this->id = 'amazon::' . $params['bucket']; - $this->updateLegacyId($params); - - $this->bucket = $params['bucket']; - $this->test = isset($params['test']); - $this->timeout = (!isset($params['timeout'])) ? 15 : $params['timeout']; - $this->rescanDelay = (!isset($params['rescanDelay'])) ? 10 : $params['rescanDelay']; - $params['region'] = empty($params['region']) ? 'eu-west-1' : $params['region']; - $params['hostname'] = empty($params['hostname']) ? 's3.amazonaws.com' : $params['hostname']; - if (!isset($params['port']) || $params['port'] === '') { - $params['port'] = ($params['use_ssl'] === false) ? 80 : 443; - } - $this->params = $params; - } - - /** - * Updates old storage ids (v0.2.1 and older) that are based on key and secret to new ones based on the bucket name. - * TODO Do this in an update.php. requires iterating over all users and loading the mount.json from their home - * - * @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(array($oldId, $this->id)); - while ($row = $stmt->fetch()) { - $storages[$row['id']] = $row['numeric_id']; - } - - if (isset($storages[$this->id]) && isset($storages[$oldId])) { - // if both ids exist, delete the old storage and corresponding filecache entries - \OC\Files\Cache\Storage::remove($oldId); - } else if (isset($storages[$oldId])) { - // if only the old id exists do an update - $stmt = \OC::$server->getDatabaseConnection()->prepare( - 'UPDATE `*PREFIX*storages` SET `id` = ? WHERE `id` = ?' - ); - $stmt->execute(array($this->id, $oldId)); - } - // only the bucket based id may exist, do nothing - } - - /** - * Remove a file or folder - * - * @param string $path - * @return bool - */ - protected function remove($path) { - // remember fileType to reduce http calls - $fileType = $this->filetype($path); - if ($fileType === 'dir') { - return $this->rmdir($path); - } else if ($fileType === 'file') { - return $this->unlink($path); - } else { - return false; - } - } - - public function mkdir($path) { - $path = $this->normalizePath($path); - - if ($this->is_dir($path)) { - return false; - } - - try { - $this->getConnection()->putObject(array( - 'Bucket' => $this->bucket, - 'Key' => $path . '/', - 'Body' => '', - 'ContentType' => 'httpd/unix-directory' - )); - $this->testTimeout(); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - - return true; - } - - public function file_exists($path) { - return $this->filetype($path) !== false; - } - - - public function rmdir($path) { - $path = $this->normalizePath($path); - - if ($this->isRoot($path)) { - return $this->clearBucket(); - } - - if (!$this->file_exists($path)) { - return false; - } - - return $this->batchDelete($path); - } - - protected function clearBucket() { - try { - $this->getConnection()->clearBucket($this->bucket); - return true; - // clearBucket() is not working with Ceph, so if it fails we try the slower approach - } catch (\Exception $e) { - return $this->batchDelete(); - } - return false; - } - - private function batchDelete ($path = null) { - $params = array( - 'Bucket' => $this->bucket - ); - if ($path !== null) { - $params['Prefix'] = $path . '/'; - } - try { - // Since there are no real directories on S3, we need - // to delete all objects prefixed with the path. - do { - // instead of the iterator, manually loop over the list ... - $objects = $this->getConnection()->listObjects($params); - // ... so we can delete the files in batches - $this->getConnection()->deleteObjects(array( - 'Bucket' => $this->bucket, - 'Objects' => $objects['Contents'] - )); - $this->testTimeout(); - // we reached the end when the list is no longer truncated - } while ($objects['IsTruncated']); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - return true; - } - - public function opendir($path) { - $path = $this->normalizePath($path); - - if ($this->isRoot($path)) { - $path = ''; - } else { - $path .= '/'; - } - - try { - $files = array(); - $result = $this->getConnection()->getIterator('ListObjects', array( - 'Bucket' => $this->bucket, - 'Delimiter' => '/', - 'Prefix' => $path - ), array('return_prefixes' => true)); - - foreach ($result as $object) { - if (isset($object['Key']) && $object['Key'] === $path) { - // it's the directory itself, skip - continue; - } - $file = basename( - isset($object['Key']) ? $object['Key'] : $object['Prefix'] - ); - $files[] = $file; - } - - return IteratorDirectory::wrap($files); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - } - - public function stat($path) { - $path = $this->normalizePath($path); - - try { - $stat = array(); - if ($this->is_dir($path)) { - //folders don't really exist - $stat['size'] = -1; //unknown - $stat['mtime'] = time() - $this->rescanDelay * 1000; - } else { - $result = $this->getConnection()->headObject(array( - 'Bucket' => $this->bucket, - 'Key' => $path - )); - - $stat['size'] = $result['ContentLength'] ? $result['ContentLength'] : 0; - if ($result['Metadata']['lastmodified']) { - $stat['mtime'] = strtotime($result['Metadata']['lastmodified']); - } else { - $stat['mtime'] = strtotime($result['LastModified']); - } - } - $stat['atime'] = time(); - - return $stat; - } catch(S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - } - - public function filetype($path) { - $path = $this->normalizePath($path); - - if ($this->isRoot($path)) { - return 'dir'; - } - - try { - if ($this->getConnection()->doesObjectExist($this->bucket, $path)) { - return 'file'; - } - if ($this->getConnection()->doesObjectExist($this->bucket, $path.'/')) { - return 'dir'; - } - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - - return false; - } - - public function unlink($path) { - $path = $this->normalizePath($path); - - if ($this->is_dir($path)) { - return $this->rmdir($path); - } - - try { - $this->getConnection()->deleteObject(array( - 'Bucket' => $this->bucket, - 'Key' => $path - )); - $this->testTimeout(); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - - return true; - } - - public function fopen($path, $mode) { - $path = $this->normalizePath($path); - - switch ($mode) { - case 'r': - case 'rb': - $tmpFile = \OCP\Files::tmpFile(); - self::$tmpFiles[$tmpFile] = $path; - - try { - $this->getConnection()->getObject(array( - 'Bucket' => $this->bucket, - 'Key' => $path, - 'SaveAs' => $tmpFile - )); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - - return fopen($tmpFile, 'r'); - case 'w': - case 'wb': - case 'a': - case 'ab': - case 'r+': - case 'w+': - case 'wb+': - case 'a+': - case 'x': - case 'x+': - case 'c': - case 'c+': - if (strrpos($path, '.') !== false) { - $ext = substr($path, strrpos($path, '.')); - } else { - $ext = ''; - } - $tmpFile = \OCP\Files::tmpFile($ext); - \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); - if ($this->file_exists($path)) { - $source = $this->fopen($path, 'r'); - file_put_contents($tmpFile, $source); - } - self::$tmpFiles[$tmpFile] = $path; - - return fopen('close://' . $tmpFile, $mode); - } - return false; - } - - public function touch($path, $mtime = null) { - $path = $this->normalizePath($path); - - $metadata = array(); - if (is_null($mtime)) { - $mtime = time(); - } - $metadata = [ - 'lastmodified' => gmdate(\Aws\Common\Enum\DateFormat::RFC1123, $mtime) - ]; - - $fileType = $this->filetype($path); - try { - if ($fileType !== false) { - if ($fileType === 'dir' && ! $this->isRoot($path)) { - $path .= '/'; - } - $this->getConnection()->copyObject([ - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey($path), - 'Metadata' => $metadata, - 'CopySource' => $this->bucket . '/' . $path, - 'MetadataDirective' => 'REPLACE', - ]); - $this->testTimeout(); - } else { - $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path); - $this->getConnection()->putObject([ - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey($path), - 'Metadata' => $metadata, - 'Body' => '', - 'ContentType' => $mimeType, - 'MetadataDirective' => 'REPLACE', - ]); - $this->testTimeout(); - } - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - - return true; - } - - public function copy($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); - - if ($this->is_file($path1)) { - try { - $this->getConnection()->copyObject(array( - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey($path2), - 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1) - )); - $this->testTimeout(); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - } else { - $this->remove($path2); - - try { - $this->getConnection()->copyObject(array( - 'Bucket' => $this->bucket, - 'Key' => $path2 . '/', - 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1 . '/') - )); - $this->testTimeout(); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $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); - } - } - } - - return true; - } - - public function rename($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); - - if ($this->is_file($path1)) { - - if ($this->copy($path1, $path2) === false) { - return false; - } - - if ($this->unlink($path1) === false) { - $this->unlink($path2); - return false; - } - } else { - - if ($this->copy($path1, $path2) === false) { - return false; - } - - if ($this->rmdir($path1) === false) { - $this->rmdir($path2); - return false; - } - } - - return true; - } - - public function test() { - $test = $this->getConnection()->getBucketAcl(array( - 'Bucket' => $this->bucket, - )); - if (isset($test) && !is_null($test->getPath('Owner/ID'))) { - return true; - } - return false; - } - - public function getId() { - return $this->id; - } - - /** - * Returns the connection - * - * @return S3Client connected client - * @throws \Exception if connection could not be made - */ - public function getConnection() { - if (!is_null($this->connection)) { - return $this->connection; - } - - $scheme = ($this->params['use_ssl'] === false) ? 'http' : 'https'; - $base_url = $scheme . '://' . $this->params['hostname'] . ':' . $this->params['port'] . '/'; - - $this->connection = S3Client::factory(array( - 'key' => $this->params['key'], - 'secret' => $this->params['secret'], - 'base_url' => $base_url, - 'region' => $this->params['region'], - S3Client::COMMAND_PARAMS => [ - 'PathStyle' => $this->params['use_path_style'], - ], - )); - - if (!$this->connection->isValidBucketName($this->bucket)) { - throw new \Exception("The configured bucket name is invalid."); - } - - if (!$this->connection->doesBucketExist($this->bucket)) { - try { - $this->connection->createBucket(array( - 'Bucket' => $this->bucket - )); - $this->connection->waitUntilBucketExists(array( - 'Bucket' => $this->bucket, - 'waiter.interval' => 1, - 'waiter.max_attempts' => 15 - )); - $this->testTimeout(); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - throw new \Exception('Creation of bucket failed. '.$e->getMessage()); - } - } - - return $this->connection; - } - - public function writeBack($tmpFile) { - if (!isset(self::$tmpFiles[$tmpFile])) { - return false; - } - - try { - $this->getConnection()->putObject(array( - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey(self::$tmpFiles[$tmpFile]), - 'SourceFile' => $tmpFile, - 'ContentType' => \OC::$server->getMimeTypeDetector()->detect($tmpFile), - 'ContentLength' => filesize($tmpFile) - )); - $this->testTimeout(); - - unlink($tmpFile); - } catch (S3Exception $e) { - \OCP\Util::logException('files_external', $e); - return false; - } - } - - /** - * check if curl is installed - */ - public static function checkDependencies() { - return true; - } - -} diff --git a/apps/files_external/lib/api.php b/apps/files_external/lib/api.php deleted file mode 100644 index 50a2f38c65b..00000000000 --- a/apps/files_external/lib/api.php +++ /dev/null @@ -1,87 +0,0 @@ -<?php -/** - * @author Jesús Macias <jmacias@solidgear.es> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files\External; - -class Api { - - /** - * Formats the given mount config to a mount entry. - * - * @param string $mountPoint mount point name, relative to the data dir - * @param array $mountConfig mount config to format - * - * @return array entry - */ - private static function formatMount($mountPoint, $mountConfig) { - // strip "/$user/files" from mount point - $mountPoint = explode('/', trim($mountPoint, '/'), 3); - $mountPoint = $mountPoint[2]; - - // split path from mount point - $path = dirname($mountPoint); - if ($path === '.') { - $path = ''; - } - - $isSystemMount = !$mountConfig['personal']; - - $permissions = \OCP\Constants::PERMISSION_READ; - // personal mounts can be deleted - if (!$isSystemMount) { - $permissions |= \OCP\Constants::PERMISSION_DELETE; - } - - $entry = array( - 'name' => basename($mountPoint), - 'path' => $path, - 'type' => 'dir', - 'backend' => $mountConfig['backend'], - 'scope' => ( $isSystemMount ? 'system' : 'personal' ), - 'permissions' => $permissions, - 'id' => $mountConfig['id'], - 'class' => $mountConfig['class'] - ); - return $entry; - } - - /** - * Returns the mount points visible for this user. - * - * @param array $params - * @return \OC_OCS_Result share information - */ - public static function getUserMounts($params) { - $entries = array(); - $user = \OC::$server->getUserSession()->getUser()->getUID(); - - $mounts = \OC_Mount_Config::getAbsoluteMountPoints($user); - foreach($mounts as $mountPoint => $mount) { - $entries[] = self::formatMount($mountPoint, $mount); - } - - return new \OC_OCS_Result($entries); - } -} diff --git a/apps/files_external/lib/auth/amazons3/accesskey.php b/apps/files_external/lib/auth/amazons3/accesskey.php deleted file mode 100644 index 296ed59a77a..00000000000 --- a/apps/files_external/lib/auth/amazons3/accesskey.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\AmazonS3; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; - -/** - * Amazon S3 access key authentication - */ -class AccessKey extends AuthMechanism { - - const SCHEME_AMAZONS3_ACCESSKEY = 'amazons3_accesskey'; - - public function __construct(IL10N $l) { - $this - ->setIdentifier('amazons3::accesskey') - ->setScheme(self::SCHEME_AMAZONS3_ACCESSKEY) - ->setText($l->t('Access key')) - ->addParameters([ - (new DefinitionParameter('key', $l->t('Access key'))), - (new DefinitionParameter('secret', $l->t('Secret key'))) - ->setType(DefinitionParameter::VALUE_PASSWORD), - ]); - } - -} diff --git a/apps/files_external/lib/auth/builtin.php b/apps/files_external/lib/auth/builtin.php deleted file mode 100644 index 8b43cb459cc..00000000000 --- a/apps/files_external/lib/auth/builtin.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_external\Lib\StorageConfig; - -/** - * Builtin authentication mechanism, for legacy backends - */ -class Builtin extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('builtin::builtin') - ->setScheme(self::SCHEME_BUILTIN) - ->setText($l->t('Builtin')) - ; - } - -} diff --git a/apps/files_external/lib/auth/iuserprovided.php b/apps/files_external/lib/auth/iuserprovided.php deleted file mode 100644 index 6852c804be5..00000000000 --- a/apps/files_external/lib/auth/iuserprovided.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth; - -use OCP\IUser; - -/** - * For auth mechanisms where the user needs to provide credentials - */ -interface IUserProvided { - /** - * @param IUser $user the user for which to save the user provided options - * @param int $mountId the mount id to save the options for - * @param array $options the user provided options - */ - public function saveBackendOptions(IUser $user, $mountId, array $options); -} diff --git a/apps/files_external/lib/auth/nullmechanism.php b/apps/files_external/lib/auth/nullmechanism.php deleted file mode 100644 index 06083729e59..00000000000 --- a/apps/files_external/lib/auth/nullmechanism.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_external\Lib\StorageConfig; - -/** - * Null authentication mechanism - */ -class NullMechanism extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('null::null') - ->setScheme(self::SCHEME_NULL) - ->setText($l->t('None')) - ; - } - -} diff --git a/apps/files_external/lib/auth/oauth1/oauth1.php b/apps/files_external/lib/auth/oauth1/oauth1.php deleted file mode 100644 index dd83c9a6a69..00000000000 --- a/apps/files_external/lib/auth/oauth1/oauth1.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\OAuth1; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; - -/** - * OAuth1 authentication - */ -class OAuth1 extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('oauth1::oauth1') - ->setScheme(self::SCHEME_OAUTH1) - ->setText($l->t('OAuth1')) - ->addParameters([ - (new DefinitionParameter('configured', 'configured')) - ->setType(DefinitionParameter::VALUE_HIDDEN), - (new DefinitionParameter('app_key', $l->t('App key'))), - (new DefinitionParameter('app_secret', $l->t('App secret'))) - ->setType(DefinitionParameter::VALUE_PASSWORD), - (new DefinitionParameter('token', 'token')) - ->setType(DefinitionParameter::VALUE_HIDDEN), - (new DefinitionParameter('token_secret', 'token_secret')) - ->setType(DefinitionParameter::VALUE_HIDDEN), - ]) - ->setCustomJs('oauth1') - ; - } - -} diff --git a/apps/files_external/lib/auth/oauth2/oauth2.php b/apps/files_external/lib/auth/oauth2/oauth2.php deleted file mode 100644 index c89007b52ba..00000000000 --- a/apps/files_external/lib/auth/oauth2/oauth2.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\OAuth2; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; - -/** - * OAuth2 authentication - */ -class OAuth2 extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('oauth2::oauth2') - ->setScheme(self::SCHEME_OAUTH2) - ->setText($l->t('OAuth2')) - ->addParameters([ - (new DefinitionParameter('configured', 'configured')) - ->setType(DefinitionParameter::VALUE_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), - ]) - ->setCustomJs('oauth2') - ; - } - -} diff --git a/apps/files_external/lib/auth/openstack/openstack.php b/apps/files_external/lib/auth/openstack/openstack.php deleted file mode 100644 index 80bbb1299f7..00000000000 --- a/apps/files_external/lib/auth/openstack/openstack.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\OpenStack; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; - -/** - * OpenStack Keystone authentication - */ -class OpenStack extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('openstack::openstack') - ->setScheme(self::SCHEME_OPENSTACK) - ->setText($l->t('OpenStack')) - ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))), - (new DefinitionParameter('password', $l->t('Password'))) - ->setType(DefinitionParameter::VALUE_PASSWORD), - (new DefinitionParameter('tenant', $l->t('Tenant name'))), - (new DefinitionParameter('url', $l->t('Identity endpoint URL'))), - ]) - ; - } - -} diff --git a/apps/files_external/lib/auth/openstack/rackspace.php b/apps/files_external/lib/auth/openstack/rackspace.php deleted file mode 100644 index c968321ca6c..00000000000 --- a/apps/files_external/lib/auth/openstack/rackspace.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\OpenStack; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; - -/** - * Rackspace authentication - */ -class Rackspace extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('openstack::rackspace') - ->setScheme(self::SCHEME_OPENSTACK) - ->setText($l->t('Rackspace')) - ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))), - (new DefinitionParameter('key', $l->t('API key'))) - ->setType(DefinitionParameter::VALUE_PASSWORD), - ]) - ; - } - -} diff --git a/apps/files_external/lib/auth/password/password.php b/apps/files_external/lib/auth/password/password.php deleted file mode 100644 index 3b1942cc4a8..00000000000 --- a/apps/files_external/lib/auth/password/password.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\Password; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; - -/** - * Basic password authentication mechanism - */ -class Password extends AuthMechanism { - - public function __construct(IL10N $l) { - $this - ->setIdentifier('password::password') - ->setScheme(self::SCHEME_PASSWORD) - ->setText($l->t('Username and password')) - ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))), - (new DefinitionParameter('password', $l->t('Password'))) - ->setType(DefinitionParameter::VALUE_PASSWORD), - ]); - } - -} diff --git a/apps/files_external/lib/auth/password/sessioncredentials.php b/apps/files_external/lib/auth/password/sessioncredentials.php deleted file mode 100644 index 429c549d80a..00000000000 --- a/apps/files_external/lib/auth/password/sessioncredentials.php +++ /dev/null @@ -1,86 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\Password; - -use \OCP\IUser; -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Lib\StorageConfig; -use \OCP\ISession; -use \OCP\Security\ICrypto; -use \OCP\Files\Storage; -use \OCA\Files_External\Lib\SessionStorageWrapper; -use \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; - -/** - * Username and password from login credentials, saved in session - */ -class SessionCredentials extends AuthMechanism { - - /** @var ISession */ - protected $session; - - /** @var ICrypto */ - protected $crypto; - - public function __construct(IL10N $l, ISession $session, ICrypto $crypto) { - $this->session = $session; - $this->crypto = $crypto; - - $this - ->setIdentifier('password::sessioncredentials') - ->setScheme(self::SCHEME_PASSWORD) - ->setText($l->t('Log-in credentials, save in session')) - ->addParameters([ - ]) - ; - - \OCP\Util::connectHook('OC_User', 'post_login', $this, 'authenticate'); - } - - /** - * Hook listener on post login - * - * @param array $params - */ - public function authenticate(array $params) { - $this->session->set('password::sessioncredentials/credentials', $this->crypto->encrypt(json_encode($params))); - } - - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { - $encrypted = $this->session->get('password::sessioncredentials/credentials'); - if (!isset($encrypted)) { - throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved'); - } - - $credentials = json_decode($this->crypto->decrypt($encrypted), true); - $storage->setBackendOption('user', $this->session->get('loginname')); - $storage->setBackendOption('password', $credentials['password']); - } - - public function wrapStorage(Storage $storage) { - return new SessionStorageWrapper(['storage' => $storage]); - } - -} diff --git a/apps/files_external/lib/auth/publickey/rsa.php b/apps/files_external/lib/auth/publickey/rsa.php deleted file mode 100644 index 9045f6818f9..00000000000 --- a/apps/files_external/lib/auth/publickey/rsa.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Auth\PublicKey; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Lib\StorageConfig; -use \OCP\IConfig; -use OCP\IUser; -use \phpseclib\Crypt\RSA as RSACrypt; - -/** - * RSA public key authentication - */ -class RSA extends AuthMechanism { - - const CREATE_KEY_BITS = 1024; - - /** @var IConfig */ - private $config; - - public function __construct(IL10N $l, IConfig $config) { - $this->config = $config; - - $this - ->setIdentifier('publickey::rsa') - ->setScheme(self::SCHEME_PUBLICKEY) - ->setText($l->t('RSA public key')) - ->addParameters([ - (new DefinitionParameter('user', $l->t('Username'))), - (new DefinitionParameter('public_key', $l->t('Public key'))), - (new DefinitionParameter('private_key', 'private_key')) - ->setType(DefinitionParameter::VALUE_HIDDEN), - ]) - ->setCustomJs('public_key') - ; - } - - 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'); - } - $storage->setBackendOption('public_key_auth', $auth); - } - - /** - * Generate a keypair - * - * @return array ['privatekey' => $privateKey, 'publickey' => $publicKey] - */ - public function createKey() { - $rsa = new RSACrypt(); - $rsa->setPublicKeyFormat(RSACrypt::PUBLIC_FORMAT_OPENSSH); - $rsa->setPassword($this->config->getSystemValue('secret', '')); - - return $rsa->createKey(self::CREATE_KEY_BITS); - } - -} diff --git a/apps/files_external/lib/backend/amazons3.php b/apps/files_external/lib/backend/amazons3.php deleted file mode 100644 index b2dedc10e4a..00000000000 --- a/apps/files_external/lib/backend/amazons3.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\AmazonS3\AccessKey; - -class AmazonS3 extends Backend { - - use LegacyDependencyCheckPolyfill; - - public function __construct(IL10N $l, AccessKey $legacyAuth) { - $this - ->setIdentifier('amazons3') - ->addIdentifierAlias('\OC\Files\Storage\AmazonS3') // legacy compat - ->setStorageClass('\OC\Files\Storage\AmazonS3') - ->setText($l->t('Amazon S3')) - ->addParameters([ - (new DefinitionParameter('bucket', $l->t('Bucket'))), - (new DefinitionParameter('hostname', $l->t('Hostname'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('port', $l->t('Port'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('region', $l->t('Region'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('use_ssl', $l->t('Enable SSL'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), - (new DefinitionParameter('use_path_style', $l->t('Enable Path Style'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), - ]) - ->addAuthScheme(AccessKey::SCHEME_AMAZONS3_ACCESSKEY) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - -} diff --git a/apps/files_external/lib/backend/dav.php b/apps/files_external/lib/backend/dav.php deleted file mode 100644 index c6e9630be9e..00000000000 --- a/apps/files_external/lib/backend/dav.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\Password\Password; - -class DAV extends Backend { - - use LegacyDependencyCheckPolyfill; - - public function __construct(IL10N $l, Password $legacyAuth) { - $this - ->setIdentifier('dav') - ->addIdentifierAlias('\OC\Files\Storage\DAV') // legacy compat - ->setStorageClass('\OC\Files\Storage\DAV') - ->setText($l->t('WebDAV')) - ->addParameters([ - (new DefinitionParameter('host', $l->t('URL'))), - (new DefinitionParameter('root', $l->t('Remote subfolder'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('secure', $l->t('Secure https://'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), - ]) - ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - -} diff --git a/apps/files_external/lib/backend/dropbox.php b/apps/files_external/lib/backend/dropbox.php deleted file mode 100644 index 7a414731192..00000000000 --- a/apps/files_external/lib/backend/dropbox.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\OAuth1\OAuth1; - -class Dropbox extends Backend { - - use LegacyDependencyCheckPolyfill; - - public function __construct(IL10N $l, OAuth1 $legacyAuth) { - $this - ->setIdentifier('dropbox') - ->addIdentifierAlias('\OC\Files\Storage\Dropbox') // legacy compat - ->setStorageClass('\OC\Files\Storage\Dropbox') - ->setText($l->t('Dropbox')) - ->addParameters([ - // all parameters handled in OAuth1 mechanism - ]) - ->addAuthScheme(AuthMechanism::SCHEME_OAUTH1) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - -} diff --git a/apps/files_external/lib/backend/ftp.php b/apps/files_external/lib/backend/ftp.php deleted file mode 100644 index b2b83a27405..00000000000 --- a/apps/files_external/lib/backend/ftp.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\Password\Password; - -class FTP extends Backend { - - use LegacyDependencyCheckPolyfill; - - public function __construct(IL10N $l, Password $legacyAuth) { - $this - ->setIdentifier('ftp') - ->addIdentifierAlias('\OC\Files\Storage\FTP') // legacy compat - ->setStorageClass('\OC\Files\Storage\FTP') - ->setText($l->t('FTP')) - ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), - (new DefinitionParameter('root', $l->t('Remote subfolder'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('secure', $l->t('Secure ftps://'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), - ]) - ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - -} diff --git a/apps/files_external/lib/backend/google.php b/apps/files_external/lib/backend/google.php deleted file mode 100644 index 93a8cd2177d..00000000000 --- a/apps/files_external/lib/backend/google.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\OAuth2\OAuth2; - -class Google extends Backend { - - use LegacyDependencyCheckPolyfill; - - public function __construct(IL10N $l, OAuth2 $legacyAuth) { - $this - ->setIdentifier('googledrive') - ->addIdentifierAlias('\OC\Files\Storage\Google') // legacy compat - ->setStorageClass('\OC\Files\Storage\Google') - ->setText($l->t('Google Drive')) - ->addParameters([ - // all parameters handled in OAuth2 mechanism - ]) - ->addAuthScheme(AuthMechanism::SCHEME_OAUTH2) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - -} diff --git a/apps/files_external/lib/backend/local.php b/apps/files_external/lib/backend/local.php deleted file mode 100644 index 1db707e7247..00000000000 --- a/apps/files_external/lib/backend/local.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\Auth\NullMechanism; - -class Local extends Backend { - - public function __construct(IL10N $l, NullMechanism $legacyAuth) { - $this - ->setIdentifier('local') - ->addIdentifierAlias('\OC\Files\Storage\Local') // legacy compat - ->setStorageClass('\OC\Files\Storage\Local') - ->setText($l->t('Local')) - ->addParameters([ - (new DefinitionParameter('datadir', $l->t('Location'))), - ]) - ->setAllowedVisibility(BackendService::VISIBILITY_ADMIN) - ->setPriority(BackendService::PRIORITY_DEFAULT + 50) - ->addAuthScheme(AuthMechanism::SCHEME_NULL) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - -} diff --git a/apps/files_external/lib/backend/owncloud.php b/apps/files_external/lib/backend/owncloud.php deleted file mode 100644 index e7da328c5f1..00000000000 --- a/apps/files_external/lib/backend/owncloud.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; - -use \OCA\Files_External\Lib\Auth\Password\Password; - -class OwnCloud extends Backend { - - public function __construct(IL10N $l, Password $legacyAuth) { - $this - ->setIdentifier('owncloud') - ->addIdentifierAlias('\OC\Files\Storage\OwnCloud') // legacy compat - ->setStorageClass('\OC\Files\Storage\OwnCloud') - ->setText($l->t('ownCloud')) - ->addParameters([ - (new DefinitionParameter('host', $l->t('URL'))), - (new DefinitionParameter('root', $l->t('Remote subfolder'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('secure', $l->t('Secure https://'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), - ]) - ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - -} diff --git a/apps/files_external/lib/backend/sftp.php b/apps/files_external/lib/backend/sftp.php deleted file mode 100644 index 3e5ecb90131..00000000000 --- a/apps/files_external/lib/backend/sftp.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; - -use \OCA\Files_External\Lib\Auth\Password\Password; - -class SFTP extends Backend { - - public function __construct(IL10N $l, Password $legacyAuth) { - $this - ->setIdentifier('sftp') - ->addIdentifierAlias('\OC\Files\Storage\SFTP') // legacy compat - ->setStorageClass('\OC\Files\Storage\SFTP') - ->setText($l->t('SFTP')) - ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), - (new DefinitionParameter('root', $l->t('Root'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - ]) - ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) - ->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - -} diff --git a/apps/files_external/lib/backend/sftp_key.php b/apps/files_external/lib/backend/sftp_key.php deleted file mode 100644 index 58dddedf784..00000000000 --- a/apps/files_external/lib/backend/sftp_key.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\Auth\PublicKey\RSA; -use \OCA\Files_External\Lib\Backend\SFTP; - -class SFTP_Key extends Backend { - - public function __construct(IL10N $l, RSA $legacyAuth, SFTP $sftpBackend) { - $this - ->setIdentifier('\OC\Files\Storage\SFTP_Key') - ->setStorageClass('\OC\Files\Storage\SFTP') - ->setText($l->t('SFTP with secret key login')) - ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), - (new DefinitionParameter('root', $l->t('Remote subfolder'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - ]) - ->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY) - ->setLegacyAuthMechanism($legacyAuth) - ->deprecateTo($sftpBackend) - ; - } - -} diff --git a/apps/files_external/lib/backend/smb.php b/apps/files_external/lib/backend/smb.php deleted file mode 100644 index 9b71636936a..00000000000 --- a/apps/files_external/lib/backend/smb.php +++ /dev/null @@ -1,69 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -use \OCA\Files_External\Lib\Auth\Password\Password; -use 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('\OC\Files\Storage\SMB') - ->setText($l->t('SMB / CIFS')) - ->addParameters([ - (new DefinitionParameter('host', $l->t('Host'))), - (new DefinitionParameter('share', $l->t('Share'))), - (new DefinitionParameter('root', $l->t('Remote subfolder'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('domain', $l->t('Domain'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - ]) - ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) - ->setLegacyAuthMechanism($legacyAuth) - ; - } - - /** - * @param StorageConfig $storage - * @param IUser $user - */ - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { - $user = $storage->getBackendOption('user'); - if ($domain = $storage->getBackendOption('domain')) { - $storage->setBackendOption('user', $domain.'\\'.$user); - } - } - -} diff --git a/apps/files_external/lib/backend/smb_oc.php b/apps/files_external/lib/backend/smb_oc.php deleted file mode 100644 index ba38754ce5a..00000000000 --- a/apps/files_external/lib/backend/smb_oc.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\Auth\Password\SessionCredentials; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; -use \OCA\Files_External\Lib\Backend\SMB; -use OCP\IUser; - -/** - * Deprecated SMB_OC class - use SMB with the password::sessioncredentials auth mechanism - */ -class SMB_OC extends Backend { - - use LegacyDependencyCheckPolyfill; - - public function __construct(IL10N $l, SessionCredentials $legacyAuth, SMB $smbBackend) { - $this - ->setIdentifier('\OC\Files\Storage\SMB_OC') - ->setStorageClass('\OC\Files\Storage\SMB') - ->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'))) - ->setType(DefinitionParameter::VALUE_BOOLEAN), - (new DefinitionParameter('share', $l->t('Share'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('root', $l->t('Remote subfolder'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - ]) - ->setPriority(BackendService::PRIORITY_DEFAULT - 10) - ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD) - ->setLegacyAuthMechanism($legacyAuth) - ->deprecateTo($smbBackend) - ; - } - - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { - $username_as_share = ($storage->getBackendOption('username_as_share') === true); - - if ($username_as_share) { - $share = '/' . $storage->getBackendOption('user'); - $storage->setBackendOption('share', $share); - } - } - -} diff --git a/apps/files_external/lib/backend/swift.php b/apps/files_external/lib/backend/swift.php deleted file mode 100644 index d6e4ac12f9a..00000000000 --- a/apps/files_external/lib/backend/swift.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib\Backend; - -use \OCP\IL10N; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\Auth\AuthMechanism; -use \OCA\Files_External\Service\BackendService; -use \OCA\Files_External\Lib\Auth\OpenStack\OpenStack; -use \OCA\Files_External\Lib\Auth\OpenStack\Rackspace; -use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill; - -class Swift extends Backend { - - use LegacyDependencyCheckPolyfill; - - public function __construct(IL10N $l, OpenStack $openstackAuth, Rackspace $rackspaceAuth) { - $this - ->setIdentifier('swift') - ->addIdentifierAlias('\OC\Files\Storage\Swift') // legacy compat - ->setStorageClass('\OC\Files\Storage\Swift') - ->setText($l->t('OpenStack Object Storage')) - ->addParameters([ - (new DefinitionParameter('service_name', $l->t('Service name'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('region', $l->t('Region'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - (new DefinitionParameter('bucket', $l->t('Bucket'))), - (new DefinitionParameter('timeout', $l->t('Request timeout (seconds)'))) - ->setFlag(DefinitionParameter::FLAG_OPTIONAL), - ]) - ->addAuthScheme(AuthMechanism::SCHEME_OPENSTACK) - ->setLegacyAuthMechanismCallback(function(array $params) use ($openstackAuth, $rackspaceAuth) { - if (isset($params['options']['key']) && $params['options']['key']) { - return $rackspaceAuth; - } - return $openstackAuth; - }) - ; - } - -} diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php deleted file mode 100644 index 70f8550f39b..00000000000 --- a/apps/files_external/lib/config.php +++ /dev/null @@ -1,413 +0,0 @@ -<?php -/** - * @author Andreas Fischer <bantu@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <schiessle@owncloud.com> - * @author Frank Karlitschek <frank@owncloud.org> - * @author Jesús Macias <jmacias@solidgear.es> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -use phpseclib\Crypt\AES; -use \OCA\Files_External\Appinfo\Application; -use \OCA\Files_External\Lib\Backend\LegacyBackend; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\Backend\Backend; -use \OCP\Files\StorageNotAvailableException; - -/** - * Class to configure mount.json globally and for users - */ -class OC_Mount_Config { - // TODO: make this class non-static and give it a proper namespace - - const MOUNT_TYPE_GLOBAL = 'global'; - const MOUNT_TYPE_GROUP = 'group'; - const MOUNT_TYPE_USER = 'user'; - const MOUNT_TYPE_PERSONAL = 'personal'; - - // whether to skip backend test (for unit tests, as this static class is not mockable) - public static $skipTest = false; - - /** @var Application */ - public static $app; - - /** - * @param string $class - * @param array $definition - * @return bool - * @deprecated 8.2.0 use \OCA\Files_External\Service\BackendService::registerBackend() - */ - public static function registerBackend($class, $definition) { - $backendService = self::$app->getContainer()->query('OCA\Files_External\Service\BackendService'); - $auth = self::$app->getContainer()->query('OCA\Files_External\Lib\Auth\Builtin'); - - $backendService->registerBackend(new LegacyBackend($class, $definition, $auth)); - - return true; - } - - /** - * Returns the mount points for the given user. - * The mount point is relative to the data directory. - * - * @param string $uid user - * @return array of mount point string as key, mountpoint config as value - * - * @deprecated 8.2.0 use UserGlobalStoragesService::getStorages() and UserStoragesService::getStorages() - */ - public static function getAbsoluteMountPoints($uid) { - $mountPoints = array(); - - $userGlobalStoragesService = self::$app->getContainer()->query('OCA\Files_External\Service\UserGlobalStoragesService'); - $userStoragesService = self::$app->getContainer()->query('OCA\Files_External\Service\UserStoragesService'); - $user = self::$app->getContainer()->query('OCP\IUserManager')->get($uid); - - $userGlobalStoragesService->setUser($user); - $userStoragesService->setUser($user); - - foreach ($userGlobalStoragesService->getStorages() as $storage) { - /** @var \OCA\Files_external\Lib\StorageConfig $storage */ - $mountPoint = '/'.$uid.'/files'.$storage->getMountPoint(); - $mountEntry = self::prepareMountPointEntry($storage, false); - foreach ($mountEntry['options'] as &$option) { - $option = self::setUserVars($uid, $option); - } - $mountPoints[$mountPoint] = $mountEntry; - } - - foreach ($userStoragesService->getStorages() as $storage) { - $mountPoint = '/'.$uid.'/files'.$storage->getMountPoint(); - $mountEntry = self::prepareMountPointEntry($storage, true); - foreach ($mountEntry['options'] as &$option) { - $option = self::setUserVars($uid, $option); - } - $mountPoints[$mountPoint] = $mountEntry; - } - - $userGlobalStoragesService->resetUser(); - $userStoragesService->resetUser(); - - return $mountPoints; - } - - /** - * Get the system mount points - * - * @return array - * - * @deprecated 8.2.0 use GlobalStoragesService::getStorages() - */ - public static function getSystemMountPoints() { - $mountPoints = []; - $service = self::$app->getContainer()->query('OCA\Files_External\Service\GlobalStoragesService'); - - foreach ($service->getStorages() as $storage) { - $mountPoints[] = self::prepareMountPointEntry($storage, false); - } - - return $mountPoints; - } - - /** - * Get the personal mount points of the current user - * - * @return array - * - * @deprecated 8.2.0 use UserStoragesService::getStorages() - */ - public static function getPersonalMountPoints() { - $mountPoints = []; - $service = self::$app->getContainer()->query('OCA\Files_External\Service\UserStoragesService'); - - foreach ($service->getStorages() as $storage) { - $mountPoints[] = self::prepareMountPointEntry($storage, true); - } - - return $mountPoints; - } - - /** - * Convert a StorageConfig to the legacy mountPoints array format - * There's a lot of extra information in here, to satisfy all of the legacy functions - * - * @param StorageConfig $storage - * @param bool $isPersonal - * @return array - */ - private static function prepareMountPointEntry(StorageConfig $storage, $isPersonal) { - $mountEntry = []; - - $mountEntry['mountpoint'] = substr($storage->getMountPoint(), 1); // remove leading slash - $mountEntry['class'] = $storage->getBackend()->getIdentifier(); - $mountEntry['backend'] = $storage->getBackend()->getText(); - $mountEntry['authMechanism'] = $storage->getAuthMechanism()->getIdentifier(); - $mountEntry['personal'] = $isPersonal; - $mountEntry['options'] = self::decryptPasswords($storage->getBackendOptions()); - $mountEntry['mountOptions'] = $storage->getMountOptions(); - $mountEntry['priority'] = $storage->getPriority(); - $mountEntry['applicable'] = [ - 'groups' => $storage->getApplicableGroups(), - 'users' => $storage->getApplicableUsers(), - ]; - // if mountpoint is applicable to all users the old API expects ['all'] - if (empty($mountEntry['applicable']['groups']) && empty($mountEntry['applicable']['users'])) { - $mountEntry['applicable']['users'] = ['all']; - } - - $mountEntry['id'] = $storage->getId(); - - return $mountEntry; - } - - /** - * fill in the correct values for $user - * - * @param string $user user value - * @param string|array $input - * @return string - */ - public static function setUserVars($user, $input) { - if (is_array($input)) { - foreach ($input as &$value) { - if (is_string($value)) { - $value = str_replace('$user', $user, $value); - } - } - } else { - if (is_string($input)) { - $input = str_replace('$user', $user, $input); - } - } - return $input; - } - - /** - * Test connecting using the given backend configuration - * - * @param string $class backend class name - * @param array $options backend configuration options - * @param boolean $isPersonal - * @return int see self::STATUS_* - * @throws Exception - */ - public static function getBackendStatus($class, $options, $isPersonal) { - if (self::$skipTest) { - return StorageNotAvailableException::STATUS_SUCCESS; - } - foreach ($options as &$option) { - $option = self::setUserVars(OCP\User::getUser(), $option); - } - if (class_exists($class)) { - try { - /** @var \OC\Files\Storage\Common $storage */ - $storage = new $class($options); - - try { - $result = $storage->test($isPersonal); - $storage->setAvailability($result); - if ($result) { - return StorageNotAvailableException::STATUS_SUCCESS; - } - } catch (\Exception $e) { - $storage->setAvailability(false); - throw $e; - } - } catch (Exception $exception) { - \OCP\Util::logException('files_external', $exception); - throw $exception; - } - } - return StorageNotAvailableException::STATUS_ERROR; - } - - /** - * Read the mount points in the config file into an array - * - * @param string|null $user If not null, personal for $user, otherwise system - * @return array - */ - public static function readData($user = null) { - if (isset($user)) { - $jsonFile = \OC::$server->getUserManager()->get($user)->getHome() . '/mount.json'; - } else { - $config = \OC::$server->getConfig(); - $datadir = $config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/'); - $jsonFile = $config->getSystemValue('mount_file', $datadir . '/mount.json'); - } - if (is_file($jsonFile)) { - $mountPoints = json_decode(file_get_contents($jsonFile), true); - if (is_array($mountPoints)) { - return $mountPoints; - } - } - return array(); - } - - /** - * Get backend dependency message - * TODO: move into AppFramework along with templates - * - * @param Backend[] $backends - * @return string - */ - public static function dependencyMessage($backends) { - $l = \OC::$server->getL10N('files_external'); - $message = ''; - $dependencyGroups = []; - - foreach ($backends as $backend) { - foreach ($backend->checkDependencies() as $dependency) { - if ($message = $dependency->getMessage()) { - $message .= '<br />' . $l->t('<b>Note:</b> ') . $message; - } else { - $dependencyGroups[$dependency->getDependency()][] = $backend; - } - } - } - - foreach ($dependencyGroups as $module => $dependants) { - $backends = implode(', ', array_map(function($backend) { - return '<i>' . $backend->getText() . '</i>'; - }, $dependants)); - $message .= '<br />' . OC_Mount_Config::getSingleDependencyMessage($l, $module, $backends); - } - - return $message; - } - - /** - * Returns a dependency missing message - * - * @param \OCP\IL10N $l - * @param string $module - * @param string $backend - * @return string - */ - private static function getSingleDependencyMessage(\OCP\IL10N $l, $module, $backend) { - switch (strtolower($module)) { - case 'curl': - return (string)$l->t('<b>Note:</b> The cURL support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', $backend); - case 'ftp': - return (string)$l->t('<b>Note:</b> The FTP support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', $backend); - default: - return (string)$l->t('<b>Note:</b> "%s" is not installed. Mounting of %s is not possible. Please ask your system administrator to install it.', array($module, $backend)); - } - } - - /** - * Encrypt passwords in the given config options - * - * @param array $options mount options - * @return array updated options - */ - public static function encryptPasswords($options) { - if (isset($options['password'])) { - $options['password_encrypted'] = self::encryptPassword($options['password']); - // do not unset the password, we want to keep the keys order - // on load... because that's how the UI currently works - $options['password'] = ''; - } - return $options; - } - - /** - * Decrypt passwords in the given config options - * - * @param array $options mount options - * @return array updated options - */ - public static function decryptPasswords($options) { - // note: legacy options might still have the unencrypted password in the "password" field - if (isset($options['password_encrypted'])) { - $options['password'] = self::decryptPassword($options['password_encrypted']); - unset($options['password_encrypted']); - } - return $options; - } - - /** - * Encrypt a single password - * - * @param string $password plain text password - * @return string encrypted password - */ - private static function encryptPassword($password) { - $cipher = self::getCipher(); - $iv = \OCP\Util::generateRandomBytes(16); - $cipher->setIV($iv); - return base64_encode($iv . $cipher->encrypt($password)); - } - - /** - * Decrypts a single password - * - * @param string $encryptedPassword encrypted password - * @return string plain text password - */ - private static function decryptPassword($encryptedPassword) { - $cipher = self::getCipher(); - $binaryPassword = base64_decode($encryptedPassword); - $iv = substr($binaryPassword, 0, 16); - $cipher->setIV($iv); - $binaryPassword = substr($binaryPassword, 16); - return $cipher->decrypt($binaryPassword); - } - - /** - * Returns the encryption cipher - * - * @return AES - */ - private static function getCipher() { - $cipher = new AES(AES::MODE_CBC); - $cipher->setKey(\OC::$server->getConfig()->getSystemValue('passwordsalt', null)); - return $cipher; - } - - /** - * Computes a hash based on the given configuration. - * This is mostly used to find out whether configurations - * are the same. - * - * @param array $config - * @return string - */ - public static function makeConfigHash($config) { - $data = json_encode( - array( - 'c' => $config['backend'], - 'a' => $config['authMechanism'], - 'm' => $config['mountpoint'], - 'o' => $config['options'], - 'p' => isset($config['priority']) ? $config['priority'] : -1, - 'mo' => isset($config['mountOptions']) ? $config['mountOptions'] : [], - ) - ); - return hash('md5', $data); - } -} diff --git a/apps/files_external/lib/config/configadapter.php b/apps/files_external/lib/config/configadapter.php deleted file mode 100644 index a19a111d3d9..00000000000 --- a/apps/files_external/lib/config/configadapter.php +++ /dev/null @@ -1,183 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Config; - -use OC\Files\Storage\Wrapper\Availability; -use OCA\Files_external\Migration\StorageMigrator; -use OCP\Files\Storage; -use OC\Files\Mount\MountPoint; -use OCP\Files\Storage\IStorageFactory; -use OCA\Files_External\Lib\PersonalMount; -use OCP\Files\Config\IMountProvider; -use OCP\IUser; -use OCA\Files_external\Service\UserStoragesService; -use OCA\Files_External\Service\UserGlobalStoragesService; -use OCA\Files_External\Lib\StorageConfig; -use OC\Files\Storage\FailedStorage; -use OCP\Files\StorageNotAvailableException; - -/** - * Make the old files_external config work with the new public mount config api - */ -class ConfigAdapter implements IMountProvider { - - /** @var UserStoragesService */ - private $userStoragesService; - - /** @var UserGlobalStoragesService */ - private $userGlobalStoragesService; - /** @var StorageMigrator */ - private $migrator; - - /** - * @param UserStoragesService $userStoragesService - * @param UserGlobalStoragesService $userGlobalStoragesService - * @param StorageMigrator $migrator - */ - public function __construct( - UserStoragesService $userStoragesService, - UserGlobalStoragesService $userGlobalStoragesService, - StorageMigrator $migrator - ) { - $this->userStoragesService = $userStoragesService; - $this->userGlobalStoragesService = $userGlobalStoragesService; - $this->migrator = $migrator; - } - - /** - * Process storage ready for mounting - * - * @param StorageConfig $storage - * @param IUser $user - */ - private function prepareStorageConfig(StorageConfig &$storage, IUser $user) { - foreach ($storage->getBackendOptions() as $option => $value) { - $storage->setBackendOption($option, \OC_Mount_Config::setUserVars( - $user->getUID(), $value - )); - } - - $objectStore = $storage->getBackendOption('objectstore'); - if ($objectStore) { - $objectClass = $objectStore['class']; - if (!is_subclass_of($objectClass, '\OCP\Files\ObjectStore\IObjectStore')) { - throw new \InvalidArgumentException('Invalid object store'); - } - $storage->setBackendOption('objectstore', new $objectClass($objectStore)); - } - - $storage->getAuthMechanism()->manipulateStorageConfig($storage, $user); - $storage->getBackend()->manipulateStorageConfig($storage, $user); - } - - /** - * Construct the storage implementation - * - * @param StorageConfig $storageConfig - * @return Storage - */ - private function constructStorage(StorageConfig $storageConfig) { - $class = $storageConfig->getBackend()->getStorageClass(); - $storage = new $class($storageConfig->getBackendOptions()); - - // auth mechanism should fire first - $storage = $storageConfig->getBackend()->wrapStorage($storage); - $storage = $storageConfig->getAuthMechanism()->wrapStorage($storage); - - return $storage; - } - - /** - * Get all mountpoints applicable for the user - * - * @param \OCP\IUser $user - * @param \OCP\Files\Storage\IStorageFactory $loader - * @return \OCP\Files\Mount\IMountPoint[] - */ - public function getMountsForUser(IUser $user, IStorageFactory $loader) { - $this->migrator->migrateUser($user); - - $mounts = []; - - $this->userStoragesService->setUser($user); - $this->userGlobalStoragesService->setUser($user); - - foreach ($this->userGlobalStoragesService->getUniqueStorages() as $storage) { - try { - $this->prepareStorageConfig($storage, $user); - $impl = $this->constructStorage($storage); - } catch (\Exception $e) { - // propagate exception into filesystem - $impl = new FailedStorage(['exception' => $e]); - } - - try { - $availability = $impl->getAvailability(); - if (!$availability['available'] && !Availability::shouldRecheck($availability)) { - $impl = new FailedStorage([ - 'exception' => new StorageNotAvailableException('Storage with mount id ' . $storage->getId() . ' is not available') - ]); - } - } catch (\Exception $e) { - // propagate exception into filesystem - $impl = new FailedStorage(['exception' => $e]); - } - - $mount = new MountPoint( - $impl, - '/' . $user->getUID() . '/files' . $storage->getMountPoint(), - null, - $loader, - $storage->getMountOptions() - ); - $mounts[$storage->getMountPoint()] = $mount; - } - - foreach ($this->userStoragesService->getStorages() as $storage) { - try { - $this->prepareStorageConfig($storage, $user); - $impl = $this->constructStorage($storage); - } catch (\Exception $e) { - // propagate exception into filesystem - $impl = new FailedStorage(['exception' => $e]); - } - - $mount = new PersonalMount( - $this->userStoragesService, - $storage->getId(), - $impl, - '/' . $user->getUID() . '/files' . $storage->getMountPoint(), - null, - $loader, - $storage->getMountOptions() - ); - $mounts[$storage->getMountPoint()] = $mount; - } - - $this->userStoragesService->resetUser(); - $this->userGlobalStoragesService->resetUser(); - - return $mounts; - } -} diff --git a/apps/files_external/lib/definitionparameter.php b/apps/files_external/lib/definitionparameter.php deleted file mode 100644 index 16c07f4b8cc..00000000000 --- a/apps/files_external/lib/definitionparameter.php +++ /dev/null @@ -1,193 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -/** - * Parameter for an external storage definition - */ -class DefinitionParameter implements \JsonSerializable { - - /** Value constants */ - const VALUE_TEXT = 0; - const VALUE_BOOLEAN = 1; - const VALUE_PASSWORD = 2; - const VALUE_HIDDEN = 3; - - /** Flag constants */ - const FLAG_NONE = 0; - const FLAG_OPTIONAL = 1; - const FLAG_USER_PROVIDED = 2; - - /** @var string name of parameter */ - private $name; - - /** @var string human-readable parameter text */ - private $text; - - /** @var int value type, see self::VALUE_* constants */ - private $type = self::VALUE_TEXT; - - /** @var int flags, see self::FLAG_* constants */ - private $flags = self::FLAG_NONE; - - /** - * @param string $name - * @param string $text - */ - public function __construct($name, $text) { - $this->name = $name; - $this->text = $text; - } - - /** - * @return string - */ - public function getName() { - return $this->name; - } - - /** - * @return string - */ - public function getText() { - return $this->text; - } - - /** - * Get value type - * - * @return int - */ - public function getType() { - return $this->type; - } - - /** - * Set value type - * - * @param int $type - * @return self - */ - public function setType($type) { - $this->type = $type; - return $this; - } - - /** - * @return string - */ - public function getTypeName() { - switch ($this->type) { - case self::VALUE_BOOLEAN: - return 'boolean'; - case self::VALUE_TEXT: - return 'text'; - case self::VALUE_PASSWORD: - return 'password'; - default: - return 'unknown'; - } - } - - /** - * @return int - */ - public function getFlags() { - return $this->flags; - } - - /** - * @param int $flags - * @return self - */ - public function setFlags($flags) { - $this->flags = $flags; - return $this; - } - - /** - * @param int $flag - * @return self - */ - public function setFlag($flag) { - $this->flags |= $flag; - return $this; - } - - /** - * @param int $flag - * @return bool - */ - public function isFlagSet($flag) { - return (bool)($this->flags & $flag); - } - - /** - * Serialize into JSON for client-side JS - * - * @return string - */ - public function jsonSerialize() { - return [ - 'value' => $this->getText(), - 'flags' => $this->getFlags(), - 'type' => $this->getType() - ]; - } - - public function isOptional() { - return $this->isFlagSet(self::FLAG_OPTIONAL) || $this->isFlagSet(self::FLAG_USER_PROVIDED); - } - - /** - * Validate a parameter value against this - * Convert type as necessary - * - * @param mixed $value Value to check - * @return bool success - */ - public function validateValue(&$value) { - switch ($this->getType()) { - case self::VALUE_BOOLEAN: - if (!is_bool($value)) { - switch ($value) { - case 'true': - $value = true; - break; - case 'false': - $value = false; - break; - default: - return false; - } - } - break; - default: - if (!$value && !$this->isOptional()) { - return false; - } - break; - } - return true; - } -} diff --git a/apps/files_external/lib/dependencytrait.php b/apps/files_external/lib/dependencytrait.php deleted file mode 100644 index eed3ba1b327..00000000000 --- a/apps/files_external/lib/dependencytrait.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -use \OCA\Files_External\Lib\MissingDependency; - -/** - * Trait for objects that have dependencies for use - */ -trait DependencyTrait { - - /** - * Check if object is valid for use - * - * @return MissingDependency[] Unsatisfied dependencies - */ - public function checkDependencies() { - return []; // no dependencies by default - } - -} - diff --git a/apps/files_external/lib/dropbox.php b/apps/files_external/lib/dropbox.php deleted file mode 100644 index 8381ccbae59..00000000000 --- a/apps/files_external/lib/dropbox.php +++ /dev/null @@ -1,352 +0,0 @@ -<?php -/** - * @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 <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Sascha Schmidt <realriot@realriot.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -use GuzzleHttp\Exception\RequestException; -use Icewind\Streams\IteratorDirectory; -use Icewind\Streams\RetryWrapper; - -require_once __DIR__ . '/../3rdparty/Dropbox/autoload.php'; - -class Dropbox extends \OC\Files\Storage\Common { - - private $dropbox; - private $root; - private $id; - private $metaData = array(); - private $oauth; - - private static $tempFiles = array(); - - public function __construct($params) { - if (isset($params['configured']) && $params['configured'] == 'true' - && isset($params['app_key']) - && isset($params['app_secret']) - && isset($params['token']) - && isset($params['token_secret']) - ) { - $this->root = isset($params['root']) ? $params['root'] : ''; - $this->id = 'dropbox::'.$params['app_key'] . $params['token']. '/' . $this->root; - $this->oauth = new \Dropbox_OAuth_Curl($params['app_key'], $params['app_secret']); - $this->oauth->setToken($params['token'], $params['token_secret']); - // note: Dropbox_API connection is lazy - $this->dropbox = new \Dropbox_API($this->oauth, 'auto'); - } else { - throw new \Exception('Creating \OC\Files\Storage\Dropbox storage failed'); - } - } - - /** - * @param string $path - */ - private function deleteMetaData($path) { - $path = ltrim($this->root.$path, '/'); - if (isset($this->metaData[$path])) { - unset($this->metaData[$path]); - return true; - } - return false; - } - - private function setMetaData($path, $metaData) { - $this->metaData[ltrim($path, '/')] = $metaData; - } - - /** - * Returns the path's metadata - * @param string $path path for which to return the metadata - * @param bool $list if true, also return the directory's contents - * @return mixed directory contents if $list is true, file metadata if $list is - * false, null if the file doesn't exist or "false" if the operation failed - */ - private function getDropBoxMetaData($path, $list = false) { - $path = ltrim($this->root.$path, '/'); - if ( ! $list && isset($this->metaData[$path])) { - return $this->metaData[$path]; - } else { - if ($list) { - try { - $response = $this->dropbox->getMetaData($path); - } catch (\Exception $exception) { - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - return false; - } - $contents = array(); - if ($response && isset($response['contents'])) { - // Cache folder's contents - foreach ($response['contents'] as $file) { - if (!isset($file['is_deleted']) || !$file['is_deleted']) { - $this->setMetaData($path.'/'.basename($file['path']), $file); - $contents[] = $file; - } - } - unset($response['contents']); - } - if (!isset($response['is_deleted']) || !$response['is_deleted']) { - $this->setMetaData($path, $response); - } - // Return contents of folder only - return $contents; - } else { - try { - $requestPath = $path; - if ($path === '.') { - $requestPath = ''; - } - - $response = $this->dropbox->getMetaData($requestPath, 'false'); - if (!isset($response['is_deleted']) || !$response['is_deleted']) { - $this->setMetaData($path, $response); - return $response; - } - return null; - } catch (\Exception $exception) { - if ($exception instanceof \Dropbox_Exception_NotFound) { - // don't log, might be a file_exist check - return false; - } - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - return false; - } - } - } - } - - public function getId(){ - return $this->id; - } - - public function mkdir($path) { - $path = $this->root.$path; - try { - $this->dropbox->createFolder($path); - return true; - } catch (\Exception $exception) { - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - return false; - } - } - - public function rmdir($path) { - return $this->unlink($path); - } - - public function opendir($path) { - $contents = $this->getDropBoxMetaData($path, true); - if ($contents !== false) { - $files = array(); - foreach ($contents as $file) { - $files[] = basename($file['path']); - } - return IteratorDirectory::wrap($files); - } - return false; - } - - public function stat($path) { - $metaData = $this->getDropBoxMetaData($path); - if ($metaData) { - $stat['size'] = $metaData['bytes']; - $stat['atime'] = time(); - $stat['mtime'] = (isset($metaData['modified'])) ? strtotime($metaData['modified']) : time(); - return $stat; - } - return false; - } - - public function filetype($path) { - if ($path == '' || $path == '/') { - return 'dir'; - } else { - $metaData = $this->getDropBoxMetaData($path); - if ($metaData) { - if ($metaData['is_dir'] == 'true') { - return 'dir'; - } else { - return 'file'; - } - } - } - return false; - } - - public function file_exists($path) { - if ($path == '' || $path == '/') { - return true; - } - if ($this->getDropBoxMetaData($path)) { - return true; - } - return false; - } - - public function unlink($path) { - try { - $this->dropbox->delete($this->root.$path); - $this->deleteMetaData($path); - return true; - } catch (\Exception $exception) { - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - return false; - } - } - - public function rename($path1, $path2) { - try { - // overwrite if target file exists and is not a directory - $destMetaData = $this->getDropBoxMetaData($path2); - if (isset($destMetaData) && $destMetaData !== false && !$destMetaData['is_dir']) { - $this->unlink($path2); - } - $this->dropbox->move($this->root.$path1, $this->root.$path2); - $this->deleteMetaData($path1); - return true; - } catch (\Exception $exception) { - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - return false; - } - } - - public function copy($path1, $path2) { - $path1 = $this->root.$path1; - $path2 = $this->root.$path2; - try { - $this->dropbox->copy($path1, $path2); - return true; - } catch (\Exception $exception) { - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - return false; - } - } - - public function fopen($path, $mode) { - $path = $this->root.$path; - switch ($mode) { - case 'r': - case 'rb': - try { - // slashes need to stay - $encodedPath = str_replace('%2F', '/', rawurlencode(trim($path, '/'))); - $downloadUrl = 'https://api-content.dropbox.com/1/files/auto/' . $encodedPath; - $headers = $this->oauth->getOAuthHeader($downloadUrl, [], 'GET'); - - $client = \OC::$server->getHTTPClientService()->newClient(); - try { - $response = $client->get($downloadUrl, [ - 'headers' => $headers, - 'stream' => true, - ]); - } catch (RequestException $e) { - if (!is_null($e->getResponse())) { - if ($e->getResponse()->getStatusCode() === 404) { - return false; - } else { - throw $e; - } - } else { - throw $e; - } - } - - $handle = $response->getBody(); - return RetryWrapper::wrap($handle); - } catch (\Exception $exception) { - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - return false; - } - case 'w': - case 'wb': - case 'a': - case 'ab': - case 'r+': - case 'w+': - case 'wb+': - case 'a+': - case 'x': - case 'x+': - case 'c': - case 'c+': - if (strrpos($path, '.') !== false) { - $ext = substr($path, strrpos($path, '.')); - } else { - $ext = ''; - } - $tmpFile = \OCP\Files::tmpFile($ext); - \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); - if ($this->file_exists($path)) { - $source = $this->fopen($path, 'r'); - file_put_contents($tmpFile, $source); - } - self::$tempFiles[$tmpFile] = $path; - return fopen('close://'.$tmpFile, $mode); - } - return false; - } - - public function writeBack($tmpFile) { - if (isset(self::$tempFiles[$tmpFile])) { - $handle = fopen($tmpFile, 'r'); - try { - $this->dropbox->putFile(self::$tempFiles[$tmpFile], $handle); - unlink($tmpFile); - $this->deleteMetaData(self::$tempFiles[$tmpFile]); - } catch (\Exception $exception) { - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - } - } - } - - public function free_space($path) { - try { - $info = $this->dropbox->getAccountInfo(); - return $info['quota_info']['quota'] - $info['quota_info']['normal']; - } catch (\Exception $exception) { - \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR); - return false; - } - } - - public function touch($path, $mtime = null) { - if ($this->file_exists($path)) { - return false; - } else { - $this->file_put_contents($path, ''); - } - return true; - } - - /** - * check if curl is installed - */ - public static function checkDependencies() { - return true; - } - -} diff --git a/apps/files_external/lib/frontenddefinitiontrait.php b/apps/files_external/lib/frontenddefinitiontrait.php deleted file mode 100644 index 9f2b7c40f7f..00000000000 --- a/apps/files_external/lib/frontenddefinitiontrait.php +++ /dev/null @@ -1,150 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -use \OCA\Files_External\Lib\DefinitionParameter; -use \OCA\Files_External\Lib\StorageConfig; - -/** - * Trait for objects that have a frontend representation - */ -trait FrontendDefinitionTrait { - - /** @var string human-readable mechanism name */ - private $text; - - /** @var DefinitionParameter[] parameters for mechanism */ - private $parameters = []; - - /** @var string|null custom JS */ - private $customJs = null; - - /** - * @return string - */ - public function getText() { - return $this->text; - } - - /** - * @param string $text - * @return self - */ - public function setText($text) { - $this->text = $text; - return $this; - } - - /** - * @param FrontendDefinitionTrait $a - * @param FrontendDefinitionTrait $b - * @return int - */ - public static function lexicalCompare(FrontendDefinitionTrait $a, FrontendDefinitionTrait $b) { - return strcmp($a->getText(), $b->getText()); - } - - /** - * @return DefinitionParameter[] - */ - public function getParameters() { - return $this->parameters; - } - - /** - * @param DefinitionParameter[] $parameters - * @return self - */ - public function addParameters(array $parameters) { - foreach ($parameters as $parameter) { - $this->addParameter($parameter); - } - return $this; - } - - /** - * @param DefinitionParameter $parameter - * @return self - */ - public function addParameter(DefinitionParameter $parameter) { - $this->parameters[$parameter->getName()] = $parameter; - return $this; - } - - /** - * @return string|null - */ - public function getCustomJs() { - return $this->customJs; - } - - /** - * @param string $custom - * @return self - */ - public function setCustomJs($custom) { - $this->customJs = $custom; - return $this; - } - - /** - * Serialize into JSON for client-side JS - * - * @return array - */ - public function jsonSerializeDefinition() { - $configuration = []; - foreach ($this->getParameters() as $parameter) { - $configuration[$parameter->getName()] = $parameter; - } - - $data = [ - 'name' => $this->getText(), - 'configuration' => $configuration, - ]; - if (isset($this->customJs)) { - $data['custom'] = $this->getCustomJs(); - } - return $data; - } - - /** - * Check if parameters are satisfied in a StorageConfig - * - * @param StorageConfig $storage - * @return bool - */ - public function validateStorageDefinition(StorageConfig $storage) { - foreach ($this->getParameters() as $name => $parameter) { - $value = $storage->getBackendOption($name); - if (!is_null($value) || !$parameter->isOptional()) { - if (!$parameter->validateValue($value)) { - return false; - } - $storage->setBackendOption($name, $value); - } - } - return true; - } - -} diff --git a/apps/files_external/lib/ftp.php b/apps/files_external/lib/ftp.php deleted file mode 100644 index 7249aeceb5d..00000000000 --- a/apps/files_external/lib/ftp.php +++ /dev/null @@ -1,155 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @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 <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -use Icewind\Streams\RetryWrapper; - -class FTP extends \OC\Files\Storage\StreamWrapper{ - private $password; - private $user; - private $host; - private $secure; - private $root; - - private static $tempFiles=array(); - - 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']; - } else { - $this->secure = false; - } - $this->root=isset($params['root'])?$params['root']:'/'; - if ( ! $this->root || $this->root[0]!='/') { - $this->root='/'.$this->root; - } - if (substr($this->root, -1) !== '/') { - $this->root .= '/'; - } - } else { - throw new \Exception('Creating \OC\Files\Storage\FTP storage failed'); - } - - } - - public function getId(){ - return 'ftp::' . $this->user . '@' . $this->host . '/' . $this->root; - } - - /** - * construct the ftp url - * @param string $path - * @return string - */ - public function constructUrl($path) { - $url='ftp'; - if ($this->secure) { - $url.='s'; - } - $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) { - if ($this->is_dir($path)) { - return $this->rmdir($path); - } - else { - $url = $this->constructUrl($path); - $result = unlink($url); - clearstatcache(true, $url); - return $result; - } - } - public function fopen($path,$mode) { - switch($mode) { - case 'r': - case 'rb': - case 'w': - case 'wb': - case 'a': - case 'ab': - //these are supported by the wrapper - $context = stream_context_create(array('ftp' => array('overwrite' => true))); - $handle = fopen($this->constructUrl($path), $mode, false, $context); - return RetryWrapper::wrap($handle); - case 'r+': - case 'w+': - case 'wb+': - case 'a+': - case 'x': - case 'x+': - case 'c': - case 'c+': - //emulate these - if (strrpos($path, '.')!==false) { - $ext=substr($path, strrpos($path, '.')); - } else { - $ext=''; - } - $tmpFile=\OCP\Files::tmpFile($ext); - \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); - if ($this->file_exists($path)) { - $this->getFile($path, $tmpFile); - } - self::$tempFiles[$tmpFile]=$path; - return fopen('close://'.$tmpFile, $mode); - } - return false; - } - - public function writeBack($tmpFile) { - if (isset(self::$tempFiles[$tmpFile])) { - $this->uploadFile($tmpFile, self::$tempFiles[$tmpFile]); - unlink($tmpFile); - } - } - - /** - * check if php-ftp is installed - */ - public static function checkDependencies() { - if (function_exists('ftp_login')) { - return(true); - } else { - return array('ftp'); - } - } - -} diff --git a/apps/files_external/lib/google.php b/apps/files_external/lib/google.php deleted file mode 100644 index 62d264dfeef..00000000000 --- a/apps/files_external/lib/google.php +++ /dev/null @@ -1,710 +0,0 @@ -<?php -/** - * @author Adam Williamson <awilliam@redhat.com> - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -use GuzzleHttp\Exception\RequestException; -use Icewind\Streams\IteratorDirectory; -use Icewind\Streams\RetryWrapper; - -set_include_path(get_include_path().PATH_SEPARATOR. - \OC_App::getAppPath('files_external').'/3rdparty/google-api-php-client/src'); -require_once 'Google/Client.php'; -require_once 'Google/Service/Drive.php'; - -class Google extends \OC\Files\Storage\Common { - - private $client; - private $id; - private $service; - private $driveFiles; - - private static $tempFiles = array(); - - // Google Doc mimetypes - const FOLDER = 'application/vnd.google-apps.folder'; - const DOCUMENT = 'application/vnd.google-apps.document'; - const SPREADSHEET = 'application/vnd.google-apps.spreadsheet'; - const DRAWING = 'application/vnd.google-apps.drawing'; - const PRESENTATION = 'application/vnd.google-apps.presentation'; - - public function __construct($params) { - if (isset($params['configured']) && $params['configured'] === 'true' - && isset($params['client_id']) && isset($params['client_secret']) - && isset($params['token']) - ) { - $this->client = new \Google_Client(); - $this->client->setClientId($params['client_id']); - $this->client->setClientSecret($params['client_secret']); - $this->client->setScopes(array('https://www.googleapis.com/auth/drive')); - $this->client->setAccessToken($params['token']); - // if curl isn't available we're likely to run into - // https://github.com/google/google-api-php-client/issues/59 - // - disable gzip to avoid it. - if (!function_exists('curl_version') || !function_exists('curl_exec')) { - $this->client->setClassConfig("Google_Http_Request", "disable_gzip", true); - } - // note: API connection is lazy - $this->service = new \Google_Service_Drive($this->client); - $token = json_decode($params['token'], true); - $this->id = 'google::'.substr($params['client_id'], 0, 30).$token['created']; - } else { - throw new \Exception('Creating \OC\Files\Storage\Google storage failed'); - } - } - - public function getId() { - return $this->id; - } - - /** - * Get the Google_Service_Drive_DriveFile object for the specified path. - * Returns false on failure. - * @param string $path - * @return \Google_Service_Drive_DriveFile|false - */ - private function getDriveFile($path) { - // Remove leading and trailing slashes - $path = trim($path, '/'); - if (isset($this->driveFiles[$path])) { - return $this->driveFiles[$path]; - } else if ($path === '') { - $root = $this->service->files->get('root'); - $this->driveFiles[$path] = $root; - return $root; - } else { - // Google Drive SDK does not have methods for retrieving files by path - // Instead we must find the id of the parent folder of the file - $parentId = $this->getDriveFile('')->getId(); - $folderNames = explode('/', $path); - $path = ''; - // Loop through each folder of this path to get to the file - foreach ($folderNames as $name) { - // Reconstruct path from beginning - if ($path === '') { - $path .= $name; - } else { - $path .= '/'.$name; - } - if (isset($this->driveFiles[$path])) { - $parentId = $this->driveFiles[$path]->getId(); - } else { - $q = "title='" . str_replace("'","\\'", $name) . "' and '" . str_replace("'","\\'", $parentId) . "' in parents and trashed = false"; - $result = $this->service->files->listFiles(array('q' => $q))->getItems(); - if (!empty($result)) { - // Google Drive allows files with the same name, ownCloud doesn't - if (count($result) > 1) { - $this->onDuplicateFileDetected($path); - return false; - } else { - $file = current($result); - $this->driveFiles[$path] = $file; - $parentId = $file->getId(); - } - } else { - // Google Docs have no extension in their title, so try without extension - $pos = strrpos($path, '.'); - if ($pos !== false) { - $pathWithoutExt = substr($path, 0, $pos); - $file = $this->getDriveFile($pathWithoutExt); - if ($file) { - // Switch cached Google_Service_Drive_DriveFile to the correct index - unset($this->driveFiles[$pathWithoutExt]); - $this->driveFiles[$path] = $file; - $parentId = $file->getId(); - } else { - return false; - } - } else { - return false; - } - } - } - } - return $this->driveFiles[$path]; - } - } - - /** - * Set the Google_Service_Drive_DriveFile object in the cache - * @param string $path - * @param Google_Service_Drive_DriveFile|false $file - */ - private function setDriveFile($path, $file) { - $path = trim($path, '/'); - $this->driveFiles[$path] = $file; - if ($file === false) { - // Set all child paths as false - $len = strlen($path); - foreach ($this->driveFiles as $key => $file) { - if (substr($key, 0, $len) === $path) { - $this->driveFiles[$key] = false; - } - } - } - } - - /** - * Write a log message to inform about duplicate file names - * @param string $path - */ - private function onDuplicateFileDetected($path) { - $about = $this->service->about->get(); - $user = $about->getName(); - \OCP\Util::writeLog('files_external', - 'Ignoring duplicate file name: '.$path.' on Google Drive for Google user: '.$user, - \OCP\Util::INFO - ); - } - - /** - * Generate file extension for a Google Doc, choosing Open Document formats for download - * @param string $mimetype - * @return string - */ - private function getGoogleDocExtension($mimetype) { - if ($mimetype === self::DOCUMENT) { - return 'odt'; - } else if ($mimetype === self::SPREADSHEET) { - return 'ods'; - } else if ($mimetype === self::DRAWING) { - return 'jpg'; - } else if ($mimetype === self::PRESENTATION) { - // Download as .odp is not available - return 'pdf'; - } else { - return ''; - } - } - - public function mkdir($path) { - if (!$this->is_dir($path)) { - $parentFolder = $this->getDriveFile(dirname($path)); - if ($parentFolder) { - $folder = new \Google_Service_Drive_DriveFile(); - $folder->setTitle(basename($path)); - $folder->setMimeType(self::FOLDER); - $parent = new \Google_Service_Drive_ParentReference(); - $parent->setId($parentFolder->getId()); - $folder->setParents(array($parent)); - $result = $this->service->files->insert($folder); - if ($result) { - $this->setDriveFile($path, $result); - } - return (bool)$result; - } - } - return false; - } - - public function rmdir($path) { - if (!$this->isDeletable($path)) { - return false; - } - if (trim($path, '/') === '') { - $dir = $this->opendir($path); - if(is_resource($dir)) { - while (($file = readdir($dir)) !== false) { - if (!\OC\Files\Filesystem::isIgnoredDir($file)) { - if (!$this->unlink($path.'/'.$file)) { - return false; - } - } - } - closedir($dir); - } - $this->driveFiles = array(); - return true; - } else { - return $this->unlink($path); - } - } - - public function opendir($path) { - $folder = $this->getDriveFile($path); - if ($folder) { - $files = array(); - $duplicates = array(); - $pageToken = true; - while ($pageToken) { - $params = array(); - if ($pageToken !== true) { - $params['pageToken'] = $pageToken; - } - $params['q'] = "'" . str_replace("'","\\'", $folder->getId()) . "' in parents and trashed = false"; - $children = $this->service->files->listFiles($params); - foreach ($children->getItems() as $child) { - $name = $child->getTitle(); - // Check if this is a Google Doc i.e. no extension in name - $extension = $child->getFileExtension(); - if (empty($extension) - && $child->getMimeType() !== self::FOLDER - ) { - $name .= '.'.$this->getGoogleDocExtension($child->getMimeType()); - } - if ($path === '') { - $filepath = $name; - } else { - $filepath = $path.'/'.$name; - } - // Google Drive allows files with the same name, ownCloud doesn't - // Prevent opendir() from returning any duplicate files - $key = array_search($name, $files); - if ($key !== false || isset($duplicates[$filepath])) { - if (!isset($duplicates[$filepath])) { - $duplicates[$filepath] = true; - $this->setDriveFile($filepath, false); - unset($files[$key]); - $this->onDuplicateFileDetected($filepath); - } - } else { - // Cache the Google_Service_Drive_DriveFile for future use - $this->setDriveFile($filepath, $child); - $files[] = $name; - } - } - $pageToken = $children->getNextPageToken(); - } - return IteratorDirectory::wrap($files); - } else { - return false; - } - } - - public function stat($path) { - $file = $this->getDriveFile($path); - if ($file) { - $stat = array(); - if ($this->filetype($path) === 'dir') { - $stat['size'] = 0; - } else { - // Check if this is a Google Doc - if ($this->getMimeType($path) !== $file->getMimeType()) { - // Return unknown file size - $stat['size'] = \OCP\Files\FileInfo::SPACE_UNKNOWN; - } else { - $stat['size'] = $file->getFileSize(); - } - } - $stat['atime'] = strtotime($file->getLastViewedByMeDate()); - $stat['mtime'] = strtotime($file->getModifiedDate()); - $stat['ctime'] = strtotime($file->getCreatedDate()); - return $stat; - } else { - return false; - } - } - - public function filetype($path) { - if ($path === '') { - return 'dir'; - } else { - $file = $this->getDriveFile($path); - if ($file) { - if ($file->getMimeType() === self::FOLDER) { - return 'dir'; - } else { - return 'file'; - } - } else { - return false; - } - } - } - - public function isUpdatable($path) { - $file = $this->getDriveFile($path); - if ($file) { - return $file->getEditable(); - } else { - return false; - } - } - - public function file_exists($path) { - return (bool)$this->getDriveFile($path); - } - - public function unlink($path) { - $file = $this->getDriveFile($path); - if ($file) { - $result = $this->service->files->trash($file->getId()); - if ($result) { - $this->setDriveFile($path, false); - } - return (bool)$result; - } else { - return false; - } - } - - public function rename($path1, $path2) { - $file = $this->getDriveFile($path1); - if ($file) { - $newFile = $this->getDriveFile($path2); - if (dirname($path1) === dirname($path2)) { - if ($newFile) { - // rename to the name of the target file, could be an office file without extension - $file->setTitle($newFile->getTitle()); - } else { - $file->setTitle(basename(($path2))); - } - } else { - // Change file parent - $parentFolder2 = $this->getDriveFile(dirname($path2)); - if ($parentFolder2) { - $parent = new \Google_Service_Drive_ParentReference(); - $parent->setId($parentFolder2->getId()); - $file->setParents(array($parent)); - } else { - return false; - } - } - // We need to get the object for the existing file with the same - // name (if there is one) before we do the patch. If oldfile - // exists and is a directory we have to delete it before we - // do the rename too. - $oldfile = $this->getDriveFile($path2); - if ($oldfile && $this->is_dir($path2)) { - $this->rmdir($path2); - $oldfile = false; - } - $result = $this->service->files->patch($file->getId(), $file); - if ($result) { - $this->setDriveFile($path1, false); - $this->setDriveFile($path2, $result); - if ($oldfile && $newFile) { - // only delete if they have a different id (same id can happen for part files) - if ($newFile->getId() !== $oldfile->getId()) { - $this->service->files->delete($oldfile->getId()); - } - } - } - return (bool)$result; - } else { - return false; - } - } - - public function fopen($path, $mode) { - $pos = strrpos($path, '.'); - if ($pos !== false) { - $ext = substr($path, $pos); - } else { - $ext = ''; - } - switch ($mode) { - case 'r': - case 'rb': - $file = $this->getDriveFile($path); - if ($file) { - $exportLinks = $file->getExportLinks(); - $mimetype = $this->getMimeType($path); - $downloadUrl = null; - if ($exportLinks && isset($exportLinks[$mimetype])) { - $downloadUrl = $exportLinks[$mimetype]; - } else { - $downloadUrl = $file->getDownloadUrl(); - } - if (isset($downloadUrl)) { - $request = new \Google_Http_Request($downloadUrl, 'GET', null, null); - $httpRequest = $this->client->getAuth()->sign($request); - // the library's service doesn't support streaming, so we use Guzzle instead - $client = \OC::$server->getHTTPClientService()->newClient(); - try { - $response = $client->get($downloadUrl, [ - 'headers' => $httpRequest->getRequestHeaders(), - 'stream' => true, - 'verify' => __DIR__ . '/../3rdparty/google-api-php-client/src/Google/IO/cacerts.pem', - ]); - } catch (RequestException $e) { - if(!is_null($e->getResponse())) { - if ($e->getResponse()->getStatusCode() === 404) { - return false; - } else { - throw $e; - } - } else { - throw $e; - } - } - - $handle = $response->getBody(); - return RetryWrapper::wrap($handle); - } - } - return false; - case 'w': - case 'wb': - case 'a': - case 'ab': - case 'r+': - case 'w+': - case 'wb+': - case 'a+': - case 'x': - case 'x+': - case 'c': - case 'c+': - $tmpFile = \OCP\Files::tmpFile($ext); - \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); - if ($this->file_exists($path)) { - $source = $this->fopen($path, 'rb'); - file_put_contents($tmpFile, $source); - } - self::$tempFiles[$tmpFile] = $path; - return fopen('close://'.$tmpFile, $mode); - } - } - - public function writeBack($tmpFile) { - if (isset(self::$tempFiles[$tmpFile])) { - $path = self::$tempFiles[$tmpFile]; - $parentFolder = $this->getDriveFile(dirname($path)); - if ($parentFolder) { - $mimetype = \OC::$server->getMimeTypeDetector()->detect($tmpFile); - $params = array( - 'mimeType' => $mimetype, - 'uploadType' => 'media' - ); - $result = false; - - $chunkSizeBytes = 10 * 1024 * 1024; - - $useChunking = false; - $size = filesize($tmpFile); - if ($size > $chunkSizeBytes) { - $useChunking = true; - } else { - $params['data'] = file_get_contents($tmpFile); - } - - if ($this->file_exists($path)) { - $file = $this->getDriveFile($path); - $this->client->setDefer($useChunking); - $request = $this->service->files->update($file->getId(), $file, $params); - } else { - $file = new \Google_Service_Drive_DriveFile(); - $file->setTitle(basename($path)); - $file->setMimeType($mimetype); - $parent = new \Google_Service_Drive_ParentReference(); - $parent->setId($parentFolder->getId()); - $file->setParents(array($parent)); - $this->client->setDefer($useChunking); - $request = $this->service->files->insert($file, $params); - } - - if ($useChunking) { - // Create a media file upload to represent our upload process. - $media = new \Google_Http_MediaFileUpload( - $this->client, - $request, - 'text/plain', - null, - true, - $chunkSizeBytes - ); - $media->setFileSize($size); - - // Upload the various chunks. $status will be false until the process is - // complete. - $status = false; - $handle = fopen($tmpFile, 'rb'); - while (!$status && !feof($handle)) { - $chunk = fread($handle, $chunkSizeBytes); - $status = $media->nextChunk($chunk); - } - - // The final value of $status will be the data from the API for the object - // that has been uploaded. - $result = false; - if ($status !== false) { - $result = $status; - } - - fclose($handle); - } else { - $result = $request; - } - - // Reset to the client to execute requests immediately in the future. - $this->client->setDefer(false); - - if ($result) { - $this->setDriveFile($path, $result); - } - } - unlink($tmpFile); - } - } - - public function getMimeType($path) { - $file = $this->getDriveFile($path); - if ($file) { - $mimetype = $file->getMimeType(); - // Convert Google Doc mimetypes, choosing Open Document formats for download - if ($mimetype === self::FOLDER) { - return 'httpd/unix-directory'; - } else if ($mimetype === self::DOCUMENT) { - return 'application/vnd.oasis.opendocument.text'; - } else if ($mimetype === self::SPREADSHEET) { - return 'application/x-vnd.oasis.opendocument.spreadsheet'; - } else if ($mimetype === self::DRAWING) { - return 'image/jpeg'; - } else if ($mimetype === self::PRESENTATION) { - // Download as .odp is not available - return 'application/pdf'; - } else { - // use extension-based detection, could be an encrypted file - return parent::getMimeType($path); - } - } else { - return false; - } - } - - public function free_space($path) { - $about = $this->service->about->get(); - return $about->getQuotaBytesTotal() - $about->getQuotaBytesUsed(); - } - - public function touch($path, $mtime = null) { - $file = $this->getDriveFile($path); - $result = false; - if ($file) { - if (isset($mtime)) { - // This is just RFC3339, but frustratingly, GDrive's API *requires* - // the fractions portion be present, while no handy PHP constant - // for RFC3339 or ISO8601 includes it. So we do it ourselves. - $file->setModifiedDate(date('Y-m-d\TH:i:s.uP', $mtime)); - $result = $this->service->files->patch($file->getId(), $file, array( - 'setModifiedDate' => true, - )); - } else { - $result = $this->service->files->touch($file->getId()); - } - } else { - $parentFolder = $this->getDriveFile(dirname($path)); - if ($parentFolder) { - $file = new \Google_Service_Drive_DriveFile(); - $file->setTitle(basename($path)); - $parent = new \Google_Service_Drive_ParentReference(); - $parent->setId($parentFolder->getId()); - $file->setParents(array($parent)); - $result = $this->service->files->insert($file); - } - } - if ($result) { - $this->setDriveFile($path, $result); - } - return (bool)$result; - } - - public function test() { - if ($this->free_space('')) { - return true; - } - return false; - } - - public function hasUpdated($path, $time) { - $appConfig = \OC::$server->getAppConfig(); - if ($this->is_file($path)) { - return parent::hasUpdated($path, $time); - } else { - // Google Drive doesn't change modified times of folders when files inside are updated - // Instead we use the Changes API to see if folders have been updated, and it's a pain - $folder = $this->getDriveFile($path); - if ($folder) { - $result = false; - $folderId = $folder->getId(); - $startChangeId = $appConfig->getValue('files_external', $this->getId().'cId'); - $params = array( - 'includeDeleted' => true, - 'includeSubscribed' => true, - ); - if (isset($startChangeId)) { - $startChangeId = (int)$startChangeId; - $largestChangeId = $startChangeId; - $params['startChangeId'] = $startChangeId + 1; - } else { - $largestChangeId = 0; - } - $pageToken = true; - while ($pageToken) { - if ($pageToken !== true) { - $params['pageToken'] = $pageToken; - } - $changes = $this->service->changes->listChanges($params); - if ($largestChangeId === 0 || $largestChangeId === $startChangeId) { - $largestChangeId = $changes->getLargestChangeId(); - } - if (isset($startChangeId)) { - // Check if a file in this folder has been updated - // There is no way to filter by folder at the API level... - foreach ($changes->getItems() as $change) { - $file = $change->getFile(); - if ($file) { - foreach ($file->getParents() as $parent) { - if ($parent->getId() === $folderId) { - $result = true; - // Check if there are changes in different folders - } else if ($change->getId() <= $largestChangeId) { - // Decrement id so this change is fetched when called again - $largestChangeId = $change->getId(); - $largestChangeId--; - } - } - } - } - $pageToken = $changes->getNextPageToken(); - } else { - // Assuming the initial scan just occurred and changes are negligible - break; - } - } - $appConfig->setValue('files_external', $this->getId().'cId', $largestChangeId); - return $result; - } - } - return false; - } - - /** - * check if curl is installed - */ - public static function checkDependencies() { - return true; - } - -} diff --git a/apps/files_external/lib/identifiertrait.php b/apps/files_external/lib/identifiertrait.php deleted file mode 100644 index c49f4fcbc8d..00000000000 --- a/apps/files_external/lib/identifiertrait.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -/** - * Trait for objects requiring an identifier (and/or identifier aliases) - * Also supports deprecation to a different object, linking the objects - */ -trait IdentifierTrait { - - /** @var string */ - protected $identifier; - - /** @var string[] */ - protected $identifierAliases = []; - - /** @var IdentifierTrait */ - protected $deprecateTo = null; - - /** - * @return string - */ - public function getIdentifier() { - return $this->identifier; - } - - /** - * @param string $identifier - * @return self - */ - public function setIdentifier($identifier) { - $this->identifier = $identifier; - $this->identifierAliases[] = $identifier; - return $this; - } - - /** - * @return string[] - */ - public function getIdentifierAliases() { - return $this->identifierAliases; - } - - /** - * @param string $alias - * @return self - */ - public function addIdentifierAlias($alias) { - $this->identifierAliases[] = $alias; - return $this; - } - - /** - * @return object|null - */ - public function getDeprecateTo() { - return $this->deprecateTo; - } - - /** - * @param object $destinationObject - * @return self - */ - public function deprecateTo($destinationObject) { - $this->deprecateTo = $destinationObject; - return $this; - } - - /** - * @return array - */ - public function jsonSerializeIdentifier() { - $data = [ - 'identifier' => $this->identifier, - 'identifierAliases' => $this->identifierAliases, - ]; - if ($this->deprecateTo) { - $data['deprecateTo'] = $this->deprecateTo->getIdentifier(); - } - return $data; - } - -} diff --git a/apps/files_external/lib/insufficientdataformeaningfulanswerexception.php b/apps/files_external/lib/insufficientdataformeaningfulanswerexception.php deleted file mode 100644 index 1906057eb67..00000000000 --- a/apps/files_external/lib/insufficientdataformeaningfulanswerexception.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -use \OCP\Files\StorageNotAvailableException; - -/** - * Authentication mechanism or backend has insufficient data - */ -class InsufficientDataForMeaningfulAnswerException extends StorageNotAvailableException { - /** - * StorageNotAvailableException constructor. - * - * @param string $message - * @param int $code - * @param \Exception $previous - * @since 6.0.0 - */ - public function __construct($message = '', $code = self::STATUS_INDETERMINATE, \Exception $previous = null) { - parent::__construct($message, $code, $previous); - } -} diff --git a/apps/files_external/lib/missingdependency.php b/apps/files_external/lib/missingdependency.php deleted file mode 100644 index a4a20dd1128..00000000000 --- a/apps/files_external/lib/missingdependency.php +++ /dev/null @@ -1,64 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -/** - * External storage backend dependency - */ -class MissingDependency { - - /** @var string */ - private $dependency; - - /** @var string|null Custom message */ - private $message = null; - - /** - * @param string $dependency - */ - public function __construct($dependency) { - $this->dependency = $dependency; - } - - /** - * @return string - */ - public function getDependency() { - return $this->dependency; - } - - /** - * @return string|null - */ - public function getMessage() { - return $this->message; - } - - /** - * @param string $message - * @return self - */ - public function setMessage($message) { - $this->message = $message; - return $this; - } -} diff --git a/apps/files_external/lib/notfoundexception.php b/apps/files_external/lib/notfoundexception.php deleted file mode 100644 index dd3dd1907a5..00000000000 --- a/apps/files_external/lib/notfoundexception.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php -/** - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_external; - -/** - * Storage is not found - */ -class NotFoundException extends \Exception { -} diff --git a/apps/files_external/lib/owncloud.php b/apps/files_external/lib/owncloud.php deleted file mode 100644 index c4824e6bd14..00000000000 --- a/apps/files_external/lib/owncloud.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -/** - * ownCloud backend for external storage based on DAV backend. - * - * The ownCloud URL consists of three parts: - * http://%host/%context/remote.php/webdav/%root - * - */ -class OwnCloud extends \OC\Files\Storage\DAV{ - const OC_URL_SUFFIX = 'remote.php/webdav'; - - public function __construct($params) { - // extract context path from host if specified - // (owncloud install path on host) - $host = $params['host']; - // strip protocol - if (substr($host, 0, 8) == "https://") { - $host = substr($host, 8); - $params['secure'] = true; - } else if (substr($host, 0, 7) == "http://") { - $host = substr($host, 7); - $params['secure'] = false; - } - $contextPath = ''; - $hostSlashPos = strpos($host, '/'); - if ($hostSlashPos !== false){ - $contextPath = substr($host, $hostSlashPos); - $host = substr($host, 0, $hostSlashPos); - } - - if (substr($contextPath, -1) !== '/'){ - $contextPath .= '/'; - } - - if (isset($params['root'])){ - $root = $params['root']; - if (substr($root, 0, 1) !== '/'){ - $root = '/' . $root; - } - } - else{ - $root = '/'; - } - - $params['host'] = $host; - $params['root'] = $contextPath . self::OC_URL_SUFFIX . $root; - - parent::__construct($params); - } -} diff --git a/apps/files_external/lib/personalmount.php b/apps/files_external/lib/personalmount.php deleted file mode 100644 index c3b71fbef32..00000000000 --- a/apps/files_external/lib/personalmount.php +++ /dev/null @@ -1,88 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -use OC\Files\Mount\MountPoint; -use OC\Files\Mount\MoveableMount; -use OCA\Files_External\Service\UserStoragesService; - -/** - * Person mount points can be moved by the user - */ -class PersonalMount extends MountPoint implements MoveableMount { - /** @var UserStoragesService */ - protected $storagesService; - - /** @var int */ - protected $numericStorageId; - - /** - * @param UserStoragesService $storagesService - * @param int $storageId - * @param \OCP\Files\Storage $storage - * @param string $mountpoint - * @param array $arguments (optional) configuration for the storage backend - * @param \OCP\Files\Storage\IStorageFactory $loader - * @param array $mountOptions mount specific options - */ - public function __construct( - UserStoragesService $storagesService, - $storageId, - $storage, - $mountpoint, - $arguments = null, - $loader = null, - $mountOptions = null - ) { - parent::__construct($storage, $mountpoint, $arguments, $loader, $mountOptions); - $this->storagesService = $storagesService; - $this->numericStorageId = $storageId; - } - - /** - * Move the mount point to $target - * - * @param string $target the target mount point - * @return bool - */ - public function moveMount($target) { - $storage = $this->storagesService->getStorage($this->numericStorageId); - // remove "/$user/files" prefix - $targetParts = explode('/', trim($target, '/'), 3); - $storage->setMountPoint($targetParts[2]); - $this->storagesService->updateStorage($storage); - $this->setMountPoint($target); - return true; - } - - /** - * Remove the mount points - * - * @return bool - */ - public function removeMount() { - $this->storagesService->removeStorage($this->numericStorageId); - return true; - } -} diff --git a/apps/files_external/lib/prioritytrait.php b/apps/files_external/lib/prioritytrait.php deleted file mode 100644 index 9745015bef4..00000000000 --- a/apps/files_external/lib/prioritytrait.php +++ /dev/null @@ -1,60 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -use \OCA\Files_External\Service\BackendService; - -/** - * Trait to implement priority mechanics for a configuration class - */ -trait PriorityTrait { - - /** @var int initial priority */ - protected $priority = BackendService::PRIORITY_DEFAULT; - - /** - * @return int - */ - public function getPriority() { - return $this->priority; - } - - /** - * @param int $priority - * @return self - */ - public function setPriority($priority) { - $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/sessionstoragewrapper.php b/apps/files_external/lib/sessionstoragewrapper.php deleted file mode 100644 index c592cb87a34..00000000000 --- a/apps/files_external/lib/sessionstoragewrapper.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -use \OCP\Files\Storage; -use \OC\Files\Storage\Wrapper\PermissionsMask; -use \OCP\Constants; - -/** - * Wrap Storage in PermissionsMask for session ephemeral use - */ -class SessionStorageWrapper extends PermissionsMask { - - /** - * @param array $arguments ['storage' => $storage] - */ - public function __construct($arguments) { - // disable sharing permission - $arguments['mask'] = Constants::PERMISSION_ALL & ~Constants::PERMISSION_SHARE; - parent::__construct($arguments); - } - -} - diff --git a/apps/files_external/lib/sftp.php b/apps/files_external/lib/sftp.php deleted file mode 100644 index a6984f3b4e0..00000000000 --- a/apps/files_external/lib/sftp.php +++ /dev/null @@ -1,467 +0,0 @@ -<?php -/** - * @author Andreas Fischer <bantu@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author hkjolhede <hkjolhede@gmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lennart Rosam <lennart.rosam@medien-systempartner.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Ross Nicoll <jrn@jrn.me.uk> - * @author SA <stephen@mthosting.net> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\Files\Storage; -use Icewind\Streams\IteratorDirectory; - -use Icewind\Streams\RetryWrapper; -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 { - private $host; - private $user; - private $root; - private $port = 22; - - private $auth; - - /** - * @var SFTP - */ - protected $client; - - /** - * @param string $host protocol://server:port - * @return array [$server, $port] - */ - private function splitHost($host) { - $input = $host; - if (strpos($host, '://') === false) { - // add a protocol to fix parse_url behavior with ipv6 - $host = 'http://' . $host; - } - - $parsed = parse_url($host); - if(is_array($parsed) && isset($parsed['port'])) { - return [$parsed['host'], $parsed['port']]; - } else if (is_array($parsed)) { - return [$parsed['host'], 22]; - } else { - return [$input, 22]; - } - } - - /** - * {@inheritdoc} - */ - public function __construct($params) { - // Register sftp:// - Stream::register(); - - $parsedHost = $this->splitHost($params['host']); - - $this->host = $parsedHost[0]; - $this->port = $parsedHost[1]; - - if (!isset($params['user'])) { - throw new \UnexpectedValueException('no authentication parameters specified'); - } - $this->user = $params['user']; - - if (isset($params['public_key_auth'])) { - $this->auth = $params['public_key_auth']; - } elseif (isset($params['password'])) { - $this->auth = $params['password']; - } else { - throw new \UnexpectedValueException('no authentication parameters specified'); - } - - $this->root - = isset($params['root']) ? $this->cleanPath($params['root']) : '/'; - - if ($this->root[0] != '/') { - $this->root = '/' . $this->root; - } - - if (substr($this->root, -1, 1) != '/') { - $this->root .= '/'; - } - } - - /** - * Returns the connection. - * - * @return \phpseclib\Net\SFTP connected client instance - * @throws \Exception when the connection failed - */ - public function getConnection() { - if (!is_null($this->client)) { - return $this->client; - } - - $hostKeys = $this->readHostKeys(); - $this->client = new \phpseclib\Net\SFTP($this->host, $this->port); - - // The SSH Host Key MUST be verified before login(). - $currentHostKey = $this->client->getServerPublicHostKey(); - if (array_key_exists($this->host, $hostKeys)) { - if ($hostKeys[$this->host] != $currentHostKey) { - throw new \Exception('Host public key does not match known key'); - } - } else { - $hostKeys[$this->host] = $currentHostKey; - $this->writeHostKeys($hostKeys); - } - - if (!$this->client->login($this->user, $this->auth)) { - throw new \Exception('Login failed'); - } - return $this->client; - } - - /** - * {@inheritdoc} - */ - public function test() { - if ( - !isset($this->host) - || !isset($this->user) - ) { - return false; - } - return $this->getConnection()->nlist() !== false; - } - - /** - * {@inheritdoc} - */ - public function getId(){ - $id = 'sftp::' . $this->user . '@' . $this->host; - if ($this->port !== 22) { - $id .= ':' . $this->port; - } - // note: this will double the root slash, - // we should not change it to keep compatible with - // old storage ids - $id .= '/' . $this->root; - return $id; - } - - /** - * @return string - */ - public function getHost() { - return $this->host; - } - - /** - * @return string - */ - public function getRoot() { - return $this->root; - } - - /** - * @return mixed - */ - public function getUser() { - return $this->user; - } - - /** - * @param string $path - * @return string - */ - private function absPath($path) { - return $this->root . $this->cleanPath($path); - } - - /** - * @return string|false - */ - private function hostKeysPath() { - try { - $storage_view = \OCP\Files::getStorage('files_external'); - if ($storage_view) { - return \OC::$server->getConfig()->getSystemValue('datadirectory') . - $storage_view->getAbsolutePath('') . - 'ssh_hostKeys'; - } - } catch (\Exception $e) { - } - return false; - } - - /** - * @param $keys - * @return bool - */ - protected function writeHostKeys($keys) { - try { - $keyPath = $this->hostKeysPath(); - if ($keyPath && file_exists($keyPath)) { - $fp = fopen($keyPath, 'w'); - foreach ($keys as $host => $key) { - fwrite($fp, $host . '::' . $key . "\n"); - } - fclose($fp); - return true; - } - } catch (\Exception $e) { - } - return false; - } - - /** - * @return array - */ - protected function readHostKeys() { - try { - $keyPath = $this->hostKeysPath(); - if (file_exists($keyPath)) { - $hosts = array(); - $keys = array(); - $lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - if ($lines) { - foreach ($lines as $line) { - $hostKeyArray = explode("::", $line, 2); - if (count($hostKeyArray) == 2) { - $hosts[] = $hostKeyArray[0]; - $keys[] = $hostKeyArray[1]; - } - } - return array_combine($hosts, $keys); - } - } - } catch (\Exception $e) { - } - return array(); - } - - /** - * {@inheritdoc} - */ - public function mkdir($path) { - try { - return $this->getConnection()->mkdir($this->absPath($path)); - } catch (\Exception $e) { - return false; - } - } - - /** - * {@inheritdoc} - */ - public function rmdir($path) { - try { - $result = $this->getConnection()->delete($this->absPath($path), true); - // workaround: stray stat cache entry when deleting empty folders - // see https://github.com/phpseclib/phpseclib/issues/706 - $this->getConnection()->clearStatCache(); - return $result; - } catch (\Exception $e) { - return false; - } - } - - /** - * {@inheritdoc} - */ - public function opendir($path) { - try { - $list = $this->getConnection()->nlist($this->absPath($path)); - if ($list === false) { - return false; - } - - $id = md5('sftp:' . $path); - $dirStream = array(); - foreach($list as $file) { - if ($file != '.' && $file != '..') { - $dirStream[] = $file; - } - } - return IteratorDirectory::wrap($dirStream); - } catch(\Exception $e) { - return false; - } - } - - /** - * {@inheritdoc} - */ - public function filetype($path) { - try { - $stat = $this->getConnection()->stat($this->absPath($path)); - if ($stat['type'] == NET_SFTP_TYPE_REGULAR) { - return 'file'; - } - - if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) { - return 'dir'; - } - } catch (\Exception $e) { - - } - return false; - } - - /** - * {@inheritdoc} - */ - public function file_exists($path) { - try { - return $this->getConnection()->stat($this->absPath($path)) !== false; - } catch (\Exception $e) { - return false; - } - } - - /** - * {@inheritdoc} - */ - public function unlink($path) { - try { - return $this->getConnection()->delete($this->absPath($path), true); - } catch (\Exception $e) { - return false; - } - } - - /** - * {@inheritdoc} - */ - public function fopen($path, $mode) { - try { - $absPath = $this->absPath($path); - switch($mode) { - case 'r': - case 'rb': - if ( !$this->file_exists($path)) { - return false; - } - case 'w': - case 'wb': - case 'a': - case 'ab': - case 'r+': - case 'w+': - case 'wb+': - case 'a+': - case 'x': - case 'x+': - case 'c': - case 'c+': - $context = stream_context_create(array('sftp' => array('session' => $this->getConnection()))); - $handle = fopen($this->constructUrl($path), $mode, false, $context); - return RetryWrapper::wrap($handle); - } - } catch (\Exception $e) { - } - return false; - } - - /** - * {@inheritdoc} - */ - public function touch($path, $mtime=null) { - try { - if (!is_null($mtime)) { - return false; - } - if (!$this->file_exists($path)) { - $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) { - $this->getConnection()->get($path, $target); - } - - /** - * @param string $path - * @param string $target - * @throws \Exception - */ - public function uploadFile($path, $target) { - $this->getConnection()->put($target, $path, NET_SFTP_LOCAL_FILE); - } - - /** - * {@inheritdoc} - */ - public function rename($source, $target) { - try { - if (!$this->is_dir($target) && $this->file_exists($target)) { - $this->unlink($target); - } - return $this->getConnection()->rename( - $this->absPath($source), - $this->absPath($target) - ); - } catch (\Exception $e) { - return false; - } - } - - /** - * {@inheritdoc} - */ - public function stat($path) { - try { - $stat = $this->getConnection()->stat($this->absPath($path)); - - $mtime = $stat ? $stat['mtime'] : -1; - $size = $stat ? $stat['size'] : 0; - - return array('mtime' => $mtime, 'size' => $size, 'ctime' => -1); - } catch (\Exception $e) { - return false; - } - } - - /** - * @param string $path - * @return string - */ - public function constructUrl($path) { - // 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; - } -} diff --git a/apps/files_external/lib/smb.php b/apps/files_external/lib/smb.php deleted file mode 100644 index 08c4b25a088..00000000000 --- a/apps/files_external/lib/smb.php +++ /dev/null @@ -1,396 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Jesús Macias <jmacias@solidgear.es> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -use Icewind\SMB\Exception\ConnectException; -use Icewind\SMB\Exception\Exception; -use Icewind\SMB\Exception\ForbiddenException; -use Icewind\SMB\Exception\NotFoundException; -use Icewind\SMB\NativeServer; -use Icewind\SMB\Server; -use Icewind\Streams\CallbackWrapper; -use Icewind\Streams\IteratorDirectory; -use OC\Cache\CappedMemoryCache; -use OC\Files\Filesystem; -use OCP\Files\StorageNotAvailableException; - -class SMB extends Common { - /** - * @var \Icewind\SMB\Server - */ - protected $server; - - /** - * @var \Icewind\SMB\Share - */ - protected $share; - - /** - * @var string - */ - protected $root; - - /** - * @var \Icewind\SMB\FileInfo[] - */ - protected $statCache; - - public function __construct($params) { - if (isset($params['host']) && isset($params['user']) && isset($params['password']) && isset($params['share'])) { - if (Server::NativeAvailable()) { - $this->server = new NativeServer($params['host'], $params['user'], $params['password']); - } else { - $this->server = new Server($params['host'], $params['user'], $params['password']); - } - $this->share = $this->server->getShare(trim($params['share'], '/')); - - $this->root = isset($params['root']) ? $params['root'] : '/'; - if (!$this->root || $this->root[0] != '/') { - $this->root = '/' . $this->root; - } - if (substr($this->root, -1, 1) != '/') { - $this->root .= '/'; - } - } else { - throw new \Exception('Invalid configuration'); - } - $this->statCache = new CappedMemoryCache(); - } - - /** - * @return string - */ - public function getId() { - // FIXME: double slash to keep compatible with the old storage ids, - // failure to do so will lead to creation of a new storage id and - // loss of shares from the storage - return 'smb::' . $this->server->getUser() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root; - } - - /** - * @param string $path - * @return string - */ - protected function buildPath($path) { - return Filesystem::normalizePath($this->root . '/' . $path); - } - - /** - * @param string $path - * @return \Icewind\SMB\IFileInfo - * @throws StorageNotAvailableException - */ - protected function getFileInfo($path) { - try { - $path = $this->buildPath($path); - if (!isset($this->statCache[$path])) { - $this->statCache[$path] = $this->share->stat($path); - } - return $this->statCache[$path]; - } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * @param string $path - * @return \Icewind\SMB\IFileInfo[] - * @throws StorageNotAvailableException - */ - protected function getFolderContents($path) { - try { - $path = $this->buildPath($path); - $files = $this->share->dir($path); - foreach ($files as $file) { - $this->statCache[$path . '/' . $file->getName()] = $file; - } - return $files; - } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * @param \Icewind\SMB\IFileInfo $info - * @return array - */ - protected function formatInfo($info) { - return array( - 'size' => $info->getSize(), - 'mtime' => $info->getMTime() - ); - } - - /** - * @param string $path - * @return array - */ - public function stat($path) { - return $this->formatInfo($this->getFileInfo($path)); - } - - /** - * @param string $path - * @return bool - */ - public function unlink($path) { - try { - if ($this->is_dir($path)) { - return $this->rmdir($path); - } else { - $path = $this->buildPath($path); - unset($this->statCache[$path]); - $this->share->del($path); - return true; - } - } catch (NotFoundException $e) { - return false; - } catch (ForbiddenException $e) { - return false; - } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $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) { - 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; - } - } - - /** - * @param string $path - * @param string $mode - * @return resource - */ - public function fopen($path, $mode) { - $fullPath = $this->buildPath($path); - try { - switch ($mode) { - case 'r': - case 'rb': - if (!$this->file_exists($path)) { - 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) { - unset($this->statCache[$fullPath]); - }); - case 'a': - case 'ab': - 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, '.')); - } else { - $ext = ''; - } - if ($this->file_exists($path)) { - if (!$this->isUpdatable($path)) { - return false; - } - $tmpFile = $this->getCachedFile($path); - } else { - if (!$this->isCreatable(dirname($path))) { - return false; - } - $tmpFile = \OCP\Files::tmpFile($ext); - } - $source = fopen($tmpFile, $mode); - $share = $this->share; - return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) { - unset($this->statCache[$fullPath]); - $share->put($tmpFile, $fullPath); - unlink($tmpFile); - }); - } - return false; - } catch (NotFoundException $e) { - return false; - } catch (ForbiddenException $e) { - return false; - } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); - } - } - - public function rmdir($path) { - try { - $this->statCache = array(); - $content = $this->share->dir($this->buildPath($path)); - foreach ($content as $file) { - if ($file->isDirectory()) { - $this->rmdir($path . '/' . $file->getName()); - } else { - $this->share->del($file->getPath()); - } - } - $this->share->rmdir($this->buildPath($path)); - return true; - } catch (NotFoundException $e) { - return false; - } catch (ForbiddenException $e) { - return false; - } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); - } - } - - public function touch($path, $time = null) { - try { - if (!$this->file_exists($path)) { - $fh = $this->share->write($this->buildPath($path)); - fclose($fh); - return true; - } - return false; - } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); - } - } - - public function opendir($path) { - try { - $files = $this->getFolderContents($path); - } catch (NotFoundException $e) { - return false; - } catch (ForbiddenException $e) { - return false; - } - $names = array_map(function ($info) { - /** @var \Icewind\SMB\IFileInfo $info */ - return $info->getName(); - }, $files); - return IteratorDirectory::wrap($names); - } - - public function filetype($path) { - try { - return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file'; - } catch (NotFoundException $e) { - return false; - } catch (ForbiddenException $e) { - return false; - } - } - - public function mkdir($path) { - $path = $this->buildPath($path); - try { - $this->share->mkdir($path); - return true; - } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); - } catch (Exception $e) { - return false; - } - } - - public function file_exists($path) { - try { - $this->getFileInfo($path); - return true; - } catch (NotFoundException $e) { - return false; - } catch (ForbiddenException $e) { - return false; - } catch (ConnectException $e) { - throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e); - } - } - - public function isReadable($path) { - try { - $info = $this->getFileInfo($path); - return !$info->isHidden(); - } catch (NotFoundException $e) { - return false; - } catch (ForbiddenException $e) { - return false; - } - } - - public function isUpdatable($path) { - try { - $info = $this->getFileInfo($path); - return !$info->isHidden() && !$info->isReadOnly(); - } catch (NotFoundException $e) { - return false; - } catch (ForbiddenException $e) { - return false; - } - } - - /** - * check if smbclient is installed - */ - public static function checkDependencies() { - return ( - (bool)\OC_Helper::findBinaryPath('smbclient') - || Server::NativeAvailable() - ) ? true : ['smbclient']; - } - - /** - * Test a storage for availability - * - * @return bool - */ - public function test() { - try { - return parent::test(); - } catch (Exception $e) { - return false; - } - } -} diff --git a/apps/files_external/lib/storagemodifiertrait.php b/apps/files_external/lib/storagemodifiertrait.php deleted file mode 100644 index 30c2108feec..00000000000 --- a/apps/files_external/lib/storagemodifiertrait.php +++ /dev/null @@ -1,69 +0,0 @@ -<?php -/** - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_External\Lib; - -use \OCP\IUser; -use \OCP\Files\Storage; -use \OCA\Files_External\Lib\StorageConfig; -use \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; -use \OCP\Files\StorageNotAvailableException; - -/** - * Trait for objects that can modify StorageConfigs and wrap Storages - * - * When a storage implementation is being prepared for use, the StorageConfig - * is passed through manipulateStorageConfig() to update any parameters as - * necessary. After the storage implementation has been constructed, it is - * passed through wrapStorage(), potentially replacing the implementation with - * a wrapped storage that changes its behaviour. - * - * Certain configuration options need to be set before the implementation is - * constructed, while others are retrieved directly from the storage - * implementation and so need a wrapper to be modified. - */ -trait StorageModifierTrait { - - /** - * Modify a StorageConfig parameters - * - * @param StorageConfig $storage - * @param IUser $user User the storage is being used as - * @throws InsufficientDataForMeaningfulAnswerException - * @throws StorageNotAvailableException - */ - public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { - } - - /** - * Wrap a Storage if necessary - * - * @param Storage $storage - * @return Storage - * @throws InsufficientDataForMeaningfulAnswerException - * @throws StorageNotAvailableException - */ - public function wrapStorage(Storage $storage) { - return $storage; - } - -} - diff --git a/apps/files_external/lib/streamwrapper.php b/apps/files_external/lib/streamwrapper.php deleted file mode 100644 index efb51f32ba4..00000000000 --- a/apps/files_external/lib/streamwrapper.php +++ /dev/null @@ -1,128 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -abstract class StreamWrapper extends Common { - - /** - * @param string $path - * @return string|null - */ - abstract public function constructUrl($path); - - public function mkdir($path) { - return mkdir($this->constructUrl($path)); - } - - public function rmdir($path) { - if ($this->is_dir($path) && $this->isDeletable($path)) { - $dh = $this->opendir($path); - if (!is_resource($dh)) { - return false; - } - while (($file = readdir($dh)) !== false) { - if ($this->is_dir($path . '/' . $file)) { - $this->rmdir($path . '/' . $file); - } else { - $this->unlink($path . '/' . $file); - } - } - $url = $this->constructUrl($path); - $success = rmdir($url); - clearstatcache(false, $url); - return $success; - } else { - return false; - } - } - - public function opendir($path) { - return opendir($this->constructUrl($path)); - } - - public function filetype($path) { - return @filetype($this->constructUrl($path)); - } - - public function file_exists($path) { - return file_exists($this->constructUrl($path)); - } - - public function unlink($path) { - $url = $this->constructUrl($path); - $success = unlink($url); - // normally unlink() is supposed to do this implicitly, - // but doing it anyway just to be sure - clearstatcache(false, $url); - return $success; - } - - public function fopen($path, $mode) { - return fopen($this->constructUrl($path), $mode); - } - - public function touch($path, $mtime = null) { - if ($this->file_exists($path)) { - if (is_null($mtime)) { - $fh = $this->fopen($path, 'a'); - fwrite($fh, ''); - fclose($fh); - - return true; - } else { - return false; //not supported - } - } else { - $this->file_put_contents($path, ''); - return true; - } - } - - /** - * @param string $path - * @param string $target - */ - public function getFile($path, $target) { - return copy($this->constructUrl($path), $target); - } - - /** - * @param string $target - */ - public function uploadFile($path, $target) { - return copy($path, $this->constructUrl($target)); - } - - public function rename($path1, $path2) { - return rename($this->constructUrl($path1), $this->constructUrl($path2)); - } - - public function stat($path) { - return stat($this->constructUrl($path)); - } - -} diff --git a/apps/files_external/lib/swift.php b/apps/files_external/lib/swift.php deleted file mode 100644 index 9282fe28669..00000000000 --- a/apps/files_external/lib/swift.php +++ /dev/null @@ -1,599 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @author Benjamin Liles <benliles@arch.tamu.edu> - * @author Christian Berendt <berendt@b1-systems.de> - * @author Daniel Tosello <tosello.daniel@gmail.com> - * @author Felix Moeller <mail@felixmoeller.de> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Martin Mattel <martin.mattel@diemattels.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tim Dettrick <t.dettrick@uq.edu.au> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -use Guzzle\Http\Url; -use Guzzle\Http\Exception\ClientErrorResponseException; -use Icewind\Streams\IteratorDirectory; -use OpenCloud; -use OpenCloud\Common\Exceptions; -use OpenCloud\OpenStack; -use OpenCloud\Rackspace; -use OpenCloud\ObjectStore\Resource\DataObject; -use OpenCloud\ObjectStore\Exception; - -class Swift extends \OC\Files\Storage\Common { - - /** - * @var \OpenCloud\ObjectStore\Service - */ - private $connection; - /** - * @var \OpenCloud\ObjectStore\Resource\Container - */ - private $container; - /** - * @var \OpenCloud\OpenStack - */ - private $anchor; - /** - * @var string - */ - private $bucket; - /** - * Connection parameters - * - * @var array - */ - private $params; - /** - * @var array - */ - private static $tmpFiles = array(); - - /** - * @param string $path - */ - private function normalizePath($path) { - $path = trim($path, '/'); - - if (!$path) { - $path = '.'; - } - - $path = str_replace('#', '%23', $path); - - return $path; - } - - const SUBCONTAINER_FILE = '.subcontainers'; - - /** - * translate directory path to container name - * - * @param string $path - * @return string - */ - private function getContainerName($path) { - $path = trim(trim($this->root, '/') . "/" . $path, '/.'); - return str_replace('/', '\\', $path); - } - - /** - * @param string $path - */ - private function doesObjectExist($path) { - try { - $this->getContainer()->getPartialObject($path); - return true; - } catch (ClientErrorResponseException $e) { - // Expected response is "404 Not Found", so only log if it isn't - if ($e->getResponse()->getStatusCode() !== 404) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - } - return false; - } - } - - public function __construct($params) { - if ((empty($params['key']) and empty($params['password'])) - or empty($params['user']) or empty($params['bucket']) - or empty($params['region']) - ) { - throw new \Exception("API Key or password, Username, Bucket and Region have to be configured."); - } - - $this->id = 'swift::' . $params['user'] . md5($params['bucket']); - - $bucketUrl = Url::factory($params['bucket']); - if ($bucketUrl->isAbsolute()) { - $this->bucket = end(($bucketUrl->getPathSegments())); - $params['endpoint_url'] = $bucketUrl->addPath('..')->normalizePath(); - } else { - $this->bucket = $params['bucket']; - } - - if (empty($params['url'])) { - $params['url'] = 'https://identity.api.rackspacecloud.com/v2.0/'; - } - - if (empty($params['service_name'])) { - $params['service_name'] = 'cloudFiles'; - } - - $this->params = $params; - } - - public function mkdir($path) { - $path = $this->normalizePath($path); - - if ($this->is_dir($path)) { - return false; - } - - if ($path !== '.') { - $path .= '/'; - } - - try { - $customHeaders = array('content-type' => 'httpd/unix-directory'); - $metadataHeaders = DataObject::stockHeaders(array()); - $allHeaders = $customHeaders + $metadataHeaders; - $this->getContainer()->uploadObject($path, '', $allHeaders); - } catch (Exceptions\CreateUpdateError $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - return false; - } - - return true; - } - - public function file_exists($path) { - $path = $this->normalizePath($path); - - if ($path !== '.' && $this->is_dir($path)) { - $path .= '/'; - } - - return $this->doesObjectExist($path); - } - - public function rmdir($path) { - $path = $this->normalizePath($path); - - if (!$this->is_dir($path) || !$this->isDeletable($path)) { - return false; - } - - $dh = $this->opendir($path); - while ($file = readdir($dh)) { - if (\OC\Files\Filesystem::isIgnoredDir($file)) { - continue; - } - - if ($this->is_dir($path . '/' . $file)) { - $this->rmdir($path . '/' . $file); - } else { - $this->unlink($path . '/' . $file); - } - } - - try { - $this->getContainer()->dataObject()->setName($path . '/')->delete(); - } catch (Exceptions\DeleteError $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - return false; - } - - return true; - } - - public function opendir($path) { - $path = $this->normalizePath($path); - - if ($path === '.') { - $path = ''; - } else { - $path .= '/'; - } - - $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of # - - try { - $files = array(); - /** @var OpenCloud\Common\Collection $objects */ - $objects = $this->getContainer()->objectList(array( - 'prefix' => $path, - 'delimiter' => '/' - )); - - /** @var OpenCloud\ObjectStore\Resource\DataObject $object */ - foreach ($objects as $object) { - $file = basename($object->getName()); - if ($file !== basename($path)) { - $files[] = $file; - } - } - - return IteratorDirectory::wrap($files); - } catch (\Exception $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - return false; - } - - } - - public function stat($path) { - $path = $this->normalizePath($path); - - if ($path === '.') { - $path = ''; - } else if ($this->is_dir($path)) { - $path .= '/'; - } - - try { - /** @var DataObject $object */ - $object = $this->getContainer()->getPartialObject($path); - } catch (ClientErrorResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - return false; - } - - $dateTime = \DateTime::createFromFormat(\DateTime::RFC1123, $object->getLastModified()); - if ($dateTime !== false) { - $mtime = $dateTime->getTimestamp(); - } else { - $mtime = null; - } - $objectMetadata = $object->getMetadata(); - $metaTimestamp = $objectMetadata->getProperty('timestamp'); - if (isset($metaTimestamp)) { - $mtime = $metaTimestamp; - } - - if (!empty($mtime)) { - $mtime = floor($mtime); - } - - $stat = array(); - $stat['size'] = (int)$object->getContentLength(); - $stat['mtime'] = $mtime; - $stat['atime'] = time(); - return $stat; - } - - public function filetype($path) { - $path = $this->normalizePath($path); - - if ($path !== '.' && $this->doesObjectExist($path)) { - return 'file'; - } - - if ($path !== '.') { - $path .= '/'; - } - - if ($this->doesObjectExist($path)) { - return 'dir'; - } - } - - public function unlink($path) { - $path = $this->normalizePath($path); - - if ($this->is_dir($path)) { - return $this->rmdir($path); - } - - try { - $this->getContainer()->dataObject()->setName($path)->delete(); - } catch (ClientErrorResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - return false; - } - - return true; - } - - public function fopen($path, $mode) { - $path = $this->normalizePath($path); - - switch ($mode) { - case 'r': - case 'rb': - try { - $c = $this->getContainer(); - $streamFactory = new \Guzzle\Stream\PhpStreamRequestFactory(); - $streamInterface = $streamFactory->fromRequest( - $c->getClient() - ->get($c->getUrl($path))); - $streamInterface->rewind(); - $stream = $streamInterface->getStream(); - stream_context_set_option($stream, 'swift','content', $streamInterface); - if(!strrpos($streamInterface - ->getMetaData('wrapper_data')[0], '404 Not Found')) { - return $stream; - } - return false; - } catch (\Guzzle\Http\Exception\BadResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - return false; - } - case 'w': - case 'wb': - case 'a': - case 'ab': - case 'r+': - case 'w+': - case 'wb+': - case 'a+': - case 'x': - case 'x+': - case 'c': - case 'c+': - if (strrpos($path, '.') !== false) { - $ext = substr($path, strrpos($path, '.')); - } else { - $ext = ''; - } - $tmpFile = \OCP\Files::tmpFile($ext); - \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); - // Fetch existing file if required - if ($mode[0] !== 'w' && $this->file_exists($path)) { - if ($mode[0] === 'x') { - // File cannot already exist - return false; - } - $source = $this->fopen($path, 'r'); - file_put_contents($tmpFile, $source); - // Seek to end if required - if ($mode[0] === 'a') { - fseek($tmpFile, 0, SEEK_END); - } - } - self::$tmpFiles[$tmpFile] = $path; - - return fopen('close://' . $tmpFile, $mode); - } - } - - public function touch($path, $mtime = null) { - $path = $this->normalizePath($path); - if (is_null($mtime)) { - $mtime = time(); - } - $metadata = array('timestamp' => $mtime); - if ($this->file_exists($path)) { - if ($this->is_dir($path) && $path != '.') { - $path .= '/'; - } - - $object = $this->getContainer()->getPartialObject($path); - $object->saveMetadata($metadata); - return true; - } else { - $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path); - $customHeaders = array('content-type' => $mimeType); - $metadataHeaders = DataObject::stockHeaders($metadata); - $allHeaders = $customHeaders + $metadataHeaders; - $this->getContainer()->uploadObject($path, '', $allHeaders); - return true; - } - } - - public function copy($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); - - $fileType = $this->filetype($path1); - if ($fileType === 'file') { - - // make way - $this->unlink($path2); - - try { - $source = $this->getContainer()->getPartialObject($path1); - $source->copy($this->bucket . '/' . $path2); - } catch (ClientErrorResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - return false; - } - - } else if ($fileType === 'dir') { - - // make way - $this->unlink($path2); - - try { - $source = $this->getContainer()->getPartialObject($path1 . '/'); - $source->copy($this->bucket . '/' . $path2 . '/'); - } catch (ClientErrorResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); - return false; - } - - $dh = $this->opendir($path1); - while ($file = readdir($dh)) { - if (\OC\Files\Filesystem::isIgnoredDir($file)) { - continue; - } - - $source = $path1 . '/' . $file; - $target = $path2 . '/' . $file; - $this->copy($source, $target); - } - - } else { - //file does not exist - return false; - } - - return true; - } - - public function rename($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); - - $fileType = $this->filetype($path1); - - if ($fileType === 'dir' || $fileType === 'file') { - - // make way - $this->unlink($path2); - - // copy - if ($this->copy($path1, $path2) === false) { - return false; - } - - // cleanup - if ($this->unlink($path1) === false) { - $this->unlink($path2); - return false; - } - - return true; - } - - return false; - } - - public function getId() { - return $this->id; - } - - /** - * Returns the connection - * - * @return OpenCloud\ObjectStore\Service connected client - * @throws \Exception if connection could not be made - */ - public function getConnection() { - if (!is_null($this->connection)) { - return $this->connection; - } - - $settings = array( - 'username' => $this->params['user'], - ); - - if (!empty($this->params['password'])) { - $settings['password'] = $this->params['password']; - } else if (!empty($this->params['key'])) { - $settings['apiKey'] = $this->params['key']; - } - - if (!empty($this->params['tenant'])) { - $settings['tenantName'] = $this->params['tenant']; - } - - if (!empty($this->params['timeout'])) { - $settings['timeout'] = $this->params['timeout']; - } - - if (isset($settings['apiKey'])) { - $this->anchor = new Rackspace($this->params['url'], $settings); - } else { - $this->anchor = new OpenStack($this->params['url'], $settings); - } - - $connection = $this->anchor->objectStoreService($this->params['service_name'], $this->params['region']); - - if (!empty($this->params['endpoint_url'])) { - $endpoint = $connection->getEndpoint(); - $endpoint->setPublicUrl($this->params['endpoint_url']); - $endpoint->setPrivateUrl($this->params['endpoint_url']); - $connection->setEndpoint($endpoint); - } - - $this->connection = $connection; - - return $this->connection; - } - - /** - * Returns the initialized object store container. - * - * @return OpenCloud\ObjectStore\Resource\Container - */ - public function getContainer() { - if (!is_null($this->container)) { - return $this->container; - } - - try { - $this->container = $this->getConnection()->getContainer($this->bucket); - } catch (ClientErrorResponseException $e) { - $this->container = $this->getConnection()->createContainer($this->bucket); - } - - if (!$this->file_exists('.')) { - $this->mkdir('.'); - } - - return $this->container; - } - - public function writeBack($tmpFile) { - if (!isset(self::$tmpFiles[$tmpFile])) { - return false; - } - $fileData = fopen($tmpFile, 'r'); - $this->getContainer()->uploadObject(self::$tmpFiles[$tmpFile], $fileData); - unlink($tmpFile); - } - - public function hasUpdated($path, $time) { - if ($this->is_file($path)) { - return parent::hasUpdated($path, $time); - } - $path = $this->normalizePath($path); - $dh = $this->opendir($path); - $content = array(); - while (($file = readdir($dh)) !== false) { - $content[] = $file; - } - if ($path === '.') { - $path = ''; - } - $cachedContent = $this->getCache()->getFolderContents($path); - $cachedNames = array_map(function ($content) { - return $content['name']; - }, $cachedContent); - sort($cachedNames); - sort($content); - return $cachedNames != $content; - } - - /** - * check if curl is installed - */ - public static function checkDependencies() { - return true; - } - -} |