Completed
Push — master ( ab4fd5...664d98 )
by Victor
20:52 queued 01:47
created

AppManager::enableAppForGroups()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 3
nop 2
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
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) 2018, 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
	 * Apps as 'appId' => [
82
	 *   'path' => '/app/path'
83
	 *   'url' => '/app/url'
84
	 * ]
85
	 * @var string[][]
86
	 */
87
	private $appDirs = [];
88
89
	/**
90
	 * @param IUserSession $userSession
91
	 * @param IAppConfig $appConfig
92
	 * @param IGroupManager $groupManager
93
	 * @param ICacheFactory $memCacheFactory
94
	 * @param EventDispatcherInterface $dispatcher
95
	 * @param IConfig $config
96
	 */
97
	public function __construct(IUserSession $userSession = null,
98
								IAppConfig $appConfig = null,
99
								IGroupManager $groupManager = null,
100
								ICacheFactory $memCacheFactory,
101
								EventDispatcherInterface $dispatcher,
102
								IConfig $config) {
103
		$this->userSession = $userSession;
104
		$this->appConfig = $appConfig;
105
		$this->groupManager = $groupManager;
106
		$this->memCacheFactory = $memCacheFactory;
107
		$this->dispatcher = $dispatcher;
108
		$this->config = $config;
109
	}
110
111
	/**
112
	 * @return string[] $appId => $enabled
113
	 */
114
	private function getInstalledAppsValues() {
115
		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...
116
			$values = $this->appConfig->getValues(false, 'enabled');
117
118
			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
119
			foreach($alwaysEnabledApps as $appId) {
120
				$values[$appId] = 'yes';
121
			}
122
123
			$this->installedAppsCache = array_filter($values, function ($value) {
124
				return $value !== 'no';
125
			});
126
			ksort($this->installedAppsCache);
127
		}
128
		return $this->installedAppsCache;
129
	}
130
131
	/**
132
	 * List all installed apps
133
	 *
134
	 * @return string[]
135
	 */
136
	public function getInstalledApps() {
137
		return array_keys($this->getInstalledAppsValues());
138
	}
139
140
	/**
141
	 * List all apps enabled for a user
142
	 *
143
	 * @param \OCP\IUser|null $user
144
	 * @return string[]
145
	 */
146
	public function getEnabledAppsForUser(IUser $user = null) {
147
		$apps = $this->getInstalledAppsValues();
148
		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
149
			return $this->checkAppForUser($enabled, $user);
0 ignored issues
show
Bug introduced by
It seems like $user defined by parameter $user on line 146 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...
150
		});
151
		return array_keys($appsForUser);
152
	}
153
154
	/**
155
	 * Check if an app is enabled for user
156
	 *
157
	 * @param string $appId
158
	 * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used
159
	 * @return bool
160
	 */
161
	public function isEnabledForUser($appId, $user = null) {
162
		if ($this->isAlwaysEnabled($appId)) {
163
			return true;
164
		}
165
		if (is_null($user) && !is_null($this->userSession)) {
166
			$user = $this->userSession->getUser();
167
		}
168
		$installedApps = $this->getInstalledAppsValues();
169
		if (isset($installedApps[$appId])) {
170
			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...
171
		} else {
172
			return false;
173
		}
174
	}
175
176
	/**
177
	 * @param string $enabled
178
	 * @param IUser $user
179
	 * @return bool
180
	 */
181
	private function checkAppForUser($enabled, $user) {
182
		if ($enabled === 'yes') {
183
			return true;
184
		} elseif (is_null($user)) {
185
			return false;
186
		} else {
187
			if(empty($enabled)){
188
				return false;
189
			}
190
191
			$groupIds = json_decode($enabled);
192
193
			if (!is_array($groupIds)) {
194
				$jsonError = json_last_error();
195
				\OC::$server->getLogger()->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']);
196
				return false;
197
			}
198
199
			$userGroups = $this->groupManager->getUserGroupIds($user);
200
			foreach ($userGroups as $groupId) {
201
				if (array_search($groupId, $groupIds) !== false) {
202
					return true;
203
				}
204
			}
205
			return false;
206
		}
207
	}
208
209
	/**
210
	 * Check if an app is installed in the instance
211
	 *
212
	 * @param string $appId
213
	 * @return bool
214
	 */
215
	public function isInstalled($appId) {
216
		$installedApps = $this->getInstalledAppsValues();
217
		return isset($installedApps[$appId]);
218
	}
219
220
	/**
221
	 * Enable an app for every user
222
	 *
223
	 * @param string $appId
224
	 * @throws \Exception
225
	 */
226 View Code Duplication
	public function enableApp($appId) {
227
		if($this->getAppPath($appId) === false) {
228
			throw new \Exception("$appId can't be enabled since it is not installed.");
229
		}
230
		$this->canEnableTheme($appId);
231
232
		$this->installedAppsCache[$appId] = 'yes';
233
		$this->appConfig->setValue($appId, 'enabled', 'yes');
234
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
235
			ManagerEvent::EVENT_APP_ENABLE, $appId
236
		));
237
		$this->clearAppsCache();
238
	}
239
240
	/**
241
	 * Do not allow more than one active app-theme
242
	 *
243
	 * @param $appId
244
	 * @throws AppManagerException
245
	 */
246
	protected function canEnableTheme($appId) {
247
		$info = $this->getAppInfo($appId);
248
		if (
249
			isset($info['types'])
250
			&& is_array($info['types'])
251
			&& in_array('theme', $info['types'])
252
		) {
253
			$apps = $this->getInstalledApps();
254
			foreach ($apps as $installedAppId) {
255
				if ($this->isTheme($installedAppId)) {
256
					throw new AppManagerException("$appId can't be enabled until $installedAppId is disabled.");
257
				}
258
			}
259
		}
260
	}
261
262
	/**
263
	 *  Wrapper for OC_App for easy mocking
264
	 *
265
	 * @param string $appId
266
	 * @return bool
267
	 */
268
	protected function isTheme($appId) {
269
		return \OC_App::isType($appId,'theme');
270
	}
271
272
	/**
273
	 * Enable an app only for specific groups
274
	 *
275
	 * @param string $appId
276
	 * @param \OCP\IGroup[] $groups
277
	 * @throws \Exception if app can't be enabled for groups
278
	 */
279
	public function enableAppForGroups($appId, $groups) {
280
		$info = $this->getAppInfo($appId);
281
		if (!empty($info['types'])) {
282
			$protectedTypes = array_intersect($this->protectedAppTypes, $info['types']);
283
			if (!empty($protectedTypes)) {
284
				throw new \Exception("$appId can't be enabled for groups.");
285
			}
286
		}
287
288
		$groupIds = array_map(function ($group) {
289
			/** @var \OCP\IGroup $group */
290
			return $group->getGID();
291
		}, $groups);
292
		$this->installedAppsCache[$appId] = json_encode($groupIds);
293
		$this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
294
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
295
			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
296
		));
297
		$this->clearAppsCache();
298
	}
299
300
	/**
301
	 * Disable an app for every user
302
	 *
303
	 * @param string $appId
304
	 * @throws \Exception if app can't be disabled
305
	 */
306 View Code Duplication
	public function disableApp($appId) {
307
		if ($this->isAlwaysEnabled($appId)) {
308
			throw new \Exception("$appId can't be disabled.");
309
		}
310
		unset($this->installedAppsCache[$appId]);
311
		$this->appConfig->setValue($appId, 'enabled', 'no');
312
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
313
			ManagerEvent::EVENT_APP_DISABLE, $appId
314
		));
315
		$this->clearAppsCache();
316
	}
317
318
	/**
319
	 * Clear the cached list of apps when enabling/disabling an app
320
	 */
321
	public function clearAppsCache() {
322
		$settingsMemCache = $this->memCacheFactory->create('settings');
323
		$settingsMemCache->clear('listApps');
324
	}
325
326
	/**
327
	 * Returns a list of apps that need upgrade
328
	 *
329
	 * @param array $ocVersion ownCloud version as array of version components
330
	 * @return array list of app info from apps that need an upgrade
331
	 *
332
	 * @internal
333
	 */
334
	public function getAppsNeedingUpgrade($ocVersion) {
335
		$appsToUpgrade = [];
336
		$apps = $this->getInstalledApps();
337
		foreach ($apps as $appId) {
338
			$appInfo = $this->getAppInfo($appId);
339
			$appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
340
			if ($appDbVersion
341
				&& isset($appInfo['version'])
342
				&& version_compare($appInfo['version'], $appDbVersion, '>')
343
				&& \OC_App::isAppCompatible($ocVersion, $appInfo)
344
			) {
345
				$appsToUpgrade[] = $appInfo;
346
			}
347
		}
348
349
		return $appsToUpgrade;
350
	}
351
352
	/**
353
	 * Returns the app information from "appinfo/info.xml".
354
	 *
355
	 * @param string $appId app id
356
	 *
357
	 * @return array app info
358
	 *
359
	 * @internal
360
	 */
361 View Code Duplication
	public function getAppInfo($appId) {
362
		$appInfo = \OC_App::getAppInfo($appId);
363
		if ($appInfo === null) {
364
			return null;
365
		}
366
		if (!isset($appInfo['version'])) {
367
			// read version from separate file
368
			$appInfo['version'] = \OC_App::getAppVersion($appId);
369
		}
370
		return $appInfo;
371
	}
372
373
	/**
374
	 * Returns a list of apps incompatible with the given version
375
	 *
376
	 * @param array $version ownCloud version as array of version components
377
	 *
378
	 * @return array list of app info from incompatible apps
379
	 *
380
	 * @internal
381
	 */
382
	public function getIncompatibleApps($version) {
383
		$apps = $this->getInstalledApps();
384
		$incompatibleApps = [];
385
		foreach ($apps as $appId) {
386
			$info = $this->getAppInfo($appId);
387
			if (!\OC_App::isAppCompatible($version, $info)) {
0 ignored issues
show
Bug introduced by
It seems like $info defined by $this->getAppInfo($appId) on line 386 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...
388
				$incompatibleApps[] = $info;
389
			}
390
		}
391
		return $incompatibleApps;
392
	}
393
394
	/**
395
	 * @inheritdoc
396
	 */
397
	public function isShipped($appId) {
398
		$this->loadShippedJson();
399
		return in_array($appId, $this->shippedApps);
400
	}
401
402
	private function isAlwaysEnabled($appId) {
403
		$alwaysEnabled = $this->getAlwaysEnabledApps();
404
		return in_array($appId, $alwaysEnabled);
405
	}
406
407
	private function loadShippedJson() {
408
		if (is_null($this->shippedApps)) {
409
			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
410
			if (!file_exists($shippedJson)) {
411
				throw new \Exception("File not found: $shippedJson");
412
			}
413
			$content = json_decode(file_get_contents($shippedJson), true);
414
			$this->shippedApps = $content['shippedApps'];
415
			$this->alwaysEnabled = $content['alwaysEnabled'];
416
		}
417
	}
418
419
	/**
420
	 * @inheritdoc
421
	 */
422
	public function getAlwaysEnabledApps() {
423
		$this->loadShippedJson();
424
		return $this->alwaysEnabled;
425
	}
426
427
	/**
428
	 * @param string $package package path
429
	 * @param bool $skipMigrations whether to skip migrations, which would only install the code
430
	 * @return string|false app id or false in case of error
431
	 * @since 10.0
432
	 */
433
	public function installApp($package, $skipMigrations = false) {
434
		$appId = Installer::installApp([
435
			'source' => 'local',
436
			'path' => $package
437
		]);
438
		return $appId;
439
	}
440
441
	/**
442
	 * @param string $package
443
	 * @return mixed
444
	 * @since 10.0
445
	 */
446
	public function updateApp($package) {
447
		return Installer::updateApp([
448
			'source' => 'local',
449
			'path' => $package
450
		]);
451
	}
452
453
	/**
454
	 * Returns the list of all apps, enabled and disabled
455
	 *
456
	 * @return string[]
457
	 * @since 10.0
458
	 */
459
	public function getAllApps() {
460
		return $this->appConfig->getApps();
461
	}
462
463
	/**
464
	 * @param string $path
465
	 * @return string[] app info
466
	 */
467
	public function readAppPackage($path) {
468
		$data = [
469
			'source' => 'path',
470
			'path' => $path,
471
		];
472
		list($appCodeDir, $path) = Installer::downloadApp($data);
473
		$appInfo = Installer::checkAppsIntegrity($data, $appCodeDir, $path);
474
		Files::rmdirr($appCodeDir);
475
		return $appInfo;
476
	}
477
478
	/**
479
	 * Indicates if app installation is supported. Usually it is but in certain
480
	 * environments it is disallowed because of hardening. In a clustered setup
481
	 * apps need to be installed on each cluster node which is out of scope of
482
	 * ownCloud itself.
483
	 *
484
	 * @return bool
485
	 * @since 10.0.3
486
	 */
487
	public function canInstall() {
488
		if ($this->config->getSystemValue('operation.mode', 'single-instance') !== 'single-instance') {
489
			return false;
490
		}
491
492
		$appsFolder = OC_App::getInstallPath();
493
		return $appsFolder !== null && is_writable($appsFolder) && is_readable($appsFolder);
494
	}
495
496
	/**
497
	 * Get the absolute path to the directory for the given app.
498
	 * If the app exists in multiple directories, the most recent version is taken.
499
	 * Returns false if not found
500
	 *
501
	 * @param string $appId
502
	 * @return string|false
503
	 * @since 10.0.5
504
	 */
505
	public function getAppPath($appId) {
506
		if (trim($appId) === '') {
507
			return false;
508
		}
509
		if (($appRoot = $this->findAppInDirectories($appId)) !== false) {
510
			return $appRoot['path'];
511
		}
512
		return false;
513
	}
514
515
	/**
516
	 * Get the HTTP Web path to the app directory for the given app, relative to the ownCloud webroot.
517
	 * If the app exists in multiple directories, web path to the most recent version is taken.
518
	 * Returns false if not found
519
	 *
520
	 * @param string $appId
521
	 * @return string|false
522
	 * @since 10.0.5
523
	 */
524
	public function getAppWebPath($appId) {
525
		if (($appRoot = $this->findAppInDirectories($appId)) !== false) {
526
			$ocWebRoot = $this->getOcWebRoot();
527
			// consider all relative ../ in the app web path as an adjustment
528
			// for oC web root
529
			while (strpos($appRoot['url'], '../') === 0) {
530
				$appRoot['url'] = substr($appRoot['url'], 3);
531
				$ocWebRoot = dirname($ocWebRoot);
532
			}
533
			return $ocWebRoot . '/' . ltrim($appRoot['url'], '/');
534
		}
535
		return false;
536
	}
537
538
	/**
539
	 * Search for an app in all app directories
540
	 * Returns an app directory as an array with keys
541
	 *  'path' - a path to the app with no trailing slash
542
	 *  'url' - a web path to the app with no trailing slash
543
	 * both are relative to OC root directory and webroot
544
	 *
545
	 * @param string $appId
546
	 * @return false|string[]
547
	 */
548
	protected function findAppInDirectories($appId) {
549
		$sanitizedAppId = \OC_App::cleanAppId($appId);
550
		if ($sanitizedAppId !== $appId) {
551
			return false;
552
		}
553
554
		if (!isset($this->appDirs[$appId])) {
555
			$possibleAppRoots = [];
556
			foreach ($this->getAppRoots() as $appRoot) {
557
				if (is_dir($appRoot['path'] . '/' . $appId)) {
558
					$possibleAppRoots[] = $appRoot;
559
				}
560
			}
561
562
			$versionToLoad = [];
563
			foreach ($possibleAppRoots as $possibleAppRoot) {
564
				$version = $this->getAppVersionByPath($possibleAppRoot['path'] . '/' . $appId);
565
				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
566
					$versionToLoad = array_merge($possibleAppRoot, ['version' => $version]);
567
					$versionToLoad['path'] .= '/' . $appId;
568
					$versionToLoad['url'] .= '/' . $appId;
569
				}
570
			}
571
572
			if (empty($versionToLoad)) {
573
				return false;
574
			}
575
			$this->saveAppPath($appId, $versionToLoad);
576
		}
577
		return $this->appDirs[$appId];
578
	}
579
580
	/**
581
	 * Save app path and webPath to internal cache
582
	 * @param string $appId
583
	 * @param string[] $appData
584
	 */
585
	protected function saveAppPath($appId, $appData) {
586
		$this->appDirs[$appId] = $appData;
587
	}
588
589
	/**
590
	 * Get OC web root
591
	 * Wrapper for easy mocking
592
	 * @return string
593
	 */
594
	protected function getOcWebRoot() {
595
		return \OC::$WEBROOT;
596
	}
597
598
	/**
599
	 * Get apps roots as an array of path and url
600
	 * Wrapper for easy mocking
601
	 * @return string[][]
602
	 */
603
	protected function getAppRoots(){
604
		return \OC::$APPSROOTS;
605
	}
606
607
	/**
608
	 * Get app's version based on it's path
609
	 * Wrapper for easy mocking
610
	 *
611
	 * @param string $path
612
	 * @return string
613
	 */
614
	protected function getAppVersionByPath($path) {
615
		return \OC_App::getAppVersionByPath($path);
616
	}
617
}
618