Completed
Push — master ( 55d0f3...b49f8e )
by Morris
23:03 queued 02:11
created

AppSettingsController::disableApp()   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 1
dl 0
loc 3
rs 10
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 OC\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
/**
57
 * @package OC\Settings\Controller
58
 */
59
class AppSettingsController extends Controller {
60
61
	/** @var \OCP\IL10N */
62
	private $l10n;
63
	/** @var IConfig */
64
	private $config;
65
	/** @var INavigationManager */
66
	private $navigationManager;
67
	/** @var IAppManager */
68
	private $appManager;
69
	/** @var CategoryFetcher */
70
	private $categoryFetcher;
71
	/** @var AppFetcher */
72
	private $appFetcher;
73
	/** @var IFactory */
74
	private $l10nFactory;
75
	/** @var BundleFetcher */
76
	private $bundleFetcher;
77
	/** @var Installer */
78
	private $installer;
79
	/** @var IURLGenerator */
80
	private $urlGenerator;
81
	/** @var ILogger */
82
	private $logger;
83
84
	/** @var array */
85
	private $allApps = [];
86
87
	/**
88
	 * @param string $appName
89
	 * @param IRequest $request
90
	 * @param IL10N $l10n
91
	 * @param IConfig $config
92
	 * @param INavigationManager $navigationManager
93
	 * @param IAppManager $appManager
94
	 * @param CategoryFetcher $categoryFetcher
95
	 * @param AppFetcher $appFetcher
96
	 * @param IFactory $l10nFactory
97
	 * @param BundleFetcher $bundleFetcher
98
	 * @param Installer $installer
99
	 * @param IURLGenerator $urlGenerator
100
	 * @param ILogger $logger
101
	 */
102 View Code Duplication
	public function __construct(string $appName,
103
								IRequest $request,
104
								IL10N $l10n,
105
								IConfig $config,
106
								INavigationManager $navigationManager,
107
								IAppManager $appManager,
108
								CategoryFetcher $categoryFetcher,
109
								AppFetcher $appFetcher,
110
								IFactory $l10nFactory,
111
								BundleFetcher $bundleFetcher,
112
								Installer $installer,
113
								IURLGenerator $urlGenerator,
114
								ILogger $logger) {
115
		parent::__construct($appName, $request);
116
		$this->l10n = $l10n;
117
		$this->config = $config;
118
		$this->navigationManager = $navigationManager;
119
		$this->appManager = $appManager;
120
		$this->categoryFetcher = $categoryFetcher;
121
		$this->appFetcher = $appFetcher;
122
		$this->l10nFactory = $l10nFactory;
123
		$this->bundleFetcher = $bundleFetcher;
124
		$this->installer = $installer;
125
		$this->urlGenerator = $urlGenerator;
126
		$this->logger = $logger;
127
	}
128
129
	/**
130
	 * @NoCSRFRequired
131
	 *
132
	 * @return TemplateResponse
133
	 */
134
	public function viewApps(): TemplateResponse {
135
		\OC_Util::addScript('settings', 'apps');
136
		\OC_Util::addVendorScript('core', 'marked/marked.min');
137
		$params = [];
138
		$params['appstoreEnabled'] = $this->config->getSystemValue('appstoreenabled', true) === true;
139
		$params['updateCount'] = count($this->getAppsWithUpdates());
140
		$params['developerDocumentation'] = $this->urlGenerator->linkToDocs('developer-manual');
141
		$params['bundles'] = $this->getBundles();
142
		$this->navigationManager->setActiveEntry('core_apps');
143
144
		$templateResponse = new TemplateResponse('settings', 'settings', ['serverData' => $params]);
145
		$policy = new ContentSecurityPolicy();
146
		$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
147
		$templateResponse->setContentSecurityPolicy($policy);
148
149
		return $templateResponse;
150
	}
151
152
	private function getAppsWithUpdates() {
153
		$appClass = new \OC_App();
154
		$apps = $appClass->listAllApps();
155
		foreach($apps as $key => $app) {
156
			$newVersion = $this->installer->isUpdateAvailable($app['id']);
157
			if($newVersion === false) {
158
				unset($apps[$key]);
159
			}
160
		}
161
		return $apps;
162
	}
163
164
	private function getBundles() {
165
		$result = [];
166
		$bundles = $this->bundleFetcher->getBundles();
167
		foreach ($bundles as $bundle) {
168
			$result[] = [
169
				'name' => $bundle->getName(),
170
				'id' => $bundle->getIdentifier(),
171
				'appIdentifiers' => $bundle->getAppIdentifiers()
172
			];
173
		}
174
		return $result;
175
176
	}
177
178
	/**
179
	 * Get all available categories
180
	 *
181
	 * @return JSONResponse
182
	 */
183
	public function listCategories(): JSONResponse {
184
		return new JSONResponse($this->getAllCategories());
185
	}
186
187
	private function getAllCategories() {
188
		$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
189
190
		$formattedCategories = [];
191
		$categories = $this->categoryFetcher->get();
192
		foreach($categories as $category) {
193
			$formattedCategories[] = [
194
				'id' => $category['id'],
195
				'ident' => $category['id'],
196
				'displayName' => isset($category['translations'][$currentLanguage]['name']) ? $category['translations'][$currentLanguage]['name'] : $category['translations']['en']['name'],
197
			];
198
		}
199
200
		return $formattedCategories;
201
	}
202
203
	private function fetchApps() {
204
		$appClass = new \OC_App();
205
		$apps = $appClass->listAllApps();
206
		foreach ($apps as $app) {
207
			$app['installed'] = true;
208
			$this->allApps[$app['id']] = $app;
209
		}
210
211
		$apps = $this->getAppsForCategory('');
212
		foreach ($apps as $app) {
213
			$app['appstore'] = true;
214
			if (!array_key_exists($app['id'], $this->allApps)) {
215
				$this->allApps[$app['id']] = $app;
216
			} else {
217
				$this->allApps[$app['id']] = array_merge($this->allApps[$app['id']], $app);
218
			}
219
		}
220
221
		// add bundle information
222
		$bundles = $this->bundleFetcher->getBundles();
223
		foreach($bundles as $bundle) {
224
			foreach($bundle->getAppIdentifiers() as $identifier) {
225
				foreach($this->allApps as &$app) {
226
					if($app['id'] === $identifier) {
227
						$app['bundleId'] = $bundle->getIdentifier();
228
						continue;
229
					}
230
				}
231
			}
232
		}
233
	}
234
235
	private function getAllApps() {
236
		return $this->allApps;
237
	}
238
	/**
239
	 * Get all available apps in a category
240
	 *
241
	 * @param string $category
0 ignored issues
show
Bug introduced by
There is no parameter named $category. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
242
	 * @return JSONResponse
243
	 * @throws \Exception
244
	 */
245
	public function listApps(): JSONResponse {
246
247
		$this->fetchApps();
248
		$apps = $this->getAllApps();
249
250
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n);
251
252
		// Extend existing app details
253
		$apps = array_map(function($appData) use ($dependencyAnalyzer) {
254
			$appstoreData = $appData['appstoreData'];
255
			$appData['screenshot'] = isset($appstoreData['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($appstoreData['screenshots'][0]['url']) : '';
256
257
			$newVersion = $this->installer->isUpdateAvailable($appData['id']);
258
			if($newVersion && $this->appManager->isInstalled($appData['id'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newVersion of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
259
				$appData['update'] = $newVersion;
260
			}
261
262
			// fix groups to be an array
263
			$groups = array();
264
			if (is_string($appData['groups'])) {
265
				$groups = json_decode($appData['groups']);
266
			}
267
			$appData['groups'] = $groups;
268
			$appData['canUnInstall'] = !$appData['active'] && $appData['removable'];
269
270
			// fix licence vs license
271
			if (isset($appData['license']) && !isset($appData['licence'])) {
272
				$appData['licence'] = $appData['license'];
273
			}
274
275
			// analyse dependencies
276
			$missing = $dependencyAnalyzer->analyze($appData);
277
			$appData['canInstall'] = empty($missing);
278
			$appData['missingDependencies'] = $missing;
279
280
			$appData['missingMinOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
281
			$appData['missingMaxOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
282
283
			return $appData;
284
		}, $apps);
285
286
		usort($apps, [$this, 'sortApps']);
287
288
		return new JSONResponse(['apps' => $apps, 'status' => 'success']);
289
	}
290
291
	/**
292
	 * Get all apps for a category from the app store
293
	 *
294
	 * @param string $requestedCategory
295
	 * @return array
296
	 * @throws \Exception
297
	 */
298
	private function getAppsForCategory($requestedCategory = ''): array {
299
		$versionParser = new VersionParser();
300
		$formattedApps = [];
301
		$apps = $this->appFetcher->get();
302
		foreach($apps as $app) {
303
			// Skip all apps not in the requested category
304
			if ($requestedCategory !== '') {
305
				$isInCategory = false;
306
				foreach($app['categories'] as $category) {
307
					if($category === $requestedCategory) {
308
						$isInCategory = true;
309
					}
310
				}
311
				if(!$isInCategory) {
312
					continue;
313
				}
314
			}
315
316
			$nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']);
317
			$nextCloudVersionDependencies = [];
318
			if($nextCloudVersion->getMinimumVersion() !== '') {
319
				$nextCloudVersionDependencies['nextcloud']['@attributes']['min-version'] = $nextCloudVersion->getMinimumVersion();
320
			}
321
			if($nextCloudVersion->getMaximumVersion() !== '') {
322
				$nextCloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion();
323
			}
324
			$phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']);
325
			$existsLocally = \OC_App::getAppPath($app['id']) !== false;
326
			$phpDependencies = [];
327
			if($phpVersion->getMinimumVersion() !== '') {
328
				$phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion();
329
			}
330
			if($phpVersion->getMaximumVersion() !== '') {
331
				$phpDependencies['php']['@attributes']['max-version'] = $phpVersion->getMaximumVersion();
332
			}
333
			if(isset($app['releases'][0]['minIntSize'])) {
334
				$phpDependencies['php']['@attributes']['min-int-size'] = $app['releases'][0]['minIntSize'];
335
			}
336
			$authors = '';
337
			foreach($app['authors'] as $key => $author) {
338
				$authors .= $author['name'];
339
				if($key !== count($app['authors']) - 1) {
340
					$authors .= ', ';
341
				}
342
			}
343
344
			$currentLanguage = substr(\OC::$server->getL10NFactory()->findLanguage(), 0, 2);
345
			$enabledValue = $this->config->getAppValue($app['id'], 'enabled', 'no');
346
			$groups = null;
347
			if($enabledValue !== 'no' && $enabledValue !== 'yes') {
348
				$groups = $enabledValue;
349
			}
350
351
			$currentVersion = '';
352
			if($this->appManager->isInstalled($app['id'])) {
353
				$currentVersion = $this->appManager->getAppVersion($app['id']);
354
			} else {
355
				$currentLanguage = $app['releases'][0]['version'];
356
			}
357
358
			$formattedApps[] = [
359
				'id' => $app['id'],
360
				'name' => isset($app['translations'][$currentLanguage]['name']) ? $app['translations'][$currentLanguage]['name'] : $app['translations']['en']['name'],
361
				'description' => isset($app['translations'][$currentLanguage]['description']) ? $app['translations'][$currentLanguage]['description'] : $app['translations']['en']['description'],
362
				'summary' => isset($app['translations'][$currentLanguage]['summary']) ? $app['translations'][$currentLanguage]['summary'] : $app['translations']['en']['summary'],
363
				'license' => $app['releases'][0]['licenses'],
364
				'author' => $authors,
365
				'shipped' => false,
366
				'version' => $currentVersion,
367
				'default_enable' => '',
368
				'types' => [],
369
				'documentation' => [
370
					'admin' => $app['adminDocs'],
371
					'user' => $app['userDocs'],
372
					'developer' => $app['developerDocs']
373
				],
374
				'website' => $app['website'],
375
				'bugs' => $app['issueTracker'],
376
				'detailpage' => $app['website'],
377
				'dependencies' => array_merge(
378
					$nextCloudVersionDependencies,
379
					$phpDependencies
380
				),
381
				'level' => ($app['isFeatured'] === true) ? 200 : 100,
382
				'missingMaxOwnCloudVersion' => false,
383
				'missingMinOwnCloudVersion' => false,
384
				'canInstall' => true,
385
				'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '',
386
				'score' => $app['ratingOverall'],
387
				'ratingNumOverall' => $app['ratingNumOverall'],
388
				'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5,
389
				'removable' => $existsLocally,
390
				'active' => $this->appManager->isEnabledForUser($app['id']),
391
				'needsDownload' => !$existsLocally,
392
				'groups' => $groups,
393
				'fromAppStore' => true,
394
				'appstoreData' => $app,
395
			];
396
		}
397
398
		return $formattedApps;
399
	}
400
401
	/**
402
	 * @PasswordConfirmationRequired
403
	 *
404
	 * @param string $appId
405
	 * @param array $groups
406
	 * @return JSONResponse
407
	 */
408
	public function enableApp(string $appId, array $groups = []): JSONResponse {
409
		return $this->enableApps([$appId], $groups);
410
	}
411
412
	/**
413
	 * Enable one or more apps
414
	 *
415
	 * apps will be enabled for specific groups only if $groups is defined
416
	 *
417
	 * @PasswordConfirmationRequired
418
	 * @param array $appIds
419
	 * @param array $groups
420
	 * @return JSONResponse
421
	 */
422
	public function enableApps(array $appIds, array $groups = []): JSONResponse {
423
		try {
424
			$updateRequired = false;
425
426
			foreach ($appIds as $appId) {
427
				$appId = OC_App::cleanAppId($appId);
428
429
				// Check if app is already downloaded
430
				/** @var Installer $installer */
431
				$installer = \OC::$server->query(Installer::class);
432
				$isDownloaded = $installer->isDownloaded($appId);
433
434
				if(!$isDownloaded) {
435
					$installer->downloadApp($appId);
436
				}
437
438
				$installer->installApp($appId);
439
440
				if (count($groups) > 0) {
441
					$this->appManager->enableAppForGroups($appId, $this->getGroupList($groups));
442
				} else {
443
					$this->appManager->enableApp($appId);
444
				}
445
				if (\OC_App::shouldUpgrade($appId)) {
446
					$updateRequired = true;
447
				}
448
			}
449
			return new JSONResponse(['data' => ['update_required' => $updateRequired]]);
450
451
		} catch (\Exception $e) {
452
			$this->logger->logException($e);
0 ignored issues
show
Documentation introduced by
$e is of type object<Exception>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
453
			return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
454
		}
455
	}
456
457
	private function getGroupList(array $groups) {
458
		$groupManager = \OC::$server->getGroupManager();
459
		$groupsList = [];
460 View Code Duplication
		foreach ($groups as $group) {
461
			$groupItem = $groupManager->get($group);
462
			if ($groupItem instanceof \OCP\IGroup) {
463
				$groupsList[] = $groupManager->get($group);
464
			}
465
		}
466
		return $groupsList;
467
	}
468
469
	/**
470
	 * @PasswordConfirmationRequired
471
	 *
472
	 * @param string $appId
473
	 * @return JSONResponse
474
	 */
475
	public function disableApp(string $appId): JSONResponse {
476
		return $this->disableApps([$appId]);
477
	}
478
479
	/**
480
	 * @PasswordConfirmationRequired
481
	 *
482
	 * @param array $appIds
483
	 * @return JSONResponse
484
	 */
485
	public function disableApps(array $appIds): JSONResponse {
486
		try {
487
			foreach ($appIds as $appId) {
488
				$appId = OC_App::cleanAppId($appId);
489
				$this->appManager->disableApp($appId);
490
			}
491
			return new JSONResponse([]);
492
		} catch (\Exception $e) {
493
			$this->logger->logException($e);
0 ignored issues
show
Documentation introduced by
$e is of type object<Exception>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
494
			return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
495
		}
496
	}
497
498
	/**
499
	 * @PasswordConfirmationRequired
500
	 *
501
	 * @param string $appId
502
	 * @return JSONResponse
503
	 */
504
	public function uninstallApp(string $appId): JSONResponse {
505
		$appId = OC_App::cleanAppId($appId);
506
		$result = $this->installer->removeApp($appId);
507
		if($result !== false) {
508
			$this->appManager->clearAppsCache();
509
			return new JSONResponse(['data' => ['appid' => $appId]]);
510
		}
511
		return new JSONResponse(['data' => ['message' => $this->l10n->t('Couldn\'t remove app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
512
	}
513
514
	/**
515
	 * @param string $appId
516
	 * @return JSONResponse
517
	 */
518
	public function updateApp(string $appId): JSONResponse {
519
		$appId = OC_App::cleanAppId($appId);
520
521
		$this->config->setSystemValue('maintenance', true);
522
		try {
523
			$result = $this->installer->updateAppstoreApp($appId);
524
			$this->config->setSystemValue('maintenance', false);
525
		} catch (\Exception $ex) {
526
			$this->config->setSystemValue('maintenance', false);
527
			return new JSONResponse(['data' => ['message' => $ex->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
528
		}
529
530
		if ($result !== false) {
531
			return new JSONResponse(['data' => ['appid' => $appId]]);
532
		}
533
		return new JSONResponse(['data' => ['message' => $this->l10n->t('Couldn\'t update app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
534
	}
535
536
	private function sortApps($a, $b) {
537
		$a = (string)$a['name'];
538
		$b = (string)$b['name'];
539
		if ($a === $b) {
540
			return 0;
541
		}
542
		return ($a < $b) ? -1 : 1;
543
	}
544
545
}
546