Completed
Push — master ( 4174c5...e4f06e )
by
unknown
25:16
created
lib/private/App/AppManager.php 1 patch
Indentation   +1093 added lines, -1093 removed lines patch added patch discarded remove patch
@@ -38,1097 +38,1097 @@
 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
-				$this->logger->info('{appId} needs and upgrade from {from} to {to}',
1119
-					[
1120
-						'appId' => $appId,
1121
-						'from' => $installedVersion,
1122
-						'to' => $currentVersion,
1123
-					]
1124
-				);
1125
-				return true;
1126
-			}
1127
-		}
1128
-		return false;
1129
-	}
1130
-
1131
-	public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool {
1132
-		return count($this->dependencyAnalyzer->analyzeServerVersion($serverVersion, $appInfo, $ignoreMax)) === 0;
1133
-	}
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
+                $this->logger->info('{appId} needs and upgrade from {from} to {to}',
1119
+                    [
1120
+                        'appId' => $appId,
1121
+                        'from' => $installedVersion,
1122
+                        'to' => $currentVersion,
1123
+                    ]
1124
+                );
1125
+                return true;
1126
+            }
1127
+        }
1128
+        return false;
1129
+    }
1130
+
1131
+    public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool {
1132
+        return count($this->dependencyAnalyzer->analyzeServerVersion($serverVersion, $appInfo, $ignoreMax)) === 0;
1133
+    }
1134 1134
 }
Please login to merge, or discard this patch.