Passed
Push — master ( 2e111e...ffa30c )
by Blizzz
16:32 queued 12s
created
lib/private/App/AppManager.php 1 patch
Indentation   +787 added lines, -787 removed lines patch added patch discarded remove patch
@@ -62,791 +62,791 @@
 block discarded – undo
62 62
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
63 63
 
64 64
 class AppManager implements IAppManager {
65
-	/**
66
-	 * Apps with these types can not be enabled for certain groups only
67
-	 * @var string[]
68
-	 */
69
-	protected $protectedAppTypes = [
70
-		'filesystem',
71
-		'prelogin',
72
-		'authentication',
73
-		'logging',
74
-		'prevent_group_restriction',
75
-	];
76
-
77
-	private IUserSession $userSession;
78
-	private IConfig $config;
79
-	private AppConfig $appConfig;
80
-	private IGroupManager $groupManager;
81
-	private ICacheFactory $memCacheFactory;
82
-	private EventDispatcherInterface $legacyDispatcher;
83
-	private IEventDispatcher $dispatcher;
84
-	private LoggerInterface $logger;
85
-
86
-	/** @var string[] $appId => $enabled */
87
-	private array $installedAppsCache = [];
88
-
89
-	/** @var string[]|null */
90
-	private ?array $shippedApps = null;
91
-
92
-	private array $alwaysEnabled = [];
93
-	private array $defaultEnabled = [];
94
-
95
-	/** @var array */
96
-	private array $appInfos = [];
97
-
98
-	/** @var array */
99
-	private array $appVersions = [];
100
-
101
-	/** @var array */
102
-	private array $autoDisabledApps = [];
103
-	private array $appTypes = [];
104
-
105
-	/** @var array<string, true> */
106
-	private array $loadedApps = [];
107
-
108
-	public function __construct(IUserSession $userSession,
109
-								IConfig $config,
110
-								AppConfig $appConfig,
111
-								IGroupManager $groupManager,
112
-								ICacheFactory $memCacheFactory,
113
-								EventDispatcherInterface $legacyDispatcher,
114
-								IEventDispatcher $dispatcher,
115
-								LoggerInterface $logger) {
116
-		$this->userSession = $userSession;
117
-		$this->config = $config;
118
-		$this->appConfig = $appConfig;
119
-		$this->groupManager = $groupManager;
120
-		$this->memCacheFactory = $memCacheFactory;
121
-		$this->legacyDispatcher = $legacyDispatcher;
122
-		$this->dispatcher = $dispatcher;
123
-		$this->logger = $logger;
124
-	}
125
-
126
-	/**
127
-	 * @return string[] $appId => $enabled
128
-	 */
129
-	private function getInstalledAppsValues(): array {
130
-		if (!$this->installedAppsCache) {
131
-			$values = $this->appConfig->getValues(false, 'enabled');
132
-
133
-			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
134
-			foreach ($alwaysEnabledApps as $appId) {
135
-				$values[$appId] = 'yes';
136
-			}
137
-
138
-			$this->installedAppsCache = array_filter($values, function ($value) {
139
-				return $value !== 'no';
140
-			});
141
-			ksort($this->installedAppsCache);
142
-		}
143
-		return $this->installedAppsCache;
144
-	}
145
-
146
-	/**
147
-	 * List all installed apps
148
-	 *
149
-	 * @return string[]
150
-	 */
151
-	public function getInstalledApps() {
152
-		return array_keys($this->getInstalledAppsValues());
153
-	}
154
-
155
-	/**
156
-	 * List all apps enabled for a user
157
-	 *
158
-	 * @param \OCP\IUser $user
159
-	 * @return string[]
160
-	 */
161
-	public function getEnabledAppsForUser(IUser $user) {
162
-		$apps = $this->getInstalledAppsValues();
163
-		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
164
-			return $this->checkAppForUser($enabled, $user);
165
-		});
166
-		return array_keys($appsForUser);
167
-	}
168
-
169
-	/**
170
-	 * @param IGroup $group
171
-	 * @return array
172
-	 */
173
-	public function getEnabledAppsForGroup(IGroup $group): array {
174
-		$apps = $this->getInstalledAppsValues();
175
-		$appsForGroups = array_filter($apps, function ($enabled) use ($group) {
176
-			return $this->checkAppForGroups($enabled, $group);
177
-		});
178
-		return array_keys($appsForGroups);
179
-	}
180
-
181
-	/**
182
-	 * Loads all apps
183
-	 *
184
-	 * @param string[] $types
185
-	 * @return bool
186
-	 *
187
-	 * This function walks through the Nextcloud directory and loads all apps
188
-	 * it can find. A directory contains an app if the file /appinfo/info.xml
189
-	 * exists.
190
-	 *
191
-	 * if $types is set to non-empty array, only apps of those types will be loaded
192
-	 */
193
-	public function loadApps(array $types = []): bool {
194
-		if ($this->config->getSystemValueBool('maintenance', false)) {
195
-			return false;
196
-		}
197
-		// Load the enabled apps here
198
-		$apps = \OC_App::getEnabledApps();
199
-
200
-		// Add each apps' folder as allowed class path
201
-		foreach ($apps as $app) {
202
-			// If the app is already loaded then autoloading it makes no sense
203
-			if (!$this->isAppLoaded($app)) {
204
-				$path = \OC_App::getAppPath($app);
205
-				if ($path !== false) {
206
-					\OC_App::registerAutoloading($app, $path);
207
-				}
208
-			}
209
-		}
210
-
211
-		// prevent app.php from printing output
212
-		ob_start();
213
-		foreach ($apps as $app) {
214
-			if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
215
-				try {
216
-					$this->loadApp($app);
217
-				} catch (\Throwable $e) {
218
-					$this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
219
-						'exception' => $e,
220
-						'app' => $app,
221
-					]);
222
-				}
223
-			}
224
-		}
225
-		ob_end_clean();
226
-
227
-		return true;
228
-	}
229
-
230
-	/**
231
-	 * check if an app is of a specific type
232
-	 *
233
-	 * @param string $app
234
-	 * @param array $types
235
-	 * @return bool
236
-	 */
237
-	public function isType(string $app, array $types): bool {
238
-		$appTypes = $this->getAppTypes($app);
239
-		foreach ($types as $type) {
240
-			if (in_array($type, $appTypes, true)) {
241
-				return true;
242
-			}
243
-		}
244
-		return false;
245
-	}
246
-
247
-	/**
248
-	 * get the types of an app
249
-	 *
250
-	 * @param string $app
251
-	 * @return string[]
252
-	 */
253
-	private function getAppTypes(string $app): array {
254
-		//load the cache
255
-		if (count($this->appTypes) === 0) {
256
-			$this->appTypes = $this->appConfig->getValues(false, 'types') ?: [];
257
-		}
258
-
259
-		if (isset($this->appTypes[$app])) {
260
-			return explode(',', $this->appTypes[$app]);
261
-		}
262
-
263
-		return [];
264
-	}
265
-
266
-	/**
267
-	 * @return array
268
-	 */
269
-	public function getAutoDisabledApps(): array {
270
-		return $this->autoDisabledApps;
271
-	}
272
-
273
-	/**
274
-	 * @param string $appId
275
-	 * @return array
276
-	 */
277
-	public function getAppRestriction(string $appId): array {
278
-		$values = $this->getInstalledAppsValues();
279
-
280
-		if (!isset($values[$appId])) {
281
-			return [];
282
-		}
283
-
284
-		if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
285
-			return [];
286
-		}
287
-		return json_decode($values[$appId], true);
288
-	}
289
-
290
-
291
-	/**
292
-	 * Check if an app is enabled for user
293
-	 *
294
-	 * @param string $appId
295
-	 * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used
296
-	 * @return bool
297
-	 */
298
-	public function isEnabledForUser($appId, $user = null) {
299
-		if ($this->isAlwaysEnabled($appId)) {
300
-			return true;
301
-		}
302
-		if ($user === null) {
303
-			$user = $this->userSession->getUser();
304
-		}
305
-		$installedApps = $this->getInstalledAppsValues();
306
-		if (isset($installedApps[$appId])) {
307
-			return $this->checkAppForUser($installedApps[$appId], $user);
308
-		} else {
309
-			return false;
310
-		}
311
-	}
312
-
313
-	private function checkAppForUser(string $enabled, ?IUser $user): bool {
314
-		if ($enabled === 'yes') {
315
-			return true;
316
-		} elseif ($user === null) {
317
-			return false;
318
-		} else {
319
-			if (empty($enabled)) {
320
-				return false;
321
-			}
322
-
323
-			$groupIds = json_decode($enabled);
324
-
325
-			if (!is_array($groupIds)) {
326
-				$jsonError = json_last_error();
327
-				$this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError);
328
-				return false;
329
-			}
330
-
331
-			$userGroups = $this->groupManager->getUserGroupIds($user);
332
-			foreach ($userGroups as $groupId) {
333
-				if (in_array($groupId, $groupIds, true)) {
334
-					return true;
335
-				}
336
-			}
337
-			return false;
338
-		}
339
-	}
340
-
341
-	private function checkAppForGroups(string $enabled, IGroup $group): bool {
342
-		if ($enabled === 'yes') {
343
-			return true;
344
-		} else {
345
-			if (empty($enabled)) {
346
-				return false;
347
-			}
348
-
349
-			$groupIds = json_decode($enabled);
350
-
351
-			if (!is_array($groupIds)) {
352
-				$jsonError = json_last_error();
353
-				$this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError);
354
-				return false;
355
-			}
356
-
357
-			return in_array($group->getGID(), $groupIds);
358
-		}
359
-	}
360
-
361
-	/**
362
-	 * Check if an app is enabled in the instance
363
-	 *
364
-	 * Notice: This actually checks if the app is enabled and not only if it is installed.
365
-	 *
366
-	 * @param string $appId
367
-	 * @param IGroup[]|String[] $groups
368
-	 * @return bool
369
-	 */
370
-	public function isInstalled($appId) {
371
-		$installedApps = $this->getInstalledAppsValues();
372
-		return isset($installedApps[$appId]);
373
-	}
374
-
375
-	public function ignoreNextcloudRequirementForApp(string $appId): void {
376
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
377
-		if (!in_array($appId, $ignoreMaxApps, true)) {
378
-			$ignoreMaxApps[] = $appId;
379
-			$this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
380
-		}
381
-	}
382
-
383
-	public function loadApp(string $app): void {
384
-		if (isset($this->loadedApps[$app])) {
385
-			return;
386
-		}
387
-		$this->loadedApps[$app] = true;
388
-		$appPath = \OC_App::getAppPath($app);
389
-		if ($appPath === false) {
390
-			return;
391
-		}
392
-		$eventLogger = \OC::$server->get(\OCP\Diagnostics\IEventLogger::class);
393
-		$eventLogger->start("bootstrap:load_app:$app", "Load $app");
394
-
395
-		// in case someone calls loadApp() directly
396
-		\OC_App::registerAutoloading($app, $appPath);
397
-
398
-		/** @var Coordinator $coordinator */
399
-		$coordinator = \OC::$server->get(Coordinator::class);
400
-		$isBootable = $coordinator->isBootable($app);
401
-
402
-		$hasAppPhpFile = is_file($appPath . '/appinfo/app.php');
403
-
404
-		$eventLogger = \OC::$server->get(IEventLogger::class);
405
-		$eventLogger->start('bootstrap:load_app_' . $app, 'Load app: ' . $app);
406
-		if ($isBootable && $hasAppPhpFile) {
407
-			$this->logger->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [
408
-				'app' => $app,
409
-			]);
410
-		} elseif ($hasAppPhpFile) {
411
-			$eventLogger->start("bootstrap:load_app:$app:app.php", "Load legacy app.php app $app");
412
-			$this->logger->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
413
-				'app' => $app,
414
-			]);
415
-			try {
416
-				self::requireAppFile($appPath);
417
-			} catch (\Throwable $ex) {
418
-				if ($ex instanceof ServerNotAvailableException) {
419
-					throw $ex;
420
-				}
421
-				if (!$this->isShipped($app) && !$this->isType($app, ['authentication'])) {
422
-					$this->logger->error("App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(), [
423
-						'exception' => $ex,
424
-					]);
425
-
426
-					// Only disable apps which are not shipped and that are not authentication apps
427
-					$this->disableApp($app, true);
428
-				} else {
429
-					$this->logger->error("App $app threw an error during app.php load: " . $ex->getMessage(), [
430
-						'exception' => $ex,
431
-					]);
432
-				}
433
-			}
434
-			$eventLogger->end("bootstrap:load_app:$app:app.php");
435
-		}
436
-
437
-		$coordinator->bootApp($app);
438
-
439
-		$eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
440
-		$info = $this->getAppInfo($app);
441
-		if (!empty($info['activity'])) {
442
-			$activityManager = \OC::$server->get(IActivityManager::class);
443
-			if (!empty($info['activity']['filters'])) {
444
-				foreach ($info['activity']['filters'] as $filter) {
445
-					$activityManager->registerFilter($filter);
446
-				}
447
-			}
448
-			if (!empty($info['activity']['settings'])) {
449
-				foreach ($info['activity']['settings'] as $setting) {
450
-					$activityManager->registerSetting($setting);
451
-				}
452
-			}
453
-			if (!empty($info['activity']['providers'])) {
454
-				foreach ($info['activity']['providers'] as $provider) {
455
-					$activityManager->registerProvider($provider);
456
-				}
457
-			}
458
-		}
459
-
460
-		if (!empty($info['settings'])) {
461
-			$settingsManager = \OC::$server->get(ISettingsManager::class);
462
-			if (!empty($info['settings']['admin'])) {
463
-				foreach ($info['settings']['admin'] as $setting) {
464
-					$settingsManager->registerSetting('admin', $setting);
465
-				}
466
-			}
467
-			if (!empty($info['settings']['admin-section'])) {
468
-				foreach ($info['settings']['admin-section'] as $section) {
469
-					$settingsManager->registerSection('admin', $section);
470
-				}
471
-			}
472
-			if (!empty($info['settings']['personal'])) {
473
-				foreach ($info['settings']['personal'] as $setting) {
474
-					$settingsManager->registerSetting('personal', $setting);
475
-				}
476
-			}
477
-			if (!empty($info['settings']['personal-section'])) {
478
-				foreach ($info['settings']['personal-section'] as $section) {
479
-					$settingsManager->registerSection('personal', $section);
480
-				}
481
-			}
482
-		}
483
-
484
-		if (!empty($info['collaboration']['plugins'])) {
485
-			// deal with one or many plugin entries
486
-			$plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
487
-				[$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
488
-			$collaboratorSearch = null;
489
-			$autoCompleteManager = null;
490
-			foreach ($plugins as $plugin) {
491
-				if ($plugin['@attributes']['type'] === 'collaborator-search') {
492
-					$pluginInfo = [
493
-						'shareType' => $plugin['@attributes']['share-type'],
494
-						'class' => $plugin['@value'],
495
-					];
496
-					$collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
497
-					$collaboratorSearch->registerPlugin($pluginInfo);
498
-				} elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
499
-					$autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
500
-					$autoCompleteManager->registerSorter($plugin['@value']);
501
-				}
502
-			}
503
-		}
504
-		$eventLogger->end("bootstrap:load_app:$app:info");
505
-
506
-		$eventLogger->end("bootstrap:load_app:$app");
507
-	}
508
-	/**
509
-	 * Check if an app is loaded
510
-	 * @param string $app app id
511
-	 * @since 26.0.0
512
-	 */
513
-	public function isAppLoaded(string $app): bool {
514
-		return isset($this->loadedApps[$app]);
515
-	}
516
-
517
-	/**
518
-	 * Load app.php from the given app
519
-	 *
520
-	 * @param string $app app name
521
-	 * @throws \Error
522
-	 */
523
-	private static function requireAppFile(string $app): void {
524
-		// encapsulated here to avoid variable scope conflicts
525
-		require_once $app . '/appinfo/app.php';
526
-	}
527
-
528
-	/**
529
-	 * Enable an app for every user
530
-	 *
531
-	 * @param string $appId
532
-	 * @param bool $forceEnable
533
-	 * @throws AppPathNotFoundException
534
-	 */
535
-	public function enableApp(string $appId, bool $forceEnable = false): void {
536
-		// Check if app exists
537
-		$this->getAppPath($appId);
538
-
539
-		if ($forceEnable) {
540
-			$this->ignoreNextcloudRequirementForApp($appId);
541
-		}
542
-
543
-		$this->installedAppsCache[$appId] = 'yes';
544
-		$this->appConfig->setValue($appId, 'enabled', 'yes');
545
-		$this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
546
-		$this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
547
-			ManagerEvent::EVENT_APP_ENABLE, $appId
548
-		));
549
-		$this->clearAppsCache();
550
-	}
551
-
552
-	/**
553
-	 * Whether a list of types contains a protected app type
554
-	 *
555
-	 * @param string[] $types
556
-	 * @return bool
557
-	 */
558
-	public function hasProtectedAppType($types) {
559
-		if (empty($types)) {
560
-			return false;
561
-		}
562
-
563
-		$protectedTypes = array_intersect($this->protectedAppTypes, $types);
564
-		return !empty($protectedTypes);
565
-	}
566
-
567
-	/**
568
-	 * Enable an app only for specific groups
569
-	 *
570
-	 * @param string $appId
571
-	 * @param IGroup[] $groups
572
-	 * @param bool $forceEnable
573
-	 * @throws \InvalidArgumentException if app can't be enabled for groups
574
-	 * @throws AppPathNotFoundException
575
-	 */
576
-	public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
577
-		// Check if app exists
578
-		$this->getAppPath($appId);
579
-
580
-		$info = $this->getAppInfo($appId);
581
-		if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
582
-			throw new \InvalidArgumentException("$appId can't be enabled for groups.");
583
-		}
584
-
585
-		if ($forceEnable) {
586
-			$this->ignoreNextcloudRequirementForApp($appId);
587
-		}
588
-
589
-		/** @var string[] $groupIds */
590
-		$groupIds = array_map(function ($group) {
591
-			/** @var IGroup $group */
592
-			return ($group instanceof IGroup)
593
-				? $group->getGID()
594
-				: $group;
595
-		}, $groups);
596
-
597
-		$this->installedAppsCache[$appId] = json_encode($groupIds);
598
-		$this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
599
-		$this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
600
-		$this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
601
-			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
602
-		));
603
-		$this->clearAppsCache();
604
-	}
605
-
606
-	/**
607
-	 * Disable an app for every user
608
-	 *
609
-	 * @param string $appId
610
-	 * @param bool $automaticDisabled
611
-	 * @throws \Exception if app can't be disabled
612
-	 */
613
-	public function disableApp($appId, $automaticDisabled = false) {
614
-		if ($this->isAlwaysEnabled($appId)) {
615
-			throw new \Exception("$appId can't be disabled.");
616
-		}
617
-
618
-		if ($automaticDisabled) {
619
-			$previousSetting = $this->appConfig->getValue($appId, 'enabled', 'yes');
620
-			if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
621
-				$previousSetting = json_decode($previousSetting, true);
622
-			}
623
-			$this->autoDisabledApps[$appId] = $previousSetting;
624
-		}
625
-
626
-		unset($this->installedAppsCache[$appId]);
627
-		$this->appConfig->setValue($appId, 'enabled', 'no');
628
-
629
-		// run uninstall steps
630
-		$appData = $this->getAppInfo($appId);
631
-		if (!is_null($appData)) {
632
-			\OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
633
-		}
634
-
635
-		$this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
636
-		$this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
637
-			ManagerEvent::EVENT_APP_DISABLE, $appId
638
-		));
639
-		$this->clearAppsCache();
640
-	}
641
-
642
-	/**
643
-	 * Get the directory for the given app.
644
-	 *
645
-	 * @param string $appId
646
-	 * @return string
647
-	 * @throws AppPathNotFoundException if app folder can't be found
648
-	 */
649
-	public function getAppPath($appId) {
650
-		$appPath = \OC_App::getAppPath($appId);
651
-		if ($appPath === false) {
652
-			throw new AppPathNotFoundException('Could not find path for ' . $appId);
653
-		}
654
-		return $appPath;
655
-	}
656
-
657
-	/**
658
-	 * Get the web path for the given app.
659
-	 *
660
-	 * @param string $appId
661
-	 * @return string
662
-	 * @throws AppPathNotFoundException if app path can't be found
663
-	 */
664
-	public function getAppWebPath(string $appId): string {
665
-		$appWebPath = \OC_App::getAppWebPath($appId);
666
-		if ($appWebPath === false) {
667
-			throw new AppPathNotFoundException('Could not find web path for ' . $appId);
668
-		}
669
-		return $appWebPath;
670
-	}
671
-
672
-	/**
673
-	 * Clear the cached list of apps when enabling/disabling an app
674
-	 */
675
-	public function clearAppsCache() {
676
-		$this->appInfos = [];
677
-	}
678
-
679
-	/**
680
-	 * Returns a list of apps that need upgrade
681
-	 *
682
-	 * @param string $version Nextcloud version as array of version components
683
-	 * @return array list of app info from apps that need an upgrade
684
-	 *
685
-	 * @internal
686
-	 */
687
-	public function getAppsNeedingUpgrade($version) {
688
-		$appsToUpgrade = [];
689
-		$apps = $this->getInstalledApps();
690
-		foreach ($apps as $appId) {
691
-			$appInfo = $this->getAppInfo($appId);
692
-			$appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
693
-			if ($appDbVersion
694
-				&& isset($appInfo['version'])
695
-				&& version_compare($appInfo['version'], $appDbVersion, '>')
696
-				&& \OC_App::isAppCompatible($version, $appInfo)
697
-			) {
698
-				$appsToUpgrade[] = $appInfo;
699
-			}
700
-		}
701
-
702
-		return $appsToUpgrade;
703
-	}
704
-
705
-	/**
706
-	 * Returns the app information from "appinfo/info.xml".
707
-	 *
708
-	 * @param string $appId app id
709
-	 *
710
-	 * @param bool $path
711
-	 * @param null $lang
712
-	 * @return array|null app info
713
-	 */
714
-	public function getAppInfo(string $appId, bool $path = false, $lang = null) {
715
-		if ($path) {
716
-			$file = $appId;
717
-		} else {
718
-			if ($lang === null && isset($this->appInfos[$appId])) {
719
-				return $this->appInfos[$appId];
720
-			}
721
-			try {
722
-				$appPath = $this->getAppPath($appId);
723
-			} catch (AppPathNotFoundException $e) {
724
-				return null;
725
-			}
726
-			$file = $appPath . '/appinfo/info.xml';
727
-		}
728
-
729
-		$parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
730
-		$data = $parser->parse($file);
731
-
732
-		if (is_array($data)) {
733
-			$data = \OC_App::parseAppInfo($data, $lang);
734
-		}
735
-
736
-		if ($lang === null) {
737
-			$this->appInfos[$appId] = $data;
738
-		}
739
-
740
-		return $data;
741
-	}
742
-
743
-	public function getAppVersion(string $appId, bool $useCache = true): string {
744
-		if (!$useCache || !isset($this->appVersions[$appId])) {
745
-			$appInfo = $this->getAppInfo($appId);
746
-			$this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
747
-		}
748
-		return $this->appVersions[$appId];
749
-	}
750
-
751
-	/**
752
-	 * Returns a list of apps incompatible with the given version
753
-	 *
754
-	 * @param string $version Nextcloud version as array of version components
755
-	 *
756
-	 * @return array list of app info from incompatible apps
757
-	 *
758
-	 * @internal
759
-	 */
760
-	public function getIncompatibleApps(string $version): array {
761
-		$apps = $this->getInstalledApps();
762
-		$incompatibleApps = [];
763
-		foreach ($apps as $appId) {
764
-			$info = $this->getAppInfo($appId);
765
-			if ($info === null) {
766
-				$incompatibleApps[] = ['id' => $appId, 'name' => $appId];
767
-			} elseif (!\OC_App::isAppCompatible($version, $info)) {
768
-				$incompatibleApps[] = $info;
769
-			}
770
-		}
771
-		return $incompatibleApps;
772
-	}
773
-
774
-	/**
775
-	 * @inheritdoc
776
-	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
777
-	 */
778
-	public function isShipped($appId) {
779
-		$this->loadShippedJson();
780
-		return in_array($appId, $this->shippedApps, true);
781
-	}
782
-
783
-	private function isAlwaysEnabled(string $appId): bool {
784
-		$alwaysEnabled = $this->getAlwaysEnabledApps();
785
-		return in_array($appId, $alwaysEnabled, true);
786
-	}
787
-
788
-	/**
789
-	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
790
-	 * @throws \Exception
791
-	 */
792
-	private function loadShippedJson(): void {
793
-		if ($this->shippedApps === null) {
794
-			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
795
-			if (!file_exists($shippedJson)) {
796
-				throw new \Exception("File not found: $shippedJson");
797
-			}
798
-			$content = json_decode(file_get_contents($shippedJson), true);
799
-			$this->shippedApps = $content['shippedApps'];
800
-			$this->alwaysEnabled = $content['alwaysEnabled'];
801
-			$this->defaultEnabled = $content['defaultEnabled'];
802
-		}
803
-	}
804
-
805
-	/**
806
-	 * @inheritdoc
807
-	 */
808
-	public function getAlwaysEnabledApps() {
809
-		$this->loadShippedJson();
810
-		return $this->alwaysEnabled;
811
-	}
812
-
813
-	/**
814
-	 * @inheritdoc
815
-	 */
816
-	public function isDefaultEnabled(string $appId): bool {
817
-		return (in_array($appId, $this->getDefaultEnabledApps()));
818
-	}
819
-
820
-	/**
821
-	 * @inheritdoc
822
-	 */
823
-	public function getDefaultEnabledApps():array {
824
-		$this->loadShippedJson();
825
-
826
-		return $this->defaultEnabled;
827
-	}
828
-
829
-	public function getDefaultAppForUser(?IUser $user = null): string {
830
-		// Set fallback to always-enabled files app
831
-		$appId = 'files';
832
-		$defaultApps = explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files'));
833
-
834
-		$user ??= $this->userSession->getUser();
835
-
836
-		if ($user !== null) {
837
-			$userDefaultApps = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp'));
838
-			$defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps));
839
-		}
840
-
841
-		// Find the first app that is enabled for the current user
842
-		foreach ($defaultApps as $defaultApp) {
843
-			$defaultApp = \OC_App::cleanAppId(strip_tags($defaultApp));
844
-			if ($this->isEnabledForUser($defaultApp, $user)) {
845
-				$appId = $defaultApp;
846
-				break;
847
-			}
848
-		}
849
-
850
-		return $appId;
851
-	}
65
+    /**
66
+     * Apps with these types can not be enabled for certain groups only
67
+     * @var string[]
68
+     */
69
+    protected $protectedAppTypes = [
70
+        'filesystem',
71
+        'prelogin',
72
+        'authentication',
73
+        'logging',
74
+        'prevent_group_restriction',
75
+    ];
76
+
77
+    private IUserSession $userSession;
78
+    private IConfig $config;
79
+    private AppConfig $appConfig;
80
+    private IGroupManager $groupManager;
81
+    private ICacheFactory $memCacheFactory;
82
+    private EventDispatcherInterface $legacyDispatcher;
83
+    private IEventDispatcher $dispatcher;
84
+    private LoggerInterface $logger;
85
+
86
+    /** @var string[] $appId => $enabled */
87
+    private array $installedAppsCache = [];
88
+
89
+    /** @var string[]|null */
90
+    private ?array $shippedApps = null;
91
+
92
+    private array $alwaysEnabled = [];
93
+    private array $defaultEnabled = [];
94
+
95
+    /** @var array */
96
+    private array $appInfos = [];
97
+
98
+    /** @var array */
99
+    private array $appVersions = [];
100
+
101
+    /** @var array */
102
+    private array $autoDisabledApps = [];
103
+    private array $appTypes = [];
104
+
105
+    /** @var array<string, true> */
106
+    private array $loadedApps = [];
107
+
108
+    public function __construct(IUserSession $userSession,
109
+                                IConfig $config,
110
+                                AppConfig $appConfig,
111
+                                IGroupManager $groupManager,
112
+                                ICacheFactory $memCacheFactory,
113
+                                EventDispatcherInterface $legacyDispatcher,
114
+                                IEventDispatcher $dispatcher,
115
+                                LoggerInterface $logger) {
116
+        $this->userSession = $userSession;
117
+        $this->config = $config;
118
+        $this->appConfig = $appConfig;
119
+        $this->groupManager = $groupManager;
120
+        $this->memCacheFactory = $memCacheFactory;
121
+        $this->legacyDispatcher = $legacyDispatcher;
122
+        $this->dispatcher = $dispatcher;
123
+        $this->logger = $logger;
124
+    }
125
+
126
+    /**
127
+     * @return string[] $appId => $enabled
128
+     */
129
+    private function getInstalledAppsValues(): array {
130
+        if (!$this->installedAppsCache) {
131
+            $values = $this->appConfig->getValues(false, 'enabled');
132
+
133
+            $alwaysEnabledApps = $this->getAlwaysEnabledApps();
134
+            foreach ($alwaysEnabledApps as $appId) {
135
+                $values[$appId] = 'yes';
136
+            }
137
+
138
+            $this->installedAppsCache = array_filter($values, function ($value) {
139
+                return $value !== 'no';
140
+            });
141
+            ksort($this->installedAppsCache);
142
+        }
143
+        return $this->installedAppsCache;
144
+    }
145
+
146
+    /**
147
+     * List all installed apps
148
+     *
149
+     * @return string[]
150
+     */
151
+    public function getInstalledApps() {
152
+        return array_keys($this->getInstalledAppsValues());
153
+    }
154
+
155
+    /**
156
+     * List all apps enabled for a user
157
+     *
158
+     * @param \OCP\IUser $user
159
+     * @return string[]
160
+     */
161
+    public function getEnabledAppsForUser(IUser $user) {
162
+        $apps = $this->getInstalledAppsValues();
163
+        $appsForUser = array_filter($apps, function ($enabled) use ($user) {
164
+            return $this->checkAppForUser($enabled, $user);
165
+        });
166
+        return array_keys($appsForUser);
167
+    }
168
+
169
+    /**
170
+     * @param IGroup $group
171
+     * @return array
172
+     */
173
+    public function getEnabledAppsForGroup(IGroup $group): array {
174
+        $apps = $this->getInstalledAppsValues();
175
+        $appsForGroups = array_filter($apps, function ($enabled) use ($group) {
176
+            return $this->checkAppForGroups($enabled, $group);
177
+        });
178
+        return array_keys($appsForGroups);
179
+    }
180
+
181
+    /**
182
+     * Loads all apps
183
+     *
184
+     * @param string[] $types
185
+     * @return bool
186
+     *
187
+     * This function walks through the Nextcloud directory and loads all apps
188
+     * it can find. A directory contains an app if the file /appinfo/info.xml
189
+     * exists.
190
+     *
191
+     * if $types is set to non-empty array, only apps of those types will be loaded
192
+     */
193
+    public function loadApps(array $types = []): bool {
194
+        if ($this->config->getSystemValueBool('maintenance', false)) {
195
+            return false;
196
+        }
197
+        // Load the enabled apps here
198
+        $apps = \OC_App::getEnabledApps();
199
+
200
+        // Add each apps' folder as allowed class path
201
+        foreach ($apps as $app) {
202
+            // If the app is already loaded then autoloading it makes no sense
203
+            if (!$this->isAppLoaded($app)) {
204
+                $path = \OC_App::getAppPath($app);
205
+                if ($path !== false) {
206
+                    \OC_App::registerAutoloading($app, $path);
207
+                }
208
+            }
209
+        }
210
+
211
+        // prevent app.php from printing output
212
+        ob_start();
213
+        foreach ($apps as $app) {
214
+            if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
215
+                try {
216
+                    $this->loadApp($app);
217
+                } catch (\Throwable $e) {
218
+                    $this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
219
+                        'exception' => $e,
220
+                        'app' => $app,
221
+                    ]);
222
+                }
223
+            }
224
+        }
225
+        ob_end_clean();
226
+
227
+        return true;
228
+    }
229
+
230
+    /**
231
+     * check if an app is of a specific type
232
+     *
233
+     * @param string $app
234
+     * @param array $types
235
+     * @return bool
236
+     */
237
+    public function isType(string $app, array $types): bool {
238
+        $appTypes = $this->getAppTypes($app);
239
+        foreach ($types as $type) {
240
+            if (in_array($type, $appTypes, true)) {
241
+                return true;
242
+            }
243
+        }
244
+        return false;
245
+    }
246
+
247
+    /**
248
+     * get the types of an app
249
+     *
250
+     * @param string $app
251
+     * @return string[]
252
+     */
253
+    private function getAppTypes(string $app): array {
254
+        //load the cache
255
+        if (count($this->appTypes) === 0) {
256
+            $this->appTypes = $this->appConfig->getValues(false, 'types') ?: [];
257
+        }
258
+
259
+        if (isset($this->appTypes[$app])) {
260
+            return explode(',', $this->appTypes[$app]);
261
+        }
262
+
263
+        return [];
264
+    }
265
+
266
+    /**
267
+     * @return array
268
+     */
269
+    public function getAutoDisabledApps(): array {
270
+        return $this->autoDisabledApps;
271
+    }
272
+
273
+    /**
274
+     * @param string $appId
275
+     * @return array
276
+     */
277
+    public function getAppRestriction(string $appId): array {
278
+        $values = $this->getInstalledAppsValues();
279
+
280
+        if (!isset($values[$appId])) {
281
+            return [];
282
+        }
283
+
284
+        if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
285
+            return [];
286
+        }
287
+        return json_decode($values[$appId], true);
288
+    }
289
+
290
+
291
+    /**
292
+     * Check if an app is enabled for user
293
+     *
294
+     * @param string $appId
295
+     * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used
296
+     * @return bool
297
+     */
298
+    public function isEnabledForUser($appId, $user = null) {
299
+        if ($this->isAlwaysEnabled($appId)) {
300
+            return true;
301
+        }
302
+        if ($user === null) {
303
+            $user = $this->userSession->getUser();
304
+        }
305
+        $installedApps = $this->getInstalledAppsValues();
306
+        if (isset($installedApps[$appId])) {
307
+            return $this->checkAppForUser($installedApps[$appId], $user);
308
+        } else {
309
+            return false;
310
+        }
311
+    }
312
+
313
+    private function checkAppForUser(string $enabled, ?IUser $user): bool {
314
+        if ($enabled === 'yes') {
315
+            return true;
316
+        } elseif ($user === null) {
317
+            return false;
318
+        } else {
319
+            if (empty($enabled)) {
320
+                return false;
321
+            }
322
+
323
+            $groupIds = json_decode($enabled);
324
+
325
+            if (!is_array($groupIds)) {
326
+                $jsonError = json_last_error();
327
+                $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError);
328
+                return false;
329
+            }
330
+
331
+            $userGroups = $this->groupManager->getUserGroupIds($user);
332
+            foreach ($userGroups as $groupId) {
333
+                if (in_array($groupId, $groupIds, true)) {
334
+                    return true;
335
+                }
336
+            }
337
+            return false;
338
+        }
339
+    }
340
+
341
+    private function checkAppForGroups(string $enabled, IGroup $group): bool {
342
+        if ($enabled === 'yes') {
343
+            return true;
344
+        } else {
345
+            if (empty($enabled)) {
346
+                return false;
347
+            }
348
+
349
+            $groupIds = json_decode($enabled);
350
+
351
+            if (!is_array($groupIds)) {
352
+                $jsonError = json_last_error();
353
+                $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError);
354
+                return false;
355
+            }
356
+
357
+            return in_array($group->getGID(), $groupIds);
358
+        }
359
+    }
360
+
361
+    /**
362
+     * Check if an app is enabled in the instance
363
+     *
364
+     * Notice: This actually checks if the app is enabled and not only if it is installed.
365
+     *
366
+     * @param string $appId
367
+     * @param IGroup[]|String[] $groups
368
+     * @return bool
369
+     */
370
+    public function isInstalled($appId) {
371
+        $installedApps = $this->getInstalledAppsValues();
372
+        return isset($installedApps[$appId]);
373
+    }
374
+
375
+    public function ignoreNextcloudRequirementForApp(string $appId): void {
376
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
377
+        if (!in_array($appId, $ignoreMaxApps, true)) {
378
+            $ignoreMaxApps[] = $appId;
379
+            $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
380
+        }
381
+    }
382
+
383
+    public function loadApp(string $app): void {
384
+        if (isset($this->loadedApps[$app])) {
385
+            return;
386
+        }
387
+        $this->loadedApps[$app] = true;
388
+        $appPath = \OC_App::getAppPath($app);
389
+        if ($appPath === false) {
390
+            return;
391
+        }
392
+        $eventLogger = \OC::$server->get(\OCP\Diagnostics\IEventLogger::class);
393
+        $eventLogger->start("bootstrap:load_app:$app", "Load $app");
394
+
395
+        // in case someone calls loadApp() directly
396
+        \OC_App::registerAutoloading($app, $appPath);
397
+
398
+        /** @var Coordinator $coordinator */
399
+        $coordinator = \OC::$server->get(Coordinator::class);
400
+        $isBootable = $coordinator->isBootable($app);
401
+
402
+        $hasAppPhpFile = is_file($appPath . '/appinfo/app.php');
403
+
404
+        $eventLogger = \OC::$server->get(IEventLogger::class);
405
+        $eventLogger->start('bootstrap:load_app_' . $app, 'Load app: ' . $app);
406
+        if ($isBootable && $hasAppPhpFile) {
407
+            $this->logger->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [
408
+                'app' => $app,
409
+            ]);
410
+        } elseif ($hasAppPhpFile) {
411
+            $eventLogger->start("bootstrap:load_app:$app:app.php", "Load legacy app.php app $app");
412
+            $this->logger->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
413
+                'app' => $app,
414
+            ]);
415
+            try {
416
+                self::requireAppFile($appPath);
417
+            } catch (\Throwable $ex) {
418
+                if ($ex instanceof ServerNotAvailableException) {
419
+                    throw $ex;
420
+                }
421
+                if (!$this->isShipped($app) && !$this->isType($app, ['authentication'])) {
422
+                    $this->logger->error("App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(), [
423
+                        'exception' => $ex,
424
+                    ]);
425
+
426
+                    // Only disable apps which are not shipped and that are not authentication apps
427
+                    $this->disableApp($app, true);
428
+                } else {
429
+                    $this->logger->error("App $app threw an error during app.php load: " . $ex->getMessage(), [
430
+                        'exception' => $ex,
431
+                    ]);
432
+                }
433
+            }
434
+            $eventLogger->end("bootstrap:load_app:$app:app.php");
435
+        }
436
+
437
+        $coordinator->bootApp($app);
438
+
439
+        $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
440
+        $info = $this->getAppInfo($app);
441
+        if (!empty($info['activity'])) {
442
+            $activityManager = \OC::$server->get(IActivityManager::class);
443
+            if (!empty($info['activity']['filters'])) {
444
+                foreach ($info['activity']['filters'] as $filter) {
445
+                    $activityManager->registerFilter($filter);
446
+                }
447
+            }
448
+            if (!empty($info['activity']['settings'])) {
449
+                foreach ($info['activity']['settings'] as $setting) {
450
+                    $activityManager->registerSetting($setting);
451
+                }
452
+            }
453
+            if (!empty($info['activity']['providers'])) {
454
+                foreach ($info['activity']['providers'] as $provider) {
455
+                    $activityManager->registerProvider($provider);
456
+                }
457
+            }
458
+        }
459
+
460
+        if (!empty($info['settings'])) {
461
+            $settingsManager = \OC::$server->get(ISettingsManager::class);
462
+            if (!empty($info['settings']['admin'])) {
463
+                foreach ($info['settings']['admin'] as $setting) {
464
+                    $settingsManager->registerSetting('admin', $setting);
465
+                }
466
+            }
467
+            if (!empty($info['settings']['admin-section'])) {
468
+                foreach ($info['settings']['admin-section'] as $section) {
469
+                    $settingsManager->registerSection('admin', $section);
470
+                }
471
+            }
472
+            if (!empty($info['settings']['personal'])) {
473
+                foreach ($info['settings']['personal'] as $setting) {
474
+                    $settingsManager->registerSetting('personal', $setting);
475
+                }
476
+            }
477
+            if (!empty($info['settings']['personal-section'])) {
478
+                foreach ($info['settings']['personal-section'] as $section) {
479
+                    $settingsManager->registerSection('personal', $section);
480
+                }
481
+            }
482
+        }
483
+
484
+        if (!empty($info['collaboration']['plugins'])) {
485
+            // deal with one or many plugin entries
486
+            $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
487
+                [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
488
+            $collaboratorSearch = null;
489
+            $autoCompleteManager = null;
490
+            foreach ($plugins as $plugin) {
491
+                if ($plugin['@attributes']['type'] === 'collaborator-search') {
492
+                    $pluginInfo = [
493
+                        'shareType' => $plugin['@attributes']['share-type'],
494
+                        'class' => $plugin['@value'],
495
+                    ];
496
+                    $collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
497
+                    $collaboratorSearch->registerPlugin($pluginInfo);
498
+                } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
499
+                    $autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
500
+                    $autoCompleteManager->registerSorter($plugin['@value']);
501
+                }
502
+            }
503
+        }
504
+        $eventLogger->end("bootstrap:load_app:$app:info");
505
+
506
+        $eventLogger->end("bootstrap:load_app:$app");
507
+    }
508
+    /**
509
+     * Check if an app is loaded
510
+     * @param string $app app id
511
+     * @since 26.0.0
512
+     */
513
+    public function isAppLoaded(string $app): bool {
514
+        return isset($this->loadedApps[$app]);
515
+    }
516
+
517
+    /**
518
+     * Load app.php from the given app
519
+     *
520
+     * @param string $app app name
521
+     * @throws \Error
522
+     */
523
+    private static function requireAppFile(string $app): void {
524
+        // encapsulated here to avoid variable scope conflicts
525
+        require_once $app . '/appinfo/app.php';
526
+    }
527
+
528
+    /**
529
+     * Enable an app for every user
530
+     *
531
+     * @param string $appId
532
+     * @param bool $forceEnable
533
+     * @throws AppPathNotFoundException
534
+     */
535
+    public function enableApp(string $appId, bool $forceEnable = false): void {
536
+        // Check if app exists
537
+        $this->getAppPath($appId);
538
+
539
+        if ($forceEnable) {
540
+            $this->ignoreNextcloudRequirementForApp($appId);
541
+        }
542
+
543
+        $this->installedAppsCache[$appId] = 'yes';
544
+        $this->appConfig->setValue($appId, 'enabled', 'yes');
545
+        $this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
546
+        $this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
547
+            ManagerEvent::EVENT_APP_ENABLE, $appId
548
+        ));
549
+        $this->clearAppsCache();
550
+    }
551
+
552
+    /**
553
+     * Whether a list of types contains a protected app type
554
+     *
555
+     * @param string[] $types
556
+     * @return bool
557
+     */
558
+    public function hasProtectedAppType($types) {
559
+        if (empty($types)) {
560
+            return false;
561
+        }
562
+
563
+        $protectedTypes = array_intersect($this->protectedAppTypes, $types);
564
+        return !empty($protectedTypes);
565
+    }
566
+
567
+    /**
568
+     * Enable an app only for specific groups
569
+     *
570
+     * @param string $appId
571
+     * @param IGroup[] $groups
572
+     * @param bool $forceEnable
573
+     * @throws \InvalidArgumentException if app can't be enabled for groups
574
+     * @throws AppPathNotFoundException
575
+     */
576
+    public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
577
+        // Check if app exists
578
+        $this->getAppPath($appId);
579
+
580
+        $info = $this->getAppInfo($appId);
581
+        if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
582
+            throw new \InvalidArgumentException("$appId can't be enabled for groups.");
583
+        }
584
+
585
+        if ($forceEnable) {
586
+            $this->ignoreNextcloudRequirementForApp($appId);
587
+        }
588
+
589
+        /** @var string[] $groupIds */
590
+        $groupIds = array_map(function ($group) {
591
+            /** @var IGroup $group */
592
+            return ($group instanceof IGroup)
593
+                ? $group->getGID()
594
+                : $group;
595
+        }, $groups);
596
+
597
+        $this->installedAppsCache[$appId] = json_encode($groupIds);
598
+        $this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
599
+        $this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
600
+        $this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
601
+            ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
602
+        ));
603
+        $this->clearAppsCache();
604
+    }
605
+
606
+    /**
607
+     * Disable an app for every user
608
+     *
609
+     * @param string $appId
610
+     * @param bool $automaticDisabled
611
+     * @throws \Exception if app can't be disabled
612
+     */
613
+    public function disableApp($appId, $automaticDisabled = false) {
614
+        if ($this->isAlwaysEnabled($appId)) {
615
+            throw new \Exception("$appId can't be disabled.");
616
+        }
617
+
618
+        if ($automaticDisabled) {
619
+            $previousSetting = $this->appConfig->getValue($appId, 'enabled', 'yes');
620
+            if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
621
+                $previousSetting = json_decode($previousSetting, true);
622
+            }
623
+            $this->autoDisabledApps[$appId] = $previousSetting;
624
+        }
625
+
626
+        unset($this->installedAppsCache[$appId]);
627
+        $this->appConfig->setValue($appId, 'enabled', 'no');
628
+
629
+        // run uninstall steps
630
+        $appData = $this->getAppInfo($appId);
631
+        if (!is_null($appData)) {
632
+            \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
633
+        }
634
+
635
+        $this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
636
+        $this->legacyDispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
637
+            ManagerEvent::EVENT_APP_DISABLE, $appId
638
+        ));
639
+        $this->clearAppsCache();
640
+    }
641
+
642
+    /**
643
+     * Get the directory for the given app.
644
+     *
645
+     * @param string $appId
646
+     * @return string
647
+     * @throws AppPathNotFoundException if app folder can't be found
648
+     */
649
+    public function getAppPath($appId) {
650
+        $appPath = \OC_App::getAppPath($appId);
651
+        if ($appPath === false) {
652
+            throw new AppPathNotFoundException('Could not find path for ' . $appId);
653
+        }
654
+        return $appPath;
655
+    }
656
+
657
+    /**
658
+     * Get the web path for the given app.
659
+     *
660
+     * @param string $appId
661
+     * @return string
662
+     * @throws AppPathNotFoundException if app path can't be found
663
+     */
664
+    public function getAppWebPath(string $appId): string {
665
+        $appWebPath = \OC_App::getAppWebPath($appId);
666
+        if ($appWebPath === false) {
667
+            throw new AppPathNotFoundException('Could not find web path for ' . $appId);
668
+        }
669
+        return $appWebPath;
670
+    }
671
+
672
+    /**
673
+     * Clear the cached list of apps when enabling/disabling an app
674
+     */
675
+    public function clearAppsCache() {
676
+        $this->appInfos = [];
677
+    }
678
+
679
+    /**
680
+     * Returns a list of apps that need upgrade
681
+     *
682
+     * @param string $version Nextcloud version as array of version components
683
+     * @return array list of app info from apps that need an upgrade
684
+     *
685
+     * @internal
686
+     */
687
+    public function getAppsNeedingUpgrade($version) {
688
+        $appsToUpgrade = [];
689
+        $apps = $this->getInstalledApps();
690
+        foreach ($apps as $appId) {
691
+            $appInfo = $this->getAppInfo($appId);
692
+            $appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
693
+            if ($appDbVersion
694
+                && isset($appInfo['version'])
695
+                && version_compare($appInfo['version'], $appDbVersion, '>')
696
+                && \OC_App::isAppCompatible($version, $appInfo)
697
+            ) {
698
+                $appsToUpgrade[] = $appInfo;
699
+            }
700
+        }
701
+
702
+        return $appsToUpgrade;
703
+    }
704
+
705
+    /**
706
+     * Returns the app information from "appinfo/info.xml".
707
+     *
708
+     * @param string $appId app id
709
+     *
710
+     * @param bool $path
711
+     * @param null $lang
712
+     * @return array|null app info
713
+     */
714
+    public function getAppInfo(string $appId, bool $path = false, $lang = null) {
715
+        if ($path) {
716
+            $file = $appId;
717
+        } else {
718
+            if ($lang === null && isset($this->appInfos[$appId])) {
719
+                return $this->appInfos[$appId];
720
+            }
721
+            try {
722
+                $appPath = $this->getAppPath($appId);
723
+            } catch (AppPathNotFoundException $e) {
724
+                return null;
725
+            }
726
+            $file = $appPath . '/appinfo/info.xml';
727
+        }
728
+
729
+        $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
730
+        $data = $parser->parse($file);
731
+
732
+        if (is_array($data)) {
733
+            $data = \OC_App::parseAppInfo($data, $lang);
734
+        }
735
+
736
+        if ($lang === null) {
737
+            $this->appInfos[$appId] = $data;
738
+        }
739
+
740
+        return $data;
741
+    }
742
+
743
+    public function getAppVersion(string $appId, bool $useCache = true): string {
744
+        if (!$useCache || !isset($this->appVersions[$appId])) {
745
+            $appInfo = $this->getAppInfo($appId);
746
+            $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
747
+        }
748
+        return $this->appVersions[$appId];
749
+    }
750
+
751
+    /**
752
+     * Returns a list of apps incompatible with the given version
753
+     *
754
+     * @param string $version Nextcloud version as array of version components
755
+     *
756
+     * @return array list of app info from incompatible apps
757
+     *
758
+     * @internal
759
+     */
760
+    public function getIncompatibleApps(string $version): array {
761
+        $apps = $this->getInstalledApps();
762
+        $incompatibleApps = [];
763
+        foreach ($apps as $appId) {
764
+            $info = $this->getAppInfo($appId);
765
+            if ($info === null) {
766
+                $incompatibleApps[] = ['id' => $appId, 'name' => $appId];
767
+            } elseif (!\OC_App::isAppCompatible($version, $info)) {
768
+                $incompatibleApps[] = $info;
769
+            }
770
+        }
771
+        return $incompatibleApps;
772
+    }
773
+
774
+    /**
775
+     * @inheritdoc
776
+     * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
777
+     */
778
+    public function isShipped($appId) {
779
+        $this->loadShippedJson();
780
+        return in_array($appId, $this->shippedApps, true);
781
+    }
782
+
783
+    private function isAlwaysEnabled(string $appId): bool {
784
+        $alwaysEnabled = $this->getAlwaysEnabledApps();
785
+        return in_array($appId, $alwaysEnabled, true);
786
+    }
787
+
788
+    /**
789
+     * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
790
+     * @throws \Exception
791
+     */
792
+    private function loadShippedJson(): void {
793
+        if ($this->shippedApps === null) {
794
+            $shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
795
+            if (!file_exists($shippedJson)) {
796
+                throw new \Exception("File not found: $shippedJson");
797
+            }
798
+            $content = json_decode(file_get_contents($shippedJson), true);
799
+            $this->shippedApps = $content['shippedApps'];
800
+            $this->alwaysEnabled = $content['alwaysEnabled'];
801
+            $this->defaultEnabled = $content['defaultEnabled'];
802
+        }
803
+    }
804
+
805
+    /**
806
+     * @inheritdoc
807
+     */
808
+    public function getAlwaysEnabledApps() {
809
+        $this->loadShippedJson();
810
+        return $this->alwaysEnabled;
811
+    }
812
+
813
+    /**
814
+     * @inheritdoc
815
+     */
816
+    public function isDefaultEnabled(string $appId): bool {
817
+        return (in_array($appId, $this->getDefaultEnabledApps()));
818
+    }
819
+
820
+    /**
821
+     * @inheritdoc
822
+     */
823
+    public function getDefaultEnabledApps():array {
824
+        $this->loadShippedJson();
825
+
826
+        return $this->defaultEnabled;
827
+    }
828
+
829
+    public function getDefaultAppForUser(?IUser $user = null): string {
830
+        // Set fallback to always-enabled files app
831
+        $appId = 'files';
832
+        $defaultApps = explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files'));
833
+
834
+        $user ??= $this->userSession->getUser();
835
+
836
+        if ($user !== null) {
837
+            $userDefaultApps = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp'));
838
+            $defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps));
839
+        }
840
+
841
+        // Find the first app that is enabled for the current user
842
+        foreach ($defaultApps as $defaultApp) {
843
+            $defaultApp = \OC_App::cleanAppId(strip_tags($defaultApp));
844
+            if ($this->isEnabledForUser($defaultApp, $user)) {
845
+                $appId = $defaultApp;
846
+                break;
847
+            }
848
+        }
849
+
850
+        return $appId;
851
+    }
852 852
 }
Please login to merge, or discard this patch.