DeclarativeManager.php 14 KB

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