Completed
Push — master ( e8cbe7...f3c40f )
by Victor
21:02 queued 11:32
created

AppManager   D

Complexity

Total Complexity 60

Size/Duplication

Total Lines 440
Duplicated Lines 2.5 %

Coupling/Cohesion

Components 2
Dependencies 16

Importance

Changes 0
Metric Value
dl 11
loc 440
rs 4.2857
c 0
b 0
f 0
wmc 60
lcom 2
cbo 16

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getInstalledAppsValues() 0 16 3
A getInstalledApps() 0 3 1
A getEnabledAppsForUser() 0 7 1
B isEnabledForUser() 0 14 5
C checkAppForUser() 0 27 7
A isInstalled() 0 4 1
A __construct() 0 13 1
A enableApp() 0 13 2
B canEnableTheme() 0 15 6
A isTheme() 0 3 1
A enableAppForGroups() 0 20 3
A disableApp() 0 11 2
A clearAppsCache() 0 4 1
B getAppsNeedingUpgrade() 0 17 6
A getAppInfo() 11 11 3
A getIncompatibleApps() 0 11 3
A isShipped() 0 4 1
A isAlwaysEnabled() 0 4 1
A loadShippedJson() 0 11 3
A getAlwaysEnabledApps() 0 4 1
A installApp() 0 7 1
A updateApp() 0 6 1
A getAllApps() 0 3 1
A readAppPackage() 0 10 1
A canInstall() 0 8 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
 * @author Arthur Schiwon <[email protected]>
4
 * @author Christoph Schaefer <christophł@wolkesicher.de>
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Jörn Friedrich Dreyer <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Thomas Müller <[email protected]>
12
 * @author Vincent Petry <[email protected]>
13
 *
14
 * @copyright Copyright (c) 2017, ownCloud GmbH
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC\App;
32
33
use OC_App;
34
use OC\Installer;
35
use OCP\App\IAppManager;
36
use OCP\App\AppManagerException;
37
use OCP\App\ManagerEvent;
38
use OCP\Files;
39
use OCP\IAppConfig;
40
use OCP\ICacheFactory;
41
use OCP\IConfig;
42
use OCP\IGroupManager;
43
use OCP\IUser;
44
use OCP\IUserSession;
45
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
46
47
class AppManager implements IAppManager {
48
49
	/**
50
	 * Apps with these types can not be enabled for certain groups only
51
	 * @var string[]
52
	 */
53
	protected $protectedAppTypes = [
54
		'filesystem',
55
		'prelogin',
56
		'authentication',
57
		'logging',
58
		'prevent_group_restriction',
59
	];
60
61
	/** @var \OCP\IUserSession */
62
	private $userSession;
63
	/** @var \OCP\IAppConfig */
64
	private $appConfig;
65
	/** @var \OCP\IGroupManager */
66
	private $groupManager;
67
	/** @var \OCP\ICacheFactory */
68
	private $memCacheFactory;
69
	/** @var string[] $appId => $enabled */
70
	private $installedAppsCache;
71
	/** @var string[] */
72
	private $shippedApps;
73
	/** @var string[] */
74
	private $alwaysEnabled;
75
	/** @var EventDispatcherInterface */
76
	private $dispatcher;
77
	/** @var IConfig */
78
	private $config;
79
80
	/**
81
	 * @param IUserSession $userSession
82
	 * @param IAppConfig $appConfig
83
	 * @param IGroupManager $groupManager
84
	 * @param ICacheFactory $memCacheFactory
85
	 * @param EventDispatcherInterface $dispatcher
86
	 * @param IConfig $config
87
	 */
88
	public function __construct(IUserSession $userSession = null,
89
								IAppConfig $appConfig,
90
								IGroupManager $groupManager,
91
								ICacheFactory $memCacheFactory,
92
								EventDispatcherInterface $dispatcher,
93
								IConfig $config) {
94
		$this->userSession = $userSession;
95
		$this->appConfig = $appConfig;
96
		$this->groupManager = $groupManager;
97
		$this->memCacheFactory = $memCacheFactory;
98
		$this->dispatcher = $dispatcher;
99
		$this->config = $config;
100
	}
101
102
	/**
103
	 * @return string[] $appId => $enabled
104
	 */
105
	private function getInstalledAppsValues() {
106
		if (!$this->installedAppsCache) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->installedAppsCache of type string[] 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...
107
			$values = $this->appConfig->getValues(false, 'enabled');
108
109
			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
110
			foreach($alwaysEnabledApps as $appId) {
111
				$values[$appId] = 'yes';
112
			}
113
114
			$this->installedAppsCache = array_filter($values, function ($value) {
115
				return $value !== 'no';
116
			});
117
			ksort($this->installedAppsCache);
118
		}
119
		return $this->installedAppsCache;
120
	}
121
122
	/**
123
	 * List all installed apps
124
	 *
125
	 * @return string[]
126
	 */
127
	public function getInstalledApps() {
128
		return array_keys($this->getInstalledAppsValues());
129
	}
130
131
	/**
132
	 * List all apps enabled for a user
133
	 *
134
	 * @param \OCP\IUser|null $user
135
	 * @return string[]
136
	 */
137
	public function getEnabledAppsForUser(IUser $user = null) {
138
		$apps = $this->getInstalledAppsValues();
139
		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
140
			return $this->checkAppForUser($enabled, $user);
0 ignored issues
show
Bug introduced by
It seems like $user defined by parameter $user on line 137 can be null; however, OC\App\AppManager::checkAppForUser() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
141
		});
142
		return array_keys($appsForUser);
143
	}
144
145
	/**
146
	 * Check if an app is enabled for user
147
	 *
148
	 * @param string $appId
149
	 * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used
150
	 * @return bool
151
	 */
152
	public function isEnabledForUser($appId, $user = null) {
153
		if ($this->isAlwaysEnabled($appId)) {
154
			return true;
155
		}
156
		if (is_null($user) && !is_null($this->userSession)) {
157
			$user = $this->userSession->getUser();
158
		}
159
		$installedApps = $this->getInstalledAppsValues();
160
		if (isset($installedApps[$appId])) {
161
			return $this->checkAppForUser($installedApps[$appId], $user);
0 ignored issues
show
Bug introduced by
It seems like $user can be null; however, checkAppForUser() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
162
		} else {
163
			return false;
164
		}
165
	}
166
167
	/**
168
	 * @param string $enabled
169
	 * @param IUser $user
170
	 * @return bool
171
	 */
172
	private function checkAppForUser($enabled, $user) {
173
		if ($enabled === 'yes') {
174
			return true;
175
		} elseif (is_null($user)) {
176
			return false;
177
		} else {
178
			if(empty($enabled)){
179
				return false;
180
			}
181
182
			$groupIds = json_decode($enabled);
183
184
			if (!is_array($groupIds)) {
185
				$jsonError = json_last_error();
186
				\OC::$server->getLogger()->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']);
187
				return false;
188
			}
189
190
			$userGroups = $this->groupManager->getUserGroupIds($user);
191
			foreach ($userGroups as $groupId) {
192
				if (array_search($groupId, $groupIds) !== false) {
193
					return true;
194
				}
195
			}
196
			return false;
197
		}
198
	}
199
200
	/**
201
	 * Check if an app is installed in the instance
202
	 *
203
	 * @param string $appId
204
	 * @return bool
205
	 */
206
	public function isInstalled($appId) {
207
		$installedApps = $this->getInstalledAppsValues();
208
		return isset($installedApps[$appId]);
209
	}
210
211
	/**
212
	 * Enable an app for every user
213
	 *
214
	 * @param string $appId
215
	 * @throws \Exception
216
	 */
217
	public function enableApp($appId) {
218
		if(OC_App::getAppPath($appId) === false) {
219
			throw new \Exception("$appId can't be enabled since it is not installed.");
220
		}
221
		$this->canEnableTheme($appId);
222
223
		$this->installedAppsCache[$appId] = 'yes';
224
		$this->appConfig->setValue($appId, 'enabled', 'yes');
225
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
226
			ManagerEvent::EVENT_APP_ENABLE, $appId
227
		));
228
		$this->clearAppsCache();
229
	}
230
231
	/**
232
	 * Do not allow more than one active app-theme
233
	 *
234
	 * @param $appId
235
	 * @throws AppManagerException
236
	 */
237
	protected function canEnableTheme($appId) {
238
		$info = $this->getAppInfo($appId);
239
		if (
240
			isset($info['types'])
241
			&& is_array($info['types'])
242
			&& in_array('theme', $info['types'])
243
		) {
244
			$apps = $this->getInstalledApps();
245
			foreach ($apps as $installedAppId) {
246
				if ($this->isTheme($installedAppId)) {
247
					throw new AppManagerException("$appId can't be enabled until $installedAppId is disabled.");
248
				}
249
			}
250
		}
251
	}
252
253
	/**
254
	 *  Wrapper for OC_App for easy mocking
255
	 *
256
	 * @param string $appId
257
	 * @return bool
258
	 */
259
	protected function isTheme($appId) {
260
		return \OC_App::isType($appId,'theme');
261
	}
262
263
	/**
264
	 * Enable an app only for specific groups
265
	 *
266
	 * @param string $appId
267
	 * @param \OCP\IGroup[] $groups
268
	 * @throws \Exception if app can't be enabled for groups
269
	 */
270
	public function enableAppForGroups($appId, $groups) {
271
		$info = $this->getAppInfo($appId);
272
		if (!empty($info['types'])) {
273
			$protectedTypes = array_intersect($this->protectedAppTypes, $info['types']);
274
			if (!empty($protectedTypes)) {
275
				throw new \Exception("$appId can't be enabled for groups.");
276
			}
277
		}
278
279
		$groupIds = array_map(function ($group) {
280
			/** @var \OCP\IGroup $group */
281
			return $group->getGID();
282
		}, $groups);
283
		$this->installedAppsCache[$appId] = json_encode($groupIds);
284
		$this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
285
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
286
			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
287
		));
288
		$this->clearAppsCache();
289
	}
290
291
	/**
292
	 * Disable an app for every user
293
	 *
294
	 * @param string $appId
295
	 * @throws \Exception if app can't be disabled
296
	 */
297
	public function disableApp($appId) {
298
		if ($this->isAlwaysEnabled($appId)) {
299
			throw new \Exception("$appId can't be disabled.");
300
		}
301
		unset($this->installedAppsCache[$appId]);
302
		$this->appConfig->setValue($appId, 'enabled', 'no');
303
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
304
			ManagerEvent::EVENT_APP_DISABLE, $appId
305
		));
306
		$this->clearAppsCache();
307
	}
308
309
	/**
310
	 * Clear the cached list of apps when enabling/disabling an app
311
	 */
312
	public function clearAppsCache() {
313
		$settingsMemCache = $this->memCacheFactory->create('settings');
314
		$settingsMemCache->clear('listApps');
315
	}
316
317
	/**
318
	 * Returns a list of apps that need upgrade
319
	 *
320
	 * @param array $ocVersion ownCloud version as array of version components
321
	 * @return array list of app info from apps that need an upgrade
322
	 *
323
	 * @internal
324
	 */
325
	public function getAppsNeedingUpgrade($ocVersion) {
326
		$appsToUpgrade = [];
327
		$apps = $this->getInstalledApps();
328
		foreach ($apps as $appId) {
329
			$appInfo = $this->getAppInfo($appId);
330
			$appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
331
			if ($appDbVersion
332
				&& isset($appInfo['version'])
333
				&& version_compare($appInfo['version'], $appDbVersion, '>')
334
				&& \OC_App::isAppCompatible($ocVersion, $appInfo)
335
			) {
336
				$appsToUpgrade[] = $appInfo;
337
			}
338
		}
339
340
		return $appsToUpgrade;
341
	}
342
343
	/**
344
	 * Returns the app information from "appinfo/info.xml".
345
	 *
346
	 * @param string $appId app id
347
	 *
348
	 * @return array app info
349
	 *
350
	 * @internal
351
	 */
352 View Code Duplication
	public function getAppInfo($appId) {
353
		$appInfo = \OC_App::getAppInfo($appId);
354
		if ($appInfo === null) {
355
			return null;
356
		}
357
		if (!isset($appInfo['version'])) {
358
			// read version from separate file
359
			$appInfo['version'] = \OC_App::getAppVersion($appId);
360
		}
361
		return $appInfo;
362
	}
363
364
	/**
365
	 * Returns a list of apps incompatible with the given version
366
	 *
367
	 * @param array $version ownCloud version as array of version components
368
	 *
369
	 * @return array list of app info from incompatible apps
370
	 *
371
	 * @internal
372
	 */
373
	public function getIncompatibleApps($version) {
374
		$apps = $this->getInstalledApps();
375
		$incompatibleApps = [];
376
		foreach ($apps as $appId) {
377
			$info = $this->getAppInfo($appId);
378
			if (!\OC_App::isAppCompatible($version, $info)) {
0 ignored issues
show
Bug introduced by
It seems like $info defined by $this->getAppInfo($appId) on line 377 can also be of type null; however, OC_App::isAppCompatible() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
379
				$incompatibleApps[] = $info;
380
			}
381
		}
382
		return $incompatibleApps;
383
	}
384
385
	/**
386
	 * @inheritdoc
387
	 */
388
	public function isShipped($appId) {
389
		$this->loadShippedJson();
390
		return in_array($appId, $this->shippedApps);
391
	}
392
393
	private function isAlwaysEnabled($appId) {
394
		$alwaysEnabled = $this->getAlwaysEnabledApps();
395
		return in_array($appId, $alwaysEnabled);
396
	}
397
398
	private function loadShippedJson() {
399
		if (is_null($this->shippedApps)) {
400
			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
401
			if (!file_exists($shippedJson)) {
402
				throw new \Exception("File not found: $shippedJson");
403
			}
404
			$content = json_decode(file_get_contents($shippedJson), true);
405
			$this->shippedApps = $content['shippedApps'];
406
			$this->alwaysEnabled = $content['alwaysEnabled'];
407
		}
408
	}
409
410
	/**
411
	 * @inheritdoc
412
	 */
413
	public function getAlwaysEnabledApps() {
414
		$this->loadShippedJson();
415
		return $this->alwaysEnabled;
416
	}
417
418
	/**
419
	 * @param string $package package path
420
	 * @param bool $skipMigrations whether to skip migrations, which would only install the code
421
	 * @return string|false app id or false in case of error
422
	 * @since 10.0
423
	 */
424
	public function installApp($package, $skipMigrations = false) {
425
		$appId = Installer::installApp([
426
			'source' => 'local',
427
			'path' => $package
428
		]);
429
		return $appId;
430
	}
431
432
	/**
433
	 * @param string $package
434
	 * @return mixed
435
	 * @since 10.0
436
	 */
437
	public function updateApp($package) {
438
		return Installer::updateApp([
439
			'source' => 'local',
440
			'path' => $package
441
		]);
442
	}
443
444
	/**
445
	 * Returns the list of all apps, enabled and disabled
446
	 *
447
	 * @return string[]
448
	 * @since 10.0
449
	 */
450
	public function getAllApps() {
451
		return $this->appConfig->getApps();
452
	}
453
454
	/**
455
	 * @param string $path
456
	 * @return string[] app info
457
	 */
458
	public function readAppPackage($path) {
459
		$data = [
460
			'source' => 'path',
461
			'path' => $path,
462
		];
463
		list($appCodeDir, $path) = Installer::downloadApp($data);
464
		$appInfo = Installer::checkAppsIntegrity($data, $appCodeDir, $path);
465
		Files::rmdirr($appCodeDir);
466
		return $appInfo;
467
	}
468
469
	/**
470
	 * Indicates if app installation is supported. Usually it is but in certain
471
	 * environments it is disallowed because of hardening. In a clustered setup
472
	 * apps need to be installed on each cluster node which is out of scope of
473
	 * ownCloud itself.
474
	 *
475
	 * @return bool
476
	 * @since 10.0.3
477
	 */
478
	public function canInstall() {
479
		if ($this->config->getSystemValue('operation.mode', 'single-instance') !== 'single-instance') {
480
			return false;
481
		}
482
483
		$appsFolder = OC_App::getInstallPath();
484
		return $appsFolder !== null && is_writable($appsFolder) && is_readable($appsFolder);
485
	}
486
}
487