Passed
Push — master ( b72d27...03dc79 )
by Roeland
21:59 queued 11:25
created

AppSettingsController::force()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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