From e49c1a5e089d55a4bb69ba2afa055fb6671dae93 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Wed, 3 Apr 2024 16:13:25 -0100 Subject: [PATCH] feat(config): implement config lexicon Signed-off-by: Maxence Lange --- lib/composer/composer/autoload_classmap.php | 3 + lib/composer/composer/autoload_static.php | 3 + lib/private/AppConfig.php | 68 +++++++++++ .../Bootstrap/RegistrationContext.php | 36 ++++++ .../ConfigLexicon/ConfigLexiconEntry.php | 115 ++++++++++++++++++ .../Bootstrap/IRegistrationContext.php | 2 + lib/public/ConfigLexicon/IConfigLexicon.php | 16 +++ .../ConfigLexicon/IConfigLexiconEntry.php | 50 ++++++++ tests/lib/AppConfigTest.php | 5 + 9 files changed, 298 insertions(+) create mode 100644 lib/private/ConfigLexicon/ConfigLexiconEntry.php create mode 100644 lib/public/ConfigLexicon/IConfigLexicon.php create mode 100644 lib/public/ConfigLexicon/IConfigLexiconEntry.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 83760755da4f5..7f64500db2f11 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -211,6 +211,8 @@ 'OCP\\Comments\\MessageTooLongException' => $baseDir . '/lib/public/Comments/MessageTooLongException.php', 'OCP\\Comments\\NotFoundException' => $baseDir . '/lib/public/Comments/NotFoundException.php', 'OCP\\Common\\Exception\\NotFoundException' => $baseDir . '/lib/public/Common/Exception/NotFoundException.php', + 'OCP\\ConfigLexicon\\IConfigLexicon' => $baseDir . '/lib/public/ConfigLexicon/IConfigLexicon.php', + 'OCP\\ConfigLexicon\\IConfigLexiconEntry' => $baseDir . '/lib/public/ConfigLexicon/IConfigLexiconEntry.php', 'OCP\\Config\\BeforePreferenceDeletedEvent' => $baseDir . '/lib/public/Config/BeforePreferenceDeletedEvent.php', 'OCP\\Config\\BeforePreferenceSetEvent' => $baseDir . '/lib/public/Config/BeforePreferenceSetEvent.php', 'OCP\\Console\\ConsoleEvent' => $baseDir . '/lib/public/Console/ConsoleEvent.php', @@ -1016,6 +1018,7 @@ 'OC\\Comments\\Manager' => $baseDir . '/lib/private/Comments/Manager.php', 'OC\\Comments\\ManagerFactory' => $baseDir . '/lib/private/Comments/ManagerFactory.php', 'OC\\Config' => $baseDir . '/lib/private/Config.php', + 'OC\\ConfigLexicon\\ConfigLexiconEntry' => $baseDir . '/lib/private/ConfigLexicon/ConfigLexiconEntry.php', 'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php', 'OC\\ContactsManager' => $baseDir . '/lib/private/ContactsManager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 56f623cbaeee4..2c3df4f42b6d6 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -244,6 +244,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Comments\\MessageTooLongException' => __DIR__ . '/../../..' . '/lib/public/Comments/MessageTooLongException.php', 'OCP\\Comments\\NotFoundException' => __DIR__ . '/../../..' . '/lib/public/Comments/NotFoundException.php', 'OCP\\Common\\Exception\\NotFoundException' => __DIR__ . '/../../..' . '/lib/public/Common/Exception/NotFoundException.php', + 'OCP\\ConfigLexicon\\IConfigLexicon' => __DIR__ . '/../../..' . '/lib/public/ConfigLexicon/IConfigLexicon.php', + 'OCP\\ConfigLexicon\\IConfigLexiconEntry' => __DIR__ . '/../../..' . '/lib/public/ConfigLexicon/IConfigLexiconEntry.php', 'OCP\\Config\\BeforePreferenceDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Config/BeforePreferenceDeletedEvent.php', 'OCP\\Config\\BeforePreferenceSetEvent' => __DIR__ . '/../../..' . '/lib/public/Config/BeforePreferenceSetEvent.php', 'OCP\\Console\\ConsoleEvent' => __DIR__ . '/../../..' . '/lib/public/Console/ConsoleEvent.php', @@ -1049,6 +1051,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Comments\\Manager' => __DIR__ . '/../../..' . '/lib/private/Comments/Manager.php', 'OC\\Comments\\ManagerFactory' => __DIR__ . '/../../..' . '/lib/private/Comments/ManagerFactory.php', 'OC\\Config' => __DIR__ . '/../../..' . '/lib/private/Config.php', + 'OC\\ConfigLexicon\\ConfigLexiconEntry' => __DIR__ . '/../../..' . '/lib/private/ConfigLexicon/ConfigLexiconEntry.php', 'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php', 'OC\\ContactsManager' => __DIR__ . '/../../..' . '/lib/private/ContactsManager.php', diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 518ba6ebf7a01..18eaf18650a6b 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -38,6 +38,9 @@ use InvalidArgumentException; use JsonException; +use OC\AppFramework\Bootstrap\Coordinator; +use OCP\ConfigLexicon\IConfigLexicon; +use OCP\ConfigLexicon\IConfigLexiconEntry; use OCP\DB\Exception as DBException; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Exceptions\AppConfigIncorrectTypeException; @@ -82,6 +85,8 @@ class AppConfig implements IAppConfig { private array $valueTypes = []; // type for all config values private bool $fastLoaded = false; private bool $lazyLoaded = false; + /** @var array ['app_id' => IConfigLexicon] */ + private array $configLexiconDetails = []; /** * $migrationCompleted is only needed to manage the previous structure @@ -97,6 +102,7 @@ public function __construct( protected IDBConnection $connection, protected LoggerInterface $logger, protected ICrypto $crypto, + private Coordinator $coordinator, ) { } @@ -455,6 +461,7 @@ private function getTypedValue( int $type ): string { $this->assertParams($app, $key, valueType: $type); + $this->compareRegisteredConfigValues($app, $key, $lazy, $type, $default); $this->loadConfig($lazy); /** @@ -746,6 +753,7 @@ private function setTypedValue( int $type ): bool { $this->assertParams($app, $key); + $this->compareRegisteredConfigValues($app, $key, $lazy, $type); $this->loadConfig($lazy); $sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type); @@ -1506,4 +1514,64 @@ private function getSensitiveKeys(string $app): array { public function clearCachedConfig(): void { $this->clearCache(); } + + /** + * @throws AppConfigUnknownKeyException + * @throws AppConfigTypeConflictException + */ + private function compareRegisteredConfigValues( + string $app, + string $key, + bool &$lazy, + int &$type, + string &$default = '', + ): void { + $configDetails = $this->getConfigDetailsFromLexicon($app); + if (!array_key_exists($key, $configDetails['entries'])) { + if ($configDetails['strict'] === true) { + throw new AppConfigUnknownKeyException('The key is not defined in the app config lexicon'); + } + return; + } + + $configValue = $configDetails['entries'][$key]; + $type &= ~self::VALUE_SENSITIVE; + + if ($configValue->getValueType() !== match($type) { + self::VALUE_STRING => IConfigLexiconEntry::TYPE_STRING, + self::VALUE_INT => IConfigLexiconEntry::TYPE_INT, + self::VALUE_FLOAT => IConfigLexiconEntry::TYPE_FLOAT, + self::VALUE_BOOL => IConfigLexiconEntry::TYPE_BOOL, + self::VALUE_ARRAY => IConfigLexiconEntry::TYPE_ARRAY, + }) { + throw new AppConfigTypeConflictException('The key is typed incorrectly in relation to the app config lexicon'); + } + + $lazy = $configValue->isLazy(); + $default = $configValue->getDefault() ?? $default; + if ($configValue->isSensitive()) { + $type |= self::VALUE_SENSITIVE; + } + if ($configValue->isDeprecated()) { + $this->logger->notice('config value ' . $key . ' from ' . $app . ' is set as deprecated'); + } + } + + private function getConfigDetailsFromLexicon(string $appId): array { + if (!array_key_exists($appId, $this->configLexiconDetails)) { + $entries = []; + $configLexicon = $this->coordinator->getRegistrationContext()->getConfigLexicon($appId); + foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) { + $entries[$configEntry->getKey()] = $configEntry; + } + + $this->configLexiconDetails[$appId] = [ + 'entries' => $entries, + 'strict' => $configLexicon?->isStrict() ?? false + ]; + } + + return $this->configLexiconDetails[$appId]; + } + } diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index b1b2c57da555a..62611866ba084 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -41,6 +41,8 @@ use OCP\Calendar\Room\IBackend as IRoomBackend; use OCP\Capabilities\ICapability; use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\ConfigLexicon\IConfigLexicon; +use OCP\ConfigLexicon\IConfigValue; use OCP\Dashboard\IManager; use OCP\Dashboard\IWidget; use OCP\EventDispatcher\IEventDispatcher; @@ -59,6 +61,8 @@ use OCP\TextProcessing\IProvider as ITextProcessingProvider; use OCP\Translation\ITranslationProvider; use OCP\UserMigration\IMigrator as IUserMigrator; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; use RuntimeException; use Throwable; @@ -160,6 +164,9 @@ class RegistrationContext { /** @var ServiceRegistration[] */ private array $declarativeSettings = []; + /** @var array */ + private array $configLexiconClasses = []; + /** @var ServiceRegistration[] */ private array $teamResourceProviders = []; @@ -411,6 +418,13 @@ public function registerDeclarativeSettings(string $declarativeSettingsClass): v $declarativeSettingsClass ); } + + public function registerConfigLexicon(string $configLexiconClass): void { + $this->context->registerConfigLexicon( + $this->appId, + $configLexiconClass + ); + } }; } @@ -590,6 +604,10 @@ public function registerDeclarativeSettings(string $appId, string $declarativeSe $this->declarativeSettings[] = new ServiceRegistration($appId, $declarativeSettingsClass); } + public function registerConfigLexicon(string $appId, string $configLexiconClass): void { + $this->configLexiconClasses[$appId] = $configLexiconClass; + } + /** * @param App[] $apps */ @@ -920,4 +938,22 @@ public function getTeamResourceProviders(): array { public function getDeclarativeSettings(): array { return $this->declarativeSettings; } + + /** + * returns IConfigLexicon registered by the app. + * null if none registered. + * + * @param string $appId + * + * @return IConfigLexicon|null + */ + public function getConfigLexicon(string $appId): ?IConfigLexicon { + if (!array_key_exists($appId, $this->configLexiconClasses)) { + return null; + } + + $configLexicon = \OCP\Server::get($this->configLexiconClasses[$appId], IConfigLexicon::class); + // confirmer IConfigLexicon avant le load ? + return $configLexicon; + } } diff --git a/lib/private/ConfigLexicon/ConfigLexiconEntry.php b/lib/private/ConfigLexicon/ConfigLexiconEntry.php new file mode 100644 index 0000000000000..981e1193c5345 --- /dev/null +++ b/lib/private/ConfigLexicon/ConfigLexiconEntry.php @@ -0,0 +1,115 @@ + + * + * @author Maxence Lange + * + * @license AGPL-3.0 or later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OC\ConfigLexicon; + +use OC; +use OCP\ConfigLexicon\IConfigLexiconEntry; + +class ConfigLexiconEntry implements IConfigLexiconEntry { + private string $definition = ''; + private ?string $default = null; + private bool $lazy = false; + private bool $sensitive = false; + private bool $deprecated = false; + + public function __construct( + private string $key, + private int $valueType, + string $definition = '', + ) { + + if (OC::$CLI) { // only store definition if ran from CLI + $this->definition = $definition; + } + } + + public function getKey(): string { + return $this->key; + } + + public function getValueType(): int { + return $this->valueType; + } + + public function withDefaultString(string $default): self { + $this->default = $default; + return $this; + } + + public function withDefaultInt(int $default): self { + $this->default = (string) $default; + return $this; + } + + public function withDefaultFloat(float $default): self { + $this->default = (string) $default; + return $this; + } + + public function withDefaultBool(bool $default): self { + $this->default = ($default) ? '1' : '0'; + return $this; + } + + public function withDefaultArray(array $default): self { + $this->default = json_encode($default); + return $this; + } + + public function getDefault(): ?string { + return $this->default; + } + + public function getDefinition(): string { + return $this->definition; + } + + public function asLazy(bool $lazy = true): self { + $this->lazy = $lazy; + return $this; + } + + public function isLazy(): bool { + return $this->lazy; + } + + public function asSensitive(bool $sensitive = true): self { + $this->sensitive = $sensitive; + return $this; + } + + public function isSensitive(): bool { + return $this->sensitive; + } + + public function asDeprecated(bool $deprecated = true): self { + $this->deprecated = $deprecated; + return $this; + } + + public function isDeprecated(): bool { + return $this->deprecated; + } +} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 09bc703e0a41c..e20bac19a22fb 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -410,4 +410,6 @@ public function registerSetupCheck(string $setupCheckClass): void; * @since 29.0.0 */ public function registerDeclarativeSettings(string $declarativeSettingsClass): void; + + public function registerConfigLexicon(string $configLexiconClass): void; } diff --git a/lib/public/ConfigLexicon/IConfigLexicon.php b/lib/public/ConfigLexicon/IConfigLexicon.php new file mode 100644 index 0000000000000..c936c0c8e2e55 --- /dev/null +++ b/lib/public/ConfigLexicon/IConfigLexicon.php @@ -0,0 +1,16 @@ + + * + * @author Maxence Lange + * + * @license AGPL-3.0 or later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\ConfigLexicon; + +interface IConfigLexiconEntry { + public const TYPE_STRING = 1; + public const TYPE_INT = 2; + public const TYPE_FLOAT = 3; + public const TYPE_BOOL = 4; + public const TYPE_ARRAY = 5; + + public function getKey(): string; + public function getValueType(): int; + + public function withDefaultString(string $default): self; + public function withDefaultInt(int $default): self; + public function withDefaultFloat(float $default): self; + public function withDefaultBool(bool $default): self; + public function withDefaultArray(array $default): self; + public function getDefault(): ?string; + + public function asLazy(bool $lazy = true): self; + public function isLazy(): bool; + public function asSensitive(bool $sensitive = true): self; + public function isSensitive(): bool; + public function asDeprecated(bool $deprecated = true): self; + public function isDeprecated(): bool; +} diff --git a/tests/lib/AppConfigTest.php b/tests/lib/AppConfigTest.php index 86bd339bc7ef9..37616fc1657fe 100644 --- a/tests/lib/AppConfigTest.php +++ b/tests/lib/AppConfigTest.php @@ -25,6 +25,7 @@ use InvalidArgumentException; use OC\AppConfig; +use OC\AppFramework\Bootstrap\Coordinator; use OCP\Exceptions\AppConfigTypeConflictException; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; @@ -44,6 +45,8 @@ class AppConfigTest extends TestCase { protected IDBConnection $connection; private LoggerInterface $logger; private ICrypto $crypto; + private Coordinator $coordinator; + private array $originalConfig; /** @@ -104,6 +107,7 @@ protected function setUp(): void { $this->connection = \OCP\Server::get(IDBConnection::class); $this->logger = \OCP\Server::get(LoggerInterface::class); $this->crypto = \OCP\Server::get(ICrypto::class); + $this->coordinator = \OCP\Server::get(Coordinator::class); // storing current config and emptying the data table $sql = $this->connection->getQueryBuilder(); @@ -194,6 +198,7 @@ private function generateAppConfig(bool $preLoading = true): IAppConfig { $this->connection, $this->logger, $this->crypto, + $this->coordinator ); $msg = ' generateAppConfig() failed to confirm cache status';