Passed
Push — master ( 17cdcf...aa80aa )
by John
09:42 queued 11s
created

AppSettingsController::viewApps()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 0
dl 0
loc 15
rs 9.8666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2016, Lukas Reschke <[email protected]>
5
 *
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Felix A. Epp <[email protected]>
8
 * @author Jan-Christoph Borchardt <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Julius Härtl <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 *
16
 * @license AGPL-3.0
17
 *
18
 * This code is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License, version 3,
20
 * as published by the Free Software Foundation.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
 * GNU Affero General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU Affero General Public License, version 3,
28
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
29
 *
30
 */
31
32
namespace OCA\Settings\Controller;
33
34
use OC\App\AppStore\Bundles\BundleFetcher;
35
use OC\App\AppStore\Fetcher\AppFetcher;
36
use OC\App\AppStore\Fetcher\CategoryFetcher;
37
use OC\App\AppStore\Version\VersionParser;
38
use OC\App\DependencyAnalyzer;
39
use OC\App\Platform;
40
use OC\Installer;
41
use OC_App;
42
use OCP\App\IAppManager;
43
use \OCP\AppFramework\Controller;
44
use OCP\AppFramework\Http;
45
use OCP\AppFramework\Http\ContentSecurityPolicy;
46
use OCP\AppFramework\Http\JSONResponse;
47
use OCP\AppFramework\Http\TemplateResponse;
48
use OCP\ILogger;
49
use OCP\INavigationManager;
50
use OCP\IRequest;
51
use OCP\IL10N;
52
use OCP\IConfig;
53
use OCP\IURLGenerator;
54
use OCP\L10N\IFactory;
55
56
class AppSettingsController extends Controller {
57
58
	/** @var \OCP\IL10N */
59
	private $l10n;
60
	/** @var IConfig */
61
	private $config;
62
	/** @var INavigationManager */
63
	private $navigationManager;
64
	/** @var IAppManager */
65
	private $appManager;
66
	/** @var CategoryFetcher */
67
	private $categoryFetcher;
68
	/** @var AppFetcher */
69
	private $appFetcher;
70
	/** @var IFactory */
71
	private $l10nFactory;
72
	/** @var BundleFetcher */
73
	private $bundleFetcher;
74
	/** @var Installer */
75
	private $installer;
76
	/** @var IURLGenerator */
77
	private $urlGenerator;
78
	/** @var ILogger */
79
	private $logger;
80
81
	/** @var array */
82
	private $allApps = [];
83
84
	/**
85
	 * @param string $appName
86
	 * @param IRequest $request
87
	 * @param IL10N $l10n
88
	 * @param IConfig $config
89
	 * @param INavigationManager $navigationManager
90
	 * @param IAppManager $appManager
91
	 * @param CategoryFetcher $categoryFetcher
92
	 * @param AppFetcher $appFetcher
93
	 * @param IFactory $l10nFactory
94
	 * @param BundleFetcher $bundleFetcher
95
	 * @param Installer $installer
96
	 * @param IURLGenerator $urlGenerator
97
	 * @param ILogger $logger
98
	 */
99
	public function __construct(string $appName,
100
								IRequest $request,
101
								IL10N $l10n,
102
								IConfig $config,
103
								INavigationManager $navigationManager,
104
								IAppManager $appManager,
105
								CategoryFetcher $categoryFetcher,
106
								AppFetcher $appFetcher,
107
								IFactory $l10nFactory,
108
								BundleFetcher $bundleFetcher,
109
								Installer $installer,
110
								IURLGenerator $urlGenerator,
111
								ILogger $logger) {
112
		parent::__construct($appName, $request);
113
		$this->l10n = $l10n;
114
		$this->config = $config;
115
		$this->navigationManager = $navigationManager;
116
		$this->appManager = $appManager;
117
		$this->categoryFetcher = $categoryFetcher;
118
		$this->appFetcher = $appFetcher;
119
		$this->l10nFactory = $l10nFactory;
120
		$this->bundleFetcher = $bundleFetcher;
121
		$this->installer = $installer;
122
		$this->urlGenerator = $urlGenerator;
123
		$this->logger = $logger;
124
	}
125
126
	/**
127
	 * @NoCSRFRequired
128
	 *
129
	 * @return TemplateResponse
130
	 */
131
	public function viewApps(): TemplateResponse {
132
		\OC_Util::addScript('settings', 'apps');
133
		$params = [];
134
		$params['appstoreEnabled'] = $this->config->getSystemValue('appstoreenabled', true) === true;
135
		$params['updateCount'] = count($this->getAppsWithUpdates());
136
		$params['developerDocumentation'] = $this->urlGenerator->linkToDocs('developer-manual');
137
		$params['bundles'] = $this->getBundles();
138
		$this->navigationManager->setActiveEntry('core_apps');
139
140
		$templateResponse = new TemplateResponse('settings', 'settings-vue', ['serverData' => $params]);
141
		$policy = new ContentSecurityPolicy();
142
		$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
143
		$templateResponse->setContentSecurityPolicy($policy);
144
145
		return $templateResponse;
146
	}
147
148
	private function getAppsWithUpdates() {
149
		$appClass = new \OC_App();
150
		$apps = $appClass->listAllApps();
151
		foreach($apps as $key => $app) {
152
			$newVersion = $this->installer->isUpdateAvailable($app['id']);
153
			if($newVersion === false) {
154
				unset($apps[$key]);
155
			}
156
		}
157
		return $apps;
158
	}
159
160
	private function getBundles() {
161
		$result = [];
162
		$bundles = $this->bundleFetcher->getBundles();
163
		foreach ($bundles as $bundle) {
164
			$result[] = [
165
				'name' => $bundle->getName(),
166
				'id' => $bundle->getIdentifier(),
167
				'appIdentifiers' => $bundle->getAppIdentifiers()
168
			];
169
		}
170
		return $result;
171
172
	}
173
174
	/**
175
	 * Get all available categories
176
	 *
177
	 * @return JSONResponse
178
	 */
179
	public function listCategories(): JSONResponse {
180
		return new JSONResponse($this->getAllCategories());
181
	}
182
183
	private function getAllCategories() {
184
		$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
185
186
		$formattedCategories = [];
187
		$categories = $this->categoryFetcher->get();
188
		foreach($categories as $category) {
189
			$formattedCategories[] = [
190
				'id' => $category['id'],
191
				'ident' => $category['id'],
192
				'displayName' => isset($category['translations'][$currentLanguage]['name']) ? $category['translations'][$currentLanguage]['name'] : $category['translations']['en']['name'],
193
			];
194
		}
195
196
		return $formattedCategories;
197
	}
198
199
	private function fetchApps() {
200
		$appClass = new \OC_App();
201
		$apps = $appClass->listAllApps();
202
		foreach ($apps as $app) {
203
			$app['installed'] = true;
204
			$this->allApps[$app['id']] = $app;
205
		}
206
207
		$apps = $this->getAppsForCategory('');
208
		foreach ($apps as $app) {
209
			$app['appstore'] = true;
210
			if (!array_key_exists($app['id'], $this->allApps)) {
211
				$this->allApps[$app['id']] = $app;
212
			} else {
213
				$this->allApps[$app['id']] = array_merge($app, $this->allApps[$app['id']]);
214
			}
215
		}
216
217
		// add bundle information
218
		$bundles = $this->bundleFetcher->getBundles();
219
		foreach($bundles as $bundle) {
220
			foreach($bundle->getAppIdentifiers() as $identifier) {
221
				foreach($this->allApps as &$app) {
222
					if($app['id'] === $identifier) {
223
						$app['bundleId'] = $bundle->getIdentifier();
224
						continue;
225
					}
226
				}
227
			}
228
		}
229
	}
230
231
	private function getAllApps() {
232
		return $this->allApps;
233
	}
234
	/**
235
	 * Get all available apps in a category
236
	 *
237
	 * @param string $category
238
	 * @return JSONResponse
239
	 * @throws \Exception
240
	 */
241
	public function listApps(): JSONResponse {
242
243
		$this->fetchApps();
244
		$apps = $this->getAllApps();
245
246
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n);
247
248
		// Extend existing app details
249
		$apps = array_map(function($appData) use ($dependencyAnalyzer) {
250
			if (isset($appData['appstoreData'])) {
251
				$appstoreData = $appData['appstoreData'];
252
				$appData['screenshot'] = isset($appstoreData['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($appstoreData['screenshots'][0]['url']) : '';
253
				$appData['category'] = $appstoreData['categories'];
254
			}
255
256
			$newVersion = $this->installer->isUpdateAvailable($appData['id']);
257
			if($newVersion) {
258
				$appData['update'] = $newVersion;
259
			}
260
261
			// fix groups to be an array
262
			$groups = array();
263
			if (is_string($appData['groups'])) {
264
				$groups = json_decode($appData['groups']);
265
			}
266
			$appData['groups'] = $groups;
267
			$appData['canUnInstall'] = !$appData['active'] && $appData['removable'];
268
269
			// fix licence vs license
270
			if (isset($appData['license']) && !isset($appData['licence'])) {
271
				$appData['licence'] = $appData['license'];
272
			}
273
274
			$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
275
			$ignoreMax = in_array($appData['id'], $ignoreMaxApps);
276
277
			// analyse dependencies
278
			$missing = $dependencyAnalyzer->analyze($appData, $ignoreMax);
279
			$appData['canInstall'] = empty($missing);
280
			$appData['missingDependencies'] = $missing;
281
282
			$appData['missingMinOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
283
			$appData['missingMaxOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
284
			$appData['isCompatible'] = $dependencyAnalyzer->isMarkedCompatible($appData);
285
286
			return $appData;
287
		}, $apps);
288
289
		usort($apps, [$this, 'sortApps']);
290
291
		return new JSONResponse(['apps' => $apps, 'status' => 'success']);
292
	}
293
294
	/**
295
	 * Get all apps for a category from the app store
296
	 *
297
	 * @param string $requestedCategory
298
	 * @return array
299
	 * @throws \Exception
300
	 */
301
	private function getAppsForCategory($requestedCategory = ''): array {
302
		$versionParser = new VersionParser();
303
		$formattedApps = [];
304
		$apps = $this->appFetcher->get();
305
		foreach($apps as $app) {
306
			// Skip all apps not in the requested category
307
			if ($requestedCategory !== '') {
308
				$isInCategory = false;
309
				foreach($app['categories'] as $category) {
310
					if($category === $requestedCategory) {
311
						$isInCategory = true;
312
					}
313
				}
314
				if(!$isInCategory) {
315
					continue;
316
				}
317
			}
318
319
			if (!isset($app['releases'][0]['rawPlatformVersionSpec'])) {
320
				continue;
321
			}
322
			$nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']);
323
			$nextCloudVersionDependencies = [];
324
			if($nextCloudVersion->getMinimumVersion() !== '') {
325
				$nextCloudVersionDependencies['nextcloud']['@attributes']['min-version'] = $nextCloudVersion->getMinimumVersion();
326
			}
327
			if($nextCloudVersion->getMaximumVersion() !== '') {
328
				$nextCloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion();
329
			}
330
			$phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']);
331
			$existsLocally = \OC_App::getAppPath($app['id']) !== false;
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

331
			$existsLocally = /** @scrutinizer ignore-deprecated */ \OC_App::getAppPath($app['id']) !== false;

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...
332
			$phpDependencies = [];
333
			if($phpVersion->getMinimumVersion() !== '') {
334
				$phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion();
335
			}
336
			if($phpVersion->getMaximumVersion() !== '') {
337
				$phpDependencies['php']['@attributes']['max-version'] = $phpVersion->getMaximumVersion();
338
			}
339
			if(isset($app['releases'][0]['minIntSize'])) {
340
				$phpDependencies['php']['@attributes']['min-int-size'] = $app['releases'][0]['minIntSize'];
341
			}
342
			$authors = '';
343
			foreach($app['authors'] as $key => $author) {
344
				$authors .= $author['name'];
345
				if($key !== count($app['authors']) - 1) {
346
					$authors .= ', ';
347
				}
348
			}
349
350
			$currentLanguage = substr(\OC::$server->getL10NFactory()->findLanguage(), 0, 2);
351
			$enabledValue = $this->config->getAppValue($app['id'], 'enabled', 'no');
352
			$groups = null;
353
			if($enabledValue !== 'no' && $enabledValue !== 'yes') {
354
				$groups = $enabledValue;
355
			}
356
357
			$currentVersion = '';
358
			if($this->appManager->isInstalled($app['id'])) {
359
				$currentVersion = $this->appManager->getAppVersion($app['id']);
360
			} else {
361
				$currentLanguage = $app['releases'][0]['version'];
362
			}
363
364
			$formattedApps[] = [
365
				'id' => $app['id'],
366
				'name' => isset($app['translations'][$currentLanguage]['name']) ? $app['translations'][$currentLanguage]['name'] : $app['translations']['en']['name'],
367
				'description' => isset($app['translations'][$currentLanguage]['description']) ? $app['translations'][$currentLanguage]['description'] : $app['translations']['en']['description'],
368
				'summary' => isset($app['translations'][$currentLanguage]['summary']) ? $app['translations'][$currentLanguage]['summary'] : $app['translations']['en']['summary'],
369
				'license' => $app['releases'][0]['licenses'],
370
				'author' => $authors,
371
				'shipped' => false,
372
				'version' => $currentVersion,
373
				'default_enable' => '',
374
				'types' => [],
375
				'documentation' => [
376
					'admin' => $app['adminDocs'],
377
					'user' => $app['userDocs'],
378
					'developer' => $app['developerDocs']
379
				],
380
				'website' => $app['website'],
381
				'bugs' => $app['issueTracker'],
382
				'detailpage' => $app['website'],
383
				'dependencies' => array_merge(
384
					$nextCloudVersionDependencies,
385
					$phpDependencies
386
				),
387
				'level' => ($app['isFeatured'] === true) ? 200 : 100,
388
				'missingMaxOwnCloudVersion' => false,
389
				'missingMinOwnCloudVersion' => false,
390
				'canInstall' => true,
391
				'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '',
392
				'score' => $app['ratingOverall'],
393
				'ratingNumOverall' => $app['ratingNumOverall'],
394
				'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5,
395
				'removable' => $existsLocally,
396
				'active' => $this->appManager->isEnabledForUser($app['id']),
397
				'needsDownload' => !$existsLocally,
398
				'groups' => $groups,
399
				'fromAppStore' => true,
400
				'appstoreData' => $app,
401
			];
402
		}
403
404
		return $formattedApps;
405
	}
406
407
	/**
408
	 * @PasswordConfirmationRequired
409
	 *
410
	 * @param string $appId
411
	 * @param array $groups
412
	 * @return JSONResponse
413
	 */
414
	public function enableApp(string $appId, array $groups = []): JSONResponse {
415
		return $this->enableApps([$appId], $groups);
416
	}
417
418
	/**
419
	 * Enable one or more apps
420
	 *
421
	 * apps will be enabled for specific groups only if $groups is defined
422
	 *
423
	 * @PasswordConfirmationRequired
424
	 * @param array $appIds
425
	 * @param array $groups
426
	 * @return JSONResponse
427
	 */
428
	public function enableApps(array $appIds, array $groups = []): JSONResponse {
429
		try {
430
			$updateRequired = false;
431
432
			foreach ($appIds as $appId) {
433
				$appId = OC_App::cleanAppId($appId);
434
435
				// Check if app is already downloaded
436
				/** @var Installer $installer */
437
				$installer = \OC::$server->query(Installer::class);
438
				$isDownloaded = $installer->isDownloaded($appId);
439
440
				if(!$isDownloaded) {
441
					$installer->downloadApp($appId);
442
				}
443
444
				$installer->installApp($appId);
445
446
				if (count($groups) > 0) {
447
					$this->appManager->enableAppForGroups($appId, $this->getGroupList($groups));
448
				} else {
449
					$this->appManager->enableApp($appId);
450
				}
451
				if (\OC_App::shouldUpgrade($appId)) {
452
					$updateRequired = true;
453
				}
454
			}
455
			return new JSONResponse(['data' => ['update_required' => $updateRequired]]);
456
457
		} catch (\Exception $e) {
458
			$this->logger->logException($e);
459
			return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
460
		}
461
	}
462
463
	private function getGroupList(array $groups) {
464
		$groupManager = \OC::$server->getGroupManager();
465
		$groupsList = [];
466
		foreach ($groups as $group) {
467
			$groupItem = $groupManager->get($group);
468
			if ($groupItem instanceof \OCP\IGroup) {
469
				$groupsList[] = $groupManager->get($group);
470
			}
471
		}
472
		return $groupsList;
473
	}
474
475
	/**
476
	 * @PasswordConfirmationRequired
477
	 *
478
	 * @param string $appId
479
	 * @return JSONResponse
480
	 */
481
	public function disableApp(string $appId): JSONResponse {
482
		return $this->disableApps([$appId]);
483
	}
484
485
	/**
486
	 * @PasswordConfirmationRequired
487
	 *
488
	 * @param array $appIds
489
	 * @return JSONResponse
490
	 */
491
	public function disableApps(array $appIds): JSONResponse {
492
		try {
493
			foreach ($appIds as $appId) {
494
				$appId = OC_App::cleanAppId($appId);
495
				$this->appManager->disableApp($appId);
496
			}
497
			return new JSONResponse([]);
498
		} catch (\Exception $e) {
499
			$this->logger->logException($e);
500
			return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
501
		}
502
	}
503
504
	/**
505
	 * @PasswordConfirmationRequired
506
	 *
507
	 * @param string $appId
508
	 * @return JSONResponse
509
	 */
510
	public function uninstallApp(string $appId): JSONResponse {
511
		$appId = OC_App::cleanAppId($appId);
512
		$result = $this->installer->removeApp($appId);
513
		if($result !== false) {
514
			$this->appManager->clearAppsCache();
515
			return new JSONResponse(['data' => ['appid' => $appId]]);
516
		}
517
		return new JSONResponse(['data' => ['message' => $this->l10n->t('Couldn\'t remove app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
518
	}
519
520
	/**
521
	 * @param string $appId
522
	 * @return JSONResponse
523
	 */
524
	public function updateApp(string $appId): JSONResponse {
525
		$appId = OC_App::cleanAppId($appId);
526
527
		$this->config->setSystemValue('maintenance', true);
528
		try {
529
			$result = $this->installer->updateAppstoreApp($appId);
530
			$this->config->setSystemValue('maintenance', false);
531
		} catch (\Exception $ex) {
532
			$this->config->setSystemValue('maintenance', false);
533
			return new JSONResponse(['data' => ['message' => $ex->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
534
		}
535
536
		if ($result !== false) {
537
			return new JSONResponse(['data' => ['appid' => $appId]]);
538
		}
539
		return new JSONResponse(['data' => ['message' => $this->l10n->t('Couldn\'t update app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
540
	}
541
542
	private function sortApps($a, $b) {
543
		$a = (string)$a['name'];
544
		$b = (string)$b['name'];
545
		if ($a === $b) {
546
			return 0;
547
		}
548
		return ($a < $b) ? -1 : 1;
549
	}
550
551
	public function force(string $appId): JSONResponse {
552
		$appId = OC_App::cleanAppId($appId);
553
554
		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
555
		if (!in_array($appId, $ignoreMaxApps, true)) {
556
			$ignoreMaxApps[] = $appId;
557
			$this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
558
		}
559
560
		return new JSONResponse();
561
	}
562
563
}
564