Completed
Push — master ( 47f195...fb7244 )
by
unknown
41:14 queued 13s
created
lib/private/App/AppManager.php 2 patches
Indentation   +895 added lines, -895 removed lines patch added patch discarded remove patch
@@ -31,899 +31,899 @@
 block discarded – undo
31 31
 use Psr\Log\LoggerInterface;
32 32
 
33 33
 class AppManager implements IAppManager {
34
-	/**
35
-	 * Apps with these types can not be enabled for certain groups only
36
-	 * @var string[]
37
-	 */
38
-	protected $protectedAppTypes = [
39
-		'filesystem',
40
-		'prelogin',
41
-		'authentication',
42
-		'logging',
43
-		'prevent_group_restriction',
44
-	];
45
-
46
-	/** @var string[] $appId => $enabled */
47
-	private array $enabledAppsCache = [];
48
-
49
-	/** @var string[]|null */
50
-	private ?array $shippedApps = null;
51
-
52
-	private array $alwaysEnabled = [];
53
-	private array $defaultEnabled = [];
54
-
55
-	/** @var array */
56
-	private array $appInfos = [];
57
-
58
-	/** @var array */
59
-	private array $appVersions = [];
60
-
61
-	/** @var array */
62
-	private array $autoDisabledApps = [];
63
-	private array $appTypes = [];
64
-
65
-	/** @var array<string, true> */
66
-	private array $loadedApps = [];
67
-
68
-	private ?AppConfig $appConfig = null;
69
-	private ?IURLGenerator $urlGenerator = null;
70
-	private ?INavigationManager $navigationManager = null;
71
-
72
-	/**
73
-	 * Be extremely careful when injecting classes here. The AppManager is used by the installer,
74
-	 * so it needs to work before installation. See how AppConfig and IURLGenerator are injected for reference
75
-	 */
76
-	public function __construct(
77
-		private IUserSession $userSession,
78
-		private IConfig $config,
79
-		private IGroupManager $groupManager,
80
-		private ICacheFactory $memCacheFactory,
81
-		private IEventDispatcher $dispatcher,
82
-		private LoggerInterface $logger,
83
-		private ServerVersion $serverVersion,
84
-	) {
85
-	}
86
-
87
-	private function getNavigationManager(): INavigationManager {
88
-		if ($this->navigationManager === null) {
89
-			$this->navigationManager = \OCP\Server::get(INavigationManager::class);
90
-		}
91
-		return $this->navigationManager;
92
-	}
93
-
94
-	public function getAppIcon(string $appId, bool $dark = false): ?string {
95
-		$possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
96
-		$icon = null;
97
-		foreach ($possibleIcons as $iconName) {
98
-			try {
99
-				$icon = $this->getUrlGenerator()->imagePath($appId, $iconName);
100
-				break;
101
-			} catch (\RuntimeException $e) {
102
-				// ignore
103
-			}
104
-		}
105
-		return $icon;
106
-	}
107
-
108
-	private function getAppConfig(): AppConfig {
109
-		if ($this->appConfig !== null) {
110
-			return $this->appConfig;
111
-		}
112
-		if (!$this->config->getSystemValueBool('installed', false)) {
113
-			throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
114
-		}
115
-		$this->appConfig = \OCP\Server::get(AppConfig::class);
116
-		return $this->appConfig;
117
-	}
118
-
119
-	private function getUrlGenerator(): IURLGenerator {
120
-		if ($this->urlGenerator !== null) {
121
-			return $this->urlGenerator;
122
-		}
123
-		if (!$this->config->getSystemValueBool('installed', false)) {
124
-			throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
125
-		}
126
-		$this->urlGenerator = \OCP\Server::get(IURLGenerator::class);
127
-		return $this->urlGenerator;
128
-	}
129
-
130
-	/**
131
-	 * For all enabled apps, return the value of their 'enabled' config key.
132
-	 *
133
-	 * @return array<string,string> appId => enabled (may be 'yes', or a json encoded list of group ids)
134
-	 */
135
-	private function getEnabledAppsValues(): array {
136
-		if (!$this->enabledAppsCache) {
137
-			$values = $this->getAppConfig()->getValues(false, 'enabled');
138
-
139
-			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
140
-			foreach ($alwaysEnabledApps as $appId) {
141
-				$values[$appId] = 'yes';
142
-			}
143
-
144
-			$this->enabledAppsCache = array_filter($values, function ($value) {
145
-				return $value !== 'no';
146
-			});
147
-			ksort($this->enabledAppsCache);
148
-		}
149
-		return $this->enabledAppsCache;
150
-	}
151
-
152
-	/**
153
-	 * Deprecated alias
154
-	 *
155
-	 * @return string[]
156
-	 */
157
-	public function getInstalledApps() {
158
-		return $this->getEnabledApps();
159
-	}
160
-
161
-	/**
162
-	 * List all enabled apps, either for everyone or for some groups
163
-	 *
164
-	 * @return list<string>
165
-	 */
166
-	public function getEnabledApps(): array {
167
-		return array_keys($this->getEnabledAppsValues());
168
-	}
169
-
170
-	/**
171
-	 * Get a list of all apps in the apps folder
172
-	 *
173
-	 * @return list<string> an array of app names (string IDs)
174
-	 */
175
-	public function getAllAppsInAppsFolders(): array {
176
-		$apps = [];
177
-
178
-		foreach (\OC::$APPSROOTS as $apps_dir) {
179
-			if (!is_readable($apps_dir['path'])) {
180
-				$this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']);
181
-				continue;
182
-			}
183
-			$dh = opendir($apps_dir['path']);
184
-
185
-			if (is_resource($dh)) {
186
-				while (($file = readdir($dh)) !== false) {
187
-					if (
188
-						$file[0] != '.' &&
189
-						is_dir($apps_dir['path'] . '/' . $file) &&
190
-						is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')
191
-					) {
192
-						$apps[] = $file;
193
-					}
194
-				}
195
-			}
196
-		}
197
-
198
-		return array_values(array_unique($apps));
199
-	}
200
-
201
-	/**
202
-	 * List all apps enabled for a user
203
-	 *
204
-	 * @param \OCP\IUser $user
205
-	 * @return string[]
206
-	 */
207
-	public function getEnabledAppsForUser(IUser $user) {
208
-		$apps = $this->getEnabledAppsValues();
209
-		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
210
-			return $this->checkAppForUser($enabled, $user);
211
-		});
212
-		return array_keys($appsForUser);
213
-	}
214
-
215
-	public function getEnabledAppsForGroup(IGroup $group): array {
216
-		$apps = $this->getEnabledAppsValues();
217
-		$appsForGroups = array_filter($apps, function ($enabled) use ($group) {
218
-			return $this->checkAppForGroups($enabled, $group);
219
-		});
220
-		return array_keys($appsForGroups);
221
-	}
222
-
223
-	/**
224
-	 * Loads all apps
225
-	 *
226
-	 * @param string[] $types
227
-	 * @return bool
228
-	 *
229
-	 * This function walks through the Nextcloud directory and loads all apps
230
-	 * it can find. A directory contains an app if the file /appinfo/info.xml
231
-	 * exists.
232
-	 *
233
-	 * if $types is set to non-empty array, only apps of those types will be loaded
234
-	 */
235
-	public function loadApps(array $types = []): bool {
236
-		if ($this->config->getSystemValueBool('maintenance', false)) {
237
-			return false;
238
-		}
239
-		// Load the enabled apps here
240
-		$apps = \OC_App::getEnabledApps();
241
-
242
-		// Add each apps' folder as allowed class path
243
-		foreach ($apps as $app) {
244
-			// If the app is already loaded then autoloading it makes no sense
245
-			if (!$this->isAppLoaded($app)) {
246
-				$path = \OC_App::getAppPath($app);
247
-				if ($path !== false) {
248
-					\OC_App::registerAutoloading($app, $path);
249
-				}
250
-			}
251
-		}
252
-
253
-		// prevent app loading from printing output
254
-		ob_start();
255
-		foreach ($apps as $app) {
256
-			if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
257
-				try {
258
-					$this->loadApp($app);
259
-				} catch (\Throwable $e) {
260
-					$this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
261
-						'exception' => $e,
262
-						'app' => $app,
263
-					]);
264
-				}
265
-			}
266
-		}
267
-		ob_end_clean();
268
-
269
-		return true;
270
-	}
271
-
272
-	/**
273
-	 * check if an app is of a specific type
274
-	 *
275
-	 * @param string $app
276
-	 * @param array $types
277
-	 * @return bool
278
-	 */
279
-	public function isType(string $app, array $types): bool {
280
-		$appTypes = $this->getAppTypes($app);
281
-		foreach ($types as $type) {
282
-			if (in_array($type, $appTypes, true)) {
283
-				return true;
284
-			}
285
-		}
286
-		return false;
287
-	}
288
-
289
-	/**
290
-	 * get the types of an app
291
-	 *
292
-	 * @param string $app
293
-	 * @return string[]
294
-	 */
295
-	private function getAppTypes(string $app): array {
296
-		//load the cache
297
-		if (count($this->appTypes) === 0) {
298
-			$this->appTypes = $this->getAppConfig()->getValues(false, 'types') ?: [];
299
-		}
300
-
301
-		if (isset($this->appTypes[$app])) {
302
-			return explode(',', $this->appTypes[$app]);
303
-		}
304
-
305
-		return [];
306
-	}
307
-
308
-	/**
309
-	 * @return array
310
-	 */
311
-	public function getAutoDisabledApps(): array {
312
-		return $this->autoDisabledApps;
313
-	}
314
-
315
-	public function getAppRestriction(string $appId): array {
316
-		$values = $this->getEnabledAppsValues();
317
-
318
-		if (!isset($values[$appId])) {
319
-			return [];
320
-		}
321
-
322
-		if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
323
-			return [];
324
-		}
325
-		return json_decode($values[$appId], true);
326
-	}
327
-
328
-	/**
329
-	 * Check if an app is enabled for user
330
-	 *
331
-	 * @param string $appId
332
-	 * @param \OCP\IUser|null $user (optional) if not defined, the currently logged in user will be used
333
-	 * @return bool
334
-	 */
335
-	public function isEnabledForUser($appId, $user = null) {
336
-		if ($this->isAlwaysEnabled($appId)) {
337
-			return true;
338
-		}
339
-		if ($user === null) {
340
-			$user = $this->userSession->getUser();
341
-		}
342
-		$enabledAppsValues = $this->getEnabledAppsValues();
343
-		if (isset($enabledAppsValues[$appId])) {
344
-			return $this->checkAppForUser($enabledAppsValues[$appId], $user);
345
-		} else {
346
-			return false;
347
-		}
348
-	}
349
-
350
-	private function checkAppForUser(string $enabled, ?IUser $user): bool {
351
-		if ($enabled === 'yes') {
352
-			return true;
353
-		} elseif ($user === null) {
354
-			return false;
355
-		} else {
356
-			if (empty($enabled)) {
357
-				return false;
358
-			}
359
-
360
-			$groupIds = json_decode($enabled);
361
-
362
-			if (!is_array($groupIds)) {
363
-				$jsonError = json_last_error();
364
-				$jsonErrorMsg = json_last_error_msg();
365
-				// 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)
366
-				$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);
367
-				return false;
368
-			}
369
-
370
-			$userGroups = $this->groupManager->getUserGroupIds($user);
371
-			foreach ($userGroups as $groupId) {
372
-				if (in_array($groupId, $groupIds, true)) {
373
-					return true;
374
-				}
375
-			}
376
-			return false;
377
-		}
378
-	}
379
-
380
-	private function checkAppForGroups(string $enabled, IGroup $group): bool {
381
-		if ($enabled === 'yes') {
382
-			return true;
383
-		} else {
384
-			if (empty($enabled)) {
385
-				return false;
386
-			}
387
-
388
-			$groupIds = json_decode($enabled);
389
-
390
-			if (!is_array($groupIds)) {
391
-				$jsonError = json_last_error();
392
-				$jsonErrorMsg = json_last_error_msg();
393
-				// 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)
394
-				$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);
395
-				return false;
396
-			}
397
-
398
-			return in_array($group->getGID(), $groupIds);
399
-		}
400
-	}
401
-
402
-	/**
403
-	 * Check if an app is enabled in the instance
404
-	 *
405
-	 * Notice: This actually checks if the app is enabled and not only if it is installed.
406
-	 *
407
-	 * @param string $appId
408
-	 */
409
-	public function isInstalled($appId): bool {
410
-		return $this->isEnabledForAnyone($appId);
411
-	}
412
-
413
-	public function isEnabledForAnyone(string $appId): bool {
414
-		$enabledAppsValues = $this->getEnabledAppsValues();
415
-		return isset($enabledAppsValues[$appId]);
416
-	}
417
-
418
-	/**
419
-	 * Overwrite the `max-version` requirement for this app.
420
-	 */
421
-	public function overwriteNextcloudRequirement(string $appId): void {
422
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
423
-		if (!in_array($appId, $ignoreMaxApps, true)) {
424
-			$ignoreMaxApps[] = $appId;
425
-		}
426
-		$this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
427
-	}
428
-
429
-	/**
430
-	 * Remove the `max-version` overwrite for this app.
431
-	 * This means this app now again can not be enabled if the `max-version` is smaller than the current Nextcloud version.
432
-	 */
433
-	public function removeOverwriteNextcloudRequirement(string $appId): void {
434
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
435
-		$ignoreMaxApps = array_filter($ignoreMaxApps, fn (string $id) => $id !== $appId);
436
-		$this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
437
-	}
438
-
439
-	public function loadApp(string $app): void {
440
-		if (isset($this->loadedApps[$app])) {
441
-			return;
442
-		}
443
-		$this->loadedApps[$app] = true;
444
-		$appPath = \OC_App::getAppPath($app);
445
-		if ($appPath === false) {
446
-			return;
447
-		}
448
-		$eventLogger = \OC::$server->get(IEventLogger::class);
449
-		$eventLogger->start("bootstrap:load_app:$app", "Load app: $app");
450
-
451
-		// in case someone calls loadApp() directly
452
-		\OC_App::registerAutoloading($app, $appPath);
453
-
454
-		if (is_file($appPath . '/appinfo/app.php')) {
455
-			$this->logger->error('/appinfo/app.php is not supported anymore, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
456
-				'app' => $app,
457
-			]);
458
-		}
459
-
460
-		$coordinator = \OCP\Server::get(Coordinator::class);
461
-		$coordinator->bootApp($app);
462
-
463
-		$eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
464
-		$info = $this->getAppInfo($app);
465
-		if (!empty($info['activity'])) {
466
-			$activityManager = \OC::$server->get(IActivityManager::class);
467
-			if (!empty($info['activity']['filters'])) {
468
-				foreach ($info['activity']['filters'] as $filter) {
469
-					$activityManager->registerFilter($filter);
470
-				}
471
-			}
472
-			if (!empty($info['activity']['settings'])) {
473
-				foreach ($info['activity']['settings'] as $setting) {
474
-					$activityManager->registerSetting($setting);
475
-				}
476
-			}
477
-			if (!empty($info['activity']['providers'])) {
478
-				foreach ($info['activity']['providers'] as $provider) {
479
-					$activityManager->registerProvider($provider);
480
-				}
481
-			}
482
-		}
483
-
484
-		if (!empty($info['settings'])) {
485
-			$settingsManager = \OC::$server->get(ISettingsManager::class);
486
-			if (!empty($info['settings']['admin'])) {
487
-				foreach ($info['settings']['admin'] as $setting) {
488
-					$settingsManager->registerSetting('admin', $setting);
489
-				}
490
-			}
491
-			if (!empty($info['settings']['admin-section'])) {
492
-				foreach ($info['settings']['admin-section'] as $section) {
493
-					$settingsManager->registerSection('admin', $section);
494
-				}
495
-			}
496
-			if (!empty($info['settings']['personal'])) {
497
-				foreach ($info['settings']['personal'] as $setting) {
498
-					$settingsManager->registerSetting('personal', $setting);
499
-				}
500
-			}
501
-			if (!empty($info['settings']['personal-section'])) {
502
-				foreach ($info['settings']['personal-section'] as $section) {
503
-					$settingsManager->registerSection('personal', $section);
504
-				}
505
-			}
506
-		}
507
-
508
-		if (!empty($info['collaboration']['plugins'])) {
509
-			// deal with one or many plugin entries
510
-			$plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
511
-				[$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
512
-			$collaboratorSearch = null;
513
-			$autoCompleteManager = null;
514
-			foreach ($plugins as $plugin) {
515
-				if ($plugin['@attributes']['type'] === 'collaborator-search') {
516
-					$pluginInfo = [
517
-						'shareType' => $plugin['@attributes']['share-type'],
518
-						'class' => $plugin['@value'],
519
-					];
520
-					$collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
521
-					$collaboratorSearch->registerPlugin($pluginInfo);
522
-				} elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
523
-					$autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
524
-					$autoCompleteManager->registerSorter($plugin['@value']);
525
-				}
526
-			}
527
-		}
528
-		$eventLogger->end("bootstrap:load_app:$app:info");
529
-
530
-		$eventLogger->end("bootstrap:load_app:$app");
531
-	}
532
-
533
-	/**
534
-	 * Check if an app is loaded
535
-	 * @param string $app app id
536
-	 * @since 26.0.0
537
-	 */
538
-	public function isAppLoaded(string $app): bool {
539
-		return isset($this->loadedApps[$app]);
540
-	}
541
-
542
-	/**
543
-	 * Enable an app for every user
544
-	 *
545
-	 * @param string $appId
546
-	 * @param bool $forceEnable
547
-	 * @throws AppPathNotFoundException
548
-	 */
549
-	public function enableApp(string $appId, bool $forceEnable = false): void {
550
-		// Check if app exists
551
-		$this->getAppPath($appId);
552
-
553
-		if ($forceEnable) {
554
-			$this->overwriteNextcloudRequirement($appId);
555
-		}
556
-
557
-		$this->enabledAppsCache[$appId] = 'yes';
558
-		$this->getAppConfig()->setValue($appId, 'enabled', 'yes');
559
-		$this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
560
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
561
-			ManagerEvent::EVENT_APP_ENABLE, $appId
562
-		));
563
-		$this->clearAppsCache();
564
-	}
565
-
566
-	/**
567
-	 * Whether a list of types contains a protected app type
568
-	 *
569
-	 * @param string[] $types
570
-	 * @return bool
571
-	 */
572
-	public function hasProtectedAppType($types) {
573
-		if (empty($types)) {
574
-			return false;
575
-		}
576
-
577
-		$protectedTypes = array_intersect($this->protectedAppTypes, $types);
578
-		return !empty($protectedTypes);
579
-	}
580
-
581
-	/**
582
-	 * Enable an app only for specific groups
583
-	 *
584
-	 * @param string $appId
585
-	 * @param IGroup[] $groups
586
-	 * @param bool $forceEnable
587
-	 * @throws \InvalidArgumentException if app can't be enabled for groups
588
-	 * @throws AppPathNotFoundException
589
-	 */
590
-	public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
591
-		// Check if app exists
592
-		$this->getAppPath($appId);
593
-
594
-		$info = $this->getAppInfo($appId);
595
-		if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
596
-			throw new \InvalidArgumentException("$appId can't be enabled for groups.");
597
-		}
598
-
599
-		if ($forceEnable) {
600
-			$this->overwriteNextcloudRequirement($appId);
601
-		}
602
-
603
-		/** @var string[] $groupIds */
604
-		$groupIds = array_map(function ($group) {
605
-			/** @var IGroup $group */
606
-			return ($group instanceof IGroup)
607
-				? $group->getGID()
608
-				: $group;
609
-		}, $groups);
610
-
611
-		$this->enabledAppsCache[$appId] = json_encode($groupIds);
612
-		$this->getAppConfig()->setValue($appId, 'enabled', json_encode($groupIds));
613
-		$this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
614
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
615
-			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
616
-		));
617
-		$this->clearAppsCache();
618
-	}
619
-
620
-	/**
621
-	 * Disable an app for every user
622
-	 *
623
-	 * @param string $appId
624
-	 * @param bool $automaticDisabled
625
-	 * @throws \Exception if app can't be disabled
626
-	 */
627
-	public function disableApp($appId, $automaticDisabled = false): void {
628
-		if ($this->isAlwaysEnabled($appId)) {
629
-			throw new \Exception("$appId can't be disabled.");
630
-		}
631
-
632
-		if ($automaticDisabled) {
633
-			$previousSetting = $this->getAppConfig()->getValue($appId, 'enabled', 'yes');
634
-			if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
635
-				$previousSetting = json_decode($previousSetting, true);
636
-			}
637
-			$this->autoDisabledApps[$appId] = $previousSetting;
638
-		}
639
-
640
-		unset($this->enabledAppsCache[$appId]);
641
-		$this->getAppConfig()->setValue($appId, 'enabled', 'no');
642
-
643
-		// run uninstall steps
644
-		$appData = $this->getAppInfo($appId);
645
-		if (!is_null($appData)) {
646
-			\OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
647
-		}
648
-
649
-		$this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
650
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
651
-			ManagerEvent::EVENT_APP_DISABLE, $appId
652
-		));
653
-		$this->clearAppsCache();
654
-	}
655
-
656
-	/**
657
-	 * Get the directory for the given app.
658
-	 *
659
-	 * @throws AppPathNotFoundException if app folder can't be found
660
-	 */
661
-	public function getAppPath(string $appId): string {
662
-		$appPath = \OC_App::getAppPath($appId);
663
-		if ($appPath === false) {
664
-			throw new AppPathNotFoundException('Could not find path for ' . $appId);
665
-		}
666
-		return $appPath;
667
-	}
668
-
669
-	/**
670
-	 * Get the web path for the given app.
671
-	 *
672
-	 * @param string $appId
673
-	 * @return string
674
-	 * @throws AppPathNotFoundException if app path can't be found
675
-	 */
676
-	public function getAppWebPath(string $appId): string {
677
-		$appWebPath = \OC_App::getAppWebPath($appId);
678
-		if ($appWebPath === false) {
679
-			throw new AppPathNotFoundException('Could not find web path for ' . $appId);
680
-		}
681
-		return $appWebPath;
682
-	}
683
-
684
-	/**
685
-	 * Clear the cached list of apps when enabling/disabling an app
686
-	 */
687
-	public function clearAppsCache(): void {
688
-		$this->appInfos = [];
689
-	}
690
-
691
-	/**
692
-	 * Returns a list of apps that need upgrade
693
-	 *
694
-	 * @param string $version Nextcloud version as array of version components
695
-	 * @return array list of app info from apps that need an upgrade
696
-	 *
697
-	 * @internal
698
-	 */
699
-	public function getAppsNeedingUpgrade($version) {
700
-		$appsToUpgrade = [];
701
-		$apps = $this->getEnabledApps();
702
-		foreach ($apps as $appId) {
703
-			$appInfo = $this->getAppInfo($appId);
704
-			$appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version');
705
-			if ($appDbVersion
706
-				&& isset($appInfo['version'])
707
-				&& version_compare($appInfo['version'], $appDbVersion, '>')
708
-				&& \OC_App::isAppCompatible($version, $appInfo)
709
-			) {
710
-				$appsToUpgrade[] = $appInfo;
711
-			}
712
-		}
713
-
714
-		return $appsToUpgrade;
715
-	}
716
-
717
-	/**
718
-	 * Returns the app information from "appinfo/info.xml".
719
-	 *
720
-	 * @param string|null $lang
721
-	 * @return array|null app info
722
-	 */
723
-	public function getAppInfo(string $appId, bool $path = false, $lang = null) {
724
-		if ($path) {
725
-			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.');
726
-		}
727
-		if ($lang === null && isset($this->appInfos[$appId])) {
728
-			return $this->appInfos[$appId];
729
-		}
730
-		try {
731
-			$appPath = $this->getAppPath($appId);
732
-		} catch (AppPathNotFoundException) {
733
-			return null;
734
-		}
735
-		$file = $appPath . '/appinfo/info.xml';
736
-
737
-		$data = $this->getAppInfoByPath($file, $lang);
738
-
739
-		if ($lang === null) {
740
-			$this->appInfos[$appId] = $data;
741
-		}
742
-
743
-		return $data;
744
-	}
745
-
746
-	public function getAppInfoByPath(string $path, ?string $lang = null): ?array {
747
-		if (!str_ends_with($path, '/appinfo/info.xml')) {
748
-			return null;
749
-		}
750
-
751
-		$parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
752
-		$data = $parser->parse($path);
753
-
754
-		if (is_array($data)) {
755
-			$data = \OC_App::parseAppInfo($data, $lang);
756
-		}
757
-
758
-		return $data;
759
-	}
760
-
761
-	public function getAppVersion(string $appId, bool $useCache = true): string {
762
-		if (!$useCache || !isset($this->appVersions[$appId])) {
763
-			if ($appId === 'core') {
764
-				$this->appVersions[$appId] = $this->serverVersion->getVersionString();
765
-			} else {
766
-				$appInfo = $this->getAppInfo($appId);
767
-				$this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
768
-			}
769
-		}
770
-		return $this->appVersions[$appId];
771
-	}
772
-
773
-	/**
774
-	 * Returns the installed versions of all apps
775
-	 *
776
-	 * @return array<string, string>
777
-	 */
778
-	public function getAppInstalledVersions(): array {
779
-		return $this->getAppConfig()->getAppInstalledVersions();
780
-	}
781
-
782
-	/**
783
-	 * Returns a list of apps incompatible with the given version
784
-	 *
785
-	 * @param string $version Nextcloud version as array of version components
786
-	 *
787
-	 * @return array list of app info from incompatible apps
788
-	 *
789
-	 * @internal
790
-	 */
791
-	public function getIncompatibleApps(string $version): array {
792
-		$apps = $this->getEnabledApps();
793
-		$incompatibleApps = [];
794
-		foreach ($apps as $appId) {
795
-			$info = $this->getAppInfo($appId);
796
-			if ($info === null) {
797
-				$incompatibleApps[] = ['id' => $appId, 'name' => $appId];
798
-			} elseif (!\OC_App::isAppCompatible($version, $info)) {
799
-				$incompatibleApps[] = $info;
800
-			}
801
-		}
802
-		return $incompatibleApps;
803
-	}
804
-
805
-	/**
806
-	 * @inheritdoc
807
-	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
808
-	 */
809
-	public function isShipped($appId) {
810
-		$this->loadShippedJson();
811
-		return in_array($appId, $this->shippedApps, true);
812
-	}
813
-
814
-	private function isAlwaysEnabled(string $appId): bool {
815
-		$alwaysEnabled = $this->getAlwaysEnabledApps();
816
-		return in_array($appId, $alwaysEnabled, true);
817
-	}
818
-
819
-	/**
820
-	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
821
-	 * @throws \Exception
822
-	 */
823
-	private function loadShippedJson(): void {
824
-		if ($this->shippedApps === null) {
825
-			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
826
-			if (!file_exists($shippedJson)) {
827
-				throw new \Exception("File not found: $shippedJson");
828
-			}
829
-			$content = json_decode(file_get_contents($shippedJson), true);
830
-			$this->shippedApps = $content['shippedApps'];
831
-			$this->alwaysEnabled = $content['alwaysEnabled'];
832
-			$this->defaultEnabled = $content['defaultEnabled'];
833
-		}
834
-	}
835
-
836
-	/**
837
-	 * @inheritdoc
838
-	 */
839
-	public function getAlwaysEnabledApps() {
840
-		$this->loadShippedJson();
841
-		return $this->alwaysEnabled;
842
-	}
843
-
844
-	/**
845
-	 * @inheritdoc
846
-	 */
847
-	public function isDefaultEnabled(string $appId): bool {
848
-		return (in_array($appId, $this->getDefaultEnabledApps()));
849
-	}
850
-
851
-	/**
852
-	 * @inheritdoc
853
-	 */
854
-	public function getDefaultEnabledApps(): array {
855
-		$this->loadShippedJson();
856
-
857
-		return $this->defaultEnabled;
858
-	}
859
-
860
-	/**
861
-	 * @inheritdoc
862
-	 */
863
-	public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
864
-		$id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
865
-		$entry = $this->getNavigationManager()->get($id);
866
-		return (string)$entry['app'];
867
-	}
868
-
869
-	/**
870
-	 * @inheritdoc
871
-	 */
872
-	public function getDefaultApps(): array {
873
-		$ids = $this->getNavigationManager()->getDefaultEntryIds();
874
-
875
-		return array_values(array_unique(array_map(function (string $id) {
876
-			$entry = $this->getNavigationManager()->get($id);
877
-			return (string)$entry['app'];
878
-		}, $ids)));
879
-	}
880
-
881
-	/**
882
-	 * @inheritdoc
883
-	 */
884
-	public function setDefaultApps(array $defaultApps): void {
885
-		$entries = $this->getNavigationManager()->getAll();
886
-		$ids = [];
887
-		foreach ($defaultApps as $defaultApp) {
888
-			foreach ($entries as $entry) {
889
-				if ((string)$entry['app'] === $defaultApp) {
890
-					$ids[] = (string)$entry['id'];
891
-					break;
892
-				}
893
-			}
894
-		}
895
-		$this->getNavigationManager()->setDefaultEntryIds($ids);
896
-	}
897
-
898
-	public function isBackendRequired(string $backend): bool {
899
-		foreach ($this->appInfos as $appInfo) {
900
-			foreach ($appInfo['dependencies']['backend'] as $appBackend) {
901
-				if ($backend === $appBackend) {
902
-					return true;
903
-				}
904
-			}
905
-		}
906
-
907
-		return false;
908
-	}
909
-
910
-	/**
911
-	 * Clean the appId from forbidden characters
912
-	 *
913
-	 * @psalm-taint-escape callable
914
-	 * @psalm-taint-escape cookie
915
-	 * @psalm-taint-escape file
916
-	 * @psalm-taint-escape has_quotes
917
-	 * @psalm-taint-escape header
918
-	 * @psalm-taint-escape html
919
-	 * @psalm-taint-escape include
920
-	 * @psalm-taint-escape ldap
921
-	 * @psalm-taint-escape shell
922
-	 * @psalm-taint-escape sql
923
-	 * @psalm-taint-escape unserialize
924
-	 */
925
-	public function cleanAppId(string $app): string {
926
-		/* Only lowercase alphanumeric is allowed */
927
-		return preg_replace('/(^[0-9_]|[^a-z0-9_]+|_$)/', '', $app);
928
-	}
34
+    /**
35
+     * Apps with these types can not be enabled for certain groups only
36
+     * @var string[]
37
+     */
38
+    protected $protectedAppTypes = [
39
+        'filesystem',
40
+        'prelogin',
41
+        'authentication',
42
+        'logging',
43
+        'prevent_group_restriction',
44
+    ];
45
+
46
+    /** @var string[] $appId => $enabled */
47
+    private array $enabledAppsCache = [];
48
+
49
+    /** @var string[]|null */
50
+    private ?array $shippedApps = null;
51
+
52
+    private array $alwaysEnabled = [];
53
+    private array $defaultEnabled = [];
54
+
55
+    /** @var array */
56
+    private array $appInfos = [];
57
+
58
+    /** @var array */
59
+    private array $appVersions = [];
60
+
61
+    /** @var array */
62
+    private array $autoDisabledApps = [];
63
+    private array $appTypes = [];
64
+
65
+    /** @var array<string, true> */
66
+    private array $loadedApps = [];
67
+
68
+    private ?AppConfig $appConfig = null;
69
+    private ?IURLGenerator $urlGenerator = null;
70
+    private ?INavigationManager $navigationManager = null;
71
+
72
+    /**
73
+     * Be extremely careful when injecting classes here. The AppManager is used by the installer,
74
+     * so it needs to work before installation. See how AppConfig and IURLGenerator are injected for reference
75
+     */
76
+    public function __construct(
77
+        private IUserSession $userSession,
78
+        private IConfig $config,
79
+        private IGroupManager $groupManager,
80
+        private ICacheFactory $memCacheFactory,
81
+        private IEventDispatcher $dispatcher,
82
+        private LoggerInterface $logger,
83
+        private ServerVersion $serverVersion,
84
+    ) {
85
+    }
86
+
87
+    private function getNavigationManager(): INavigationManager {
88
+        if ($this->navigationManager === null) {
89
+            $this->navigationManager = \OCP\Server::get(INavigationManager::class);
90
+        }
91
+        return $this->navigationManager;
92
+    }
93
+
94
+    public function getAppIcon(string $appId, bool $dark = false): ?string {
95
+        $possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
96
+        $icon = null;
97
+        foreach ($possibleIcons as $iconName) {
98
+            try {
99
+                $icon = $this->getUrlGenerator()->imagePath($appId, $iconName);
100
+                break;
101
+            } catch (\RuntimeException $e) {
102
+                // ignore
103
+            }
104
+        }
105
+        return $icon;
106
+    }
107
+
108
+    private function getAppConfig(): AppConfig {
109
+        if ($this->appConfig !== null) {
110
+            return $this->appConfig;
111
+        }
112
+        if (!$this->config->getSystemValueBool('installed', false)) {
113
+            throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
114
+        }
115
+        $this->appConfig = \OCP\Server::get(AppConfig::class);
116
+        return $this->appConfig;
117
+    }
118
+
119
+    private function getUrlGenerator(): IURLGenerator {
120
+        if ($this->urlGenerator !== null) {
121
+            return $this->urlGenerator;
122
+        }
123
+        if (!$this->config->getSystemValueBool('installed', false)) {
124
+            throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
125
+        }
126
+        $this->urlGenerator = \OCP\Server::get(IURLGenerator::class);
127
+        return $this->urlGenerator;
128
+    }
129
+
130
+    /**
131
+     * For all enabled apps, return the value of their 'enabled' config key.
132
+     *
133
+     * @return array<string,string> appId => enabled (may be 'yes', or a json encoded list of group ids)
134
+     */
135
+    private function getEnabledAppsValues(): array {
136
+        if (!$this->enabledAppsCache) {
137
+            $values = $this->getAppConfig()->getValues(false, 'enabled');
138
+
139
+            $alwaysEnabledApps = $this->getAlwaysEnabledApps();
140
+            foreach ($alwaysEnabledApps as $appId) {
141
+                $values[$appId] = 'yes';
142
+            }
143
+
144
+            $this->enabledAppsCache = array_filter($values, function ($value) {
145
+                return $value !== 'no';
146
+            });
147
+            ksort($this->enabledAppsCache);
148
+        }
149
+        return $this->enabledAppsCache;
150
+    }
151
+
152
+    /**
153
+     * Deprecated alias
154
+     *
155
+     * @return string[]
156
+     */
157
+    public function getInstalledApps() {
158
+        return $this->getEnabledApps();
159
+    }
160
+
161
+    /**
162
+     * List all enabled apps, either for everyone or for some groups
163
+     *
164
+     * @return list<string>
165
+     */
166
+    public function getEnabledApps(): array {
167
+        return array_keys($this->getEnabledAppsValues());
168
+    }
169
+
170
+    /**
171
+     * Get a list of all apps in the apps folder
172
+     *
173
+     * @return list<string> an array of app names (string IDs)
174
+     */
175
+    public function getAllAppsInAppsFolders(): array {
176
+        $apps = [];
177
+
178
+        foreach (\OC::$APPSROOTS as $apps_dir) {
179
+            if (!is_readable($apps_dir['path'])) {
180
+                $this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']);
181
+                continue;
182
+            }
183
+            $dh = opendir($apps_dir['path']);
184
+
185
+            if (is_resource($dh)) {
186
+                while (($file = readdir($dh)) !== false) {
187
+                    if (
188
+                        $file[0] != '.' &&
189
+                        is_dir($apps_dir['path'] . '/' . $file) &&
190
+                        is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')
191
+                    ) {
192
+                        $apps[] = $file;
193
+                    }
194
+                }
195
+            }
196
+        }
197
+
198
+        return array_values(array_unique($apps));
199
+    }
200
+
201
+    /**
202
+     * List all apps enabled for a user
203
+     *
204
+     * @param \OCP\IUser $user
205
+     * @return string[]
206
+     */
207
+    public function getEnabledAppsForUser(IUser $user) {
208
+        $apps = $this->getEnabledAppsValues();
209
+        $appsForUser = array_filter($apps, function ($enabled) use ($user) {
210
+            return $this->checkAppForUser($enabled, $user);
211
+        });
212
+        return array_keys($appsForUser);
213
+    }
214
+
215
+    public function getEnabledAppsForGroup(IGroup $group): array {
216
+        $apps = $this->getEnabledAppsValues();
217
+        $appsForGroups = array_filter($apps, function ($enabled) use ($group) {
218
+            return $this->checkAppForGroups($enabled, $group);
219
+        });
220
+        return array_keys($appsForGroups);
221
+    }
222
+
223
+    /**
224
+     * Loads all apps
225
+     *
226
+     * @param string[] $types
227
+     * @return bool
228
+     *
229
+     * This function walks through the Nextcloud directory and loads all apps
230
+     * it can find. A directory contains an app if the file /appinfo/info.xml
231
+     * exists.
232
+     *
233
+     * if $types is set to non-empty array, only apps of those types will be loaded
234
+     */
235
+    public function loadApps(array $types = []): bool {
236
+        if ($this->config->getSystemValueBool('maintenance', false)) {
237
+            return false;
238
+        }
239
+        // Load the enabled apps here
240
+        $apps = \OC_App::getEnabledApps();
241
+
242
+        // Add each apps' folder as allowed class path
243
+        foreach ($apps as $app) {
244
+            // If the app is already loaded then autoloading it makes no sense
245
+            if (!$this->isAppLoaded($app)) {
246
+                $path = \OC_App::getAppPath($app);
247
+                if ($path !== false) {
248
+                    \OC_App::registerAutoloading($app, $path);
249
+                }
250
+            }
251
+        }
252
+
253
+        // prevent app loading from printing output
254
+        ob_start();
255
+        foreach ($apps as $app) {
256
+            if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
257
+                try {
258
+                    $this->loadApp($app);
259
+                } catch (\Throwable $e) {
260
+                    $this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
261
+                        'exception' => $e,
262
+                        'app' => $app,
263
+                    ]);
264
+                }
265
+            }
266
+        }
267
+        ob_end_clean();
268
+
269
+        return true;
270
+    }
271
+
272
+    /**
273
+     * check if an app is of a specific type
274
+     *
275
+     * @param string $app
276
+     * @param array $types
277
+     * @return bool
278
+     */
279
+    public function isType(string $app, array $types): bool {
280
+        $appTypes = $this->getAppTypes($app);
281
+        foreach ($types as $type) {
282
+            if (in_array($type, $appTypes, true)) {
283
+                return true;
284
+            }
285
+        }
286
+        return false;
287
+    }
288
+
289
+    /**
290
+     * get the types of an app
291
+     *
292
+     * @param string $app
293
+     * @return string[]
294
+     */
295
+    private function getAppTypes(string $app): array {
296
+        //load the cache
297
+        if (count($this->appTypes) === 0) {
298
+            $this->appTypes = $this->getAppConfig()->getValues(false, 'types') ?: [];
299
+        }
300
+
301
+        if (isset($this->appTypes[$app])) {
302
+            return explode(',', $this->appTypes[$app]);
303
+        }
304
+
305
+        return [];
306
+    }
307
+
308
+    /**
309
+     * @return array
310
+     */
311
+    public function getAutoDisabledApps(): array {
312
+        return $this->autoDisabledApps;
313
+    }
314
+
315
+    public function getAppRestriction(string $appId): array {
316
+        $values = $this->getEnabledAppsValues();
317
+
318
+        if (!isset($values[$appId])) {
319
+            return [];
320
+        }
321
+
322
+        if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
323
+            return [];
324
+        }
325
+        return json_decode($values[$appId], true);
326
+    }
327
+
328
+    /**
329
+     * Check if an app is enabled for user
330
+     *
331
+     * @param string $appId
332
+     * @param \OCP\IUser|null $user (optional) if not defined, the currently logged in user will be used
333
+     * @return bool
334
+     */
335
+    public function isEnabledForUser($appId, $user = null) {
336
+        if ($this->isAlwaysEnabled($appId)) {
337
+            return true;
338
+        }
339
+        if ($user === null) {
340
+            $user = $this->userSession->getUser();
341
+        }
342
+        $enabledAppsValues = $this->getEnabledAppsValues();
343
+        if (isset($enabledAppsValues[$appId])) {
344
+            return $this->checkAppForUser($enabledAppsValues[$appId], $user);
345
+        } else {
346
+            return false;
347
+        }
348
+    }
349
+
350
+    private function checkAppForUser(string $enabled, ?IUser $user): bool {
351
+        if ($enabled === 'yes') {
352
+            return true;
353
+        } elseif ($user === null) {
354
+            return false;
355
+        } else {
356
+            if (empty($enabled)) {
357
+                return false;
358
+            }
359
+
360
+            $groupIds = json_decode($enabled);
361
+
362
+            if (!is_array($groupIds)) {
363
+                $jsonError = json_last_error();
364
+                $jsonErrorMsg = json_last_error_msg();
365
+                // 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)
366
+                $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);
367
+                return false;
368
+            }
369
+
370
+            $userGroups = $this->groupManager->getUserGroupIds($user);
371
+            foreach ($userGroups as $groupId) {
372
+                if (in_array($groupId, $groupIds, true)) {
373
+                    return true;
374
+                }
375
+            }
376
+            return false;
377
+        }
378
+    }
379
+
380
+    private function checkAppForGroups(string $enabled, IGroup $group): bool {
381
+        if ($enabled === 'yes') {
382
+            return true;
383
+        } else {
384
+            if (empty($enabled)) {
385
+                return false;
386
+            }
387
+
388
+            $groupIds = json_decode($enabled);
389
+
390
+            if (!is_array($groupIds)) {
391
+                $jsonError = json_last_error();
392
+                $jsonErrorMsg = json_last_error_msg();
393
+                // 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)
394
+                $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);
395
+                return false;
396
+            }
397
+
398
+            return in_array($group->getGID(), $groupIds);
399
+        }
400
+    }
401
+
402
+    /**
403
+     * Check if an app is enabled in the instance
404
+     *
405
+     * Notice: This actually checks if the app is enabled and not only if it is installed.
406
+     *
407
+     * @param string $appId
408
+     */
409
+    public function isInstalled($appId): bool {
410
+        return $this->isEnabledForAnyone($appId);
411
+    }
412
+
413
+    public function isEnabledForAnyone(string $appId): bool {
414
+        $enabledAppsValues = $this->getEnabledAppsValues();
415
+        return isset($enabledAppsValues[$appId]);
416
+    }
417
+
418
+    /**
419
+     * Overwrite the `max-version` requirement for this app.
420
+     */
421
+    public function overwriteNextcloudRequirement(string $appId): void {
422
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
423
+        if (!in_array($appId, $ignoreMaxApps, true)) {
424
+            $ignoreMaxApps[] = $appId;
425
+        }
426
+        $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
427
+    }
428
+
429
+    /**
430
+     * Remove the `max-version` overwrite for this app.
431
+     * This means this app now again can not be enabled if the `max-version` is smaller than the current Nextcloud version.
432
+     */
433
+    public function removeOverwriteNextcloudRequirement(string $appId): void {
434
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
435
+        $ignoreMaxApps = array_filter($ignoreMaxApps, fn (string $id) => $id !== $appId);
436
+        $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
437
+    }
438
+
439
+    public function loadApp(string $app): void {
440
+        if (isset($this->loadedApps[$app])) {
441
+            return;
442
+        }
443
+        $this->loadedApps[$app] = true;
444
+        $appPath = \OC_App::getAppPath($app);
445
+        if ($appPath === false) {
446
+            return;
447
+        }
448
+        $eventLogger = \OC::$server->get(IEventLogger::class);
449
+        $eventLogger->start("bootstrap:load_app:$app", "Load app: $app");
450
+
451
+        // in case someone calls loadApp() directly
452
+        \OC_App::registerAutoloading($app, $appPath);
453
+
454
+        if (is_file($appPath . '/appinfo/app.php')) {
455
+            $this->logger->error('/appinfo/app.php is not supported anymore, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
456
+                'app' => $app,
457
+            ]);
458
+        }
459
+
460
+        $coordinator = \OCP\Server::get(Coordinator::class);
461
+        $coordinator->bootApp($app);
462
+
463
+        $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
464
+        $info = $this->getAppInfo($app);
465
+        if (!empty($info['activity'])) {
466
+            $activityManager = \OC::$server->get(IActivityManager::class);
467
+            if (!empty($info['activity']['filters'])) {
468
+                foreach ($info['activity']['filters'] as $filter) {
469
+                    $activityManager->registerFilter($filter);
470
+                }
471
+            }
472
+            if (!empty($info['activity']['settings'])) {
473
+                foreach ($info['activity']['settings'] as $setting) {
474
+                    $activityManager->registerSetting($setting);
475
+                }
476
+            }
477
+            if (!empty($info['activity']['providers'])) {
478
+                foreach ($info['activity']['providers'] as $provider) {
479
+                    $activityManager->registerProvider($provider);
480
+                }
481
+            }
482
+        }
483
+
484
+        if (!empty($info['settings'])) {
485
+            $settingsManager = \OC::$server->get(ISettingsManager::class);
486
+            if (!empty($info['settings']['admin'])) {
487
+                foreach ($info['settings']['admin'] as $setting) {
488
+                    $settingsManager->registerSetting('admin', $setting);
489
+                }
490
+            }
491
+            if (!empty($info['settings']['admin-section'])) {
492
+                foreach ($info['settings']['admin-section'] as $section) {
493
+                    $settingsManager->registerSection('admin', $section);
494
+                }
495
+            }
496
+            if (!empty($info['settings']['personal'])) {
497
+                foreach ($info['settings']['personal'] as $setting) {
498
+                    $settingsManager->registerSetting('personal', $setting);
499
+                }
500
+            }
501
+            if (!empty($info['settings']['personal-section'])) {
502
+                foreach ($info['settings']['personal-section'] as $section) {
503
+                    $settingsManager->registerSection('personal', $section);
504
+                }
505
+            }
506
+        }
507
+
508
+        if (!empty($info['collaboration']['plugins'])) {
509
+            // deal with one or many plugin entries
510
+            $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
511
+                [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
512
+            $collaboratorSearch = null;
513
+            $autoCompleteManager = null;
514
+            foreach ($plugins as $plugin) {
515
+                if ($plugin['@attributes']['type'] === 'collaborator-search') {
516
+                    $pluginInfo = [
517
+                        'shareType' => $plugin['@attributes']['share-type'],
518
+                        'class' => $plugin['@value'],
519
+                    ];
520
+                    $collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
521
+                    $collaboratorSearch->registerPlugin($pluginInfo);
522
+                } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
523
+                    $autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
524
+                    $autoCompleteManager->registerSorter($plugin['@value']);
525
+                }
526
+            }
527
+        }
528
+        $eventLogger->end("bootstrap:load_app:$app:info");
529
+
530
+        $eventLogger->end("bootstrap:load_app:$app");
531
+    }
532
+
533
+    /**
534
+     * Check if an app is loaded
535
+     * @param string $app app id
536
+     * @since 26.0.0
537
+     */
538
+    public function isAppLoaded(string $app): bool {
539
+        return isset($this->loadedApps[$app]);
540
+    }
541
+
542
+    /**
543
+     * Enable an app for every user
544
+     *
545
+     * @param string $appId
546
+     * @param bool $forceEnable
547
+     * @throws AppPathNotFoundException
548
+     */
549
+    public function enableApp(string $appId, bool $forceEnable = false): void {
550
+        // Check if app exists
551
+        $this->getAppPath($appId);
552
+
553
+        if ($forceEnable) {
554
+            $this->overwriteNextcloudRequirement($appId);
555
+        }
556
+
557
+        $this->enabledAppsCache[$appId] = 'yes';
558
+        $this->getAppConfig()->setValue($appId, 'enabled', 'yes');
559
+        $this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
560
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
561
+            ManagerEvent::EVENT_APP_ENABLE, $appId
562
+        ));
563
+        $this->clearAppsCache();
564
+    }
565
+
566
+    /**
567
+     * Whether a list of types contains a protected app type
568
+     *
569
+     * @param string[] $types
570
+     * @return bool
571
+     */
572
+    public function hasProtectedAppType($types) {
573
+        if (empty($types)) {
574
+            return false;
575
+        }
576
+
577
+        $protectedTypes = array_intersect($this->protectedAppTypes, $types);
578
+        return !empty($protectedTypes);
579
+    }
580
+
581
+    /**
582
+     * Enable an app only for specific groups
583
+     *
584
+     * @param string $appId
585
+     * @param IGroup[] $groups
586
+     * @param bool $forceEnable
587
+     * @throws \InvalidArgumentException if app can't be enabled for groups
588
+     * @throws AppPathNotFoundException
589
+     */
590
+    public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
591
+        // Check if app exists
592
+        $this->getAppPath($appId);
593
+
594
+        $info = $this->getAppInfo($appId);
595
+        if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
596
+            throw new \InvalidArgumentException("$appId can't be enabled for groups.");
597
+        }
598
+
599
+        if ($forceEnable) {
600
+            $this->overwriteNextcloudRequirement($appId);
601
+        }
602
+
603
+        /** @var string[] $groupIds */
604
+        $groupIds = array_map(function ($group) {
605
+            /** @var IGroup $group */
606
+            return ($group instanceof IGroup)
607
+                ? $group->getGID()
608
+                : $group;
609
+        }, $groups);
610
+
611
+        $this->enabledAppsCache[$appId] = json_encode($groupIds);
612
+        $this->getAppConfig()->setValue($appId, 'enabled', json_encode($groupIds));
613
+        $this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
614
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
615
+            ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
616
+        ));
617
+        $this->clearAppsCache();
618
+    }
619
+
620
+    /**
621
+     * Disable an app for every user
622
+     *
623
+     * @param string $appId
624
+     * @param bool $automaticDisabled
625
+     * @throws \Exception if app can't be disabled
626
+     */
627
+    public function disableApp($appId, $automaticDisabled = false): void {
628
+        if ($this->isAlwaysEnabled($appId)) {
629
+            throw new \Exception("$appId can't be disabled.");
630
+        }
631
+
632
+        if ($automaticDisabled) {
633
+            $previousSetting = $this->getAppConfig()->getValue($appId, 'enabled', 'yes');
634
+            if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
635
+                $previousSetting = json_decode($previousSetting, true);
636
+            }
637
+            $this->autoDisabledApps[$appId] = $previousSetting;
638
+        }
639
+
640
+        unset($this->enabledAppsCache[$appId]);
641
+        $this->getAppConfig()->setValue($appId, 'enabled', 'no');
642
+
643
+        // run uninstall steps
644
+        $appData = $this->getAppInfo($appId);
645
+        if (!is_null($appData)) {
646
+            \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
647
+        }
648
+
649
+        $this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
650
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
651
+            ManagerEvent::EVENT_APP_DISABLE, $appId
652
+        ));
653
+        $this->clearAppsCache();
654
+    }
655
+
656
+    /**
657
+     * Get the directory for the given app.
658
+     *
659
+     * @throws AppPathNotFoundException if app folder can't be found
660
+     */
661
+    public function getAppPath(string $appId): string {
662
+        $appPath = \OC_App::getAppPath($appId);
663
+        if ($appPath === false) {
664
+            throw new AppPathNotFoundException('Could not find path for ' . $appId);
665
+        }
666
+        return $appPath;
667
+    }
668
+
669
+    /**
670
+     * Get the web path for the given app.
671
+     *
672
+     * @param string $appId
673
+     * @return string
674
+     * @throws AppPathNotFoundException if app path can't be found
675
+     */
676
+    public function getAppWebPath(string $appId): string {
677
+        $appWebPath = \OC_App::getAppWebPath($appId);
678
+        if ($appWebPath === false) {
679
+            throw new AppPathNotFoundException('Could not find web path for ' . $appId);
680
+        }
681
+        return $appWebPath;
682
+    }
683
+
684
+    /**
685
+     * Clear the cached list of apps when enabling/disabling an app
686
+     */
687
+    public function clearAppsCache(): void {
688
+        $this->appInfos = [];
689
+    }
690
+
691
+    /**
692
+     * Returns a list of apps that need upgrade
693
+     *
694
+     * @param string $version Nextcloud version as array of version components
695
+     * @return array list of app info from apps that need an upgrade
696
+     *
697
+     * @internal
698
+     */
699
+    public function getAppsNeedingUpgrade($version) {
700
+        $appsToUpgrade = [];
701
+        $apps = $this->getEnabledApps();
702
+        foreach ($apps as $appId) {
703
+            $appInfo = $this->getAppInfo($appId);
704
+            $appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version');
705
+            if ($appDbVersion
706
+                && isset($appInfo['version'])
707
+                && version_compare($appInfo['version'], $appDbVersion, '>')
708
+                && \OC_App::isAppCompatible($version, $appInfo)
709
+            ) {
710
+                $appsToUpgrade[] = $appInfo;
711
+            }
712
+        }
713
+
714
+        return $appsToUpgrade;
715
+    }
716
+
717
+    /**
718
+     * Returns the app information from "appinfo/info.xml".
719
+     *
720
+     * @param string|null $lang
721
+     * @return array|null app info
722
+     */
723
+    public function getAppInfo(string $appId, bool $path = false, $lang = null) {
724
+        if ($path) {
725
+            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.');
726
+        }
727
+        if ($lang === null && isset($this->appInfos[$appId])) {
728
+            return $this->appInfos[$appId];
729
+        }
730
+        try {
731
+            $appPath = $this->getAppPath($appId);
732
+        } catch (AppPathNotFoundException) {
733
+            return null;
734
+        }
735
+        $file = $appPath . '/appinfo/info.xml';
736
+
737
+        $data = $this->getAppInfoByPath($file, $lang);
738
+
739
+        if ($lang === null) {
740
+            $this->appInfos[$appId] = $data;
741
+        }
742
+
743
+        return $data;
744
+    }
745
+
746
+    public function getAppInfoByPath(string $path, ?string $lang = null): ?array {
747
+        if (!str_ends_with($path, '/appinfo/info.xml')) {
748
+            return null;
749
+        }
750
+
751
+        $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
752
+        $data = $parser->parse($path);
753
+
754
+        if (is_array($data)) {
755
+            $data = \OC_App::parseAppInfo($data, $lang);
756
+        }
757
+
758
+        return $data;
759
+    }
760
+
761
+    public function getAppVersion(string $appId, bool $useCache = true): string {
762
+        if (!$useCache || !isset($this->appVersions[$appId])) {
763
+            if ($appId === 'core') {
764
+                $this->appVersions[$appId] = $this->serverVersion->getVersionString();
765
+            } else {
766
+                $appInfo = $this->getAppInfo($appId);
767
+                $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
768
+            }
769
+        }
770
+        return $this->appVersions[$appId];
771
+    }
772
+
773
+    /**
774
+     * Returns the installed versions of all apps
775
+     *
776
+     * @return array<string, string>
777
+     */
778
+    public function getAppInstalledVersions(): array {
779
+        return $this->getAppConfig()->getAppInstalledVersions();
780
+    }
781
+
782
+    /**
783
+     * Returns a list of apps incompatible with the given version
784
+     *
785
+     * @param string $version Nextcloud version as array of version components
786
+     *
787
+     * @return array list of app info from incompatible apps
788
+     *
789
+     * @internal
790
+     */
791
+    public function getIncompatibleApps(string $version): array {
792
+        $apps = $this->getEnabledApps();
793
+        $incompatibleApps = [];
794
+        foreach ($apps as $appId) {
795
+            $info = $this->getAppInfo($appId);
796
+            if ($info === null) {
797
+                $incompatibleApps[] = ['id' => $appId, 'name' => $appId];
798
+            } elseif (!\OC_App::isAppCompatible($version, $info)) {
799
+                $incompatibleApps[] = $info;
800
+            }
801
+        }
802
+        return $incompatibleApps;
803
+    }
804
+
805
+    /**
806
+     * @inheritdoc
807
+     * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
808
+     */
809
+    public function isShipped($appId) {
810
+        $this->loadShippedJson();
811
+        return in_array($appId, $this->shippedApps, true);
812
+    }
813
+
814
+    private function isAlwaysEnabled(string $appId): bool {
815
+        $alwaysEnabled = $this->getAlwaysEnabledApps();
816
+        return in_array($appId, $alwaysEnabled, true);
817
+    }
818
+
819
+    /**
820
+     * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
821
+     * @throws \Exception
822
+     */
823
+    private function loadShippedJson(): void {
824
+        if ($this->shippedApps === null) {
825
+            $shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
826
+            if (!file_exists($shippedJson)) {
827
+                throw new \Exception("File not found: $shippedJson");
828
+            }
829
+            $content = json_decode(file_get_contents($shippedJson), true);
830
+            $this->shippedApps = $content['shippedApps'];
831
+            $this->alwaysEnabled = $content['alwaysEnabled'];
832
+            $this->defaultEnabled = $content['defaultEnabled'];
833
+        }
834
+    }
835
+
836
+    /**
837
+     * @inheritdoc
838
+     */
839
+    public function getAlwaysEnabledApps() {
840
+        $this->loadShippedJson();
841
+        return $this->alwaysEnabled;
842
+    }
843
+
844
+    /**
845
+     * @inheritdoc
846
+     */
847
+    public function isDefaultEnabled(string $appId): bool {
848
+        return (in_array($appId, $this->getDefaultEnabledApps()));
849
+    }
850
+
851
+    /**
852
+     * @inheritdoc
853
+     */
854
+    public function getDefaultEnabledApps(): array {
855
+        $this->loadShippedJson();
856
+
857
+        return $this->defaultEnabled;
858
+    }
859
+
860
+    /**
861
+     * @inheritdoc
862
+     */
863
+    public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
864
+        $id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
865
+        $entry = $this->getNavigationManager()->get($id);
866
+        return (string)$entry['app'];
867
+    }
868
+
869
+    /**
870
+     * @inheritdoc
871
+     */
872
+    public function getDefaultApps(): array {
873
+        $ids = $this->getNavigationManager()->getDefaultEntryIds();
874
+
875
+        return array_values(array_unique(array_map(function (string $id) {
876
+            $entry = $this->getNavigationManager()->get($id);
877
+            return (string)$entry['app'];
878
+        }, $ids)));
879
+    }
880
+
881
+    /**
882
+     * @inheritdoc
883
+     */
884
+    public function setDefaultApps(array $defaultApps): void {
885
+        $entries = $this->getNavigationManager()->getAll();
886
+        $ids = [];
887
+        foreach ($defaultApps as $defaultApp) {
888
+            foreach ($entries as $entry) {
889
+                if ((string)$entry['app'] === $defaultApp) {
890
+                    $ids[] = (string)$entry['id'];
891
+                    break;
892
+                }
893
+            }
894
+        }
895
+        $this->getNavigationManager()->setDefaultEntryIds($ids);
896
+    }
897
+
898
+    public function isBackendRequired(string $backend): bool {
899
+        foreach ($this->appInfos as $appInfo) {
900
+            foreach ($appInfo['dependencies']['backend'] as $appBackend) {
901
+                if ($backend === $appBackend) {
902
+                    return true;
903
+                }
904
+            }
905
+        }
906
+
907
+        return false;
908
+    }
909
+
910
+    /**
911
+     * Clean the appId from forbidden characters
912
+     *
913
+     * @psalm-taint-escape callable
914
+     * @psalm-taint-escape cookie
915
+     * @psalm-taint-escape file
916
+     * @psalm-taint-escape has_quotes
917
+     * @psalm-taint-escape header
918
+     * @psalm-taint-escape html
919
+     * @psalm-taint-escape include
920
+     * @psalm-taint-escape ldap
921
+     * @psalm-taint-escape shell
922
+     * @psalm-taint-escape sql
923
+     * @psalm-taint-escape unserialize
924
+     */
925
+    public function cleanAppId(string $app): string {
926
+        /* Only lowercase alphanumeric is allowed */
927
+        return preg_replace('/(^[0-9_]|[^a-z0-9_]+|_$)/', '', $app);
928
+    }
929 929
 }
Please login to merge, or discard this patch.
Spacing   +21 added lines, -21 removed lines patch added patch discarded remove patch
@@ -92,7 +92,7 @@  discard block
 block discarded – undo
92 92
 	}
93 93
 
94 94
 	public function getAppIcon(string $appId, bool $dark = false): ?string {
95
-		$possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
95
+		$possibleIcons = $dark ? [$appId.'-dark.svg', 'app-dark.svg'] : [$appId.'.svg', 'app.svg'];
96 96
 		$icon = null;
97 97
 		foreach ($possibleIcons as $iconName) {
98 98
 			try {
@@ -141,7 +141,7 @@  discard block
 block discarded – undo
141 141
 				$values[$appId] = 'yes';
142 142
 			}
143 143
 
144
-			$this->enabledAppsCache = array_filter($values, function ($value) {
144
+			$this->enabledAppsCache = array_filter($values, function($value) {
145 145
 				return $value !== 'no';
146 146
 			});
147 147
 			ksort($this->enabledAppsCache);
@@ -177,7 +177,7 @@  discard block
 block discarded – undo
177 177
 
178 178
 		foreach (\OC::$APPSROOTS as $apps_dir) {
179 179
 			if (!is_readable($apps_dir['path'])) {
180
-				$this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']);
180
+				$this->logger->warning('unable to read app folder : '.$apps_dir['path'], ['app' => 'core']);
181 181
 				continue;
182 182
 			}
183 183
 			$dh = opendir($apps_dir['path']);
@@ -186,8 +186,8 @@  discard block
 block discarded – undo
186 186
 				while (($file = readdir($dh)) !== false) {
187 187
 					if (
188 188
 						$file[0] != '.' &&
189
-						is_dir($apps_dir['path'] . '/' . $file) &&
190
-						is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')
189
+						is_dir($apps_dir['path'].'/'.$file) &&
190
+						is_file($apps_dir['path'].'/'.$file.'/appinfo/info.xml')
191 191
 					) {
192 192
 						$apps[] = $file;
193 193
 					}
@@ -206,7 +206,7 @@  discard block
 block discarded – undo
206 206
 	 */
207 207
 	public function getEnabledAppsForUser(IUser $user) {
208 208
 		$apps = $this->getEnabledAppsValues();
209
-		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
209
+		$appsForUser = array_filter($apps, function($enabled) use ($user) {
210 210
 			return $this->checkAppForUser($enabled, $user);
211 211
 		});
212 212
 		return array_keys($appsForUser);
@@ -214,7 +214,7 @@  discard block
 block discarded – undo
214 214
 
215 215
 	public function getEnabledAppsForGroup(IGroup $group): array {
216 216
 		$apps = $this->getEnabledAppsValues();
217
-		$appsForGroups = array_filter($apps, function ($enabled) use ($group) {
217
+		$appsForGroups = array_filter($apps, function($enabled) use ($group) {
218 218
 			return $this->checkAppForGroups($enabled, $group);
219 219
 		});
220 220
 		return array_keys($appsForGroups);
@@ -257,7 +257,7 @@  discard block
 block discarded – undo
257 257
 				try {
258 258
 					$this->loadApp($app);
259 259
 				} catch (\Throwable $e) {
260
-					$this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
260
+					$this->logger->emergency('Error during app loading: '.$e->getMessage(), [
261 261
 						'exception' => $e,
262 262
 						'app' => $app,
263 263
 					]);
@@ -363,7 +363,7 @@  discard block
 block discarded – undo
363 363
 				$jsonError = json_last_error();
364 364
 				$jsonErrorMsg = json_last_error_msg();
365 365
 				// 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)
366
-				$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);
366
+				$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);
367 367
 				return false;
368 368
 			}
369 369
 
@@ -391,7 +391,7 @@  discard block
 block discarded – undo
391 391
 				$jsonError = json_last_error();
392 392
 				$jsonErrorMsg = json_last_error_msg();
393 393
 				// 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)
394
-				$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);
394
+				$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);
395 395
 				return false;
396 396
 			}
397 397
 
@@ -451,7 +451,7 @@  discard block
 block discarded – undo
451 451
 		// in case someone calls loadApp() directly
452 452
 		\OC_App::registerAutoloading($app, $appPath);
453 453
 
454
-		if (is_file($appPath . '/appinfo/app.php')) {
454
+		if (is_file($appPath.'/appinfo/app.php')) {
455 455
 			$this->logger->error('/appinfo/app.php is not supported anymore, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
456 456
 				'app' => $app,
457 457
 			]);
@@ -601,7 +601,7 @@  discard block
 block discarded – undo
601 601
 		}
602 602
 
603 603
 		/** @var string[] $groupIds */
604
-		$groupIds = array_map(function ($group) {
604
+		$groupIds = array_map(function($group) {
605 605
 			/** @var IGroup $group */
606 606
 			return ($group instanceof IGroup)
607 607
 				? $group->getGID()
@@ -661,7 +661,7 @@  discard block
 block discarded – undo
661 661
 	public function getAppPath(string $appId): string {
662 662
 		$appPath = \OC_App::getAppPath($appId);
663 663
 		if ($appPath === false) {
664
-			throw new AppPathNotFoundException('Could not find path for ' . $appId);
664
+			throw new AppPathNotFoundException('Could not find path for '.$appId);
665 665
 		}
666 666
 		return $appPath;
667 667
 	}
@@ -676,7 +676,7 @@  discard block
 block discarded – undo
676 676
 	public function getAppWebPath(string $appId): string {
677 677
 		$appWebPath = \OC_App::getAppWebPath($appId);
678 678
 		if ($appWebPath === false) {
679
-			throw new AppPathNotFoundException('Could not find web path for ' . $appId);
679
+			throw new AppPathNotFoundException('Could not find web path for '.$appId);
680 680
 		}
681 681
 		return $appWebPath;
682 682
 	}
@@ -732,7 +732,7 @@  discard block
 block discarded – undo
732 732
 		} catch (AppPathNotFoundException) {
733 733
 			return null;
734 734
 		}
735
-		$file = $appPath . '/appinfo/info.xml';
735
+		$file = $appPath.'/appinfo/info.xml';
736 736
 
737 737
 		$data = $this->getAppInfoByPath($file, $lang);
738 738
 
@@ -822,7 +822,7 @@  discard block
 block discarded – undo
822 822
 	 */
823 823
 	private function loadShippedJson(): void {
824 824
 		if ($this->shippedApps === null) {
825
-			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
825
+			$shippedJson = \OC::$SERVERROOT.'/core/shipped.json';
826 826
 			if (!file_exists($shippedJson)) {
827 827
 				throw new \Exception("File not found: $shippedJson");
828 828
 			}
@@ -863,7 +863,7 @@  discard block
 block discarded – undo
863 863
 	public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
864 864
 		$id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
865 865
 		$entry = $this->getNavigationManager()->get($id);
866
-		return (string)$entry['app'];
866
+		return (string) $entry['app'];
867 867
 	}
868 868
 
869 869
 	/**
@@ -872,9 +872,9 @@  discard block
 block discarded – undo
872 872
 	public function getDefaultApps(): array {
873 873
 		$ids = $this->getNavigationManager()->getDefaultEntryIds();
874 874
 
875
-		return array_values(array_unique(array_map(function (string $id) {
875
+		return array_values(array_unique(array_map(function(string $id) {
876 876
 			$entry = $this->getNavigationManager()->get($id);
877
-			return (string)$entry['app'];
877
+			return (string) $entry['app'];
878 878
 		}, $ids)));
879 879
 	}
880 880
 
@@ -886,8 +886,8 @@  discard block
 block discarded – undo
886 886
 		$ids = [];
887 887
 		foreach ($defaultApps as $defaultApp) {
888 888
 			foreach ($entries as $entry) {
889
-				if ((string)$entry['app'] === $defaultApp) {
890
-					$ids[] = (string)$entry['id'];
889
+				if ((string) $entry['app'] === $defaultApp) {
890
+					$ids[] = (string) $entry['id'];
891 891
 					break;
892 892
 				}
893 893
 			}
Please login to merge, or discard this patch.
lib/private/AppFramework/Bootstrap/Coordinator.php 1 patch
Indentation   +137 added lines, -137 removed lines patch added patch discarded remove patch
@@ -27,151 +27,151 @@
 block discarded – undo
27 27
 use function in_array;
28 28
 
29 29
 class Coordinator {
30
-	/** @var RegistrationContext|null */
31
-	private $registrationContext;
32
-
33
-	/** @var array<string,true> */
34
-	private array $bootedApps = [];
35
-
36
-	public function __construct(
37
-		private IServerContainer $serverContainer,
38
-		private Registry $registry,
39
-		private IManager $dashboardManager,
40
-		private IEventDispatcher $eventDispatcher,
41
-		private IEventLogger $eventLogger,
42
-		private IAppManager $appManager,
43
-		private LoggerInterface $logger,
44
-	) {
45
-	}
46
-
47
-	public function runInitialRegistration(): void {
48
-		$this->registerApps(OC_App::getEnabledApps());
49
-	}
50
-
51
-	public function runLazyRegistration(string $appId): void {
52
-		$this->registerApps([$appId]);
53
-	}
54
-
55
-	/**
56
-	 * @param string[] $appIds
57
-	 */
58
-	private function registerApps(array $appIds): void {
59
-		$this->eventLogger->start('bootstrap:register_apps', '');
60
-		if ($this->registrationContext === null) {
61
-			$this->registrationContext = new RegistrationContext($this->logger);
62
-		}
63
-		$apps = [];
64
-		foreach ($appIds as $appId) {
65
-			$this->eventLogger->start("bootstrap:register_app:$appId", "Register $appId");
66
-			$this->eventLogger->start("bootstrap:register_app:$appId:autoloader", "Setup autoloader for $appId");
67
-			/*
30
+    /** @var RegistrationContext|null */
31
+    private $registrationContext;
32
+
33
+    /** @var array<string,true> */
34
+    private array $bootedApps = [];
35
+
36
+    public function __construct(
37
+        private IServerContainer $serverContainer,
38
+        private Registry $registry,
39
+        private IManager $dashboardManager,
40
+        private IEventDispatcher $eventDispatcher,
41
+        private IEventLogger $eventLogger,
42
+        private IAppManager $appManager,
43
+        private LoggerInterface $logger,
44
+    ) {
45
+    }
46
+
47
+    public function runInitialRegistration(): void {
48
+        $this->registerApps(OC_App::getEnabledApps());
49
+    }
50
+
51
+    public function runLazyRegistration(string $appId): void {
52
+        $this->registerApps([$appId]);
53
+    }
54
+
55
+    /**
56
+     * @param string[] $appIds
57
+     */
58
+    private function registerApps(array $appIds): void {
59
+        $this->eventLogger->start('bootstrap:register_apps', '');
60
+        if ($this->registrationContext === null) {
61
+            $this->registrationContext = new RegistrationContext($this->logger);
62
+        }
63
+        $apps = [];
64
+        foreach ($appIds as $appId) {
65
+            $this->eventLogger->start("bootstrap:register_app:$appId", "Register $appId");
66
+            $this->eventLogger->start("bootstrap:register_app:$appId:autoloader", "Setup autoloader for $appId");
67
+            /*
68 68
 			 * First, we have to enable the app's autoloader
69 69
 			 */
70
-			try {
71
-				$path = $this->appManager->getAppPath($appId);
72
-			} catch (AppPathNotFoundException) {
73
-				// Ignore
74
-				continue;
75
-			}
76
-			OC_App::registerAutoloading($appId, $path);
77
-			$this->eventLogger->end("bootstrap:register_app:$appId:autoloader");
78
-
79
-			/*
70
+            try {
71
+                $path = $this->appManager->getAppPath($appId);
72
+            } catch (AppPathNotFoundException) {
73
+                // Ignore
74
+                continue;
75
+            }
76
+            OC_App::registerAutoloading($appId, $path);
77
+            $this->eventLogger->end("bootstrap:register_app:$appId:autoloader");
78
+
79
+            /*
80 80
 			 * Next we check if there is an application class, and it implements
81 81
 			 * the \OCP\AppFramework\Bootstrap\IBootstrap interface
82 82
 			 */
83
-			$appNameSpace = App::buildAppNamespace($appId);
84
-			$applicationClassName = $appNameSpace . '\\AppInfo\\Application';
85
-			try {
86
-				if (class_exists($applicationClassName) && in_array(IBootstrap::class, class_implements($applicationClassName), true)) {
87
-					$this->eventLogger->start("bootstrap:register_app:$appId:application", "Load `Application` instance for $appId");
88
-					try {
89
-						/** @var IBootstrap|App $application */
90
-						$apps[$appId] = $application = $this->serverContainer->query($applicationClassName);
91
-					} catch (QueryException $e) {
92
-						// Weird, but ok
93
-						$this->eventLogger->end("bootstrap:register_app:$appId");
94
-						continue;
95
-					}
96
-					$this->eventLogger->end("bootstrap:register_app:$appId:application");
97
-
98
-					$this->eventLogger->start("bootstrap:register_app:$appId:register", "`Application::register` for $appId");
99
-					$application->register($this->registrationContext->for($appId));
100
-					$this->eventLogger->end("bootstrap:register_app:$appId:register");
101
-				}
102
-			} catch (Throwable $e) {
103
-				$this->logger->emergency('Error during app service registration: ' . $e->getMessage(), [
104
-					'exception' => $e,
105
-					'app' => $appId,
106
-				]);
107
-				$this->eventLogger->end("bootstrap:register_app:$appId");
108
-				continue;
109
-			}
110
-			$this->eventLogger->end("bootstrap:register_app:$appId");
111
-		}
112
-
113
-		$this->eventLogger->start('bootstrap:register_apps:apply', 'Apply all the registered service by apps');
114
-		/**
115
-		 * Now that all register methods have been called, we can delegate the registrations
116
-		 * to the actual services
117
-		 */
118
-		$this->registrationContext->delegateCapabilityRegistrations($apps);
119
-		$this->registrationContext->delegateCrashReporterRegistrations($apps, $this->registry);
120
-		$this->registrationContext->delegateDashboardPanelRegistrations($this->dashboardManager);
121
-		$this->registrationContext->delegateEventListenerRegistrations($this->eventDispatcher);
122
-		$this->registrationContext->delegateContainerRegistrations($apps);
123
-		$this->eventLogger->end('bootstrap:register_apps:apply');
124
-		$this->eventLogger->end('bootstrap:register_apps');
125
-	}
126
-
127
-	public function getRegistrationContext(): ?RegistrationContext {
128
-		return $this->registrationContext;
129
-	}
130
-
131
-	public function bootApp(string $appId): void {
132
-		if (isset($this->bootedApps[$appId])) {
133
-			return;
134
-		}
135
-		$this->bootedApps[$appId] = true;
136
-
137
-		$appNameSpace = App::buildAppNamespace($appId);
138
-		$applicationClassName = $appNameSpace . '\\AppInfo\\Application';
139
-		if (!class_exists($applicationClassName)) {
140
-			// Nothing to boot
141
-			return;
142
-		}
143
-
144
-		/*
83
+            $appNameSpace = App::buildAppNamespace($appId);
84
+            $applicationClassName = $appNameSpace . '\\AppInfo\\Application';
85
+            try {
86
+                if (class_exists($applicationClassName) && in_array(IBootstrap::class, class_implements($applicationClassName), true)) {
87
+                    $this->eventLogger->start("bootstrap:register_app:$appId:application", "Load `Application` instance for $appId");
88
+                    try {
89
+                        /** @var IBootstrap|App $application */
90
+                        $apps[$appId] = $application = $this->serverContainer->query($applicationClassName);
91
+                    } catch (QueryException $e) {
92
+                        // Weird, but ok
93
+                        $this->eventLogger->end("bootstrap:register_app:$appId");
94
+                        continue;
95
+                    }
96
+                    $this->eventLogger->end("bootstrap:register_app:$appId:application");
97
+
98
+                    $this->eventLogger->start("bootstrap:register_app:$appId:register", "`Application::register` for $appId");
99
+                    $application->register($this->registrationContext->for($appId));
100
+                    $this->eventLogger->end("bootstrap:register_app:$appId:register");
101
+                }
102
+            } catch (Throwable $e) {
103
+                $this->logger->emergency('Error during app service registration: ' . $e->getMessage(), [
104
+                    'exception' => $e,
105
+                    'app' => $appId,
106
+                ]);
107
+                $this->eventLogger->end("bootstrap:register_app:$appId");
108
+                continue;
109
+            }
110
+            $this->eventLogger->end("bootstrap:register_app:$appId");
111
+        }
112
+
113
+        $this->eventLogger->start('bootstrap:register_apps:apply', 'Apply all the registered service by apps');
114
+        /**
115
+         * Now that all register methods have been called, we can delegate the registrations
116
+         * to the actual services
117
+         */
118
+        $this->registrationContext->delegateCapabilityRegistrations($apps);
119
+        $this->registrationContext->delegateCrashReporterRegistrations($apps, $this->registry);
120
+        $this->registrationContext->delegateDashboardPanelRegistrations($this->dashboardManager);
121
+        $this->registrationContext->delegateEventListenerRegistrations($this->eventDispatcher);
122
+        $this->registrationContext->delegateContainerRegistrations($apps);
123
+        $this->eventLogger->end('bootstrap:register_apps:apply');
124
+        $this->eventLogger->end('bootstrap:register_apps');
125
+    }
126
+
127
+    public function getRegistrationContext(): ?RegistrationContext {
128
+        return $this->registrationContext;
129
+    }
130
+
131
+    public function bootApp(string $appId): void {
132
+        if (isset($this->bootedApps[$appId])) {
133
+            return;
134
+        }
135
+        $this->bootedApps[$appId] = true;
136
+
137
+        $appNameSpace = App::buildAppNamespace($appId);
138
+        $applicationClassName = $appNameSpace . '\\AppInfo\\Application';
139
+        if (!class_exists($applicationClassName)) {
140
+            // Nothing to boot
141
+            return;
142
+        }
143
+
144
+        /*
145 145
 		 * Now it is time to fetch an instance of the App class. For classes
146 146
 		 * that implement \OCP\AppFramework\Bootstrap\IBootstrap this means
147 147
 		 * the instance was already created for register, but any other
148 148
 		 * (legacy) code will now do their magic via the constructor.
149 149
 		 */
150
-		$this->eventLogger->start('bootstrap:boot_app:' . $appId, "Call `Application::boot` for $appId");
151
-		try {
152
-			/** @var App $application */
153
-			$application = $this->serverContainer->query($applicationClassName);
154
-			if ($application instanceof IBootstrap) {
155
-				/** @var BootContext $context */
156
-				$context = new BootContext($application->getContainer());
157
-				$application->boot($context);
158
-			}
159
-		} catch (QueryException $e) {
160
-			$this->logger->error("Could not boot $appId: " . $e->getMessage(), [
161
-				'exception' => $e,
162
-			]);
163
-		} catch (Throwable $e) {
164
-			$this->logger->emergency("Could not boot $appId: " . $e->getMessage(), [
165
-				'exception' => $e,
166
-			]);
167
-		}
168
-		$this->eventLogger->end('bootstrap:boot_app:' . $appId);
169
-	}
170
-
171
-	public function isBootable(string $appId) {
172
-		$appNameSpace = App::buildAppNamespace($appId);
173
-		$applicationClassName = $appNameSpace . '\\AppInfo\\Application';
174
-		return class_exists($applicationClassName) &&
175
-			in_array(IBootstrap::class, class_implements($applicationClassName), true);
176
-	}
150
+        $this->eventLogger->start('bootstrap:boot_app:' . $appId, "Call `Application::boot` for $appId");
151
+        try {
152
+            /** @var App $application */
153
+            $application = $this->serverContainer->query($applicationClassName);
154
+            if ($application instanceof IBootstrap) {
155
+                /** @var BootContext $context */
156
+                $context = new BootContext($application->getContainer());
157
+                $application->boot($context);
158
+            }
159
+        } catch (QueryException $e) {
160
+            $this->logger->error("Could not boot $appId: " . $e->getMessage(), [
161
+                'exception' => $e,
162
+            ]);
163
+        } catch (Throwable $e) {
164
+            $this->logger->emergency("Could not boot $appId: " . $e->getMessage(), [
165
+                'exception' => $e,
166
+            ]);
167
+        }
168
+        $this->eventLogger->end('bootstrap:boot_app:' . $appId);
169
+    }
170
+
171
+    public function isBootable(string $appId) {
172
+        $appNameSpace = App::buildAppNamespace($appId);
173
+        $applicationClassName = $appNameSpace . '\\AppInfo\\Application';
174
+        return class_exists($applicationClassName) &&
175
+            in_array(IBootstrap::class, class_implements($applicationClassName), true);
176
+    }
177 177
 }
Please login to merge, or discard this patch.
lib/public/AppFramework/Bootstrap/IBootstrap.php 1 patch
Indentation   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -12,22 +12,22 @@
 block discarded – undo
12 12
  * @since 20.0.0
13 13
  */
14 14
 interface IBootstrap {
15
-	/**
16
-	 * @param IRegistrationContext $context
17
-	 *
18
-	 * @since 20.0.0
19
-	 */
20
-	public function register(IRegistrationContext $context): void;
15
+    /**
16
+     * @param IRegistrationContext $context
17
+     *
18
+     * @since 20.0.0
19
+     */
20
+    public function register(IRegistrationContext $context): void;
21 21
 
22
-	/**
23
-	 * Boot the application
24
-	 *
25
-	 * At this stage you can assume that all services are registered and the DI
26
-	 * container(s) are ready to be queried.
27
-	 *
28
-	 * @param IBootContext $context
29
-	 *
30
-	 * @since 20.0.0
31
-	 */
32
-	public function boot(IBootContext $context): void;
22
+    /**
23
+     * Boot the application
24
+     *
25
+     * At this stage you can assume that all services are registered and the DI
26
+     * container(s) are ready to be queried.
27
+     *
28
+     * @param IBootContext $context
29
+     *
30
+     * @since 20.0.0
31
+     */
32
+    public function boot(IBootContext $context): void;
33 33
 }
Please login to merge, or discard this patch.