Merge pull request #54414 from nextcloud/feat/noid/compare-defaults-on-preset

feat(preset): compare default for all preset
feat/cache-user-config
Maxence Lange 2025-08-19 12:31:45 +07:00 committed by GitHub
commit 8a2b02ab9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 128 additions and 2 deletions

@ -56,6 +56,7 @@ return [
['name' => 'TwoFactorSettings#index', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'GET' , 'root' => ''],
['name' => 'TwoFactorSettings#update', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'PUT' , 'root' => ''],
['name' => 'AISettings#update', 'url' => '/settings/api/admin/ai', 'verb' => 'PUT' , 'root' => ''],
['name' => 'Preset#getPreset', 'url' => '/settings/preset', 'verb' => 'GET' , 'root' => ''],
['name' => 'Help#help', 'url' => '/settings/help/{mode}', 'verb' => 'GET', 'defaults' => ['mode' => ''] , 'root' => ''],

@ -32,6 +32,7 @@ return array(
'OCA\\Settings\\Controller\\LogSettingsController' => $baseDir . '/../lib/Controller/LogSettingsController.php',
'OCA\\Settings\\Controller\\MailSettingsController' => $baseDir . '/../lib/Controller/MailSettingsController.php',
'OCA\\Settings\\Controller\\PersonalSettingsController' => $baseDir . '/../lib/Controller/PersonalSettingsController.php',
'OCA\\Settings\\Controller\\PresetController' => $baseDir . '/../lib/Controller/PresetController.php',
'OCA\\Settings\\Controller\\ReasonsController' => $baseDir . '/../lib/Controller/ReasonsController.php',
'OCA\\Settings\\Controller\\TwoFactorSettingsController' => $baseDir . '/../lib/Controller/TwoFactorSettingsController.php',
'OCA\\Settings\\Controller\\UsersController' => $baseDir . '/../lib/Controller/UsersController.php',

@ -47,6 +47,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Controller\\LogSettingsController' => __DIR__ . '/..' . '/../lib/Controller/LogSettingsController.php',
'OCA\\Settings\\Controller\\MailSettingsController' => __DIR__ . '/..' . '/../lib/Controller/MailSettingsController.php',
'OCA\\Settings\\Controller\\PersonalSettingsController' => __DIR__ . '/..' . '/../lib/Controller/PersonalSettingsController.php',
'OCA\\Settings\\Controller\\PresetController' => __DIR__ . '/..' . '/../lib/Controller/PresetController.php',
'OCA\\Settings\\Controller\\ReasonsController' => __DIR__ . '/..' . '/../lib/Controller/ReasonsController.php',
'OCA\\Settings\\Controller\\TwoFactorSettingsController' => __DIR__ . '/..' . '/../lib/Controller/TwoFactorSettingsController.php',
'OCA\\Settings\\Controller\\UsersController' => __DIR__ . '/..' . '/../lib/Controller/UsersController.php',

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Settings\Controller;
use OC\Config\PresetManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class PresetController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private readonly PresetManager $presetManager,
) {
parent::__construct($appName, $request);
}
public function getPreset(): DataResponse {
return new DataResponse($this->presetManager->retrieveLexiconPreset());
}
}

@ -11,6 +11,7 @@ namespace OC\Core\Command\Config;
use OC\Config\PresetManager;
use OC\Core\Command\Base;
use OCP\Config\Lexicon\Preset as ConfigLexiconPreset;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -28,7 +29,8 @@ class Preset extends Base {
$this->setName('config:preset')
->setDescription('Select a config preset')
->addArgument('preset', InputArgument::OPTIONAL, 'Preset to use for all unset config values', '')
->addOption('list', '', InputOption::VALUE_NONE, 'display available preset');
->addOption('list', '', InputOption::VALUE_NONE, 'display available preset')
->addOption('compare', '', InputOption::VALUE_NONE, 'compare preset');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
@ -38,6 +40,24 @@ class Preset extends Base {
return self::SUCCESS;
}
if ($input->getOption('compare')) {
$list = $this->presetManager->retrieveLexiconPreset();
if ($input->getOption('output') === 'plain') {
$table = new Table($output);
$table->setHeaders(['app', 'config key', 'value', ...array_map(static fn (ConfigLexiconPreset $p): string => $p->name, ConfigLexiconPreset::cases())]);
foreach ($list as $appId => $entries) {
foreach ($entries as $item) {
$table->addRow([$appId, $item['entry']['key'], '<comment>' . ($item['value'] ?? '') . '</comment>', ...($item['defaults'] ?? [])]);
}
}
$table->render();
return self::SUCCESS;
}
$this->writeArrayInOutputFormat($input, $output, $list);
return self::SUCCESS;
}
$presetArg = $input->getArgument('preset');
if ($presetArg !== '') {
$preset = $this->getEnum($presetArg, $list);

@ -1791,7 +1791,13 @@ class AppConfig implements IAppConfig {
return $this->configLexiconDetails[$appId];
}
private function getLexiconEntry(string $appId, string $key): ?Entry {
/**
* get Lexicon Entry using appId and config key entry
*
* @return Entry|null NULL if entry does not exist in app's Lexicon
* @internal
*/
public function getLexiconEntry(string $appId, string $key): ?Entry {
return $this->getConfigDetailsFromLexicon($appId)['entries'][$key] ?? null;
}

@ -9,10 +9,13 @@ declare(strict_types=1);
namespace OC\Config;
use OC\App\AppManager;
use OC\AppConfig;
use OC\Installer;
use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
use OCP\Config\Lexicon\Preset;
use OCP\Exceptions\AppConfigUnknownKeyException;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\Server;
use Psr\Log\LoggerInterface;
@ -57,6 +60,67 @@ class PresetManager {
return $this->configLexiconPreset;
}
/**
* get lexicon config entries affected by Preset and its default values
*
* **Warning** This method MUST be considered resource-needy!
*
* @return array<string, list<array{defaults: array{CLUB: null|string, FAMILY: null|string, LARGE: null|string, MEDIUM: null|string, NONE: null|string, PRIVATE: null|string, SCHOOL: null|string, SHARED: null|string, SMALL: null|string, UNIVERSITY: null|string}, entry: array{definition: string, deprecated: bool, key: string, lazy: bool, note: string, type: 'ARRAY'|'BOOL'|'FLOAT'|'INT'|'MIXED'|'STRING'}, value?: mixed}>>
*/
public function retrieveLexiconPreset(?string $appId = null): array {
if ($appId === null) {
$apps = [];
foreach (['core'] + Server::get(IAppManager::class)->getEnabledApps() as $app) {
$preset = $this->retrieveLexiconPreset($app);
$apps[$app] = $preset[$app];
}
return $apps;
}
/** @var AppConfig|null $appConfig */
$appConfig = Server::get(IAppConfig::class);
$lexicon = $appConfig->getConfigDetailsFromLexicon($appId);
$presets = [];
foreach ($lexicon['entries'] as $entry) {
$defaults = [];
foreach (Preset::cases() as $case) {
// for each case, we need to use a fresh IAppConfig with clear cache
// cloning to avoid conflict while emulating preset
$newConfig = clone $appConfig;
$newConfig->clearCache(); // needed to ignore cache and rebuild default
$newLexicon = $newConfig->getLexiconEntry($appId, $entry->getKey());
$defaults[$case->name] = $newLexicon?->getDefault($case);
}
// compare all value from $defaults, if more than 1 exist we have a preset
$uniqueness = array_unique($defaults);
if (count($uniqueness) < 2) {
continue;
}
$details = [
'entry' => [
'key' => $entry->getKey(),
'type' => $entry->getValueType()->name,
'definition' => $entry->getDefinition(),
'lazy' => $entry->isLazy(),
'deprecated' => $entry->isDeprecated(),
'note' => $entry->getNote(),
],
'defaults' => $defaults
];
try {
$details['value'] = $appConfig->getDetails($appId, $entry->getKey())['value'];
} catch (AppConfigUnknownKeyException) {
}
$presets[] = $details;
}
return [$appId => $presets];
}
/**
* Enable and/or Disable a list of apps based on the currently selected Preset
*/