Passed
Push — master ( 8f3a07...e1a300 )
by Lukas
13:39 queued 11s
created
lib/private/Installer.php 2 patches
Indentation   +589 added lines, -589 removed lines patch added patch discarded remove patch
@@ -59,593 +59,593 @@
 block discarded – undo
59 59
  * This class provides the functionality needed to install, update and remove apps
60 60
  */
61 61
 class Installer {
62
-	/** @var AppFetcher */
63
-	private $appFetcher;
64
-	/** @var IClientService */
65
-	private $clientService;
66
-	/** @var ITempManager */
67
-	private $tempManager;
68
-	/** @var ILogger */
69
-	private $logger;
70
-	/** @var IConfig */
71
-	private $config;
72
-	/** @var array - for caching the result of app fetcher */
73
-	private $apps = null;
74
-	/** @var bool|null - for caching the result of the ready status */
75
-	private $isInstanceReadyForUpdates = null;
76
-	/** @var bool */
77
-	private $isCLI;
78
-
79
-	/**
80
-	 * @param AppFetcher $appFetcher
81
-	 * @param IClientService $clientService
82
-	 * @param ITempManager $tempManager
83
-	 * @param ILogger $logger
84
-	 * @param IConfig $config
85
-	 */
86
-	public function __construct(
87
-		AppFetcher $appFetcher,
88
-		IClientService $clientService,
89
-		ITempManager $tempManager,
90
-		ILogger $logger,
91
-		IConfig $config,
92
-		bool $isCLI
93
-	) {
94
-		$this->appFetcher = $appFetcher;
95
-		$this->clientService = $clientService;
96
-		$this->tempManager = $tempManager;
97
-		$this->logger = $logger;
98
-		$this->config = $config;
99
-		$this->isCLI = $isCLI;
100
-	}
101
-
102
-	/**
103
-	 * Installs an app that is located in one of the app folders already
104
-	 *
105
-	 * @param string $appId App to install
106
-	 * @param bool $forceEnable
107
-	 * @throws \Exception
108
-	 * @return string app ID
109
-	 */
110
-	public function installApp(string $appId, bool $forceEnable = false): string {
111
-		$app = \OC_App::findAppInDirectories($appId);
112
-		if ($app === false) {
113
-			throw new \Exception('App not found in any app directory');
114
-		}
115
-
116
-		$basedir = $app['path'].'/'.$appId;
117
-
118
-		if (is_file($basedir . '/appinfo/database.xml')) {
119
-			throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
120
-		}
121
-
122
-		$info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
123
-
124
-		$l = \OC::$server->getL10N('core');
125
-
126
-		if (!is_array($info)) {
127
-			throw new \Exception(
128
-				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
129
-					[$appId]
130
-				)
131
-			);
132
-		}
133
-
134
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
135
-		$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
136
-
137
-		$version = implode('.', \OCP\Util::getVersion());
138
-		if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
139
-			throw new \Exception(
140
-				// TODO $l
141
-				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
142
-					[$info['name']]
143
-				)
144
-			);
145
-		}
146
-
147
-		// check for required dependencies
148
-		\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
149
-		/** @var Coordinator $coordinator */
150
-		$coordinator = \OC::$server->get(Coordinator::class);
151
-		$coordinator->runLazyRegistration($appId);
152
-		\OC_App::registerAutoloading($appId, $basedir);
153
-
154
-		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
155
-		if ($previousVersion) {
156
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
157
-		}
158
-
159
-		//install the database
160
-		$ms = new MigrationService($info['id'], \OC::$server->get(Connection::class));
161
-		$ms->migrate('latest', true);
162
-
163
-		if ($previousVersion) {
164
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
165
-		}
166
-
167
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
168
-
169
-		//run appinfo/install.php
170
-		self::includeAppScript($basedir . '/appinfo/install.php');
171
-
172
-		$appData = OC_App::getAppInfo($appId);
173
-		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
174
-
175
-		//set the installed version
176
-		\OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
177
-		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
178
-
179
-		//set remote/public handlers
180
-		foreach ($info['remote'] as $name => $path) {
181
-			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
182
-		}
183
-		foreach ($info['public'] as $name => $path) {
184
-			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
185
-		}
186
-
187
-		OC_App::setAppTypes($info['id']);
188
-
189
-		return $info['id'];
190
-	}
191
-
192
-	/**
193
-	 * Updates the specified app from the appstore
194
-	 *
195
-	 * @param string $appId
196
-	 * @param bool [$allowUnstable] Allow unstable releases
197
-	 * @return bool
198
-	 */
199
-	public function updateAppstoreApp($appId, $allowUnstable = false) {
200
-		if ($this->isUpdateAvailable($appId, $allowUnstable)) {
201
-			try {
202
-				$this->downloadApp($appId, $allowUnstable);
203
-			} catch (\Exception $e) {
204
-				$this->logger->logException($e, [
205
-					'level' => ILogger::ERROR,
206
-					'app' => 'core',
207
-				]);
208
-				return false;
209
-			}
210
-			return OC_App::updateApp($appId);
211
-		}
212
-
213
-		return false;
214
-	}
215
-
216
-	/**
217
-	 * Split the certificate file in individual certs
218
-	 *
219
-	 * @param string $cert
220
-	 * @return string[]
221
-	 */
222
-	private function splitCerts(string $cert): array {
223
-		preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
224
-
225
-		return $matches[0];
226
-	}
227
-
228
-	/**
229
-	 * Downloads an app and puts it into the app directory
230
-	 *
231
-	 * @param string $appId
232
-	 * @param bool [$allowUnstable]
233
-	 *
234
-	 * @throws \Exception If the installation was not successful
235
-	 */
236
-	public function downloadApp($appId, $allowUnstable = false) {
237
-		$appId = strtolower($appId);
238
-
239
-		$apps = $this->appFetcher->get($allowUnstable);
240
-		foreach ($apps as $app) {
241
-			if ($app['id'] === $appId) {
242
-				// Load the certificate
243
-				$certificate = new X509();
244
-				$rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
245
-				$rootCrts = $this->splitCerts($rootCrt);
246
-				foreach ($rootCrts as $rootCrt) {
247
-					$certificate->loadCA($rootCrt);
248
-				}
249
-				$loadedCertificate = $certificate->loadX509($app['certificate']);
250
-
251
-				// Verify if the certificate has been revoked
252
-				$crl = new X509();
253
-				foreach ($rootCrts as $rootCrt) {
254
-					$crl->loadCA($rootCrt);
255
-				}
256
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
257
-				if ($crl->validateSignature() !== true) {
258
-					throw new \Exception('Could not validate CRL signature');
259
-				}
260
-				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
261
-				$revoked = $crl->getRevoked($csn);
262
-				if ($revoked !== false) {
263
-					throw new \Exception(
264
-						sprintf(
265
-							'Certificate "%s" has been revoked',
266
-							$csn
267
-						)
268
-					);
269
-				}
270
-
271
-				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
272
-				if ($certificate->validateSignature() !== true) {
273
-					throw new \Exception(
274
-						sprintf(
275
-							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
276
-							$appId
277
-						)
278
-					);
279
-				}
280
-
281
-				// Verify if the certificate is issued for the requested app id
282
-				$certInfo = openssl_x509_parse($app['certificate']);
283
-				if (!isset($certInfo['subject']['CN'])) {
284
-					throw new \Exception(
285
-						sprintf(
286
-							'App with id %s has a cert with no CN',
287
-							$appId
288
-						)
289
-					);
290
-				}
291
-				if ($certInfo['subject']['CN'] !== $appId) {
292
-					throw new \Exception(
293
-						sprintf(
294
-							'App with id %s has a cert issued to %s',
295
-							$appId,
296
-							$certInfo['subject']['CN']
297
-						)
298
-					);
299
-				}
300
-
301
-				// Download the release
302
-				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
303
-				$timeout = $this->isCLI ? 0 : 120;
304
-				$client = $this->clientService->newClient();
305
-				$client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
306
-
307
-				// Check if the signature actually matches the downloaded content
308
-				$certificate = openssl_get_publickey($app['certificate']);
309
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
310
-				// PHP 8+ deprecates openssl_free_key and automatically destroys the key instance when it goes out of scope
311
-				if ((PHP_VERSION_ID < 80000)) {
312
-					openssl_free_key($certificate);
313
-				}
314
-
315
-				if ($verified === true) {
316
-					// Seems to match, let's proceed
317
-					$extractDir = $this->tempManager->getTemporaryFolder();
318
-					$archive = new TAR($tempFile);
319
-
320
-					if ($archive) {
321
-						if (!$archive->extract($extractDir)) {
322
-							$errorMessage = 'Could not extract app ' . $appId;
323
-
324
-							$archiveError = $archive->getError();
325
-							if ($archiveError instanceof \PEAR_Error) {
326
-								$errorMessage .= ': ' . $archiveError->getMessage();
327
-							}
328
-
329
-							throw new \Exception($errorMessage);
330
-						}
331
-						$allFiles = scandir($extractDir);
332
-						$folders = array_diff($allFiles, ['.', '..']);
333
-						$folders = array_values($folders);
334
-
335
-						if (count($folders) > 1) {
336
-							throw new \Exception(
337
-								sprintf(
338
-									'Extracted app %s has more than 1 folder',
339
-									$appId
340
-								)
341
-							);
342
-						}
343
-
344
-						// Check if appinfo/info.xml has the same app ID as well
345
-						if ((PHP_VERSION_ID < 80000)) {
346
-							$loadEntities = libxml_disable_entity_loader(false);
347
-							$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
348
-							libxml_disable_entity_loader($loadEntities);
349
-						} else {
350
-							$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
351
-						}
352
-						if ((string)$xml->id !== $appId) {
353
-							throw new \Exception(
354
-								sprintf(
355
-									'App for id %s has a wrong app ID in info.xml: %s',
356
-									$appId,
357
-									(string)$xml->id
358
-								)
359
-							);
360
-						}
361
-
362
-						// Check if the version is lower than before
363
-						$currentVersion = OC_App::getAppVersion($appId);
364
-						$newVersion = (string)$xml->version;
365
-						if (version_compare($currentVersion, $newVersion) === 1) {
366
-							throw new \Exception(
367
-								sprintf(
368
-									'App for id %s has version %s and tried to update to lower version %s',
369
-									$appId,
370
-									$currentVersion,
371
-									$newVersion
372
-								)
373
-							);
374
-						}
375
-
376
-						$baseDir = OC_App::getInstallPath() . '/' . $appId;
377
-						// Remove old app with the ID if existent
378
-						OC_Helper::rmdirr($baseDir);
379
-						// Move to app folder
380
-						if (@mkdir($baseDir)) {
381
-							$extractDir .= '/' . $folders[0];
382
-							OC_Helper::copyr($extractDir, $baseDir);
383
-						}
384
-						OC_Helper::copyr($extractDir, $baseDir);
385
-						OC_Helper::rmdirr($extractDir);
386
-						return;
387
-					} else {
388
-						throw new \Exception(
389
-							sprintf(
390
-								'Could not extract app with ID %s to %s',
391
-								$appId,
392
-								$extractDir
393
-							)
394
-						);
395
-					}
396
-				} else {
397
-					// Signature does not match
398
-					throw new \Exception(
399
-						sprintf(
400
-							'App with id %s has invalid signature',
401
-							$appId
402
-						)
403
-					);
404
-				}
405
-			}
406
-		}
407
-
408
-		throw new \Exception(
409
-			sprintf(
410
-				'Could not download app %s',
411
-				$appId
412
-			)
413
-		);
414
-	}
415
-
416
-	/**
417
-	 * Check if an update for the app is available
418
-	 *
419
-	 * @param string $appId
420
-	 * @param bool $allowUnstable
421
-	 * @return string|false false or the version number of the update
422
-	 */
423
-	public function isUpdateAvailable($appId, $allowUnstable = false) {
424
-		if ($this->isInstanceReadyForUpdates === null) {
425
-			$installPath = OC_App::getInstallPath();
426
-			if ($installPath === false || $installPath === null) {
427
-				$this->isInstanceReadyForUpdates = false;
428
-			} else {
429
-				$this->isInstanceReadyForUpdates = true;
430
-			}
431
-		}
432
-
433
-		if ($this->isInstanceReadyForUpdates === false) {
434
-			return false;
435
-		}
436
-
437
-		if ($this->isInstalledFromGit($appId) === true) {
438
-			return false;
439
-		}
440
-
441
-		if ($this->apps === null) {
442
-			$this->apps = $this->appFetcher->get($allowUnstable);
443
-		}
444
-
445
-		foreach ($this->apps as $app) {
446
-			if ($app['id'] === $appId) {
447
-				$currentVersion = OC_App::getAppVersion($appId);
448
-
449
-				if (!isset($app['releases'][0]['version'])) {
450
-					return false;
451
-				}
452
-				$newestVersion = $app['releases'][0]['version'];
453
-				if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
454
-					return $newestVersion;
455
-				} else {
456
-					return false;
457
-				}
458
-			}
459
-		}
460
-
461
-		return false;
462
-	}
463
-
464
-	/**
465
-	 * Check if app has been installed from git
466
-	 * @param string $name name of the application to remove
467
-	 * @return boolean
468
-	 *
469
-	 * The function will check if the path contains a .git folder
470
-	 */
471
-	private function isInstalledFromGit($appId) {
472
-		$app = \OC_App::findAppInDirectories($appId);
473
-		if ($app === false) {
474
-			return false;
475
-		}
476
-		$basedir = $app['path'].'/'.$appId;
477
-		return file_exists($basedir.'/.git/');
478
-	}
479
-
480
-	/**
481
-	 * Check if app is already downloaded
482
-	 * @param string $name name of the application to remove
483
-	 * @return boolean
484
-	 *
485
-	 * The function will check if the app is already downloaded in the apps repository
486
-	 */
487
-	public function isDownloaded($name) {
488
-		foreach (\OC::$APPSROOTS as $dir) {
489
-			$dirToTest = $dir['path'];
490
-			$dirToTest .= '/';
491
-			$dirToTest .= $name;
492
-			$dirToTest .= '/';
493
-
494
-			if (is_dir($dirToTest)) {
495
-				return true;
496
-			}
497
-		}
498
-
499
-		return false;
500
-	}
501
-
502
-	/**
503
-	 * Removes an app
504
-	 * @param string $appId ID of the application to remove
505
-	 * @return boolean
506
-	 *
507
-	 *
508
-	 * This function works as follows
509
-	 *   -# call uninstall repair steps
510
-	 *   -# removing the files
511
-	 *
512
-	 * The function will not delete preferences, tables and the configuration,
513
-	 * this has to be done by the function oc_app_uninstall().
514
-	 */
515
-	public function removeApp($appId) {
516
-		if ($this->isDownloaded($appId)) {
517
-			if (\OC::$server->getAppManager()->isShipped($appId)) {
518
-				return false;
519
-			}
520
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
521
-			OC_Helper::rmdirr($appDir);
522
-			return true;
523
-		} else {
524
-			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
525
-
526
-			return false;
527
-		}
528
-	}
529
-
530
-	/**
531
-	 * Installs the app within the bundle and marks the bundle as installed
532
-	 *
533
-	 * @param Bundle $bundle
534
-	 * @throws \Exception If app could not get installed
535
-	 */
536
-	public function installAppBundle(Bundle $bundle) {
537
-		$appIds = $bundle->getAppIdentifiers();
538
-		foreach ($appIds as $appId) {
539
-			if (!$this->isDownloaded($appId)) {
540
-				$this->downloadApp($appId);
541
-			}
542
-			$this->installApp($appId);
543
-			$app = new OC_App();
544
-			$app->enable($appId);
545
-		}
546
-		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
547
-		$bundles[] = $bundle->getIdentifier();
548
-		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
549
-	}
550
-
551
-	/**
552
-	 * Installs shipped apps
553
-	 *
554
-	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
555
-	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
556
-	 *                         working ownCloud at the end instead of an aborted update.
557
-	 * @return array Array of error messages (appid => Exception)
558
-	 */
559
-	public static function installShippedApps($softErrors = false) {
560
-		$appManager = \OC::$server->getAppManager();
561
-		$config = \OC::$server->getConfig();
562
-		$errors = [];
563
-		foreach (\OC::$APPSROOTS as $app_dir) {
564
-			if ($dir = opendir($app_dir['path'])) {
565
-				while (false !== ($filename = readdir($dir))) {
566
-					if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
567
-						if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
568
-							if ($config->getAppValue($filename, "installed_version", null) === null) {
569
-								$info = OC_App::getAppInfo($filename);
570
-								$enabled = isset($info['default_enable']);
571
-								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
572
-									  && $config->getAppValue($filename, 'enabled') !== 'no') {
573
-									if ($softErrors) {
574
-										try {
575
-											Installer::installShippedApp($filename);
576
-										} catch (HintException $e) {
577
-											if ($e->getPrevious() instanceof TableExistsException) {
578
-												$errors[$filename] = $e;
579
-												continue;
580
-											}
581
-											throw $e;
582
-										}
583
-									} else {
584
-										Installer::installShippedApp($filename);
585
-									}
586
-									$config->setAppValue($filename, 'enabled', 'yes');
587
-								}
588
-							}
589
-						}
590
-					}
591
-				}
592
-				closedir($dir);
593
-			}
594
-		}
595
-
596
-		return $errors;
597
-	}
598
-
599
-	/**
600
-	 * install an app already placed in the app folder
601
-	 * @param string $app id of the app to install
602
-	 * @return integer
603
-	 */
604
-	public static function installShippedApp($app) {
605
-		//install the database
606
-		$appPath = OC_App::getAppPath($app);
607
-		\OC_App::registerAutoloading($app, $appPath);
608
-
609
-		$ms = new MigrationService($app, \OC::$server->get(Connection::class));
610
-		$ms->migrate('latest', true);
611
-
612
-		//run appinfo/install.php
613
-		self::includeAppScript("$appPath/appinfo/install.php");
614
-
615
-		$info = OC_App::getAppInfo($app);
616
-		if (is_null($info)) {
617
-			return false;
618
-		}
619
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
620
-
621
-		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
622
-
623
-		$config = \OC::$server->getConfig();
624
-
625
-		$config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
626
-		if (array_key_exists('ocsid', $info)) {
627
-			$config->setAppValue($app, 'ocsid', $info['ocsid']);
628
-		}
629
-
630
-		//set remote/public handlers
631
-		foreach ($info['remote'] as $name => $path) {
632
-			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
633
-		}
634
-		foreach ($info['public'] as $name => $path) {
635
-			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
636
-		}
637
-
638
-		OC_App::setAppTypes($info['id']);
639
-
640
-		return $info['id'];
641
-	}
642
-
643
-	/**
644
-	 * @param string $script
645
-	 */
646
-	private static function includeAppScript($script) {
647
-		if (file_exists($script)) {
648
-			include $script;
649
-		}
650
-	}
62
+    /** @var AppFetcher */
63
+    private $appFetcher;
64
+    /** @var IClientService */
65
+    private $clientService;
66
+    /** @var ITempManager */
67
+    private $tempManager;
68
+    /** @var ILogger */
69
+    private $logger;
70
+    /** @var IConfig */
71
+    private $config;
72
+    /** @var array - for caching the result of app fetcher */
73
+    private $apps = null;
74
+    /** @var bool|null - for caching the result of the ready status */
75
+    private $isInstanceReadyForUpdates = null;
76
+    /** @var bool */
77
+    private $isCLI;
78
+
79
+    /**
80
+     * @param AppFetcher $appFetcher
81
+     * @param IClientService $clientService
82
+     * @param ITempManager $tempManager
83
+     * @param ILogger $logger
84
+     * @param IConfig $config
85
+     */
86
+    public function __construct(
87
+        AppFetcher $appFetcher,
88
+        IClientService $clientService,
89
+        ITempManager $tempManager,
90
+        ILogger $logger,
91
+        IConfig $config,
92
+        bool $isCLI
93
+    ) {
94
+        $this->appFetcher = $appFetcher;
95
+        $this->clientService = $clientService;
96
+        $this->tempManager = $tempManager;
97
+        $this->logger = $logger;
98
+        $this->config = $config;
99
+        $this->isCLI = $isCLI;
100
+    }
101
+
102
+    /**
103
+     * Installs an app that is located in one of the app folders already
104
+     *
105
+     * @param string $appId App to install
106
+     * @param bool $forceEnable
107
+     * @throws \Exception
108
+     * @return string app ID
109
+     */
110
+    public function installApp(string $appId, bool $forceEnable = false): string {
111
+        $app = \OC_App::findAppInDirectories($appId);
112
+        if ($app === false) {
113
+            throw new \Exception('App not found in any app directory');
114
+        }
115
+
116
+        $basedir = $app['path'].'/'.$appId;
117
+
118
+        if (is_file($basedir . '/appinfo/database.xml')) {
119
+            throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
120
+        }
121
+
122
+        $info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
123
+
124
+        $l = \OC::$server->getL10N('core');
125
+
126
+        if (!is_array($info)) {
127
+            throw new \Exception(
128
+                $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
129
+                    [$appId]
130
+                )
131
+            );
132
+        }
133
+
134
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
135
+        $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
136
+
137
+        $version = implode('.', \OCP\Util::getVersion());
138
+        if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
139
+            throw new \Exception(
140
+                // TODO $l
141
+                $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
142
+                    [$info['name']]
143
+                )
144
+            );
145
+        }
146
+
147
+        // check for required dependencies
148
+        \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
149
+        /** @var Coordinator $coordinator */
150
+        $coordinator = \OC::$server->get(Coordinator::class);
151
+        $coordinator->runLazyRegistration($appId);
152
+        \OC_App::registerAutoloading($appId, $basedir);
153
+
154
+        $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
155
+        if ($previousVersion) {
156
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
157
+        }
158
+
159
+        //install the database
160
+        $ms = new MigrationService($info['id'], \OC::$server->get(Connection::class));
161
+        $ms->migrate('latest', true);
162
+
163
+        if ($previousVersion) {
164
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
165
+        }
166
+
167
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
168
+
169
+        //run appinfo/install.php
170
+        self::includeAppScript($basedir . '/appinfo/install.php');
171
+
172
+        $appData = OC_App::getAppInfo($appId);
173
+        OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
174
+
175
+        //set the installed version
176
+        \OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
177
+        \OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
178
+
179
+        //set remote/public handlers
180
+        foreach ($info['remote'] as $name => $path) {
181
+            \OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
182
+        }
183
+        foreach ($info['public'] as $name => $path) {
184
+            \OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
185
+        }
186
+
187
+        OC_App::setAppTypes($info['id']);
188
+
189
+        return $info['id'];
190
+    }
191
+
192
+    /**
193
+     * Updates the specified app from the appstore
194
+     *
195
+     * @param string $appId
196
+     * @param bool [$allowUnstable] Allow unstable releases
197
+     * @return bool
198
+     */
199
+    public function updateAppstoreApp($appId, $allowUnstable = false) {
200
+        if ($this->isUpdateAvailable($appId, $allowUnstable)) {
201
+            try {
202
+                $this->downloadApp($appId, $allowUnstable);
203
+            } catch (\Exception $e) {
204
+                $this->logger->logException($e, [
205
+                    'level' => ILogger::ERROR,
206
+                    'app' => 'core',
207
+                ]);
208
+                return false;
209
+            }
210
+            return OC_App::updateApp($appId);
211
+        }
212
+
213
+        return false;
214
+    }
215
+
216
+    /**
217
+     * Split the certificate file in individual certs
218
+     *
219
+     * @param string $cert
220
+     * @return string[]
221
+     */
222
+    private function splitCerts(string $cert): array {
223
+        preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
224
+
225
+        return $matches[0];
226
+    }
227
+
228
+    /**
229
+     * Downloads an app and puts it into the app directory
230
+     *
231
+     * @param string $appId
232
+     * @param bool [$allowUnstable]
233
+     *
234
+     * @throws \Exception If the installation was not successful
235
+     */
236
+    public function downloadApp($appId, $allowUnstable = false) {
237
+        $appId = strtolower($appId);
238
+
239
+        $apps = $this->appFetcher->get($allowUnstable);
240
+        foreach ($apps as $app) {
241
+            if ($app['id'] === $appId) {
242
+                // Load the certificate
243
+                $certificate = new X509();
244
+                $rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
245
+                $rootCrts = $this->splitCerts($rootCrt);
246
+                foreach ($rootCrts as $rootCrt) {
247
+                    $certificate->loadCA($rootCrt);
248
+                }
249
+                $loadedCertificate = $certificate->loadX509($app['certificate']);
250
+
251
+                // Verify if the certificate has been revoked
252
+                $crl = new X509();
253
+                foreach ($rootCrts as $rootCrt) {
254
+                    $crl->loadCA($rootCrt);
255
+                }
256
+                $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
257
+                if ($crl->validateSignature() !== true) {
258
+                    throw new \Exception('Could not validate CRL signature');
259
+                }
260
+                $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
261
+                $revoked = $crl->getRevoked($csn);
262
+                if ($revoked !== false) {
263
+                    throw new \Exception(
264
+                        sprintf(
265
+                            'Certificate "%s" has been revoked',
266
+                            $csn
267
+                        )
268
+                    );
269
+                }
270
+
271
+                // Verify if the certificate has been issued by the Nextcloud Code Authority CA
272
+                if ($certificate->validateSignature() !== true) {
273
+                    throw new \Exception(
274
+                        sprintf(
275
+                            'App with id %s has a certificate not issued by a trusted Code Signing Authority',
276
+                            $appId
277
+                        )
278
+                    );
279
+                }
280
+
281
+                // Verify if the certificate is issued for the requested app id
282
+                $certInfo = openssl_x509_parse($app['certificate']);
283
+                if (!isset($certInfo['subject']['CN'])) {
284
+                    throw new \Exception(
285
+                        sprintf(
286
+                            'App with id %s has a cert with no CN',
287
+                            $appId
288
+                        )
289
+                    );
290
+                }
291
+                if ($certInfo['subject']['CN'] !== $appId) {
292
+                    throw new \Exception(
293
+                        sprintf(
294
+                            'App with id %s has a cert issued to %s',
295
+                            $appId,
296
+                            $certInfo['subject']['CN']
297
+                        )
298
+                    );
299
+                }
300
+
301
+                // Download the release
302
+                $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
303
+                $timeout = $this->isCLI ? 0 : 120;
304
+                $client = $this->clientService->newClient();
305
+                $client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
306
+
307
+                // Check if the signature actually matches the downloaded content
308
+                $certificate = openssl_get_publickey($app['certificate']);
309
+                $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
310
+                // PHP 8+ deprecates openssl_free_key and automatically destroys the key instance when it goes out of scope
311
+                if ((PHP_VERSION_ID < 80000)) {
312
+                    openssl_free_key($certificate);
313
+                }
314
+
315
+                if ($verified === true) {
316
+                    // Seems to match, let's proceed
317
+                    $extractDir = $this->tempManager->getTemporaryFolder();
318
+                    $archive = new TAR($tempFile);
319
+
320
+                    if ($archive) {
321
+                        if (!$archive->extract($extractDir)) {
322
+                            $errorMessage = 'Could not extract app ' . $appId;
323
+
324
+                            $archiveError = $archive->getError();
325
+                            if ($archiveError instanceof \PEAR_Error) {
326
+                                $errorMessage .= ': ' . $archiveError->getMessage();
327
+                            }
328
+
329
+                            throw new \Exception($errorMessage);
330
+                        }
331
+                        $allFiles = scandir($extractDir);
332
+                        $folders = array_diff($allFiles, ['.', '..']);
333
+                        $folders = array_values($folders);
334
+
335
+                        if (count($folders) > 1) {
336
+                            throw new \Exception(
337
+                                sprintf(
338
+                                    'Extracted app %s has more than 1 folder',
339
+                                    $appId
340
+                                )
341
+                            );
342
+                        }
343
+
344
+                        // Check if appinfo/info.xml has the same app ID as well
345
+                        if ((PHP_VERSION_ID < 80000)) {
346
+                            $loadEntities = libxml_disable_entity_loader(false);
347
+                            $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
348
+                            libxml_disable_entity_loader($loadEntities);
349
+                        } else {
350
+                            $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
351
+                        }
352
+                        if ((string)$xml->id !== $appId) {
353
+                            throw new \Exception(
354
+                                sprintf(
355
+                                    'App for id %s has a wrong app ID in info.xml: %s',
356
+                                    $appId,
357
+                                    (string)$xml->id
358
+                                )
359
+                            );
360
+                        }
361
+
362
+                        // Check if the version is lower than before
363
+                        $currentVersion = OC_App::getAppVersion($appId);
364
+                        $newVersion = (string)$xml->version;
365
+                        if (version_compare($currentVersion, $newVersion) === 1) {
366
+                            throw new \Exception(
367
+                                sprintf(
368
+                                    'App for id %s has version %s and tried to update to lower version %s',
369
+                                    $appId,
370
+                                    $currentVersion,
371
+                                    $newVersion
372
+                                )
373
+                            );
374
+                        }
375
+
376
+                        $baseDir = OC_App::getInstallPath() . '/' . $appId;
377
+                        // Remove old app with the ID if existent
378
+                        OC_Helper::rmdirr($baseDir);
379
+                        // Move to app folder
380
+                        if (@mkdir($baseDir)) {
381
+                            $extractDir .= '/' . $folders[0];
382
+                            OC_Helper::copyr($extractDir, $baseDir);
383
+                        }
384
+                        OC_Helper::copyr($extractDir, $baseDir);
385
+                        OC_Helper::rmdirr($extractDir);
386
+                        return;
387
+                    } else {
388
+                        throw new \Exception(
389
+                            sprintf(
390
+                                'Could not extract app with ID %s to %s',
391
+                                $appId,
392
+                                $extractDir
393
+                            )
394
+                        );
395
+                    }
396
+                } else {
397
+                    // Signature does not match
398
+                    throw new \Exception(
399
+                        sprintf(
400
+                            'App with id %s has invalid signature',
401
+                            $appId
402
+                        )
403
+                    );
404
+                }
405
+            }
406
+        }
407
+
408
+        throw new \Exception(
409
+            sprintf(
410
+                'Could not download app %s',
411
+                $appId
412
+            )
413
+        );
414
+    }
415
+
416
+    /**
417
+     * Check if an update for the app is available
418
+     *
419
+     * @param string $appId
420
+     * @param bool $allowUnstable
421
+     * @return string|false false or the version number of the update
422
+     */
423
+    public function isUpdateAvailable($appId, $allowUnstable = false) {
424
+        if ($this->isInstanceReadyForUpdates === null) {
425
+            $installPath = OC_App::getInstallPath();
426
+            if ($installPath === false || $installPath === null) {
427
+                $this->isInstanceReadyForUpdates = false;
428
+            } else {
429
+                $this->isInstanceReadyForUpdates = true;
430
+            }
431
+        }
432
+
433
+        if ($this->isInstanceReadyForUpdates === false) {
434
+            return false;
435
+        }
436
+
437
+        if ($this->isInstalledFromGit($appId) === true) {
438
+            return false;
439
+        }
440
+
441
+        if ($this->apps === null) {
442
+            $this->apps = $this->appFetcher->get($allowUnstable);
443
+        }
444
+
445
+        foreach ($this->apps as $app) {
446
+            if ($app['id'] === $appId) {
447
+                $currentVersion = OC_App::getAppVersion($appId);
448
+
449
+                if (!isset($app['releases'][0]['version'])) {
450
+                    return false;
451
+                }
452
+                $newestVersion = $app['releases'][0]['version'];
453
+                if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
454
+                    return $newestVersion;
455
+                } else {
456
+                    return false;
457
+                }
458
+            }
459
+        }
460
+
461
+        return false;
462
+    }
463
+
464
+    /**
465
+     * Check if app has been installed from git
466
+     * @param string $name name of the application to remove
467
+     * @return boolean
468
+     *
469
+     * The function will check if the path contains a .git folder
470
+     */
471
+    private function isInstalledFromGit($appId) {
472
+        $app = \OC_App::findAppInDirectories($appId);
473
+        if ($app === false) {
474
+            return false;
475
+        }
476
+        $basedir = $app['path'].'/'.$appId;
477
+        return file_exists($basedir.'/.git/');
478
+    }
479
+
480
+    /**
481
+     * Check if app is already downloaded
482
+     * @param string $name name of the application to remove
483
+     * @return boolean
484
+     *
485
+     * The function will check if the app is already downloaded in the apps repository
486
+     */
487
+    public function isDownloaded($name) {
488
+        foreach (\OC::$APPSROOTS as $dir) {
489
+            $dirToTest = $dir['path'];
490
+            $dirToTest .= '/';
491
+            $dirToTest .= $name;
492
+            $dirToTest .= '/';
493
+
494
+            if (is_dir($dirToTest)) {
495
+                return true;
496
+            }
497
+        }
498
+
499
+        return false;
500
+    }
501
+
502
+    /**
503
+     * Removes an app
504
+     * @param string $appId ID of the application to remove
505
+     * @return boolean
506
+     *
507
+     *
508
+     * This function works as follows
509
+     *   -# call uninstall repair steps
510
+     *   -# removing the files
511
+     *
512
+     * The function will not delete preferences, tables and the configuration,
513
+     * this has to be done by the function oc_app_uninstall().
514
+     */
515
+    public function removeApp($appId) {
516
+        if ($this->isDownloaded($appId)) {
517
+            if (\OC::$server->getAppManager()->isShipped($appId)) {
518
+                return false;
519
+            }
520
+            $appDir = OC_App::getInstallPath() . '/' . $appId;
521
+            OC_Helper::rmdirr($appDir);
522
+            return true;
523
+        } else {
524
+            \OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
525
+
526
+            return false;
527
+        }
528
+    }
529
+
530
+    /**
531
+     * Installs the app within the bundle and marks the bundle as installed
532
+     *
533
+     * @param Bundle $bundle
534
+     * @throws \Exception If app could not get installed
535
+     */
536
+    public function installAppBundle(Bundle $bundle) {
537
+        $appIds = $bundle->getAppIdentifiers();
538
+        foreach ($appIds as $appId) {
539
+            if (!$this->isDownloaded($appId)) {
540
+                $this->downloadApp($appId);
541
+            }
542
+            $this->installApp($appId);
543
+            $app = new OC_App();
544
+            $app->enable($appId);
545
+        }
546
+        $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
547
+        $bundles[] = $bundle->getIdentifier();
548
+        $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
549
+    }
550
+
551
+    /**
552
+     * Installs shipped apps
553
+     *
554
+     * This function installs all apps found in the 'apps' directory that should be enabled by default;
555
+     * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
556
+     *                         working ownCloud at the end instead of an aborted update.
557
+     * @return array Array of error messages (appid => Exception)
558
+     */
559
+    public static function installShippedApps($softErrors = false) {
560
+        $appManager = \OC::$server->getAppManager();
561
+        $config = \OC::$server->getConfig();
562
+        $errors = [];
563
+        foreach (\OC::$APPSROOTS as $app_dir) {
564
+            if ($dir = opendir($app_dir['path'])) {
565
+                while (false !== ($filename = readdir($dir))) {
566
+                    if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
567
+                        if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
568
+                            if ($config->getAppValue($filename, "installed_version", null) === null) {
569
+                                $info = OC_App::getAppInfo($filename);
570
+                                $enabled = isset($info['default_enable']);
571
+                                if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
572
+                                      && $config->getAppValue($filename, 'enabled') !== 'no') {
573
+                                    if ($softErrors) {
574
+                                        try {
575
+                                            Installer::installShippedApp($filename);
576
+                                        } catch (HintException $e) {
577
+                                            if ($e->getPrevious() instanceof TableExistsException) {
578
+                                                $errors[$filename] = $e;
579
+                                                continue;
580
+                                            }
581
+                                            throw $e;
582
+                                        }
583
+                                    } else {
584
+                                        Installer::installShippedApp($filename);
585
+                                    }
586
+                                    $config->setAppValue($filename, 'enabled', 'yes');
587
+                                }
588
+                            }
589
+                        }
590
+                    }
591
+                }
592
+                closedir($dir);
593
+            }
594
+        }
595
+
596
+        return $errors;
597
+    }
598
+
599
+    /**
600
+     * install an app already placed in the app folder
601
+     * @param string $app id of the app to install
602
+     * @return integer
603
+     */
604
+    public static function installShippedApp($app) {
605
+        //install the database
606
+        $appPath = OC_App::getAppPath($app);
607
+        \OC_App::registerAutoloading($app, $appPath);
608
+
609
+        $ms = new MigrationService($app, \OC::$server->get(Connection::class));
610
+        $ms->migrate('latest', true);
611
+
612
+        //run appinfo/install.php
613
+        self::includeAppScript("$appPath/appinfo/install.php");
614
+
615
+        $info = OC_App::getAppInfo($app);
616
+        if (is_null($info)) {
617
+            return false;
618
+        }
619
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
620
+
621
+        OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
622
+
623
+        $config = \OC::$server->getConfig();
624
+
625
+        $config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
626
+        if (array_key_exists('ocsid', $info)) {
627
+            $config->setAppValue($app, 'ocsid', $info['ocsid']);
628
+        }
629
+
630
+        //set remote/public handlers
631
+        foreach ($info['remote'] as $name => $path) {
632
+            $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
633
+        }
634
+        foreach ($info['public'] as $name => $path) {
635
+            $config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
636
+        }
637
+
638
+        OC_App::setAppTypes($info['id']);
639
+
640
+        return $info['id'];
641
+    }
642
+
643
+    /**
644
+     * @param string $script
645
+     */
646
+    private static function includeAppScript($script) {
647
+        if (file_exists($script)) {
648
+            include $script;
649
+        }
650
+    }
651 651
 }
Please login to merge, or discard this patch.
Spacing   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -115,8 +115,8 @@  discard block
 block discarded – undo
115 115
 
116 116
 		$basedir = $app['path'].'/'.$appId;
117 117
 
118
-		if (is_file($basedir . '/appinfo/database.xml')) {
119
-			throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
118
+		if (is_file($basedir.'/appinfo/database.xml')) {
119
+			throw new \Exception('The appinfo/database.xml file is not longer supported. Used in '.$appId);
120 120
 		}
121 121
 
122 122
 		$info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
@@ -167,7 +167,7 @@  discard block
 block discarded – undo
167 167
 		\OC_App::setupBackgroundJobs($info['background-jobs']);
168 168
 
169 169
 		//run appinfo/install.php
170
-		self::includeAppScript($basedir . '/appinfo/install.php');
170
+		self::includeAppScript($basedir.'/appinfo/install.php');
171 171
 
172 172
 		$appData = OC_App::getAppInfo($appId);
173 173
 		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
@@ -241,7 +241,7 @@  discard block
 block discarded – undo
241 241
 			if ($app['id'] === $appId) {
242 242
 				// Load the certificate
243 243
 				$certificate = new X509();
244
-				$rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
244
+				$rootCrt = file_get_contents(__DIR__.'/../../resources/codesigning/root.crt');
245 245
 				$rootCrts = $this->splitCerts($rootCrt);
246 246
 				foreach ($rootCrts as $rootCrt) {
247 247
 					$certificate->loadCA($rootCrt);
@@ -253,7 +253,7 @@  discard block
 block discarded – undo
253 253
 				foreach ($rootCrts as $rootCrt) {
254 254
 					$crl->loadCA($rootCrt);
255 255
 				}
256
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
256
+				$crl->loadCRL(file_get_contents(__DIR__.'/../../resources/codesigning/root.crl'));
257 257
 				if ($crl->validateSignature() !== true) {
258 258
 					throw new \Exception('Could not validate CRL signature');
259 259
 				}
@@ -306,7 +306,7 @@  discard block
 block discarded – undo
306 306
 
307 307
 				// Check if the signature actually matches the downloaded content
308 308
 				$certificate = openssl_get_publickey($app['certificate']);
309
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
309
+				$verified = (bool) openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
310 310
 				// PHP 8+ deprecates openssl_free_key and automatically destroys the key instance when it goes out of scope
311 311
 				if ((PHP_VERSION_ID < 80000)) {
312 312
 					openssl_free_key($certificate);
@@ -319,11 +319,11 @@  discard block
 block discarded – undo
319 319
 
320 320
 					if ($archive) {
321 321
 						if (!$archive->extract($extractDir)) {
322
-							$errorMessage = 'Could not extract app ' . $appId;
322
+							$errorMessage = 'Could not extract app '.$appId;
323 323
 
324 324
 							$archiveError = $archive->getError();
325 325
 							if ($archiveError instanceof \PEAR_Error) {
326
-								$errorMessage .= ': ' . $archiveError->getMessage();
326
+								$errorMessage .= ': '.$archiveError->getMessage();
327 327
 							}
328 328
 
329 329
 							throw new \Exception($errorMessage);
@@ -344,24 +344,24 @@  discard block
 block discarded – undo
344 344
 						// Check if appinfo/info.xml has the same app ID as well
345 345
 						if ((PHP_VERSION_ID < 80000)) {
346 346
 							$loadEntities = libxml_disable_entity_loader(false);
347
-							$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
347
+							$xml = simplexml_load_file($extractDir.'/'.$folders[0].'/appinfo/info.xml');
348 348
 							libxml_disable_entity_loader($loadEntities);
349 349
 						} else {
350
-							$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
350
+							$xml = simplexml_load_file($extractDir.'/'.$folders[0].'/appinfo/info.xml');
351 351
 						}
352
-						if ((string)$xml->id !== $appId) {
352
+						if ((string) $xml->id !== $appId) {
353 353
 							throw new \Exception(
354 354
 								sprintf(
355 355
 									'App for id %s has a wrong app ID in info.xml: %s',
356 356
 									$appId,
357
-									(string)$xml->id
357
+									(string) $xml->id
358 358
 								)
359 359
 							);
360 360
 						}
361 361
 
362 362
 						// Check if the version is lower than before
363 363
 						$currentVersion = OC_App::getAppVersion($appId);
364
-						$newVersion = (string)$xml->version;
364
+						$newVersion = (string) $xml->version;
365 365
 						if (version_compare($currentVersion, $newVersion) === 1) {
366 366
 							throw new \Exception(
367 367
 								sprintf(
@@ -373,12 +373,12 @@  discard block
 block discarded – undo
373 373
 							);
374 374
 						}
375 375
 
376
-						$baseDir = OC_App::getInstallPath() . '/' . $appId;
376
+						$baseDir = OC_App::getInstallPath().'/'.$appId;
377 377
 						// Remove old app with the ID if existent
378 378
 						OC_Helper::rmdirr($baseDir);
379 379
 						// Move to app folder
380 380
 						if (@mkdir($baseDir)) {
381
-							$extractDir .= '/' . $folders[0];
381
+							$extractDir .= '/'.$folders[0];
382 382
 							OC_Helper::copyr($extractDir, $baseDir);
383 383
 						}
384 384
 						OC_Helper::copyr($extractDir, $baseDir);
@@ -517,7 +517,7 @@  discard block
 block discarded – undo
517 517
 			if (\OC::$server->getAppManager()->isShipped($appId)) {
518 518
 				return false;
519 519
 			}
520
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
520
+			$appDir = OC_App::getInstallPath().'/'.$appId;
521 521
 			OC_Helper::rmdirr($appDir);
522 522
 			return true;
523 523
 		} else {
Please login to merge, or discard this patch.