diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php index 8eebc550d0..5b12377fdb 100644 --- a/apps/files_external/lib/AppInfo/Application.php +++ b/apps/files_external/lib/AppInfo/Application.php @@ -29,6 +29,7 @@ namespace OCA\Files_External\AppInfo; +use OCA\Files_External\Config\UserPlaceholderHandler; use OCA\Files_External\Lib\Auth\PublicKey\RSAPrivateKey; use OCA\Files_External\Lib\Auth\SMB\KerberosAuth; use \OCP\AppFramework\App; @@ -67,7 +68,12 @@ use OCP\Files\Config\IUserMountCache; */ class Application extends App implements IBackendProvider, IAuthMechanismProvider { - public function __construct(array $urlParams = array()) { + /** + * Application constructor. + * + * @throws \OCP\AppFramework\QueryException + */ + public function __construct(array $urlParams = []) { parent::__construct('files_external', $urlParams); $container = $this->getContainer(); @@ -76,9 +82,13 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide return $c->getServer()->query('UserMountCache'); }); + /** @var BackendService $backendService */ $backendService = $container->query(BackendService::class); $backendService->registerBackendProvider($this); $backendService->registerAuthMechanismProvider($this); + $backendService->registerConfigHandler('user', function() use ($container) { + return $container->query(UserPlaceholderHandler::class); + }); // 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 diff --git a/apps/files_external/lib/Config/ConfigAdapter.php b/apps/files_external/lib/Config/ConfigAdapter.php index 34e96df044..8d9c7ea8d1 100644 --- a/apps/files_external/lib/Config/ConfigAdapter.php +++ b/apps/files_external/lib/Config/ConfigAdapter.php @@ -29,7 +29,6 @@ namespace OCA\Files_External\Config; use OC\Files\Storage\Wrapper\Availability; use OCA\Files_External\Migration\StorageMigrator; use OCP\Files\Storage; -use OC\Files\Mount\MountPoint; use OCP\Files\Storage\IStorageFactory; use OCA\Files_External\Lib\PersonalMount; use OCP\Files\Config\IMountProvider; @@ -73,12 +72,11 @@ class ConfigAdapter implements IMountProvider { * * @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, \OC_Mount_Config::setUserVars( - $user->getUID(), $value - )); + $storage->setBackendOption($option, \OC_Mount_Config::substitutePlaceholdersInConfig($value)); } $objectStore = $storage->getBackendOption('objectstore'); diff --git a/apps/files_external/lib/Config/IConfigHandler.php b/apps/files_external/lib/Config/IConfigHandler.php new file mode 100644 index 0000000000..3e2ec32ba2 --- /dev/null +++ b/apps/files_external/lib/Config/IConfigHandler.php @@ -0,0 +1,39 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +namespace OCA\Files_External\Config; + +/** + * Interface IConfigHandler + * + * @package OCA\Files_External\Config + * @since 16.0.0 + */ +interface IConfigHandler { + /** + * @param mixed $optionValue + * @return mixed the same type as $optionValue + * @since 16.0.0 + */ + public function handle($optionValue); +} diff --git a/apps/files_external/lib/Config/SimpleSubstitutionTrait.php b/apps/files_external/lib/Config/SimpleSubstitutionTrait.php new file mode 100644 index 0000000000..9abbfe0df2 --- /dev/null +++ b/apps/files_external/lib/Config/SimpleSubstitutionTrait.php @@ -0,0 +1,86 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +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 + */ + private $placeholder; + + /** @var string */ + protected $sanitizedPlaceholder; + + /** + * @param mixed $optionValue + * @param string $replacement + * @return mixed + * @since 16.0.0 + */ + private function processInput($optionValue, string $replacement) { + $this->checkPlaceholder(); + if (is_array($optionValue)) { + foreach ($optionValue as &$value) { + $value = $this->substituteIfString($value, $replacement); + } + } else { + $optionValue = $this->substituteIfString($optionValue, $replacement); + } + return $optionValue; + } + + /** + * @throws \RuntimeException + */ + protected function checkPlaceholder(): void { + $this->sanitizedPlaceholder = trim(strtolower($this->placeholder)); + if(!(bool)\preg_match('/^[a-z0-9]*$/', $this->sanitizedPlaceholder)) { + throw new \RuntimeException(sprintf( + 'Invalid placeholder %s, only [a-z0-9] are allowed', $this->sanitizedPlaceholder + )); + } + if($this->sanitizedPlaceholder === '') { + throw new \RuntimeException('Invalid empty placeholder'); + } + } + + /** + * @param mixed $value + * @param string $replacement + * @return mixed + */ + protected function substituteIfString($value, string $replacement) { + if(is_string($value)) { + return str_ireplace('$' . $this->sanitizedPlaceholder, $replacement, $value); + } + return $value; + } +} diff --git a/apps/files_external/lib/Config/UserPlaceholderHandler.php b/apps/files_external/lib/Config/UserPlaceholderHandler.php new file mode 100644 index 0000000000..721d3bbe02 --- /dev/null +++ b/apps/files_external/lib/Config/UserPlaceholderHandler.php @@ -0,0 +1,53 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +namespace OCA\Files_External\Config; + +use OCP\IUserSession; + +class UserPlaceholderHandler implements IConfigHandler { + use SimpleSubstitutionTrait; + + /** @var IUserSession */ + private $session; + + public function __construct(IUserSession $session) { + $this->session = $session; + $this->placeholder = 'user'; + } + + /** + * @param mixed $optionValue + * @return mixed the same type as $optionValue + * @since 16.0.0 + */ + public function handle($optionValue) { + $user = $this->session->getUser(); + if($user === null) { + return $optionValue; + } + $uid = $user->getUID(); + + return $this->processInput($optionValue, $uid); + } +} diff --git a/apps/files_external/lib/Service/BackendService.php b/apps/files_external/lib/Service/BackendService.php index bd6c525f31..05bda1998d 100644 --- a/apps/files_external/lib/Service/BackendService.php +++ b/apps/files_external/lib/Service/BackendService.php @@ -23,6 +23,7 @@ namespace OCA\Files_External\Service; +use OCA\Files_External\Config\IConfigHandler; use \OCP\IConfig; use \OCA\Files_External\Lib\Backend\Backend; @@ -67,6 +68,11 @@ class BackendService { /** @var IAuthMechanismProvider[] */ private $authMechanismProviders = []; + /** @var callable[] */ + private $configHandlerLoaders = []; + + private $configHandlers = []; + /** * @param IConfig $config */ @@ -280,4 +286,66 @@ class BackendService { protected function isAllowedAuthMechanism(AuthMechanism $authMechanism) { return true; // not implemented } + + /** + * registers a configuration handler + * + * The function of the provided $placeholder is mostly to act a sorting + * criteria, so longer placeholders are replaced first. This avoids + * "@user" overwriting parts of "@userMail" and "@userLang", for example. + * The provided value should not contain the @ prefix, only a-z0-9 are + * allowed. Upper case letters are lower cased, the replacement is case- + * insensitive. + * + * The configHandlerLoader should just instantiate the handler on demand. + * For now all handlers are instantiated when a mount is loaded, independent + * of whether the placeholder is present or not. This may change in future. + * + * @since 16.0.0 + */ + public function registerConfigHandler(string $placeholder, callable $configHandlerLoader) { + $placeholder = trim(strtolower($placeholder)); + if(!(bool)\preg_match('/^[a-z0-9]*$/', $placeholder)) { + throw new \RuntimeException(sprintf( + 'Invalid placeholder %s, only [a-z0-9] are allowed', $placeholder + )); + } + if($placeholder === '') { + throw new \RuntimeException('Invalid empty placeholder'); + } + if(isset($this->configHandlerLoaders[$placeholder]) || isset($this->configHandlers[$placeholder])) { + throw new \RuntimeException(sprintf('A handler is already registered for %s', $placeholder)); + } + $this->configHandlerLoaders[$placeholder] = $configHandlerLoader; + } + + protected function loadConfigHandlers():void { + $newLoaded = false; + foreach ($this->configHandlerLoaders as $placeholder => $loader) { + $handler = $loader(); + if(!$handler instanceof IConfigHandler) { + throw new \RuntimeException(sprintf( + 'Handler for %s is not an instance of IConfigHandler', $placeholder + )); + } + $this->configHandlers[] = $handler; + $newLoaded = true; + } + $this->configHandlerLoaders = []; + if($newLoaded) { + // ensure those with longest placeholders come first, + // to avoid substring matches + uksort($this->configHandlers, function ($phA, $phB) { + return strlen($phB) <=> strlen($phA); + }); + } + } + + /** + * @since 16.0.0 + */ + public function getConfigHandlers() { + $this->loadConfigHandlers(); + return $this->configHandlers; + } } diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index 7fb118c2de..2078abc029 100644 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -35,6 +35,8 @@ * */ +use OCA\Files_External\Config\IConfigHandler; +use OCA\Files_External\Config\UserPlaceholderHandler; use phpseclib\Crypt\AES; use \OCA\Files_External\AppInfo\Application; use \OCA\Files_External\Lib\Backend\LegacyBackend; @@ -104,7 +106,7 @@ class OC_Mount_Config { $mountPoint = '/'.$uid.'/files'.$storage->getMountPoint(); $mountEntry = self::prepareMountPointEntry($storage, false); foreach ($mountEntry['options'] as &$option) { - $option = self::setUserVars($uid, $option); + $option = self::substitutePlaceholdersInConfig($option); } $mountPoints[$mountPoint] = $mountEntry; } @@ -113,7 +115,7 @@ class OC_Mount_Config { $mountPoint = '/'.$uid.'/files'.$storage->getMountPoint(); $mountEntry = self::prepareMountPointEntry($storage, true); foreach ($mountEntry['options'] as &$option) { - $option = self::setUserVars($uid, $option); + $option = self::substitutePlaceholdersInConfig($uid, $option); } $mountPoints[$mountPoint] = $mountEntry; } @@ -199,18 +201,26 @@ class OC_Mount_Config { * @param string $user user value * @param string|array $input * @return string + * @deprecated use self::substitutePlaceholdersInConfig($input) */ public static function setUserVars($user, $input) { - if (is_array($input)) { - foreach ($input as &$value) { - if (is_string($value)) { - $value = str_replace('$user', $user, $value); - } - } - } else { - if (is_string($input)) { - $input = str_replace('$user', $user, $input); - } + $handler = self::$app->getContainer()->query(UserPlaceholderHandler::class); + return $handler->handle($input); + } + + /** + * @param mixed $input + * @return mixed + * @throws \OCP\AppFramework\QueryException + * @since 16.0.0 + */ + public static function substitutePlaceholdersInConfig($input) { + /** @var BackendService $backendService */ + $backendService = self::$app->getContainer()->query(BackendService::class); + /** @var IConfigHandler[] $handlers */ + $handlers = $backendService->getConfigHandlers(); + foreach ($handlers as $handler) { + $input = $handler->handle($input); } return $input; } @@ -229,7 +239,7 @@ class OC_Mount_Config { return StorageNotAvailableException::STATUS_SUCCESS; } foreach ($options as &$option) { - $option = self::setUserVars(OCP\User::getUser(), $option); + $option = self::substitutePlaceholdersInConfig($option); } if (class_exists($class)) { try { diff --git a/apps/files_external/tests/Config/UserPlaceholderHandlerTest.php b/apps/files_external/tests/Config/UserPlaceholderHandlerTest.php new file mode 100644 index 0000000000..d44a146cc8 --- /dev/null +++ b/apps/files_external/tests/Config/UserPlaceholderHandlerTest.php @@ -0,0 +1,81 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +namespace OCA\files_external\tests\Config; + +use OCA\Files_External\Config\UserPlaceholderHandler; +use OCP\IUser; +use OCP\IUserSession; + +class UserPlaceholderHandlerTest extends \Test\TestCase { + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject */ + protected $user; + + /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + protected $session; + + /** @var UserPlaceholderHandler */ + protected $handler; + + public function setUp() { + parent::setUp(); + + $this->user = $this->createMock(IUser::class); + $this->user->expects($this->any()) + ->method('getUid') + ->willReturn('alice'); + $this->session = $this->createMock(IUserSession::class); + + $this->handler = new UserPlaceholderHandler($this->session); + } + + protected function setUser() { + $this->session->expects($this->any()) + ->method('getUser') + ->willReturn($this->user); + } + + public function optionProvider() { + return [ + ['/foo/bar/$user/foobar', '/foo/bar/alice/foobar'], + [['/foo/bar/$user/foobar'], ['/foo/bar/alice/foobar']], + [['/FOO/BAR/$USER/FOOBAR'], ['/FOO/BAR/alice/FOOBAR']], + ]; + } + + /** + * @dataProvider optionProvider + */ + public function testHandle($option, $expected) { + $this->setUser(); + $this->assertSame($expected, $this->handler->handle($option)); + } + + /** + * @dataProvider optionProvider + */ + public function testHandleNoUser($option) { + $this->assertSame($option, $this->handler->handle($option)); + } + +} diff --git a/apps/files_external/tests/Service/BackendServiceTest.php b/apps/files_external/tests/Service/BackendServiceTest.php index e8a3181658..1e3b6b5875 100644 --- a/apps/files_external/tests/Service/BackendServiceTest.php +++ b/apps/files_external/tests/Service/BackendServiceTest.php @@ -23,31 +23,27 @@ */ namespace OCA\Files_External\Tests\Service; +use OCA\Files_External\Config\IConfigHandler; use OCA\Files_External\Lib\Auth\AuthMechanism; use OCA\Files_External\Lib\Backend\Backend; use OCA\Files_External\Lib\Config\IAuthMechanismProvider; use OCA\Files_External\Lib\Config\IBackendProvider; -use \OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\BackendService; use OCP\IConfig; -use OCP\IL10N; class BackendServiceTest extends \Test\TestCase { - /** @var \OCP\IConfig */ + /** @var \OCP\IConfig|\PHPUnit_Framework_MockObject_MockObject */ protected $config; - /** @var \OCP\IL10N */ - protected $l10n; - protected function setUp() { $this->config = $this->createMock(IConfig::class); - $this->l10n = $this->createMock(IL10N::class); } /** * @param string $class * - * @return \OCA\Files_External\Lib\Backend\Backend + * @return \OCA\Files_External\Lib\Backend\Backend|\PHPUnit_Framework_MockObject_MockObject */ protected function getBackendMock($class) { $backend = $this->getMockBuilder(Backend::class) @@ -61,7 +57,7 @@ class BackendServiceTest extends \Test\TestCase { /** * @param string $class * - * @return \OCA\Files_External\Lib\Auth\AuthMechanism + * @return \OCA\Files_External\Lib\Auth\AuthMechanism|\PHPUnit_Framework_MockObject_MockObject */ protected function getAuthMechanismMock($class) { $backend = $this->getMockBuilder(AuthMechanism::class) @@ -73,10 +69,11 @@ class BackendServiceTest extends \Test\TestCase { } public function testRegisterBackend() { - $service = new BackendService($this->config, $this->l10n); + $service = new BackendService($this->config); $backend = $this->getBackendMock('\Foo\Bar'); + /** @var \OCA\Files_External\Lib\Backend\Backend|\PHPUnit_Framework_MockObject_MockObject $backendAlias */ $backendAlias = $this->getMockBuilder(Backend::class) ->disableOriginalConstructor() ->getMock(); @@ -100,11 +97,12 @@ class BackendServiceTest extends \Test\TestCase { } public function testBackendProvider() { - $service = new BackendService($this->config, $this->l10n); + $service = new BackendService($this->config); $backend1 = $this->getBackendMock('\Foo\Bar'); $backend2 = $this->getBackendMock('\Bar\Foo'); + /** @var IBackendProvider|\PHPUnit_Framework_MockObject_MockObject $providerMock */ $providerMock = $this->createMock(IBackendProvider::class); $providerMock->expects($this->once()) ->method('getBackends') @@ -118,11 +116,12 @@ class BackendServiceTest extends \Test\TestCase { } public function testAuthMechanismProvider() { - $service = new BackendService($this->config, $this->l10n); + $service = new BackendService($this->config); $backend1 = $this->getAuthMechanismMock('\Foo\Bar'); $backend2 = $this->getAuthMechanismMock('\Bar\Foo'); + /** @var IAuthMechanismProvider|\PHPUnit_Framework_MockObject_MockObject $providerMock */ $providerMock = $this->createMock(IAuthMechanismProvider::class); $providerMock->expects($this->once()) ->method('getAuthMechanisms') @@ -136,18 +135,20 @@ class BackendServiceTest extends \Test\TestCase { } public function testMultipleBackendProviders() { - $service = new BackendService($this->config, $this->l10n); + $service = new BackendService($this->config); $backend1a = $this->getBackendMock('\Foo\Bar'); $backend1b = $this->getBackendMock('\Bar\Foo'); $backend2 = $this->getBackendMock('\Dead\Beef'); + /** @var IBackendProvider|\PHPUnit_Framework_MockObject_MockObject $provider1Mock */ $provider1Mock = $this->createMock(IBackendProvider::class); $provider1Mock->expects($this->once()) ->method('getBackends') ->willReturn([$backend1a, $backend1b]); $service->registerBackendProvider($provider1Mock); + /** @var IBackendProvider|\PHPUnit_Framework_MockObject_MockObject $provider2Mock */ $provider2Mock = $this->createMock(IBackendProvider::class); $provider2Mock->expects($this->once()) ->method('getBackends') @@ -169,7 +170,7 @@ class BackendServiceTest extends \Test\TestCase { ['files_external', 'user_mounting_backends', '', 'identifier:\User\Mount\Allowed,identifier_alias'] ])); - $service = new BackendService($this->config, $this->l10n); + $service = new BackendService($this->config); $backendAllowed = $this->getBackendMock('\User\Mount\Allowed'); $backendAllowed->expects($this->never()) @@ -193,7 +194,7 @@ class BackendServiceTest extends \Test\TestCase { } public function testGetAvailableBackends() { - $service = new BackendService($this->config, $this->l10n); + $service = new BackendService($this->config); $backendAvailable = $this->getBackendMock('\Backend\Available'); $backendAvailable->expects($this->once()) @@ -216,5 +217,49 @@ class BackendServiceTest extends \Test\TestCase { $this->assertArrayNotHasKey('identifier:\Backend\NotAvailable', $availableBackends); } + public function invalidConfigPlaceholderProvider() { + return [ + [['@user']], + [['hællo']], + [['spa ce']], + [['yo\o']], + [['']], + [['xxyoloxx', 'invÆlid']], + [['tautology', 'tautology']], + [['tautology2', 'TAUTOLOGY2']], + ]; + } + + /** + * @dataProvider invalidConfigPlaceholderProvider + * @expectedException \RuntimeException + */ + public function testRegisterConfigHandlerInvalid(array $placeholders) { + $service = new BackendService($this->config); + $mock = $this->createMock(IConfigHandler::class); + $cb = function () use ($mock) { return $mock; }; + foreach ($placeholders as $placeholder) { + $service->registerConfigHandler($placeholder, $cb); + } + } + + public function testConfigHandlers() { + $service = new BackendService($this->config); + $mock = $this->createMock(IConfigHandler::class); + $mock->expects($this->exactly(3)) + ->method('handle'); + $cb = function () use ($mock) { return $mock; }; + $service->registerConfigHandler('one', $cb); + $service->registerConfigHandler('2', $cb); + $service->registerConfigHandler('Three', $cb); + + /** @var IConfigHandler[] $handlers */ + $handlers = $service->getConfigHandlers(); + + foreach ($handlers as $handler) { + $handler->handle('Something'); + } + } + }