Skip to content

Commit 57c612f

Browse files
committed
feat(config): implementation of lexicon
Signed-off-by: Maxence Lange <[email protected]>
1 parent 3822db5 commit 57c612f

File tree

11 files changed

+546
-1
lines changed

11 files changed

+546
-1
lines changed

lib/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
'NCU\\Config\\Exceptions\\TypeConflictException' => $baseDir . '/lib/unstable/Config/Exceptions/TypeConflictException.php',
1212
'NCU\\Config\\Exceptions\\UnknownKeyException' => $baseDir . '/lib/unstable/Config/Exceptions/UnknownKeyException.php',
1313
'NCU\\Config\\IUserConfig' => $baseDir . '/lib/unstable/Config/IUserConfig.php',
14+
'NCU\\Config\\Lexicon\\ConfigLexiconEntry' => $baseDir . '/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php',
15+
'NCU\\Config\\Lexicon\\ConfigLexiconStrictness' => $baseDir . '/lib/unstable/Config/Lexicon/ConfigLexiconStrictness.php',
16+
'NCU\\Config\\Lexicon\\IConfigLexicon' => $baseDir . '/lib/unstable/Config/Lexicon/IConfigLexicon.php',
1417
'NCU\\Config\\ValueType' => $baseDir . '/lib/unstable/Config/ValueType.php',
1518
'OCP\\Accounts\\IAccount' => $baseDir . '/lib/public/Accounts/IAccount.php',
1619
'OCP\\Accounts\\IAccountManager' => $baseDir . '/lib/public/Accounts/IAccountManager.php',

lib/composer/composer/autoload_static.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
5252
'NCU\\Config\\Exceptions\\TypeConflictException' => __DIR__ . '/../../..' . '/lib/unstable/Config/Exceptions/TypeConflictException.php',
5353
'NCU\\Config\\Exceptions\\UnknownKeyException' => __DIR__ . '/../../..' . '/lib/unstable/Config/Exceptions/UnknownKeyException.php',
5454
'NCU\\Config\\IUserConfig' => __DIR__ . '/../../..' . '/lib/unstable/Config/IUserConfig.php',
55+
'NCU\\Config\\Lexicon\\ConfigLexiconEntry' => __DIR__ . '/../../..' . '/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php',
56+
'NCU\\Config\\Lexicon\\ConfigLexiconStrictness' => __DIR__ . '/../../..' . '/lib/unstable/Config/Lexicon/ConfigLexiconStrictness.php',
57+
'NCU\\Config\\Lexicon\\IConfigLexicon' => __DIR__ . '/../../..' . '/lib/unstable/Config/Lexicon/IConfigLexicon.php',
5558
'NCU\\Config\\ValueType' => __DIR__ . '/../../..' . '/lib/unstable/Config/ValueType.php',
5659
'OCP\\Accounts\\IAccount' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccount.php',
5760
'OCP\\Accounts\\IAccountManager' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccountManager.php',

lib/private/AppConfig.php

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
use InvalidArgumentException;
1313
use JsonException;
14+
use NCU\Config\Lexicon\ConfigLexiconEntry;
15+
use NCU\Config\Lexicon\ConfigLexiconStrictness;
16+
use NCU\Config\Lexicon\IConfigLexicon;
17+
use OC\AppFramework\Bootstrap\Coordinator;
1418
use OCP\DB\Exception as DBException;
1519
use OCP\DB\QueryBuilder\IQueryBuilder;
1620
use OCP\Exceptions\AppConfigIncorrectTypeException;
@@ -55,6 +59,8 @@ class AppConfig implements IAppConfig {
5559
private array $valueTypes = []; // type for all config values
5660
private bool $fastLoaded = false;
5761
private bool $lazyLoaded = false;
62+
/** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
63+
private array $configLexiconDetails = [];
5864

5965
/**
6066
* $migrationCompleted is only needed to manage the previous structure
@@ -430,6 +436,9 @@ private function getTypedValue(
430436
int $type,
431437
): string {
432438
$this->assertParams($app, $key, valueType: $type);
439+
if (!$this->compareRegisteredConfigValues($app, $key, $lazy, $type, $default)) {
440+
return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
441+
}
433442
$this->loadConfig($app, $lazy);
434443

435444
/**
@@ -721,6 +730,9 @@ private function setTypedValue(
721730
int $type,
722731
): bool {
723732
$this->assertParams($app, $key);
733+
if (!$this->compareRegisteredConfigValues($app, $key, $lazy, $type)) {
734+
return false; // returns false as database is not updated
735+
}
724736
$this->loadConfig(null, $lazy);
725737

726738
$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
@@ -1559,4 +1571,113 @@ private function getSensitiveKeys(string $app): array {
15591571
public function clearCachedConfig(): void {
15601572
$this->clearCache();
15611573
}
1574+
1575+
/**
1576+
* verify and compare current use of config values with defined lexicon
1577+
*
1578+
* @throws AppConfigUnknownKeyException
1579+
* @throws AppConfigTypeConflictException
1580+
*/
1581+
private function compareRegisteredConfigValues(
1582+
string $app,
1583+
string $key,
1584+
bool &$lazy,
1585+
int &$type,
1586+
string &$default = '',
1587+
): bool {
1588+
if (in_array($key,
1589+
[
1590+
'enabled',
1591+
'installed_version',
1592+
'types',
1593+
])) {
1594+
return true; // we don't break stuff for this list of config key.
1595+
}
1596+
$configDetails = $this->getConfigDetailsFromLexicon($app);
1597+
if (!array_key_exists($key, $configDetails['entries'])) {
1598+
return $this->applyLexiconStrictness(
1599+
$configDetails['strictness'],
1600+
'The app config key ' . $app . '/' . $key . ' is not defined in the config lexicon'
1601+
);
1602+
}
1603+
1604+
/** @var ConfigLexiconEntry $configValue */
1605+
$configValue = $configDetails['entries'][$key];
1606+
$type &= ~self::VALUE_SENSITIVE;
1607+
1608+
if ($type === self::VALUE_MIXED) {
1609+
$type = $configValue->getValueType()->value; // we overwrite if value was requested as mixed
1610+
} elseif ($configValue->getValueType()->value !== $type) {
1611+
throw new AppConfigTypeConflictException('The app config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1612+
}
1613+
1614+
$lazy = $configValue->isLazy();
1615+
$default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
1616+
if ($configValue->isFlagged(self::FLAG_SENSITIVE)) {
1617+
$type |= self::VALUE_SENSITIVE;
1618+
}
1619+
if ($configValue->isDeprecated()) {
1620+
$this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.');
1621+
}
1622+
1623+
return true;
1624+
}
1625+
1626+
/**
1627+
* manage ConfigLexicon behavior based on strictness set in IConfigLexicon
1628+
*
1629+
* @see IConfigLexicon::getStrictness()
1630+
* @param string $app
1631+
* @param string $key
1632+
* @param ConfigLexiconStrictness $strictness
1633+
*
1634+
* @return bool TRUE if conflict can be fully ignored
1635+
* @throws AppConfigUnknownKeyException
1636+
*/
1637+
private function applyLexiconStrictness(
1638+
?ConfigLexiconStrictness $strictness,
1639+
string $line = '',
1640+
): bool {
1641+
if ($strictness === null) {
1642+
return true;
1643+
}
1644+
1645+
switch ($strictness) {
1646+
case ConfigLexiconStrictness::IGNORE:
1647+
return true;
1648+
case ConfigLexiconStrictness::NOTICE:
1649+
$this->logger->notice($line);
1650+
return true;
1651+
case ConfigLexiconStrictness::WARNING:
1652+
$this->logger->warning($line);
1653+
return false;
1654+
}
1655+
1656+
throw new AppConfigUnknownKeyException($line);
1657+
}
1658+
1659+
/**
1660+
* extract details from registered $appId's config lexicon
1661+
*
1662+
* @param string $appId
1663+
*
1664+
* @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
1665+
*/
1666+
private function getConfigDetailsFromLexicon(string $appId): array {
1667+
if (!array_key_exists($appId, $this->configLexiconDetails)) {
1668+
$entries = [];
1669+
$bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
1670+
$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
1671+
foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) {
1672+
$entries[$configEntry->getKey()] = $configEntry;
1673+
}
1674+
1675+
$this->configLexiconDetails[$appId] = [
1676+
'entries' => $entries,
1677+
'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
1678+
];
1679+
}
1680+
1681+
return $this->configLexiconDetails[$appId];
1682+
}
15621683
}

lib/private/AppFramework/Bootstrap/RegistrationContext.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace OC\AppFramework\Bootstrap;
1111

1212
use Closure;
13+
use NCU\Config\Lexicon\IConfigLexicon;
1314
use OC\Support\CrashReport\Registry;
1415
use OCP\AppFramework\App;
1516
use OCP\AppFramework\Bootstrap\IRegistrationContext;
@@ -141,6 +142,9 @@ class RegistrationContext {
141142
/** @var ServiceRegistration<IDeclarativeSettingsForm>[] */
142143
private array $declarativeSettings = [];
143144

145+
/** @var array<array-key, string> */
146+
private array $configLexiconClasses = [];
147+
144148
/** @var ServiceRegistration<ITeamResourceProvider>[] */
145149
private array $teamResourceProviders = [];
146150

@@ -422,6 +426,13 @@ public function registerMailProvider(string $class): void {
422426
$class
423427
);
424428
}
429+
430+
public function registerConfigLexicon(string $configLexiconClass): void {
431+
$this->context->registerConfigLexicon(
432+
$this->appId,
433+
$configLexiconClass
434+
);
435+
}
425436
};
426437
}
427438

@@ -621,6 +632,13 @@ public function registerMailProvider(string $appId, string $class): void {
621632
$this->mailProviders[] = new ServiceRegistration($appId, $class);
622633
}
623634

635+
/**
636+
* @psalm-param class-string<IConfigLexicon> $configLexiconClass
637+
*/
638+
public function registerConfigLexicon(string $appId, string $configLexiconClass): void {
639+
$this->configLexiconClasses[$appId] = $configLexiconClass;
640+
}
641+
624642
/**
625643
* @param App[] $apps
626644
*/
@@ -972,4 +990,20 @@ public function getTaskProcessingTaskTypes(): array {
972990
public function getMailProviders(): array {
973991
return $this->mailProviders;
974992
}
993+
994+
/**
995+
* returns IConfigLexicon registered by the app.
996+
* null if none registered.
997+
*
998+
* @param string $appId
999+
*
1000+
* @return IConfigLexicon|null
1001+
*/
1002+
public function getConfigLexicon(string $appId): ?IConfigLexicon {
1003+
if (!array_key_exists($appId, $this->configLexiconClasses)) {
1004+
return null;
1005+
}
1006+
1007+
return \OCP\Server::get($this->configLexiconClasses[$appId]);
1008+
}
9751009
}

lib/private/Config/UserConfig.php

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
use NCU\Config\Exceptions\TypeConflictException;
1616
use NCU\Config\Exceptions\UnknownKeyException;
1717
use NCU\Config\IUserConfig;
18+
use NCU\Config\Lexicon\ConfigLexiconEntry;
19+
use NCU\Config\Lexicon\ConfigLexiconStrictness;
1820
use NCU\Config\ValueType;
21+
use OC\AppFramework\Bootstrap\Coordinator;
1922
use OCP\DB\Exception as DBException;
2023
use OCP\DB\IResult;
2124
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -63,6 +66,8 @@ class UserConfig implements IUserConfig {
6366
private array $fastLoaded = [];
6467
/** @var array<string, boolean> ['user_id' => bool] */
6568
private array $lazyLoaded = [];
69+
/** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
70+
private array $configLexiconDetails = [];
6671

6772
public function __construct(
6873
protected IDBConnection $connection,
@@ -706,6 +711,9 @@ private function getTypedValue(
706711
ValueType $type,
707712
): string {
708713
$this->assertParams($userId, $app, $key);
714+
if (!$this->compareRegisteredConfigValues($app, $key, $lazy, $type, default: $default)) {
715+
return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
716+
}
709717
$this->loadConfig($userId, $lazy);
710718

711719
/**
@@ -1038,14 +1046,17 @@ private function setTypedValue(
10381046
ValueType $type,
10391047
): bool {
10401048
$this->assertParams($userId, $app, $key);
1049+
if (!$this->compareRegisteredConfigValues($app, $key, $lazy, $type, $flags)) {
1050+
return false; // returns false as database is not updated
1051+
}
10411052
$this->loadConfig($userId, $lazy);
10421053

10431054
$inserted = $refreshCache = false;
10441055
$origValue = $value;
10451056
$sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags);
10461057
if ($sensitive || ($this->hasKey($userId, $app, $key, $lazy) && $this->isSensitive($userId, $app, $key, $lazy))) {
10471058
$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1048-
$flags |= UserConfig::FLAG_SENSITIVE;
1059+
$flags |= self::FLAG_SENSITIVE;
10491060
}
10501061

10511062
// if requested, we fill the 'indexed' field with current value
@@ -1803,4 +1814,96 @@ private function decryptSensitiveValue(string $userId, string $app, string $key,
18031814
]);
18041815
}
18051816
}
1817+
1818+
/**
1819+
* verify and compare current use of config values with defined lexicon
1820+
*
1821+
* @throws UnknownKeyException
1822+
* @throws TypeConflictException
1823+
*/
1824+
private function compareRegisteredConfigValues(
1825+
string $app,
1826+
string $key,
1827+
bool &$lazy,
1828+
ValueType &$type,
1829+
int &$flags = 0,
1830+
string &$default = '',
1831+
): bool {
1832+
$configDetails = $this->getConfigDetailsFromLexicon($app);
1833+
if (!array_key_exists($key, $configDetails['entries'])) {
1834+
return $this->applyLexiconStrictness($configDetails['strictness'], 'The user config key ' . $app . '/' . $key . ' is not defined in the config lexicon');
1835+
}
1836+
1837+
/** @var ConfigLexiconEntry $configValue */
1838+
$configValue = $configDetails['entries'][$key];
1839+
if ($type === ValueType::MIXED) {
1840+
$type = $configValue->getValueType()->value; // we overwrite if value was requested as mixed
1841+
} elseif ($configValue->getValueType()->value !== $type) {
1842+
throw new TypeConflictException('The user config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1843+
}
1844+
1845+
$lazy = $configValue->isLazy();
1846+
$default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
1847+
$flags = $configValue->getFlags();
1848+
1849+
if ($configValue->isDeprecated()) {
1850+
$this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
1851+
}
1852+
1853+
return true;
1854+
}
1855+
1856+
/**
1857+
* manage ConfigLexicon behavior based on strictness set in IConfigLexicon
1858+
*
1859+
* @see IConfigLexicon::getStrictness()
1860+
* @param ConfigLexiconStrictness|null $strictness
1861+
* @param string $line
1862+
*
1863+
* @return bool TRUE if conflict can be fully ignored
1864+
* @throws UnknownKeyException
1865+
*/
1866+
private function applyLexiconStrictness(?ConfigLexiconStrictness $strictness, string $line = ''): bool {
1867+
if ($strictness === null) {
1868+
return true;
1869+
}
1870+
1871+
switch ($strictness) {
1872+
case ConfigLexiconStrictness::IGNORE:
1873+
return true;
1874+
case ConfigLexiconStrictness::NOTICE:
1875+
$this->logger->notice($line);
1876+
return true;
1877+
case ConfigLexiconStrictness::WARNING:
1878+
$this->logger->warning($line);
1879+
return false;
1880+
}
1881+
1882+
throw new UnknownKeyException($line);
1883+
}
1884+
1885+
/**
1886+
* extract details from registered $appId's config lexicon
1887+
*
1888+
* @param string $appId
1889+
*
1890+
* @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
1891+
*/
1892+
private function getConfigDetailsFromLexicon(string $appId): array {
1893+
if (!array_key_exists($appId, $this->configLexiconDetails)) {
1894+
$entries = [];
1895+
$bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
1896+
$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
1897+
foreach ($configLexicon?->getUserPreferences() ?? [] as $configEntry) {
1898+
$entries[$configEntry->getKey()] = $configEntry;
1899+
}
1900+
1901+
$this->configLexiconDetails[$appId] = [
1902+
'entries' => $entries,
1903+
'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
1904+
];
1905+
}
1906+
1907+
return $this->configLexiconDetails[$appId];
1908+
}
18061909
}

0 commit comments

Comments
 (0)