Completed
Push — master ( 25e905...7a327b )
by
unknown
37:33
created
apps/settings/lib/Sections/Admin/Users.php 1 patch
Indentation   +20 added lines, -20 removed lines patch added patch discarded remove patch
@@ -13,24 +13,24 @@
 block discarded – undo
13 13
 use OCP\Settings\IIconSection;
14 14
 
15 15
 class Users implements IIconSection {
16
-	public function __construct(
17
-		private IL10N $l,
18
-	) {
19
-	}
20
-
21
-	public function getID(): string {
22
-		return 'usersdelegation';
23
-	}
24
-
25
-	public function getName(): string {
26
-		return $this->l->t('Users');
27
-	}
28
-
29
-	public function getPriority(): int {
30
-		return 55;
31
-	}
32
-
33
-	public function getIcon(): string {
34
-		return '';
35
-	}
16
+    public function __construct(
17
+        private IL10N $l,
18
+    ) {
19
+    }
20
+
21
+    public function getID(): string {
22
+        return 'usersdelegation';
23
+    }
24
+
25
+    public function getName(): string {
26
+        return $this->l->t('Users');
27
+    }
28
+
29
+    public function getPriority(): int {
30
+        return 55;
31
+    }
32
+
33
+    public function getIcon(): string {
34
+        return '';
35
+    }
36 36
 }
Please login to merge, or discard this patch.
apps/settings/lib/Settings/Admin/Delegation.php 1 patch
Indentation   +77 added lines, -77 removed lines patch added patch discarded remove patch
@@ -17,91 +17,91 @@
 block discarded – undo
17 17
 use OCP\Settings\ISettings;
18 18
 
19 19
 class Delegation implements ISettings {
20
-	public function __construct(
21
-		private IManager $settingManager,
22
-		private IInitialState $initialStateService,
23
-		private IGroupManager $groupManager,
24
-		private AuthorizedGroupService $authorizedGroupService,
25
-		private IURLGenerator $urlGenerator,
26
-	) {
27
-	}
20
+    public function __construct(
21
+        private IManager $settingManager,
22
+        private IInitialState $initialStateService,
23
+        private IGroupManager $groupManager,
24
+        private AuthorizedGroupService $authorizedGroupService,
25
+        private IURLGenerator $urlGenerator,
26
+    ) {
27
+    }
28 28
 
29
-	/**
30
-	 * Filter out the ISettings that are not IDelegatedSettings from $innerSection
31
-	 * and add them to $settings.
32
-	 *
33
-	 * @param IDelegatedSettings[] $settings
34
-	 * @param ISettings[] $innerSection
35
-	 * @return IDelegatedSettings[]
36
-	 */
37
-	private function getDelegatedSettings(array $settings, array $innerSection): array {
38
-		foreach ($innerSection as $setting) {
39
-			if ($setting instanceof IDelegatedSettings) {
40
-				$settings[] = $setting;
41
-			}
42
-		}
43
-		return $settings;
44
-	}
29
+    /**
30
+     * Filter out the ISettings that are not IDelegatedSettings from $innerSection
31
+     * and add them to $settings.
32
+     *
33
+     * @param IDelegatedSettings[] $settings
34
+     * @param ISettings[] $innerSection
35
+     * @return IDelegatedSettings[]
36
+     */
37
+    private function getDelegatedSettings(array $settings, array $innerSection): array {
38
+        foreach ($innerSection as $setting) {
39
+            if ($setting instanceof IDelegatedSettings) {
40
+                $settings[] = $setting;
41
+            }
42
+        }
43
+        return $settings;
44
+    }
45 45
 
46
-	private function initSettingState(): void {
47
-		// Available settings page initialization
48
-		$delegatedSettings = $this->settingManager->getAdminDelegatedSettings();
49
-		$settings = [];
50
-		foreach ($delegatedSettings as ['section' => $section, 'settings' => $sectionSettings]) {
51
-			foreach ($sectionSettings as $setting) {
52
-				$sectionName = $section->getName() . ($setting->getName() !== null ? ' - ' . $setting->getName() : '');
53
-				$settings[] = [
54
-					'class' => get_class($setting),
55
-					'sectionName' => $sectionName,
56
-					'id' => mb_strtolower(str_replace(' ', '-', $sectionName)),
57
-					'priority' => $section->getPriority(),
58
-				];
59
-			}
60
-		}
61
-		$this->initialStateService->provideInitialState('available-settings', $settings);
62
-	}
46
+    private function initSettingState(): void {
47
+        // Available settings page initialization
48
+        $delegatedSettings = $this->settingManager->getAdminDelegatedSettings();
49
+        $settings = [];
50
+        foreach ($delegatedSettings as ['section' => $section, 'settings' => $sectionSettings]) {
51
+            foreach ($sectionSettings as $setting) {
52
+                $sectionName = $section->getName() . ($setting->getName() !== null ? ' - ' . $setting->getName() : '');
53
+                $settings[] = [
54
+                    'class' => get_class($setting),
55
+                    'sectionName' => $sectionName,
56
+                    'id' => mb_strtolower(str_replace(' ', '-', $sectionName)),
57
+                    'priority' => $section->getPriority(),
58
+                ];
59
+            }
60
+        }
61
+        $this->initialStateService->provideInitialState('available-settings', $settings);
62
+    }
63 63
 
64
-	public function initAvailableGroupState(): void {
65
-		// Available groups initialization
66
-		$groups = [];
67
-		$groupsClass = $this->groupManager->search('');
68
-		foreach ($groupsClass as $group) {
69
-			if ($group->getGID() === 'admin') {
70
-				continue; // Admin already have access to everything
71
-			}
72
-			$groups[] = [
73
-				'displayName' => $group->getDisplayName(),
74
-				'gid' => $group->getGID(),
75
-			];
76
-		}
77
-		$this->initialStateService->provideInitialState('available-groups', $groups);
78
-	}
64
+    public function initAvailableGroupState(): void {
65
+        // Available groups initialization
66
+        $groups = [];
67
+        $groupsClass = $this->groupManager->search('');
68
+        foreach ($groupsClass as $group) {
69
+            if ($group->getGID() === 'admin') {
70
+                continue; // Admin already have access to everything
71
+            }
72
+            $groups[] = [
73
+                'displayName' => $group->getDisplayName(),
74
+                'gid' => $group->getGID(),
75
+            ];
76
+        }
77
+        $this->initialStateService->provideInitialState('available-groups', $groups);
78
+    }
79 79
 
80
-	public function initAuthorizedGroupState(): void {
81
-		// Already set authorized groups
82
-		$this->initialStateService->provideInitialState('authorized-groups', $this->authorizedGroupService->findAll());
83
-	}
80
+    public function initAuthorizedGroupState(): void {
81
+        // Already set authorized groups
82
+        $this->initialStateService->provideInitialState('authorized-groups', $this->authorizedGroupService->findAll());
83
+    }
84 84
 
85
-	public function getForm(): TemplateResponse {
86
-		$this->initSettingState();
87
-		$this->initAvailableGroupState();
88
-		$this->initAuthorizedGroupState();
89
-		$this->initialStateService->provideInitialState('authorized-settings-doc-link', $this->urlGenerator->linkToDocs('admin-delegation'));
85
+    public function getForm(): TemplateResponse {
86
+        $this->initSettingState();
87
+        $this->initAvailableGroupState();
88
+        $this->initAuthorizedGroupState();
89
+        $this->initialStateService->provideInitialState('authorized-settings-doc-link', $this->urlGenerator->linkToDocs('admin-delegation'));
90 90
 
91
-		return new TemplateResponse(Application::APP_ID, 'settings/admin/delegation', [], '');
92
-	}
91
+        return new TemplateResponse(Application::APP_ID, 'settings/admin/delegation', [], '');
92
+    }
93 93
 
94
-	/**
95
-	 * @return string the section ID, e.g. 'sharing'
96
-	 */
97
-	public function getSection() {
98
-		return 'admindelegation';
99
-	}
94
+    /**
95
+     * @return string the section ID, e.g. 'sharing'
96
+     */
97
+    public function getSection() {
98
+        return 'admindelegation';
99
+    }
100 100
 
101
-	/*
101
+    /*
102 102
 	 * @inheritdoc
103 103
 	 */
104
-	public function getPriority() {
105
-		return 75;
106
-	}
104
+    public function getPriority() {
105
+        return 75;
106
+    }
107 107
 }
Please login to merge, or discard this patch.
apps/settings/lib/Settings/Admin/Users.php 1 patch
Indentation   +20 added lines, -20 removed lines patch added patch discarded remove patch
@@ -16,24 +16,24 @@
 block discarded – undo
16 16
  * Empty settings class, used only for admin delegation.
17 17
  */
18 18
 class Users implements IDelegatedSettings {
19
-	public function getForm(): TemplateResponse {
20
-		throw new \Exception('Admin delegation settings should never be rendered');
21
-	}
22
-
23
-	public function getSection(): ?string {
24
-		return 'usersdelegation';
25
-	}
26
-
27
-	public function getPriority(): int {
28
-		return 0;
29
-	}
30
-
31
-	public function getName(): ?string {
32
-		/* Use section name alone */
33
-		return null;
34
-	}
35
-
36
-	public function getAuthorizedAppConfig(): array {
37
-		return [];
38
-	}
19
+    public function getForm(): TemplateResponse {
20
+        throw new \Exception('Admin delegation settings should never be rendered');
21
+    }
22
+
23
+    public function getSection(): ?string {
24
+        return 'usersdelegation';
25
+    }
26
+
27
+    public function getPriority(): int {
28
+        return 0;
29
+    }
30
+
31
+    public function getName(): ?string {
32
+        /* Use section name alone */
33
+        return null;
34
+    }
35
+
36
+    public function getAuthorizedAppConfig(): array {
37
+        return [];
38
+    }
39 39
 }
Please login to merge, or discard this patch.
apps/webhook_listeners/lib/Settings/Admin.php 1 patch
Indentation   +23 added lines, -23 removed lines patch added patch discarded remove patch
@@ -17,27 +17,27 @@
 block discarded – undo
17 17
  * Empty settings class, used only for admin delegation for now as there is no UI
18 18
  */
19 19
 class Admin implements IDelegatedSettings {
20
-	/**
21
-	 * Empty template response
22
-	 */
23
-	public function getForm(): TemplateResponse {
24
-		throw new \Exception('Admin delegation settings should never be rendered');
25
-	}
26
-
27
-	public function getSection(): string {
28
-		return Application::APP_ID . '-admin';
29
-	}
30
-
31
-	public function getPriority(): int {
32
-		return 0;
33
-	}
34
-
35
-	public function getName(): ?string {
36
-		/* Use section name alone */
37
-		return null;
38
-	}
39
-
40
-	public function getAuthorizedAppConfig(): array {
41
-		return [];
42
-	}
20
+    /**
21
+     * Empty template response
22
+     */
23
+    public function getForm(): TemplateResponse {
24
+        throw new \Exception('Admin delegation settings should never be rendered');
25
+    }
26
+
27
+    public function getSection(): string {
28
+        return Application::APP_ID . '-admin';
29
+    }
30
+
31
+    public function getPriority(): int {
32
+        return 0;
33
+    }
34
+
35
+    public function getName(): ?string {
36
+        /* Use section name alone */
37
+        return null;
38
+    }
39
+
40
+    public function getAuthorizedAppConfig(): array {
41
+        return [];
42
+    }
43 43
 }
Please login to merge, or discard this patch.
apps/webhook_listeners/lib/Settings/AdminSection.php 1 patch
Indentation   +20 added lines, -20 removed lines patch added patch discarded remove patch
@@ -14,24 +14,24 @@
 block discarded – undo
14 14
 use OCP\Settings\IIconSection;
15 15
 
16 16
 class AdminSection implements IIconSection {
17
-	public function __construct(
18
-		private IL10N $l,
19
-	) {
20
-	}
21
-
22
-	public function getID(): string {
23
-		return Application::APP_ID . '-admin';
24
-	}
25
-
26
-	public function getName(): string {
27
-		return $this->l->t('Webhooks');
28
-	}
29
-
30
-	public function getPriority(): int {
31
-		return 56;
32
-	}
33
-
34
-	public function getIcon(): string {
35
-		return '';
36
-	}
17
+    public function __construct(
18
+        private IL10N $l,
19
+    ) {
20
+    }
21
+
22
+    public function getID(): string {
23
+        return Application::APP_ID . '-admin';
24
+    }
25
+
26
+    public function getName(): string {
27
+        return $this->l->t('Webhooks');
28
+    }
29
+
30
+    public function getPriority(): int {
31
+        return 56;
32
+    }
33
+
34
+    public function getIcon(): string {
35
+        return '';
36
+    }
37 37
 }
Please login to merge, or discard this patch.
lib/private/App/AppManager.php 1 patch
Indentation   +1086 added lines, -1086 removed lines patch added patch discarded remove patch
@@ -38,1090 +38,1090 @@
 block discarded – undo
38 38
 use Psr\Log\LoggerInterface;
39 39
 
40 40
 class AppManager implements IAppManager {
41
-	/**
42
-	 * Apps with these types can not be enabled for certain groups only
43
-	 * @var string[]
44
-	 */
45
-	protected $protectedAppTypes = [
46
-		'filesystem',
47
-		'prelogin',
48
-		'authentication',
49
-		'logging',
50
-		'prevent_group_restriction',
51
-	];
52
-
53
-	/** @var string[] $appId => $enabled */
54
-	private array $enabledAppsCache = [];
55
-
56
-	/** @var string[]|null */
57
-	private ?array $shippedApps = null;
58
-
59
-	private array $alwaysEnabled = [];
60
-	private array $defaultEnabled = [];
61
-
62
-	/** @var array */
63
-	private array $appInfos = [];
64
-
65
-	/** @var array */
66
-	private array $appVersions = [];
67
-
68
-	/** @var array */
69
-	private array $autoDisabledApps = [];
70
-	private array $appTypes = [];
71
-
72
-	/** @var array<string, true> */
73
-	private array $loadedApps = [];
74
-
75
-	private ?AppConfig $appConfig = null;
76
-	private ?IURLGenerator $urlGenerator = null;
77
-	private ?INavigationManager $navigationManager = null;
78
-
79
-	/**
80
-	 * Be extremely careful when injecting classes here. The AppManager is used by the installer,
81
-	 * so it needs to work before installation. See how AppConfig and IURLGenerator are injected for reference
82
-	 */
83
-	public function __construct(
84
-		private IUserSession $userSession,
85
-		private IConfig $config,
86
-		private IGroupManager $groupManager,
87
-		private ICacheFactory $memCacheFactory,
88
-		private IEventDispatcher $dispatcher,
89
-		private LoggerInterface $logger,
90
-		private ServerVersion $serverVersion,
91
-		private ConfigManager $configManager,
92
-		private DependencyAnalyzer $dependencyAnalyzer,
93
-	) {
94
-	}
95
-
96
-	private function getNavigationManager(): INavigationManager {
97
-		if ($this->navigationManager === null) {
98
-			$this->navigationManager = Server::get(INavigationManager::class);
99
-		}
100
-		return $this->navigationManager;
101
-	}
102
-
103
-	public function getAppIcon(string $appId, bool $dark = false): ?string {
104
-		$possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
105
-		$icon = null;
106
-		foreach ($possibleIcons as $iconName) {
107
-			try {
108
-				$icon = $this->getUrlGenerator()->imagePath($appId, $iconName);
109
-				break;
110
-			} catch (\RuntimeException $e) {
111
-				// ignore
112
-			}
113
-		}
114
-		return $icon;
115
-	}
116
-
117
-	private function getAppConfig(): AppConfig {
118
-		if ($this->appConfig !== null) {
119
-			return $this->appConfig;
120
-		}
121
-		if (!$this->config->getSystemValueBool('installed', false)) {
122
-			throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
123
-		}
124
-		$this->appConfig = Server::get(AppConfig::class);
125
-		return $this->appConfig;
126
-	}
127
-
128
-	private function getUrlGenerator(): IURLGenerator {
129
-		if ($this->urlGenerator !== null) {
130
-			return $this->urlGenerator;
131
-		}
132
-		if (!$this->config->getSystemValueBool('installed', false)) {
133
-			throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
134
-		}
135
-		$this->urlGenerator = Server::get(IURLGenerator::class);
136
-		return $this->urlGenerator;
137
-	}
138
-
139
-	/**
140
-	 * For all enabled apps, return the value of their 'enabled' config key.
141
-	 *
142
-	 * @return array<string,string> appId => enabled (may be 'yes', or a json encoded list of group ids)
143
-	 */
144
-	private function getEnabledAppsValues(): array {
145
-		if (!$this->enabledAppsCache) {
146
-			/** @var array<string,string> */
147
-			$values = $this->getAppConfig()->searchValues('enabled', false, IAppConfig::VALUE_STRING);
148
-
149
-			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
150
-			foreach ($alwaysEnabledApps as $appId) {
151
-				$values[$appId] = 'yes';
152
-			}
153
-
154
-			$this->enabledAppsCache = array_filter($values, function ($value) {
155
-				return $value !== 'no';
156
-			});
157
-			ksort($this->enabledAppsCache);
158
-		}
159
-		return $this->enabledAppsCache;
160
-	}
161
-
162
-	/**
163
-	 * Deprecated alias
164
-	 *
165
-	 * @return string[]
166
-	 */
167
-	public function getInstalledApps() {
168
-		return $this->getEnabledApps();
169
-	}
170
-
171
-	/**
172
-	 * List all enabled apps, either for everyone or for some groups
173
-	 *
174
-	 * @return list<string>
175
-	 */
176
-	public function getEnabledApps(): array {
177
-		return array_keys($this->getEnabledAppsValues());
178
-	}
179
-
180
-	/**
181
-	 * Get a list of all apps in the apps folder
182
-	 *
183
-	 * @return list<string> an array of app names (string IDs)
184
-	 */
185
-	public function getAllAppsInAppsFolders(): array {
186
-		$apps = [];
187
-
188
-		foreach (\OC::$APPSROOTS as $apps_dir) {
189
-			if (!is_readable($apps_dir['path'])) {
190
-				$this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']);
191
-				continue;
192
-			}
193
-			$dh = opendir($apps_dir['path']);
194
-
195
-			if (is_resource($dh)) {
196
-				while (($file = readdir($dh)) !== false) {
197
-					if (
198
-						$file[0] != '.'
199
-						&& is_dir($apps_dir['path'] . '/' . $file)
200
-						&& is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')
201
-					) {
202
-						$apps[] = $file;
203
-					}
204
-				}
205
-			}
206
-		}
207
-
208
-		return array_values(array_unique($apps));
209
-	}
210
-
211
-	/**
212
-	 * List all apps enabled for a user
213
-	 *
214
-	 * @param \OCP\IUser $user
215
-	 * @return list<string>
216
-	 */
217
-	public function getEnabledAppsForUser(IUser $user) {
218
-		$apps = $this->getEnabledAppsValues();
219
-		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
220
-			return $this->checkAppForUser($enabled, $user);
221
-		});
222
-		return array_keys($appsForUser);
223
-	}
224
-
225
-	public function getEnabledAppsForGroup(IGroup $group): array {
226
-		$apps = $this->getEnabledAppsValues();
227
-		$appsForGroups = array_filter($apps, function ($enabled) use ($group) {
228
-			return $this->checkAppForGroups($enabled, $group);
229
-		});
230
-		return array_keys($appsForGroups);
231
-	}
232
-
233
-	/**
234
-	 * Loads all apps
235
-	 *
236
-	 * @param string[] $types
237
-	 * @return bool
238
-	 *
239
-	 * This function walks through the Nextcloud directory and loads all apps
240
-	 * it can find. A directory contains an app if the file /appinfo/info.xml
241
-	 * exists.
242
-	 *
243
-	 * if $types is set to non-empty array, only apps of those types will be loaded
244
-	 */
245
-	public function loadApps(array $types = []): bool {
246
-		if ($this->config->getSystemValueBool('maintenance', false)) {
247
-			return false;
248
-		}
249
-		// Load the enabled apps here
250
-		$apps = \OC_App::getEnabledApps();
251
-
252
-		// Add each apps' folder as allowed class path
253
-		foreach ($apps as $app) {
254
-			// If the app is already loaded then autoloading it makes no sense
255
-			if (!$this->isAppLoaded($app)) {
256
-				try {
257
-					$path = $this->getAppPath($app);
258
-					\OC_App::registerAutoloading($app, $path);
259
-				} catch (AppPathNotFoundException $e) {
260
-					$this->logger->info('Error during app loading: ' . $e->getMessage(), [
261
-						'exception' => $e,
262
-						'app' => $app,
263
-					]);
264
-				}
265
-			}
266
-		}
267
-
268
-		// prevent app loading from printing output
269
-		ob_start();
270
-		foreach ($apps as $app) {
271
-			if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
272
-				try {
273
-					$this->loadApp($app);
274
-				} catch (\Throwable $e) {
275
-					$this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
276
-						'exception' => $e,
277
-						'app' => $app,
278
-					]);
279
-				}
280
-			}
281
-		}
282
-		ob_end_clean();
283
-
284
-		return true;
285
-	}
286
-
287
-	/**
288
-	 * check if an app is of a specific type
289
-	 *
290
-	 * @param string $app
291
-	 * @param array $types
292
-	 * @return bool
293
-	 */
294
-	public function isType(string $app, array $types): bool {
295
-		$appTypes = $this->getAppTypes($app);
296
-		foreach ($types as $type) {
297
-			if (in_array($type, $appTypes, true)) {
298
-				return true;
299
-			}
300
-		}
301
-		return false;
302
-	}
303
-
304
-	/**
305
-	 * get the types of an app
306
-	 *
307
-	 * @param string $app
308
-	 * @return string[]
309
-	 */
310
-	private function getAppTypes(string $app): array {
311
-		//load the cache
312
-		if (count($this->appTypes) === 0) {
313
-			$this->appTypes = $this->getAppConfig()->getValues(false, 'types') ?: [];
314
-		}
315
-
316
-		if (isset($this->appTypes[$app])) {
317
-			return explode(',', $this->appTypes[$app]);
318
-		}
319
-
320
-		return [];
321
-	}
322
-
323
-	/**
324
-	 * @return array
325
-	 */
326
-	public function getAutoDisabledApps(): array {
327
-		return $this->autoDisabledApps;
328
-	}
329
-
330
-	public function getAppRestriction(string $appId): array {
331
-		$values = $this->getEnabledAppsValues();
332
-
333
-		if (!isset($values[$appId])) {
334
-			return [];
335
-		}
336
-
337
-		if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
338
-			return [];
339
-		}
340
-		return json_decode($values[$appId], true);
341
-	}
342
-
343
-	/**
344
-	 * Check if an app is enabled for user
345
-	 *
346
-	 * @param string $appId
347
-	 * @param \OCP\IUser|null $user (optional) if not defined, the currently logged in user will be used
348
-	 * @return bool
349
-	 */
350
-	public function isEnabledForUser($appId, $user = null) {
351
-		if ($this->isAlwaysEnabled($appId)) {
352
-			return true;
353
-		}
354
-		if ($user === null) {
355
-			$user = $this->userSession->getUser();
356
-		}
357
-		$enabledAppsValues = $this->getEnabledAppsValues();
358
-		if (isset($enabledAppsValues[$appId])) {
359
-			return $this->checkAppForUser($enabledAppsValues[$appId], $user);
360
-		} else {
361
-			return false;
362
-		}
363
-	}
364
-
365
-	private function checkAppForUser(string $enabled, ?IUser $user): bool {
366
-		if ($enabled === 'yes') {
367
-			return true;
368
-		} elseif ($user === null) {
369
-			return false;
370
-		} else {
371
-			if (empty($enabled)) {
372
-				return false;
373
-			}
374
-
375
-			$groupIds = json_decode($enabled);
376
-
377
-			if (!is_array($groupIds)) {
378
-				$jsonError = json_last_error();
379
-				$jsonErrorMsg = json_last_error_msg();
380
-				// this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason)
381
-				$this->logger->warning('AppManager::checkAppForUser - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg);
382
-				return false;
383
-			}
384
-
385
-			$userGroups = $this->groupManager->getUserGroupIds($user);
386
-			foreach ($userGroups as $groupId) {
387
-				if (in_array($groupId, $groupIds, true)) {
388
-					return true;
389
-				}
390
-			}
391
-			return false;
392
-		}
393
-	}
394
-
395
-	private function checkAppForGroups(string $enabled, IGroup $group): bool {
396
-		if ($enabled === 'yes') {
397
-			return true;
398
-		} else {
399
-			if (empty($enabled)) {
400
-				return false;
401
-			}
402
-
403
-			$groupIds = json_decode($enabled);
404
-
405
-			if (!is_array($groupIds)) {
406
-				$jsonError = json_last_error();
407
-				$jsonErrorMsg = json_last_error_msg();
408
-				// this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason)
409
-				$this->logger->warning('AppManager::checkAppForGroups - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg);
410
-				return false;
411
-			}
412
-
413
-			return in_array($group->getGID(), $groupIds);
414
-		}
415
-	}
416
-
417
-	/**
418
-	 * Check if an app is enabled in the instance
419
-	 *
420
-	 * Notice: This actually checks if the app is enabled and not only if it is installed.
421
-	 *
422
-	 * @param string $appId
423
-	 */
424
-	public function isInstalled($appId): bool {
425
-		return $this->isEnabledForAnyone($appId);
426
-	}
427
-
428
-	public function isEnabledForAnyone(string $appId): bool {
429
-		$enabledAppsValues = $this->getEnabledAppsValues();
430
-		return isset($enabledAppsValues[$appId]);
431
-	}
432
-
433
-	/**
434
-	 * Overwrite the `max-version` requirement for this app.
435
-	 */
436
-	public function overwriteNextcloudRequirement(string $appId): void {
437
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
438
-		if (!in_array($appId, $ignoreMaxApps, true)) {
439
-			$ignoreMaxApps[] = $appId;
440
-		}
441
-		$this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
442
-	}
443
-
444
-	/**
445
-	 * Remove the `max-version` overwrite for this app.
446
-	 * This means this app now again can not be enabled if the `max-version` is smaller than the current Nextcloud version.
447
-	 */
448
-	public function removeOverwriteNextcloudRequirement(string $appId): void {
449
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
450
-		$ignoreMaxApps = array_filter($ignoreMaxApps, fn (string $id) => $id !== $appId);
451
-		$this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
452
-	}
453
-
454
-	public function loadApp(string $app): void {
455
-		if (isset($this->loadedApps[$app])) {
456
-			return;
457
-		}
458
-		$this->loadedApps[$app] = true;
459
-		try {
460
-			$appPath = $this->getAppPath($app);
461
-		} catch (AppPathNotFoundException $e) {
462
-			$this->logger->info('Error during app loading: ' . $e->getMessage(), [
463
-				'exception' => $e,
464
-				'app' => $app,
465
-			]);
466
-			return;
467
-		}
468
-		$eventLogger = \OC::$server->get(IEventLogger::class);
469
-		$eventLogger->start("bootstrap:load_app:$app", "Load app: $app");
470
-
471
-		// in case someone calls loadApp() directly
472
-		\OC_App::registerAutoloading($app, $appPath);
473
-
474
-		if (is_file($appPath . '/appinfo/app.php')) {
475
-			$this->logger->error('/appinfo/app.php is not supported anymore, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
476
-				'app' => $app,
477
-			]);
478
-		}
479
-
480
-		$coordinator = Server::get(Coordinator::class);
481
-		$coordinator->bootApp($app);
482
-
483
-		$eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
484
-		$info = $this->getAppInfo($app);
485
-		if (!empty($info['activity'])) {
486
-			$activityManager = \OC::$server->get(IActivityManager::class);
487
-			if (!empty($info['activity']['filters'])) {
488
-				foreach ($info['activity']['filters'] as $filter) {
489
-					$activityManager->registerFilter($filter);
490
-				}
491
-			}
492
-			if (!empty($info['activity']['settings'])) {
493
-				foreach ($info['activity']['settings'] as $setting) {
494
-					$activityManager->registerSetting($setting);
495
-				}
496
-			}
497
-			if (!empty($info['activity']['providers'])) {
498
-				foreach ($info['activity']['providers'] as $provider) {
499
-					$activityManager->registerProvider($provider);
500
-				}
501
-			}
502
-		}
503
-
504
-		if (!empty($info['settings'])) {
505
-			$settingsManager = \OCP\Server::get(ISettingsManager::class);
506
-			if (!empty($info['settings']['admin'])) {
507
-				foreach ($info['settings']['admin'] as $setting) {
508
-					$settingsManager->registerSetting('admin', $setting);
509
-				}
510
-			}
511
-			if (!empty($info['settings']['admin-section'])) {
512
-				foreach ($info['settings']['admin-section'] as $section) {
513
-					$settingsManager->registerSection('admin', $section);
514
-				}
515
-			}
516
-			if (!empty($info['settings']['personal'])) {
517
-				foreach ($info['settings']['personal'] as $setting) {
518
-					$settingsManager->registerSetting('personal', $setting);
519
-				}
520
-			}
521
-			if (!empty($info['settings']['personal-section'])) {
522
-				foreach ($info['settings']['personal-section'] as $section) {
523
-					$settingsManager->registerSection('personal', $section);
524
-				}
525
-			}
526
-			if (!empty($info['settings']['admin-delegation'])) {
527
-				foreach ($info['settings']['admin-delegation'] as $setting) {
528
-					$settingsManager->registerSetting(ISettingsManager::SETTINGS_DELEGATION, $setting);
529
-				}
530
-			}
531
-			if (!empty($info['settings']['admin-delegation-section'])) {
532
-				foreach ($info['settings']['admin-delegation-section'] as $section) {
533
-					$settingsManager->registerSection(ISettingsManager::SETTINGS_DELEGATION, $section);
534
-				}
535
-			}
536
-		}
537
-
538
-		if (!empty($info['collaboration']['plugins'])) {
539
-			// deal with one or many plugin entries
540
-			$plugins = isset($info['collaboration']['plugins']['plugin']['@value'])
541
-				? [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
542
-			$collaboratorSearch = null;
543
-			$autoCompleteManager = null;
544
-			foreach ($plugins as $plugin) {
545
-				if ($plugin['@attributes']['type'] === 'collaborator-search') {
546
-					$pluginInfo = [
547
-						'shareType' => $plugin['@attributes']['share-type'],
548
-						'class' => $plugin['@value'],
549
-					];
550
-					$collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
551
-					$collaboratorSearch->registerPlugin($pluginInfo);
552
-				} elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
553
-					$autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
554
-					$autoCompleteManager->registerSorter($plugin['@value']);
555
-				}
556
-			}
557
-		}
558
-		$eventLogger->end("bootstrap:load_app:$app:info");
559
-
560
-		$eventLogger->end("bootstrap:load_app:$app");
561
-	}
562
-
563
-	/**
564
-	 * Check if an app is loaded
565
-	 * @param string $app app id
566
-	 * @since 26.0.0
567
-	 */
568
-	public function isAppLoaded(string $app): bool {
569
-		return isset($this->loadedApps[$app]);
570
-	}
571
-
572
-	/**
573
-	 * Enable an app for every user
574
-	 *
575
-	 * @param string $appId
576
-	 * @param bool $forceEnable
577
-	 * @throws AppPathNotFoundException
578
-	 * @throws \InvalidArgumentException if the application is not installed yet
579
-	 */
580
-	public function enableApp(string $appId, bool $forceEnable = false): void {
581
-		// Check if app exists
582
-		$this->getAppPath($appId);
583
-
584
-		if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
585
-			throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
586
-		}
587
-
588
-		if ($forceEnable) {
589
-			$this->overwriteNextcloudRequirement($appId);
590
-		}
591
-
592
-		$this->enabledAppsCache[$appId] = 'yes';
593
-		$this->getAppConfig()->setValue($appId, 'enabled', 'yes');
594
-		$this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
595
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
596
-			ManagerEvent::EVENT_APP_ENABLE, $appId
597
-		));
598
-		$this->clearAppsCache();
599
-
600
-		$this->configManager->migrateConfigLexiconKeys($appId);
601
-	}
602
-
603
-	/**
604
-	 * Whether a list of types contains a protected app type
605
-	 *
606
-	 * @param string[] $types
607
-	 * @return bool
608
-	 */
609
-	public function hasProtectedAppType($types) {
610
-		if (empty($types)) {
611
-			return false;
612
-		}
613
-
614
-		$protectedTypes = array_intersect($this->protectedAppTypes, $types);
615
-		return !empty($protectedTypes);
616
-	}
617
-
618
-	/**
619
-	 * Enable an app only for specific groups
620
-	 *
621
-	 * @param string $appId
622
-	 * @param IGroup[] $groups
623
-	 * @param bool $forceEnable
624
-	 * @throws \InvalidArgumentException if app can't be enabled for groups
625
-	 * @throws AppPathNotFoundException
626
-	 */
627
-	public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
628
-		// Check if app exists
629
-		$this->getAppPath($appId);
630
-
631
-		$info = $this->getAppInfo($appId);
632
-		if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
633
-			throw new \InvalidArgumentException("$appId can't be enabled for groups.");
634
-		}
635
-
636
-		if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
637
-			throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
638
-		}
639
-
640
-		if ($forceEnable) {
641
-			$this->overwriteNextcloudRequirement($appId);
642
-		}
643
-
644
-		/** @var string[] $groupIds */
645
-		$groupIds = array_map(function ($group) {
646
-			/** @var IGroup $group */
647
-			return ($group instanceof IGroup)
648
-				? $group->getGID()
649
-				: $group;
650
-		}, $groups);
651
-
652
-		$this->enabledAppsCache[$appId] = json_encode($groupIds);
653
-		$this->getAppConfig()->setValue($appId, 'enabled', json_encode($groupIds));
654
-		$this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
655
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
656
-			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
657
-		));
658
-		$this->clearAppsCache();
659
-
660
-		$this->configManager->migrateConfigLexiconKeys($appId);
661
-	}
662
-
663
-	/**
664
-	 * Disable an app for every user
665
-	 *
666
-	 * @param string $appId
667
-	 * @param bool $automaticDisabled
668
-	 * @throws \Exception if app can't be disabled
669
-	 */
670
-	public function disableApp($appId, $automaticDisabled = false): void {
671
-		if ($this->isAlwaysEnabled($appId)) {
672
-			throw new \Exception("$appId can't be disabled.");
673
-		}
674
-
675
-		if ($automaticDisabled) {
676
-			$previousSetting = $this->getAppConfig()->getValue($appId, 'enabled', 'yes');
677
-			if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
678
-				$previousSetting = json_decode($previousSetting, true);
679
-			}
680
-			$this->autoDisabledApps[$appId] = $previousSetting;
681
-		}
682
-
683
-		unset($this->enabledAppsCache[$appId]);
684
-		$this->getAppConfig()->setValue($appId, 'enabled', 'no');
685
-
686
-		// run uninstall steps
687
-		$appData = $this->getAppInfo($appId);
688
-		if (!is_null($appData)) {
689
-			\OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
690
-		}
691
-
692
-		$this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
693
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
694
-			ManagerEvent::EVENT_APP_DISABLE, $appId
695
-		));
696
-		$this->clearAppsCache();
697
-	}
698
-
699
-	/**
700
-	 * Get the directory for the given app.
701
-	 *
702
-	 * @psalm-taint-specialize
703
-	 *
704
-	 * @throws AppPathNotFoundException if app folder can't be found
705
-	 */
706
-	public function getAppPath(string $appId, bool $ignoreCache = false): string {
707
-		$appId = $this->cleanAppId($appId);
708
-		if ($appId === '') {
709
-			throw new AppPathNotFoundException('App id is empty');
710
-		} elseif ($appId === 'core') {
711
-			return __DIR__ . '/../../../core';
712
-		}
713
-
714
-		if (($dir = $this->findAppInDirectories($appId, $ignoreCache)) != false) {
715
-			return $dir['path'] . '/' . $appId;
716
-		}
717
-		throw new AppPathNotFoundException('Could not find path for ' . $appId);
718
-	}
719
-
720
-	/**
721
-	 * Get the web path for the given app.
722
-	 *
723
-	 * @throws AppPathNotFoundException if app path can't be found
724
-	 */
725
-	public function getAppWebPath(string $appId): string {
726
-		if (($dir = $this->findAppInDirectories($appId)) != false) {
727
-			return \OC::$WEBROOT . $dir['url'] . '/' . $appId;
728
-		}
729
-		throw new AppPathNotFoundException('Could not find web path for ' . $appId);
730
-	}
731
-
732
-	/**
733
-	 * Find the apps root for an app id.
734
-	 *
735
-	 * If multiple copies are found, the apps root the latest version is returned.
736
-	 *
737
-	 * @param bool $ignoreCache ignore cache and rebuild it
738
-	 * @return false|array{path: string, url: string} the apps root shape
739
-	 */
740
-	public function findAppInDirectories(string $appId, bool $ignoreCache = false) {
741
-		$sanitizedAppId = $this->cleanAppId($appId);
742
-		if ($sanitizedAppId !== $appId) {
743
-			return false;
744
-		}
745
-		// FIXME replace by a property or a cache
746
-		static $app_dir = [];
747
-
748
-		if (isset($app_dir[$appId]) && !$ignoreCache) {
749
-			return $app_dir[$appId];
750
-		}
751
-
752
-		$possibleApps = [];
753
-		foreach (\OC::$APPSROOTS as $dir) {
754
-			if (file_exists($dir['path'] . '/' . $appId)) {
755
-				$possibleApps[] = $dir;
756
-			}
757
-		}
758
-
759
-		if (empty($possibleApps)) {
760
-			return false;
761
-		} elseif (count($possibleApps) === 1) {
762
-			$dir = array_shift($possibleApps);
763
-			$app_dir[$appId] = $dir;
764
-			return $dir;
765
-		} else {
766
-			$versionToLoad = [];
767
-			foreach ($possibleApps as $possibleApp) {
768
-				$appData = $this->getAppInfoByPath($possibleApp['path'] . '/' . $appId . '/appinfo/info.xml');
769
-				$version = $appData['version'] ?? '';
770
-				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
771
-					$versionToLoad = [
772
-						'dir' => $possibleApp,
773
-						'version' => $version,
774
-					];
775
-				}
776
-			}
777
-			if (!isset($versionToLoad['dir'])) {
778
-				return false;
779
-			}
780
-			$app_dir[$appId] = $versionToLoad['dir'];
781
-			return $versionToLoad['dir'];
782
-		}
783
-	}
784
-
785
-	/**
786
-	 * Clear the cached list of apps when enabling/disabling an app
787
-	 */
788
-	public function clearAppsCache(): void {
789
-		$this->appInfos = [];
790
-	}
791
-
792
-	/**
793
-	 * Returns a list of apps that need upgrade
794
-	 *
795
-	 * @param string $version Nextcloud version as array of version components
796
-	 * @return array list of app info from apps that need an upgrade
797
-	 *
798
-	 * @internal
799
-	 */
800
-	public function getAppsNeedingUpgrade($version) {
801
-		$appsToUpgrade = [];
802
-		$apps = $this->getEnabledApps();
803
-		foreach ($apps as $appId) {
804
-			$appInfo = $this->getAppInfo($appId);
805
-			$appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version');
806
-			if ($appDbVersion
807
-				&& isset($appInfo['version'])
808
-				&& version_compare($appInfo['version'], $appDbVersion, '>')
809
-				&& $this->isAppCompatible($version, $appInfo)
810
-			) {
811
-				$appsToUpgrade[] = $appInfo;
812
-			}
813
-		}
814
-
815
-		return $appsToUpgrade;
816
-	}
817
-
818
-	/**
819
-	 * Returns the app information from "appinfo/info.xml".
820
-	 *
821
-	 * @param string|null $lang
822
-	 * @return array|null app info
823
-	 */
824
-	public function getAppInfo(string $appId, bool $path = false, $lang = null) {
825
-		if ($path) {
826
-			throw new \InvalidArgumentException('Calling IAppManager::getAppInfo() with a path is no longer supported. Please call IAppManager::getAppInfoByPath() instead and verify that the path is good before calling.');
827
-		}
828
-		if ($lang === null && isset($this->appInfos[$appId])) {
829
-			return $this->appInfos[$appId];
830
-		}
831
-		try {
832
-			$appPath = $this->getAppPath($appId);
833
-		} catch (AppPathNotFoundException) {
834
-			return null;
835
-		}
836
-		$file = $appPath . '/appinfo/info.xml';
837
-
838
-		$data = $this->getAppInfoByPath($file, $lang);
839
-
840
-		if ($lang === null) {
841
-			$this->appInfos[$appId] = $data;
842
-		}
843
-
844
-		return $data;
845
-	}
846
-
847
-	public function getAppInfoByPath(string $path, ?string $lang = null): ?array {
848
-		if (!str_ends_with($path, '/appinfo/info.xml')) {
849
-			return null;
850
-		}
851
-
852
-		$parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
853
-		$data = $parser->parse($path);
854
-
855
-		if (is_array($data)) {
856
-			$data = $parser->applyL10N($data, $lang);
857
-		}
858
-
859
-		return $data;
860
-	}
861
-
862
-	public function getAppVersion(string $appId, bool $useCache = true): string {
863
-		if (!$useCache || !isset($this->appVersions[$appId])) {
864
-			if ($appId === 'core') {
865
-				$this->appVersions[$appId] = $this->serverVersion->getVersionString();
866
-			} else {
867
-				$appInfo = $this->getAppInfo($appId);
868
-				$this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
869
-			}
870
-		}
871
-		return $this->appVersions[$appId];
872
-	}
873
-
874
-	/**
875
-	 * Returns the installed versions of all apps
876
-	 *
877
-	 * @return array<string, string>
878
-	 */
879
-	public function getAppInstalledVersions(bool $onlyEnabled = false): array {
880
-		return $this->getAppConfig()->getAppInstalledVersions($onlyEnabled);
881
-	}
882
-
883
-	/**
884
-	 * Returns a list of apps incompatible with the given version
885
-	 *
886
-	 * @param string $version Nextcloud version as array of version components
887
-	 *
888
-	 * @return array list of app info from incompatible apps
889
-	 *
890
-	 * @internal
891
-	 */
892
-	public function getIncompatibleApps(string $version): array {
893
-		$apps = $this->getEnabledApps();
894
-		$incompatibleApps = [];
895
-		foreach ($apps as $appId) {
896
-			$info = $this->getAppInfo($appId);
897
-			if ($info === null) {
898
-				$incompatibleApps[] = ['id' => $appId, 'name' => $appId];
899
-			} elseif (!$this->isAppCompatible($version, $info)) {
900
-				$incompatibleApps[] = $info;
901
-			}
902
-		}
903
-		return $incompatibleApps;
904
-	}
905
-
906
-	/**
907
-	 * @inheritdoc
908
-	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
909
-	 */
910
-	public function isShipped($appId) {
911
-		$this->loadShippedJson();
912
-		return in_array($appId, $this->shippedApps, true);
913
-	}
914
-
915
-	private function isAlwaysEnabled(string $appId): bool {
916
-		if ($appId === 'core') {
917
-			return true;
918
-		}
919
-
920
-		$alwaysEnabled = $this->getAlwaysEnabledApps();
921
-		return in_array($appId, $alwaysEnabled, true);
922
-	}
923
-
924
-	/**
925
-	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
926
-	 * @throws \Exception
927
-	 */
928
-	private function loadShippedJson(): void {
929
-		if ($this->shippedApps === null) {
930
-			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
931
-			if (!file_exists($shippedJson)) {
932
-				throw new \Exception("File not found: $shippedJson");
933
-			}
934
-			$content = json_decode(file_get_contents($shippedJson), true);
935
-			$this->shippedApps = $content['shippedApps'];
936
-			$this->alwaysEnabled = $content['alwaysEnabled'];
937
-			$this->defaultEnabled = $content['defaultEnabled'];
938
-		}
939
-	}
940
-
941
-	/**
942
-	 * @inheritdoc
943
-	 */
944
-	public function getAlwaysEnabledApps() {
945
-		$this->loadShippedJson();
946
-		return $this->alwaysEnabled;
947
-	}
948
-
949
-	/**
950
-	 * @inheritdoc
951
-	 */
952
-	public function isDefaultEnabled(string $appId): bool {
953
-		return (in_array($appId, $this->getDefaultEnabledApps()));
954
-	}
955
-
956
-	/**
957
-	 * @inheritdoc
958
-	 */
959
-	public function getDefaultEnabledApps(): array {
960
-		$this->loadShippedJson();
961
-
962
-		return $this->defaultEnabled;
963
-	}
964
-
965
-	/**
966
-	 * @inheritdoc
967
-	 */
968
-	public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
969
-		$id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
970
-		$entry = $this->getNavigationManager()->get($id);
971
-		return (string)$entry['app'];
972
-	}
973
-
974
-	/**
975
-	 * @inheritdoc
976
-	 */
977
-	public function getDefaultApps(): array {
978
-		$ids = $this->getNavigationManager()->getDefaultEntryIds();
979
-
980
-		return array_values(array_unique(array_map(function (string $id) {
981
-			$entry = $this->getNavigationManager()->get($id);
982
-			return (string)$entry['app'];
983
-		}, $ids)));
984
-	}
985
-
986
-	/**
987
-	 * @inheritdoc
988
-	 */
989
-	public function setDefaultApps(array $defaultApps): void {
990
-		$entries = $this->getNavigationManager()->getAll();
991
-		$ids = [];
992
-		foreach ($defaultApps as $defaultApp) {
993
-			foreach ($entries as $entry) {
994
-				if ((string)$entry['app'] === $defaultApp) {
995
-					$ids[] = (string)$entry['id'];
996
-					break;
997
-				}
998
-			}
999
-		}
1000
-		$this->getNavigationManager()->setDefaultEntryIds($ids);
1001
-	}
1002
-
1003
-	public function isBackendRequired(string $backend): bool {
1004
-		foreach ($this->appInfos as $appInfo) {
1005
-			if (
1006
-				isset($appInfo['dependencies']['backend'])
1007
-				&& is_array($appInfo['dependencies']['backend'])
1008
-				&& in_array($backend, $appInfo['dependencies']['backend'], true)
1009
-			) {
1010
-				return true;
1011
-			}
1012
-		}
1013
-
1014
-		return false;
1015
-	}
1016
-
1017
-	/**
1018
-	 * Clean the appId from forbidden characters
1019
-	 *
1020
-	 * @psalm-taint-escape callable
1021
-	 * @psalm-taint-escape cookie
1022
-	 * @psalm-taint-escape file
1023
-	 * @psalm-taint-escape has_quotes
1024
-	 * @psalm-taint-escape header
1025
-	 * @psalm-taint-escape html
1026
-	 * @psalm-taint-escape include
1027
-	 * @psalm-taint-escape ldap
1028
-	 * @psalm-taint-escape shell
1029
-	 * @psalm-taint-escape sql
1030
-	 * @psalm-taint-escape unserialize
1031
-	 */
1032
-	public function cleanAppId(string $app): string {
1033
-		/* Only lowercase alphanumeric is allowed */
1034
-		return preg_replace('/(^[0-9_-]+|[^a-z0-9_-]+|[_-]+$)/', '', $app);
1035
-	}
1036
-
1037
-	/**
1038
-	 * Run upgrade tasks for an app after the code has already been updated
1039
-	 *
1040
-	 * @throws AppPathNotFoundException if app folder can't be found
1041
-	 */
1042
-	public function upgradeApp(string $appId): bool {
1043
-		// for apps distributed with core, we refresh app path in case the downloaded version
1044
-		// have been installed in custom apps and not in the default path
1045
-		$appPath = $this->getAppPath($appId, true);
1046
-
1047
-		$this->clearAppsCache();
1048
-		$l = \OC::$server->getL10N('core');
1049
-		$appData = $this->getAppInfo($appId, false, $l->getLanguageCode());
1050
-		if ($appData === null) {
1051
-			throw new AppPathNotFoundException('Could not find ' . $appId);
1052
-		}
1053
-
1054
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
1055
-		$ignoreMax = in_array($appId, $ignoreMaxApps, true);
1056
-		\OC_App::checkAppDependencies(
1057
-			$this->config,
1058
-			$l,
1059
-			$appData,
1060
-			$ignoreMax
1061
-		);
1062
-
1063
-		\OC_App::registerAutoloading($appId, $appPath, true);
1064
-		\OC_App::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
1065
-
1066
-		$ms = new MigrationService($appId, Server::get(\OC\DB\Connection::class));
1067
-		$ms->migrate();
1068
-
1069
-		\OC_App::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
1070
-		$queue = Server::get(IJobList::class);
1071
-		foreach ($appData['repair-steps']['live-migration'] as $step) {
1072
-			$queue->add(\OC\Migration\BackgroundRepair::class, [
1073
-				'app' => $appId,
1074
-				'step' => $step]);
1075
-		}
1076
-
1077
-		// update appversion in app manager
1078
-		$this->clearAppsCache();
1079
-		$this->getAppVersion($appId, false);
1080
-
1081
-		// Setup background jobs
1082
-		foreach ($appData['background-jobs'] as $job) {
1083
-			$queue->add($job);
1084
-		}
1085
-
1086
-		//set remote/public handlers
1087
-		foreach ($appData['remote'] as $name => $path) {
1088
-			$this->config->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
1089
-		}
1090
-		foreach ($appData['public'] as $name => $path) {
1091
-			$this->config->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1092
-		}
1093
-
1094
-		\OC_App::setAppTypes($appId);
1095
-
1096
-		$version = $this->getAppVersion($appId);
1097
-		$this->config->setAppValue($appId, 'installed_version', $version);
1098
-
1099
-		// migrate eventual new config keys in the process
1100
-		/** @psalm-suppress InternalMethod */
1101
-		$this->configManager->migrateConfigLexiconKeys($appId);
1102
-		$this->configManager->updateLexiconEntries($appId);
1103
-
1104
-		$this->dispatcher->dispatchTyped(new AppUpdateEvent($appId));
1105
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
1106
-			ManagerEvent::EVENT_APP_UPDATE, $appId
1107
-		));
1108
-
1109
-		return true;
1110
-	}
1111
-
1112
-	public function isUpgradeRequired(string $appId): bool {
1113
-		$versions = $this->getAppInstalledVersions();
1114
-		$currentVersion = $this->getAppVersion($appId);
1115
-		if ($currentVersion && isset($versions[$appId])) {
1116
-			$installedVersion = $versions[$appId];
1117
-			if (!version_compare($currentVersion, $installedVersion, '=')) {
1118
-				return true;
1119
-			}
1120
-		}
1121
-		return false;
1122
-	}
1123
-
1124
-	public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool {
1125
-		return count($this->dependencyAnalyzer->analyzeServerVersion($serverVersion, $appInfo, $ignoreMax)) === 0;
1126
-	}
41
+    /**
42
+     * Apps with these types can not be enabled for certain groups only
43
+     * @var string[]
44
+     */
45
+    protected $protectedAppTypes = [
46
+        'filesystem',
47
+        'prelogin',
48
+        'authentication',
49
+        'logging',
50
+        'prevent_group_restriction',
51
+    ];
52
+
53
+    /** @var string[] $appId => $enabled */
54
+    private array $enabledAppsCache = [];
55
+
56
+    /** @var string[]|null */
57
+    private ?array $shippedApps = null;
58
+
59
+    private array $alwaysEnabled = [];
60
+    private array $defaultEnabled = [];
61
+
62
+    /** @var array */
63
+    private array $appInfos = [];
64
+
65
+    /** @var array */
66
+    private array $appVersions = [];
67
+
68
+    /** @var array */
69
+    private array $autoDisabledApps = [];
70
+    private array $appTypes = [];
71
+
72
+    /** @var array<string, true> */
73
+    private array $loadedApps = [];
74
+
75
+    private ?AppConfig $appConfig = null;
76
+    private ?IURLGenerator $urlGenerator = null;
77
+    private ?INavigationManager $navigationManager = null;
78
+
79
+    /**
80
+     * Be extremely careful when injecting classes here. The AppManager is used by the installer,
81
+     * so it needs to work before installation. See how AppConfig and IURLGenerator are injected for reference
82
+     */
83
+    public function __construct(
84
+        private IUserSession $userSession,
85
+        private IConfig $config,
86
+        private IGroupManager $groupManager,
87
+        private ICacheFactory $memCacheFactory,
88
+        private IEventDispatcher $dispatcher,
89
+        private LoggerInterface $logger,
90
+        private ServerVersion $serverVersion,
91
+        private ConfigManager $configManager,
92
+        private DependencyAnalyzer $dependencyAnalyzer,
93
+    ) {
94
+    }
95
+
96
+    private function getNavigationManager(): INavigationManager {
97
+        if ($this->navigationManager === null) {
98
+            $this->navigationManager = Server::get(INavigationManager::class);
99
+        }
100
+        return $this->navigationManager;
101
+    }
102
+
103
+    public function getAppIcon(string $appId, bool $dark = false): ?string {
104
+        $possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
105
+        $icon = null;
106
+        foreach ($possibleIcons as $iconName) {
107
+            try {
108
+                $icon = $this->getUrlGenerator()->imagePath($appId, $iconName);
109
+                break;
110
+            } catch (\RuntimeException $e) {
111
+                // ignore
112
+            }
113
+        }
114
+        return $icon;
115
+    }
116
+
117
+    private function getAppConfig(): AppConfig {
118
+        if ($this->appConfig !== null) {
119
+            return $this->appConfig;
120
+        }
121
+        if (!$this->config->getSystemValueBool('installed', false)) {
122
+            throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
123
+        }
124
+        $this->appConfig = Server::get(AppConfig::class);
125
+        return $this->appConfig;
126
+    }
127
+
128
+    private function getUrlGenerator(): IURLGenerator {
129
+        if ($this->urlGenerator !== null) {
130
+            return $this->urlGenerator;
131
+        }
132
+        if (!$this->config->getSystemValueBool('installed', false)) {
133
+            throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
134
+        }
135
+        $this->urlGenerator = Server::get(IURLGenerator::class);
136
+        return $this->urlGenerator;
137
+    }
138
+
139
+    /**
140
+     * For all enabled apps, return the value of their 'enabled' config key.
141
+     *
142
+     * @return array<string,string> appId => enabled (may be 'yes', or a json encoded list of group ids)
143
+     */
144
+    private function getEnabledAppsValues(): array {
145
+        if (!$this->enabledAppsCache) {
146
+            /** @var array<string,string> */
147
+            $values = $this->getAppConfig()->searchValues('enabled', false, IAppConfig::VALUE_STRING);
148
+
149
+            $alwaysEnabledApps = $this->getAlwaysEnabledApps();
150
+            foreach ($alwaysEnabledApps as $appId) {
151
+                $values[$appId] = 'yes';
152
+            }
153
+
154
+            $this->enabledAppsCache = array_filter($values, function ($value) {
155
+                return $value !== 'no';
156
+            });
157
+            ksort($this->enabledAppsCache);
158
+        }
159
+        return $this->enabledAppsCache;
160
+    }
161
+
162
+    /**
163
+     * Deprecated alias
164
+     *
165
+     * @return string[]
166
+     */
167
+    public function getInstalledApps() {
168
+        return $this->getEnabledApps();
169
+    }
170
+
171
+    /**
172
+     * List all enabled apps, either for everyone or for some groups
173
+     *
174
+     * @return list<string>
175
+     */
176
+    public function getEnabledApps(): array {
177
+        return array_keys($this->getEnabledAppsValues());
178
+    }
179
+
180
+    /**
181
+     * Get a list of all apps in the apps folder
182
+     *
183
+     * @return list<string> an array of app names (string IDs)
184
+     */
185
+    public function getAllAppsInAppsFolders(): array {
186
+        $apps = [];
187
+
188
+        foreach (\OC::$APPSROOTS as $apps_dir) {
189
+            if (!is_readable($apps_dir['path'])) {
190
+                $this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']);
191
+                continue;
192
+            }
193
+            $dh = opendir($apps_dir['path']);
194
+
195
+            if (is_resource($dh)) {
196
+                while (($file = readdir($dh)) !== false) {
197
+                    if (
198
+                        $file[0] != '.'
199
+                        && is_dir($apps_dir['path'] . '/' . $file)
200
+                        && is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')
201
+                    ) {
202
+                        $apps[] = $file;
203
+                    }
204
+                }
205
+            }
206
+        }
207
+
208
+        return array_values(array_unique($apps));
209
+    }
210
+
211
+    /**
212
+     * List all apps enabled for a user
213
+     *
214
+     * @param \OCP\IUser $user
215
+     * @return list<string>
216
+     */
217
+    public function getEnabledAppsForUser(IUser $user) {
218
+        $apps = $this->getEnabledAppsValues();
219
+        $appsForUser = array_filter($apps, function ($enabled) use ($user) {
220
+            return $this->checkAppForUser($enabled, $user);
221
+        });
222
+        return array_keys($appsForUser);
223
+    }
224
+
225
+    public function getEnabledAppsForGroup(IGroup $group): array {
226
+        $apps = $this->getEnabledAppsValues();
227
+        $appsForGroups = array_filter($apps, function ($enabled) use ($group) {
228
+            return $this->checkAppForGroups($enabled, $group);
229
+        });
230
+        return array_keys($appsForGroups);
231
+    }
232
+
233
+    /**
234
+     * Loads all apps
235
+     *
236
+     * @param string[] $types
237
+     * @return bool
238
+     *
239
+     * This function walks through the Nextcloud directory and loads all apps
240
+     * it can find. A directory contains an app if the file /appinfo/info.xml
241
+     * exists.
242
+     *
243
+     * if $types is set to non-empty array, only apps of those types will be loaded
244
+     */
245
+    public function loadApps(array $types = []): bool {
246
+        if ($this->config->getSystemValueBool('maintenance', false)) {
247
+            return false;
248
+        }
249
+        // Load the enabled apps here
250
+        $apps = \OC_App::getEnabledApps();
251
+
252
+        // Add each apps' folder as allowed class path
253
+        foreach ($apps as $app) {
254
+            // If the app is already loaded then autoloading it makes no sense
255
+            if (!$this->isAppLoaded($app)) {
256
+                try {
257
+                    $path = $this->getAppPath($app);
258
+                    \OC_App::registerAutoloading($app, $path);
259
+                } catch (AppPathNotFoundException $e) {
260
+                    $this->logger->info('Error during app loading: ' . $e->getMessage(), [
261
+                        'exception' => $e,
262
+                        'app' => $app,
263
+                    ]);
264
+                }
265
+            }
266
+        }
267
+
268
+        // prevent app loading from printing output
269
+        ob_start();
270
+        foreach ($apps as $app) {
271
+            if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
272
+                try {
273
+                    $this->loadApp($app);
274
+                } catch (\Throwable $e) {
275
+                    $this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
276
+                        'exception' => $e,
277
+                        'app' => $app,
278
+                    ]);
279
+                }
280
+            }
281
+        }
282
+        ob_end_clean();
283
+
284
+        return true;
285
+    }
286
+
287
+    /**
288
+     * check if an app is of a specific type
289
+     *
290
+     * @param string $app
291
+     * @param array $types
292
+     * @return bool
293
+     */
294
+    public function isType(string $app, array $types): bool {
295
+        $appTypes = $this->getAppTypes($app);
296
+        foreach ($types as $type) {
297
+            if (in_array($type, $appTypes, true)) {
298
+                return true;
299
+            }
300
+        }
301
+        return false;
302
+    }
303
+
304
+    /**
305
+     * get the types of an app
306
+     *
307
+     * @param string $app
308
+     * @return string[]
309
+     */
310
+    private function getAppTypes(string $app): array {
311
+        //load the cache
312
+        if (count($this->appTypes) === 0) {
313
+            $this->appTypes = $this->getAppConfig()->getValues(false, 'types') ?: [];
314
+        }
315
+
316
+        if (isset($this->appTypes[$app])) {
317
+            return explode(',', $this->appTypes[$app]);
318
+        }
319
+
320
+        return [];
321
+    }
322
+
323
+    /**
324
+     * @return array
325
+     */
326
+    public function getAutoDisabledApps(): array {
327
+        return $this->autoDisabledApps;
328
+    }
329
+
330
+    public function getAppRestriction(string $appId): array {
331
+        $values = $this->getEnabledAppsValues();
332
+
333
+        if (!isset($values[$appId])) {
334
+            return [];
335
+        }
336
+
337
+        if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
338
+            return [];
339
+        }
340
+        return json_decode($values[$appId], true);
341
+    }
342
+
343
+    /**
344
+     * Check if an app is enabled for user
345
+     *
346
+     * @param string $appId
347
+     * @param \OCP\IUser|null $user (optional) if not defined, the currently logged in user will be used
348
+     * @return bool
349
+     */
350
+    public function isEnabledForUser($appId, $user = null) {
351
+        if ($this->isAlwaysEnabled($appId)) {
352
+            return true;
353
+        }
354
+        if ($user === null) {
355
+            $user = $this->userSession->getUser();
356
+        }
357
+        $enabledAppsValues = $this->getEnabledAppsValues();
358
+        if (isset($enabledAppsValues[$appId])) {
359
+            return $this->checkAppForUser($enabledAppsValues[$appId], $user);
360
+        } else {
361
+            return false;
362
+        }
363
+    }
364
+
365
+    private function checkAppForUser(string $enabled, ?IUser $user): bool {
366
+        if ($enabled === 'yes') {
367
+            return true;
368
+        } elseif ($user === null) {
369
+            return false;
370
+        } else {
371
+            if (empty($enabled)) {
372
+                return false;
373
+            }
374
+
375
+            $groupIds = json_decode($enabled);
376
+
377
+            if (!is_array($groupIds)) {
378
+                $jsonError = json_last_error();
379
+                $jsonErrorMsg = json_last_error_msg();
380
+                // this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason)
381
+                $this->logger->warning('AppManager::checkAppForUser - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg);
382
+                return false;
383
+            }
384
+
385
+            $userGroups = $this->groupManager->getUserGroupIds($user);
386
+            foreach ($userGroups as $groupId) {
387
+                if (in_array($groupId, $groupIds, true)) {
388
+                    return true;
389
+                }
390
+            }
391
+            return false;
392
+        }
393
+    }
394
+
395
+    private function checkAppForGroups(string $enabled, IGroup $group): bool {
396
+        if ($enabled === 'yes') {
397
+            return true;
398
+        } else {
399
+            if (empty($enabled)) {
400
+                return false;
401
+            }
402
+
403
+            $groupIds = json_decode($enabled);
404
+
405
+            if (!is_array($groupIds)) {
406
+                $jsonError = json_last_error();
407
+                $jsonErrorMsg = json_last_error_msg();
408
+                // this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason)
409
+                $this->logger->warning('AppManager::checkAppForGroups - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg);
410
+                return false;
411
+            }
412
+
413
+            return in_array($group->getGID(), $groupIds);
414
+        }
415
+    }
416
+
417
+    /**
418
+     * Check if an app is enabled in the instance
419
+     *
420
+     * Notice: This actually checks if the app is enabled and not only if it is installed.
421
+     *
422
+     * @param string $appId
423
+     */
424
+    public function isInstalled($appId): bool {
425
+        return $this->isEnabledForAnyone($appId);
426
+    }
427
+
428
+    public function isEnabledForAnyone(string $appId): bool {
429
+        $enabledAppsValues = $this->getEnabledAppsValues();
430
+        return isset($enabledAppsValues[$appId]);
431
+    }
432
+
433
+    /**
434
+     * Overwrite the `max-version` requirement for this app.
435
+     */
436
+    public function overwriteNextcloudRequirement(string $appId): void {
437
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
438
+        if (!in_array($appId, $ignoreMaxApps, true)) {
439
+            $ignoreMaxApps[] = $appId;
440
+        }
441
+        $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
442
+    }
443
+
444
+    /**
445
+     * Remove the `max-version` overwrite for this app.
446
+     * This means this app now again can not be enabled if the `max-version` is smaller than the current Nextcloud version.
447
+     */
448
+    public function removeOverwriteNextcloudRequirement(string $appId): void {
449
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
450
+        $ignoreMaxApps = array_filter($ignoreMaxApps, fn (string $id) => $id !== $appId);
451
+        $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
452
+    }
453
+
454
+    public function loadApp(string $app): void {
455
+        if (isset($this->loadedApps[$app])) {
456
+            return;
457
+        }
458
+        $this->loadedApps[$app] = true;
459
+        try {
460
+            $appPath = $this->getAppPath($app);
461
+        } catch (AppPathNotFoundException $e) {
462
+            $this->logger->info('Error during app loading: ' . $e->getMessage(), [
463
+                'exception' => $e,
464
+                'app' => $app,
465
+            ]);
466
+            return;
467
+        }
468
+        $eventLogger = \OC::$server->get(IEventLogger::class);
469
+        $eventLogger->start("bootstrap:load_app:$app", "Load app: $app");
470
+
471
+        // in case someone calls loadApp() directly
472
+        \OC_App::registerAutoloading($app, $appPath);
473
+
474
+        if (is_file($appPath . '/appinfo/app.php')) {
475
+            $this->logger->error('/appinfo/app.php is not supported anymore, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
476
+                'app' => $app,
477
+            ]);
478
+        }
479
+
480
+        $coordinator = Server::get(Coordinator::class);
481
+        $coordinator->bootApp($app);
482
+
483
+        $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
484
+        $info = $this->getAppInfo($app);
485
+        if (!empty($info['activity'])) {
486
+            $activityManager = \OC::$server->get(IActivityManager::class);
487
+            if (!empty($info['activity']['filters'])) {
488
+                foreach ($info['activity']['filters'] as $filter) {
489
+                    $activityManager->registerFilter($filter);
490
+                }
491
+            }
492
+            if (!empty($info['activity']['settings'])) {
493
+                foreach ($info['activity']['settings'] as $setting) {
494
+                    $activityManager->registerSetting($setting);
495
+                }
496
+            }
497
+            if (!empty($info['activity']['providers'])) {
498
+                foreach ($info['activity']['providers'] as $provider) {
499
+                    $activityManager->registerProvider($provider);
500
+                }
501
+            }
502
+        }
503
+
504
+        if (!empty($info['settings'])) {
505
+            $settingsManager = \OCP\Server::get(ISettingsManager::class);
506
+            if (!empty($info['settings']['admin'])) {
507
+                foreach ($info['settings']['admin'] as $setting) {
508
+                    $settingsManager->registerSetting('admin', $setting);
509
+                }
510
+            }
511
+            if (!empty($info['settings']['admin-section'])) {
512
+                foreach ($info['settings']['admin-section'] as $section) {
513
+                    $settingsManager->registerSection('admin', $section);
514
+                }
515
+            }
516
+            if (!empty($info['settings']['personal'])) {
517
+                foreach ($info['settings']['personal'] as $setting) {
518
+                    $settingsManager->registerSetting('personal', $setting);
519
+                }
520
+            }
521
+            if (!empty($info['settings']['personal-section'])) {
522
+                foreach ($info['settings']['personal-section'] as $section) {
523
+                    $settingsManager->registerSection('personal', $section);
524
+                }
525
+            }
526
+            if (!empty($info['settings']['admin-delegation'])) {
527
+                foreach ($info['settings']['admin-delegation'] as $setting) {
528
+                    $settingsManager->registerSetting(ISettingsManager::SETTINGS_DELEGATION, $setting);
529
+                }
530
+            }
531
+            if (!empty($info['settings']['admin-delegation-section'])) {
532
+                foreach ($info['settings']['admin-delegation-section'] as $section) {
533
+                    $settingsManager->registerSection(ISettingsManager::SETTINGS_DELEGATION, $section);
534
+                }
535
+            }
536
+        }
537
+
538
+        if (!empty($info['collaboration']['plugins'])) {
539
+            // deal with one or many plugin entries
540
+            $plugins = isset($info['collaboration']['plugins']['plugin']['@value'])
541
+                ? [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
542
+            $collaboratorSearch = null;
543
+            $autoCompleteManager = null;
544
+            foreach ($plugins as $plugin) {
545
+                if ($plugin['@attributes']['type'] === 'collaborator-search') {
546
+                    $pluginInfo = [
547
+                        'shareType' => $plugin['@attributes']['share-type'],
548
+                        'class' => $plugin['@value'],
549
+                    ];
550
+                    $collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
551
+                    $collaboratorSearch->registerPlugin($pluginInfo);
552
+                } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
553
+                    $autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
554
+                    $autoCompleteManager->registerSorter($plugin['@value']);
555
+                }
556
+            }
557
+        }
558
+        $eventLogger->end("bootstrap:load_app:$app:info");
559
+
560
+        $eventLogger->end("bootstrap:load_app:$app");
561
+    }
562
+
563
+    /**
564
+     * Check if an app is loaded
565
+     * @param string $app app id
566
+     * @since 26.0.0
567
+     */
568
+    public function isAppLoaded(string $app): bool {
569
+        return isset($this->loadedApps[$app]);
570
+    }
571
+
572
+    /**
573
+     * Enable an app for every user
574
+     *
575
+     * @param string $appId
576
+     * @param bool $forceEnable
577
+     * @throws AppPathNotFoundException
578
+     * @throws \InvalidArgumentException if the application is not installed yet
579
+     */
580
+    public function enableApp(string $appId, bool $forceEnable = false): void {
581
+        // Check if app exists
582
+        $this->getAppPath($appId);
583
+
584
+        if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
585
+            throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
586
+        }
587
+
588
+        if ($forceEnable) {
589
+            $this->overwriteNextcloudRequirement($appId);
590
+        }
591
+
592
+        $this->enabledAppsCache[$appId] = 'yes';
593
+        $this->getAppConfig()->setValue($appId, 'enabled', 'yes');
594
+        $this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
595
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
596
+            ManagerEvent::EVENT_APP_ENABLE, $appId
597
+        ));
598
+        $this->clearAppsCache();
599
+
600
+        $this->configManager->migrateConfigLexiconKeys($appId);
601
+    }
602
+
603
+    /**
604
+     * Whether a list of types contains a protected app type
605
+     *
606
+     * @param string[] $types
607
+     * @return bool
608
+     */
609
+    public function hasProtectedAppType($types) {
610
+        if (empty($types)) {
611
+            return false;
612
+        }
613
+
614
+        $protectedTypes = array_intersect($this->protectedAppTypes, $types);
615
+        return !empty($protectedTypes);
616
+    }
617
+
618
+    /**
619
+     * Enable an app only for specific groups
620
+     *
621
+     * @param string $appId
622
+     * @param IGroup[] $groups
623
+     * @param bool $forceEnable
624
+     * @throws \InvalidArgumentException if app can't be enabled for groups
625
+     * @throws AppPathNotFoundException
626
+     */
627
+    public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
628
+        // Check if app exists
629
+        $this->getAppPath($appId);
630
+
631
+        $info = $this->getAppInfo($appId);
632
+        if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
633
+            throw new \InvalidArgumentException("$appId can't be enabled for groups.");
634
+        }
635
+
636
+        if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
637
+            throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
638
+        }
639
+
640
+        if ($forceEnable) {
641
+            $this->overwriteNextcloudRequirement($appId);
642
+        }
643
+
644
+        /** @var string[] $groupIds */
645
+        $groupIds = array_map(function ($group) {
646
+            /** @var IGroup $group */
647
+            return ($group instanceof IGroup)
648
+                ? $group->getGID()
649
+                : $group;
650
+        }, $groups);
651
+
652
+        $this->enabledAppsCache[$appId] = json_encode($groupIds);
653
+        $this->getAppConfig()->setValue($appId, 'enabled', json_encode($groupIds));
654
+        $this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
655
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
656
+            ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
657
+        ));
658
+        $this->clearAppsCache();
659
+
660
+        $this->configManager->migrateConfigLexiconKeys($appId);
661
+    }
662
+
663
+    /**
664
+     * Disable an app for every user
665
+     *
666
+     * @param string $appId
667
+     * @param bool $automaticDisabled
668
+     * @throws \Exception if app can't be disabled
669
+     */
670
+    public function disableApp($appId, $automaticDisabled = false): void {
671
+        if ($this->isAlwaysEnabled($appId)) {
672
+            throw new \Exception("$appId can't be disabled.");
673
+        }
674
+
675
+        if ($automaticDisabled) {
676
+            $previousSetting = $this->getAppConfig()->getValue($appId, 'enabled', 'yes');
677
+            if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
678
+                $previousSetting = json_decode($previousSetting, true);
679
+            }
680
+            $this->autoDisabledApps[$appId] = $previousSetting;
681
+        }
682
+
683
+        unset($this->enabledAppsCache[$appId]);
684
+        $this->getAppConfig()->setValue($appId, 'enabled', 'no');
685
+
686
+        // run uninstall steps
687
+        $appData = $this->getAppInfo($appId);
688
+        if (!is_null($appData)) {
689
+            \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
690
+        }
691
+
692
+        $this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
693
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
694
+            ManagerEvent::EVENT_APP_DISABLE, $appId
695
+        ));
696
+        $this->clearAppsCache();
697
+    }
698
+
699
+    /**
700
+     * Get the directory for the given app.
701
+     *
702
+     * @psalm-taint-specialize
703
+     *
704
+     * @throws AppPathNotFoundException if app folder can't be found
705
+     */
706
+    public function getAppPath(string $appId, bool $ignoreCache = false): string {
707
+        $appId = $this->cleanAppId($appId);
708
+        if ($appId === '') {
709
+            throw new AppPathNotFoundException('App id is empty');
710
+        } elseif ($appId === 'core') {
711
+            return __DIR__ . '/../../../core';
712
+        }
713
+
714
+        if (($dir = $this->findAppInDirectories($appId, $ignoreCache)) != false) {
715
+            return $dir['path'] . '/' . $appId;
716
+        }
717
+        throw new AppPathNotFoundException('Could not find path for ' . $appId);
718
+    }
719
+
720
+    /**
721
+     * Get the web path for the given app.
722
+     *
723
+     * @throws AppPathNotFoundException if app path can't be found
724
+     */
725
+    public function getAppWebPath(string $appId): string {
726
+        if (($dir = $this->findAppInDirectories($appId)) != false) {
727
+            return \OC::$WEBROOT . $dir['url'] . '/' . $appId;
728
+        }
729
+        throw new AppPathNotFoundException('Could not find web path for ' . $appId);
730
+    }
731
+
732
+    /**
733
+     * Find the apps root for an app id.
734
+     *
735
+     * If multiple copies are found, the apps root the latest version is returned.
736
+     *
737
+     * @param bool $ignoreCache ignore cache and rebuild it
738
+     * @return false|array{path: string, url: string} the apps root shape
739
+     */
740
+    public function findAppInDirectories(string $appId, bool $ignoreCache = false) {
741
+        $sanitizedAppId = $this->cleanAppId($appId);
742
+        if ($sanitizedAppId !== $appId) {
743
+            return false;
744
+        }
745
+        // FIXME replace by a property or a cache
746
+        static $app_dir = [];
747
+
748
+        if (isset($app_dir[$appId]) && !$ignoreCache) {
749
+            return $app_dir[$appId];
750
+        }
751
+
752
+        $possibleApps = [];
753
+        foreach (\OC::$APPSROOTS as $dir) {
754
+            if (file_exists($dir['path'] . '/' . $appId)) {
755
+                $possibleApps[] = $dir;
756
+            }
757
+        }
758
+
759
+        if (empty($possibleApps)) {
760
+            return false;
761
+        } elseif (count($possibleApps) === 1) {
762
+            $dir = array_shift($possibleApps);
763
+            $app_dir[$appId] = $dir;
764
+            return $dir;
765
+        } else {
766
+            $versionToLoad = [];
767
+            foreach ($possibleApps as $possibleApp) {
768
+                $appData = $this->getAppInfoByPath($possibleApp['path'] . '/' . $appId . '/appinfo/info.xml');
769
+                $version = $appData['version'] ?? '';
770
+                if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
771
+                    $versionToLoad = [
772
+                        'dir' => $possibleApp,
773
+                        'version' => $version,
774
+                    ];
775
+                }
776
+            }
777
+            if (!isset($versionToLoad['dir'])) {
778
+                return false;
779
+            }
780
+            $app_dir[$appId] = $versionToLoad['dir'];
781
+            return $versionToLoad['dir'];
782
+        }
783
+    }
784
+
785
+    /**
786
+     * Clear the cached list of apps when enabling/disabling an app
787
+     */
788
+    public function clearAppsCache(): void {
789
+        $this->appInfos = [];
790
+    }
791
+
792
+    /**
793
+     * Returns a list of apps that need upgrade
794
+     *
795
+     * @param string $version Nextcloud version as array of version components
796
+     * @return array list of app info from apps that need an upgrade
797
+     *
798
+     * @internal
799
+     */
800
+    public function getAppsNeedingUpgrade($version) {
801
+        $appsToUpgrade = [];
802
+        $apps = $this->getEnabledApps();
803
+        foreach ($apps as $appId) {
804
+            $appInfo = $this->getAppInfo($appId);
805
+            $appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version');
806
+            if ($appDbVersion
807
+                && isset($appInfo['version'])
808
+                && version_compare($appInfo['version'], $appDbVersion, '>')
809
+                && $this->isAppCompatible($version, $appInfo)
810
+            ) {
811
+                $appsToUpgrade[] = $appInfo;
812
+            }
813
+        }
814
+
815
+        return $appsToUpgrade;
816
+    }
817
+
818
+    /**
819
+     * Returns the app information from "appinfo/info.xml".
820
+     *
821
+     * @param string|null $lang
822
+     * @return array|null app info
823
+     */
824
+    public function getAppInfo(string $appId, bool $path = false, $lang = null) {
825
+        if ($path) {
826
+            throw new \InvalidArgumentException('Calling IAppManager::getAppInfo() with a path is no longer supported. Please call IAppManager::getAppInfoByPath() instead and verify that the path is good before calling.');
827
+        }
828
+        if ($lang === null && isset($this->appInfos[$appId])) {
829
+            return $this->appInfos[$appId];
830
+        }
831
+        try {
832
+            $appPath = $this->getAppPath($appId);
833
+        } catch (AppPathNotFoundException) {
834
+            return null;
835
+        }
836
+        $file = $appPath . '/appinfo/info.xml';
837
+
838
+        $data = $this->getAppInfoByPath($file, $lang);
839
+
840
+        if ($lang === null) {
841
+            $this->appInfos[$appId] = $data;
842
+        }
843
+
844
+        return $data;
845
+    }
846
+
847
+    public function getAppInfoByPath(string $path, ?string $lang = null): ?array {
848
+        if (!str_ends_with($path, '/appinfo/info.xml')) {
849
+            return null;
850
+        }
851
+
852
+        $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
853
+        $data = $parser->parse($path);
854
+
855
+        if (is_array($data)) {
856
+            $data = $parser->applyL10N($data, $lang);
857
+        }
858
+
859
+        return $data;
860
+    }
861
+
862
+    public function getAppVersion(string $appId, bool $useCache = true): string {
863
+        if (!$useCache || !isset($this->appVersions[$appId])) {
864
+            if ($appId === 'core') {
865
+                $this->appVersions[$appId] = $this->serverVersion->getVersionString();
866
+            } else {
867
+                $appInfo = $this->getAppInfo($appId);
868
+                $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
869
+            }
870
+        }
871
+        return $this->appVersions[$appId];
872
+    }
873
+
874
+    /**
875
+     * Returns the installed versions of all apps
876
+     *
877
+     * @return array<string, string>
878
+     */
879
+    public function getAppInstalledVersions(bool $onlyEnabled = false): array {
880
+        return $this->getAppConfig()->getAppInstalledVersions($onlyEnabled);
881
+    }
882
+
883
+    /**
884
+     * Returns a list of apps incompatible with the given version
885
+     *
886
+     * @param string $version Nextcloud version as array of version components
887
+     *
888
+     * @return array list of app info from incompatible apps
889
+     *
890
+     * @internal
891
+     */
892
+    public function getIncompatibleApps(string $version): array {
893
+        $apps = $this->getEnabledApps();
894
+        $incompatibleApps = [];
895
+        foreach ($apps as $appId) {
896
+            $info = $this->getAppInfo($appId);
897
+            if ($info === null) {
898
+                $incompatibleApps[] = ['id' => $appId, 'name' => $appId];
899
+            } elseif (!$this->isAppCompatible($version, $info)) {
900
+                $incompatibleApps[] = $info;
901
+            }
902
+        }
903
+        return $incompatibleApps;
904
+    }
905
+
906
+    /**
907
+     * @inheritdoc
908
+     * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
909
+     */
910
+    public function isShipped($appId) {
911
+        $this->loadShippedJson();
912
+        return in_array($appId, $this->shippedApps, true);
913
+    }
914
+
915
+    private function isAlwaysEnabled(string $appId): bool {
916
+        if ($appId === 'core') {
917
+            return true;
918
+        }
919
+
920
+        $alwaysEnabled = $this->getAlwaysEnabledApps();
921
+        return in_array($appId, $alwaysEnabled, true);
922
+    }
923
+
924
+    /**
925
+     * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
926
+     * @throws \Exception
927
+     */
928
+    private function loadShippedJson(): void {
929
+        if ($this->shippedApps === null) {
930
+            $shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
931
+            if (!file_exists($shippedJson)) {
932
+                throw new \Exception("File not found: $shippedJson");
933
+            }
934
+            $content = json_decode(file_get_contents($shippedJson), true);
935
+            $this->shippedApps = $content['shippedApps'];
936
+            $this->alwaysEnabled = $content['alwaysEnabled'];
937
+            $this->defaultEnabled = $content['defaultEnabled'];
938
+        }
939
+    }
940
+
941
+    /**
942
+     * @inheritdoc
943
+     */
944
+    public function getAlwaysEnabledApps() {
945
+        $this->loadShippedJson();
946
+        return $this->alwaysEnabled;
947
+    }
948
+
949
+    /**
950
+     * @inheritdoc
951
+     */
952
+    public function isDefaultEnabled(string $appId): bool {
953
+        return (in_array($appId, $this->getDefaultEnabledApps()));
954
+    }
955
+
956
+    /**
957
+     * @inheritdoc
958
+     */
959
+    public function getDefaultEnabledApps(): array {
960
+        $this->loadShippedJson();
961
+
962
+        return $this->defaultEnabled;
963
+    }
964
+
965
+    /**
966
+     * @inheritdoc
967
+     */
968
+    public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
969
+        $id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
970
+        $entry = $this->getNavigationManager()->get($id);
971
+        return (string)$entry['app'];
972
+    }
973
+
974
+    /**
975
+     * @inheritdoc
976
+     */
977
+    public function getDefaultApps(): array {
978
+        $ids = $this->getNavigationManager()->getDefaultEntryIds();
979
+
980
+        return array_values(array_unique(array_map(function (string $id) {
981
+            $entry = $this->getNavigationManager()->get($id);
982
+            return (string)$entry['app'];
983
+        }, $ids)));
984
+    }
985
+
986
+    /**
987
+     * @inheritdoc
988
+     */
989
+    public function setDefaultApps(array $defaultApps): void {
990
+        $entries = $this->getNavigationManager()->getAll();
991
+        $ids = [];
992
+        foreach ($defaultApps as $defaultApp) {
993
+            foreach ($entries as $entry) {
994
+                if ((string)$entry['app'] === $defaultApp) {
995
+                    $ids[] = (string)$entry['id'];
996
+                    break;
997
+                }
998
+            }
999
+        }
1000
+        $this->getNavigationManager()->setDefaultEntryIds($ids);
1001
+    }
1002
+
1003
+    public function isBackendRequired(string $backend): bool {
1004
+        foreach ($this->appInfos as $appInfo) {
1005
+            if (
1006
+                isset($appInfo['dependencies']['backend'])
1007
+                && is_array($appInfo['dependencies']['backend'])
1008
+                && in_array($backend, $appInfo['dependencies']['backend'], true)
1009
+            ) {
1010
+                return true;
1011
+            }
1012
+        }
1013
+
1014
+        return false;
1015
+    }
1016
+
1017
+    /**
1018
+     * Clean the appId from forbidden characters
1019
+     *
1020
+     * @psalm-taint-escape callable
1021
+     * @psalm-taint-escape cookie
1022
+     * @psalm-taint-escape file
1023
+     * @psalm-taint-escape has_quotes
1024
+     * @psalm-taint-escape header
1025
+     * @psalm-taint-escape html
1026
+     * @psalm-taint-escape include
1027
+     * @psalm-taint-escape ldap
1028
+     * @psalm-taint-escape shell
1029
+     * @psalm-taint-escape sql
1030
+     * @psalm-taint-escape unserialize
1031
+     */
1032
+    public function cleanAppId(string $app): string {
1033
+        /* Only lowercase alphanumeric is allowed */
1034
+        return preg_replace('/(^[0-9_-]+|[^a-z0-9_-]+|[_-]+$)/', '', $app);
1035
+    }
1036
+
1037
+    /**
1038
+     * Run upgrade tasks for an app after the code has already been updated
1039
+     *
1040
+     * @throws AppPathNotFoundException if app folder can't be found
1041
+     */
1042
+    public function upgradeApp(string $appId): bool {
1043
+        // for apps distributed with core, we refresh app path in case the downloaded version
1044
+        // have been installed in custom apps and not in the default path
1045
+        $appPath = $this->getAppPath($appId, true);
1046
+
1047
+        $this->clearAppsCache();
1048
+        $l = \OC::$server->getL10N('core');
1049
+        $appData = $this->getAppInfo($appId, false, $l->getLanguageCode());
1050
+        if ($appData === null) {
1051
+            throw new AppPathNotFoundException('Could not find ' . $appId);
1052
+        }
1053
+
1054
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
1055
+        $ignoreMax = in_array($appId, $ignoreMaxApps, true);
1056
+        \OC_App::checkAppDependencies(
1057
+            $this->config,
1058
+            $l,
1059
+            $appData,
1060
+            $ignoreMax
1061
+        );
1062
+
1063
+        \OC_App::registerAutoloading($appId, $appPath, true);
1064
+        \OC_App::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
1065
+
1066
+        $ms = new MigrationService($appId, Server::get(\OC\DB\Connection::class));
1067
+        $ms->migrate();
1068
+
1069
+        \OC_App::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
1070
+        $queue = Server::get(IJobList::class);
1071
+        foreach ($appData['repair-steps']['live-migration'] as $step) {
1072
+            $queue->add(\OC\Migration\BackgroundRepair::class, [
1073
+                'app' => $appId,
1074
+                'step' => $step]);
1075
+        }
1076
+
1077
+        // update appversion in app manager
1078
+        $this->clearAppsCache();
1079
+        $this->getAppVersion($appId, false);
1080
+
1081
+        // Setup background jobs
1082
+        foreach ($appData['background-jobs'] as $job) {
1083
+            $queue->add($job);
1084
+        }
1085
+
1086
+        //set remote/public handlers
1087
+        foreach ($appData['remote'] as $name => $path) {
1088
+            $this->config->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
1089
+        }
1090
+        foreach ($appData['public'] as $name => $path) {
1091
+            $this->config->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1092
+        }
1093
+
1094
+        \OC_App::setAppTypes($appId);
1095
+
1096
+        $version = $this->getAppVersion($appId);
1097
+        $this->config->setAppValue($appId, 'installed_version', $version);
1098
+
1099
+        // migrate eventual new config keys in the process
1100
+        /** @psalm-suppress InternalMethod */
1101
+        $this->configManager->migrateConfigLexiconKeys($appId);
1102
+        $this->configManager->updateLexiconEntries($appId);
1103
+
1104
+        $this->dispatcher->dispatchTyped(new AppUpdateEvent($appId));
1105
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
1106
+            ManagerEvent::EVENT_APP_UPDATE, $appId
1107
+        ));
1108
+
1109
+        return true;
1110
+    }
1111
+
1112
+    public function isUpgradeRequired(string $appId): bool {
1113
+        $versions = $this->getAppInstalledVersions();
1114
+        $currentVersion = $this->getAppVersion($appId);
1115
+        if ($currentVersion && isset($versions[$appId])) {
1116
+            $installedVersion = $versions[$appId];
1117
+            if (!version_compare($currentVersion, $installedVersion, '=')) {
1118
+                return true;
1119
+            }
1120
+        }
1121
+        return false;
1122
+    }
1123
+
1124
+    public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool {
1125
+        return count($this->dependencyAnalyzer->analyzeServerVersion($serverVersion, $appInfo, $ignoreMax)) === 0;
1126
+    }
1127 1127
 }
Please login to merge, or discard this patch.
lib/private/App/InfoParser.php 1 patch
Indentation   +315 added lines, -315 removed lines patch added patch discarded remove patch
@@ -11,344 +11,344 @@
 block discarded – undo
11 11
 use function simplexml_load_string;
12 12
 
13 13
 class InfoParser {
14
-	/**
15
-	 * @param ICache|null $cache
16
-	 */
17
-	public function __construct(
18
-		private ?ICache $cache = null,
19
-	) {
20
-	}
14
+    /**
15
+     * @param ICache|null $cache
16
+     */
17
+    public function __construct(
18
+        private ?ICache $cache = null,
19
+    ) {
20
+    }
21 21
 
22
-	/**
23
-	 * @param string $file the xml file to be loaded
24
-	 * @return null|array where null is an indicator for an error
25
-	 */
26
-	public function parse(string $file): ?array {
27
-		if (!file_exists($file)) {
28
-			return null;
29
-		}
22
+    /**
23
+     * @param string $file the xml file to be loaded
24
+     * @return null|array where null is an indicator for an error
25
+     */
26
+    public function parse(string $file): ?array {
27
+        if (!file_exists($file)) {
28
+            return null;
29
+        }
30 30
 
31
-		if ($this->cache !== null) {
32
-			$fileCacheKey = $file . filemtime($file);
33
-			if ($cachedValue = $this->cache->get($fileCacheKey)) {
34
-				return json_decode($cachedValue, true);
35
-			}
36
-		}
31
+        if ($this->cache !== null) {
32
+            $fileCacheKey = $file . filemtime($file);
33
+            if ($cachedValue = $this->cache->get($fileCacheKey)) {
34
+                return json_decode($cachedValue, true);
35
+            }
36
+        }
37 37
 
38
-		libxml_use_internal_errors(true);
39
-		$xml = simplexml_load_string(file_get_contents($file));
38
+        libxml_use_internal_errors(true);
39
+        $xml = simplexml_load_string(file_get_contents($file));
40 40
 
41
-		if ($xml === false) {
42
-			libxml_clear_errors();
43
-			return null;
44
-		}
45
-		$array = $this->xmlToArray($xml);
41
+        if ($xml === false) {
42
+            libxml_clear_errors();
43
+            return null;
44
+        }
45
+        $array = $this->xmlToArray($xml);
46 46
 
47
-		if (is_string($array)) {
48
-			return null;
49
-		}
47
+        if (is_string($array)) {
48
+            return null;
49
+        }
50 50
 
51
-		if (!array_key_exists('info', $array)) {
52
-			$array['info'] = [];
53
-		}
54
-		if (!array_key_exists('remote', $array)) {
55
-			$array['remote'] = [];
56
-		}
57
-		if (!array_key_exists('public', $array)) {
58
-			$array['public'] = [];
59
-		}
60
-		if (!array_key_exists('types', $array)) {
61
-			$array['types'] = [];
62
-		}
63
-		if (!array_key_exists('repair-steps', $array)) {
64
-			$array['repair-steps'] = [];
65
-		}
66
-		if (!array_key_exists('install', $array['repair-steps'])) {
67
-			$array['repair-steps']['install'] = [];
68
-		}
69
-		if (!array_key_exists('pre-migration', $array['repair-steps'])) {
70
-			$array['repair-steps']['pre-migration'] = [];
71
-		}
72
-		if (!array_key_exists('post-migration', $array['repair-steps'])) {
73
-			$array['repair-steps']['post-migration'] = [];
74
-		}
75
-		if (!array_key_exists('live-migration', $array['repair-steps'])) {
76
-			$array['repair-steps']['live-migration'] = [];
77
-		}
78
-		if (!array_key_exists('uninstall', $array['repair-steps'])) {
79
-			$array['repair-steps']['uninstall'] = [];
80
-		}
81
-		if (!array_key_exists('background-jobs', $array)) {
82
-			$array['background-jobs'] = [];
83
-		}
84
-		if (!array_key_exists('two-factor-providers', $array)) {
85
-			$array['two-factor-providers'] = [];
86
-		}
87
-		if (!array_key_exists('commands', $array)) {
88
-			$array['commands'] = [];
89
-		}
90
-		if (!array_key_exists('activity', $array)) {
91
-			$array['activity'] = [];
92
-		}
93
-		if (!array_key_exists('filters', $array['activity'])) {
94
-			$array['activity']['filters'] = [];
95
-		}
96
-		if (!array_key_exists('settings', $array['activity'])) {
97
-			$array['activity']['settings'] = [];
98
-		}
99
-		if (!array_key_exists('providers', $array['activity'])) {
100
-			$array['activity']['providers'] = [];
101
-		}
102
-		if (!array_key_exists('settings', $array)) {
103
-			$array['settings'] = [];
104
-		}
105
-		if (!array_key_exists('admin', $array['settings'])) {
106
-			$array['settings']['admin'] = [];
107
-		}
108
-		if (!array_key_exists('admin-section', $array['settings'])) {
109
-			$array['settings']['admin-section'] = [];
110
-		}
111
-		if (!array_key_exists('personal', $array['settings'])) {
112
-			$array['settings']['personal'] = [];
113
-		}
114
-		if (!array_key_exists('personal-section', $array['settings'])) {
115
-			$array['settings']['personal-section'] = [];
116
-		}
117
-		if (!array_key_exists('dependencies', $array)) {
118
-			$array['dependencies'] = [];
119
-		}
120
-		if (!array_key_exists('backend', $array['dependencies'])) {
121
-			$array['dependencies']['backend'] = [];
122
-		}
51
+        if (!array_key_exists('info', $array)) {
52
+            $array['info'] = [];
53
+        }
54
+        if (!array_key_exists('remote', $array)) {
55
+            $array['remote'] = [];
56
+        }
57
+        if (!array_key_exists('public', $array)) {
58
+            $array['public'] = [];
59
+        }
60
+        if (!array_key_exists('types', $array)) {
61
+            $array['types'] = [];
62
+        }
63
+        if (!array_key_exists('repair-steps', $array)) {
64
+            $array['repair-steps'] = [];
65
+        }
66
+        if (!array_key_exists('install', $array['repair-steps'])) {
67
+            $array['repair-steps']['install'] = [];
68
+        }
69
+        if (!array_key_exists('pre-migration', $array['repair-steps'])) {
70
+            $array['repair-steps']['pre-migration'] = [];
71
+        }
72
+        if (!array_key_exists('post-migration', $array['repair-steps'])) {
73
+            $array['repair-steps']['post-migration'] = [];
74
+        }
75
+        if (!array_key_exists('live-migration', $array['repair-steps'])) {
76
+            $array['repair-steps']['live-migration'] = [];
77
+        }
78
+        if (!array_key_exists('uninstall', $array['repair-steps'])) {
79
+            $array['repair-steps']['uninstall'] = [];
80
+        }
81
+        if (!array_key_exists('background-jobs', $array)) {
82
+            $array['background-jobs'] = [];
83
+        }
84
+        if (!array_key_exists('two-factor-providers', $array)) {
85
+            $array['two-factor-providers'] = [];
86
+        }
87
+        if (!array_key_exists('commands', $array)) {
88
+            $array['commands'] = [];
89
+        }
90
+        if (!array_key_exists('activity', $array)) {
91
+            $array['activity'] = [];
92
+        }
93
+        if (!array_key_exists('filters', $array['activity'])) {
94
+            $array['activity']['filters'] = [];
95
+        }
96
+        if (!array_key_exists('settings', $array['activity'])) {
97
+            $array['activity']['settings'] = [];
98
+        }
99
+        if (!array_key_exists('providers', $array['activity'])) {
100
+            $array['activity']['providers'] = [];
101
+        }
102
+        if (!array_key_exists('settings', $array)) {
103
+            $array['settings'] = [];
104
+        }
105
+        if (!array_key_exists('admin', $array['settings'])) {
106
+            $array['settings']['admin'] = [];
107
+        }
108
+        if (!array_key_exists('admin-section', $array['settings'])) {
109
+            $array['settings']['admin-section'] = [];
110
+        }
111
+        if (!array_key_exists('personal', $array['settings'])) {
112
+            $array['settings']['personal'] = [];
113
+        }
114
+        if (!array_key_exists('personal-section', $array['settings'])) {
115
+            $array['settings']['personal-section'] = [];
116
+        }
117
+        if (!array_key_exists('dependencies', $array)) {
118
+            $array['dependencies'] = [];
119
+        }
120
+        if (!array_key_exists('backend', $array['dependencies'])) {
121
+            $array['dependencies']['backend'] = [];
122
+        }
123 123
 
124
-		if (array_key_exists('types', $array)) {
125
-			if (is_array($array['types'])) {
126
-				foreach ($array['types'] as $type => $v) {
127
-					unset($array['types'][$type]);
128
-					if (is_string($type)) {
129
-						$array['types'][] = $type;
130
-					}
131
-				}
132
-			} else {
133
-				$array['types'] = [];
134
-			}
135
-		}
136
-		if (isset($array['repair-steps']['install']['step']) && is_array($array['repair-steps']['install']['step'])) {
137
-			$array['repair-steps']['install'] = $array['repair-steps']['install']['step'];
138
-		}
139
-		if (isset($array['repair-steps']['pre-migration']['step']) && is_array($array['repair-steps']['pre-migration']['step'])) {
140
-			$array['repair-steps']['pre-migration'] = $array['repair-steps']['pre-migration']['step'];
141
-		}
142
-		if (isset($array['repair-steps']['post-migration']['step']) && is_array($array['repair-steps']['post-migration']['step'])) {
143
-			$array['repair-steps']['post-migration'] = $array['repair-steps']['post-migration']['step'];
144
-		}
145
-		if (isset($array['repair-steps']['live-migration']['step']) && is_array($array['repair-steps']['live-migration']['step'])) {
146
-			$array['repair-steps']['live-migration'] = $array['repair-steps']['live-migration']['step'];
147
-		}
148
-		if (isset($array['repair-steps']['uninstall']['step']) && is_array($array['repair-steps']['uninstall']['step'])) {
149
-			$array['repair-steps']['uninstall'] = $array['repair-steps']['uninstall']['step'];
150
-		}
151
-		if (isset($array['background-jobs']['job']) && is_array($array['background-jobs']['job'])) {
152
-			$array['background-jobs'] = $array['background-jobs']['job'];
153
-		}
154
-		if (isset($array['commands']['command']) && is_array($array['commands']['command'])) {
155
-			$array['commands'] = $array['commands']['command'];
156
-		}
157
-		if (isset($array['two-factor-providers']['provider']) && is_array($array['two-factor-providers']['provider'])) {
158
-			$array['two-factor-providers'] = $array['two-factor-providers']['provider'];
159
-		}
160
-		if (isset($array['activity']['filters']['filter']) && is_array($array['activity']['filters']['filter'])) {
161
-			$array['activity']['filters'] = $array['activity']['filters']['filter'];
162
-		}
163
-		if (isset($array['activity']['settings']['setting']) && is_array($array['activity']['settings']['setting'])) {
164
-			$array['activity']['settings'] = $array['activity']['settings']['setting'];
165
-		}
166
-		if (isset($array['activity']['providers']['provider']) && is_array($array['activity']['providers']['provider'])) {
167
-			$array['activity']['providers'] = $array['activity']['providers']['provider'];
168
-		}
169
-		if (isset($array['collaboration']['collaborators']['searchPlugins']['searchPlugin'])
170
-			&& is_array($array['collaboration']['collaborators']['searchPlugins']['searchPlugin'])
171
-			&& !isset($array['collaboration']['collaborators']['searchPlugins']['searchPlugin']['class'])
172
-		) {
173
-			$array['collaboration']['collaborators']['searchPlugins'] = $array['collaboration']['collaborators']['searchPlugins']['searchPlugin'];
174
-		}
175
-		if (isset($array['settings']['admin']) && !is_array($array['settings']['admin'])) {
176
-			$array['settings']['admin'] = [$array['settings']['admin']];
177
-		}
178
-		if (isset($array['settings']['admin-section']) && !is_array($array['settings']['admin-section'])) {
179
-			$array['settings']['admin-section'] = [$array['settings']['admin-section']];
180
-		}
181
-		if (isset($array['settings']['personal']) && !is_array($array['settings']['personal'])) {
182
-			$array['settings']['personal'] = [$array['settings']['personal']];
183
-		}
184
-		if (isset($array['settings']['personal-section']) && !is_array($array['settings']['personal-section'])) {
185
-			$array['settings']['personal-section'] = [$array['settings']['personal-section']];
186
-		}
187
-		if (isset($array['settings']['admin-delegation']) && !is_array($array['settings']['admin-delegation'])) {
188
-			$array['settings']['admin-delegation'] = [$array['settings']['admin-delegation']];
189
-		}
190
-		if (isset($array['settings']['admin-delegation-section']) && !is_array($array['settings']['admin-delegation-section'])) {
191
-			$array['settings']['admin-delegation-section'] = [$array['settings']['admin-delegation-section']];
192
-		}
193
-		if (isset($array['navigations']['navigation']) && $this->isNavigationItem($array['navigations']['navigation'])) {
194
-			$array['navigations']['navigation'] = [$array['navigations']['navigation']];
195
-		}
196
-		if (isset($array['dependencies']['backend']) && !is_array($array['dependencies']['backend'])) {
197
-			$array['dependencies']['backend'] = [$array['dependencies']['backend']];
198
-		}
124
+        if (array_key_exists('types', $array)) {
125
+            if (is_array($array['types'])) {
126
+                foreach ($array['types'] as $type => $v) {
127
+                    unset($array['types'][$type]);
128
+                    if (is_string($type)) {
129
+                        $array['types'][] = $type;
130
+                    }
131
+                }
132
+            } else {
133
+                $array['types'] = [];
134
+            }
135
+        }
136
+        if (isset($array['repair-steps']['install']['step']) && is_array($array['repair-steps']['install']['step'])) {
137
+            $array['repair-steps']['install'] = $array['repair-steps']['install']['step'];
138
+        }
139
+        if (isset($array['repair-steps']['pre-migration']['step']) && is_array($array['repair-steps']['pre-migration']['step'])) {
140
+            $array['repair-steps']['pre-migration'] = $array['repair-steps']['pre-migration']['step'];
141
+        }
142
+        if (isset($array['repair-steps']['post-migration']['step']) && is_array($array['repair-steps']['post-migration']['step'])) {
143
+            $array['repair-steps']['post-migration'] = $array['repair-steps']['post-migration']['step'];
144
+        }
145
+        if (isset($array['repair-steps']['live-migration']['step']) && is_array($array['repair-steps']['live-migration']['step'])) {
146
+            $array['repair-steps']['live-migration'] = $array['repair-steps']['live-migration']['step'];
147
+        }
148
+        if (isset($array['repair-steps']['uninstall']['step']) && is_array($array['repair-steps']['uninstall']['step'])) {
149
+            $array['repair-steps']['uninstall'] = $array['repair-steps']['uninstall']['step'];
150
+        }
151
+        if (isset($array['background-jobs']['job']) && is_array($array['background-jobs']['job'])) {
152
+            $array['background-jobs'] = $array['background-jobs']['job'];
153
+        }
154
+        if (isset($array['commands']['command']) && is_array($array['commands']['command'])) {
155
+            $array['commands'] = $array['commands']['command'];
156
+        }
157
+        if (isset($array['two-factor-providers']['provider']) && is_array($array['two-factor-providers']['provider'])) {
158
+            $array['two-factor-providers'] = $array['two-factor-providers']['provider'];
159
+        }
160
+        if (isset($array['activity']['filters']['filter']) && is_array($array['activity']['filters']['filter'])) {
161
+            $array['activity']['filters'] = $array['activity']['filters']['filter'];
162
+        }
163
+        if (isset($array['activity']['settings']['setting']) && is_array($array['activity']['settings']['setting'])) {
164
+            $array['activity']['settings'] = $array['activity']['settings']['setting'];
165
+        }
166
+        if (isset($array['activity']['providers']['provider']) && is_array($array['activity']['providers']['provider'])) {
167
+            $array['activity']['providers'] = $array['activity']['providers']['provider'];
168
+        }
169
+        if (isset($array['collaboration']['collaborators']['searchPlugins']['searchPlugin'])
170
+            && is_array($array['collaboration']['collaborators']['searchPlugins']['searchPlugin'])
171
+            && !isset($array['collaboration']['collaborators']['searchPlugins']['searchPlugin']['class'])
172
+        ) {
173
+            $array['collaboration']['collaborators']['searchPlugins'] = $array['collaboration']['collaborators']['searchPlugins']['searchPlugin'];
174
+        }
175
+        if (isset($array['settings']['admin']) && !is_array($array['settings']['admin'])) {
176
+            $array['settings']['admin'] = [$array['settings']['admin']];
177
+        }
178
+        if (isset($array['settings']['admin-section']) && !is_array($array['settings']['admin-section'])) {
179
+            $array['settings']['admin-section'] = [$array['settings']['admin-section']];
180
+        }
181
+        if (isset($array['settings']['personal']) && !is_array($array['settings']['personal'])) {
182
+            $array['settings']['personal'] = [$array['settings']['personal']];
183
+        }
184
+        if (isset($array['settings']['personal-section']) && !is_array($array['settings']['personal-section'])) {
185
+            $array['settings']['personal-section'] = [$array['settings']['personal-section']];
186
+        }
187
+        if (isset($array['settings']['admin-delegation']) && !is_array($array['settings']['admin-delegation'])) {
188
+            $array['settings']['admin-delegation'] = [$array['settings']['admin-delegation']];
189
+        }
190
+        if (isset($array['settings']['admin-delegation-section']) && !is_array($array['settings']['admin-delegation-section'])) {
191
+            $array['settings']['admin-delegation-section'] = [$array['settings']['admin-delegation-section']];
192
+        }
193
+        if (isset($array['navigations']['navigation']) && $this->isNavigationItem($array['navigations']['navigation'])) {
194
+            $array['navigations']['navigation'] = [$array['navigations']['navigation']];
195
+        }
196
+        if (isset($array['dependencies']['backend']) && !is_array($array['dependencies']['backend'])) {
197
+            $array['dependencies']['backend'] = [$array['dependencies']['backend']];
198
+        }
199 199
 
200
-		// Ensure some fields are always arrays
201
-		if (isset($array['screenshot']) && !is_array($array['screenshot'])) {
202
-			$array['screenshot'] = [$array['screenshot']];
203
-		}
204
-		if (isset($array['author']) && !is_array($array['author'])) {
205
-			$array['author'] = [$array['author']];
206
-		}
207
-		if (isset($array['category']) && !is_array($array['category'])) {
208
-			$array['category'] = [$array['category']];
209
-		}
200
+        // Ensure some fields are always arrays
201
+        if (isset($array['screenshot']) && !is_array($array['screenshot'])) {
202
+            $array['screenshot'] = [$array['screenshot']];
203
+        }
204
+        if (isset($array['author']) && !is_array($array['author'])) {
205
+            $array['author'] = [$array['author']];
206
+        }
207
+        if (isset($array['category']) && !is_array($array['category'])) {
208
+            $array['category'] = [$array['category']];
209
+        }
210 210
 
211
-		if ($this->cache !== null) {
212
-			$this->cache->set($fileCacheKey, json_encode($array));
213
-		}
214
-		return $array;
215
-	}
211
+        if ($this->cache !== null) {
212
+            $this->cache->set($fileCacheKey, json_encode($array));
213
+        }
214
+        return $array;
215
+    }
216 216
 
217
-	private function isNavigationItem(array $data): bool {
218
-		// Allow settings navigation items with no route entry
219
-		$type = $data['type'] ?? 'link';
220
-		if ($type === 'settings') {
221
-			return isset($data['name']);
222
-		}
223
-		return isset($data['name'], $data['route']);
224
-	}
217
+    private function isNavigationItem(array $data): bool {
218
+        // Allow settings navigation items with no route entry
219
+        $type = $data['type'] ?? 'link';
220
+        if ($type === 'settings') {
221
+            return isset($data['name']);
222
+        }
223
+        return isset($data['name'], $data['route']);
224
+    }
225 225
 
226
-	public function xmlToArray(\SimpleXMLElement $xml): array|string {
227
-		$children = $xml->children();
228
-		if ($children === null || count($children) === 0) {
229
-			return (string)$xml;
230
-		}
226
+    public function xmlToArray(\SimpleXMLElement $xml): array|string {
227
+        $children = $xml->children();
228
+        if ($children === null || count($children) === 0) {
229
+            return (string)$xml;
230
+        }
231 231
 
232
-		$array = [];
233
-		foreach ($children as $element => $node) {
234
-			if ($element === null) {
235
-				throw new \InvalidArgumentException('xml contains a null element');
236
-			}
237
-			$totalElement = count($xml->{$element});
232
+        $array = [];
233
+        foreach ($children as $element => $node) {
234
+            if ($element === null) {
235
+                throw new \InvalidArgumentException('xml contains a null element');
236
+            }
237
+            $totalElement = count($xml->{$element});
238 238
 
239
-			if (!isset($array[$element])) {
240
-				$array[$element] = $totalElement > 1 ? [] : '';
241
-			}
242
-			/** @var \SimpleXMLElement $node */
243
-			// Has attributes
244
-			if ($attributes = $node->attributes()) {
245
-				$data = [
246
-					'@attributes' => [],
247
-				];
248
-				$converted = $this->xmlToArray($node);
249
-				if (is_string($converted)) {
250
-					if (!empty($converted)) {
251
-						$data['@value'] = $converted;
252
-					}
253
-				} else {
254
-					$data = array_merge($data, $converted);
255
-				}
256
-				foreach ($attributes as $attr => $value) {
257
-					if ($attr === null) {
258
-						throw new \InvalidArgumentException('xml contains a null element');
259
-					}
260
-					$data['@attributes'][$attr] = (string)$value;
261
-				}
239
+            if (!isset($array[$element])) {
240
+                $array[$element] = $totalElement > 1 ? [] : '';
241
+            }
242
+            /** @var \SimpleXMLElement $node */
243
+            // Has attributes
244
+            if ($attributes = $node->attributes()) {
245
+                $data = [
246
+                    '@attributes' => [],
247
+                ];
248
+                $converted = $this->xmlToArray($node);
249
+                if (is_string($converted)) {
250
+                    if (!empty($converted)) {
251
+                        $data['@value'] = $converted;
252
+                    }
253
+                } else {
254
+                    $data = array_merge($data, $converted);
255
+                }
256
+                foreach ($attributes as $attr => $value) {
257
+                    if ($attr === null) {
258
+                        throw new \InvalidArgumentException('xml contains a null element');
259
+                    }
260
+                    $data['@attributes'][$attr] = (string)$value;
261
+                }
262 262
 
263
-				if ($totalElement > 1) {
264
-					$array[$element][] = $data;
265
-				} else {
266
-					$array[$element] = $data;
267
-				}
268
-				// Just a value
269
-			} else {
270
-				if ($totalElement > 1) {
271
-					$array[$element][] = $this->xmlToArray($node);
272
-				} else {
273
-					$array[$element] = $this->xmlToArray($node);
274
-				}
275
-			}
276
-		}
263
+                if ($totalElement > 1) {
264
+                    $array[$element][] = $data;
265
+                } else {
266
+                    $array[$element] = $data;
267
+                }
268
+                // Just a value
269
+            } else {
270
+                if ($totalElement > 1) {
271
+                    $array[$element][] = $this->xmlToArray($node);
272
+                } else {
273
+                    $array[$element] = $this->xmlToArray($node);
274
+                }
275
+            }
276
+        }
277 277
 
278
-		return $array;
279
-	}
278
+        return $array;
279
+    }
280 280
 
281
-	/**
282
-	 * Select the appropriate l10n version for fields name, summary and description
283
-	 */
284
-	public function applyL10N(array $data, ?string $lang = null): array {
285
-		if ($lang !== '' && $lang !== null) {
286
-			if (isset($data['name']) && is_array($data['name'])) {
287
-				$data['name'] = $this->findBestL10NOption($data['name'], $lang);
288
-			}
289
-			if (isset($data['summary']) && is_array($data['summary'])) {
290
-				$data['summary'] = $this->findBestL10NOption($data['summary'], $lang);
291
-			}
292
-			if (isset($data['description']) && is_array($data['description'])) {
293
-				$data['description'] = trim($this->findBestL10NOption($data['description'], $lang));
294
-			}
295
-		} elseif (isset($data['description']) && is_string($data['description'])) {
296
-			$data['description'] = trim($data['description']);
297
-		} else {
298
-			$data['description'] = '';
299
-		}
281
+    /**
282
+     * Select the appropriate l10n version for fields name, summary and description
283
+     */
284
+    public function applyL10N(array $data, ?string $lang = null): array {
285
+        if ($lang !== '' && $lang !== null) {
286
+            if (isset($data['name']) && is_array($data['name'])) {
287
+                $data['name'] = $this->findBestL10NOption($data['name'], $lang);
288
+            }
289
+            if (isset($data['summary']) && is_array($data['summary'])) {
290
+                $data['summary'] = $this->findBestL10NOption($data['summary'], $lang);
291
+            }
292
+            if (isset($data['description']) && is_array($data['description'])) {
293
+                $data['description'] = trim($this->findBestL10NOption($data['description'], $lang));
294
+            }
295
+        } elseif (isset($data['description']) && is_string($data['description'])) {
296
+            $data['description'] = trim($data['description']);
297
+        } else {
298
+            $data['description'] = '';
299
+        }
300 300
 
301
-		return $data;
302
-	}
301
+        return $data;
302
+    }
303 303
 
304
-	protected function findBestL10NOption(array $options, string $lang): string {
305
-		// only a single option
306
-		if (isset($options['@value'])) {
307
-			return $options['@value'];
308
-		}
304
+    protected function findBestL10NOption(array $options, string $lang): string {
305
+        // only a single option
306
+        if (isset($options['@value'])) {
307
+            return $options['@value'];
308
+        }
309 309
 
310
-		$fallback = $similarLangFallback = $englishFallback = false;
310
+        $fallback = $similarLangFallback = $englishFallback = false;
311 311
 
312
-		$lang = strtolower($lang);
313
-		$similarLang = $lang;
314
-		$pos = strpos($similarLang, '_');
315
-		if ($pos !== false && $pos > 0) {
316
-			// For "de_DE" we want to find "de" and the other way around
317
-			$similarLang = substr($lang, 0, $pos);
318
-		}
312
+        $lang = strtolower($lang);
313
+        $similarLang = $lang;
314
+        $pos = strpos($similarLang, '_');
315
+        if ($pos !== false && $pos > 0) {
316
+            // For "de_DE" we want to find "de" and the other way around
317
+            $similarLang = substr($lang, 0, $pos);
318
+        }
319 319
 
320
-		foreach ($options as $option) {
321
-			if (is_array($option)) {
322
-				if ($fallback === false) {
323
-					$fallback = $option['@value'];
324
-				}
320
+        foreach ($options as $option) {
321
+            if (is_array($option)) {
322
+                if ($fallback === false) {
323
+                    $fallback = $option['@value'];
324
+                }
325 325
 
326
-				if (!isset($option['@attributes']['lang'])) {
327
-					continue;
328
-				}
326
+                if (!isset($option['@attributes']['lang'])) {
327
+                    continue;
328
+                }
329 329
 
330
-				$attributeLang = strtolower($option['@attributes']['lang']);
331
-				if ($attributeLang === $lang) {
332
-					return $option['@value'];
333
-				}
330
+                $attributeLang = strtolower($option['@attributes']['lang']);
331
+                if ($attributeLang === $lang) {
332
+                    return $option['@value'];
333
+                }
334 334
 
335
-				if ($attributeLang === $similarLang) {
336
-					$similarLangFallback = $option['@value'];
337
-				} elseif (str_starts_with($attributeLang, $similarLang . '_')) {
338
-					if ($similarLangFallback === false) {
339
-						$similarLangFallback = $option['@value'];
340
-					}
341
-				}
342
-			} else {
343
-				$englishFallback = $option;
344
-			}
345
-		}
335
+                if ($attributeLang === $similarLang) {
336
+                    $similarLangFallback = $option['@value'];
337
+                } elseif (str_starts_with($attributeLang, $similarLang . '_')) {
338
+                    if ($similarLangFallback === false) {
339
+                        $similarLangFallback = $option['@value'];
340
+                    }
341
+                }
342
+            } else {
343
+                $englishFallback = $option;
344
+            }
345
+        }
346 346
 
347
-		if ($similarLangFallback !== false) {
348
-			return $similarLangFallback;
349
-		} elseif ($englishFallback !== false) {
350
-			return $englishFallback;
351
-		}
352
-		return (string)$fallback;
353
-	}
347
+        if ($similarLangFallback !== false) {
348
+            return $similarLangFallback;
349
+        } elseif ($englishFallback !== false) {
350
+            return $englishFallback;
351
+        }
352
+        return (string)$fallback;
353
+    }
354 354
 }
Please login to merge, or discard this patch.
lib/private/Settings/Manager.php 1 patch
Indentation   +325 added lines, -325 removed lines patch added patch discarded remove patch
@@ -24,329 +24,329 @@
 block discarded – undo
24 24
 use Psr\Log\LoggerInterface;
25 25
 
26 26
 class Manager implements IManager {
27
-	private ?IL10N $l = null;
28
-
29
-	/** @var array<self::SETTINGS_*, list<class-string<IIconSection>>> */
30
-	protected array $sectionClasses = [];
31
-
32
-	/** @var array<self::SETTINGS_*, array<string, IIconSection>> */
33
-	protected array $sections = [];
34
-
35
-	/** @var array<class-string<ISettings>, self::SETTINGS_*> */
36
-	protected array $settingClasses = [];
37
-
38
-	/** @var array<self::SETTINGS_*, array<string, list<ISettings>>> */
39
-	protected array $settings = [];
40
-
41
-	public function __construct(
42
-		private LoggerInterface $log,
43
-		private IFactory $l10nFactory,
44
-		private IURLGenerator $url,
45
-		private IServerContainer $container,
46
-		private AuthorizedGroupMapper $mapper,
47
-		private IGroupManager $groupManager,
48
-		private ISubAdmin $subAdmin,
49
-	) {
50
-	}
51
-
52
-	/**
53
-	 * @inheritdoc
54
-	 */
55
-	public function registerSection(string $type, string $section) {
56
-		if (!isset($this->sectionClasses[$type])) {
57
-			$this->sectionClasses[$type] = [];
58
-		}
59
-
60
-		$this->sectionClasses[$type][] = $section;
61
-	}
62
-
63
-	/**
64
-	 * @psalm-param self::SETTINGS_* $type
65
-	 *
66
-	 * @return IIconSection[]
67
-	 */
68
-	protected function getSections(string $type): array {
69
-		if (!isset($this->sections[$type])) {
70
-			$this->sections[$type] = [];
71
-		}
72
-
73
-		if (!isset($this->sectionClasses[$type])) {
74
-			return $this->sections[$type];
75
-		}
76
-
77
-		foreach (array_unique($this->sectionClasses[$type]) as $index => $class) {
78
-			try {
79
-				/** @var IIconSection $section */
80
-				$section = $this->container->get($class);
81
-			} catch (QueryException $e) {
82
-				$this->log->info($e->getMessage(), ['exception' => $e]);
83
-				continue;
84
-			}
85
-
86
-			$sectionID = $section->getID();
87
-
88
-			if (!$this->isKnownDuplicateSectionId($sectionID) && isset($this->sections[$type][$sectionID])) {
89
-				$e = new \InvalidArgumentException('Section with the same ID already registered: ' . $sectionID . ', class: ' . $class);
90
-				$this->log->info($e->getMessage(), ['exception' => $e]);
91
-				continue;
92
-			}
93
-
94
-			$this->sections[$type][$sectionID] = $section;
95
-
96
-			unset($this->sectionClasses[$type][$index]);
97
-		}
98
-
99
-		return $this->sections[$type];
100
-	}
101
-
102
-	/**
103
-	 * @inheritdoc
104
-	 */
105
-	public function getSection(string $type, string $sectionId): ?IIconSection {
106
-		if (isset($this->sections[$type]) && isset($this->sections[$type][$sectionId])) {
107
-			return $this->sections[$type][$sectionId];
108
-		}
109
-		return null;
110
-	}
111
-
112
-	protected function isKnownDuplicateSectionId(string $sectionID): bool {
113
-		return in_array($sectionID, [
114
-			'connected-accounts',
115
-			'notifications',
116
-		], true);
117
-	}
118
-
119
-	/**
120
-	 * @inheritdoc
121
-	 */
122
-	public function registerSetting(string $type, string $setting) {
123
-		$this->settingClasses[$setting] = $type;
124
-	}
125
-
126
-	/**
127
-	 * @psalm-param self::SETTINGS_* $type The type of the setting.
128
-	 * @param string $section
129
-	 * @param ?Closure $filter optional filter to apply on all loaded ISettings
130
-	 *
131
-	 * @return ISettings[]
132
-	 */
133
-	protected function getSettings(string $type, string $section, ?Closure $filter = null): array {
134
-		if (!isset($this->settings[$type])) {
135
-			$this->settings[$type] = [];
136
-		}
137
-		if (!isset($this->settings[$type][$section])) {
138
-			$this->settings[$type][$section] = [];
139
-
140
-			foreach ($this->settingClasses as $class => $settingsType) {
141
-				if ($type !== $settingsType) {
142
-					continue;
143
-				}
144
-
145
-				try {
146
-					/** @var ISettings $setting */
147
-					$setting = $this->container->get($class);
148
-				} catch (QueryException $e) {
149
-					$this->log->info($e->getMessage(), ['exception' => $e]);
150
-					continue;
151
-				}
152
-
153
-				if (!$setting instanceof ISettings) {
154
-					$e = new \InvalidArgumentException('Invalid settings setting registered (' . $class . ')');
155
-					$this->log->info($e->getMessage(), ['exception' => $e]);
156
-					continue;
157
-				}
158
-				$settingSection = $setting->getSection();
159
-				if ($settingSection === null) {
160
-					continue;
161
-				}
162
-
163
-				if (!isset($this->settings[$settingsType][$settingSection])) {
164
-					$this->settings[$settingsType][$settingSection] = [];
165
-				}
166
-				$this->settings[$settingsType][$settingSection][] = $setting;
167
-
168
-				unset($this->settingClasses[$class]);
169
-			}
170
-		}
171
-
172
-		if ($filter !== null) {
173
-			return array_values(array_filter($this->settings[$type][$section], $filter));
174
-		}
175
-
176
-		return $this->settings[$type][$section];
177
-	}
178
-
179
-	/**
180
-	 * @inheritdoc
181
-	 */
182
-	public function getAdminSections(): array {
183
-		// built-in sections
184
-		$sections = [];
185
-
186
-		$appSections = $this->getSections('admin');
187
-
188
-		foreach ($appSections as $section) {
189
-			/** @var IIconSection $section */
190
-			if (!isset($sections[$section->getPriority()])) {
191
-				$sections[$section->getPriority()] = [];
192
-			}
193
-
194
-			$sections[$section->getPriority()][] = $section;
195
-		}
196
-
197
-		ksort($sections);
198
-
199
-		return $sections;
200
-	}
201
-
202
-	/**
203
-	 * @inheritdoc
204
-	 */
205
-	public function getAdminSettings(string $section, bool $subAdminOnly = false): array {
206
-		if ($subAdminOnly) {
207
-			$subAdminSettingsFilter = function (ISettings $settings) {
208
-				return $settings instanceof ISubAdminSettings;
209
-			};
210
-			$appSettings = $this->getSettings('admin', $section, $subAdminSettingsFilter);
211
-		} else {
212
-			$appSettings = $this->getSettings('admin', $section);
213
-		}
214
-
215
-		$settings = [];
216
-		foreach ($appSettings as $setting) {
217
-			if (!isset($settings[$setting->getPriority()])) {
218
-				$settings[$setting->getPriority()] = [];
219
-			}
220
-			$settings[$setting->getPriority()][] = $setting;
221
-		}
222
-
223
-		ksort($settings);
224
-		return $settings;
225
-	}
226
-
227
-	/**
228
-	 * @inheritdoc
229
-	 */
230
-	public function getPersonalSections(): array {
231
-		if ($this->l === null) {
232
-			$this->l = $this->l10nFactory->get('lib');
233
-		}
234
-
235
-		$sections = [];
236
-
237
-		if (count($this->getPersonalSettings('additional')) > 1) {
238
-			$sections[98] = [new Section('additional', $this->l->t('Additional settings'), 0, $this->url->imagePath('core', 'actions/settings-dark.svg'))];
239
-		}
240
-
241
-		$appSections = $this->getSections('personal');
242
-
243
-		foreach ($appSections as $section) {
244
-			/** @var IIconSection $section */
245
-			if (!isset($sections[$section->getPriority()])) {
246
-				$sections[$section->getPriority()] = [];
247
-			}
248
-
249
-			$sections[$section->getPriority()][] = $section;
250
-		}
251
-
252
-		ksort($sections);
253
-
254
-		return $sections;
255
-	}
256
-
257
-	/**
258
-	 * @inheritdoc
259
-	 */
260
-	public function getPersonalSettings(string $section): array {
261
-		$settings = [];
262
-		$appSettings = $this->getSettings('personal', $section);
263
-
264
-		foreach ($appSettings as $setting) {
265
-			if (!isset($settings[$setting->getPriority()])) {
266
-				$settings[$setting->getPriority()] = [];
267
-			}
268
-			$settings[$setting->getPriority()][] = $setting;
269
-		}
270
-
271
-		ksort($settings);
272
-		return $settings;
273
-	}
274
-
275
-	/**
276
-	 * @inheritdoc
277
-	 */
278
-	public function getAllowedAdminSettings(string $section, IUser $user): array {
279
-		$isAdmin = $this->groupManager->isAdmin($user->getUID());
280
-		if ($isAdmin) {
281
-			$appSettings = $this->getSettings('admin', $section);
282
-		} else {
283
-			$authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user);
284
-			if ($this->subAdmin->isSubAdmin($user)) {
285
-				$authorizedGroupFilter = function (ISettings $settings) use ($authorizedSettingsClasses) {
286
-					return $settings instanceof ISubAdminSettings
287
-						|| in_array(get_class($settings), $authorizedSettingsClasses) === true;
288
-				};
289
-			} else {
290
-				$authorizedGroupFilter = function (ISettings $settings) use ($authorizedSettingsClasses) {
291
-					return in_array(get_class($settings), $authorizedSettingsClasses) === true;
292
-				};
293
-			}
294
-			$appSettings = $this->getSettings('admin', $section, $authorizedGroupFilter);
295
-		}
296
-
297
-		$settings = [];
298
-		foreach ($appSettings as $setting) {
299
-			if (!isset($settings[$setting->getPriority()])) {
300
-				$settings[$setting->getPriority()] = [];
301
-			}
302
-			$settings[$setting->getPriority()][] = $setting;
303
-		}
304
-
305
-		ksort($settings);
306
-		return $settings;
307
-	}
308
-
309
-	/**
310
-	 * @inheritdoc
311
-	 */
312
-	public function getAllAllowedAdminSettings(IUser $user): array {
313
-		$this->getSettings('admin', ''); // Make sure all the settings are loaded
314
-		$settings = [];
315
-		$authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user);
316
-		foreach ($this->settings['admin'] as $section) {
317
-			foreach ($section as $setting) {
318
-				if (in_array(get_class($setting), $authorizedSettingsClasses) === true) {
319
-					$settings[] = $setting;
320
-				}
321
-			}
322
-		}
323
-		return $settings;
324
-	}
325
-
326
-	/**
327
-	 * @return array<string, array{section:IIconSection,settings:list<IDelegatedSettings>}>
328
-	 */
329
-	public function getAdminDelegatedSettings(): array {
330
-		$sections = $this->getAdminSections();
331
-		$settings = [];
332
-		foreach ($sections as $sectionPriority) {
333
-			foreach ($sectionPriority as $section) {
334
-				/** @var IDelegatedSettings[] */
335
-				$sectionSettings = array_merge(
336
-					$this->getSettings(self::SETTINGS_ADMIN, $section->getID(), fn (ISettings $settings): bool => $settings instanceof IDelegatedSettings),
337
-					$this->getSettings(self::SETTINGS_DELEGATION, $section->getID(), fn (ISettings $settings): bool => $settings instanceof IDelegatedSettings),
338
-				);
339
-				usort(
340
-					$sectionSettings,
341
-					fn (ISettings $s1, ISettings $s2) => $s1->getPriority() <=> $s2->getPriority()
342
-				);
343
-				$settings[$section->getID()] = [
344
-					'section' => $section,
345
-					'settings' => $sectionSettings,
346
-				];
347
-			}
348
-		}
349
-		uasort($settings, fn (array $a, array $b) => $a['section']->getPriority() <=> $b['section']->getPriority());
350
-		return $settings;
351
-	}
27
+    private ?IL10N $l = null;
28
+
29
+    /** @var array<self::SETTINGS_*, list<class-string<IIconSection>>> */
30
+    protected array $sectionClasses = [];
31
+
32
+    /** @var array<self::SETTINGS_*, array<string, IIconSection>> */
33
+    protected array $sections = [];
34
+
35
+    /** @var array<class-string<ISettings>, self::SETTINGS_*> */
36
+    protected array $settingClasses = [];
37
+
38
+    /** @var array<self::SETTINGS_*, array<string, list<ISettings>>> */
39
+    protected array $settings = [];
40
+
41
+    public function __construct(
42
+        private LoggerInterface $log,
43
+        private IFactory $l10nFactory,
44
+        private IURLGenerator $url,
45
+        private IServerContainer $container,
46
+        private AuthorizedGroupMapper $mapper,
47
+        private IGroupManager $groupManager,
48
+        private ISubAdmin $subAdmin,
49
+    ) {
50
+    }
51
+
52
+    /**
53
+     * @inheritdoc
54
+     */
55
+    public function registerSection(string $type, string $section) {
56
+        if (!isset($this->sectionClasses[$type])) {
57
+            $this->sectionClasses[$type] = [];
58
+        }
59
+
60
+        $this->sectionClasses[$type][] = $section;
61
+    }
62
+
63
+    /**
64
+     * @psalm-param self::SETTINGS_* $type
65
+     *
66
+     * @return IIconSection[]
67
+     */
68
+    protected function getSections(string $type): array {
69
+        if (!isset($this->sections[$type])) {
70
+            $this->sections[$type] = [];
71
+        }
72
+
73
+        if (!isset($this->sectionClasses[$type])) {
74
+            return $this->sections[$type];
75
+        }
76
+
77
+        foreach (array_unique($this->sectionClasses[$type]) as $index => $class) {
78
+            try {
79
+                /** @var IIconSection $section */
80
+                $section = $this->container->get($class);
81
+            } catch (QueryException $e) {
82
+                $this->log->info($e->getMessage(), ['exception' => $e]);
83
+                continue;
84
+            }
85
+
86
+            $sectionID = $section->getID();
87
+
88
+            if (!$this->isKnownDuplicateSectionId($sectionID) && isset($this->sections[$type][$sectionID])) {
89
+                $e = new \InvalidArgumentException('Section with the same ID already registered: ' . $sectionID . ', class: ' . $class);
90
+                $this->log->info($e->getMessage(), ['exception' => $e]);
91
+                continue;
92
+            }
93
+
94
+            $this->sections[$type][$sectionID] = $section;
95
+
96
+            unset($this->sectionClasses[$type][$index]);
97
+        }
98
+
99
+        return $this->sections[$type];
100
+    }
101
+
102
+    /**
103
+     * @inheritdoc
104
+     */
105
+    public function getSection(string $type, string $sectionId): ?IIconSection {
106
+        if (isset($this->sections[$type]) && isset($this->sections[$type][$sectionId])) {
107
+            return $this->sections[$type][$sectionId];
108
+        }
109
+        return null;
110
+    }
111
+
112
+    protected function isKnownDuplicateSectionId(string $sectionID): bool {
113
+        return in_array($sectionID, [
114
+            'connected-accounts',
115
+            'notifications',
116
+        ], true);
117
+    }
118
+
119
+    /**
120
+     * @inheritdoc
121
+     */
122
+    public function registerSetting(string $type, string $setting) {
123
+        $this->settingClasses[$setting] = $type;
124
+    }
125
+
126
+    /**
127
+     * @psalm-param self::SETTINGS_* $type The type of the setting.
128
+     * @param string $section
129
+     * @param ?Closure $filter optional filter to apply on all loaded ISettings
130
+     *
131
+     * @return ISettings[]
132
+     */
133
+    protected function getSettings(string $type, string $section, ?Closure $filter = null): array {
134
+        if (!isset($this->settings[$type])) {
135
+            $this->settings[$type] = [];
136
+        }
137
+        if (!isset($this->settings[$type][$section])) {
138
+            $this->settings[$type][$section] = [];
139
+
140
+            foreach ($this->settingClasses as $class => $settingsType) {
141
+                if ($type !== $settingsType) {
142
+                    continue;
143
+                }
144
+
145
+                try {
146
+                    /** @var ISettings $setting */
147
+                    $setting = $this->container->get($class);
148
+                } catch (QueryException $e) {
149
+                    $this->log->info($e->getMessage(), ['exception' => $e]);
150
+                    continue;
151
+                }
152
+
153
+                if (!$setting instanceof ISettings) {
154
+                    $e = new \InvalidArgumentException('Invalid settings setting registered (' . $class . ')');
155
+                    $this->log->info($e->getMessage(), ['exception' => $e]);
156
+                    continue;
157
+                }
158
+                $settingSection = $setting->getSection();
159
+                if ($settingSection === null) {
160
+                    continue;
161
+                }
162
+
163
+                if (!isset($this->settings[$settingsType][$settingSection])) {
164
+                    $this->settings[$settingsType][$settingSection] = [];
165
+                }
166
+                $this->settings[$settingsType][$settingSection][] = $setting;
167
+
168
+                unset($this->settingClasses[$class]);
169
+            }
170
+        }
171
+
172
+        if ($filter !== null) {
173
+            return array_values(array_filter($this->settings[$type][$section], $filter));
174
+        }
175
+
176
+        return $this->settings[$type][$section];
177
+    }
178
+
179
+    /**
180
+     * @inheritdoc
181
+     */
182
+    public function getAdminSections(): array {
183
+        // built-in sections
184
+        $sections = [];
185
+
186
+        $appSections = $this->getSections('admin');
187
+
188
+        foreach ($appSections as $section) {
189
+            /** @var IIconSection $section */
190
+            if (!isset($sections[$section->getPriority()])) {
191
+                $sections[$section->getPriority()] = [];
192
+            }
193
+
194
+            $sections[$section->getPriority()][] = $section;
195
+        }
196
+
197
+        ksort($sections);
198
+
199
+        return $sections;
200
+    }
201
+
202
+    /**
203
+     * @inheritdoc
204
+     */
205
+    public function getAdminSettings(string $section, bool $subAdminOnly = false): array {
206
+        if ($subAdminOnly) {
207
+            $subAdminSettingsFilter = function (ISettings $settings) {
208
+                return $settings instanceof ISubAdminSettings;
209
+            };
210
+            $appSettings = $this->getSettings('admin', $section, $subAdminSettingsFilter);
211
+        } else {
212
+            $appSettings = $this->getSettings('admin', $section);
213
+        }
214
+
215
+        $settings = [];
216
+        foreach ($appSettings as $setting) {
217
+            if (!isset($settings[$setting->getPriority()])) {
218
+                $settings[$setting->getPriority()] = [];
219
+            }
220
+            $settings[$setting->getPriority()][] = $setting;
221
+        }
222
+
223
+        ksort($settings);
224
+        return $settings;
225
+    }
226
+
227
+    /**
228
+     * @inheritdoc
229
+     */
230
+    public function getPersonalSections(): array {
231
+        if ($this->l === null) {
232
+            $this->l = $this->l10nFactory->get('lib');
233
+        }
234
+
235
+        $sections = [];
236
+
237
+        if (count($this->getPersonalSettings('additional')) > 1) {
238
+            $sections[98] = [new Section('additional', $this->l->t('Additional settings'), 0, $this->url->imagePath('core', 'actions/settings-dark.svg'))];
239
+        }
240
+
241
+        $appSections = $this->getSections('personal');
242
+
243
+        foreach ($appSections as $section) {
244
+            /** @var IIconSection $section */
245
+            if (!isset($sections[$section->getPriority()])) {
246
+                $sections[$section->getPriority()] = [];
247
+            }
248
+
249
+            $sections[$section->getPriority()][] = $section;
250
+        }
251
+
252
+        ksort($sections);
253
+
254
+        return $sections;
255
+    }
256
+
257
+    /**
258
+     * @inheritdoc
259
+     */
260
+    public function getPersonalSettings(string $section): array {
261
+        $settings = [];
262
+        $appSettings = $this->getSettings('personal', $section);
263
+
264
+        foreach ($appSettings as $setting) {
265
+            if (!isset($settings[$setting->getPriority()])) {
266
+                $settings[$setting->getPriority()] = [];
267
+            }
268
+            $settings[$setting->getPriority()][] = $setting;
269
+        }
270
+
271
+        ksort($settings);
272
+        return $settings;
273
+    }
274
+
275
+    /**
276
+     * @inheritdoc
277
+     */
278
+    public function getAllowedAdminSettings(string $section, IUser $user): array {
279
+        $isAdmin = $this->groupManager->isAdmin($user->getUID());
280
+        if ($isAdmin) {
281
+            $appSettings = $this->getSettings('admin', $section);
282
+        } else {
283
+            $authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user);
284
+            if ($this->subAdmin->isSubAdmin($user)) {
285
+                $authorizedGroupFilter = function (ISettings $settings) use ($authorizedSettingsClasses) {
286
+                    return $settings instanceof ISubAdminSettings
287
+                        || in_array(get_class($settings), $authorizedSettingsClasses) === true;
288
+                };
289
+            } else {
290
+                $authorizedGroupFilter = function (ISettings $settings) use ($authorizedSettingsClasses) {
291
+                    return in_array(get_class($settings), $authorizedSettingsClasses) === true;
292
+                };
293
+            }
294
+            $appSettings = $this->getSettings('admin', $section, $authorizedGroupFilter);
295
+        }
296
+
297
+        $settings = [];
298
+        foreach ($appSettings as $setting) {
299
+            if (!isset($settings[$setting->getPriority()])) {
300
+                $settings[$setting->getPriority()] = [];
301
+            }
302
+            $settings[$setting->getPriority()][] = $setting;
303
+        }
304
+
305
+        ksort($settings);
306
+        return $settings;
307
+    }
308
+
309
+    /**
310
+     * @inheritdoc
311
+     */
312
+    public function getAllAllowedAdminSettings(IUser $user): array {
313
+        $this->getSettings('admin', ''); // Make sure all the settings are loaded
314
+        $settings = [];
315
+        $authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user);
316
+        foreach ($this->settings['admin'] as $section) {
317
+            foreach ($section as $setting) {
318
+                if (in_array(get_class($setting), $authorizedSettingsClasses) === true) {
319
+                    $settings[] = $setting;
320
+                }
321
+            }
322
+        }
323
+        return $settings;
324
+    }
325
+
326
+    /**
327
+     * @return array<string, array{section:IIconSection,settings:list<IDelegatedSettings>}>
328
+     */
329
+    public function getAdminDelegatedSettings(): array {
330
+        $sections = $this->getAdminSections();
331
+        $settings = [];
332
+        foreach ($sections as $sectionPriority) {
333
+            foreach ($sectionPriority as $section) {
334
+                /** @var IDelegatedSettings[] */
335
+                $sectionSettings = array_merge(
336
+                    $this->getSettings(self::SETTINGS_ADMIN, $section->getID(), fn (ISettings $settings): bool => $settings instanceof IDelegatedSettings),
337
+                    $this->getSettings(self::SETTINGS_DELEGATION, $section->getID(), fn (ISettings $settings): bool => $settings instanceof IDelegatedSettings),
338
+                );
339
+                usort(
340
+                    $sectionSettings,
341
+                    fn (ISettings $s1, ISettings $s2) => $s1->getPriority() <=> $s2->getPriority()
342
+                );
343
+                $settings[$section->getID()] = [
344
+                    'section' => $section,
345
+                    'settings' => $sectionSettings,
346
+                ];
347
+            }
348
+        }
349
+        uasort($settings, fn (array $a, array $b) => $a['section']->getPriority() <=> $b['section']->getPriority());
350
+        return $settings;
351
+    }
352 352
 }
Please login to merge, or discard this patch.
lib/public/Settings/IManager.php 1 patch
Indentation   +118 added lines, -118 removed lines patch added patch discarded remove patch
@@ -13,122 +13,122 @@
 block discarded – undo
13 13
  * @since 9.1
14 14
  */
15 15
 interface IManager {
16
-	/**
17
-	 * @since 9.1.0
18
-	 * @deprecated 29.0.0 Use {@see self::SETTINGS_ADMIN} instead
19
-	 */
20
-	public const KEY_ADMIN_SETTINGS = 'admin';
21
-
22
-	/**
23
-	 * @since 9.1.0
24
-	 * @deprecated 29.0.0 Use {@see self::SETTINGS_ADMIN} instead
25
-	 */
26
-	public const KEY_ADMIN_SECTION = 'admin-section';
27
-
28
-	/**
29
-	 * @since 13.0.0
30
-	 * @deprecated 29.0.0 Use {@see self::SETTINGS_PERSONAL} instead
31
-	 */
32
-	public const KEY_PERSONAL_SETTINGS = 'personal';
33
-
34
-	/**
35
-	 * @since 13.0.0
36
-	 * @deprecated 29.0.0 Use {@see self::SETTINGS_PERSONAL} instead
37
-	 */
38
-	public const KEY_PERSONAL_SECTION = 'personal-section';
39
-
40
-	/**
41
-	 * @since 29.0.0
42
-	 */
43
-	public const SETTINGS_ADMIN = 'admin';
44
-
45
-	/**
46
-	 * @since 29.0.0
47
-	 */
48
-	public const SETTINGS_PERSONAL = 'personal';
49
-
50
-	/**
51
-	 * @since 33.0.0
52
-	 * For settings only used for delegation but not appearing in settings menu
53
-	 */
54
-	public const SETTINGS_DELEGATION = 'delegation';
55
-
56
-	/**
57
-	 * @psalm-param self::SETTINGS_* $type
58
-	 * @param class-string<IIconSection> $section
59
-	 * @since 14.0.0
60
-	 */
61
-	public function registerSection(string $type, string $section);
62
-
63
-	/**
64
-	 * @psalm-param self::SETTINGS_* $type
65
-	 * @param class-string<ISettings> $setting
66
-	 * @since 14.0.0
67
-	 */
68
-	public function registerSetting(string $type, string $setting);
69
-
70
-	/**
71
-	 * returns a list of the admin sections
72
-	 *
73
-	 * @return array<int, list<IIconSection>> list of sections with priority as key
74
-	 * @since 9.1.0
75
-	 */
76
-	public function getAdminSections(): array;
77
-
78
-	/**
79
-	 * returns a list of the personal sections
80
-	 *
81
-	 * @return array<int, list<IIconSection>> list of sections with priority as key
82
-	 * @since 13.0.0
83
-	 */
84
-	public function getPersonalSections(): array;
85
-
86
-	/**
87
-	 * returns a list of the admin settings
88
-	 *
89
-	 * @param string $section the section id for which to load the settings
90
-	 * @param bool $subAdminOnly only return settings sub admins are supposed to see (since 17.0.0)
91
-	 * @return array<int, list<ISettings>> list of settings with priority as key
92
-	 * @since 9.1.0
93
-	 */
94
-	public function getAdminSettings(string $section, bool $subAdminOnly = false): array;
95
-
96
-	/**
97
-	 * Returns a list of admin settings that the given user can use for the give section
98
-	 *
99
-	 * @return array<int, list<ISettings>> List of admin-settings the user has access to, with priority as key.
100
-	 * @since 23.0.0
101
-	 */
102
-	public function getAllowedAdminSettings(string $section, IUser $user): array;
103
-
104
-	/**
105
-	 * Returns a list of admin settings that the given user can use.
106
-	 *
107
-	 * @return list<ISettings> The array of admin settings there admin delegation is allowed.
108
-	 * @since 23.0.0
109
-	 */
110
-	public function getAllAllowedAdminSettings(IUser $user): array;
111
-
112
-	/**
113
-	 * returns a list of the personal  settings
114
-	 *
115
-	 * @param string $section the section id for which to load the settings
116
-	 * @return array<int, list<ISettings>> list of settings with priority as key
117
-	 * @since 13.0.0
118
-	 */
119
-	public function getPersonalSettings(string $section): array;
120
-
121
-	/**
122
-	 * Get a specific section by type and id
123
-	 * @psalm-param self::SETTINGS_* $type
124
-	 * @since 25.0.0
125
-	 */
126
-	public function getSection(string $type, string $sectionId): ?IIconSection;
127
-
128
-	/**
129
-	 * Return admin delegated settings, sorted by priority and grouped by section
130
-	 * @return array<string, array{section:IIconSection,settings:list<IDelegatedSettings>}>
131
-	 * @since 33.0.0
132
-	 */
133
-	public function getAdminDelegatedSettings(): array;
16
+    /**
17
+     * @since 9.1.0
18
+     * @deprecated 29.0.0 Use {@see self::SETTINGS_ADMIN} instead
19
+     */
20
+    public const KEY_ADMIN_SETTINGS = 'admin';
21
+
22
+    /**
23
+     * @since 9.1.0
24
+     * @deprecated 29.0.0 Use {@see self::SETTINGS_ADMIN} instead
25
+     */
26
+    public const KEY_ADMIN_SECTION = 'admin-section';
27
+
28
+    /**
29
+     * @since 13.0.0
30
+     * @deprecated 29.0.0 Use {@see self::SETTINGS_PERSONAL} instead
31
+     */
32
+    public const KEY_PERSONAL_SETTINGS = 'personal';
33
+
34
+    /**
35
+     * @since 13.0.0
36
+     * @deprecated 29.0.0 Use {@see self::SETTINGS_PERSONAL} instead
37
+     */
38
+    public const KEY_PERSONAL_SECTION = 'personal-section';
39
+
40
+    /**
41
+     * @since 29.0.0
42
+     */
43
+    public const SETTINGS_ADMIN = 'admin';
44
+
45
+    /**
46
+     * @since 29.0.0
47
+     */
48
+    public const SETTINGS_PERSONAL = 'personal';
49
+
50
+    /**
51
+     * @since 33.0.0
52
+     * For settings only used for delegation but not appearing in settings menu
53
+     */
54
+    public const SETTINGS_DELEGATION = 'delegation';
55
+
56
+    /**
57
+     * @psalm-param self::SETTINGS_* $type
58
+     * @param class-string<IIconSection> $section
59
+     * @since 14.0.0
60
+     */
61
+    public function registerSection(string $type, string $section);
62
+
63
+    /**
64
+     * @psalm-param self::SETTINGS_* $type
65
+     * @param class-string<ISettings> $setting
66
+     * @since 14.0.0
67
+     */
68
+    public function registerSetting(string $type, string $setting);
69
+
70
+    /**
71
+     * returns a list of the admin sections
72
+     *
73
+     * @return array<int, list<IIconSection>> list of sections with priority as key
74
+     * @since 9.1.0
75
+     */
76
+    public function getAdminSections(): array;
77
+
78
+    /**
79
+     * returns a list of the personal sections
80
+     *
81
+     * @return array<int, list<IIconSection>> list of sections with priority as key
82
+     * @since 13.0.0
83
+     */
84
+    public function getPersonalSections(): array;
85
+
86
+    /**
87
+     * returns a list of the admin settings
88
+     *
89
+     * @param string $section the section id for which to load the settings
90
+     * @param bool $subAdminOnly only return settings sub admins are supposed to see (since 17.0.0)
91
+     * @return array<int, list<ISettings>> list of settings with priority as key
92
+     * @since 9.1.0
93
+     */
94
+    public function getAdminSettings(string $section, bool $subAdminOnly = false): array;
95
+
96
+    /**
97
+     * Returns a list of admin settings that the given user can use for the give section
98
+     *
99
+     * @return array<int, list<ISettings>> List of admin-settings the user has access to, with priority as key.
100
+     * @since 23.0.0
101
+     */
102
+    public function getAllowedAdminSettings(string $section, IUser $user): array;
103
+
104
+    /**
105
+     * Returns a list of admin settings that the given user can use.
106
+     *
107
+     * @return list<ISettings> The array of admin settings there admin delegation is allowed.
108
+     * @since 23.0.0
109
+     */
110
+    public function getAllAllowedAdminSettings(IUser $user): array;
111
+
112
+    /**
113
+     * returns a list of the personal  settings
114
+     *
115
+     * @param string $section the section id for which to load the settings
116
+     * @return array<int, list<ISettings>> list of settings with priority as key
117
+     * @since 13.0.0
118
+     */
119
+    public function getPersonalSettings(string $section): array;
120
+
121
+    /**
122
+     * Get a specific section by type and id
123
+     * @psalm-param self::SETTINGS_* $type
124
+     * @since 25.0.0
125
+     */
126
+    public function getSection(string $type, string $sectionId): ?IIconSection;
127
+
128
+    /**
129
+     * Return admin delegated settings, sorted by priority and grouped by section
130
+     * @return array<string, array{section:IIconSection,settings:list<IDelegatedSettings>}>
131
+     * @since 33.0.0
132
+     */
133
+    public function getAdminDelegatedSettings(): array;
134 134
 }
Please login to merge, or discard this patch.