fix(files_external): adjust settings

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-01-12 12:28:18 +01:00 committed by nextcloud-command
parent 545c72becb
commit 5ed7c4fd97
9 changed files with 130 additions and 319 deletions

View file

@ -236,7 +236,7 @@ abstract class StoragesController extends Controller {
} catch (StorageNotAvailableException $e) {
$storage->setStatus(
(int)$e->getCode(),
$this->l10n->t('%s', [$e->getMessage()])
$e->getMessage(),
);
} catch (\Exception $e) {
// FIXME: convert storage exceptions to StorageNotAvailableException

View file

@ -263,7 +263,7 @@ class BackendService {
* @param Backend $backend
* @return bool
*/
protected function isAllowedUserBackend(Backend $backend): bool {
public function isAllowedUserBackend(Backend $backend): bool {
return ($this->isUserMountingAllowed() && array_intersect($backend->getIdentifierAliases(), $this->userMountingBackends));
}

View file

@ -27,14 +27,14 @@ class Admin implements ISettings {
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
) {
$this->visibility = BackendService::VISIBILITY_ADMIN;
}
/**
* @return TemplateResponse
*/
public function getForm() {
// Shared settings (user & admin)
$this->setInitialState(BackendService::VISIBILITY_ADMIN);
$this->setInitialState();
// Admin specific
$backends = $this->backendService->getAvailableBackends();
@ -42,16 +42,6 @@ class Admin implements ISettings {
$this->initialState->provideInitialState('user-mounting', [
'allowUserMounting' => $this->backendService->isUserMountingAllowed(),
'allowedBackends' => array_values(array_map(fn (Backend $backend) => $backend->getIdentifier(), $allowedBackends)),
'backends' => array_values(
array_map(
fn (Backend $backend) => [
'id' => $backend->getIdentifier(),
'displayName' => $backend->getText(),
'deprecated' => $backend->getDeprecateTo()?->getIdentifier(),
],
$backends,
),
),
]);
$this->loadScriptsAndStyles();

View file

@ -12,28 +12,28 @@ use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
use OCA\Files_External\Lib\Backend\Backend;
use OCA\Files_External\Service\BackendService;
use OCP\AppFramework\Services\IInitialState;
use OCP\Encryption\IManager;
use OCP\IURLGenerator;
use OCP\Util;
trait CommonSettingsTrait {
private BackendService $backendService;
private IManager $encryptionManager;
private IInitialState $initialState;
private IURLGenerator $urlGenerator;
private GlobalAuth $globalAuth;
private int $visibility;
private ?string $userId = null;
/** @var Backend[]|null */
private ?array $backends = null;
/**
* Set the initial state for the user / admin settings
*
* @param int $visibilityType The visibility type used to determine which options to show (admin vs user settings)
*/
protected function setInitialState(int $visibilityType) {
protected function setInitialState() {
$allowUserMounting = $this->backendService->isUserMountingAllowed();
$isAdmin = $visibilityType === BackendService::VISIBILITY_ADMIN;
$isAdmin = $this->visibility === BackendService::VISIBILITY_ADMIN;
$canCreateMounts = $isAdmin || $allowUserMounting;
$this->initialState->provideInitialState('settings', [
@ -43,6 +43,7 @@ trait CommonSettingsTrait {
'dependencyIssues' => $canCreateMounts ? $this->dependencyMessage() : null,
/** Is this the admin settings or just user settings */
'isAdmin' => $isAdmin,
'hasEncryption' => $this->encryptionManager->isEnabled(),
]);
$this->initialState->provideInitialState(
@ -54,24 +55,42 @@ trait CommonSettingsTrait {
$this->globalAuth->getAuth($this->userId ?? ''),
),
);
$this->initialState->provideInitialState(
'allowedBackends',
array_map(fn (Backend $backend) => $backend->getIdentifier(), $this->getAvailableBackends()),
);
$this->initialState->provideInitialState(
'backends',
array_values($this->backendService->getAvailableBackends()),
);
$this->initialState->provideInitialState(
'authMechanisms',
array_values($this->backendService->getAuthMechanisms()),
);
}
/**
* Load the frontend script including the custom backend dependencies
*/
protected function loadScriptsAndStyles() {
Util::addStyle('files_external', 'init_settings');
Util::addInitScript('files_external', 'init_settings');
Util::addScript('files_external', 'settings');
Util::addStyle('files_external', 'settings');
// load custom JS
foreach ($this->backendService->getAvailableBackends() as $backend) {
foreach ($backend->getCustomJs() as $script) {
Util::addStyle('files_external', $script);
Util::addScript('files_external', $script);
}
}
foreach ($this->backendService->getAuthMechanisms() as $authMechanism) {
foreach ($authMechanism->getCustomJs() as $script) {
Util::addStyle('files_external', $script);
Util::addScript('files_external', $script);
}
}
@ -86,7 +105,7 @@ trait CommonSettingsTrait {
$dependencyGroups = [];
// Try all backends and check their dependencies
foreach ($this->backendService->getAvailableBackends() as $backend) {
foreach ($this->getAvailableBackends() as $backend) {
foreach ($backend->checkDependencies() as $dependency) {
$dependencyMessage = $dependency->getMessage();
if ($dependencyMessage !== null) {
@ -108,4 +127,15 @@ trait CommonSettingsTrait {
'modules' => $missingModules,
];
}
private function getAvailableBackends(): array {
if ($this->backends === null) {
$backends = $this->backendService->getAvailableBackends();
if ($this->visibility === BackendService::VISIBILITY_PERSONAL) {
$backends = array_filter($backends, $this->backendService->isAllowedUserBackend(...));
}
$this->backends = array_values($backends);
}
return $this->backends;
}
}

View file

@ -10,6 +10,7 @@ use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
use OCA\Files_External\Service\BackendService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Encryption\IManager;
use OCP\IURLGenerator;
use OCP\Settings\ISettings;
@ -22,23 +23,26 @@ class Personal implements ISettings {
private GlobalAuth $globalAuth,
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
private IManager $encryptionManager,
) {
$this->userId = $userId;
$this->visibility = BackendService::VISIBILITY_PERSONAL;
}
/**
* @return TemplateResponse
*/
public function getForm() {
$this->setInitialState(BackendService::VISIBILITY_PERSONAL);
$this->setInitialState();
$this->loadScriptsAndStyles();
return new TemplateResponse('files_external', 'settings', renderAs: '');
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection() {
if (!$this->backendService->isUserMountingAllowed()) {
return null;
}
return 'externalstorages';
}

View file

@ -3,6 +3,8 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { IBackend } from '../types.ts'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
@ -12,14 +14,9 @@ import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwit
const userMounting = loadState<{
allowUserMounting: boolean
allowedBackends: string[]
backends: {
id: string
displayName: string
deprecated?: string
}[]
}>('files_external', 'user-mounting')
const availableBackends = userMounting.backends
const availableBackends = loadState<IBackend[]>('files_external', 'backends')
const allowUserMounting = ref(userMounting.allowUserMounting)
const allowedBackends = ref<string[]>(userMounting.allowedBackends)
@ -79,23 +76,14 @@ watch(allowedBackends, (newValue, oldValue) => {
<legend>
{{ t('files_external', 'External storage backends people are allowed to mount') }}
</legend>
<template v-for="backend of availableBackends">
<NcCheckboxRadioSwitch
v-if="!backend.deprecated"
:key="backend.id"
v-model="allowedBackends"
:value="backend.id"
name="allowUserMountingBackends[]">
{{ backend.displayName }}
</NcCheckboxRadioSwitch>
<input
v-else-if="backend.id in allowedBackends"
:key="`${backend.id}-deprecated`"
:data-deprecate-to="backend.deprecated"
:value="backend.id"
name="allowUserMountingBackends[]"
type="hidden">
</template>
<NcCheckboxRadioSwitch
v-for="backend of availableBackends"
:key="backend.identifier"
v-model="allowedBackends"
:value="backend.identifier"
name="allowUserMountingBackends[]">
{{ backend.name }}
</NcCheckboxRadioSwitch>
</fieldset>
</form>
</template>

View file

@ -1,3 +1,10 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getLoggerBuilder } from '@nextcloud/logger'
export default getLoggerBuilder().setApp('files_external').build()
export default getLoggerBuilder()
.setApp('files_external')
.build()

View file

@ -1,240 +0,0 @@
<?php
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;
/** @var array $_ */
$canCreateMounts = $_['visibilityType'] === BackendService::VISIBILITY_ADMIN || $_['allowUserMounting'];
$l->t('Enable encryption');
$l->t('Enable previews');
$l->t('Enable sharing');
$l->t('Check for changes');
$l->t('Never');
$l->t('Once every direct access');
$l->t('Read only');
script('files_external', [
'settings',
'templates'
]);
style('files_external', 'settings');
// load custom JS
foreach ($_['backends'] as $backend) {
/** @var Backend $backend */
$scripts = $backend->getCustomJs();
foreach ($scripts as $script) {
script('files_external', $script);
}
}
foreach ($_['authMechanisms'] as $authMechanism) {
/** @var AuthMechanism $authMechanism */
$scripts = $authMechanism->getCustomJs();
foreach ($scripts as $script) {
script('files_external', $script);
}
}
function writeParameterInput($parameter, $options, $classes = []) {
$value = '';
if (isset($options[$parameter->getName()])) {
$value = $options[$parameter->getName()];
}
$placeholder = $parameter->getText();
$is_optional = $parameter->isFlagSet(DefinitionParameter::FLAG_OPTIONAL);
switch ($parameter->getType()) {
case DefinitionParameter::VALUE_PASSWORD: ?>
<?php if ($is_optional) {
$classes[] = 'optional';
} ?>
<input type="password"
<?php if (!empty($classes)): ?> class="<?php p(implode(' ', $classes)); ?>"<?php endif; ?>
data-parameter="<?php p($parameter->getName()); ?>"
value="<?php p($value); ?>"
placeholder="<?php p($placeholder); ?>"
/>
<?php
break;
case DefinitionParameter::VALUE_BOOLEAN: ?>
<?php $checkboxId = uniqid('checkbox_'); ?>
<div>
<label>
<input type="checkbox"
id="<?php p($checkboxId); ?>"
<?php if (!empty($classes)): ?> class="checkbox <?php p(implode(' ', $classes)); ?>"<?php endif; ?>
data-parameter="<?php p($parameter->getName()); ?>"
<?php if ($value === true): ?> checked="checked"<?php endif; ?>
/>
<?php p($placeholder); ?>
</label>
</div>
<?php
break;
case DefinitionParameter::VALUE_HIDDEN: ?>
<input type="hidden"
<?php if (!empty($classes)): ?> class="<?php p(implode(' ', $classes)); ?>"<?php endif; ?>
data-parameter="<?php p($parameter->getName()); ?>"
value="<?php p($value); ?>"
/>
<?php
break;
default: ?>
<?php if ($is_optional) {
$classes[] = 'optional';
} ?>
<input type="text"
<?php if (!empty($classes)): ?> class="<?php p(implode(' ', $classes)); ?>"<?php endif; ?>
data-parameter="<?php p($parameter->getName()); ?>"
value="<?php p($value); ?>"
placeholder="<?php p($placeholder); ?>"
/>
<?php
}
}
?>
<div class="emptyfilelist emptycontent hidden">
<div class="icon-external"></div>
<h2><?php p($l->t('No external storage configured or you don\'t have the permission to configure them')); ?></h2>
</div>
<?php
$canCreateNewLocalStorage = \OC::$server->getConfig()->getSystemValue('files_external_allow_create_new_local', true);
?>
<form data-can-create="<?php echo $canCreateMounts?'true':'false' ?>" data-can-create-local="<?php echo $canCreateNewLocalStorage?'true':'false' ?>" id="files_external" class="section" data-encryption-enabled="<?php echo $_['encryptionEnabled']?'true': 'false'; ?>">
<h2 class="inlineblock" data-anchor-name="external-storage"><?php p($l->t('External storage')); ?></h2>
<a target="_blank" rel="noreferrer" class="icon-info" title="<?php p($l->t('Open documentation'));?>" href="<?php p(link_to_docs('admin-external-storage')); ?>"></a>
<p class="settings-hint"><?php p($l->t('External storage enables you to mount external storage services and devices as secondary Nextcloud storage devices. You may also allow people to mount their own external storage services.')); ?></p>
<?php if (isset($_['dependencies']) and ($_['dependencies'] !== '') and $canCreateMounts) {
print_unescaped('' . $_['dependencies'] . '');
} ?>
<table id="externalStorage" class="grid" data-admin='<?php print_unescaped(json_encode($_['visibilityType'] === BackendService::VISIBILITY_ADMIN)); ?>'>
<thead>
<tr>
<th></th>
<th><?php p($l->t('Folder name')); ?></th>
<th><?php p($l->t('External storage')); ?></th>
<th><?php p($l->t('Authentication')); ?></th>
<th><?php p($l->t('Configuration')); ?></th>
<?php if ($_['visibilityType'] === BackendService::VISIBILITY_ADMIN) {
print_unescaped('<th>' . $l->t('Available for') . '</th>');
} ?>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr class="externalStorageLoading">
<td colspan="8">
<span id="externalStorageLoading" class="icon icon-loading"></span>
</td>
</tr>
<tr id="addMountPoint"
<?php if (!$canCreateMounts): ?>
style="display: none;"
<?php endif; ?>
>
<td class="status">
<span data-placement="right" title="<?php p($l->t('Click to recheck the configuration')); ?>"></span>
</td>
<td class="mountPoint"><input type="text" name="mountPoint" value=""
placeholder="<?php p($l->t('Folder name')); ?>">
</td>
<td class="backend">
<select id="selectBackend" class="selectBackend" data-configurations='<?php p(json_encode($_['backends'])); ?>'>
<option value="" disabled selected
style="display:none;">
<?php p($l->t('Add storage')); ?>
</option>
<?php
$sortedBackends = array_filter($_['backends'], function ($backend) use ($_) {
return $backend->isVisibleFor($_['visibilityType']);
});
uasort($sortedBackends, function ($a, $b) {
return strcasecmp($a->getText(), $b->getText());
});
?>
<?php foreach ($sortedBackends as $backend): ?>
<?php if ($backend->getDeprecateTo() || (!$canCreateNewLocalStorage && $backend->getIdentifier() == 'local')) {
continue;
} // ignore deprecated backends?>
<option value="<?php p($backend->getIdentifier()); ?>"><?php p($backend->getText()); ?></option>
<?php endforeach; ?>
</select>
</td>
<td class="authentication" data-mechanisms='<?php p(json_encode($_['authMechanisms'])); ?>'></td>
<td class="configuration"></td>
<?php if ($_['visibilityType'] === BackendService::VISIBILITY_ADMIN): ?>
<td class="applicable" align="right">
<label><input type="checkbox" class="applicableToAllUsers" checked="" /><?php p($l->t('All people')); ?></label>
<div class="applicableUsersContainer">
<input type="hidden" class="applicableUsers" style="width:20em;" value="" />
</div>
</td>
<?php endif; ?>
<td class="mountOptionsToggle hidden">
<button type="button" class="icon-more" aria-expanded="false" title="<?php p($l->t('Advanced settings')); ?>"></button>
<input type="hidden" class="mountOptions" value="" />
</td>
<td class="save hidden">
<button type="button" class="icon-checkmark" title="<?php p($l->t('Save')); ?>"></button>
</td>
</tr>
</tbody>
</table>
<?php if ($_['visibilityType'] === BackendService::VISIBILITY_ADMIN): ?>
<input type="checkbox" name="allowUserMounting" id="allowUserMounting" class="checkbox"
value="1" <?php if ($_['allowUserMounting']) {
print_unescaped(' checked="checked"');
} ?> />
<label for="allowUserMounting"><?php p($l->t('Allow people to mount external storage')); ?></label> <span id="userMountingMsg" class="msg"></span>
<p id="userMountingBackends"<?php if (!$_['allowUserMounting']): ?> class="hidden"<?php endif; ?>>
<?php
$userBackends = array_filter($_['backends'], function ($backend) {
return $backend->isAllowedVisibleFor(BackendService::VISIBILITY_PERSONAL);
});
?>
<?php $i = 0;
foreach ($userBackends as $backend): ?>
<?php if ($deprecateTo = $backend->getDeprecateTo()): ?>
<input type="hidden" id="allowUserMountingBackends<?php p($i); ?>" name="allowUserMountingBackends[]" value="<?php p($backend->getIdentifier()); ?>" data-deprecate-to="<?php p($deprecateTo->getIdentifier()); ?>" />
<?php else: ?>
<input type="checkbox" id="allowUserMountingBackends<?php p($i); ?>" class="checkbox" name="allowUserMountingBackends[]" value="<?php p($backend->getIdentifier()); ?>" <?php if ($backend->isVisibleFor(BackendService::VISIBILITY_PERSONAL)) {
print_unescaped(' checked="checked"');
} ?> />
<label for="allowUserMountingBackends<?php p($i); ?>"><?php p($backend->getText()); ?></label> <br />
<?php endif; ?>
<?php $i++; ?>
<?php endforeach; ?>
</p>
<?php endif; ?>
</form>
<div class="followupsection">
<form autocomplete="false" action="#"
id="global_credentials" method="post"
class="<?php if (isset($_['visibilityType']) && $_['visibilityType'] === BackendService::VISIBILITY_PERSONAL) {
print_unescaped('global_credentials__personal');
} ?>">
<h2><?php p($l->t('Global credentials')); ?></h2>
<p class="settings-hint"><?php p($l->t('Global credentials can be used to authenticate with multiple external storages that have the same credentials.')); ?></p>
<input type="text" name="username"
autocomplete="false"
value="<?php p($_['globalCredentials']['user']); ?>"
placeholder="<?php p($l->t('Login')) ?>"/>
<input type="password" name="password"
autocomplete="false"
value="<?php p($_['globalCredentials']['password']); ?>"
placeholder="<?php p($l->t('Password')) ?>"/>
<input type="hidden" name="uid"
value="<?php p($_['globalCredentialsUid']); ?>"/>
<input type="submit" value="<?php p($l->t('Save')) ?>"/>
</form>
</div>

View file

@ -8,12 +8,15 @@ declare(strict_types=1);
namespace OCA\Files_External\Tests\Settings;
use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
use OCA\Files_External\MountConfig;
use OCA\Files_External\Lib\Backend\Backend;
use OCA\Files_External\Service\BackendService;
use OCA\Files_External\Service\GlobalStoragesService;
use OCA\Files_External\Settings\Admin;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Encryption\IManager;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@ -22,6 +25,9 @@ class AdminTest extends TestCase {
private GlobalStoragesService&MockObject $globalStoragesService;
private BackendService&MockObject $backendService;
private GlobalAuth&MockObject $globalAuth;
private IInitialState&MockObject $initialState;
private IURLGenerator&MockObject $urlGenerator;
private IAppManager&MockObject $appManager;
private Admin $admin;
protected function setUp(): void {
@ -30,58 +36,84 @@ class AdminTest extends TestCase {
$this->globalStoragesService = $this->createMock(GlobalStoragesService::class);
$this->backendService = $this->createMock(BackendService::class);
$this->globalAuth = $this->createMock(GlobalAuth::class);
$this->initialState = $this->createMock(IInitialState::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->admin = new Admin(
$this->encryptionManager,
$this->globalStoragesService,
$this->backendService,
$this->globalAuth
$this->globalAuth,
$this->initialState,
$this->urlGenerator,
$this->appManager,
);
}
public function testGetForm(): void {
$backends = [
$this->createMock(Backend::class),
];
$backends[0]->method('checkDependencies')->willReturn([]);
$backends[0]->method('getIdentifier')->willReturn('backend1');
$authMechanism = $this->createMock(GlobalAuth::class);
$this->encryptionManager
->expects($this->once())
->method('isEnabled')
->willReturn(false);
$this->globalStoragesService
->expects($this->once())
->method('getStorages')
->willReturn(['a', 'b', 'c']);
$this->backendService
->expects($this->once())
->expects($this->atLeastOnce())
->method('getAvailableBackends')
->willReturn(['d', 'e', 'f']);
->willReturn($backends);
$this->backendService
->expects($this->once())
->expects($this->atLeastOnce())
->method('getAuthMechanisms')
->willReturn(['g', 'h', 'i']);
->willReturn([$authMechanism]);
$this->backendService
->expects($this->once())
->expects($this->atLeastOnce())
->method('isUserMountingAllowed')
->willReturn(true);
$this->backendService
->expects($this->exactly(2))
->method('getBackends')
->willReturn([]);
$this->globalAuth
->expects($this->once())
->method('getAuth')
->with('')
->willReturn('asdf:asdf');
$params = [
'encryptionEnabled' => false,
'visibilityType' => BackendService::VISIBILITY_ADMIN,
'storages' => ['a', 'b', 'c'],
'backends' => ['d', 'e', 'f'],
'authMechanisms' => ['g', 'h', 'i'],
'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()),
'allowUserMounting' => true,
'globalCredentials' => 'asdf:asdf',
'globalCredentialsUid' => '',
];
$expected = new TemplateResponse('files_external', 'settings', $params, '');
->willReturn(['asdf' => 'asdf']);
$initialState = [];
$this->initialState
->expects($this->atLeastOnce())
->method('provideInitialState')
->willReturnCallback(function () use (&$initialState) {
$args = func_get_args();
$initialState[$args[0]] = $args[1];
});
$expected = new TemplateResponse('files_external', 'settings', renderAs: '');
$this->assertEquals($expected, $this->admin->getForm());
$this->assertEquals($initialState, [
'settings' => [
'docUrl' => '',
'dependencyIssues' => [
'messages' => [],
'modules' => [],
],
'isAdmin' => true,
'hasEncryption' => false,
],
'global-credentials' => [
'uid' => '',
'asdf' => 'asdf',
],
'allowedBackends' => ['backend1'],
'backends' => $backends,
'authMechanisms' => [$authMechanism],
'user-mounting' => [
'allowUserMounting' => true,
'allowedBackends' => [],
],
]);
}
public function testGetSection(): void {