Passed
Push — master ( d4d33e...10214f )
by Morris
13:41 queued 11s
created

Installer::downloadApp()   F

Complexity

Conditions 19
Paths 94

Size

Total Lines 169
Code Lines 106

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 106
c 1
b 0
f 0
nc 94
nop 2
dl 0
loc 169
rs 3.6133

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2016, Lukas Reschke <[email protected]>
5
 *
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Brice Maron <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Daniel Kesselberg <[email protected]>
10
 * @author Frank Karlitschek <[email protected]>
11
 * @author Georg Ehrke <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author John Molakvoæ (skjnldsv) <[email protected]>
14
 * @author Julius Härtl <[email protected]>
15
 * @author Kamil Domanski <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Markus Staab <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Robin Appelman <[email protected]>
20
 * @author Roeland Jago Douma <[email protected]>
21
 * @author root "root@oc.(none)"
22
 * @author Thomas Müller <[email protected]>
23
 * @author Thomas Tanghus <[email protected]>
24
 *
25
 * @license AGPL-3.0
26
 *
27
 * This code is free software: you can redistribute it and/or modify
28
 * it under the terms of the GNU Affero General Public License, version 3,
29
 * as published by the Free Software Foundation.
30
 *
31
 * This program is distributed in the hope that it will be useful,
32
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
33
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34
 * GNU Affero General Public License for more details.
35
 *
36
 * You should have received a copy of the GNU Affero General Public License, version 3,
37
 * along with this program. If not, see <http://www.gnu.org/licenses/>
38
 *
39
 */
40
41
namespace OC;
42
43
use Doctrine\DBAL\Exception\TableExistsException;
44
use OC\App\AppStore\Bundles\Bundle;
45
use OC\App\AppStore\Fetcher\AppFetcher;
46
use OC\AppFramework\Bootstrap\Coordinator;
47
use OC\Archive\TAR;
48
use OC\DB\Connection;
49
use OC_App;
50
use OC_DB;
51
use OC_Helper;
52
use OCP\Http\Client\IClientService;
53
use OCP\IConfig;
54
use OCP\ILogger;
55
use OCP\ITempManager;
56
use phpseclib\File\X509;
57
58
/**
59
 * This class provides the functionality needed to install, update and remove apps
60
 */
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
		$info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
118
119
		$l = \OC::$server->getL10N('core');
120
121
		if (!is_array($info)) {
122
			throw new \Exception(
123
				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
124
					[$appId]
125
				)
126
			);
127
		}
128
129
		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
130
		$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
131
132
		$version = implode('.', \OCP\Util::getVersion());
133
		if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
134
			throw new \Exception(
135
				// TODO $l
136
				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
137
					[$info['name']]
138
				)
139
			);
140
		}
141
142
		// check for required dependencies
143
		\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
144
		/** @var Coordinator $coordinator */
145
		$coordinator = \OC::$server->get(Coordinator::class);
146
		$coordinator->runLazyRegistration($appId);
147
		\OC_App::registerAutoloading($appId, $basedir);
148
149
		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $default of OCP\IConfig::getAppValue(). ( Ignorable by Annotation )

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

149
		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', /** @scrutinizer ignore-type */ false);
Loading history...
150
		if ($previousVersion) {
151
			OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
152
		}
153
154
		//install the database
155
		if (is_file($basedir.'/appinfo/database.xml')) {
156
			if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
0 ignored issues
show
introduced by
The condition OC::server->getConfig()-...lled_version') === null is always false.
Loading history...
157
				OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
158
			} else {
159
				OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
160
			}
161
		} else {
162
			$ms = new \OC\DB\MigrationService($info['id'], \OC::$server->get(Connection::class));
163
			$ms->migrate('latest', true);
164
		}
165
		if ($previousVersion) {
166
			OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
167
		}
168
169
		\OC_App::setupBackgroundJobs($info['background-jobs']);
170
171
		//run appinfo/install.php
172
		self::includeAppScript($basedir . '/appinfo/install.php');
173
174
		$appData = OC_App::getAppInfo($appId);
175
		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
176
177
		//set the installed version
178
		\OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
179
		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
180
181
		//set remote/public handlers
182
		foreach ($info['remote'] as $name => $path) {
183
			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
184
		}
185
		foreach ($info['public'] as $name => $path) {
186
			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
187
		}
188
189
		OC_App::setAppTypes($info['id']);
190
191
		return $info['id'];
192
	}
193
194
	/**
195
	 * Updates the specified app from the appstore
196
	 *
197
	 * @param string $appId
198
	 * @param bool [$allowUnstable] Allow unstable releases
0 ignored issues
show
Documentation Bug introduced by
The doc comment [$allowUnstable] at position 0 could not be parsed: Unknown type name '[' at position 0 in [$allowUnstable].
Loading history...
199
	 * @return bool
200
	 */
201
	public function updateAppstoreApp($appId, $allowUnstable = false) {
202
		if ($this->isUpdateAvailable($appId, $allowUnstable)) {
203
			try {
204
				$this->downloadApp($appId, $allowUnstable);
205
			} catch (\Exception $e) {
206
				$this->logger->logException($e, [
207
					'level' => ILogger::ERROR,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

207
					'level' => /** @scrutinizer ignore-deprecated */ ILogger::ERROR,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
208
					'app' => 'core',
209
				]);
210
				return false;
211
			}
212
			return OC_App::updateApp($appId);
213
		}
214
215
		return false;
216
	}
217
218
	/**
219
	 * Split the certificate file in individual certs
220
	 *
221
	 * @param string $cert
222
	 * @return string[]
223
	 */
224
	private function splitCerts(string $cert): array {
225
		preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
226
227
		return $matches[0];
228
	}
229
230
	/**
231
	 * Downloads an app and puts it into the app directory
232
	 *
233
	 * @param string $appId
234
	 * @param bool [$allowUnstable]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [$allowUnstable] at position 0 could not be parsed: Unknown type name '[' at position 0 in [$allowUnstable].
Loading history...
235
	 *
236
	 * @throws \Exception If the installation was not successful
237
	 */
238
	public function downloadApp($appId, $allowUnstable = false) {
239
		$appId = strtolower($appId);
240
241
		$apps = $this->appFetcher->get($allowUnstable);
242
		foreach ($apps as $app) {
243
			if ($app['id'] === $appId) {
244
				// Load the certificate
245
				$certificate = new X509();
246
				$rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
247
				$rootCrts = $this->splitCerts($rootCrt);
248
				foreach ($rootCrts as $rootCrt) {
249
					$certificate->loadCA($rootCrt);
250
				}
251
				$loadedCertificate = $certificate->loadX509($app['certificate']);
252
253
				// Verify if the certificate has been revoked
254
				$crl = new X509();
255
				foreach ($rootCrts as $rootCrt) {
256
					$crl->loadCA($rootCrt);
257
				}
258
				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
259
				if ($crl->validateSignature() !== true) {
260
					throw new \Exception('Could not validate CRL signature');
261
				}
262
				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
263
				$revoked = $crl->getRevoked($csn);
264
				if ($revoked !== false) {
265
					throw new \Exception(
266
						sprintf(
267
							'Certificate "%s" has been revoked',
268
							$csn
269
						)
270
					);
271
				}
272
273
				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
274
				if ($certificate->validateSignature() !== true) {
275
					throw new \Exception(
276
						sprintf(
277
							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
278
							$appId
279
						)
280
					);
281
				}
282
283
				// Verify if the certificate is issued for the requested app id
284
				$certInfo = openssl_x509_parse($app['certificate']);
285
				if (!isset($certInfo['subject']['CN'])) {
286
					throw new \Exception(
287
						sprintf(
288
							'App with id %s has a cert with no CN',
289
							$appId
290
						)
291
					);
292
				}
293
				if ($certInfo['subject']['CN'] !== $appId) {
294
					throw new \Exception(
295
						sprintf(
296
							'App with id %s has a cert issued to %s',
297
							$appId,
298
							$certInfo['subject']['CN']
299
						)
300
					);
301
				}
302
303
				// Download the release
304
				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
305
				$timeout = $this->isCLI ? 0 : 120;
306
				$client = $this->clientService->newClient();
307
				$client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
308
309
				// Check if the signature actually matches the downloaded content
310
				$certificate = openssl_get_publickey($app['certificate']);
311
				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
312
				openssl_free_key($certificate);
313
314
				if ($verified === true) {
315
					// Seems to match, let's proceed
316
					$extractDir = $this->tempManager->getTemporaryFolder();
317
					$archive = new TAR($tempFile);
318
319
					if ($archive) {
320
						if (!$archive->extract($extractDir)) {
321
							$errorMessage = 'Could not extract app ' . $appId;
322
323
							$archiveError = $archive->getError();
324
							if ($archiveError instanceof \PEAR_Error) {
325
								$errorMessage .= ': ' . $archiveError->getMessage();
326
							}
327
328
							throw new \Exception($errorMessage);
329
						}
330
						$allFiles = scandir($extractDir);
331
						$folders = array_diff($allFiles, ['.', '..']);
332
						$folders = array_values($folders);
333
334
						if (count($folders) > 1) {
335
							throw new \Exception(
336
								sprintf(
337
									'Extracted app %s has more than 1 folder',
338
									$appId
339
								)
340
							);
341
						}
342
343
						// Check if appinfo/info.xml has the same app ID as well
344
						$loadEntities = libxml_disable_entity_loader(false);
345
						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
346
						libxml_disable_entity_loader($loadEntities);
347
						if ((string)$xml->id !== $appId) {
348
							throw new \Exception(
349
								sprintf(
350
									'App for id %s has a wrong app ID in info.xml: %s',
351
									$appId,
352
									(string)$xml->id
353
								)
354
							);
355
						}
356
357
						// Check if the version is lower than before
358
						$currentVersion = OC_App::getAppVersion($appId);
359
						$newVersion = (string)$xml->version;
360
						if (version_compare($currentVersion, $newVersion) === 1) {
361
							throw new \Exception(
362
								sprintf(
363
									'App for id %s has version %s and tried to update to lower version %s',
364
									$appId,
365
									$currentVersion,
366
									$newVersion
367
								)
368
							);
369
						}
370
371
						$baseDir = OC_App::getInstallPath() . '/' . $appId;
0 ignored issues
show
Bug introduced by
Are you sure OC_App::getInstallPath() of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

371
						$baseDir = /** @scrutinizer ignore-type */ OC_App::getInstallPath() . '/' . $appId;
Loading history...
372
						// Remove old app with the ID if existent
373
						OC_Helper::rmdirr($baseDir);
374
						// Move to app folder
375
						if (@mkdir($baseDir)) {
376
							$extractDir .= '/' . $folders[0];
377
							OC_Helper::copyr($extractDir, $baseDir);
378
						}
379
						OC_Helper::copyr($extractDir, $baseDir);
380
						OC_Helper::rmdirr($extractDir);
381
						return;
382
					} else {
383
						throw new \Exception(
384
							sprintf(
385
								'Could not extract app with ID %s to %s',
386
								$appId,
387
								$extractDir
388
							)
389
						);
390
					}
391
				} else {
392
					// Signature does not match
393
					throw new \Exception(
394
						sprintf(
395
							'App with id %s has invalid signature',
396
							$appId
397
						)
398
					);
399
				}
400
			}
401
		}
402
403
		throw new \Exception(
404
			sprintf(
405
				'Could not download app %s',
406
				$appId
407
			)
408
		);
409
	}
410
411
	/**
412
	 * Check if an update for the app is available
413
	 *
414
	 * @param string $appId
415
	 * @param bool $allowUnstable
416
	 * @return string|false false or the version number of the update
417
	 */
418
	public function isUpdateAvailable($appId, $allowUnstable = false) {
419
		if ($this->isInstanceReadyForUpdates === null) {
420
			$installPath = OC_App::getInstallPath();
421
			if ($installPath === false || $installPath === null) {
422
				$this->isInstanceReadyForUpdates = false;
423
			} else {
424
				$this->isInstanceReadyForUpdates = true;
425
			}
426
		}
427
428
		if ($this->isInstanceReadyForUpdates === false) {
429
			return false;
430
		}
431
432
		if ($this->isInstalledFromGit($appId) === true) {
433
			return false;
434
		}
435
436
		if ($this->apps === null) {
437
			$this->apps = $this->appFetcher->get($allowUnstable);
438
		}
439
440
		foreach ($this->apps as $app) {
441
			if ($app['id'] === $appId) {
442
				$currentVersion = OC_App::getAppVersion($appId);
443
444
				if (!isset($app['releases'][0]['version'])) {
445
					return false;
446
				}
447
				$newestVersion = $app['releases'][0]['version'];
448
				if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
449
					return $newestVersion;
450
				} else {
451
					return false;
452
				}
453
			}
454
		}
455
456
		return false;
457
	}
458
459
	/**
460
	 * Check if app has been installed from git
461
	 * @param string $name name of the application to remove
462
	 * @return boolean
463
	 *
464
	 * The function will check if the path contains a .git folder
465
	 */
466
	private function isInstalledFromGit($appId) {
467
		$app = \OC_App::findAppInDirectories($appId);
468
		if ($app === false) {
469
			return false;
470
		}
471
		$basedir = $app['path'].'/'.$appId;
472
		return file_exists($basedir.'/.git/');
473
	}
474
475
	/**
476
	 * Check if app is already downloaded
477
	 * @param string $name name of the application to remove
478
	 * @return boolean
479
	 *
480
	 * The function will check if the app is already downloaded in the apps repository
481
	 */
482
	public function isDownloaded($name) {
483
		foreach (\OC::$APPSROOTS as $dir) {
484
			$dirToTest = $dir['path'];
485
			$dirToTest .= '/';
486
			$dirToTest .= $name;
487
			$dirToTest .= '/';
488
489
			if (is_dir($dirToTest)) {
490
				return true;
491
			}
492
		}
493
494
		return false;
495
	}
496
497
	/**
498
	 * Removes an app
499
	 * @param string $appId ID of the application to remove
500
	 * @return boolean
501
	 *
502
	 *
503
	 * This function works as follows
504
	 *   -# call uninstall repair steps
505
	 *   -# removing the files
506
	 *
507
	 * The function will not delete preferences, tables and the configuration,
508
	 * this has to be done by the function oc_app_uninstall().
509
	 */
510
	public function removeApp($appId) {
511
		if ($this->isDownloaded($appId)) {
512
			if (\OC::$server->getAppManager()->isShipped($appId)) {
513
				return false;
514
			}
515
			$appDir = OC_App::getInstallPath() . '/' . $appId;
0 ignored issues
show
Bug introduced by
Are you sure OC_App::getInstallPath() of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

515
			$appDir = /** @scrutinizer ignore-type */ OC_App::getInstallPath() . '/' . $appId;
Loading history...
516
			OC_Helper::rmdirr($appDir);
517
			return true;
518
		} else {
519
			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

519
			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
520
521
			return false;
522
		}
523
	}
524
525
	/**
526
	 * Installs the app within the bundle and marks the bundle as installed
527
	 *
528
	 * @param Bundle $bundle
529
	 * @throws \Exception If app could not get installed
530
	 */
531
	public function installAppBundle(Bundle $bundle) {
532
		$appIds = $bundle->getAppIdentifiers();
533
		foreach ($appIds as $appId) {
534
			if (!$this->isDownloaded($appId)) {
535
				$this->downloadApp($appId);
536
			}
537
			$this->installApp($appId);
538
			$app = new OC_App();
539
			$app->enable($appId);
540
		}
541
		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
542
		$bundles[] = $bundle->getIdentifier();
543
		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
544
	}
545
546
	/**
547
	 * Installs shipped apps
548
	 *
549
	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
550
	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
551
	 *                         working ownCloud at the end instead of an aborted update.
552
	 * @return array Array of error messages (appid => Exception)
553
	 */
554
	public static function installShippedApps($softErrors = false) {
555
		$appManager = \OC::$server->getAppManager();
556
		$config = \OC::$server->getConfig();
557
		$errors = [];
558
		foreach (\OC::$APPSROOTS as $app_dir) {
559
			if ($dir = opendir($app_dir['path'])) {
560
				while (false !== ($filename = readdir($dir))) {
561
					if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
562
						if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
563
							if ($config->getAppValue($filename, "installed_version", null) === null) {
564
								$info = OC_App::getAppInfo($filename);
565
								$enabled = isset($info['default_enable']);
566
								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
567
									  && $config->getAppValue($filename, 'enabled') !== 'no') {
568
									if ($softErrors) {
569
										try {
570
											Installer::installShippedApp($filename);
571
										} catch (HintException $e) {
572
											if ($e->getPrevious() instanceof TableExistsException) {
573
												$errors[$filename] = $e;
574
												continue;
575
											}
576
											throw $e;
577
										}
578
									} else {
579
										Installer::installShippedApp($filename);
580
									}
581
									$config->setAppValue($filename, 'enabled', 'yes');
582
								}
583
							}
584
						}
585
					}
586
				}
587
				closedir($dir);
588
			}
589
		}
590
591
		return $errors;
592
	}
593
594
	/**
595
	 * install an app already placed in the app folder
596
	 * @param string $app id of the app to install
597
	 * @return integer
598
	 */
599
	public static function installShippedApp($app) {
600
		//install the database
601
		$appPath = OC_App::getAppPath($app);
602
		\OC_App::registerAutoloading($app, $appPath);
603
604
		if (is_file("$appPath/appinfo/database.xml")) {
605
			try {
606
				OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
607
			} catch (TableExistsException $e) {
608
				throw new HintException(
609
					'Failed to enable app ' . $app,
610
					'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
611
					0, $e
612
				);
613
			}
614
		} else {
615
			$ms = new \OC\DB\MigrationService($app, \OC::$server->get(Connection::class));
616
			$ms->migrate('latest', true);
617
		}
618
619
		//run appinfo/install.php
620
		self::includeAppScript("$appPath/appinfo/install.php");
621
622
		$info = OC_App::getAppInfo($app);
623
		if (is_null($info)) {
624
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
625
		}
626
		\OC_App::setupBackgroundJobs($info['background-jobs']);
627
628
		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
629
630
		$config = \OC::$server->getConfig();
631
632
		$config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
633
		if (array_key_exists('ocsid', $info)) {
634
			$config->setAppValue($app, 'ocsid', $info['ocsid']);
635
		}
636
637
		//set remote/public handlers
638
		foreach ($info['remote'] as $name => $path) {
639
			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
640
		}
641
		foreach ($info['public'] as $name => $path) {
642
			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
643
		}
644
645
		OC_App::setAppTypes($info['id']);
646
647
		return $info['id'];
648
	}
649
650
	/**
651
	 * @param string $script
652
	 */
653
	private static function includeAppScript($script) {
654
		if (file_exists($script)) {
655
			include $script;
656
		}
657
	}
658
}
659