diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 236fa77590706..be03e314e8f26 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -216,6 +216,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', @@ -1025,6 +1027,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 e6035fd13b242..3b512b29a4def 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -249,6 +249,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', @@ -1058,6 +1060,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 338792e23d179..3444c7ae94a35 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 OC\ConfigLexicon\ConfigLexiconEntry; +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, strict: bool}> ['app_id' => ['strict' => bool, 'entries' => ['config_key' => IConfigLexiconEntry[]]] */ + 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, ) { } @@ -457,6 +463,7 @@ private function getTypedValue( int $type ): string { $this->assertParams($app, $key, valueType: $type); + $this->compareRegisteredConfigValues($app, $key, $lazy, $type, $default); $this->loadConfig($lazy); /** @@ -748,6 +755,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); @@ -1567,4 +1575,73 @@ private function getSensitiveKeys(string $app): array { public function clearCachedConfig(): void { $this->clearCache(); } + + /** + * verify and compare current use of config values with defined lexicon + * + * @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 ' . $app . '/' . $key . ' is not defined in the config lexicon'); + } + return; + } + + /** @var ConfigLexiconEntry $configValue */ + $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 ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon'); + } + + $lazy = $configValue->isLazy(); + $default = $configValue->getDefault() ?? $default; + if ($configValue->isSensitive()) { + $type |= self::VALUE_SENSITIVE; + } + if ($configValue->isDeprecated()) { + $this->logger->notice('config value ' . $app . '/' . $key . ' is set as deprecated.'); + } + } + + /** + * extract details from registered $appId's config lexicon + * + * @param string $appId + * + * @return array{entries: array, strict: bool} + */ + 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..a2ed2f8770cf5 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,13 @@ public function registerDeclarativeSettings(string $appId, string $declarativeSe $this->declarativeSettings[] = new ServiceRegistration($appId, $declarativeSettingsClass); } + /** + * @psalm-param class-string $configLexiconClass + */ + public function registerConfigLexicon(string $appId, string $configLexiconClass): void { + $this->configLexiconClasses[$appId] = $configLexiconClass; + } + /** * @param App[] $apps */ @@ -920,4 +941,20 @@ 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; + } + + return \OCP\Server::get($this->configLexiconClasses[$appId]); + } } diff --git a/lib/private/ConfigLexicon/ConfigLexiconEntry.php b/lib/private/ConfigLexicon/ConfigLexiconEntry.php new file mode 100644 index 0000000000000..c92ba84a3a0cf --- /dev/null +++ b/lib/private/ConfigLexicon/ConfigLexiconEntry.php @@ -0,0 +1,198 @@ + + * + * @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; + +/** + * Model that represent config values within an app config lexicon. + * + * @see IConfigLexicon + * @since 30.0.0 + */ +class ConfigLexiconEntry implements IConfigLexiconEntry { + private string $definition = ''; + private ?string $default = null; + + /** + * @param string $key config key + * @param int $valueType type of config value ({@see self::TYPE_STRING} and others) + * @param string $definition optional description of config key available when using occ command + * @param bool $lazy set config value as lazy + * @param bool $sensitive set config value as sensitive + * @param bool $deprecated set config key as deprecated + */ + public function __construct( + private readonly string $key, + private readonly int $valueType, + string $definition = '', + private readonly bool $lazy = false, + private readonly bool $sensitive = false, + private readonly bool $deprecated = false + ) { + if (OC::$CLI) { // only store definition if ran from CLI + $this->definition = $definition; + } + } + + /** + * @inheritDoc + * + * @return string config key + * @since 30.0.0 + */ + public function getKey(): string { + return $this->key; + } + + /** + * @inheritDoc + * + * @return int + * @see self::TYPE_STRING and others + * @since 30.0.0 + */ + public function getValueType(): int { + return $this->valueType; + } + + /** + * @inheritDoc + * + * @param string $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultString(string $default): self { + $this->default = $default; + return $this; + } + + /** + * @inheritDoc + * + * @param int $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultInt(int $default): self { + $this->default = (string) $default; + return $this; + } + + /** + * @inheritDoc + * + * @param float $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultFloat(float $default): self { + $this->default = (string) $default; + return $this; + } + + /** + * @inheritDoc + * + * @param bool $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultBool(bool $default): self { + $this->default = ($default) ? '1' : '0'; + return $this; + } + + /** + * @inheritDoc + * + * @param array $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultArray(array $default): self { + $this->default = json_encode($default); + return $this; + } + + /** + * @inheritDoc + * + * @return string|null NULL if no default is set + * @since 30.0.0 + */ + public function getDefault(): ?string { + return $this->default; + } + + /** + * @inheritDoc + * + * @return string + * @since 30.0.0 + */ + public function getDefinition(): string { + return $this->definition; + } + + /** + * @inheritDoc + * + * @see IAppConfig for details on lazy config values + * @return bool TRUE if config value is lazy + * @since 30.0.0 + */ + public function isLazy(): bool { + return $this->lazy; + } + + /** + * @inheritDoc + * + * @see IAppConfig for details on sensitive config values + * @return bool TRUE if config value is sensitive + * @since 30.0.0 + */ + public function isSensitive(): bool { + return $this->sensitive; + } + + /** + * @inheritDoc + * + * @return bool TRUE if config si deprecated + * @since 30.0.0 + */ + 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..548a20fb14d7b 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -410,4 +410,15 @@ public function registerSetupCheck(string $setupCheckClass): void; * @since 29.0.0 */ public function registerDeclarativeSettings(string $declarativeSettingsClass): void; + + /** + * Register an implementation of \OCP\ConfigLexicon\IConfigLexicon that + * will handle the implementation of config lexicon + * + * @param string $configLexiconClass + * @psalm-param class-string<\OCP\ConfigLexicon\IConfigLexicon> $configLexiconClass + * @return void + * @since 30.0.0 + */ + 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..aa942047e185a --- /dev/null +++ b/lib/public/ConfigLexicon/IConfigLexicon.php @@ -0,0 +1,59 @@ + + * + * @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; + +/** + * This interface needs to be implemented if you want to define a config lexicon for your application + * The config lexicon is used to avoid conflicts and problems when storing/retrieving config values + * + * @since 30.0.0 + */ +interface IConfigLexicon { + + /** + * set your application config lexicon as strict or not. + * When set as strict, using a config key not set in the lexicon will throw an exception. + * + * @return bool + * @since 30.0.0 + */ + public function isStrict(): bool; + + /** + * define the list of entries of your application config lexicon, related to AppConfig. + * + * @return IConfigLexiconEntry[] + * @since 30.0.0 + */ + public function getAppConfigs(): array; + + /** + * define the list of entries of your application config lexicon, related to UserPreference. + * + * @return IConfigLexiconEntry[] + * @since 30.0.0 + */ + public function getUserPreferences(): array; +} diff --git a/lib/public/ConfigLexicon/IConfigLexiconEntry.php b/lib/public/ConfigLexicon/IConfigLexiconEntry.php new file mode 100644 index 0000000000000..b07756539d91f --- /dev/null +++ b/lib/public/ConfigLexicon/IConfigLexiconEntry.php @@ -0,0 +1,154 @@ + + * + * @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; + + +use OCP\IAppConfig; + +/** + * Model that represent config values within an app config lexicon. + * + * @see IConfigLexicon + * @since 30.0.0 + */ +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; + + /** + * returns the config key. + * + * @return string config key + * @since 30.0.0 + */ + public function getKey(): string; + + /** + * returns the type of the config value. + * + * @return int + * @see self::TYPE_STRING and others + * @since 30.0.0 + */ + public function getValueType(): int; + + /** + * set default value (as string) for config value. + * + * @param string $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultString(string $default): self; + + /** + * set default value (as int) for config value. + * + * @param int $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultInt(int $default): self; + + /** + * set default value (as float) for config value. + * + * @param float $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultFloat(float $default): self; + + /** + * set default value (as bool) for config value. + * + * @param bool $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultBool(bool $default): self; + + /** + * set default value (as array) for config value. + * + * @param array $default + * + * @return self + * @since 30.0.0 + */ + public function withDefaultArray(array $default): self; + + /** + * returns the default value set for this config key. + * default value is returned as string or NULL if not set. + * + * @return string|null NULL if no default is set + * @since 30.0.0 + */ + public function getDefault(): ?string; + + + /** + * returns the description for config key, only available when process is initiated from occ. + * returns empty string if not set or if process is not initiated from occ. + * + * @return string + * @since 30.0.0 + */ + public function getDefinition(): string; + + /** + * returns if config value is set as LAZY. + * + * @see IAppConfig for details on lazy config values + * @return bool TRUE if config value is lazy + * @since 30.0.0 + */ + public function isLazy(): bool; + + /** + * returns if config value is set as SENSITIVE. + * + * @see IAppConfig for details on sensitive config values + * @return bool TRUE if config value is sensitive + * @since 30.0.0 + */ + public function isSensitive(): bool; + + /** + * returns if config key is deprecated. + * + * @return bool TRUE if config si deprecated + * @since 30.0.0 + */ + public function isDeprecated(): bool; +} diff --git a/tests/lib/AppConfigTest.php b/tests/lib/AppConfigTest.php index d25a25c8e6637..d12771da5e38a 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';