Completed
Push — master ( 3da7b8...fc6dc3 )
by Thomas
22:50 queued 13:32
created

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