Passed
Push — master ( 223a91...782554 )
by Morris
11:37
created

AppManager::checkAppForGroups()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 5
nop 2
dl 0
loc 19
rs 9.5222
c 0
b 0
f 0
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 Joas Schilling <[email protected]>
10
 * @author Julius Haertl <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author Vincent Petry <[email protected]>
16
 *
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OC\App;
34
35
use OC\AppConfig;
36
use OCP\App\AppPathNotFoundException;
37
use OCP\App\IAppManager;
38
use OCP\App\ManagerEvent;
39
use OCP\ICacheFactory;
40
use OCP\IGroup;
41
use OCP\IGroupManager;
42
use OCP\ILogger;
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 IUserSession */
62
	private $userSession;
63
64
	/** @var AppConfig */
65
	private $appConfig;
66
67
	/** @var IGroupManager */
68
	private $groupManager;
69
70
	/** @var ICacheFactory */
71
	private $memCacheFactory;
72
73
	/** @var EventDispatcherInterface */
74
	private $dispatcher;
75
76
	/** @var ILogger */
77
	private $logger;
78
79
	/** @var string[] $appId => $enabled */
80
	private $installedAppsCache;
81
82
	/** @var string[] */
83
	private $shippedApps;
84
85
	/** @var string[] */
86
	private $alwaysEnabled;
87
88
	/** @var array */
89
	private $appInfos = [];
90
91
	/** @var array */
92
	private $appVersions = [];
93
94
	/**
95
	 * @param IUserSession $userSession
96
	 * @param AppConfig $appConfig
97
	 * @param IGroupManager $groupManager
98
	 * @param ICacheFactory $memCacheFactory
99
	 * @param EventDispatcherInterface $dispatcher
100
	 */
101
	public function __construct(IUserSession $userSession,
102
								AppConfig $appConfig,
103
								IGroupManager $groupManager,
104
								ICacheFactory $memCacheFactory,
105
								EventDispatcherInterface $dispatcher,
106
								ILogger $logger) {
107
		$this->userSession = $userSession;
108
		$this->appConfig = $appConfig;
109
		$this->groupManager = $groupManager;
110
		$this->memCacheFactory = $memCacheFactory;
111
		$this->dispatcher = $dispatcher;
112
		$this->logger = $logger;
113
	}
114
115
	/**
116
	 * @return string[] $appId => $enabled
117
	 */
118
	private function getInstalledAppsValues() {
119
		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...
120
			$values = $this->appConfig->getValues(false, 'enabled');
121
122
			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
123
			foreach($alwaysEnabledApps as $appId) {
124
				$values[$appId] = 'yes';
125
			}
126
127
			$this->installedAppsCache = array_filter($values, function ($value) {
128
				return $value !== 'no';
129
			});
130
			ksort($this->installedAppsCache);
131
		}
132
		return $this->installedAppsCache;
133
	}
134
135
	/**
136
	 * List all installed apps
137
	 *
138
	 * @return string[]
139
	 */
140
	public function getInstalledApps() {
141
		return array_keys($this->getInstalledAppsValues());
142
	}
143
144
	/**
145
	 * List all apps enabled for a user
146
	 *
147
	 * @param \OCP\IUser $user
148
	 * @return string[]
149
	 */
150
	public function getEnabledAppsForUser(IUser $user) {
151
		$apps = $this->getInstalledAppsValues();
152
		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
153
			return $this->checkAppForUser($enabled, $user);
154
		});
155
		return array_keys($appsForUser);
156
	}
157
158
	/**
159
	 * @param \OCP\IGroup $group
160
	 * @return array
161
	 */
162
	public function getEnabledAppsForGroup(IGroup $group): array {
163
		$apps = $this->getInstalledAppsValues();
164
		$appsForGroups = array_filter($apps, function ($enabled) use ($group) {
165
			return $this->checkAppForGroups($enabled, $group);
166
		});
167
		return array_keys($appsForGroups);
168
	}
169
170
	/**
171
	 * @param string $appId
172
	 * @return array
173
	 */
174
	public function getAppRestriction(string $appId): array {
175
		$values = $this->getInstalledAppsValues();
176
177
		if (!isset($values[$appId])) {
178
			return [];
179
		}
180
181
		if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
182
			return [];
183
		}
184
		return json_decode($values[$appId]);
185
	}
186
187
188
	/**
189
	 * Check if an app is enabled for user
190
	 *
191
	 * @param string $appId
192
	 * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used
193
	 * @return bool
194
	 */
195
	public function isEnabledForUser($appId, $user = null) {
196
		if ($this->isAlwaysEnabled($appId)) {
197
			return true;
198
		}
199
		if ($user === null) {
200
			$user = $this->userSession->getUser();
201
		}
202
		$installedApps = $this->getInstalledAppsValues();
203
		if (isset($installedApps[$appId])) {
204
			return $this->checkAppForUser($installedApps[$appId], $user);
205
		} else {
206
			return false;
207
		}
208
	}
209
210
	/**
211
	 * @param string $enabled
212
	 * @param IUser $user
213
	 * @return bool
214
	 */
215
	private function checkAppForUser($enabled, $user) {
216
		if ($enabled === 'yes') {
217
			return true;
218
		} elseif ($user === null) {
219
			return false;
220
		} else {
221
			if(empty($enabled)){
222
				return false;
223
			}
224
225
			$groupIds = json_decode($enabled);
226
227
			if (!is_array($groupIds)) {
228
				$jsonError = json_last_error();
229
				$this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']);
230
				return false;
231
			}
232
233
			$userGroups = $this->groupManager->getUserGroupIds($user);
234
			foreach ($userGroups as $groupId) {
235
				if (in_array($groupId, $groupIds, true)) {
236
					return true;
237
				}
238
			}
239
			return false;
240
		}
241
	}
242
243
	/**
244
	 * @param string $enabled
245
	 * @param IGroup $group
246
	 * @return bool
247
	 */
248
	private function checkAppForGroups(string $enabled, IGroup $group): bool {
249
		if ($enabled === 'yes') {
250
			return true;
251
		} elseif ($group === null) {
252
			return false;
253
		} else {
254
			if (empty($enabled)) {
255
				return false;
256
			}
257
258
			$groupIds = json_decode($enabled);
259
260
			if (!is_array($groupIds)) {
261
				$jsonError = json_last_error();
262
				$this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']);
263
				return false;
264
			}
265
266
			return in_array($group->getGID(), $groupIds);
267
		}
268
	}
269
270
	/**
271
	 * Check if an app is enabled in the instance
272
	 *
273
	 * Notice: This actually checks if the app is enabled and not only if it is installed.
274
	 *
275
	 * @param string $appId
276
	 * @param \OCP\IGroup[]|String[] $groups
277
	 * @return bool
278
	 */
279
	public function isInstalled($appId) {
280
		$installedApps = $this->getInstalledAppsValues();
281
		return isset($installedApps[$appId]);
282
	}
283
284
	/**
285
	 * Enable an app for every user
286
	 *
287
	 * @param string $appId
288
	 * @throws AppPathNotFoundException
289
	 */
290
	public function enableApp($appId) {
291
		// Check if app exists
292
		$this->getAppPath($appId);
293
294
		$this->installedAppsCache[$appId] = 'yes';
295
		$this->appConfig->setValue($appId, 'enabled', 'yes');
296
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
297
			ManagerEvent::EVENT_APP_ENABLE, $appId
298
		));
299
		$this->clearAppsCache();
300
	}
301
302
	/**
303
	 * Whether a list of types contains a protected app type
304
	 *
305
	 * @param string[] $types
306
	 * @return bool
307
	 */
308
	public function hasProtectedAppType($types) {
309
		if (empty($types)) {
310
			return false;
311
		}
312
313
		$protectedTypes = array_intersect($this->protectedAppTypes, $types);
314
		return !empty($protectedTypes);
315
	}
316
317
	/**
318
	 * Enable an app only for specific groups
319
	 *
320
	 * @param string $appId
321
	 * @param \OCP\IGroup[] $groups
322
	 * @throws \InvalidArgumentException if app can't be enabled for groups
323
	 * @throws AppPathNotFoundException
324
	 */
325
	public function enableAppForGroups($appId, $groups) {
326
		// Check if app exists
327
		$this->getAppPath($appId);
328
329
		$info = $this->getAppInfo($appId);
330
		if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
331
			throw new \InvalidArgumentException("$appId can't be enabled for groups.");
332
		}
333
334
		$groupIds = array_map(function ($group) {
335
			/** @var \OCP\IGroup $group */
336
			return ($group instanceof IGroup)
0 ignored issues
show
introduced by
$group is always a sub-type of OCP\IGroup.
Loading history...
337
				? $group->getGID()
338
				: $group;
339
		}, $groups);
340
341
		$this->installedAppsCache[$appId] = json_encode($groupIds);
342
		$this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
343
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
344
			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
345
		));
346
		$this->clearAppsCache();
347
348
	}
349
350
	/**
351
	 * Disable an app for every user
352
	 *
353
	 * @param string $appId
354
	 * @throws \Exception if app can't be disabled
355
	 */
356
	public function disableApp($appId) {
357
		if ($this->isAlwaysEnabled($appId)) {
358
			throw new \Exception("$appId can't be disabled.");
359
		}
360
		unset($this->installedAppsCache[$appId]);
361
		$this->appConfig->setValue($appId, 'enabled', 'no');
362
363
		// run uninstall steps
364
		$appData = $this->getAppInfo($appId);
365
		if (!is_null($appData)) {
366
			\OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
367
		}
368
369
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
370
			ManagerEvent::EVENT_APP_DISABLE, $appId
371
		));
372
		$this->clearAppsCache();
373
	}
374
375
	/**
376
	 * Get the directory for the given app.
377
	 *
378
	 * @param string $appId
379
	 * @return string
380
	 * @throws AppPathNotFoundException if app folder can't be found
381
	 */
382
	public function getAppPath($appId) {
383
		$appPath = \OC_App::getAppPath($appId);
384
		if($appPath === false) {
385
			throw new AppPathNotFoundException('Could not find path for ' . $appId);
386
		}
387
		return $appPath;
388
	}
389
390
	/**
391
	 * Clear the cached list of apps when enabling/disabling an app
392
	 */
393
	public function clearAppsCache() {
394
		$settingsMemCache = $this->memCacheFactory->createDistributed('settings');
395
		$settingsMemCache->clear('listApps');
396
		$this->appInfos = [];
397
	}
398
399
	/**
400
	 * Returns a list of apps that need upgrade
401
	 *
402
	 * @param string $version Nextcloud version as array of version components
403
	 * @return array list of app info from apps that need an upgrade
404
	 *
405
	 * @internal
406
	 */
407
	public function getAppsNeedingUpgrade($version) {
408
		$appsToUpgrade = [];
409
		$apps = $this->getInstalledApps();
410
		foreach ($apps as $appId) {
411
			$appInfo = $this->getAppInfo($appId);
412
			$appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
413
			if ($appDbVersion
414
				&& isset($appInfo['version'])
415
				&& version_compare($appInfo['version'], $appDbVersion, '>')
416
				&& \OC_App::isAppCompatible($version, $appInfo)
417
			) {
418
				$appsToUpgrade[] = $appInfo;
419
			}
420
		}
421
422
		return $appsToUpgrade;
423
	}
424
425
	/**
426
	 * Returns the app information from "appinfo/info.xml".
427
	 *
428
	 * @param string $appId app id
429
	 *
430
	 * @param bool $path
431
	 * @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...
432
	 * @return array|null app info
433
	 */
434
	public function getAppInfo(string $appId, bool $path = false, $lang = null) {
435
		if ($path) {
436
			$file = $appId;
437
		} else {
438
			if ($lang === null && isset($this->appInfos[$appId])) {
439
				return $this->appInfos[$appId];
440
			}
441
			try {
442
				$appPath = $this->getAppPath($appId);
443
			} catch (AppPathNotFoundException $e) {
444
				return null;
445
			}
446
			$file = $appPath . '/appinfo/info.xml';
447
		}
448
449
		$parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
450
		$data = $parser->parse($file);
451
452
		if (is_array($data)) {
453
			$data = \OC_App::parseAppInfo($data, $lang);
454
		}
455
456
		if ($lang === null) {
0 ignored issues
show
introduced by
The condition $lang === null is always true.
Loading history...
457
			$this->appInfos[$appId] = $data;
458
		}
459
460
		return $data;
461
	}
462
463
	public function getAppVersion(string $appId, bool $useCache = true): string {
464
		if(!$useCache || !isset($this->appVersions[$appId])) {
465
			$appInfo = \OC::$server->getAppManager()->getAppInfo($appId);
466
			$this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
467
		}
468
		return $this->appVersions[$appId];
469
	}
470
471
	/**
472
	 * Returns a list of apps incompatible with the given version
473
	 *
474
	 * @param string $version Nextcloud version as array of version components
475
	 *
476
	 * @return array list of app info from incompatible apps
477
	 *
478
	 * @internal
479
	 */
480
	public function getIncompatibleApps(string $version): array {
481
		$apps = $this->getInstalledApps();
482
		$incompatibleApps = array();
483
		foreach ($apps as $appId) {
484
			$info = $this->getAppInfo($appId);
485
			if ($info === null) {
486
				$incompatibleApps[] = ['id' => $appId];
487
			} else if (!\OC_App::isAppCompatible($version, $info)) {
488
				$incompatibleApps[] = $info;
489
			}
490
		}
491
		return $incompatibleApps;
492
	}
493
494
	/**
495
	 * @inheritdoc
496
	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
497
	 */
498
	public function isShipped($appId) {
499
		$this->loadShippedJson();
500
		return in_array($appId, $this->shippedApps, true);
501
	}
502
503
	private function isAlwaysEnabled($appId) {
504
		$alwaysEnabled = $this->getAlwaysEnabledApps();
505
		return in_array($appId, $alwaysEnabled, true);
506
	}
507
508
	/**
509
	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
510
	 * @throws \Exception
511
	 */
512
	private function loadShippedJson() {
513
		if ($this->shippedApps === null) {
514
			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
515
			if (!file_exists($shippedJson)) {
516
				throw new \Exception("File not found: $shippedJson");
517
			}
518
			$content = json_decode(file_get_contents($shippedJson), true);
519
			$this->shippedApps = $content['shippedApps'];
520
			$this->alwaysEnabled = $content['alwaysEnabled'];
521
		}
522
	}
523
524
	/**
525
	 * @inheritdoc
526
	 */
527
	public function getAlwaysEnabledApps() {
528
		$this->loadShippedJson();
529
		return $this->alwaysEnabled;
530
	}
531
}
532