1
0

DeclarativeManager.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Settings;
  8. use Exception;
  9. use OC\AppFramework\Bootstrap\Coordinator;
  10. use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
  11. use OCP\EventDispatcher\IEventDispatcher;
  12. use OCP\IAppConfig;
  13. use OCP\IConfig;
  14. use OCP\IGroupManager;
  15. use OCP\IUser;
  16. use OCP\Server;
  17. use OCP\Settings\DeclarativeSettingsTypes;
  18. use OCP\Settings\Events\DeclarativeSettingsGetValueEvent;
  19. use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent;
  20. use OCP\Settings\Events\DeclarativeSettingsSetValueEvent;
  21. use OCP\Settings\IDeclarativeManager;
  22. use OCP\Settings\IDeclarativeSettingsForm;
  23. use OCP\Settings\IDeclarativeSettingsFormWithHandlers;
  24. use Psr\Log\LoggerInterface;
  25. /**
  26. * @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm
  27. * @psalm-import-type DeclarativeSettingsStorageType from IDeclarativeSettingsForm
  28. * @psalm-import-type DeclarativeSettingsSectionType from IDeclarativeSettingsForm
  29. * @psalm-import-type DeclarativeSettingsFormSchemaWithValues from IDeclarativeSettingsForm
  30. * @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm
  31. */
  32. class DeclarativeManager implements IDeclarativeManager {
  33. /** @var array<string, list<IDeclarativeSettingsForm>> */
  34. private array $declarativeForms = [];
  35. /**
  36. * @var array<string, list<DeclarativeSettingsFormSchemaWithoutValues>>
  37. */
  38. private array $appSchemas = [];
  39. public function __construct(
  40. private IEventDispatcher $eventDispatcher,
  41. private IGroupManager $groupManager,
  42. private Coordinator $coordinator,
  43. private IConfig $config,
  44. private IAppConfig $appConfig,
  45. private LoggerInterface $logger,
  46. ) {
  47. }
  48. /**
  49. * @inheritdoc
  50. */
  51. public function registerSchema(string $app, array $schema): void {
  52. $this->appSchemas[$app] ??= [];
  53. if (!$this->validateSchema($app, $schema)) {
  54. throw new Exception('Invalid schema. Please check the logs for more details.');
  55. }
  56. foreach ($this->appSchemas[$app] as $otherSchema) {
  57. if ($otherSchema['id'] === $schema['id']) {
  58. throw new Exception('Duplicate form IDs detected: ' . $schema['id']);
  59. }
  60. }
  61. $fieldIDs = array_map(fn ($field) => $field['id'], $schema['fields']);
  62. $otherFieldIDs = array_merge(...array_map(fn ($schema) => array_map(fn ($field) => $field['id'], $schema['fields']), $this->appSchemas[$app]));
  63. $intersectionFieldIDs = array_intersect($fieldIDs, $otherFieldIDs);
  64. if (count($intersectionFieldIDs) > 0) {
  65. throw new Exception('Non unique field IDs detected: ' . join(', ', $intersectionFieldIDs));
  66. }
  67. $this->appSchemas[$app][] = $schema;
  68. }
  69. /**
  70. * @inheritdoc
  71. */
  72. public function loadSchemas(): void {
  73. if (empty($this->declarativeForms)) {
  74. $declarativeSettings = $this->coordinator->getRegistrationContext()->getDeclarativeSettings();
  75. foreach ($declarativeSettings as $declarativeSetting) {
  76. $app = $declarativeSetting->getAppId();
  77. /** @var IDeclarativeSettingsForm $declarativeForm */
  78. $declarativeForm = Server::get($declarativeSetting->getService());
  79. $this->registerSchema($app, $declarativeForm->getSchema());
  80. $this->declarativeForms[$app][] = $declarativeForm;
  81. }
  82. }
  83. $this->eventDispatcher->dispatchTyped(new DeclarativeSettingsRegisterFormEvent($this));
  84. }
  85. /**
  86. * @inheritdoc
  87. */
  88. public function getFormIDs(IUser $user, string $type, string $section): array {
  89. $isAdmin = $this->groupManager->isAdmin($user->getUID());
  90. /** @var array<string, list<string>> $formIds */
  91. $formIds = [];
  92. foreach ($this->appSchemas as $app => $schemas) {
  93. $ids = [];
  94. usort($schemas, [$this, 'sortSchemasByPriorityCallback']);
  95. foreach ($schemas as $schema) {
  96. if ($schema['section_type'] === DeclarativeSettingsTypes::SECTION_TYPE_ADMIN && !$isAdmin) {
  97. continue;
  98. }
  99. if ($schema['section_type'] === $type && $schema['section_id'] === $section) {
  100. $ids[] = $schema['id'];
  101. }
  102. }
  103. if (!empty($ids)) {
  104. $formIds[$app] = array_merge($formIds[$app] ?? [], $ids);
  105. }
  106. }
  107. return $formIds;
  108. }
  109. /**
  110. * @inheritdoc
  111. * @throws Exception
  112. */
  113. public function getFormsWithValues(IUser $user, ?string $type, ?string $section): array {
  114. $isAdmin = $this->groupManager->isAdmin($user->getUID());
  115. $forms = [];
  116. foreach ($this->appSchemas as $app => $schemas) {
  117. foreach ($schemas as $schema) {
  118. if ($type !== null && $schema['section_type'] !== $type) {
  119. continue;
  120. }
  121. if ($section !== null && $schema['section_id'] !== $section) {
  122. continue;
  123. }
  124. // If listing all fields skip the admin fields which a non-admin user has no access to
  125. if ($type === null && $schema['section_type'] === 'admin' && !$isAdmin) {
  126. continue;
  127. }
  128. $s = $schema;
  129. $s['app'] = $app;
  130. foreach ($s['fields'] as &$field) {
  131. $field['value'] = $this->getValue($user, $app, $schema['id'], $field['id']);
  132. }
  133. unset($field);
  134. /** @var DeclarativeSettingsFormSchemaWithValues $s */
  135. $forms[] = $s;
  136. }
  137. }
  138. usort($forms, [$this, 'sortSchemasByPriorityCallback']);
  139. return $forms;
  140. }
  141. private function sortSchemasByPriorityCallback(mixed $a, mixed $b): int {
  142. if ($a['priority'] === $b['priority']) {
  143. return 0;
  144. }
  145. return $a['priority'] > $b['priority'] ? -1 : 1;
  146. }
  147. /**
  148. * @return DeclarativeSettingsStorageType
  149. */
  150. private function getStorageType(string $app, string $fieldId): string {
  151. if (array_key_exists($app, $this->appSchemas)) {
  152. foreach ($this->appSchemas[$app] as $schema) {
  153. foreach ($schema['fields'] as $field) {
  154. if ($field['id'] == $fieldId) {
  155. if (array_key_exists('storage_type', $field)) {
  156. return $field['storage_type'];
  157. }
  158. }
  159. }
  160. if (array_key_exists('storage_type', $schema)) {
  161. return $schema['storage_type'];
  162. }
  163. }
  164. }
  165. return DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL;
  166. }
  167. /**
  168. * @return DeclarativeSettingsSectionType
  169. * @throws Exception
  170. */
  171. private function getSectionType(string $app, string $fieldId): string {
  172. if (array_key_exists($app, $this->appSchemas)) {
  173. foreach ($this->appSchemas[$app] as $schema) {
  174. foreach ($schema['fields'] as $field) {
  175. if ($field['id'] == $fieldId) {
  176. return $schema['section_type'];
  177. }
  178. }
  179. }
  180. }
  181. throw new Exception('Unknown fieldId "' . $fieldId . '"');
  182. }
  183. /**
  184. * @psalm-param DeclarativeSettingsSectionType $sectionType
  185. * @throws NotAdminException
  186. */
  187. private function assertAuthorized(IUser $user, string $sectionType): void {
  188. if ($sectionType === 'admin' && !$this->groupManager->isAdmin($user->getUID())) {
  189. throw new NotAdminException('Logged in user does not have permission to access these settings.');
  190. }
  191. }
  192. /**
  193. * @return DeclarativeSettingsValueTypes
  194. * @throws Exception
  195. * @throws NotAdminException
  196. */
  197. private function getValue(IUser $user, string $app, string $formId, string $fieldId): mixed {
  198. $sectionType = $this->getSectionType($app, $fieldId);
  199. $this->assertAuthorized($user, $sectionType);
  200. $storageType = $this->getStorageType($app, $fieldId);
  201. switch ($storageType) {
  202. case DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL:
  203. $form = $this->getForm($app, $formId);
  204. if ($form !== null && $form instanceof IDeclarativeSettingsFormWithHandlers) {
  205. return $form->getValue($fieldId, $user);
  206. }
  207. $event = new DeclarativeSettingsGetValueEvent($user, $app, $formId, $fieldId);
  208. $this->eventDispatcher->dispatchTyped($event);
  209. return $event->getValue();
  210. case DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL:
  211. return $this->getInternalValue($user, $app, $formId, $fieldId);
  212. default:
  213. throw new Exception('Unknown storage type "' . $storageType . '"');
  214. }
  215. }
  216. /**
  217. * @inheritdoc
  218. */
  219. public function setValue(IUser $user, string $app, string $formId, string $fieldId, mixed $value): void {
  220. $sectionType = $this->getSectionType($app, $fieldId);
  221. $this->assertAuthorized($user, $sectionType);
  222. $storageType = $this->getStorageType($app, $fieldId);
  223. switch ($storageType) {
  224. case DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL:
  225. $form = $this->getForm($app, $formId);
  226. if ($form !== null && $form instanceof IDeclarativeSettingsFormWithHandlers) {
  227. $form->setValue($fieldId, $value, $user);
  228. break;
  229. }
  230. // fall back to event handling
  231. $this->eventDispatcher->dispatchTyped(new DeclarativeSettingsSetValueEvent($user, $app, $formId, $fieldId, $value));
  232. break;
  233. case DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL:
  234. $this->saveInternalValue($user, $app, $fieldId, $value);
  235. break;
  236. default:
  237. throw new Exception('Unknown storage type "' . $storageType . '"');
  238. }
  239. }
  240. /**
  241. * If a declarative setting was registered as a form and not just a schema
  242. * then this will yield the registering form.
  243. */
  244. private function getForm(string $app, string $formId): ?IDeclarativeSettingsForm {
  245. $allForms = $this->declarativeForms[$app] ?? [];
  246. foreach ($allForms as $form) {
  247. if ($form->getSchema()['id'] === $formId) {
  248. return $form;
  249. }
  250. }
  251. return null;
  252. }
  253. private function getInternalValue(IUser $user, string $app, string $formId, string $fieldId): mixed {
  254. $sectionType = $this->getSectionType($app, $fieldId);
  255. $defaultValue = $this->getDefaultValue($app, $formId, $fieldId);
  256. switch ($sectionType) {
  257. case DeclarativeSettingsTypes::SECTION_TYPE_ADMIN:
  258. return $this->config->getAppValue($app, $fieldId, $defaultValue);
  259. case DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL:
  260. return $this->config->getUserValue($user->getUID(), $app, $fieldId, $defaultValue);
  261. default:
  262. throw new Exception('Unknown section type "' . $sectionType . '"');
  263. }
  264. }
  265. private function saveInternalValue(IUser $user, string $app, string $fieldId, mixed $value): void {
  266. $sectionType = $this->getSectionType($app, $fieldId);
  267. switch ($sectionType) {
  268. case DeclarativeSettingsTypes::SECTION_TYPE_ADMIN:
  269. $this->appConfig->setValueString($app, $fieldId, $value);
  270. break;
  271. case DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL:
  272. $this->config->setUserValue($user->getUID(), $app, $fieldId, $value);
  273. break;
  274. default:
  275. throw new Exception('Unknown section type "' . $sectionType . '"');
  276. }
  277. }
  278. private function getDefaultValue(string $app, string $formId, string $fieldId): mixed {
  279. foreach ($this->appSchemas[$app] as $schema) {
  280. if ($schema['id'] === $formId) {
  281. foreach ($schema['fields'] as $field) {
  282. if ($field['id'] === $fieldId) {
  283. if (isset($field['default'])) {
  284. if (is_array($field['default'])) {
  285. return json_encode($field['default']);
  286. }
  287. return $field['default'];
  288. }
  289. }
  290. }
  291. }
  292. }
  293. return null;
  294. }
  295. private function validateSchema(string $appId, array $schema): bool {
  296. if (!isset($schema['id'])) {
  297. $this->logger->warning('Attempt to register a declarative settings schema with no id', ['app' => $appId]);
  298. return false;
  299. }
  300. $formId = $schema['id'];
  301. if (!isset($schema['section_type'])) {
  302. $this->logger->warning('Declarative settings: missing section_type', ['app' => $appId, 'form_id' => $formId]);
  303. return false;
  304. }
  305. if (!in_array($schema['section_type'], [DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL])) {
  306. $this->logger->warning('Declarative settings: invalid section_type', ['app' => $appId, 'form_id' => $formId, 'section_type' => $schema['section_type']]);
  307. return false;
  308. }
  309. if (!isset($schema['section_id'])) {
  310. $this->logger->warning('Declarative settings: missing section_id', ['app' => $appId, 'form_id' => $formId]);
  311. return false;
  312. }
  313. if (!isset($schema['storage_type'])) {
  314. $this->logger->warning('Declarative settings: missing storage_type', ['app' => $appId, 'form_id' => $formId]);
  315. return false;
  316. }
  317. if (!in_array($schema['storage_type'], [DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL, DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL])) {
  318. $this->logger->warning('Declarative settings: invalid storage_type', ['app' => $appId, 'form_id' => $formId, 'storage_type' => $schema['storage_type']]);
  319. return false;
  320. }
  321. if (!isset($schema['title'])) {
  322. $this->logger->warning('Declarative settings: missing title', ['app' => $appId, 'form_id' => $formId]);
  323. return false;
  324. }
  325. if (!isset($schema['fields']) || !is_array($schema['fields'])) {
  326. $this->logger->warning('Declarative settings: missing or invalid fields', ['app' => $appId, 'form_id' => $formId]);
  327. return false;
  328. }
  329. foreach ($schema['fields'] as $field) {
  330. if (!isset($field['id'])) {
  331. $this->logger->warning('Declarative settings: missing field id', ['app' => $appId, 'form_id' => $formId, 'field' => $field]);
  332. return false;
  333. }
  334. $fieldId = $field['id'];
  335. if (!isset($field['title'])) {
  336. $this->logger->warning('Declarative settings: missing field title', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]);
  337. return false;
  338. }
  339. if (!isset($field['type'])) {
  340. $this->logger->warning('Declarative settings: missing field type', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]);
  341. return false;
  342. }
  343. if (!in_array($field['type'], [
  344. DeclarativeSettingsTypes::MULTI_SELECT, DeclarativeSettingsTypes::MULTI_CHECKBOX, DeclarativeSettingsTypes::RADIO,
  345. DeclarativeSettingsTypes::SELECT, DeclarativeSettingsTypes::CHECKBOX,
  346. DeclarativeSettingsTypes::URL, DeclarativeSettingsTypes::EMAIL, DeclarativeSettingsTypes::NUMBER,
  347. DeclarativeSettingsTypes::TEL, DeclarativeSettingsTypes::TEXT, DeclarativeSettingsTypes::PASSWORD,
  348. ])) {
  349. $this->logger->warning('Declarative settings: invalid field type', [
  350. 'app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId, 'type' => $field['type'],
  351. ]);
  352. return false;
  353. }
  354. if (!$this->validateField($appId, $formId, $field)) {
  355. return false;
  356. }
  357. }
  358. return true;
  359. }
  360. private function validateField(string $appId, string $formId, array $field): bool {
  361. $fieldId = $field['id'];
  362. if (in_array($field['type'], [
  363. DeclarativeSettingsTypes::MULTI_SELECT, DeclarativeSettingsTypes::MULTI_CHECKBOX, DeclarativeSettingsTypes::RADIO,
  364. DeclarativeSettingsTypes::SELECT
  365. ])) {
  366. if (!isset($field['options'])) {
  367. $this->logger->warning('Declarative settings: missing field options', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]);
  368. return false;
  369. }
  370. if (!is_array($field['options'])) {
  371. $this->logger->warning('Declarative settings: field options should be an array', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]);
  372. return false;
  373. }
  374. }
  375. return true;
  376. }
  377. }