Passed
Push — master ( f452e2...9c2d70 )
by Roeland
10:46 queued 10s
created

AppManager::getAutoDisabledApps()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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