AppManager   F
last analyzed

Complexity

Total Complexity 134

Size/Duplication

Total Lines 787
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 335
dl 0
loc 787
rs 2
c 0
b 0
f 0
wmc 134

36 Methods

Rating   Name   Duplication   Size   Complexity  
A requireAppFile() 0 3 1
A getInstalledAppsValues() 0 15 3
A checkAppForGroups() 0 17 4
A ignoreNextcloudRequirementForApp() 0 5 2
A getEnabledAppsForUser() 0 6 1
A isEnabledForUser() 0 12 4
A getAppPath() 0 6 2
A enableApp() 0 15 2
A isAppLoaded() 0 2 1
A enableAppForGroups() 0 28 5
B loadApps() 0 35 10
A getAppRestriction() 0 11 4
A __construct() 0 16 1
A hasProtectedAppType() 0 7 2
A getInstalledApps() 0 2 1
A getAppWebPath() 0 6 2
A getAutoDisabledApps() 0 2 1
A isInstalled() 0 3 1
A getAppTypes() 0 11 4
A getEnabledAppsForGroup() 0 6 1
A disableApp() 0 27 6
A isType() 0 8 3
B checkAppForUser() 0 25 7
F loadApp() 0 124 31
A isShipped() 0 3 1
B getAppInfo() 0 27 7
A isDefaultEnabled() 0 2 1
A loadShippedJson() 0 10 3
A isAlwaysEnabled() 0 3 1
A getAppsNeedingUpgrade() 0 16 6
A getAppVersion() 0 6 5
A getDefaultEnabledApps() 0 4 1
A getAlwaysEnabledApps() 0 3 1
A getDefaultAppForUser() 0 22 4
A clearAppsCache() 0 2 1
A getIncompatibleApps() 0 12 4

How to fix   Complexity   

Complex Class

Complex classes like AppManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AppManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Christoph Schaefer "christophł@wolkesicher.de"
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Daniel Kesselberg <[email protected]>
10
 * @author Daniel Rudolf <[email protected]>
11
 * @author Greta Doci <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author Julius Haertl <[email protected]>
14
 * @author Julius Härtl <[email protected]>
15
 * @author Lukas Reschke <[email protected]>
16
 * @author Maxence Lange <[email protected]>
17
 * @author Morris Jobke <[email protected]>
18
 * @author Robin Appelman <[email protected]>
19
 * @author Roeland Jago Douma <[email protected]>
20
 * @author Thomas Müller <[email protected]>
21
 * @author Tobia De Koninck <[email protected]>
22
 * @author Vincent Petry <[email protected]>
23
 *
24
 * @license AGPL-3.0
25
 *
26
 * This code is free software: you can redistribute it and/or modify
27
 * it under the terms of the GNU Affero General Public License, version 3,
28
 * as published by the Free Software Foundation.
29
 *
30
 * This program is distributed in the hope that it will be useful,
31
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33
 * GNU Affero General Public License for more details.
34
 *
35
 * You should have received a copy of the GNU Affero General Public License, version 3,
36
 * along with this program. If not, see <http://www.gnu.org/licenses/>
37
 *
38
 */
39
namespace OC\App;
40
41
use OC\AppConfig;
42
use OC\AppFramework\Bootstrap\Coordinator;
43
use OC\ServerNotAvailableException;
44
use OCP\Activity\IManager as IActivityManager;
45
use OCP\App\AppPathNotFoundException;
46
use OCP\App\Events\AppDisableEvent;
47
use OCP\App\Events\AppEnableEvent;
48
use OCP\App\IAppManager;
49
use OCP\App\ManagerEvent;
50
use OCP\EventDispatcher\IEventDispatcher;
51
use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager;
52
use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch;
53
use OCP\Diagnostics\IEventLogger;
54
use OCP\ICacheFactory;
55
use OCP\IConfig;
56
use OCP\IGroup;
57
use OCP\IGroupManager;
58
use OCP\IUser;
59
use OCP\IUserSession;
60
use OCP\Settings\IManager as ISettingsManager;
61
use Psr\Log\LoggerInterface;
62
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
63
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->installedAppsCache of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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);
0 ignored issues
show
Bug introduced by
Are you sure print_r($enabled, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

327
				$this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . /** @scrutinizer ignore-type */ print_r($enabled, true) . ' - json error code: ' . $jsonError);
Loading history...
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);
0 ignored issues
show
Bug introduced by
Are you sure print_r($enabled, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

353
				$this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . /** @scrutinizer ignore-type */ print_r($enabled, true) . ' - json error code: ' . $jsonError);
Loading history...
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(
0 ignored issues
show
Bug introduced by
OCP\App\ManagerEvent::EVENT_APP_ENABLE of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

546
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new OCP\App\ManagerEvent...ENT_APP_ENABLE, $appId). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

546
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
547
                           dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Deprecated Code introduced by
The constant OCP\App\ManagerEvent::EVENT_APP_ENABLE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

546
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-deprecated */ ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
547
			ManagerEvent::EVENT_APP_ENABLE, $appId
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\App\ManagerEvent::EVENT_APP_ENABLE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

547
			/** @scrutinizer ignore-deprecated */ ManagerEvent::EVENT_APP_ENABLE, $appId

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
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)
0 ignored issues
show
introduced by
$group is always a sub-type of OCP\IGroup.
Loading history...
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(
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new OCP\App\ManagerEvent...ROUPS, $appId, $groups). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

600
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
601
                           dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
OCP\App\ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

600
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
Loading history...
Deprecated Code introduced by
The constant OCP\App\ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

600
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-deprecated */ ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
601
			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\App\ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

601
			/** @scrutinizer ignore-deprecated */ ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
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(
0 ignored issues
show
Bug introduced by
OCP\App\ManagerEvent::EVENT_APP_DISABLE of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

636
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
Loading history...
Deprecated Code introduced by
The constant OCP\App\ManagerEvent::EVENT_APP_DISABLE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

636
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-deprecated */ ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new OCP\App\ManagerEvent...NT_APP_DISABLE, $appId). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

636
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
637
                           dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
637
			ManagerEvent::EVENT_APP_DISABLE, $appId
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\App\ManagerEvent::EVENT_APP_DISABLE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

637
			/** @scrutinizer ignore-deprecated */ ManagerEvent::EVENT_APP_DISABLE, $appId

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $lang is correct as it would always require null to be passed?
Loading history...
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) {
0 ignored issues
show
introduced by
The condition $lang === null is always true.
Loading history...
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
}
853