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