Completed
Push — master ( d953db...ac63c2 )
by Morris
38s
created

AppManager::getAppInfo()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 18
nc 10
nop 3
dl 0
loc 28
rs 6.7272
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\IGroupManager;
41
use OCP\IUser;
42
use OCP\IUserSession;
43
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
44
45
class AppManager implements IAppManager {
46
47
	/**
48
	 * Apps with these types can not be enabled for certain groups only
49
	 * @var string[]
50
	 */
51
	protected $protectedAppTypes = [
52
		'filesystem',
53
		'prelogin',
54
		'authentication',
55
		'logging',
56
		'prevent_group_restriction',
57
	];
58
59
	/** @var IUserSession */
60
	private $userSession;
61
62
	/** @var AppConfig */
63
	private $appConfig;
64
65
	/** @var IGroupManager */
66
	private $groupManager;
67
68
	/** @var ICacheFactory */
69
	private $memCacheFactory;
70
71
	/** @var EventDispatcherInterface */
72
	private $dispatcher;
73
74
	/** @var string[] $appId => $enabled */
75
	private $installedAppsCache;
76
77
	/** @var string[] */
78
	private $shippedApps;
79
80
	/** @var string[] */
81
	private $alwaysEnabled;
82
83
	/** @var array */
84
	private $appInfos = [];
85
86
	/** @var array */
87
	private $appVersions = [];
88
89
	/**
90
	 * @param IUserSession $userSession
91
	 * @param AppConfig $appConfig
92
	 * @param IGroupManager $groupManager
93
	 * @param ICacheFactory $memCacheFactory
94
	 * @param EventDispatcherInterface $dispatcher
95
	 */
96
	public function __construct(IUserSession $userSession,
97
								AppConfig $appConfig,
98
								IGroupManager $groupManager,
99
								ICacheFactory $memCacheFactory,
100
								EventDispatcherInterface $dispatcher) {
101
		$this->userSession = $userSession;
102
		$this->appConfig = $appConfig;
103
		$this->groupManager = $groupManager;
104
		$this->memCacheFactory = $memCacheFactory;
105
		$this->dispatcher = $dispatcher;
106
	}
107
108
	/**
109
	 * @return string[] $appId => $enabled
110
	 */
111
	private function getInstalledAppsValues() {
112
		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...
113
			$values = $this->appConfig->getValues(false, 'enabled');
114
115
			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
116
			foreach($alwaysEnabledApps as $appId) {
117
				$values[$appId] = 'yes';
118
			}
119
120
			$this->installedAppsCache = array_filter($values, function ($value) {
121
				return $value !== 'no';
122
			});
123
			ksort($this->installedAppsCache);
124
		}
125
		return $this->installedAppsCache;
126
	}
127
128
	/**
129
	 * List all installed apps
130
	 *
131
	 * @return string[]
132
	 */
133
	public function getInstalledApps() {
134
		return array_keys($this->getInstalledAppsValues());
135
	}
136
137
	/**
138
	 * List all apps enabled for a user
139
	 *
140
	 * @param \OCP\IUser $user
141
	 * @return string[]
142
	 */
143
	public function getEnabledAppsForUser(IUser $user) {
144
		$apps = $this->getInstalledAppsValues();
145
		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
146
			return $this->checkAppForUser($enabled, $user);
147
		});
148
		return array_keys($appsForUser);
149
	}
150
151
	/**
152
	 * Check if an app is enabled for user
153
	 *
154
	 * @param string $appId
155
	 * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used
156
	 * @return bool
157
	 */
158
	public function isEnabledForUser($appId, $user = null) {
159
		if ($this->isAlwaysEnabled($appId)) {
160
			return true;
161
		}
162
		if ($user === null) {
163
			$user = $this->userSession->getUser();
164
		}
165
		$installedApps = $this->getInstalledAppsValues();
166
		if (isset($installedApps[$appId])) {
167
			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...
168
		} else {
169
			return false;
170
		}
171
	}
172
173
	/**
174
	 * @param string $enabled
175
	 * @param IUser $user
176
	 * @return bool
177
	 */
178
	private function checkAppForUser($enabled, $user) {
179
		if ($enabled === 'yes') {
180
			return true;
181
		} elseif ($user === null) {
182
			return false;
183
		} else {
184
			if(empty($enabled)){
185
				return false;
186
			}
187
188
			$groupIds = json_decode($enabled);
189
190
			if (!is_array($groupIds)) {
191
				$jsonError = json_last_error();
192
				\OC::$server->getLogger()->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']);
193
				return false;
194
			}
195
196
			$userGroups = $this->groupManager->getUserGroupIds($user);
197
			foreach ($userGroups as $groupId) {
198
				if (in_array($groupId, $groupIds, true)) {
199
					return true;
200
				}
201
			}
202
			return false;
203
		}
204
	}
205
206
	/**
207
	 * Check if an app is installed in the instance
208
	 *
209
	 * @param string $appId
210
	 * @return bool
211
	 */
212
	public function isInstalled($appId) {
213
		$installedApps = $this->getInstalledAppsValues();
214
		return isset($installedApps[$appId]);
215
	}
216
217
	/**
218
	 * Enable an app for every user
219
	 *
220
	 * @param string $appId
221
	 * @throws AppPathNotFoundException
222
	 */
223 View Code Duplication
	public function enableApp($appId) {
224
		// Check if app exists
225
		$this->getAppPath($appId);
226
227
		$this->installedAppsCache[$appId] = 'yes';
228
		$this->appConfig->setValue($appId, 'enabled', 'yes');
229
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
230
			ManagerEvent::EVENT_APP_ENABLE, $appId
231
		));
232
		$this->clearAppsCache();
233
	}
234
235
	/**
236
	 * Whether a list of types contains a protected app type
237
	 *
238
	 * @param string[] $types
239
	 * @return bool
240
	 */
241
	public function hasProtectedAppType($types) {
242
		if (empty($types)) {
243
			return false;
244
		}
245
246
		$protectedTypes = array_intersect($this->protectedAppTypes, $types);
247
		return !empty($protectedTypes);
248
	}
249
250
	/**
251
	 * Enable an app only for specific groups
252
	 *
253
	 * @param string $appId
254
	 * @param \OCP\IGroup[] $groups
255
	 * @throws \Exception if app can't be enabled for groups
256
	 */
257
	public function enableAppForGroups($appId, $groups) {
258
		$info = $this->getAppInfo($appId);
259
		if (!empty($info['types'])) {
260
			$protectedTypes = array_intersect($this->protectedAppTypes, $info['types']);
261
			if (!empty($protectedTypes)) {
262
				throw new \Exception("$appId can't be enabled for groups.");
263
			}
264
		}
265
266
		$groupIds = array_map(function ($group) {
267
			/** @var \OCP\IGroup $group */
268
			return $group->getGID();
269
		}, $groups);
270
		$this->installedAppsCache[$appId] = json_encode($groupIds);
271
		$this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
272
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
273
			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
274
		));
275
		$this->clearAppsCache();
276
	}
277
278
	/**
279
	 * Disable an app for every user
280
	 *
281
	 * @param string $appId
282
	 * @throws \Exception if app can't be disabled
283
	 */
284 View Code Duplication
	public function disableApp($appId) {
285
		if ($this->isAlwaysEnabled($appId)) {
286
			throw new \Exception("$appId can't be disabled.");
287
		}
288
		unset($this->installedAppsCache[$appId]);
289
		$this->appConfig->setValue($appId, 'enabled', 'no');
290
		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
291
			ManagerEvent::EVENT_APP_DISABLE, $appId
292
		));
293
		$this->clearAppsCache();
294
	}
295
296
	/**
297
	 * Get the directory for the given app.
298
	 *
299
	 * @param string $appId
300
	 * @return string
301
	 * @throws AppPathNotFoundException if app folder can't be found
302
	 */
303
	public function getAppPath($appId) {
304
		$appPath = \OC_App::getAppPath($appId);
305
		if($appPath === false) {
306
			throw new AppPathNotFoundException('Could not find path for ' . $appId);
307
		}
308
		return $appPath;
309
	}
310
311
	/**
312
	 * Clear the cached list of apps when enabling/disabling an app
313
	 */
314
	public function clearAppsCache() {
315
		$settingsMemCache = $this->memCacheFactory->createDistributed('settings');
316
		$settingsMemCache->clear('listApps');
317
	}
318
319
	/**
320
	 * Returns a list of apps that need upgrade
321
	 *
322
	 * @param string $version Nextcloud version as array of version components
323
	 * @return array list of app info from apps that need an upgrade
324
	 *
325
	 * @internal
326
	 */
327
	public function getAppsNeedingUpgrade($version) {
328
		$appsToUpgrade = [];
329
		$apps = $this->getInstalledApps();
330
		foreach ($apps as $appId) {
331
			$appInfo = $this->getAppInfo($appId);
332
			$appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
333
			if ($appDbVersion
334
				&& isset($appInfo['version'])
335
				&& version_compare($appInfo['version'], $appDbVersion, '>')
336
				&& \OC_App::isAppCompatible($version, $appInfo)
337
			) {
338
				$appsToUpgrade[] = $appInfo;
339
			}
340
		}
341
342
		return $appsToUpgrade;
343
	}
344
345
	/**
346
	 * Returns the app information from "appinfo/info.xml".
347
	 *
348
	 * @param string $appId app id
349
	 *
350
	 * @param bool $path
351
	 * @param null $lang
352
	 * @return array app info
353
	 */
354
	public function getAppInfo(string $appId, bool $path = false, $lang = null) {
355
		if ($path) {
356
			$file = $appId;
357
		} else {
358
			if ($lang === null && isset($this->appInfos[$appId])) {
359
				return $this->appInfos[$appId];
360
			}
361
			try {
362
				$appPath = $this->getAppPath($appId);
363
			} catch (AppPathNotFoundException $e) {
364
				return null;
365
			}
366
			$file = $appPath . '/appinfo/info.xml';
367
		}
368
369
		$parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
370
		$data = $parser->parse($file);
371
372
		if (is_array($data)) {
373
			$data = \OC_App::parseAppInfo($data, $lang);
374
		}
375
376
		if ($lang === null) {
377
			$this->appInfos[$appId] = $data;
378
		}
379
380
		return $data;
381
	}
382
383
	public function getAppVersion(string $appId, bool $useCache = true): string {
384
		if(!$useCache || !isset($this->appVersions[$appId])) {
385
			$appInfo = \OC::$server->getAppManager()->getAppInfo($appId);
386
			$this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
387
		}
388
		return $this->appVersions[$appId];
389
	}
390
391
	/**
392
	 * Returns a list of apps incompatible with the given version
393
	 *
394
	 * @param string $version Nextcloud version as array of version components
395
	 *
396
	 * @return array list of app info from incompatible apps
397
	 *
398
	 * @internal
399
	 */
400
	public function getIncompatibleApps($version) {
401
		$apps = $this->getInstalledApps();
402
		$incompatibleApps = array();
403
		foreach ($apps as $appId) {
404
			$info = $this->getAppInfo($appId);
405
			if (!\OC_App::isAppCompatible($version, $info)) {
406
				$incompatibleApps[] = $info;
407
			}
408
		}
409
		return $incompatibleApps;
410
	}
411
412
	/**
413
	 * @inheritdoc
414
	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
415
	 */
416
	public function isShipped($appId) {
417
		$this->loadShippedJson();
418
		return in_array($appId, $this->shippedApps, true);
419
	}
420
421
	private function isAlwaysEnabled($appId) {
422
		$alwaysEnabled = $this->getAlwaysEnabledApps();
423
		return in_array($appId, $alwaysEnabled, true);
424
	}
425
426
	/**
427
	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
428
	 * @throws \Exception
429
	 */
430 View Code Duplication
	private function loadShippedJson() {
431
		if ($this->shippedApps === null) {
432
			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
433
			if (!file_exists($shippedJson)) {
434
				throw new \Exception("File not found: $shippedJson");
435
			}
436
			$content = json_decode(file_get_contents($shippedJson), true);
437
			$this->shippedApps = $content['shippedApps'];
438
			$this->alwaysEnabled = $content['alwaysEnabled'];
439
		}
440
	}
441
442
	/**
443
	 * @inheritdoc
444
	 */
445
	public function getAlwaysEnabledApps() {
446
		$this->loadShippedJson();
447
		return $this->alwaysEnabled;
448
	}
449
}
450