Skip to content

Commit 2294ff1

Browse files
committed
feat(lexicon): configurable presets
Signed-off-by: Maxence Lange <[email protected]>
1 parent acc2311 commit 2294ff1

File tree

7 files changed

+183
-27
lines changed

7 files changed

+183
-27
lines changed

core/preset/default.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
return [
4+
'core' => [
5+
'appConfig' => [
6+
'!loadedConfigPreset' => 'default',
7+
],
8+
'userConfig' => [
9+
],
10+
],
11+
'any_app' => [
12+
'__appEnabledAtInstall' => true, // enable app right after the end of the Nextcloud installation process
13+
'__appLocked' => true, // lock the status of the app, disabling the enable/disable status switching
14+
'appConfig' => [
15+
'any_key' => 'value',
16+
],
17+
'userConfig' => [
18+
],
19+
],
20+
];

lib/private/AppConfig.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use NCU\Config\Lexicon\ConfigLexiconStrictness;
1616
use NCU\Config\Lexicon\IConfigLexicon;
1717
use OC\AppFramework\Bootstrap\Coordinator;
18+
use OC\Config\PresetManager;
1819
use OCP\DB\Exception as DBException;
1920
use OCP\DB\QueryBuilder\IQueryBuilder;
2021
use OCP\Exceptions\AppConfigIncorrectTypeException;
@@ -69,6 +70,7 @@ public function __construct(
6970
protected IDBConnection $connection,
7071
protected LoggerInterface $logger,
7172
protected ICrypto $crypto,
73+
private readonly PresetManager $presetManager,
7274
) {
7375
}
7476

@@ -1597,15 +1599,25 @@ private function matchAndApplyLexiconDefinition(
15971599
}
15981600

15991601
$lazy = $configValue->isLazy();
1600-
$default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
16011602
if ($configValue->isFlagged(self::FLAG_SENSITIVE)) {
16021603
$type |= self::VALUE_SENSITIVE;
16031604
}
16041605
if ($configValue->isDeprecated()) {
16051606
$this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.');
16061607
}
16071608

1608-
return true;
1609+
$preset = $this->presetManager->getPreset();
1610+
$enforcedValue = $preset->isAppConfigEnforced($app, $key);
1611+
if (!$enforcedValue && $this->hasKey($app, $key, $lazy)) {
1612+
// if key exists there should be no need to extract default
1613+
return true;
1614+
}
1615+
1616+
// preset, then default from Lexicon
1617+
$default = $preset->getAppConfigDefault($app, $key) ?? $configValue->getDefault() ?? $default; // default from Lexicon got priority
1618+
1619+
// returning false will make get() returning $default and set() not changing value in database
1620+
return !$enforcedValue;
16091621
}
16101622

16111623
/**

lib/private/Config/Lexicon/CoreConfigLexicon.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function getStrictness(): ConfigLexiconStrictness {
2828
public function getAppConfigs(): array {
2929
return [
3030
new ConfigLexiconEntry('lastcron', ValueType::INT, 0, 'timestamp of last cron execution'),
31+
new ConfigLexiconEntry('loadedConfigPreset', ValueType::STRING, '', 'used to confirm the loading of config.preset', true),
3132
];
3233
}
3334

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace OC\Config\Model;
4+
5+
use OCP\Server;
6+
use Psr\Log\LoggerInterface;
7+
8+
/**
9+
* @since 32.0.0
10+
*/
11+
class PresetDefault {
12+
private array $preset = [];
13+
public function __construct(
14+
private readonly string $presetFile,
15+
private readonly bool $invalidate = false,
16+
) {
17+
$this->loadEntries();
18+
}
19+
20+
public function getKeys(string $appId): array {
21+
return array_keys($this->preset[$appId] ?? []);
22+
}
23+
24+
public function isAppConfigEnforced(string $appId, string $key): bool {
25+
return (($this->preset[$appId]['appConfig']['!' . $key] ?? null) !== null);
26+
}
27+
28+
public function getAppConfigDefault(string $appId, string $key): ?string {
29+
return $this->preset[$appId]['appConfig']['!' . $key] ?? $this->preset[$appId]['appConfig'][$key] ?? null;
30+
}
31+
32+
public function isUserConfigEnforced(string $appId, string $key): bool {
33+
return (($this->preset[$appId]['userConfig']['!' . $key] ?? null) !== null);
34+
}
35+
36+
public function getUserConfigDefault(string $appId, string $key): ?string {
37+
return $this->preset[$appId]['userConfig'][$key] ?? null;
38+
}
39+
40+
private function loadEntries(): void {
41+
if ($this->invalidate && function_exists('opcache_invalidate')) {
42+
@opcache_invalidate($this->presetFile, false);
43+
44+
}
45+
46+
$fp = (file_exists($this->presetFile) ? fopen($this->presetFile, 'r') : false);
47+
if (!$fp) {
48+
Server::get(LoggerInterface::class)->warning(sprintf('Preset file %s does not exist', $this->presetFile));
49+
return;
50+
}
51+
52+
if (!flock($fp, LOCK_SH)) {
53+
Server::get(LoggerInterface::class)->warning(sprintf('Could not acquire a shared lock on preset file %s', $this->presetFile));
54+
return;
55+
}
56+
57+
try {
58+
$alreadySent = (!defined('PHPUNIT_RUN') && headers_sent());
59+
$preset = include $this->presetFile;
60+
} finally {
61+
flock($fp, LOCK_UN);
62+
fclose($fp);
63+
}
64+
65+
if (!$alreadySent && !defined('PHPUNIT_RUN') && headers_sent()) {
66+
Server::get(LoggerInterface::class)->warning(sprintf('Preset file has leading content, please remove everything before "<?php" in %s', $this->presetFile));
67+
throw new \Exception('preset file has leading content.');
68+
}
69+
70+
if ($preset && is_array($preset)) {
71+
$this->preset = $preset;
72+
}
73+
}
74+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OC\Config;
10+
11+
use OC\Config\Model\PresetDefault;
12+
use OCP\IConfig;
13+
use Psr\Log\LoggerInterface;
14+
15+
/**
16+
* tools to work on preset
17+
*
18+
* @since 32.0.0
19+
*/
20+
class PresetManager {
21+
public const CONFIG_PRESET = 'config.preset';
22+
private ?PresetDefault $presetDefault = null;
23+
public function __construct(
24+
private readonly IConfig $config,
25+
private readonly LoggerInterface $logger,
26+
) {
27+
}
28+
29+
/**
30+
* @since 32.0.0
31+
* @return PresetDefault
32+
*/
33+
public function getPreset(): PresetDefault {
34+
if ($this->presetDefault === null) {
35+
$this->loadPresetFile();
36+
}
37+
38+
return $this->presetDefault;
39+
}
40+
41+
/**
42+
* @param string|null $presetFile
43+
* @since 32.0.0
44+
*/
45+
public function loadPresetFile(?string $presetFile = null): void {
46+
$this->presetDefault = new PresetDefault($presetFile ?? $this->getPresetFilepath());
47+
}
48+
49+
/**
50+
* @param string $presetFile
51+
* @since 32.0.0
52+
* @return bool
53+
*/
54+
public function parsePresetFile(string $presetFile): bool {
55+
// TOOD: loadPresetFile, verify value type with configlexicon
56+
return true;
57+
}
58+
59+
private function getPresetFilepath(): string {
60+
$preset = $this->config->getSystemValueString(self::CONFIG_PRESET, 'default');
61+
// TODO: sanitize $preset ?
62+
return \OC::$SERVERROOT . '/core/preset/' . $preset . '.php';
63+
}
64+
}

lib/private/Config/UserConfig.php

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public function __construct(
7070
protected IConfig $config,
7171
protected LoggerInterface $logger,
7272
protected ICrypto $crypto,
73+
private readonly PresetManager $presetManager,
7374
) {
7475
}
7576

@@ -1865,40 +1866,20 @@ private function matchAndApplyLexiconDefinition(
18651866
$this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
18661867
}
18671868

1868-
$enforcedValue = $this->config->getSystemValue('lexicon.default.userconfig.enforced', [])[$app][$key] ?? false;
1869+
$preset = $this->presetManager->getPreset();
1870+
$enforcedValue = $preset->isUserConfigEnforced($app, $key);
18691871
if (!$enforcedValue && $this->hasKey($userId, $app, $key, $lazy)) {
18701872
// if key exists there should be no need to extract default
18711873
return true;
18721874
}
18731875

1874-
// default from Lexicon got priority but it can still be overwritten by admin
1875-
$default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault() ?? $default;
1876+
// preset, then default from Lexicon
1877+
$default = $preset->getUserConfigDefault($app, $key) ?? $configValue->getDefault() ?? $default;
18761878

18771879
// returning false will make get() returning $default and set() not changing value in database
18781880
return !$enforcedValue;
18791881
}
18801882

1881-
/**
1882-
* get default value set in config/config.php if stored in key:
1883-
*
1884-
* 'lexicon.default.userconfig' => [
1885-
* <appId> => [
1886-
* <configKey> => 'my value',
1887-
* ]
1888-
* ],
1889-
*
1890-
* The entry is converted to string to fit the expected type when managing default value
1891-
*/
1892-
private function getSystemDefault(string $appId, ConfigLexiconEntry $configValue): ?string {
1893-
$default = $this->config->getSystemValue('lexicon.default.userconfig', [])[$appId][$configValue->getKey()] ?? null;
1894-
if ($default === null) {
1895-
// no system default, using default default.
1896-
return null;
1897-
}
1898-
1899-
return $configValue->convertToString($default);
1900-
}
1901-
19021883
/**
19031884
* manage ConfigLexicon behavior based on strictness set in IConfigLexicon
19041885
*

lib/unstable/Config/Lexicon/ConfigLexiconEntry.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ConfigLexiconEntry {
2121
private ?string $default = null;
2222

2323
/**
24-
* @param string $key config key
24+
* @param string $key config key, can only contain alphanumerical chars and -._
2525
* @param ValueType $type type of config value
2626
* @param string $definition optional description of config key available when using occ command
2727
* @param bool $lazy set config value as lazy
@@ -41,6 +41,10 @@ public function __construct(
4141
private readonly int $flags = 0,
4242
private readonly bool $deprecated = false,
4343
) {
44+
// key can only contain alphanumeric chars and _-.
45+
if (preg_match("/[^[:alnum:]\-._]/", $key)) {
46+
throw new \Exception();
47+
}
4448
/** @psalm-suppress UndefinedClass */
4549
if (\OC::$CLI) { // only store definition if ran from CLI
4650
$this->definition = $definition;

0 commit comments

Comments
 (0)