Passed
Push — master ( 20e8ee...b3f663 )
by Roeland
09:40 queued 11s
created

AppManager::getAlwaysEnabledApps()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
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
 * @author Daniel Rudolf <[email protected]>
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OC\App;
35
36
use OC\AppConfig;
37
use OCP\App\AppPathNotFoundException;
38
use OCP\App\IAppManager;
39
use OCP\App\ManagerEvent;
40
use OCP\ICacheFactory;
41
use OCP\IGroup;
42
use OCP\IGroupManager;
43
use OCP\ILogger;
44
use OCP\IUser;
45
use OCP\IUserSession;
46
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
47
48
class AppManager implements IAppManager {
49
50
	/**
51
	 * Apps with these types can not be enabled for certain groups only
52
	 * @var string[]
53
	 */
54
	protected $protectedAppTypes = [
55
		'filesystem',
56
		'prelogin',
57
		'authentication',
58
		'logging',
59
		'prevent_group_restriction',
60
	];
61
62
	/** @var IUserSession */
63
	private $userSession;
64
65
	/** @var AppConfig */
66
	private $appConfig;
67
68
	/** @var IGroupManager */
69
	private $groupManager;
70
71
	/** @var ICacheFactory */
72
	private $memCacheFactory;
73
74
	/** @var EventDispatcherInterface */
75
	private $dispatcher;
76
77
	/** @var ILogger */
78
	private $logger;
79
80
	/** @var string[] $appId => $enabled */
81
	private $installedAppsCache;
82
83
	/** @var string[] */
84
	private $shippedApps;
85
86
	/** @var string[] */
87
	private $alwaysEnabled;
88
89
	/** @var array */
90
	private $appInfos = [];
91
92
	/** @var array */
93
	private $appVersions = [];
94
95
	/** @var array */
96
	private $autoDisabledApps = [];
97
98
	/**
99
	 * @param IUserSession $userSession
100
	 * @param AppConfig $appConfig
101
	 * @param IGroupManager $groupManager
102
	 * @param ICacheFactory $memCacheFactory
103
	 * @param EventDispatcherInterface $dispatcher
104
	 */
105
	public function __construct(IUserSession $userSession,
106
								AppConfig $appConfig,
107
								IGroupManager $groupManager,
108
								ICacheFactory $memCacheFactory,
109
								EventDispatcherInterface $dispatcher,
110
								ILogger $logger) {
111
		$this->userSession = $userSession;
112
		$this->appConfig = $appConfig;
113
		$this->groupManager = $groupManager;
114
		$this->memCacheFactory = $memCacheFactory;
115
		$this->dispatcher = $dispatcher;
116
		$this->logger = $logger;
117
	}
118
119
	/**
120
	 * @return string[] $appId => $enabled
121
	 */
122
	private function getInstalledAppsValues() {
123
		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...
124
			$values = $this->appConfig->getValues(false, 'enabled');
125
126
			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
127
			foreach($alwaysEnabledApps as $appId) {
128
				$values[$appId] = 'yes';
129
			}
130
131
			$this->installedAppsCache = array_filter($values, function ($value) {
132
				return $value !== 'no';
133
			});
134
			ksort($this->installedAppsCache);
135
		}
136
		return $this->installedAppsCache;
137
	}
138
139
	/**
140
	 * List all installed apps
141
	 *
142
	 * @return string[]
143
	 */
144
	public function getInstalledApps() {
145
		return array_keys($this->getInstalledAppsValues());
146
	}
147
148
	/**
149
	 * List all apps enabled for a user
150
	 *
151
	 * @param \OCP\IUser $user
152
	 * @return string[]
153
	 */
154
	public function getEnabledAppsForUser(IUser $user) {
155
		$apps = $this->getInstalledAppsValues();
156
		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
157
			return $this->checkAppForUser($enabled, $user);
158
		});
159
		return array_keys($appsForUser);
160
	}
161
162
	/**
163
	 * @param \OCP\IGroup $group
164
	 * @return array
165
	 */
166
	public function getEnabledAppsForGroup(IGroup $group): array {
167
		$apps = $this->getInstalledAppsValues();
168
		$appsForGroups = array_filter($apps, function ($enabled) use ($group) {
169
			return $this->checkAppForGroups($enabled, $group);
170
		});
171
		return array_keys($appsForGroups);
172
	}
173
174
	/**
175
	 * @return array
176
	 */
177
	public function getAutoDisabledApps(): array {
178
		return $this->autoDisabledApps;
179
	}
180
181
	/**
182
	 * @param string $appId
183
	 * @return array
184
	 */
185
	public function getAppRestriction(string $appId): array {
186
		$values = $this->getInstalledAppsValues();
187
188
		if (!isset($values[$appId])) {
189
			return [];
190
		}
191
192
		if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
193
			return [];
194
		}
195
		return json_decode($values[$appId]);
196
	}
197
198
199
	/**
200
	 * Check if an app is enabled for user
201
	 *
202
	 * @param string $appId
203
	 * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used
204
	 * @return bool
205
	 */
206
	public function isEnabledForUser($appId, $user = null) {
207
		if ($this->isAlwaysEnabled($appId)) {
208
			return true;
209
		}
210
		if ($user === null) {
211
			$user = $this->userSession->getUser();
212
		}
213
		$installedApps = $this->getInstalledAppsValues();
214
		if (isset($installedApps[$appId])) {
215
			return $this->checkAppForUser($installedApps[$appId], $user);
216
		} else {
217
			return false;
218
		}
219
	}
220
221
	/**
222
	 * @param string $enabled
223
	 * @param IUser $user
224
	 * @return bool
225
	 */
226
	private function checkAppForUser($enabled, $user) {
227
		if ($enabled === 'yes') {
228
			return true;
229
		} elseif ($user === null) {
230
			return false;
231
		} else {
232
			if(empty($enabled)){
233
				return false;
234
			}
235
236
			$groupIds = json_decode($enabled);
237
238
			if (!is_array($groupIds)) {
239
				$jsonError = json_last_error();
240
				$this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']);
241
				return false;
242
			}
243
244
			$userGroups = $this->groupManager->getUserGroupIds($user);
245
			foreach ($userGroups as $groupId) {
246
				if (in_array($groupId, $groupIds, true)) {
247
					return true;
248
				}
249
			}
250
			return false;
251
		}
252
	}
253
254
	/**
255
	 * @param string $enabled
256
	 * @param IGroup $group
257
	 * @return bool
258
	 */
259
	private function checkAppForGroups(string $enabled, IGroup $group): bool {
260
		if ($enabled === 'yes') {
261
			return true;
262
		} elseif ($group === null) {
263
			return false;
264
		} else {
265
			if (empty($enabled)) {
266
				return false;
267
			}
268
269
			$groupIds = json_decode($enabled);
270
271
			if (!is_array($groupIds)) {
272
				$jsonError = json_last_error();
273
				$this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']);
274
				return false;
275
			}
276
277
			return in_array($group->getGID(), $groupIds);
278
		}
279
	}
280
281
	/**
282
	 * Check if an app is enabled in the instance
283
	 *
284
	 * Notice: This actually checks if the app is enabled and not only if it is installed.
285
	 *
286
	 * @param string $appId
287
	 * @param \OCP\IGroup[]|String[] $groups
288
	 * @return bool
289
	 */
290
	public function isInstalled($appId) {
291
		$installedApps = $this->getInstalledAppsValues();
292
		return isset($installedApps[$appId]);
293
	}
294
295
	/**
296
	 * Enable an app for every user
297
	 *
298
	 * @param string $appId
299
	 * @throws AppPathNotFoundException
300
	 */
301
	public function enableApp($appId) {
302
		// Check if app exists
303
		$this->getAppPath($appId);
304
305
		$this->installedAppsCache[$appId] = 'yes';
306
		$this->appConfig->setValue($appId, 'enabled', 'yes');
307
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
308
			ManagerEvent::EVENT_APP_ENABLE, $appId
309
		));
310
		$this->clearAppsCache();
311
	}
312
313
	/**
314
	 * Whether a list of types contains a protected app type
315
	 *
316
	 * @param string[] $types
317
	 * @return bool
318
	 */
319
	public function hasProtectedAppType($types) {
320
		if (empty($types)) {
321
			return false;
322
		}
323
324
		$protectedTypes = array_intersect($this->protectedAppTypes, $types);
325
		return !empty($protectedTypes);
326
	}
327
328
	/**
329
	 * Enable an app only for specific groups
330
	 *
331
	 * @param string $appId
332
	 * @param \OCP\IGroup[] $groups
333
	 * @throws \InvalidArgumentException if app can't be enabled for groups
334
	 * @throws AppPathNotFoundException
335
	 */
336
	public function enableAppForGroups($appId, $groups) {
337
		// Check if app exists
338
		$this->getAppPath($appId);
339
340
		$info = $this->getAppInfo($appId);
341
		if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
342
			throw new \InvalidArgumentException("$appId can't be enabled for groups.");
343
		}
344
345
		$groupIds = array_map(function ($group) {
346
			/** @var \OCP\IGroup $group */
347
			return ($group instanceof IGroup)
0 ignored issues
show
introduced by
$group is always a sub-type of OCP\IGroup.
Loading history...
348
				? $group->getGID()
349
				: $group;
350
		}, $groups);
351
352
		$this->installedAppsCache[$appId] = json_encode($groupIds);
353
		$this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
354
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
355
			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
356
		));
357
		$this->clearAppsCache();
358
359
	}
360
361
	/**
362
	 * Disable an app for every user
363
	 *
364
	 * @param string $appId
365
	 * @param bool $automaticDisabled
366
	 * @throws \Exception if app can't be disabled
367
	 */
368
	public function disableApp($appId, $automaticDisabled = false) {
369
		if ($this->isAlwaysEnabled($appId)) {
370
			throw new \Exception("$appId can't be disabled.");
371
		}
372
373
		if ($automaticDisabled) {
374
			$this->autoDisabledApps[] = $appId;
375
		}
376
377
		unset($this->installedAppsCache[$appId]);
378
		$this->appConfig->setValue($appId, 'enabled', 'no');
379
380
		// run uninstall steps
381
		$appData = $this->getAppInfo($appId);
382
		if (!is_null($appData)) {
383
			\OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
384
		}
385
386
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
387
			ManagerEvent::EVENT_APP_DISABLE, $appId
388
		));
389
		$this->clearAppsCache();
390
	}
391
392
	/**
393
	 * Get the directory for the given app.
394
	 *
395
	 * @param string $appId
396
	 * @return string
397
	 * @throws AppPathNotFoundException if app folder can't be found
398
	 */
399
	public function getAppPath($appId) {
400
		$appPath = \OC_App::getAppPath($appId);
0 ignored issues
show
Deprecated Code introduced by
The function OC_App::getAppPath() has been deprecated: 11.0.0 use \OC::$server->getAppManager()->getAppPath() ( Ignorable by Annotation )

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

400
		$appPath = /** @scrutinizer ignore-deprecated */ \OC_App::getAppPath($appId);

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

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

Loading history...
401
		if($appPath === false) {
402
			throw new AppPathNotFoundException('Could not find path for ' . $appId);
403
		}
404
		return $appPath;
405
	}
406
407
	/**
408
	 * Get the web path for the given app.
409
	 *
410
	 * @param string $appId
411
	 * @return string
412
	 * @throws AppPathNotFoundException if app path can't be found
413
	 */
414
	public function getAppWebPath(string $appId): string {
415
		$appWebPath = \OC_App::getAppWebPath($appId);
0 ignored issues
show
Deprecated Code introduced by
The function OC_App::getAppWebPath() has been deprecated: 18.0.0 use \OC::$server->getAppManager()->getAppWebPath() ( Ignorable by Annotation )

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

415
		$appWebPath = /** @scrutinizer ignore-deprecated */ \OC_App::getAppWebPath($appId);

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

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

Loading history...
416
		if($appWebPath === false) {
417
			throw new AppPathNotFoundException('Could not find web path for ' . $appId);
418
		}
419
		return $appWebPath;
420
	}
421
422
	/**
423
	 * Clear the cached list of apps when enabling/disabling an app
424
	 */
425
	public function clearAppsCache() {
426
		$settingsMemCache = $this->memCacheFactory->createDistributed('settings');
427
		$settingsMemCache->clear('listApps');
428
		$this->appInfos = [];
429
	}
430
431
	/**
432
	 * Returns a list of apps that need upgrade
433
	 *
434
	 * @param string $version Nextcloud version as array of version components
435
	 * @return array list of app info from apps that need an upgrade
436
	 *
437
	 * @internal
438
	 */
439
	public function getAppsNeedingUpgrade($version) {
440
		$appsToUpgrade = [];
441
		$apps = $this->getInstalledApps();
442
		foreach ($apps as $appId) {
443
			$appInfo = $this->getAppInfo($appId);
444
			$appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
445
			if ($appDbVersion
446
				&& isset($appInfo['version'])
447
				&& version_compare($appInfo['version'], $appDbVersion, '>')
448
				&& \OC_App::isAppCompatible($version, $appInfo)
449
			) {
450
				$appsToUpgrade[] = $appInfo;
451
			}
452
		}
453
454
		return $appsToUpgrade;
455
	}
456
457
	/**
458
	 * Returns the app information from "appinfo/info.xml".
459
	 *
460
	 * @param string $appId app id
461
	 *
462
	 * @param bool $path
463
	 * @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...
464
	 * @return array|null app info
465
	 */
466
	public function getAppInfo(string $appId, bool $path = false, $lang = null) {
467
		if ($path) {
468
			$file = $appId;
469
		} else {
470
			if ($lang === null && isset($this->appInfos[$appId])) {
471
				return $this->appInfos[$appId];
472
			}
473
			try {
474
				$appPath = $this->getAppPath($appId);
475
			} catch (AppPathNotFoundException $e) {
476
				return null;
477
			}
478
			$file = $appPath . '/appinfo/info.xml';
479
		}
480
481
		$parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
482
		$data = $parser->parse($file);
483
484
		if (is_array($data)) {
485
			$data = \OC_App::parseAppInfo($data, $lang);
486
		}
487
488
		if ($lang === null) {
0 ignored issues
show
introduced by
The condition $lang === null is always true.
Loading history...
489
			$this->appInfos[$appId] = $data;
490
		}
491
492
		return $data;
493
	}
494
495
	public function getAppVersion(string $appId, bool $useCache = true): string {
496
		if(!$useCache || !isset($this->appVersions[$appId])) {
497
			$appInfo = $this->getAppInfo($appId);
498
			$this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
499
		}
500
		return $this->appVersions[$appId];
501
	}
502
503
	/**
504
	 * Returns a list of apps incompatible with the given version
505
	 *
506
	 * @param string $version Nextcloud version as array of version components
507
	 *
508
	 * @return array list of app info from incompatible apps
509
	 *
510
	 * @internal
511
	 */
512
	public function getIncompatibleApps(string $version): array {
513
		$apps = $this->getInstalledApps();
514
		$incompatibleApps = array();
515
		foreach ($apps as $appId) {
516
			$info = $this->getAppInfo($appId);
517
			if ($info === null) {
518
				$incompatibleApps[] = ['id' => $appId];
519
			} else if (!\OC_App::isAppCompatible($version, $info)) {
520
				$incompatibleApps[] = $info;
521
			}
522
		}
523
		return $incompatibleApps;
524
	}
525
526
	/**
527
	 * @inheritdoc
528
	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
529
	 */
530
	public function isShipped($appId) {
531
		$this->loadShippedJson();
532
		return in_array($appId, $this->shippedApps, true);
533
	}
534
535
	private function isAlwaysEnabled($appId) {
536
		$alwaysEnabled = $this->getAlwaysEnabledApps();
537
		return in_array($appId, $alwaysEnabled, true);
538
	}
539
540
	/**
541
	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
542
	 * @throws \Exception
543
	 */
544
	private function loadShippedJson() {
545
		if ($this->shippedApps === null) {
546
			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
547
			if (!file_exists($shippedJson)) {
548
				throw new \Exception("File not found: $shippedJson");
549
			}
550
			$content = json_decode(file_get_contents($shippedJson), true);
551
			$this->shippedApps = $content['shippedApps'];
552
			$this->alwaysEnabled = $content['alwaysEnabled'];
553
		}
554
	}
555
556
	/**
557
	 * @inheritdoc
558
	 */
559
	public function getAlwaysEnabledApps() {
560
		$this->loadShippedJson();
561
		return $this->alwaysEnabled;
562
	}
563
}
564