AppSettingsController   F
last analyzed

Complexity

Total Complexity 71

Size/Duplication

Total Lines 505
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 258
dl 0
loc 505
rs 2.7199
c 1
b 0
f 0
wmc 71

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 25 1
A getBundles() 0 11 2
A getAppsWithUpdates() 0 10 3
A disableApp() 0 2 1
A uninstallApp() 0 8 2
A updateApp() 0 16 3
A enableApps() 0 31 6
A getGroupList() 0 10 3
B fetchApps() 0 31 9
A enableApp() 0 2 1
A disableApps() 0 10 3
A listCategories() 0 2 1
A getAllApps() 0 2 1
A sortApps() 0 7 3
A force() 0 4 1
A viewApps() 0 14 1
B listApps() 0 56 9
A getAllCategories() 0 14 2
F getAppsForCategory() 0 104 19

How to fix   Complexity   

Complex Class

Complex classes like AppSettingsController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AppSettingsController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2016, Lukas Reschke <[email protected]>
5
 *
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Daniel Kesselberg <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author John Molakvoæ <[email protected]>
11
 * @author Julius Härtl <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Morris Jobke <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Thomas Müller <[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
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\IConfig;
49
use OCP\IL10N;
50
use OCP\INavigationManager;
51
use OCP\IRequest;
52
use OCP\IURLGenerator;
53
use OCP\L10N\IFactory;
54
use Psr\Log\LoggerInterface;
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 LoggerInterface */
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 LoggerInterface $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
								LoggerInterface $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
		$params = [];
133
		$params['appstoreEnabled'] = $this->config->getSystemValueBool('appstoreenabled', true);
134
		$params['updateCount'] = count($this->getAppsWithUpdates());
135
		$params['developerDocumentation'] = $this->urlGenerator->linkToDocs('developer-manual');
136
		$params['bundles'] = $this->getBundles();
137
		$this->navigationManager->setActiveEntry('core_apps');
138
139
		$templateResponse = new TemplateResponse('settings', 'settings-vue', ['serverData' => $params, 'pageTitle' => $this->l10n->t('Apps')]);
140
		$policy = new ContentSecurityPolicy();
141
		$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
142
		$templateResponse->setContentSecurityPolicy($policy);
143
144
		return $templateResponse;
145
	}
146
147
	private function getAppsWithUpdates() {
148
		$appClass = new \OC_App();
149
		$apps = $appClass->listAllApps();
150
		foreach ($apps as $key => $app) {
151
			$newVersion = $this->installer->isUpdateAvailable($app['id']);
152
			if ($newVersion === false) {
153
				unset($apps[$key]);
154
			}
155
		}
156
		return $apps;
157
	}
158
159
	private function getBundles() {
160
		$result = [];
161
		$bundles = $this->bundleFetcher->getBundles();
162
		foreach ($bundles as $bundle) {
163
			$result[] = [
164
				'name' => $bundle->getName(),
165
				'id' => $bundle->getIdentifier(),
166
				'appIdentifiers' => $bundle->getAppIdentifiers()
167
			];
168
		}
169
		return $result;
170
	}
171
172
	/**
173
	 * Get all available categories
174
	 *
175
	 * @return JSONResponse
176
	 */
177
	public function listCategories(): JSONResponse {
178
		return new JSONResponse($this->getAllCategories());
179
	}
180
181
	private function getAllCategories() {
182
		$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
183
184
		$formattedCategories = [];
185
		$categories = $this->categoryFetcher->get();
186
		foreach ($categories as $category) {
187
			$formattedCategories[] = [
188
				'id' => $category['id'],
189
				'ident' => $category['id'],
190
				'displayName' => $category['translations'][$currentLanguage]['name'] ?? $category['translations']['en']['name'],
191
			];
192
		}
193
194
		return $formattedCategories;
195
	}
196
197
	private function fetchApps() {
198
		$appClass = new \OC_App();
199
		$apps = $appClass->listAllApps();
200
		foreach ($apps as $app) {
201
			$app['installed'] = true;
202
			$this->allApps[$app['id']] = $app;
203
		}
204
205
		$apps = $this->getAppsForCategory('');
206
		$supportedApps = $appClass->getSupportedApps();
207
		foreach ($apps as $app) {
208
			$app['appstore'] = true;
209
			if (!array_key_exists($app['id'], $this->allApps)) {
210
				$this->allApps[$app['id']] = $app;
211
			} else {
212
				$this->allApps[$app['id']] = array_merge($app, $this->allApps[$app['id']]);
213
			}
214
215
			if (in_array($app['id'], $supportedApps)) {
216
				$this->allApps[$app['id']]['level'] = \OC_App::supportedApp;
217
			}
218
		}
219
220
		// add bundle information
221
		$bundles = $this->bundleFetcher->getBundles();
222
		foreach ($bundles as $bundle) {
223
			foreach ($bundle->getAppIdentifiers() as $identifier) {
224
				foreach ($this->allApps as &$app) {
225
					if ($app['id'] === $identifier) {
226
						$app['bundleIds'][] = $bundle->getIdentifier();
227
						continue;
228
					}
229
				}
230
			}
231
		}
232
	}
233
234
	private function getAllApps() {
235
		return $this->allApps;
236
	}
237
	/**
238
	 * Get all available apps in a category
239
	 *
240
	 * @return JSONResponse
241
	 * @throws \Exception
242
	 */
243
	public function listApps(): JSONResponse {
244
		$this->fetchApps();
245
		$apps = $this->getAllApps();
246
247
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n);
248
249
		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
250
		if (!is_array($ignoreMaxApps)) {
251
			$this->logger->warning('The value given for app_install_overwrite is not an array. Ignoring...');
252
			$ignoreMaxApps = [];
253
		}
254
255
		// Extend existing app details
256
		$apps = array_map(function (array $appData) use ($dependencyAnalyzer, $ignoreMaxApps) {
257
			if (isset($appData['appstoreData'])) {
258
				$appstoreData = $appData['appstoreData'];
259
				$appData['screenshot'] = isset($appstoreData['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($appstoreData['screenshots'][0]['url']) : '';
260
				$appData['category'] = $appstoreData['categories'];
261
				$appData['releases'] = $appstoreData['releases'];
262
			}
263
264
			$newVersion = $this->installer->isUpdateAvailable($appData['id']);
265
			if ($newVersion) {
266
				$appData['update'] = $newVersion;
267
			}
268
269
			// fix groups to be an array
270
			$groups = [];
271
			if (is_string($appData['groups'])) {
272
				$groups = json_decode($appData['groups']);
273
			}
274
			$appData['groups'] = $groups;
275
			$appData['canUnInstall'] = !$appData['active'] && $appData['removable'];
276
277
			// fix licence vs license
278
			if (isset($appData['license']) && !isset($appData['licence'])) {
279
				$appData['licence'] = $appData['license'];
280
			}
281
282
			$ignoreMax = in_array($appData['id'], $ignoreMaxApps);
283
284
			// analyse dependencies
285
			$missing = $dependencyAnalyzer->analyze($appData, $ignoreMax);
286
			$appData['canInstall'] = empty($missing);
287
			$appData['missingDependencies'] = $missing;
288
289
			$appData['missingMinOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
290
			$appData['missingMaxOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
291
			$appData['isCompatible'] = $dependencyAnalyzer->isMarkedCompatible($appData);
292
293
			return $appData;
294
		}, $apps);
295
296
		usort($apps, [$this, 'sortApps']);
297
298
		return new JSONResponse(['apps' => $apps, 'status' => 'success']);
299
	}
300
301
	/**
302
	 * Get all apps for a category from the app store
303
	 *
304
	 * @param string $requestedCategory
305
	 * @return array
306
	 * @throws \Exception
307
	 */
308
	private function getAppsForCategory($requestedCategory = ''): array {
309
		$versionParser = new VersionParser();
310
		$formattedApps = [];
311
		$apps = $this->appFetcher->get();
312
		foreach ($apps as $app) {
313
			// Skip all apps not in the requested category
314
			if ($requestedCategory !== '') {
315
				$isInCategory = false;
316
				foreach ($app['categories'] as $category) {
317
					if ($category === $requestedCategory) {
318
						$isInCategory = true;
319
					}
320
				}
321
				if (!$isInCategory) {
322
					continue;
323
				}
324
			}
325
326
			if (!isset($app['releases'][0]['rawPlatformVersionSpec'])) {
327
				continue;
328
			}
329
			$nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']);
330
			$nextCloudVersionDependencies = [];
331
			if ($nextCloudVersion->getMinimumVersion() !== '') {
332
				$nextCloudVersionDependencies['nextcloud']['@attributes']['min-version'] = $nextCloudVersion->getMinimumVersion();
333
			}
334
			if ($nextCloudVersion->getMaximumVersion() !== '') {
335
				$nextCloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion();
336
			}
337
			$phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']);
338
			$existsLocally = \OC_App::getAppPath($app['id']) !== false;
339
			$phpDependencies = [];
340
			if ($phpVersion->getMinimumVersion() !== '') {
341
				$phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion();
342
			}
343
			if ($phpVersion->getMaximumVersion() !== '') {
344
				$phpDependencies['php']['@attributes']['max-version'] = $phpVersion->getMaximumVersion();
345
			}
346
			if (isset($app['releases'][0]['minIntSize'])) {
347
				$phpDependencies['php']['@attributes']['min-int-size'] = $app['releases'][0]['minIntSize'];
348
			}
349
			$authors = '';
350
			foreach ($app['authors'] as $key => $author) {
351
				$authors .= $author['name'];
352
				if ($key !== count($app['authors']) - 1) {
353
					$authors .= ', ';
354
				}
355
			}
356
357
			$currentLanguage = substr(\OC::$server->getL10NFactory()->findLanguage(), 0, 2);
358
			$enabledValue = $this->config->getAppValue($app['id'], 'enabled', 'no');
359
			$groups = null;
360
			if ($enabledValue !== 'no' && $enabledValue !== 'yes') {
361
				$groups = $enabledValue;
362
			}
363
364
			$currentVersion = '';
365
			if ($this->appManager->isInstalled($app['id'])) {
366
				$currentVersion = $this->appManager->getAppVersion($app['id']);
367
			} else {
368
				$currentLanguage = $app['releases'][0]['version'];
369
			}
370
371
			$formattedApps[] = [
372
				'id' => $app['id'],
373
				'name' => $app['translations'][$currentLanguage]['name'] ?? $app['translations']['en']['name'],
374
				'description' => $app['translations'][$currentLanguage]['description'] ?? $app['translations']['en']['description'],
375
				'summary' => $app['translations'][$currentLanguage]['summary'] ?? $app['translations']['en']['summary'],
376
				'license' => $app['releases'][0]['licenses'],
377
				'author' => $authors,
378
				'shipped' => false,
379
				'version' => $currentVersion,
380
				'default_enable' => '',
381
				'types' => [],
382
				'documentation' => [
383
					'admin' => $app['adminDocs'],
384
					'user' => $app['userDocs'],
385
					'developer' => $app['developerDocs']
386
				],
387
				'website' => $app['website'],
388
				'bugs' => $app['issueTracker'],
389
				'detailpage' => $app['website'],
390
				'dependencies' => array_merge(
391
					$nextCloudVersionDependencies,
392
					$phpDependencies
393
				),
394
				'level' => ($app['isFeatured'] === true) ? 200 : 100,
395
				'missingMaxOwnCloudVersion' => false,
396
				'missingMinOwnCloudVersion' => false,
397
				'canInstall' => true,
398
				'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '',
399
				'score' => $app['ratingOverall'],
400
				'ratingNumOverall' => $app['ratingNumOverall'],
401
				'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5,
402
				'removable' => $existsLocally,
403
				'active' => $this->appManager->isEnabledForUser($app['id']),
404
				'needsDownload' => !$existsLocally,
405
				'groups' => $groups,
406
				'fromAppStore' => true,
407
				'appstoreData' => $app,
408
			];
409
		}
410
411
		return $formattedApps;
412
	}
413
414
	/**
415
	 * @PasswordConfirmationRequired
416
	 *
417
	 * @param string $appId
418
	 * @param array $groups
419
	 * @return JSONResponse
420
	 */
421
	public function enableApp(string $appId, array $groups = []): JSONResponse {
422
		return $this->enableApps([$appId], $groups);
423
	}
424
425
	/**
426
	 * Enable one or more apps
427
	 *
428
	 * apps will be enabled for specific groups only if $groups is defined
429
	 *
430
	 * @PasswordConfirmationRequired
431
	 * @param array $appIds
432
	 * @param array $groups
433
	 * @return JSONResponse
434
	 */
435
	public function enableApps(array $appIds, array $groups = []): JSONResponse {
436
		try {
437
			$updateRequired = false;
438
439
			foreach ($appIds as $appId) {
440
				$appId = OC_App::cleanAppId($appId);
441
442
				// Check if app is already downloaded
443
				/** @var Installer $installer */
444
				$installer = \OC::$server->query(Installer::class);
445
				$isDownloaded = $installer->isDownloaded($appId);
446
447
				if (!$isDownloaded) {
448
					$installer->downloadApp($appId);
449
				}
450
451
				$installer->installApp($appId);
452
453
				if (count($groups) > 0) {
454
					$this->appManager->enableAppForGroups($appId, $this->getGroupList($groups));
455
				} else {
456
					$this->appManager->enableApp($appId);
457
				}
458
				if (\OC_App::shouldUpgrade($appId)) {
459
					$updateRequired = true;
460
				}
461
			}
462
			return new JSONResponse(['data' => ['update_required' => $updateRequired]]);
463
		} catch (\Exception $e) {
464
			$this->logger->error('could not enable apps', ['exception' => $e]);
465
			return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
466
		}
467
	}
468
469
	private function getGroupList(array $groups) {
470
		$groupManager = \OC::$server->getGroupManager();
471
		$groupsList = [];
472
		foreach ($groups as $group) {
473
			$groupItem = $groupManager->get($group);
474
			if ($groupItem instanceof \OCP\IGroup) {
475
				$groupsList[] = $groupManager->get($group);
476
			}
477
		}
478
		return $groupsList;
479
	}
480
481
	/**
482
	 * @PasswordConfirmationRequired
483
	 *
484
	 * @param string $appId
485
	 * @return JSONResponse
486
	 */
487
	public function disableApp(string $appId): JSONResponse {
488
		return $this->disableApps([$appId]);
489
	}
490
491
	/**
492
	 * @PasswordConfirmationRequired
493
	 *
494
	 * @param array $appIds
495
	 * @return JSONResponse
496
	 */
497
	public function disableApps(array $appIds): JSONResponse {
498
		try {
499
			foreach ($appIds as $appId) {
500
				$appId = OC_App::cleanAppId($appId);
501
				$this->appManager->disableApp($appId);
502
			}
503
			return new JSONResponse([]);
504
		} catch (\Exception $e) {
505
			$this->logger->error('could not disable app', ['exception' => $e]);
506
			return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
507
		}
508
	}
509
510
	/**
511
	 * @PasswordConfirmationRequired
512
	 *
513
	 * @param string $appId
514
	 * @return JSONResponse
515
	 */
516
	public function uninstallApp(string $appId): JSONResponse {
517
		$appId = OC_App::cleanAppId($appId);
518
		$result = $this->installer->removeApp($appId);
519
		if ($result !== false) {
520
			$this->appManager->clearAppsCache();
521
			return new JSONResponse(['data' => ['appid' => $appId]]);
522
		}
523
		return new JSONResponse(['data' => ['message' => $this->l10n->t('Could not remove app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
524
	}
525
526
	/**
527
	 * @param string $appId
528
	 * @return JSONResponse
529
	 */
530
	public function updateApp(string $appId): JSONResponse {
531
		$appId = OC_App::cleanAppId($appId);
532
533
		$this->config->setSystemValue('maintenance', true);
534
		try {
535
			$result = $this->installer->updateAppstoreApp($appId);
536
			$this->config->setSystemValue('maintenance', false);
537
		} catch (\Exception $ex) {
538
			$this->config->setSystemValue('maintenance', false);
539
			return new JSONResponse(['data' => ['message' => $ex->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
540
		}
541
542
		if ($result !== false) {
543
			return new JSONResponse(['data' => ['appid' => $appId]]);
544
		}
545
		return new JSONResponse(['data' => ['message' => $this->l10n->t('Could not update app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
546
	}
547
548
	private function sortApps($a, $b) {
549
		$a = (string)$a['name'];
550
		$b = (string)$b['name'];
551
		if ($a === $b) {
552
			return 0;
553
		}
554
		return ($a < $b) ? -1 : 1;
555
	}
556
557
	public function force(string $appId): JSONResponse {
558
		$appId = OC_App::cleanAppId($appId);
559
		$this->appManager->ignoreNextcloudRequirementForApp($appId);
0 ignored issues
show
Bug introduced by
The method ignoreNextcloudRequirementForApp() does not exist on OCP\App\IAppManager. Since it exists in all sub-types, consider adding an abstract or default implementation to OCP\App\IAppManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

559
		$this->appManager->/** @scrutinizer ignore-call */ 
560
                     ignoreNextcloudRequirementForApp($appId);
Loading history...
560
		return new JSONResponse();
561
	}
562
}
563