migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,181 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Ross Nicoll <jrn@jrn.me.uk>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\Lib\Auth\AmazonS3\AccessKey;
use OCA\Files_External\Lib\Auth\Builtin;
use OCA\Files_External\Lib\Auth\NullMechanism;
use OCA\Files_External\Lib\Auth\OAuth1\OAuth1;
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\Files\Config\IMountProviderCollection;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\User\Events\UserDeletedEvent;
require_once __DIR__ . '/../../3rdparty/autoload.php';
/**
* @package OCA\Files_External\AppInfo
*/
class Application extends App implements IBackendProvider, IAuthMechanismProvider, IBootstrap {
public const APP_ID = 'files_external';
/**
* Application constructor.
*
* @throws \OCP\AppFramework\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);
}
public function boot(IBootContext $context): void {
$context->injectFn(function (IMountProviderCollection $mountProviderCollection, ConfigAdapter $configAdapter) {
$mountProviderCollection->registerProvider($configAdapter);
});
$context->injectFn(function (BackendService $backendService, UserPlaceholderHandler $userConfigHandler) {
$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_OAUTH1 mechanisms
$container->get(OAuth1::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),
];
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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\IJob;
use OCP\BackgroundJob\TimedJob;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ICredentialsManager;
class CredentialsCleanup extends TimedJob {
private $credentialsManager;
private $userGlobalStoragesService;
private $userManager;
public function __construct(
ITimeFactory $time,
ICredentialsManager $credentialsManager,
UserGlobalStoragesService $userGlobalStoragesService,
IUserManager $userManager
) {
parent::__construct($time);
$this->credentialsManager = $credentialsManager;
$this->userGlobalStoragesService = $userGlobalStoragesService;
$this->userManager = $userManager;
// run every day
$this->setInterval(24 * 60 * 60);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
}
protected function run($argument) {
$this->userManager->callForSeenUsers(function (IUser $user) {
$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);
}
});
}
}
@@ -0,0 +1,141 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\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;
use Symfony\Component\HttpFoundation\Response;
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 Response::HTTP_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 Response::HTTP_NOT_FOUND;
}
}
foreach ($addGroups as $addGroup) {
if (!$this->groupManager->groupExists($addGroup)) {
$output->writeln('<error>Group "' . $addGroup . '" not found</error>');
return Response::HTTP_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;
}
}
@@ -0,0 +1,120 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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->getType() !== DefinitionParameter::VALUE_HIDDEN;
});
return array_map(function (DefinitionParameter $parameter) {
return $parameter->getTypeName();
}, $configuration);
}
}
@@ -0,0 +1,113 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Ardinis <Ardinis@users.noreply.github.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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 Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpFoundation\Response;
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 Response::HTTP_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($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);
}
}
@@ -0,0 +1,201 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\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;
use Symfony\Component\HttpFoundation\Response;
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 Response::HTTP_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 Response::HTTP_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;
}
}
@@ -0,0 +1,95 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\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;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\HttpFoundation\Response;
class Delete extends Base {
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: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 Response::HTTP_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);
$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;
}
}
@@ -0,0 +1,59 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
}
}
@@ -0,0 +1,196 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
}
}
@@ -0,0 +1,257 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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 = ['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, '***');
}
}
}
}
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;
}
}
@@ -0,0 +1,342 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
*
* @author Ari Selseng <ari@selseng.net>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Command;
use Doctrine\DBAL\Exception\DriverException;
use OC\Core\Command\Base;
use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
use OCA\Files_External\Lib\StorageConfig;
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\Files\StorageNotAvailableException;
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 Base {
public function __construct(
private GlobalStoragesService $globalService,
private IDBConnection $connection,
private LoggerInterface $logger,
private IUserManager $userManager
) {
parent::__construct();
}
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();
}
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;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$mount = $this->globalService->getStorage($input->getArgument('mount_id'));
if (is_null($mount)) {
$output->writeln('<error>Mount not found</error>');
return self::FAILURE;
}
$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 {
$storage = $this->createStorage($mount);
} 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 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) {
$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 createStorage(StorageConfig $mount): IStorage {
$class = $mount->getBackend()->getStorageClass();
return new $class($mount->getBackendOptions());
}
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((string)\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((string)\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>');
}
}
}
@@ -0,0 +1,72 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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($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);
}
}
@@ -0,0 +1,138 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\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;
use Symfony\Component\HttpFoundation\Response;
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 Response::HTTP_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(
\OCA\Files_External\MountConfig::getBackendStatus(
$backend->getStorageClass(),
$storage->getBackendOptions(),
false
)
);
} 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()
);
}
}
}
@@ -0,0 +1,176 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Config;
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\Service\UserGlobalStoragesService;
use OCA\Files_External\Service\UserStoragesService;
use OCP\Files\Config\IMountProvider;
use OCP\Files\Storage;
use OCP\Files\Storage\IStorageFactory;
use OCP\Files\StorageNotAvailableException;
use OCP\IUser;
use Psr\Clock\ClockInterface;
/**
* 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,
) {
}
/**
* Process storage ready for mounting
*
* @param StorageConfig $storage
* @param IUser $user
* @throws \OCP\AppFramework\QueryException
*/
private function prepareStorageConfig(StorageConfig &$storage, IUser $user) {
foreach ($storage->getBackendOptions() as $option => $value) {
$storage->setBackendOption($option, \OCA\Files_External\MountConfig::substitutePlaceholdersInConfig($value, $user->getUID()));
}
$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->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);
\OC\Files\Cache\Storage::getGlobalCache()->loadForStorageIds(array_map(function (Storage\IStorage $storage) {
return $storage->getId();
}, $storages));
$availableStorages = array_map(function (Storage\IStorage $storage, StorageConfig $storageConfig) {
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, Storage\IStorage $storage) use ($user, $loader) {
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;
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Julius Härtl <jus@bitgrid.net>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
/** @var StorageConfig */
protected $storageConfig;
public function __construct(StorageConfig $storageConfig, $storage, $mountpoint, $arguments = null, $loader = null, $mountOptions = null, $mountId = null) {
$this->storageConfig = $storageConfig;
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;
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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);
}
@@ -0,0 +1,87 @@
<?php
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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;
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Config;
use OCP\Files\Mount\ISystemMountPoint;
class SystemMountPoint extends ExternalMountPoint implements ISystemMountPoint {
}
@@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 IUserSession */
private $session;
/** @var ShareManager */
private $shareManager;
/** @var IRequest */
private $request;
/** @var string */
private $userId;
/** @var IUserManager */
private $userManager;
public function __construct(IUserSession $session, ShareManager $manager, IRequest $request, IUserManager $userManager) {
$this->session = $session;
$this->shareManager = $manager;
$this->request = $request;
$this->userManager = $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 && $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;
}
}
@@ -0,0 +1,43 @@
<?php
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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);
}
}
@@ -0,0 +1,120 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Martin Mattel <martin.mattel@diemattels.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Ross Nicoll <jrn@jrn.me.uk>
*
* @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\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\JSONResponse;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IUserSession;
class AjaxController extends Controller {
/** @var RSA */
private $rsaMechanism;
/** @var GlobalAuth */
private $globalAuth;
/** @var IUserSession */
private $userSession;
/** @var IGroupManager */
private $groupManager;
/**
* @param string $appName
* @param IRequest $request
* @param RSA $rsaMechanism
* @param GlobalAuth $globalAuth
* @param IUserSession $userSession
* @param IGroupManager $groupManager
*/
public function __construct($appName,
IRequest $request,
RSA $rsaMechanism,
GlobalAuth $globalAuth,
IUserSession $userSession,
IGroupManager $groupManager) {
parent::__construct($appName, $request);
$this->rsaMechanism = $rsaMechanism;
$this->globalAuth = $globalAuth;
$this->userSession = $userSession;
$this->groupManager = $groupManager;
}
/**
* @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.
*
* @NoAdminRequired
* @param int $keyLength
*/
public function getSshKeys($keyLength = 1024) {
$key = $this->generateSshKeys($keyLength);
return new JSONResponse(
['data' => [
'private_key' => $key['privatekey'],
'public_key' => $key['publickey']
],
'status' => 'success'
]);
}
/**
* @NoAdminRequired
*
* @param string $uid
* @param string $user
* @param string $password
* @return bool
*/
public function saveGlobalCredentials($uid, $user, $password) {
$currentUser = $this->userSession->getUser();
// Non-admins can only edit their own credentials
$allowedToEdit = ($currentUser->getUID() === $uid);
if ($allowedToEdit) {
$this->globalAuth->saveAuth($uid, $user, $password);
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jesús Macias <jmacias@solidgear.es>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
/**
* @psalm-import-type Files_ExternalMount from ResponseDefinitions
*/
class ApiController extends OCSController {
private UserGlobalStoragesService $userGlobalStoragesService;
private UserStoragesService $userStoragesService;
public function __construct(
string $appName,
IRequest $request,
UserGlobalStoragesService $userGlobalStorageService,
UserStoragesService $userStorageService
) {
parent::__construct($appName, $request);
$this->userGlobalStoragesService = $userGlobalStorageService;
$this->userStoragesService = $userStorageService;
}
/**
* 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 = \OCP\Constants::PERMISSION_READ;
// personal mounts can be deleted
if (!$isSystemMount) {
$permissions |= \OCP\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;
}
/**
* @NoAdminRequired
*
* Get the mount points visible for this user
*
* @return DataResponse<Http::STATUS_OK, Files_ExternalMount[], array{}>
*
* 200: User mounts returned
*/
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);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Ask for credentials using a browser's native basic auth prompt
* Then returns it if provided
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
public function askNativeAuth(): DataResponse {
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
$response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
$response->addHeader('WWW-Authenticate', 'Basic realm="Storage authentification needed"');
return $response;
}
$user = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
// Reset auth
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
// Using 401 again to ensure we clear any cached Authorization
return new DataResponse([
'user' => $user,
'password' => $password,
], Http::STATUS_UNAUTHORIZED);
}
}
@@ -0,0 +1,208 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Controller;
use OCA\Files_External\NotFoundException;
use OCA\Files_External\Service\GlobalStoragesService;
use OCP\AppFramework\Http;
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
*/
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
* @param bool $testOnly whether to storage should only test the connection or do more things
*
* @return DataResponse
*/
public function update(
$id,
$mountPoint,
$backend,
$authMechanism,
$backendOptions,
$mountOptions,
$applicableUsers,
$applicableGroups,
$priority,
$testOnly = true
) {
$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, $testOnly);
return new DataResponse(
$storage->jsonSerialize(true),
Http::STATUS_OK
);
}
}
@@ -0,0 +1,339 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Jesús Macias <jmacias@solidgear.es>
* @author Joas Schilling <coding@schilljs.com>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\NotFoundException;
use OCA\Files_External\Service\StoragesService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
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
* @param bool $testOnly whether to storage should only test the connection or do more things
*/
protected function updateStorageStatus(StorageConfig &$storage, $testOnly = true) {
try {
$this->manipulateStorageConfig($storage);
/** @var Backend */
$backend = $storage->getBackend();
// update status (can be time-consuming)
$storage->setStatus(
\OCA\Files_External\MountConfig::getBackendStatus(
$backend->getStorageClass(),
$storage->getBackendOptions(),
false,
$testOnly
)
);
} catch (InsufficientDataForMeaningfulAnswerException $e) {
$status = $e->getCode() ? $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
* @param bool $testOnly whether to storage should only test the connection or do more things
*
* @return DataResponse
*/
public function show($id, $testOnly = true) {
try {
$storage = $this->service->getStorage($id);
$this->updateStorageStatus($storage, $testOnly);
} 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
*/
public function destroy($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);
}
}
@@ -0,0 +1,215 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\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
* @param bool $testOnly whether to storage should only test the connection or do more things
* @return DataResponse
*
* @NoAdminRequired
*/
public function show($id, $testOnly = true) {
try {
$storage = $this->service->getStorage($id);
$this->updateStorageStatus($storage, $testOnly);
} 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
* @param bool $testOnly whether to storage should only test the connection or do more things
*
* @return DataResponse
*
* @NoAdminRequired
*/
public function update(
$id,
$backendOptions,
$testOnly = true
) {
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 user editable', [$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, $testOnly);
$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
}
}
}
}
@@ -0,0 +1,237 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\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
*
* @NoAdminRequired
*
* @return DataResponse
*/
public function index() {
return parent::index();
}
/**
* Return storage
*
* @NoAdminRequired
*
* {@inheritdoc}
*/
public function show($id, $testOnly = true) {
return parent::show($id, $testOnly);
}
/**
* 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
*/
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
* @param bool $testOnly whether to storage should only test the connection or do more things
*
* @return DataResponse
*
* @NoAdminRequired
*/
public function update(
$id,
$mountPoint,
$backend,
$authMechanism,
$backendOptions,
$mountOptions,
$testOnly = true
) {
$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, $testOnly);
return new DataResponse(
$storage->jsonSerialize(true),
Http::STATUS_OK
);
}
/**
* Delete storage
*
* @NoAdminRequired
*
* {@inheritdoc}
*/
public function destroy($id) {
return parent::destroy($id);
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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),
]);
}
}
@@ -0,0 +1,121 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Auth;
use OCA\Files_External\Lib\FrontendDefinitionTrait;
use OCA\Files_External\Lib\IdentifierTrait;
use OCA\Files_External\Lib\IFrontendDefinition;
use OCA\Files_External\Lib\IIdentifier;
use OCA\Files_External\Lib\StorageConfig;
use OCA\Files_External\Lib\StorageModifierTrait;
use OCA\Files_External\Lib\VisibilityTrait;
/**
* Authentication mechanism
*
* An authentication mechanism can have services injected during construction,
* such as \OCP\IDB for database operations. This allows an authentication
* mechanism to perform advanced operations based on provided information.
*
* 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.
*
* This class uses the following traits:
* - VisibilityTrait
* Restrict usage to admin-only/none
* - FrontendDefinitionTrait
* Specify configuration parameters and other definitions
* - StorageModifierTrait
* Object can affect storage mounting
*/
class AuthMechanism implements \JsonSerializable, IIdentifier, IFrontendDefinition {
/** Standard authentication schemes */
public const SCHEME_NULL = 'null';
public const SCHEME_BUILTIN = 'builtin';
public const SCHEME_PASSWORD = 'password';
public const SCHEME_OAUTH1 = 'oauth1';
public const SCHEME_OAUTH2 = 'oauth2';
public const SCHEME_PUBLICKEY = 'publickey';
public const SCHEME_OPENSTACK = 'openstack';
public const SCHEME_SMB = 'smb';
use VisibilityTrait;
use FrontendDefinitionTrait;
use StorageModifierTrait;
use IdentifierTrait;
/** @var string */
protected $scheme;
/**
* Get the authentication scheme implemented
* See self::SCHEME_* constants
*
* @return string
*/
public function getScheme() {
return $this->scheme;
}
/**
* @param string $scheme
* @return $this
*/
public function setScheme($scheme) {
$this->scheme = $scheme;
return $this;
}
/**
* Serialize into JSON for client-side JS
*/
public function jsonSerialize(): array {
$data = $this->jsonSerializeDefinition();
$data += $this->jsonSerializeIdentifier();
$data['scheme'] = $this->getScheme();
$data['visibility'] = $this->getVisibility();
return $data;
}
/**
* Check if parameters are satisfied in a StorageConfig
*
* @param StorageConfig $storage
* @return bool
*/
public function validateStorage(StorageConfig $storage) {
// does the backend actually support this scheme
$supportedSchemes = $storage->getBackend()->getAuthSchemes();
if (!isset($supportedSchemes[$this->getScheme()])) {
return false;
}
return $this->validateStorageDefinition($storage);
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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'))
;
}
}
@@ -0,0 +1,36 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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);
}
@@ -0,0 +1,43 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud GmbH.
*
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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)
;
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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'))
;
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Auth\OAuth1;
use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\DefinitionParameter;
use OCP\IL10N;
/**
* OAuth1 authentication
*/
class OAuth1 extends AuthMechanism {
public function __construct(IL10N $l) {
$this
->setIdentifier('oauth1::oauth1')
->setScheme(self::SCHEME_OAUTH1)
->setText($l->t('OAuth1'))
->addParameters([
(new DefinitionParameter('configured', 'configured'))
->setType(DefinitionParameter::VALUE_HIDDEN),
new DefinitionParameter('app_key', $l->t('App key')),
(new DefinitionParameter('app_secret', $l->t('App secret')))
->setType(DefinitionParameter::VALUE_PASSWORD),
(new DefinitionParameter('token', 'token'))
->setType(DefinitionParameter::VALUE_HIDDEN),
(new DefinitionParameter('token_secret', 'token_secret'))
->setType(DefinitionParameter::VALUE_HIDDEN),
])
->addCustomJs('oauth1')
;
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Auth\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_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),
])
->addCustomJs('oauth2')
;
}
}
@@ -0,0 +1,49 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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('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')),
])
;
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @author Julien Lutran <julien.lutran@corp.ovh.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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('Username')),
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'))
])
;
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Auth\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('Username')),
(new DefinitionParameter('key', $l->t('API key')))
->setType(DefinitionParameter::VALUE_PASSWORD),
])
;
}
}
@@ -0,0 +1,88 @@
<?php
/**
* @copyright Copyright (c) 2015, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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';
/** @var ICredentialsManager */
protected $credentialsManager;
public function __construct(IL10N $l, ICredentialsManager $credentialsManager) {
$this->credentialsManager = $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 {
return $auth;
}
}
public function saveAuth($uid, $user, $password) {
$this->credentialsManager->store($uid, self::CREDENTIALS_IDENTIFIER, [
'user' => $user,
'password' => $password
]);
}
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']);
}
}
}
@@ -0,0 +1,142 @@
<?php
/**
* @copyright Copyright (c) 2015, ownCloud, Inc.
*
* @author blizzz <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 OCA\Files_External\Lib\Auth\AuthMechanism;
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';
/** @var ISession */
protected $session;
/** @var ICredentialsManager */
protected $credentialsManager;
/** @var CredentialsStore */
private $credentialsStore;
/** @var ILDAPProviderFactory */
private $ldapFactory;
public function __construct(
IL10N $l,
ISession $session,
ICredentialsManager $credentialsManager,
CredentialsStore $credentialsStore,
IEventDispatcher $eventDispatcher,
ILDAPProviderFactory $ldapFactory
) {
$this->session = $session;
$this->credentialsManager = $credentialsManager;
$this->credentialsStore = $credentialsStore;
$this->ldapFactory = $ldapFactory;
$this
->setIdentifier('password::logincredentials')
->setScheme(self::SCHEME_PASSWORD)
->setText($l->t('Log-in credentials, save in database'))
->addParameters([
]);
$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;
}
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);
}
}
@@ -0,0 +1,45 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Auth\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('Username and password'))
->addParameters([
new DefinitionParameter('user', $l->t('Username')),
(new DefinitionParameter('password', $l->t('Password')))
->setType(DefinitionParameter::VALUE_PASSWORD),
]);
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\SessionStorageWrapper;
use OCA\Files_External\Lib\StorageConfig;
use OCP\Authentication\Exceptions\CredentialsUnavailableException;
use OCP\Authentication\LoginCredentials\IStore as CredentialsStore;
use OCP\Files\Storage;
use OCP\Files\StorageAuthException;
use OCP\IL10N;
use OCP\IUser;
/**
* Username and password from login credentials, saved in session
*/
class SessionCredentials extends AuthMechanism {
/** @var CredentialsStore */
private $credentialsStore;
public function __construct(IL10N $l, CredentialsStore $credentialsStore) {
$this->credentialsStore = $credentialsStore;
$this->setIdentifier('password::sessioncredentials')
->setScheme(self::SCHEME_PASSWORD)
->setText($l->t('Log-in credentials, save in session'))
->addParameters([]);
}
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
try {
$credentials = $this->credentialsStore->getLoginCredentials();
} catch (CredentialsUnavailableException $e) {
throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved');
}
if ($user === null) {
throw new StorageAuthException('Session unavailable');
}
if ($credentials->getUID() !== $user->getUID()) {
throw new StorageAuthException('Session credentials for storage owner not available');
}
$storage->setBackendOption('user', $credentials->getLoginName());
$storage->setBackendOption('password', $credentials->getPassword());
}
public function wrapStorage(Storage $storage) {
return new SessionStorageWrapper(['storage' => $storage]);
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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;
/**
* User provided Global Username and Password
*/
class UserGlobalAuth extends AuthMechanism {
private const CREDENTIALS_IDENTIFIER = 'password::global';
/** @var ICredentialsManager */
protected $credentialsManager;
public function __construct(IL10N $l, ICredentialsManager $credentialsManager) {
$this->credentialsManager = $credentialsManager;
$this
->setIdentifier('password::global::user')
->setVisibility(BackendService::VISIBILITY_DEFAULT)
->setScheme(self::SCHEME_PASSWORD)
->setText($l->t('Global credentials, user 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;
}
// 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);
}
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']);
}
}
}
@@ -0,0 +1,88 @@
<?php
/**
* @copyright Copyright (c) 2015, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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/';
/** @var ICredentialsManager */
protected $credentialsManager;
public function __construct(IL10N $l, ICredentialsManager $credentialsManager) {
$this->credentialsManager = $credentialsManager;
$this
->setIdentifier('password::userprovided')
->setVisibility(BackendService::VISIBILITY_ADMIN)
->setScheme(self::SCHEME_PASSWORD)
->setText($l->t('User entered, store in database'))
->addParameters([
(new DefinitionParameter('user', $l->t('Username')))
->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) {
$this->credentialsManager->store($user->getUID(), $this->getCredentialsIdentifier($mountId), [
'user' => $options['user'], // explicitly copy the fields we want instead of just passing the entire $options array
'password' => $options['password'] // this way we prevent users from being able to modify any other field
]);
}
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
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']);
}
}
@@ -0,0 +1,89 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Auth\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 {
/** @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),
])
->addCustomJs('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'))) {
// 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);
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
/** @var IConfig */
private $config;
public function __construct(IL10N $l, IConfig $config) {
$this->config = $config;
$this
->setIdentifier('publickey::rsa_private')
->setScheme(self::SCHEME_PUBLICKEY)
->setText($l->t('RSA private key'))
->addParameters([
new DefinitionParameter('user', $l->t('Username')),
(new DefinitionParameter('password', $l->t('Password')))
->setFlag(DefinitionParameter::FLAG_OPTIONAL)
->setType(DefinitionParameter::VALUE_PASSWORD),
new DefinitionParameter('private_key', $l->t('Private 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'))) {
// 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);
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
/** @var IStore */
private $credentialsStore;
public function __construct(IL10N $l, 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);
$this->credentialsStore = $credentialsStore;
}
public function getCredentialsStore(): IStore {
return $this->credentialsStore;
}
}
@@ -0,0 +1,35 @@
<?php
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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'));
}
}
@@ -0,0 +1,65 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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),
])
->addAuthScheme(AccessKey::SCHEME_AMAZONS3_ACCESSKEY)
->addAuthScheme(AuthMechanism::SCHEME_NULL)
->setLegacyAuthMechanism($legacyAuth)
;
}
}
@@ -0,0 +1,160 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Backend;
use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\DependencyTrait;
use OCA\Files_External\Lib\FrontendDefinitionTrait;
use OCA\Files_External\Lib\IdentifierTrait;
use OCA\Files_External\Lib\IFrontendDefinition;
use OCA\Files_External\Lib\IIdentifier;
use OCA\Files_External\Lib\PriorityTrait;
use OCA\Files_External\Lib\StorageConfig;
use OCA\Files_External\Lib\StorageModifierTrait;
use OCA\Files_External\Lib\VisibilityTrait;
use OCP\Files\Storage\IStorage;
/**
* Storage backend
*
* A backend can have services injected during construction,
* such as \OCP\IDB for database operations. This allows a backend
* to perform advanced operations based on provided information.
*
* 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.
*
* This class uses the following traits:
* - VisibilityTrait
* Restrict usage to admin-only/none
* - FrontendDefinitionTrait
* Specify configuration parameters and other definitions
* - PriorityTrait
* Allow objects to prioritize over others with the same mountpoint
* - DependencyTrait
* The object requires certain dependencies to be met
* - StorageModifierTrait
* Object can affect storage mounting
*/
class Backend implements \JsonSerializable, IIdentifier, IFrontendDefinition {
use VisibilityTrait;
use FrontendDefinitionTrait;
use PriorityTrait;
use DependencyTrait;
use StorageModifierTrait;
use IdentifierTrait;
/** @var string storage class */
private $storageClass;
/** @var array 'scheme' => true, supported authentication schemes */
private $authSchemes = [];
/** @var AuthMechanism|callable authentication mechanism fallback */
private $legacyAuthMechanism;
/**
* @return class-string<IStorage>
*/
public function getStorageClass() {
return $this->storageClass;
}
/**
* @param string $class
* @return $this
*/
public function setStorageClass($class) {
$this->storageClass = $class;
return $this;
}
/**
* @return array
*/
public function getAuthSchemes() {
if (empty($this->authSchemes)) {
return [AuthMechanism::SCHEME_NULL => true];
}
return $this->authSchemes;
}
/**
* @param string $scheme
* @return self
*/
public function addAuthScheme($scheme) {
$this->authSchemes[$scheme] = true;
return $this;
}
/**
* @param array $parameters storage parameters, for dynamic mechanism selection
* @return AuthMechanism
*/
public function getLegacyAuthMechanism(array $parameters = []) {
if (is_callable($this->legacyAuthMechanism)) {
return call_user_func($this->legacyAuthMechanism, $parameters);
}
return $this->legacyAuthMechanism;
}
public function setLegacyAuthMechanism(AuthMechanism $authMechanism): self {
$this->legacyAuthMechanism = $authMechanism;
return $this;
}
/**
* @param callable $callback dynamic auth mechanism selection
*/
public function setLegacyAuthMechanismCallback(callable $callback): self {
$this->legacyAuthMechanism = $callback;
return $this;
}
/**
* Serialize into JSON for client-side JS
*/
public function jsonSerialize(): array {
$data = $this->jsonSerializeDefinition();
$data += $this->jsonSerializeIdentifier();
$data['backend'] = $data['name']; // legacy compat
$data['priority'] = $this->getPriority();
$data['authSchemes'] = $this->getAuthSchemes();
return $data;
}
/**
* Check if parameters are satisfied in a StorageConfig
*
* @param StorageConfig $storage
* @return bool
*/
public function validateStorage(StorageConfig $storage) {
return $this->validateStorageDefinition($storage);
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\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)
;
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\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('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)
;
}
}
@@ -0,0 +1,66 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud GmbH.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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 {
/** @var string Invalid backend id */
private $invalidId;
/**
* Constructs a new InvalidBackend with the id of the invalid backend
* for display purposes
*
* @param string $invalidId id of the backend that did not exist
*/
public function __construct($invalidId) {
$this->invalidId = $invalidId;
$this
->setIdentifier($invalidId)
->setStorageClass('\OC\Files\Storage\FailedStorage')
->setText('Unknown storage backend ' . $invalidId);
}
/**
* Returns the invalid backend id
*
* @return string invalid backend id
*/
public function getInvalidId() {
return $this->invalidId;
}
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
$storage->setBackendOption('exception', new \Exception('Unknown storage backend "' . $this->invalidId . '"', StorageNotAvailableException::STATUS_ERROR));
}
}
@@ -0,0 +1,103 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Backend;
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 OCA\Files_External\MountConfig::registerBackend()
*/
class LegacyBackend extends Backend {
use LegacyDependencyCheckPolyfill {
LegacyDependencyCheckPolyfill::checkDependencies as doCheckDependencies;
}
/** @var bool */
protected $hasDependencies = false;
/**
* @param string $class
* @param array $definition
* @param Builtin $authMechanism
*/
public function __construct($class, array $definition, Builtin $authMechanism) {
$this
->setIdentifier($class)
->setStorageClass($class)
->setText($definition['backend'])
->addAuthScheme(Builtin::SCHEME_BUILTIN)
->setLegacyAuthMechanism($authMechanism)
;
foreach ($definition['configuration'] as $name => $placeholder) {
$flags = DefinitionParameter::FLAG_NONE;
$type = DefinitionParameter::VALUE_TEXT;
if ($placeholder[0] === '&') {
$flags = DefinitionParameter::FLAG_OPTIONAL;
$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;
}
$this->addParameter((new DefinitionParameter($name, $placeholder))
->setType($type)
->setFlags($flags)
);
}
if (isset($definition['priority'])) {
$this->setPriority($definition['priority']);
}
if (isset($definition['custom'])) {
$this->addCustomJs($definition['custom']);
}
if (isset($definition['has_dependencies']) && $definition['has_dependencies']) {
$this->hasDependencies = true;
}
}
/**
* @return MissingDependency[]
*/
public function checkDependencies() {
if ($this->hasDependencies) {
return $this->doCheckDependencies();
}
return [];
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\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);
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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)
;
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\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('root', $l->t('Root')))
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
])
->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY)
->setLegacyAuthMechanism($legacyAuth)
;
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\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)
;
}
}
@@ -0,0 +1,141 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Valdnet <47037905+Valdnet@users.noreply.github.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Backend;
use Icewind\SMB\BasicAuth;
use Icewind\SMB\KerberosApacheAuth;
use Icewind\SMB\KerberosAuth;
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\LegacyDependencyCheckPolyfill;
use OCA\Files_External\Lib\StorageConfig;
use OCP\IL10N;
use OCP\IUser;
class SMB extends Backend {
use LegacyDependencyCheckPolyfill;
public function __construct(IL10N $l, Password $legacyAuth) {
$this
->setIdentifier('smb')
->addIdentifierAlias('\OC\Files\Storage\SMB')// legacy compat
->setStorageClass('\OCA\Files_External\Lib\Storage\SMB')
->setText($l->t('SMB/CIFS'))
->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 user has no read permissions, comes with a performance penalty")),
(new DefinitionParameter('timeout', $l->t('Timeout')))
->setType(DefinitionParameter::VALUE_HIDDEN)
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
])
->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
->addAuthScheme(AuthMechanism::SCHEME_SMB)
->setLegacyAuthMechanism($legacyAuth);
}
public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
$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 KerberosApacheAuth();
// check if a kerberos ticket is available, else fallback to session credentials
if ($kerbAuth->checkTicket()) {
$smbAuth = $kerbAuth;
} else {
try {
$credentials = $credentialsStore->getLoginCredentials();
$user = $credentials->getLoginName();
$pass = $credentials->getPassword();
preg_match('/(.*)@(.*)/', $user, $matches);
$realm = $storage->getBackendOption('default_realm');
if (empty($realm)) {
$realm = 'WORKGROUP';
}
if (count($matches) === 0) {
$username = $user;
$workgroup = $realm;
} else {
$username = $matches[1];
$workgroup = $matches[2];
}
$smbAuth = new BasicAuth(
$username,
$workgroup,
$pass
);
} catch (\Exception $e) {
throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved');
}
}
break;
default:
throw new \InvalidArgumentException('unknown authentication backend');
}
}
$storage->setBackendOption('auth', $smbAuth);
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Valdnet <47037905+Valdnet@users.noreply.github.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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('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);
}
}
}
@@ -0,0 +1,62 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julien Lutran <julien.lutran@corp.ovh.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
})
;
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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();
}
@@ -0,0 +1,38 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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();
}
@@ -0,0 +1,241 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
public const VALUE_HIDDEN = 3;
/** Flag constants */
public const FLAG_NONE = 0;
public const FLAG_OPTIONAL = 1;
public const FLAG_USER_PROVIDED = 2;
/** @var string name of parameter */
private string $name;
/** @var string human-readable parameter text */
private string $text;
/** @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;
/** @var mixed */
private $defaultValue;
/**
* @param string $name parameter name
* @param string $text parameter description
* @param mixed $defaultValue default value
*/
public function __construct(string $name, string $text, $defaultValue = null) {
$this->name = $name;
$this->text = $text;
$this->defaultValue = $defaultValue;
}
/**
* @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;
}
}
@@ -0,0 +1,37 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib;
/**
* 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
}
}
@@ -0,0 +1,123 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
}
}
@@ -0,0 +1,56 @@
<?php
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @license AGPL-3.0-or-later
*
* 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;
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;
}
@@ -0,0 +1,27 @@
<?php
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @license AGPL-3.0-or-later
*
* 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;
interface IIdentifier {
public function getIdentifier(): string;
public function setIdentifier(string $identifier): self;
}
@@ -0,0 +1,78 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
}
}
@@ -0,0 +1,44 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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);
}
}
@@ -0,0 +1,68 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib;
use OCP\Files\Storage\IStorage;
/**
* Polyfill for checking dependencies using legacy Storage::checkDependencies()
*/
trait LegacyDependencyCheckPolyfill {
/**
* @return class-string<IStorage>
*/
abstract public function getStorageClass();
/**
* Check if object is valid for use
*
* @return MissingDependency[] Unsatisfied dependencies
*/
public function checkDependencies() {
$ret = [];
$result = call_user_func([$this->getStorageClass(), 'checkDependencies']);
if ($result !== true) {
if (!is_array($result)) {
$result = [$result];
}
foreach ($result as $key => $value) {
if (!($value instanceof MissingDependency)) {
$module = null;
$message = null;
if (is_numeric($key)) {
$module = $value;
} else {
$module = $key;
$message = $value;
}
$value = new MissingDependency($module);
$value->setMessage($message);
}
$ret[] = $value;
}
}
return $ret;
}
}
@@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib;
/**
* 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;
}
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;
}
}
@@ -0,0 +1,151 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 \Icewind\SMB\INotifyHandler
*/
private $shareNotifyHandler;
/**
* @var string
*/
private $root;
private $oldRenamePath = null;
/**
* SMBNotifyHandler constructor.
*
* @param \Icewind\SMB\INotifyHandler $shareNotifyHandler
* @param string $root
*/
public function __construct(\Icewind\SMB\INotifyHandler $shareNotifyHandler, $root) {
$this->shareNotifyHandler = $shareNotifyHandler;
$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;
}
}
}
@@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Julius Härtl <jus@bitgrid.net>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
/**
* Person mount points can be moved by the user
*/
class PersonalMount extends ExternalMountPoint implements MoveableMount {
/** @var UserStoragesService */
protected $storagesService;
/** @var int id of the external storage (mount) (not the numeric id of the resulting storage!) */
protected $numericExternalStorageId;
/**
* @param UserStoragesService $storagesService
* @param int $storageId
* @param IStorage $storage
* @param string $mountpoint
* @param array $arguments (optional) configuration for the storage backend
* @param \OCP\Files\Storage\IStorageFactory $loader
* @param array $mountOptions mount specific options
*/
public function __construct(
UserStoragesService $storagesService,
StorageConfig $storageConfig,
$externalStorageId,
$storage,
$mountpoint,
$arguments = null,
$loader = null,
$mountOptions = null,
$mountId = null
) {
parent::__construct($storageConfig, $storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId);
$this->storagesService = $storagesService;
$this->numericExternalStorageId = $externalStorageId;
}
/**
* 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;
}
}
@@ -0,0 +1,50 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
}
}
@@ -0,0 +1,41 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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 $arguments ['storage' => $storage]
*/
public function __construct($arguments) {
// disable sharing permission
$arguments['mask'] = Constants::PERMISSION_ALL & ~Constants::PERMISSION_SHARE;
parent::__construct($arguments);
}
}
@@ -0,0 +1,790 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author André Gaul <gaul@web-yard.de>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christian Berendt <berendt@b1-systems.de>
* @author Christopher T. Johnson <ctjctj@gmail.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author enoch <lanxenet@hotmail.com>
* @author Johan Björk <johanimon@gmail.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Julius Härtl <jus@bitgrid.net>
* @author Martin Mattel <martin.mattel@diemattels.at>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Philipp Kapfer <philipp.kapfer@gmx.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Storage;
use Aws\S3\Exception\S3Exception;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Files\Cache\CacheEntry;
use OC\Files\ObjectStore\S3ConnectionTrait;
use OC\Files\ObjectStore\S3ObjectTrait;
use OCP\Cache\CappedMemoryCache;
use OCP\Constants;
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeDetector;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\Server;
use Psr\Log\LoggerInterface;
class AmazonS3 extends \OC\Files\Storage\Common {
use S3ConnectionTrait;
use S3ObjectTrait;
private LoggerInterface $logger;
public function needsPartFile() {
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($parameters) {
parent::__construct($parameters);
$this->parseParams($parameters);
$this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']);
$this->objectCache = new CappedMemoryCache();
$this->directoryCache = new CappedMemoryCache();
$this->filesCache = new CappedMemoryCache();
$this->mimeDetector = Server::get(IMimeTypeDetector::class);
/** @var ICacheFactory $cacheFactory */
$cacheFactory = Server::get(ICacheFactory::class);
$this->memCache = $cacheFactory->createLocal('s3-external');
$this->logger = Server::get(LoggerInterface::class);
}
/**
* @param string $path
* @return string correctly encoded path
*/
private function normalizePath($path) {
$path = trim($path, '/');
if (!$path) {
$path = '.';
}
return $path;
}
private function isRoot($path) {
return $path === '.';
}
private function cleanKey($path) {
if ($this->isRoot($path)) {
return '/';
}
return $path;
}
private function clearCache() {
$this->objectCache = new CappedMemoryCache();
$this->directoryCache = new CappedMemoryCache();
$this->filesCache = new CappedMemoryCache();
}
private function invalidateCache($key) {
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]);
}
/**
* @return array|false
*/
private function headObject(string $key) {
if (!isset($this->objectCache[$key])) {
try {
$this->objectCache[$key] = $this->getConnection()->headObject([
'Bucket' => $this->bucket,
'Key' => $key
])->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
*
* @param $path
* @return bool
* @throws \Exception
*/
private function doesDirectoryExist($path) {
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;
}
/**
* 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);
} elseif ($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([
'Bucket' => $this->bucket,
'Key' => $path . '/',
'Body' => '',
'ContentType' => FileInfo::MIMETYPE_FOLDER
]);
$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($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;
}
$this->invalidateCache($path);
return $this->batchDelete($path);
}
protected function clearBucket() {
$this->clearCache();
return $this->batchDelete();
}
private function batchDelete($path = null) {
// 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($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($path) {
$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.
*
* @param $path
* @return int|mixed
*/
private function getContentLength($path) {
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.
*
* @param $path
* @return mixed|string
*/
private function getLastModified($path) {
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($path) {
$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($path) {
$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($path) {
$type = $this->filetype($path);
if (!$type) {
return 0;
}
return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
}
public function unlink($path) {
$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($path, $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 = \OC::$server->getTempManager()->getTemporaryFile();
$handle = fopen($tmpFile, 'w');
return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
$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 = \OC::$server->getTempManager()->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) {
$this->writeBack($tmpFile, $path);
});
}
return false;
}
public function touch($path, $mtime = null) {
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->testTimeout();
} catch (S3Exception $e) {
$this->logger->error($e->getMessage(), [
'app' => 'files_external',
'exception' => $e,
]);
return false;
}
$this->invalidateCache($path);
return true;
}
public function copy($source, $target, $isFile = null) {
$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($source, $target) {
$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() {
$this->getConnection()->headBucket([
'Bucket' => $this->bucket
]);
return true;
}
public function getId() {
return $this->id;
}
public function writeBack($tmpFile, $path) {
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() {
return true;
}
public function getDirectoryContent($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($path, $time) {
// 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;
}
}
}
@@ -0,0 +1,380 @@
<?php
/**
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\StorageNotAvailableException;
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($params) {
if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
$this->host = $params['host'];
$this->username = $params['user'];
$this->password = $params['password'];
if (isset($params['secure'])) {
if (is_string($params['secure'])) {
$this->secure = ($params['secure'] === 'true');
} else {
$this->secure = (bool)$params['secure'];
}
} else {
$this->secure = false;
}
$this->root = isset($params['root']) ? '/' . ltrim($params['root']) : '/';
$this->port = $params['port'] ?? 21;
$this->utf8Mode = isset($params['utf8']) && $params['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() {
return 'ftp::' . $this->username . '@' . $this->host . '/' . $this->root;
}
protected function buildPath($path) {
return rtrim($this->root . '/' . $path, '/');
}
public static function checkDependencies() {
if (function_exists('ftp_login')) {
return true;
} else {
return ['ftp'];
}
}
public function filemtime($path) {
$result = $this->getConnection()->mdtm($this->buildPath($path));
if ($result === -1) {
if ($this->is_dir($path)) {
$list = $this->getConnection()->mlsd($this->buildPath($path));
if (!$list) {
\OC::$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 {
\OC::$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($path): false|int|float {
$result = $this->getConnection()->size($this->buildPath($path));
if ($result === -1) {
return false;
} else {
return $result;
}
}
public function rmdir($path) {
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;
}
}
/**
* @param string $path
* @return bool
*/
private function recursiveRmDir($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() {
try {
return $this->getConnection()->systype() !== false;
} catch (\Exception $e) {
return false;
}
}
public function stat($path) {
if (!$this->file_exists($path)) {
return false;
}
return [
'mtime' => $this->filemtime($path),
'size' => $this->filesize($path),
];
}
public function file_exists($path) {
if ($path === '' || $path === '.' || $path === '/') {
return true;
}
return $this->filetype($path) !== false;
}
public function unlink($path) {
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($path) {
$files = $this->getConnection()->nlist($this->buildPath($path));
return IteratorDirectory::wrap($files);
}
public function mkdir($path) {
if ($this->is_dir($path)) {
return false;
}
return $this->getConnection()->mkdir($this->buildPath($path)) !== false;
}
public function is_dir($path) {
if ($path === "") {
return true;
}
if ($this->getConnection()->chdir($this->buildPath($path)) === true) {
$this->getConnection()->chdir('/');
return true;
} else {
return false;
}
}
public function is_file($path) {
return $this->filesize($path) !== false;
}
public function filetype($path) {
if ($this->is_dir($path)) {
return 'dir';
} elseif ($this->is_file($path)) {
return 'file';
} else {
return false;
}
}
public function fopen($path, $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 = \OC::$server->getTempManager()->getTemporaryFile();
}
$source = fopen($tmpFile, $mode);
return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $path) {
$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) {
$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($path, $mtime = null) {
if ($this->file_exists($path)) {
return false;
} else {
$this->file_put_contents($path, '');
return true;
}
}
public function rename($source, $target) {
$this->unlink($target);
return $this->getConnection()->rename($this->buildPath($source), $this->buildPath($target));
}
public function getDirectoryContent($directory): \Traversable {
$files = $this->getConnection()->mlsd($this->buildPath($directory));
$mimeTypeDetector = \OC::$server->getMimeTypeDetector();
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;
}
}
}
@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Lib\Storage;
/**
* Low level wrapper around the ftp functions that smooths over some difference between servers
*/
class FtpConnection {
/** @var resource|\FTP\Connection */
private $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() {
if ($this->connection) {
ftp_close($this->connection);
}
$this->connection = null;
}
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,
];
}
}
@@ -0,0 +1,83 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Storage;
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 \OC\Files\Storage\DAV implements IDisableEncryptionStorage {
public 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;
} elseif (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 (!str_ends_with($contextPath, '/')) {
$contextPath .= '/';
}
if (isset($params['root'])) {
$root = '/' . ltrim($params['root'], '/');
} else {
$root = '/';
}
$params['host'] = $host;
$params['root'] = $contextPath . self::OC_URL_SUFFIX . $root;
$params['authType'] = Client::AUTH_BASIC;
parent::__construct($params);
}
public function needsPartFile() {
return false;
}
}
@@ -0,0 +1,587 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Andreas Fischer <bantu@owncloud.com>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author hkjolhede <hkjolhede@gmail.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lennart Rosam <lennart.rosam@medien-systempartner.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Ross Nicoll <jrn@jrn.me.uk>
* @author SA <stephen@mthosting.net>
* @author Senorsen <senorsen.zhang@gmail.com>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Storage;
use Icewind\Streams\CountWrapper;
use Icewind\Streams\IteratorDirectory;
use Icewind\Streams\RetryWrapper;
use OC\Files\Storage\Common;
use OCP\Constants;
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeDetector;
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 IMimeTypeDetector $mimeTypeDetector;
public const COPY_CHUNK_SIZE = 8 * 1024 * 1024;
/**
* @param string $host protocol://server:port
* @return array [$server, $port]
*/
private function splitHost($host) {
$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];
}
}
/**
* {@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'];
}
if (isset($params['password']) && $params['password'] !== '') {
$this->auth[] = $params['password'];
}
if ($this->auth === []) {
throw new \UnexpectedValueException('no authentication parameters specified');
}
$this->root
= isset($params['root']) ? $this->cleanPath($params['root']) : '/';
$this->root = '/' . ltrim($this->root, '/');
$this->root = rtrim($this->root, '/') . '/';
$this->mimeTypeDetector = \OC::$server->get(IMimeTypeDetector::class);
}
/**
* 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);
}
$login = false;
foreach ($this->auth as $auth) {
/** @psalm-suppress TooManyArguments */
$login = $this->client->login($this->user, $auth);
if ($login === true) {
break;
}
}
if ($login === false) {
throw new \Exception('Login failed');
}
return $this->client;
}
/**
* {@inheritdoc}
*/
public function test() {
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', \OC::$SERVERROOT . '/data') .
$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 = [];
$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 [];
}
/**
* {@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 = [];
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 (!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;
}
/**
* {@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);
$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]]);
return fopen('sftpwrite://' . trim($absPath, '/'), 'w', false, $context);
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;
}
/**
* {@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);
}
/**
* {@inheritdoc}
*/
public function rename($source, $target) {
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($path) {
try {
$stat = $this->getConnection()->stat($this->absPath($path));
$mtime = $stat ? (int)$stat['mtime'] : -1;
$size = $stat ? (int)$stat['size'] : 0;
return ['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;
}
public function file_put_contents($path, $data) {
/** @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) {
$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) {
return $size;
} else {
throw new \Exception("Failed to write steam to sftp storage");
}
}
public function copy($source, $target) {
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($path) {
$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($path) {
$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));
}
}
@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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
*
* @param string $name
* @throws \BadMethodCallException
*/
protected function loadContext($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($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;
}
}
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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
*
* @param string $name
* @throws \BadMethodCallException
*/
protected function loadContext($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;
}
}
@@ -0,0 +1,786 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jesús Macias <jmacias@solidgear.es>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Juan Pablo Villafañez <jvillafanez@solidgear.es>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Julius Härtl <jus@bitgrid.net>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Philipp Kapfer <philipp.kapfer@gmx.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roland Tapken <roland@bitarbeiter.net>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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\System;
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\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 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($params) {
if (!isset($params['host'])) {
throw new \Exception('Invalid configuration, no host provided');
}
if (isset($params['auth'])) {
$auth = $params['auth'];
} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
[$workgroup, $user] = $this->splitUser($params['user']);
$auth = new BasicAuth($user, $workgroup, $params['password']);
} else {
throw new \Exception('Invalid configuration, no credentials provided');
}
if (isset($params['logger'])) {
if (!$params['logger'] instanceof LoggerInterface) {
throw new \Exception(
'Invalid logger. Got '
. get_class($params['logger'])
. ' Expected ' . LoggerInterface::class
);
}
$this->logger = $params['logger'];
} else {
$this->logger = \OC::$server->get(LoggerInterface::class);
}
$options = new Options();
if (isset($params['timeout'])) {
$timeout = (int)$params['timeout'];
if ($timeout > 0) {
$options->setTimeout($timeout);
}
}
$serverFactory = new ServerFactory($options);
$this->server = $serverFactory->createServer($params['host'], $auth);
$this->share = $this->server->getShare(trim($params['share'], '/'));
$this->root = $params['root'] ?? '/';
$this->root = '/' . ltrim($this->root, '/');
$this->root = rtrim($this->root, '/') . '/';
$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
$this->caseSensitive = (bool) ($params['case_sensitive'] ?? true);
$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
$this->statCache = new CappedMemoryCache();
parent::__construct($params);
}
private function splitUser($user) {
if (str_contains($user, '/')) {
return explode('/', $user, 2);
} elseif (str_contains($user, '\\')) {
return explode('\\', $user);
}
return [null, $user];
}
/**
* @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->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
}
/**
* @param string $path
* @return string
*/
protected function buildPath($path) {
return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
}
protected 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;
}
}
/**
* @param string $path
* @return IFileInfo
* @throws StorageAuthException
*/
protected function getFileInfo($path) {
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);
}
}
/**
* @param \Exception $e
* @return never
* @throws StorageAuthException
*/
protected function throwUnavailable(\Exception $e) {
$this->logger->error('Error while getting file info', ['exception' => $e]);
throw new StorageAuthException($e->getMessage(), $e);
}
/**
* get the acl from fileinfo that is relevant for the configured user
*
* @param IFileInfo $file
* @return ACL|null
*/
private function getACL(IFileInfo $file): ?ACL {
$acls = $file->getAcls();
foreach ($acls as $user => $acl) {
[, $user] = $this->splitUser($user); // strip domain
if ($user === $this->server->getAuth()->getUsername()) {
return $acl;
}
}
return null;
}
/**
* @param string $path
* @return \Generator<IFileInfo>
* @throws StorageNotAvailableException
*/
protected function getFolderContents($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);
}
}
/**
* @param IFileInfo $info
* @return array
*/
protected function formatInfo($info) {
$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
* @return bool true if the rename is successful, false otherwise
*/
public function rename($source, $target, $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($path, $retry = true) {
try {
$result = $this->formatInfo($this->getFileInfo($path));
} catch (ForbiddenException $e) {
return false;
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (TimedOutException $e) {
if ($retry) {
return $this->stat($path, false);
} else {
throw $e;
}
}
if ($this->remoteIsShare() && $this->isRootDir($path)) {
$result['mtime'] = $this->shareMTime();
}
return $result;
}
/**
* get the best guess for the modification time of the share
*
* @return int
*/
private function shareMTime() {
$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)
*
* @param string $path the path
* @return bool
*/
private function isRootDir($path) {
return $path === '' || $path === '/' || $path === '.';
}
/**
* Check if our root points to a smb share
*
* @return bool true if our root points to a share false otherwise
*/
private function remoteIsShare() {
return $this->share->getName() && (!$this->root || $this->root === '/');
}
/**
* @param string $path
* @return bool
*/
public function unlink($path) {
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
*
* @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|bool
*/
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 = \OC::$server->getTempManager()->getTemporaryFile($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 (OutOfSpaceException $e) {
throw new EntityTooLargeException("not enough available space to create file", 0, $e);
} catch (ConnectException $e) {
$this->logger->error('Error while opening file', ['exception' => $e]);
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
}
}
public function rmdir($path) {
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($path, $mtime = null) {
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($path) {
try {
$fileInfo = $this->getFileInfo($path);
} catch (\OCP\Files\NotFoundException $e) {
return null;
} catch (ForbiddenException $e) {
return null;
}
if (!$fileInfo) {
return null;
}
return $this->getMetaDataFromFileInfo($fileInfo);
}
private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
$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'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
}
$data['mtime'] = $fileInfo->getMTime();
if ($fileInfo->isDirectory()) {
$data['size'] = -1; //unknown
} else {
$data['size'] = $fileInfo->getSize();
}
$data['etag'] = $this->getETag($fileInfo->getPath());
$data['storage_mtime'] = $data['mtime'];
$data['permissions'] = $permissions;
$data['name'] = $fileInfo->getName();
return $data;
}
public function opendir($path) {
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($directory): \Traversable {
try {
$files = $this->getFolderContents($directory);
foreach ($files as $file) {
yield $this->getMetaDataFromFileInfo($file);
}
} catch (NotFoundException $e) {
return;
} catch (NotPermittedException $e) {
return;
}
}
public function filetype($path) {
try {
return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
} catch (\OCP\Files\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) {
$this->logger->error('Error while creating folder', ['exception' => $e]);
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
} catch (Exception $e) {
return false;
}
}
public function file_exists($path) {
try {
// Case sensitive filesystem doesn't matter for root directory
if ($this->caseSensitive === false && $path !== '') {
$filename = basename($path);
$siblings = $this->getDirectoryContent(dirname($this->buildPath($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 (ForbiddenException $e) {
return false;
} catch (ConnectException $e) {
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
}
}
public function isReadable($path) {
try {
$info = $this->getFileInfo($path);
return $this->showHidden || !$info->isHidden();
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
}
}
public function isUpdatable($path) {
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 (ForbiddenException $e) {
return false;
}
}
public function isDeletable($path) {
try {
$info = $this->getFileInfo($path);
return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
} catch (\OCP\Files\NotFoundException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
}
}
/**
* check if smbclient is installed
*/
public static function checkDependencies() {
return (
(bool)\OC_Helper::findBinaryPath('smbclient')
|| NativeServer::available(new System())
) ? true : ['smbclient'];
}
/**
* Test a storage for availability
*
* @return bool
*/
public function test() {
try {
return parent::test();
} catch (StorageAuthException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
} catch (Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return false;
}
}
public function listen($path, callable $callback) {
$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($path) {
$path = '/' . ltrim($path, '/');
$shareNotifyHandler = $this->share->notify($this->buildPath($path));
return new SMBNotifyHandler($shareNotifyHandler, $this->root);
}
}
@@ -0,0 +1,127 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Storage;
abstract class StreamWrapper extends \OC\Files\Storage\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($source, $target) {
return rename($this->constructUrl($source), $this->constructUrl($target));
}
public function stat($path) {
return stat($this->constructUrl($path));
}
}
@@ -0,0 +1,630 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Benjamin Liles <benliles@arch.tamu.edu>
* @author Christian Berendt <berendt@b1-systems.de>
* @author Christopher Bartz <bartz@dkrz.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Felix Moeller <mail@felixmoeller.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Martin Mattel <martin.mattel@diemattels.at>
* @author Michael Zamot <michael@zamot.io>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Philipp Kapfer <philipp.kapfer@gmx.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tim Dettrick <t.dettrick@uq.edu.au>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib\Storage;
use GuzzleHttp\Psr7\Uri;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Files\ObjectStore\SwiftFactory;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\StorageBadConfigException;
use OpenStack\Common\Error\BadResponseError;
use OpenStack\ObjectStore\v1\Models\StorageObject;
use Psr\Log\LoggerInterface;
class Swift extends \OC\Files\Storage\Common {
/** @var SwiftFactory */
private $connectionFactory;
/**
* @var \OpenStack\ObjectStore\v1\Models\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 \OCP\ICache
*/
private $objectCache;
/**
* @param string $path
* @return mixed|string
*/
private function normalizePath(string $path) {
$path = trim($path, '/');
if (!$path) {
$path = '.';
}
$path = str_replace('#', '%23', $path);
return $path;
}
public const SUBCONTAINER_FILE = '.subcontainers';
/**
* translate directory path to container name
*
* @param string $path
* @return string
*/
/**
* Fetches an object from the API.
* If the object is cached already or a
* failed "doesn't exist" response was cached,
* that one will be returned.
*
* @param string $path
* @return StorageObject|bool object
* or false if the object did not exist
* @throws \OCP\Files\StorageAuthException
* @throws \OCP\Files\StorageNotAvailableException
*/
private function fetchObject(string $path) {
$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) {
\OC::$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.
*
* @param string $path
*
* @return bool true if the object exist, false otherwise
* @throws \OCP\Files\StorageAuthException
* @throws \OCP\Files\StorageNotAvailableException
*/
private function doesObjectExist($path) {
return $this->fetchObject($path) !== false;
}
public function __construct($params) {
if ((empty($params['key']) and empty($params['password']))
or (empty($params['user']) && empty($params['userid'])) or empty($params['bucket'])
or empty($params['region'])
) {
throw new StorageBadConfigException("API Key or password, Username, Bucket and Region have to be configured.");
}
$user = $params['user'];
$this->id = 'swift::' . $user . md5($params['bucket']);
$bucketUrl = new Uri($params['bucket']);
if ($bucketUrl->getHost()) {
$params['bucket'] = basename($bucketUrl->getPath());
$params['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath()));
}
if (empty($params['url'])) {
$params['url'] = 'https://identity.api.rackspacecloud.com/v2.0/';
}
if (empty($params['service_name'])) {
$params['service_name'] = 'cloudFiles';
}
$params['autocreate'] = true;
if (isset($params['domain'])) {
$params['user'] = [
'name' => $params['user'],
'password' => $params['password'],
'domain' => [
'name' => $params['domain'],
]
];
}
$this->params = $params;
// FIXME: private class...
$this->objectCache = new \OCP\Cache\CappedMemoryCache();
$this->connectionFactory = new SwiftFactory(
\OC::$server->getMemCacheFactory()->createDistributed('swift/'),
$this->params,
\OC::$server->get(LoggerInterface::class)
);
$this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory);
$this->bucket = $params['bucket'];
$this->mimeDetector = \OC::$server->get(IMimeTypeDetector::class);
}
public function mkdir($path) {
$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) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
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->objectStore->deleteObject($path . '/');
$this->objectCache->remove($path . '/');
} catch (BadResponseError $e) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
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 = [];
$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) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
}
public function stat($path) {
$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) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
$dateTime = $object->lastModified ? \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified) : false;
$mtime = $dateTime ? $dateTime->getTimestamp() : null;
$objectMetadata = $object->getMetadata();
if (isset($objectMetadata['timestamp'])) {
$mtime = $objectMetadata['timestamp'];
}
if (!empty($mtime)) {
$mtime = floor($mtime);
}
$stat = [];
$stat['size'] = (int)$object->contentLength;
$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->objectStore->deleteObject($path);
$this->objectCache->remove($path);
$this->objectCache->remove($path . '/');
} catch (BadResponseError $e) {
if ($e->getResponse()->getStatusCode() !== 404) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
throw $e;
}
}
return true;
}
public function fopen($path, $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) {
\OC::$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 = \OC::$server->getTempManager()->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) {
$this->writeBack($tmpFile, $path);
});
}
}
public function touch($path, $mtime = null) {
$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($source, $target) {
$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) {
\OC::$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) {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
'exception' => $e,
'app' => 'files_external',
]);
return false;
}
$dh = $this->opendir($source);
while ($file = readdir($dh)) {
if (\OC\Files\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($source, $target) {
$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() {
return $this->id;
}
/**
* Returns the initialized object store container.
*
* @return \OpenStack\ObjectStore\v1\Models\Container
* @throws \OCP\Files\StorageAuthException
* @throws \OCP\Files\StorageNotAvailableException
*/
public function getContainer() {
if (is_null($this->container)) {
$this->container = $this->connectionFactory->getContainer();
if (!$this->file_exists('.')) {
$this->mkdir('.');
}
}
return $this->container;
}
public function writeBack($tmpFile, $path) {
$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($path, $time) {
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() {
return true;
}
}
@@ -0,0 +1,456 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jesús Macias <jmacias@solidgear.es>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib;
use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\Auth\IUserProvided;
use OCA\Files_External\Lib\Backend\Backend;
use OCA\Files_External\ResponseDefinitions;
/**
* External storage configuration
*
* @psalm-import-type Files_ExternalStorageConfig from ResponseDefinitions
*/
class StorageConfig implements \JsonSerializable {
public const MOUNT_TYPE_ADMIN = 1;
public const MOUNT_TYPE_PERSONAl = 2;
/**
* Storage config id
*
* @var int
*/
private $id;
/**
* Backend
*
* @var Backend
*/
private $backend;
/**
* Authentication mechanism
*
* @var AuthMechanism
*/
private $authMechanism;
/**
* Backend options
*
* @var array<string, mixed>
*/
private $backendOptions = [];
/**
* Mount point path, relative to the user's "files" folder
*
* @var string
*/
private $mountPoint;
/**
* Storage status
*
* @var int
*/
private $status;
/**
* Status message
*
* @var string
*/
private $statusMessage;
/**
* Priority
*
* @var int
*/
private $priority;
/**
* List of users who have access to this storage
*
* @var string[]
*/
private $applicableUsers = [];
/**
* List of groups that have access to this storage
*
* @var string[]
*/
private $applicableGroups = [];
/**
* Mount-specific options
*
* @var array<string, mixed>
*/
private $mountOptions = [];
/**
* Whether it's a personal or admin mount
*
* @var int
*/
private $type;
/**
* Creates a storage config
*
* @param int|string $id config id or null for a new config
*/
public function __construct($id = null) {
$this->id = $id ?? -1;
$this->mountOptions['enable_sharing'] = false;
}
/**
* Returns the configuration id
*
* @return int
*/
public function getId() {
return $this->id;
}
/**
* Sets the configuration id
*
* @param int $id configuration id
*/
public function setId(int $id): void {
$this->id = $id;
}
/**
* Returns mount point path relative to the user's
* "files" folder.
*
* @return string path
*/
public function getMountPoint() {
return $this->mountPoint;
}
/**
* Sets mount point path relative to the user's
* "files" folder.
* The path will be normalized.
*
* @param string $mountPoint path
*/
public function setMountPoint($mountPoint) {
$this->mountPoint = \OC\Files\Filesystem::normalizePath($mountPoint);
}
/**
* @return Backend
*/
public function getBackend() {
return $this->backend;
}
/**
* @param Backend $backend
*/
public function setBackend(Backend $backend) {
$this->backend = $backend;
}
/**
* @return AuthMechanism
*/
public function getAuthMechanism() {
return $this->authMechanism;
}
/**
* @param AuthMechanism $authMechanism
*/
public function setAuthMechanism(AuthMechanism $authMechanism) {
$this->authMechanism = $authMechanism;
}
/**
* Returns the external storage backend-specific options
*
* @return array backend options
*/
public function getBackendOptions() {
return $this->backendOptions;
}
/**
* Sets the external storage backend-specific options
*
* @param array $backendOptions backend options
*/
public function setBackendOptions($backendOptions) {
if ($this->getBackend() instanceof Backend) {
$parameters = $this->getBackend()->getParameters();
foreach ($backendOptions as $key => $value) {
if (isset($parameters[$key])) {
switch ($parameters[$key]->getType()) {
case \OCA\Files_External\Lib\DefinitionParameter::VALUE_BOOLEAN:
$value = (bool)$value;
break;
}
$backendOptions[$key] = $value;
}
}
}
$this->backendOptions = $backendOptions;
}
/**
* @param string $key
* @return mixed
*/
public function getBackendOption($key) {
if (isset($this->backendOptions[$key])) {
return $this->backendOptions[$key];
}
return null;
}
/**
* @param string $key
* @param mixed $value
*/
public function setBackendOption($key, $value) {
$this->backendOptions[$key] = $value;
}
/**
* Returns the mount priority
*
* @return int priority
*/
public function getPriority() {
return $this->priority;
}
/**
* Sets the mount priority
*
* @param int $priority priority
*/
public function setPriority($priority) {
$this->priority = $priority;
}
/**
* Returns the users for which to mount this storage
*
* @return string[] applicable users
*/
public function getApplicableUsers() {
return $this->applicableUsers;
}
/**
* Sets the users for which to mount this storage
*
* @param string[]|null $applicableUsers applicable users
*/
public function setApplicableUsers($applicableUsers) {
if (is_null($applicableUsers)) {
$applicableUsers = [];
}
$this->applicableUsers = $applicableUsers;
}
/**
* Returns the groups for which to mount this storage
*
* @return string[] applicable groups
*/
public function getApplicableGroups() {
return $this->applicableGroups;
}
/**
* Sets the groups for which to mount this storage
*
* @param string[]|null $applicableGroups applicable groups
*/
public function setApplicableGroups($applicableGroups) {
if (is_null($applicableGroups)) {
$applicableGroups = [];
}
$this->applicableGroups = $applicableGroups;
}
/**
* Returns the mount-specific options
*
* @return array mount specific options
*/
public function getMountOptions() {
return $this->mountOptions;
}
/**
* Sets the mount-specific options
*
* @param array $mountOptions applicable groups
*/
public function setMountOptions($mountOptions) {
if (is_null($mountOptions)) {
$mountOptions = [];
}
$this->mountOptions = $mountOptions;
}
/**
* @param string $key
* @return mixed
*/
public function getMountOption($key) {
if (isset($this->mountOptions[$key])) {
return $this->mountOptions[$key];
}
return null;
}
/**
* @param string $key
* @param mixed $value
*/
public function setMountOption($key, $value) {
$this->mountOptions[$key] = $value;
}
/**
* Gets the storage status, whether the config worked last time
*
* @return int $status status
*/
public function getStatus() {
return $this->status;
}
/**
* Gets the message describing the storage status
*
* @return string|null
*/
public function getStatusMessage() {
return $this->statusMessage;
}
/**
* Sets the storage status, whether the config worked last time
*
* @param int $status status
* @param string|null $message optional message
*/
public function setStatus($status, $message = null) {
$this->status = $status;
$this->statusMessage = $message;
}
/**
* @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
*/
public function setType($type) {
$this->type = $type;
}
/**
* Serialize config to JSON
* @return Files_ExternalStorageConfig
*/
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();
$result['backendOptions'] = $this->backendOptions;
if (!is_null($this->priority)) {
$result['priority'] = $this->priority;
}
if (!empty($this->applicableUsers)) {
$result['applicableUsers'] = $this->applicableUsers;
}
if (!empty($this->applicableGroups)) {
$result['applicableGroups'] = $this->applicableGroups;
}
if (!empty($this->mountOptions)) {
$result['mountOptions'] = $this->mountOptions;
}
if (!is_null($this->status)) {
$result['status'] = $this->status;
}
if (!is_null($this->statusMessage)) {
$result['statusMessage'] = $this->statusMessage;
}
$result['userProvided'] = $this->authMechanism instanceof IUserProvided;
$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;
}
}
}
}
}
@@ -0,0 +1,66 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib;
use OCP\Files\Storage;
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
* @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;
}
}
@@ -0,0 +1,136 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Lib;
use OCA\Files_External\Service\BackendService;
/**
* Trait to implement visibility mechanics for a configuration class
*
* The standard visibility defines which users/groups can use or see the
* object. The allowed visibility defines the maximum visibility allowed to be
* set on the object. The standard visibility is often set dynamically by
* stored configuration parameters that can be modified by the administrator,
* while the allowed visibility is set directly by the object and cannot be
* modified by the administrator.
*/
trait VisibilityTrait {
/** @var int visibility */
protected $visibility = BackendService::VISIBILITY_DEFAULT;
/** @var int allowed visibilities */
protected $allowedVisibility = BackendService::VISIBILITY_DEFAULT;
/**
* @return int
*/
public function getVisibility() {
return $this->visibility;
}
/**
* Check if the backend is visible for a user type
*
* @param int $visibility
* @return bool
*/
public function isVisibleFor($visibility) {
if ($this->visibility & $visibility) {
return true;
}
return false;
}
/**
* @param int $visibility
* @return self
*/
public function setVisibility($visibility) {
$this->visibility = $visibility;
$this->allowedVisibility |= $visibility;
return $this;
}
/**
* @param int $visibility
* @return self
*/
public function addVisibility($visibility) {
return $this->setVisibility($this->visibility | $visibility);
}
/**
* @param int $visibility
* @return self
*/
public function removeVisibility($visibility) {
return $this->setVisibility($this->visibility & ~$visibility);
}
/**
* @return int
*/
public function getAllowedVisibility() {
return $this->allowedVisibility;
}
/**
* Check if the backend is allowed to be visible for a user type
*
* @param int $allowedVisibility
* @return bool
*/
public function isAllowedVisibleFor($allowedVisibility) {
if ($this->allowedVisibility & $allowedVisibility) {
return true;
}
return false;
}
/**
* @param int $allowedVisibility
* @return self
*/
public function setAllowedVisibility($allowedVisibility) {
$this->allowedVisibility = $allowedVisibility;
$this->visibility &= $allowedVisibility;
return $this;
}
/**
* @param int $allowedVisibility
* @return self
*/
public function addAllowedVisibility($allowedVisibility) {
return $this->setAllowedVisibility($this->allowedVisibility | $allowedVisibility);
}
/**
* @param int $allowedVisibility
* @return self
*/
public function removeAllowedVisibility($allowedVisibility) {
return $this->setAllowedVisibility($this->allowedVisibility & ~$allowedVisibility);
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Listener;
use OCA\Files_External\Service\DBConfigService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Group\Events\GroupDeletedEvent;
class GroupDeletedListener implements IEventListener {
/** @var DBConfigService */
private $config;
public function __construct(DBConfigService $config) {
$this->config = $config;
}
public function handle(Event $event): void {
if (!$event instanceof GroupDeletedEvent) {
return;
}
$this->config->modifyMountsOnGroupDelete($event->getGroup()->getGID());
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Listener;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_External\AppInfo\Application;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\Util;
/**
* @template-implements IEventListener<Event|LoadAdditionalScriptsEvent>
*/
class LoadAdditionalListener implements IEventListener {
public function __construct(
private IConfig $config,
private IInitialState $initialState,
) {
}
public function handle(Event $event): void {
if (!($event instanceof LoadAdditionalScriptsEvent)) {
return;
}
$allowUserMounting = $this->config->getAppValue('files_external', 'allow_user_mounting', 'no') === 'yes';
$this->initialState->provideInitialState('allowUserMounting', $allowUserMounting);
Util::addInitScript(Application::APP_ID, 'init');
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020, Morris Jobke <hey@morrisjobke.de>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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;
class StorePasswordListener implements IEventListener {
/** @var ICredentialsManager */
private $credentialsManager;
public function __construct(ICredentialsManager $credentialsManager) {
$this->credentialsManager = $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);
}
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External\Listener;
use OCA\Files_External\Service\DBConfigService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\User\Events\UserDeletedEvent;
class UserDeletedListener implements IEventListener {
/** @var DBConfigService */
private $config;
public function __construct(DBConfigService $config) {
$this->config = $config;
}
public function handle(Event $event): void {
if (!$event instanceof UserDeletedEvent) {
return;
}
$this->config->modifyMountsOnUserDelete($event->getUser()->getUID());
}
}
@@ -0,0 +1,73 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Greta Doci <gretadoci@gmail.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External\Migration;
use OCP\IUser;
use OCP\IUserSession;
class DummyUserSession implements IUserSession {
/**
* @var IUser
*/
private $user;
public function login($uid, $password) {
}
public function logout() {
}
public function setUser($user) {
$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
}
}
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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;
}
}
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
/** @var IDBConnection */
private $connection;
/** @var LoggerInterface */
private $logger;
public function __construct(IDBConnection $connection, LoggerInterface $logger) {
$this->connection = $connection;
$this->logger = $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 \OCP\DB\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;
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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;
}
}
@@ -0,0 +1,285 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Andreas Fischer <bantu@owncloud.com>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Frank Karlitschek <frank@karlitschek.de>
* @author Jesús Macias <jmacias@solidgear.es>
* @author Joas Schilling <coding@schilljs.com>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Philipp Kapfer <philipp.kapfer@gmx.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External;
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\Files\StorageNotAvailableException;
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;
/** @var UserGlobalStoragesService */
private $userGlobalStorageService;
/** @var UserStoragesService */
private $userStorageService;
/** @var GlobalStoragesService */
private $globalStorageService;
public function __construct(
UserGlobalStoragesService $userGlobalStorageService,
UserStoragesService $userStorageService,
GlobalStoragesService $globalStorageService
) {
$this->userGlobalStorageService = $userGlobalStorageService;
$this->userStorageService = $userStorageService;
$this->globalStorageService = $globalStorageService;
}
/**
* @param mixed $input
* @param string|null $userId
* @return mixed
* @throws \OCP\AppFramework\QueryException
* @since 16.0.0
*/
public static function substitutePlaceholdersInConfig($input, string $userId = null) {
/** @var BackendService $backendService */
$backendService = \OC::$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, $isPersonal, $testOnly = true) {
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 \OC\Files\Storage\Common $storage */
$storage = new $class($options);
try {
$result = $storage->test($isPersonal, $testOnly);
$storage->setAvailability($result);
if ($result) {
return StorageNotAvailableException::STATUS_SUCCESS;
}
} catch (\Exception $e) {
$storage->setAvailability(false);
throw $e;
}
} catch (\Exception $exception) {
\OC::$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 = \OC::$server->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(\OCP\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 = \OC::$server->getSecureRandom()->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(\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(
[
'c' => $config['backend'],
'a' => $config['authMechanism'],
'm' => $config['mountpoint'],
'o' => $config['options'],
'p' => $config['priority'] ?? -1,
'mo' => $config['mountOptions'] ?? [],
]
);
return hash('md5', $data);
}
}
@@ -0,0 +1,29 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Files_External;
/**
* Storage is not found
*/
class NotFoundException extends \Exception {
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Kate Döen <kate.doeen@nextcloud.com>
*
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_External;
/**
* @psalm-type Files_ExternalStorageConfig = array{
* applicableGroups?: string[],
* applicableUsers?: 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 {
}

Some files were not shown because too many files have changed in this diff Show More