Passed
Push — master ( 0c7bed...48f1f9 )
by Joas
14:07 queued 12s
created
lib/private/Installer.php 1 patch
Indentation   +575 added lines, -575 removed lines patch added patch discarded remove patch
@@ -56,579 +56,579 @@
 block discarded – undo
56 56
  * This class provides the functionality needed to install, update and remove apps
57 57
  */
58 58
 class Installer {
59
-	/** @var AppFetcher */
60
-	private $appFetcher;
61
-	/** @var IClientService */
62
-	private $clientService;
63
-	/** @var ITempManager */
64
-	private $tempManager;
65
-	/** @var ILogger */
66
-	private $logger;
67
-	/** @var IConfig */
68
-	private $config;
69
-	/** @var array - for caching the result of app fetcher */
70
-	private $apps = null;
71
-	/** @var bool|null - for caching the result of the ready status */
72
-	private $isInstanceReadyForUpdates = null;
73
-	/** @var bool */
74
-	private $isCLI;
75
-
76
-	/**
77
-	 * @param AppFetcher $appFetcher
78
-	 * @param IClientService $clientService
79
-	 * @param ITempManager $tempManager
80
-	 * @param ILogger $logger
81
-	 * @param IConfig $config
82
-	 */
83
-	public function __construct(
84
-		AppFetcher $appFetcher,
85
-		IClientService $clientService,
86
-		ITempManager $tempManager,
87
-		ILogger $logger,
88
-		IConfig $config,
89
-		bool $isCLI
90
-	) {
91
-		$this->appFetcher = $appFetcher;
92
-		$this->clientService = $clientService;
93
-		$this->tempManager = $tempManager;
94
-		$this->logger = $logger;
95
-		$this->config = $config;
96
-		$this->isCLI = $isCLI;
97
-	}
98
-
99
-	/**
100
-	 * Installs an app that is located in one of the app folders already
101
-	 *
102
-	 * @param string $appId App to install
103
-	 * @param bool $forceEnable
104
-	 * @throws \Exception
105
-	 * @return string app ID
106
-	 */
107
-	public function installApp(string $appId, bool $forceEnable = false): string {
108
-		$app = \OC_App::findAppInDirectories($appId);
109
-		if ($app === false) {
110
-			throw new \Exception('App not found in any app directory');
111
-		}
112
-
113
-		$basedir = $app['path'].'/'.$appId;
114
-		$info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
115
-
116
-		$l = \OC::$server->getL10N('core');
117
-
118
-		if (!is_array($info)) {
119
-			throw new \Exception(
120
-				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
121
-					[$appId]
122
-				)
123
-			);
124
-		}
125
-
126
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
127
-		$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
128
-
129
-		$version = implode('.', \OCP\Util::getVersion());
130
-		if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
131
-			throw new \Exception(
132
-				// TODO $l
133
-				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
134
-					[$info['name']]
135
-				)
136
-			);
137
-		}
138
-
139
-		// check for required dependencies
140
-		\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
141
-		\OC_App::registerAutoloading($appId, $basedir);
142
-
143
-		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
144
-		if ($previousVersion) {
145
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
146
-		}
147
-
148
-		//install the database
149
-		if (is_file($basedir.'/appinfo/database.xml')) {
150
-			if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
151
-				OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
152
-			} else {
153
-				OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
154
-			}
155
-		} else {
156
-			$ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection());
157
-			$ms->migrate('latest', true);
158
-		}
159
-		if ($previousVersion) {
160
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
161
-		}
162
-
163
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
164
-
165
-		//run appinfo/install.php
166
-		self::includeAppScript($basedir . '/appinfo/install.php');
167
-
168
-		$appData = OC_App::getAppInfo($appId);
169
-		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
170
-
171
-		//set the installed version
172
-		\OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
173
-		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
174
-
175
-		//set remote/public handlers
176
-		foreach ($info['remote'] as $name => $path) {
177
-			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
178
-		}
179
-		foreach ($info['public'] as $name => $path) {
180
-			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
181
-		}
182
-
183
-		OC_App::setAppTypes($info['id']);
184
-
185
-		return $info['id'];
186
-	}
187
-
188
-	/**
189
-	 * Updates the specified app from the appstore
190
-	 *
191
-	 * @param string $appId
192
-	 * @param bool [$allowUnstable] Allow unstable releases
193
-	 * @return bool
194
-	 */
195
-	public function updateAppstoreApp($appId, $allowUnstable = false) {
196
-		if ($this->isUpdateAvailable($appId, $allowUnstable)) {
197
-			try {
198
-				$this->downloadApp($appId, $allowUnstable);
199
-			} catch (\Exception $e) {
200
-				$this->logger->logException($e, [
201
-					'level' => ILogger::ERROR,
202
-					'app' => 'core',
203
-				]);
204
-				return false;
205
-			}
206
-			return OC_App::updateApp($appId);
207
-		}
208
-
209
-		return false;
210
-	}
211
-
212
-	/**
213
-	 * Downloads an app and puts it into the app directory
214
-	 *
215
-	 * @param string $appId
216
-	 * @param bool [$allowUnstable]
217
-	 *
218
-	 * @throws \Exception If the installation was not successful
219
-	 */
220
-	public function downloadApp($appId, $allowUnstable = false) {
221
-		$appId = strtolower($appId);
222
-
223
-		$apps = $this->appFetcher->get($allowUnstable);
224
-		foreach ($apps as $app) {
225
-			if ($app['id'] === $appId) {
226
-				// Load the certificate
227
-				$certificate = new X509();
228
-				$certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
229
-				$loadedCertificate = $certificate->loadX509($app['certificate']);
230
-
231
-				// Verify if the certificate has been revoked
232
-				$crl = new X509();
233
-				$crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
234
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
235
-				if ($crl->validateSignature() !== true) {
236
-					throw new \Exception('Could not validate CRL signature');
237
-				}
238
-				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
239
-				$revoked = $crl->getRevoked($csn);
240
-				if ($revoked !== false) {
241
-					throw new \Exception(
242
-						sprintf(
243
-							'Certificate "%s" has been revoked',
244
-							$csn
245
-						)
246
-					);
247
-				}
248
-
249
-				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
250
-				if ($certificate->validateSignature() !== true) {
251
-					throw new \Exception(
252
-						sprintf(
253
-							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
254
-							$appId
255
-						)
256
-					);
257
-				}
258
-
259
-				// Verify if the certificate is issued for the requested app id
260
-				$certInfo = openssl_x509_parse($app['certificate']);
261
-				if (!isset($certInfo['subject']['CN'])) {
262
-					throw new \Exception(
263
-						sprintf(
264
-							'App with id %s has a cert with no CN',
265
-							$appId
266
-						)
267
-					);
268
-				}
269
-				if ($certInfo['subject']['CN'] !== $appId) {
270
-					throw new \Exception(
271
-						sprintf(
272
-							'App with id %s has a cert issued to %s',
273
-							$appId,
274
-							$certInfo['subject']['CN']
275
-						)
276
-					);
277
-				}
278
-
279
-				// Download the release
280
-				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
281
-				$timeout = $this->isCLI ? 0 : 120;
282
-				$client = $this->clientService->newClient();
283
-				$client->get($app['releases'][0]['download'], ['save_to' => $tempFile, 'timeout' => $timeout]);
284
-
285
-				// Check if the signature actually matches the downloaded content
286
-				$certificate = openssl_get_publickey($app['certificate']);
287
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
288
-				openssl_free_key($certificate);
289
-
290
-				if ($verified === true) {
291
-					// Seems to match, let's proceed
292
-					$extractDir = $this->tempManager->getTemporaryFolder();
293
-					$archive = new TAR($tempFile);
294
-
295
-					if ($archive) {
296
-						if (!$archive->extract($extractDir)) {
297
-							$errorMessage = 'Could not extract app ' . $appId;
298
-
299
-							$archiveError = $archive->getError();
300
-							if ($archiveError instanceof \PEAR_Error) {
301
-								$errorMessage .= ': ' . $archiveError->getMessage();
302
-							}
303
-
304
-							throw new \Exception($errorMessage);
305
-						}
306
-						$allFiles = scandir($extractDir);
307
-						$folders = array_diff($allFiles, ['.', '..']);
308
-						$folders = array_values($folders);
309
-
310
-						if (count($folders) > 1) {
311
-							throw new \Exception(
312
-								sprintf(
313
-									'Extracted app %s has more than 1 folder',
314
-									$appId
315
-								)
316
-							);
317
-						}
318
-
319
-						// Check if appinfo/info.xml has the same app ID as well
320
-						$loadEntities = libxml_disable_entity_loader(false);
321
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
322
-						libxml_disable_entity_loader($loadEntities);
323
-						if ((string)$xml->id !== $appId) {
324
-							throw new \Exception(
325
-								sprintf(
326
-									'App for id %s has a wrong app ID in info.xml: %s',
327
-									$appId,
328
-									(string)$xml->id
329
-								)
330
-							);
331
-						}
332
-
333
-						// Check if the version is lower than before
334
-						$currentVersion = OC_App::getAppVersion($appId);
335
-						$newVersion = (string)$xml->version;
336
-						if (version_compare($currentVersion, $newVersion) === 1) {
337
-							throw new \Exception(
338
-								sprintf(
339
-									'App for id %s has version %s and tried to update to lower version %s',
340
-									$appId,
341
-									$currentVersion,
342
-									$newVersion
343
-								)
344
-							);
345
-						}
346
-
347
-						$baseDir = OC_App::getInstallPath() . '/' . $appId;
348
-						// Remove old app with the ID if existent
349
-						OC_Helper::rmdirr($baseDir);
350
-						// Move to app folder
351
-						if (@mkdir($baseDir)) {
352
-							$extractDir .= '/' . $folders[0];
353
-							OC_Helper::copyr($extractDir, $baseDir);
354
-						}
355
-						OC_Helper::copyr($extractDir, $baseDir);
356
-						OC_Helper::rmdirr($extractDir);
357
-						return;
358
-					} else {
359
-						throw new \Exception(
360
-							sprintf(
361
-								'Could not extract app with ID %s to %s',
362
-								$appId,
363
-								$extractDir
364
-							)
365
-						);
366
-					}
367
-				} else {
368
-					// Signature does not match
369
-					throw new \Exception(
370
-						sprintf(
371
-							'App with id %s has invalid signature',
372
-							$appId
373
-						)
374
-					);
375
-				}
376
-			}
377
-		}
378
-
379
-		throw new \Exception(
380
-			sprintf(
381
-				'Could not download app %s',
382
-				$appId
383
-			)
384
-		);
385
-	}
386
-
387
-	/**
388
-	 * Check if an update for the app is available
389
-	 *
390
-	 * @param string $appId
391
-	 * @param bool $allowUnstable
392
-	 * @return string|false false or the version number of the update
393
-	 */
394
-	public function isUpdateAvailable($appId, $allowUnstable = false) {
395
-		if ($this->isInstanceReadyForUpdates === null) {
396
-			$installPath = OC_App::getInstallPath();
397
-			if ($installPath === false || $installPath === null) {
398
-				$this->isInstanceReadyForUpdates = false;
399
-			} else {
400
-				$this->isInstanceReadyForUpdates = true;
401
-			}
402
-		}
403
-
404
-		if ($this->isInstanceReadyForUpdates === false) {
405
-			return false;
406
-		}
407
-
408
-		if ($this->isInstalledFromGit($appId) === true) {
409
-			return false;
410
-		}
411
-
412
-		if ($this->apps === null) {
413
-			$this->apps = $this->appFetcher->get($allowUnstable);
414
-		}
415
-
416
-		foreach ($this->apps as $app) {
417
-			if ($app['id'] === $appId) {
418
-				$currentVersion = OC_App::getAppVersion($appId);
419
-
420
-				if (!isset($app['releases'][0]['version'])) {
421
-					return false;
422
-				}
423
-				$newestVersion = $app['releases'][0]['version'];
424
-				if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
425
-					return $newestVersion;
426
-				} else {
427
-					return false;
428
-				}
429
-			}
430
-		}
431
-
432
-		return false;
433
-	}
434
-
435
-	/**
436
-	 * Check if app has been installed from git
437
-	 * @param string $name name of the application to remove
438
-	 * @return boolean
439
-	 *
440
-	 * The function will check if the path contains a .git folder
441
-	 */
442
-	private function isInstalledFromGit($appId) {
443
-		$app = \OC_App::findAppInDirectories($appId);
444
-		if ($app === false) {
445
-			return false;
446
-		}
447
-		$basedir = $app['path'].'/'.$appId;
448
-		return file_exists($basedir.'/.git/');
449
-	}
450
-
451
-	/**
452
-	 * Check if app is already downloaded
453
-	 * @param string $name name of the application to remove
454
-	 * @return boolean
455
-	 *
456
-	 * The function will check if the app is already downloaded in the apps repository
457
-	 */
458
-	public function isDownloaded($name) {
459
-		foreach (\OC::$APPSROOTS as $dir) {
460
-			$dirToTest = $dir['path'];
461
-			$dirToTest .= '/';
462
-			$dirToTest .= $name;
463
-			$dirToTest .= '/';
464
-
465
-			if (is_dir($dirToTest)) {
466
-				return true;
467
-			}
468
-		}
469
-
470
-		return false;
471
-	}
472
-
473
-	/**
474
-	 * Removes an app
475
-	 * @param string $appId ID of the application to remove
476
-	 * @return boolean
477
-	 *
478
-	 *
479
-	 * This function works as follows
480
-	 *   -# call uninstall repair steps
481
-	 *   -# removing the files
482
-	 *
483
-	 * The function will not delete preferences, tables and the configuration,
484
-	 * this has to be done by the function oc_app_uninstall().
485
-	 */
486
-	public function removeApp($appId) {
487
-		if ($this->isDownloaded($appId)) {
488
-			if (\OC::$server->getAppManager()->isShipped($appId)) {
489
-				return false;
490
-			}
491
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
492
-			OC_Helper::rmdirr($appDir);
493
-			return true;
494
-		} else {
495
-			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
496
-
497
-			return false;
498
-		}
499
-	}
500
-
501
-	/**
502
-	 * Installs the app within the bundle and marks the bundle as installed
503
-	 *
504
-	 * @param Bundle $bundle
505
-	 * @throws \Exception If app could not get installed
506
-	 */
507
-	public function installAppBundle(Bundle $bundle) {
508
-		$appIds = $bundle->getAppIdentifiers();
509
-		foreach ($appIds as $appId) {
510
-			if (!$this->isDownloaded($appId)) {
511
-				$this->downloadApp($appId);
512
-			}
513
-			$this->installApp($appId);
514
-			$app = new OC_App();
515
-			$app->enable($appId);
516
-		}
517
-		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
518
-		$bundles[] = $bundle->getIdentifier();
519
-		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
520
-	}
521
-
522
-	/**
523
-	 * Installs shipped apps
524
-	 *
525
-	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
526
-	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
527
-	 *                         working ownCloud at the end instead of an aborted update.
528
-	 * @return array Array of error messages (appid => Exception)
529
-	 */
530
-	public static function installShippedApps($softErrors = false) {
531
-		$appManager = \OC::$server->getAppManager();
532
-		$config = \OC::$server->getConfig();
533
-		$errors = [];
534
-		foreach (\OC::$APPSROOTS as $app_dir) {
535
-			if ($dir = opendir($app_dir['path'])) {
536
-				while (false !== ($filename = readdir($dir))) {
537
-					if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
538
-						if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
539
-							if ($config->getAppValue($filename, "installed_version", null) === null) {
540
-								$info = OC_App::getAppInfo($filename);
541
-								$enabled = isset($info['default_enable']);
542
-								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
543
-									  && $config->getAppValue($filename, 'enabled') !== 'no') {
544
-									if ($softErrors) {
545
-										try {
546
-											Installer::installShippedApp($filename);
547
-										} catch (HintException $e) {
548
-											if ($e->getPrevious() instanceof TableExistsException) {
549
-												$errors[$filename] = $e;
550
-												continue;
551
-											}
552
-											throw $e;
553
-										}
554
-									} else {
555
-										Installer::installShippedApp($filename);
556
-									}
557
-									$config->setAppValue($filename, 'enabled', 'yes');
558
-								}
559
-							}
560
-						}
561
-					}
562
-				}
563
-				closedir($dir);
564
-			}
565
-		}
566
-
567
-		return $errors;
568
-	}
569
-
570
-	/**
571
-	 * install an app already placed in the app folder
572
-	 * @param string $app id of the app to install
573
-	 * @return integer
574
-	 */
575
-	public static function installShippedApp($app) {
576
-		//install the database
577
-		$appPath = OC_App::getAppPath($app);
578
-		\OC_App::registerAutoloading($app, $appPath);
579
-
580
-		if (is_file("$appPath/appinfo/database.xml")) {
581
-			try {
582
-				OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
583
-			} catch (TableExistsException $e) {
584
-				throw new HintException(
585
-					'Failed to enable app ' . $app,
586
-					'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
587
-					0, $e
588
-				);
589
-			}
590
-		} else {
591
-			$ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection());
592
-			$ms->migrate('latest', true);
593
-		}
594
-
595
-		//run appinfo/install.php
596
-		self::includeAppScript("$appPath/appinfo/install.php");
597
-
598
-		$info = OC_App::getAppInfo($app);
599
-		if (is_null($info)) {
600
-			return false;
601
-		}
602
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
603
-
604
-		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
605
-
606
-		$config = \OC::$server->getConfig();
607
-
608
-		$config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
609
-		if (array_key_exists('ocsid', $info)) {
610
-			$config->setAppValue($app, 'ocsid', $info['ocsid']);
611
-		}
612
-
613
-		//set remote/public handlers
614
-		foreach ($info['remote'] as $name => $path) {
615
-			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
616
-		}
617
-		foreach ($info['public'] as $name => $path) {
618
-			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
619
-		}
620
-
621
-		OC_App::setAppTypes($info['id']);
622
-
623
-		return $info['id'];
624
-	}
625
-
626
-	/**
627
-	 * @param string $script
628
-	 */
629
-	private static function includeAppScript($script) {
630
-		if (file_exists($script)) {
631
-			include $script;
632
-		}
633
-	}
59
+    /** @var AppFetcher */
60
+    private $appFetcher;
61
+    /** @var IClientService */
62
+    private $clientService;
63
+    /** @var ITempManager */
64
+    private $tempManager;
65
+    /** @var ILogger */
66
+    private $logger;
67
+    /** @var IConfig */
68
+    private $config;
69
+    /** @var array - for caching the result of app fetcher */
70
+    private $apps = null;
71
+    /** @var bool|null - for caching the result of the ready status */
72
+    private $isInstanceReadyForUpdates = null;
73
+    /** @var bool */
74
+    private $isCLI;
75
+
76
+    /**
77
+     * @param AppFetcher $appFetcher
78
+     * @param IClientService $clientService
79
+     * @param ITempManager $tempManager
80
+     * @param ILogger $logger
81
+     * @param IConfig $config
82
+     */
83
+    public function __construct(
84
+        AppFetcher $appFetcher,
85
+        IClientService $clientService,
86
+        ITempManager $tempManager,
87
+        ILogger $logger,
88
+        IConfig $config,
89
+        bool $isCLI
90
+    ) {
91
+        $this->appFetcher = $appFetcher;
92
+        $this->clientService = $clientService;
93
+        $this->tempManager = $tempManager;
94
+        $this->logger = $logger;
95
+        $this->config = $config;
96
+        $this->isCLI = $isCLI;
97
+    }
98
+
99
+    /**
100
+     * Installs an app that is located in one of the app folders already
101
+     *
102
+     * @param string $appId App to install
103
+     * @param bool $forceEnable
104
+     * @throws \Exception
105
+     * @return string app ID
106
+     */
107
+    public function installApp(string $appId, bool $forceEnable = false): string {
108
+        $app = \OC_App::findAppInDirectories($appId);
109
+        if ($app === false) {
110
+            throw new \Exception('App not found in any app directory');
111
+        }
112
+
113
+        $basedir = $app['path'].'/'.$appId;
114
+        $info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
115
+
116
+        $l = \OC::$server->getL10N('core');
117
+
118
+        if (!is_array($info)) {
119
+            throw new \Exception(
120
+                $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
121
+                    [$appId]
122
+                )
123
+            );
124
+        }
125
+
126
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
127
+        $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
128
+
129
+        $version = implode('.', \OCP\Util::getVersion());
130
+        if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
131
+            throw new \Exception(
132
+                // TODO $l
133
+                $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
134
+                    [$info['name']]
135
+                )
136
+            );
137
+        }
138
+
139
+        // check for required dependencies
140
+        \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
141
+        \OC_App::registerAutoloading($appId, $basedir);
142
+
143
+        $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
144
+        if ($previousVersion) {
145
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
146
+        }
147
+
148
+        //install the database
149
+        if (is_file($basedir.'/appinfo/database.xml')) {
150
+            if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
151
+                OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
152
+            } else {
153
+                OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
154
+            }
155
+        } else {
156
+            $ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection());
157
+            $ms->migrate('latest', true);
158
+        }
159
+        if ($previousVersion) {
160
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
161
+        }
162
+
163
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
164
+
165
+        //run appinfo/install.php
166
+        self::includeAppScript($basedir . '/appinfo/install.php');
167
+
168
+        $appData = OC_App::getAppInfo($appId);
169
+        OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
170
+
171
+        //set the installed version
172
+        \OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
173
+        \OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
174
+
175
+        //set remote/public handlers
176
+        foreach ($info['remote'] as $name => $path) {
177
+            \OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
178
+        }
179
+        foreach ($info['public'] as $name => $path) {
180
+            \OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
181
+        }
182
+
183
+        OC_App::setAppTypes($info['id']);
184
+
185
+        return $info['id'];
186
+    }
187
+
188
+    /**
189
+     * Updates the specified app from the appstore
190
+     *
191
+     * @param string $appId
192
+     * @param bool [$allowUnstable] Allow unstable releases
193
+     * @return bool
194
+     */
195
+    public function updateAppstoreApp($appId, $allowUnstable = false) {
196
+        if ($this->isUpdateAvailable($appId, $allowUnstable)) {
197
+            try {
198
+                $this->downloadApp($appId, $allowUnstable);
199
+            } catch (\Exception $e) {
200
+                $this->logger->logException($e, [
201
+                    'level' => ILogger::ERROR,
202
+                    'app' => 'core',
203
+                ]);
204
+                return false;
205
+            }
206
+            return OC_App::updateApp($appId);
207
+        }
208
+
209
+        return false;
210
+    }
211
+
212
+    /**
213
+     * Downloads an app and puts it into the app directory
214
+     *
215
+     * @param string $appId
216
+     * @param bool [$allowUnstable]
217
+     *
218
+     * @throws \Exception If the installation was not successful
219
+     */
220
+    public function downloadApp($appId, $allowUnstable = false) {
221
+        $appId = strtolower($appId);
222
+
223
+        $apps = $this->appFetcher->get($allowUnstable);
224
+        foreach ($apps as $app) {
225
+            if ($app['id'] === $appId) {
226
+                // Load the certificate
227
+                $certificate = new X509();
228
+                $certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
229
+                $loadedCertificate = $certificate->loadX509($app['certificate']);
230
+
231
+                // Verify if the certificate has been revoked
232
+                $crl = new X509();
233
+                $crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
234
+                $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
235
+                if ($crl->validateSignature() !== true) {
236
+                    throw new \Exception('Could not validate CRL signature');
237
+                }
238
+                $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
239
+                $revoked = $crl->getRevoked($csn);
240
+                if ($revoked !== false) {
241
+                    throw new \Exception(
242
+                        sprintf(
243
+                            'Certificate "%s" has been revoked',
244
+                            $csn
245
+                        )
246
+                    );
247
+                }
248
+
249
+                // Verify if the certificate has been issued by the Nextcloud Code Authority CA
250
+                if ($certificate->validateSignature() !== true) {
251
+                    throw new \Exception(
252
+                        sprintf(
253
+                            'App with id %s has a certificate not issued by a trusted Code Signing Authority',
254
+                            $appId
255
+                        )
256
+                    );
257
+                }
258
+
259
+                // Verify if the certificate is issued for the requested app id
260
+                $certInfo = openssl_x509_parse($app['certificate']);
261
+                if (!isset($certInfo['subject']['CN'])) {
262
+                    throw new \Exception(
263
+                        sprintf(
264
+                            'App with id %s has a cert with no CN',
265
+                            $appId
266
+                        )
267
+                    );
268
+                }
269
+                if ($certInfo['subject']['CN'] !== $appId) {
270
+                    throw new \Exception(
271
+                        sprintf(
272
+                            'App with id %s has a cert issued to %s',
273
+                            $appId,
274
+                            $certInfo['subject']['CN']
275
+                        )
276
+                    );
277
+                }
278
+
279
+                // Download the release
280
+                $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
281
+                $timeout = $this->isCLI ? 0 : 120;
282
+                $client = $this->clientService->newClient();
283
+                $client->get($app['releases'][0]['download'], ['save_to' => $tempFile, 'timeout' => $timeout]);
284
+
285
+                // Check if the signature actually matches the downloaded content
286
+                $certificate = openssl_get_publickey($app['certificate']);
287
+                $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
288
+                openssl_free_key($certificate);
289
+
290
+                if ($verified === true) {
291
+                    // Seems to match, let's proceed
292
+                    $extractDir = $this->tempManager->getTemporaryFolder();
293
+                    $archive = new TAR($tempFile);
294
+
295
+                    if ($archive) {
296
+                        if (!$archive->extract($extractDir)) {
297
+                            $errorMessage = 'Could not extract app ' . $appId;
298
+
299
+                            $archiveError = $archive->getError();
300
+                            if ($archiveError instanceof \PEAR_Error) {
301
+                                $errorMessage .= ': ' . $archiveError->getMessage();
302
+                            }
303
+
304
+                            throw new \Exception($errorMessage);
305
+                        }
306
+                        $allFiles = scandir($extractDir);
307
+                        $folders = array_diff($allFiles, ['.', '..']);
308
+                        $folders = array_values($folders);
309
+
310
+                        if (count($folders) > 1) {
311
+                            throw new \Exception(
312
+                                sprintf(
313
+                                    'Extracted app %s has more than 1 folder',
314
+                                    $appId
315
+                                )
316
+                            );
317
+                        }
318
+
319
+                        // Check if appinfo/info.xml has the same app ID as well
320
+                        $loadEntities = libxml_disable_entity_loader(false);
321
+                        $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
322
+                        libxml_disable_entity_loader($loadEntities);
323
+                        if ((string)$xml->id !== $appId) {
324
+                            throw new \Exception(
325
+                                sprintf(
326
+                                    'App for id %s has a wrong app ID in info.xml: %s',
327
+                                    $appId,
328
+                                    (string)$xml->id
329
+                                )
330
+                            );
331
+                        }
332
+
333
+                        // Check if the version is lower than before
334
+                        $currentVersion = OC_App::getAppVersion($appId);
335
+                        $newVersion = (string)$xml->version;
336
+                        if (version_compare($currentVersion, $newVersion) === 1) {
337
+                            throw new \Exception(
338
+                                sprintf(
339
+                                    'App for id %s has version %s and tried to update to lower version %s',
340
+                                    $appId,
341
+                                    $currentVersion,
342
+                                    $newVersion
343
+                                )
344
+                            );
345
+                        }
346
+
347
+                        $baseDir = OC_App::getInstallPath() . '/' . $appId;
348
+                        // Remove old app with the ID if existent
349
+                        OC_Helper::rmdirr($baseDir);
350
+                        // Move to app folder
351
+                        if (@mkdir($baseDir)) {
352
+                            $extractDir .= '/' . $folders[0];
353
+                            OC_Helper::copyr($extractDir, $baseDir);
354
+                        }
355
+                        OC_Helper::copyr($extractDir, $baseDir);
356
+                        OC_Helper::rmdirr($extractDir);
357
+                        return;
358
+                    } else {
359
+                        throw new \Exception(
360
+                            sprintf(
361
+                                'Could not extract app with ID %s to %s',
362
+                                $appId,
363
+                                $extractDir
364
+                            )
365
+                        );
366
+                    }
367
+                } else {
368
+                    // Signature does not match
369
+                    throw new \Exception(
370
+                        sprintf(
371
+                            'App with id %s has invalid signature',
372
+                            $appId
373
+                        )
374
+                    );
375
+                }
376
+            }
377
+        }
378
+
379
+        throw new \Exception(
380
+            sprintf(
381
+                'Could not download app %s',
382
+                $appId
383
+            )
384
+        );
385
+    }
386
+
387
+    /**
388
+     * Check if an update for the app is available
389
+     *
390
+     * @param string $appId
391
+     * @param bool $allowUnstable
392
+     * @return string|false false or the version number of the update
393
+     */
394
+    public function isUpdateAvailable($appId, $allowUnstable = false) {
395
+        if ($this->isInstanceReadyForUpdates === null) {
396
+            $installPath = OC_App::getInstallPath();
397
+            if ($installPath === false || $installPath === null) {
398
+                $this->isInstanceReadyForUpdates = false;
399
+            } else {
400
+                $this->isInstanceReadyForUpdates = true;
401
+            }
402
+        }
403
+
404
+        if ($this->isInstanceReadyForUpdates === false) {
405
+            return false;
406
+        }
407
+
408
+        if ($this->isInstalledFromGit($appId) === true) {
409
+            return false;
410
+        }
411
+
412
+        if ($this->apps === null) {
413
+            $this->apps = $this->appFetcher->get($allowUnstable);
414
+        }
415
+
416
+        foreach ($this->apps as $app) {
417
+            if ($app['id'] === $appId) {
418
+                $currentVersion = OC_App::getAppVersion($appId);
419
+
420
+                if (!isset($app['releases'][0]['version'])) {
421
+                    return false;
422
+                }
423
+                $newestVersion = $app['releases'][0]['version'];
424
+                if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
425
+                    return $newestVersion;
426
+                } else {
427
+                    return false;
428
+                }
429
+            }
430
+        }
431
+
432
+        return false;
433
+    }
434
+
435
+    /**
436
+     * Check if app has been installed from git
437
+     * @param string $name name of the application to remove
438
+     * @return boolean
439
+     *
440
+     * The function will check if the path contains a .git folder
441
+     */
442
+    private function isInstalledFromGit($appId) {
443
+        $app = \OC_App::findAppInDirectories($appId);
444
+        if ($app === false) {
445
+            return false;
446
+        }
447
+        $basedir = $app['path'].'/'.$appId;
448
+        return file_exists($basedir.'/.git/');
449
+    }
450
+
451
+    /**
452
+     * Check if app is already downloaded
453
+     * @param string $name name of the application to remove
454
+     * @return boolean
455
+     *
456
+     * The function will check if the app is already downloaded in the apps repository
457
+     */
458
+    public function isDownloaded($name) {
459
+        foreach (\OC::$APPSROOTS as $dir) {
460
+            $dirToTest = $dir['path'];
461
+            $dirToTest .= '/';
462
+            $dirToTest .= $name;
463
+            $dirToTest .= '/';
464
+
465
+            if (is_dir($dirToTest)) {
466
+                return true;
467
+            }
468
+        }
469
+
470
+        return false;
471
+    }
472
+
473
+    /**
474
+     * Removes an app
475
+     * @param string $appId ID of the application to remove
476
+     * @return boolean
477
+     *
478
+     *
479
+     * This function works as follows
480
+     *   -# call uninstall repair steps
481
+     *   -# removing the files
482
+     *
483
+     * The function will not delete preferences, tables and the configuration,
484
+     * this has to be done by the function oc_app_uninstall().
485
+     */
486
+    public function removeApp($appId) {
487
+        if ($this->isDownloaded($appId)) {
488
+            if (\OC::$server->getAppManager()->isShipped($appId)) {
489
+                return false;
490
+            }
491
+            $appDir = OC_App::getInstallPath() . '/' . $appId;
492
+            OC_Helper::rmdirr($appDir);
493
+            return true;
494
+        } else {
495
+            \OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
496
+
497
+            return false;
498
+        }
499
+    }
500
+
501
+    /**
502
+     * Installs the app within the bundle and marks the bundle as installed
503
+     *
504
+     * @param Bundle $bundle
505
+     * @throws \Exception If app could not get installed
506
+     */
507
+    public function installAppBundle(Bundle $bundle) {
508
+        $appIds = $bundle->getAppIdentifiers();
509
+        foreach ($appIds as $appId) {
510
+            if (!$this->isDownloaded($appId)) {
511
+                $this->downloadApp($appId);
512
+            }
513
+            $this->installApp($appId);
514
+            $app = new OC_App();
515
+            $app->enable($appId);
516
+        }
517
+        $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
518
+        $bundles[] = $bundle->getIdentifier();
519
+        $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
520
+    }
521
+
522
+    /**
523
+     * Installs shipped apps
524
+     *
525
+     * This function installs all apps found in the 'apps' directory that should be enabled by default;
526
+     * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
527
+     *                         working ownCloud at the end instead of an aborted update.
528
+     * @return array Array of error messages (appid => Exception)
529
+     */
530
+    public static function installShippedApps($softErrors = false) {
531
+        $appManager = \OC::$server->getAppManager();
532
+        $config = \OC::$server->getConfig();
533
+        $errors = [];
534
+        foreach (\OC::$APPSROOTS as $app_dir) {
535
+            if ($dir = opendir($app_dir['path'])) {
536
+                while (false !== ($filename = readdir($dir))) {
537
+                    if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
538
+                        if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
539
+                            if ($config->getAppValue($filename, "installed_version", null) === null) {
540
+                                $info = OC_App::getAppInfo($filename);
541
+                                $enabled = isset($info['default_enable']);
542
+                                if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
543
+                                      && $config->getAppValue($filename, 'enabled') !== 'no') {
544
+                                    if ($softErrors) {
545
+                                        try {
546
+                                            Installer::installShippedApp($filename);
547
+                                        } catch (HintException $e) {
548
+                                            if ($e->getPrevious() instanceof TableExistsException) {
549
+                                                $errors[$filename] = $e;
550
+                                                continue;
551
+                                            }
552
+                                            throw $e;
553
+                                        }
554
+                                    } else {
555
+                                        Installer::installShippedApp($filename);
556
+                                    }
557
+                                    $config->setAppValue($filename, 'enabled', 'yes');
558
+                                }
559
+                            }
560
+                        }
561
+                    }
562
+                }
563
+                closedir($dir);
564
+            }
565
+        }
566
+
567
+        return $errors;
568
+    }
569
+
570
+    /**
571
+     * install an app already placed in the app folder
572
+     * @param string $app id of the app to install
573
+     * @return integer
574
+     */
575
+    public static function installShippedApp($app) {
576
+        //install the database
577
+        $appPath = OC_App::getAppPath($app);
578
+        \OC_App::registerAutoloading($app, $appPath);
579
+
580
+        if (is_file("$appPath/appinfo/database.xml")) {
581
+            try {
582
+                OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
583
+            } catch (TableExistsException $e) {
584
+                throw new HintException(
585
+                    'Failed to enable app ' . $app,
586
+                    'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
587
+                    0, $e
588
+                );
589
+            }
590
+        } else {
591
+            $ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection());
592
+            $ms->migrate('latest', true);
593
+        }
594
+
595
+        //run appinfo/install.php
596
+        self::includeAppScript("$appPath/appinfo/install.php");
597
+
598
+        $info = OC_App::getAppInfo($app);
599
+        if (is_null($info)) {
600
+            return false;
601
+        }
602
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
603
+
604
+        OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
605
+
606
+        $config = \OC::$server->getConfig();
607
+
608
+        $config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
609
+        if (array_key_exists('ocsid', $info)) {
610
+            $config->setAppValue($app, 'ocsid', $info['ocsid']);
611
+        }
612
+
613
+        //set remote/public handlers
614
+        foreach ($info['remote'] as $name => $path) {
615
+            $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
616
+        }
617
+        foreach ($info['public'] as $name => $path) {
618
+            $config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
619
+        }
620
+
621
+        OC_App::setAppTypes($info['id']);
622
+
623
+        return $info['id'];
624
+    }
625
+
626
+    /**
627
+     * @param string $script
628
+     */
629
+    private static function includeAppScript($script) {
630
+        if (file_exists($script)) {
631
+            include $script;
632
+        }
633
+    }
634 634
 }
Please login to merge, or discard this patch.
lib/private/Setup/AbstractDatabase.php 1 patch
Indentation   +104 added lines, -104 removed lines patch added patch discarded remove patch
@@ -38,118 +38,118 @@
 block discarded – undo
38 38
 
39 39
 abstract class AbstractDatabase {
40 40
 
41
-	/** @var IL10N */
42
-	protected $trans;
43
-	/** @var string */
44
-	protected $dbUser;
45
-	/** @var string */
46
-	protected $dbPassword;
47
-	/** @var string */
48
-	protected $dbName;
49
-	/** @var string */
50
-	protected $dbHost;
51
-	/** @var string */
52
-	protected $dbPort;
53
-	/** @var string */
54
-	protected $tablePrefix;
55
-	/** @var SystemConfig */
56
-	protected $config;
57
-	/** @var ILogger */
58
-	protected $logger;
59
-	/** @var ISecureRandom */
60
-	protected $random;
41
+    /** @var IL10N */
42
+    protected $trans;
43
+    /** @var string */
44
+    protected $dbUser;
45
+    /** @var string */
46
+    protected $dbPassword;
47
+    /** @var string */
48
+    protected $dbName;
49
+    /** @var string */
50
+    protected $dbHost;
51
+    /** @var string */
52
+    protected $dbPort;
53
+    /** @var string */
54
+    protected $tablePrefix;
55
+    /** @var SystemConfig */
56
+    protected $config;
57
+    /** @var ILogger */
58
+    protected $logger;
59
+    /** @var ISecureRandom */
60
+    protected $random;
61 61
 
62
-	public function __construct(IL10N $trans, SystemConfig $config, ILogger $logger, ISecureRandom $random) {
63
-		$this->trans = $trans;
64
-		$this->config = $config;
65
-		$this->logger = $logger;
66
-		$this->random = $random;
67
-	}
62
+    public function __construct(IL10N $trans, SystemConfig $config, ILogger $logger, ISecureRandom $random) {
63
+        $this->trans = $trans;
64
+        $this->config = $config;
65
+        $this->logger = $logger;
66
+        $this->random = $random;
67
+    }
68 68
 
69
-	public function validate($config) {
70
-		$errors = [];
71
-		if (empty($config['dbuser']) && empty($config['dbname'])) {
72
-			$errors[] = $this->trans->t("%s enter the database username and name.", [$this->dbprettyname]);
73
-		} elseif (empty($config['dbuser'])) {
74
-			$errors[] = $this->trans->t("%s enter the database username.", [$this->dbprettyname]);
75
-		} elseif (empty($config['dbname'])) {
76
-			$errors[] = $this->trans->t("%s enter the database name.", [$this->dbprettyname]);
77
-		}
78
-		if (substr_count($config['dbname'], '.') >= 1) {
79
-			$errors[] = $this->trans->t("%s you may not use dots in the database name", [$this->dbprettyname]);
80
-		}
81
-		return $errors;
82
-	}
69
+    public function validate($config) {
70
+        $errors = [];
71
+        if (empty($config['dbuser']) && empty($config['dbname'])) {
72
+            $errors[] = $this->trans->t("%s enter the database username and name.", [$this->dbprettyname]);
73
+        } elseif (empty($config['dbuser'])) {
74
+            $errors[] = $this->trans->t("%s enter the database username.", [$this->dbprettyname]);
75
+        } elseif (empty($config['dbname'])) {
76
+            $errors[] = $this->trans->t("%s enter the database name.", [$this->dbprettyname]);
77
+        }
78
+        if (substr_count($config['dbname'], '.') >= 1) {
79
+            $errors[] = $this->trans->t("%s you may not use dots in the database name", [$this->dbprettyname]);
80
+        }
81
+        return $errors;
82
+    }
83 83
 
84
-	public function initialize($config) {
85
-		$dbUser = $config['dbuser'];
86
-		$dbPass = $config['dbpass'];
87
-		$dbName = $config['dbname'];
88
-		$dbHost = !empty($config['dbhost']) ? $config['dbhost'] : 'localhost';
89
-		$dbPort = !empty($config['dbport']) ? $config['dbport'] : '';
90
-		$dbTablePrefix = isset($config['dbtableprefix']) ? $config['dbtableprefix'] : 'oc_';
84
+    public function initialize($config) {
85
+        $dbUser = $config['dbuser'];
86
+        $dbPass = $config['dbpass'];
87
+        $dbName = $config['dbname'];
88
+        $dbHost = !empty($config['dbhost']) ? $config['dbhost'] : 'localhost';
89
+        $dbPort = !empty($config['dbport']) ? $config['dbport'] : '';
90
+        $dbTablePrefix = isset($config['dbtableprefix']) ? $config['dbtableprefix'] : 'oc_';
91 91
 
92
-		$this->config->setValues([
93
-			'dbname' => $dbName,
94
-			'dbhost' => $dbHost,
95
-			'dbport' => $dbPort,
96
-			'dbtableprefix' => $dbTablePrefix,
97
-		]);
92
+        $this->config->setValues([
93
+            'dbname' => $dbName,
94
+            'dbhost' => $dbHost,
95
+            'dbport' => $dbPort,
96
+            'dbtableprefix' => $dbTablePrefix,
97
+        ]);
98 98
 
99
-		$this->dbUser = $dbUser;
100
-		$this->dbPassword = $dbPass;
101
-		$this->dbName = $dbName;
102
-		$this->dbHost = $dbHost;
103
-		$this->dbPort = $dbPort;
104
-		$this->tablePrefix = $dbTablePrefix;
105
-	}
99
+        $this->dbUser = $dbUser;
100
+        $this->dbPassword = $dbPass;
101
+        $this->dbName = $dbName;
102
+        $this->dbHost = $dbHost;
103
+        $this->dbPort = $dbPort;
104
+        $this->tablePrefix = $dbTablePrefix;
105
+    }
106 106
 
107
-	/**
108
-	 * @param array $configOverwrite
109
-	 * @return \OC\DB\Connection
110
-	 */
111
-	protected function connect(array $configOverwrite = []) {
112
-		$connectionParams = [
113
-			'host' => $this->dbHost,
114
-			'user' => $this->dbUser,
115
-			'password' => $this->dbPassword,
116
-			'tablePrefix' => $this->tablePrefix,
117
-			'dbname' => $this->dbName
118
-		];
107
+    /**
108
+     * @param array $configOverwrite
109
+     * @return \OC\DB\Connection
110
+     */
111
+    protected function connect(array $configOverwrite = []) {
112
+        $connectionParams = [
113
+            'host' => $this->dbHost,
114
+            'user' => $this->dbUser,
115
+            'password' => $this->dbPassword,
116
+            'tablePrefix' => $this->tablePrefix,
117
+            'dbname' => $this->dbName
118
+        ];
119 119
 
120
-		// adding port support through installer
121
-		if (!empty($this->dbPort)) {
122
-			if (ctype_digit($this->dbPort)) {
123
-				$connectionParams['port'] = $this->dbPort;
124
-			} else {
125
-				$connectionParams['unix_socket'] = $this->dbPort;
126
-			}
127
-		} elseif (strpos($this->dbHost, ':')) {
128
-			// Host variable may carry a port or socket.
129
-			list($host, $portOrSocket) = explode(':', $this->dbHost, 2);
130
-			if (ctype_digit($portOrSocket)) {
131
-				$connectionParams['port'] = $portOrSocket;
132
-			} else {
133
-				$connectionParams['unix_socket'] = $portOrSocket;
134
-			}
135
-			$connectionParams['host'] = $host;
136
-		}
120
+        // adding port support through installer
121
+        if (!empty($this->dbPort)) {
122
+            if (ctype_digit($this->dbPort)) {
123
+                $connectionParams['port'] = $this->dbPort;
124
+            } else {
125
+                $connectionParams['unix_socket'] = $this->dbPort;
126
+            }
127
+        } elseif (strpos($this->dbHost, ':')) {
128
+            // Host variable may carry a port or socket.
129
+            list($host, $portOrSocket) = explode(':', $this->dbHost, 2);
130
+            if (ctype_digit($portOrSocket)) {
131
+                $connectionParams['port'] = $portOrSocket;
132
+            } else {
133
+                $connectionParams['unix_socket'] = $portOrSocket;
134
+            }
135
+            $connectionParams['host'] = $host;
136
+        }
137 137
 
138
-		$connectionParams = array_merge($connectionParams, $configOverwrite);
139
-		$cf = new ConnectionFactory($this->config);
140
-		return $cf->getConnection($this->config->getValue('dbtype', 'sqlite'), $connectionParams);
141
-	}
138
+        $connectionParams = array_merge($connectionParams, $configOverwrite);
139
+        $cf = new ConnectionFactory($this->config);
140
+        return $cf->getConnection($this->config->getValue('dbtype', 'sqlite'), $connectionParams);
141
+    }
142 142
 
143
-	/**
144
-	 * @param string $userName
145
-	 */
146
-	abstract public function setupDatabase($userName);
143
+    /**
144
+     * @param string $userName
145
+     */
146
+    abstract public function setupDatabase($userName);
147 147
 
148
-	public function runMigrations() {
149
-		if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) {
150
-			return;
151
-		}
152
-		$ms = new MigrationService('core', \OC::$server->getDatabaseConnection());
153
-		$ms->migrate('latest', true);
154
-	}
148
+    public function runMigrations() {
149
+        if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) {
150
+            return;
151
+        }
152
+        $ms = new MigrationService('core', \OC::$server->getDatabaseConnection());
153
+        $ms->migrate('latest', true);
154
+    }
155 155
 }
Please login to merge, or discard this patch.
lib/private/DB/MigrationService.php 2 patches
Indentation   +573 added lines, -573 removed lines patch added patch discarded remove patch
@@ -46,577 +46,577 @@
 block discarded – undo
46 46
 
47 47
 class MigrationService {
48 48
 
49
-	/** @var boolean */
50
-	private $migrationTableCreated;
51
-	/** @var array */
52
-	private $migrations;
53
-	/** @var IOutput */
54
-	private $output;
55
-	/** @var Connection */
56
-	private $connection;
57
-	/** @var string */
58
-	private $appName;
59
-	/** @var bool */
60
-	private $checkOracle;
61
-
62
-	/**
63
-	 * MigrationService constructor.
64
-	 *
65
-	 * @param $appName
66
-	 * @param IDBConnection $connection
67
-	 * @param AppLocator $appLocator
68
-	 * @param IOutput|null $output
69
-	 * @throws \Exception
70
-	 */
71
-	public function __construct($appName, IDBConnection $connection, IOutput $output = null, AppLocator $appLocator = null) {
72
-		$this->appName = $appName;
73
-		$this->connection = $connection;
74
-		$this->output = $output;
75
-		if (null === $this->output) {
76
-			$this->output = new SimpleOutput(\OC::$server->getLogger(), $appName);
77
-		}
78
-
79
-		if ($appName === 'core') {
80
-			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
81
-			$this->migrationsNamespace = 'OC\\Core\\Migrations';
82
-			$this->checkOracle = true;
83
-		} else {
84
-			if (null === $appLocator) {
85
-				$appLocator = new AppLocator();
86
-			}
87
-			$appPath = $appLocator->getAppPath($appName);
88
-			$namespace = App::buildAppNamespace($appName);
89
-			$this->migrationsPath = "$appPath/lib/Migration";
90
-			$this->migrationsNamespace = $namespace . '\\Migration';
91
-
92
-			$infoParser = new InfoParser();
93
-			$info = $infoParser->parse($appPath . '/appinfo/info.xml');
94
-			if (!isset($info['dependencies']['database'])) {
95
-				$this->checkOracle = true;
96
-			} else {
97
-				$this->checkOracle = false;
98
-				foreach ($info['dependencies']['database'] as $database) {
99
-					if (\is_string($database) && $database === 'oci') {
100
-						$this->checkOracle = true;
101
-					} elseif (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
102
-						$this->checkOracle = true;
103
-					}
104
-				}
105
-			}
106
-		}
107
-	}
108
-
109
-	/**
110
-	 * Returns the name of the app for which this migration is executed
111
-	 *
112
-	 * @return string
113
-	 */
114
-	public function getApp() {
115
-		return $this->appName;
116
-	}
117
-
118
-	/**
119
-	 * @return bool
120
-	 * @codeCoverageIgnore - this will implicitly tested on installation
121
-	 */
122
-	private function createMigrationTable() {
123
-		if ($this->migrationTableCreated) {
124
-			return false;
125
-		}
126
-
127
-		if ($this->connection->tableExists('migrations')) {
128
-			$this->migrationTableCreated = true;
129
-			return false;
130
-		}
131
-
132
-		$schema = new SchemaWrapper($this->connection);
133
-
134
-		/**
135
-		 * We drop the table when it has different columns or the definition does not
136
-		 * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
137
-		 */
138
-		try {
139
-			$table = $schema->getTable('migrations');
140
-			$columns = $table->getColumns();
141
-
142
-			if (count($columns) === 2) {
143
-				try {
144
-					$column = $table->getColumn('app');
145
-					$schemaMismatch = $column->getLength() !== 255;
146
-
147
-					if (!$schemaMismatch) {
148
-						$column = $table->getColumn('version');
149
-						$schemaMismatch = $column->getLength() !== 255;
150
-					}
151
-				} catch (SchemaException $e) {
152
-					// One of the columns is missing
153
-					$schemaMismatch = true;
154
-				}
155
-
156
-				if (!$schemaMismatch) {
157
-					// Table exists and schema matches: return back!
158
-					$this->migrationTableCreated = true;
159
-					return false;
160
-				}
161
-			}
162
-
163
-			// Drop the table, when it didn't match our expectations.
164
-			$this->connection->dropTable('migrations');
165
-
166
-			// Recreate the schema after the table was dropped.
167
-			$schema = new SchemaWrapper($this->connection);
168
-		} catch (SchemaException $e) {
169
-			// Table not found, no need to panic, we will create it.
170
-		}
171
-
172
-		$table = $schema->createTable('migrations');
173
-		$table->addColumn('app', Types::STRING, ['length' => 255]);
174
-		$table->addColumn('version', Types::STRING, ['length' => 255]);
175
-		$table->setPrimaryKey(['app', 'version']);
176
-
177
-		$this->connection->migrateToSchema($schema->getWrappedSchema());
178
-
179
-		$this->migrationTableCreated = true;
180
-
181
-		return true;
182
-	}
183
-
184
-	/**
185
-	 * Returns all versions which have already been applied
186
-	 *
187
-	 * @return string[]
188
-	 * @codeCoverageIgnore - no need to test this
189
-	 */
190
-	public function getMigratedVersions() {
191
-		$this->createMigrationTable();
192
-		$qb = $this->connection->getQueryBuilder();
193
-
194
-		$qb->select('version')
195
-			->from('migrations')
196
-			->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
197
-			->orderBy('version');
198
-
199
-		$result = $qb->execute();
200
-		$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
201
-		$result->closeCursor();
202
-
203
-		return $rows;
204
-	}
205
-
206
-	/**
207
-	 * Returns all versions which are available in the migration folder
208
-	 *
209
-	 * @return array
210
-	 */
211
-	public function getAvailableVersions() {
212
-		$this->ensureMigrationsAreLoaded();
213
-		return array_map('strval', array_keys($this->migrations));
214
-	}
215
-
216
-	protected function findMigrations() {
217
-		$directory = realpath($this->migrationsPath);
218
-		if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
219
-			return [];
220
-		}
221
-
222
-		$iterator = new \RegexIterator(
223
-			new \RecursiveIteratorIterator(
224
-				new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
225
-				\RecursiveIteratorIterator::LEAVES_ONLY
226
-			),
227
-			'#^.+\\/Version[^\\/]{1,255}\\.php$#i',
228
-			\RegexIterator::GET_MATCH);
229
-
230
-		$files = array_keys(iterator_to_array($iterator));
231
-		uasort($files, function ($a, $b) {
232
-			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
233
-			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
234
-			if (!empty($matchA) && !empty($matchB)) {
235
-				if ($matchA[1] !== $matchB[1]) {
236
-					return ($matchA[1] < $matchB[1]) ? -1 : 1;
237
-				}
238
-				return ($matchA[2] < $matchB[2]) ? -1 : 1;
239
-			}
240
-			return (basename($a) < basename($b)) ? -1 : 1;
241
-		});
242
-
243
-		$migrations = [];
244
-
245
-		foreach ($files as $file) {
246
-			$className = basename($file, '.php');
247
-			$version = (string) substr($className, 7);
248
-			if ($version === '0') {
249
-				throw new \InvalidArgumentException(
250
-					"Cannot load a migrations with the name '$version' because it is a reserved number"
251
-				);
252
-			}
253
-			$migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
254
-		}
255
-
256
-		return $migrations;
257
-	}
258
-
259
-	/**
260
-	 * @param string $to
261
-	 * @return string[]
262
-	 */
263
-	private function getMigrationsToExecute($to) {
264
-		$knownMigrations = $this->getMigratedVersions();
265
-		$availableMigrations = $this->getAvailableVersions();
266
-
267
-		$toBeExecuted = [];
268
-		foreach ($availableMigrations as $v) {
269
-			if ($to !== 'latest' && $v > $to) {
270
-				continue;
271
-			}
272
-			if ($this->shallBeExecuted($v, $knownMigrations)) {
273
-				$toBeExecuted[] = $v;
274
-			}
275
-		}
276
-
277
-		return $toBeExecuted;
278
-	}
279
-
280
-	/**
281
-	 * @param string $m
282
-	 * @param string[] $knownMigrations
283
-	 * @return bool
284
-	 */
285
-	private function shallBeExecuted($m, $knownMigrations) {
286
-		if (in_array($m, $knownMigrations)) {
287
-			return false;
288
-		}
289
-
290
-		return true;
291
-	}
292
-
293
-	/**
294
-	 * @param string $version
295
-	 */
296
-	private function markAsExecuted($version) {
297
-		$this->connection->insertIfNotExist('*PREFIX*migrations', [
298
-			'app' => $this->appName,
299
-			'version' => $version
300
-		]);
301
-	}
302
-
303
-	/**
304
-	 * Returns the name of the table which holds the already applied versions
305
-	 *
306
-	 * @return string
307
-	 */
308
-	public function getMigrationsTableName() {
309
-		return $this->connection->getPrefix() . 'migrations';
310
-	}
311
-
312
-	/**
313
-	 * Returns the namespace of the version classes
314
-	 *
315
-	 * @return string
316
-	 */
317
-	public function getMigrationsNamespace() {
318
-		return $this->migrationsNamespace;
319
-	}
320
-
321
-	/**
322
-	 * Returns the directory which holds the versions
323
-	 *
324
-	 * @return string
325
-	 */
326
-	public function getMigrationsDirectory() {
327
-		return $this->migrationsPath;
328
-	}
329
-
330
-	/**
331
-	 * Return the explicit version for the aliases; current, next, prev, latest
332
-	 *
333
-	 * @param string $alias
334
-	 * @return mixed|null|string
335
-	 */
336
-	public function getMigration($alias) {
337
-		switch ($alias) {
338
-			case 'current':
339
-				return $this->getCurrentVersion();
340
-			case 'next':
341
-				return $this->getRelativeVersion($this->getCurrentVersion(), 1);
342
-			case 'prev':
343
-				return $this->getRelativeVersion($this->getCurrentVersion(), -1);
344
-			case 'latest':
345
-				$this->ensureMigrationsAreLoaded();
346
-
347
-				$migrations = $this->getAvailableVersions();
348
-				return @end($migrations);
349
-		}
350
-		return '0';
351
-	}
352
-
353
-	/**
354
-	 * @param string $version
355
-	 * @param int $delta
356
-	 * @return null|string
357
-	 */
358
-	private function getRelativeVersion($version, $delta) {
359
-		$this->ensureMigrationsAreLoaded();
360
-
361
-		$versions = $this->getAvailableVersions();
362
-		array_unshift($versions, 0);
363
-		$offset = array_search($version, $versions, true);
364
-		if ($offset === false || !isset($versions[$offset + $delta])) {
365
-			// Unknown version or delta out of bounds.
366
-			return null;
367
-		}
368
-
369
-		return (string) $versions[$offset + $delta];
370
-	}
371
-
372
-	/**
373
-	 * @return string
374
-	 */
375
-	private function getCurrentVersion() {
376
-		$m = $this->getMigratedVersions();
377
-		if (count($m) === 0) {
378
-			return '0';
379
-		}
380
-		$migrations = array_values($m);
381
-		return @end($migrations);
382
-	}
383
-
384
-	/**
385
-	 * @param string $version
386
-	 * @return string
387
-	 * @throws \InvalidArgumentException
388
-	 */
389
-	private function getClass($version) {
390
-		$this->ensureMigrationsAreLoaded();
391
-
392
-		if (isset($this->migrations[$version])) {
393
-			return $this->migrations[$version];
394
-		}
395
-
396
-		throw new \InvalidArgumentException("Version $version is unknown.");
397
-	}
398
-
399
-	/**
400
-	 * Allows to set an IOutput implementation which is used for logging progress and messages
401
-	 *
402
-	 * @param IOutput $output
403
-	 */
404
-	public function setOutput(IOutput $output) {
405
-		$this->output = $output;
406
-	}
407
-
408
-	/**
409
-	 * Applies all not yet applied versions up to $to
410
-	 *
411
-	 * @param string $to
412
-	 * @param bool $schemaOnly
413
-	 * @throws \InvalidArgumentException
414
-	 */
415
-	public function migrate($to = 'latest', $schemaOnly = false) {
416
-		if ($schemaOnly) {
417
-			$this->migrateSchemaOnly($to);
418
-			return;
419
-		}
420
-
421
-		// read known migrations
422
-		$toBeExecuted = $this->getMigrationsToExecute($to);
423
-		foreach ($toBeExecuted as $version) {
424
-			$this->executeStep($version, $schemaOnly);
425
-		}
426
-	}
427
-
428
-	/**
429
-	 * Applies all not yet applied versions up to $to
430
-	 *
431
-	 * @param string $to
432
-	 * @throws \InvalidArgumentException
433
-	 */
434
-	public function migrateSchemaOnly($to = 'latest') {
435
-		// read known migrations
436
-		$toBeExecuted = $this->getMigrationsToExecute($to);
437
-
438
-		if (empty($toBeExecuted)) {
439
-			return;
440
-		}
441
-
442
-		$toSchema = null;
443
-		foreach ($toBeExecuted as $version) {
444
-			$instance = $this->createInstance($version);
445
-
446
-			$toSchema = $instance->changeSchema($this->output, function () use ($toSchema) {
447
-				return $toSchema ?: new SchemaWrapper($this->connection);
448
-			}, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
449
-
450
-			$this->markAsExecuted($version);
451
-		}
452
-
453
-		if ($toSchema instanceof SchemaWrapper) {
454
-			$targetSchema = $toSchema->getWrappedSchema();
455
-			if ($this->checkOracle) {
456
-				$beforeSchema = $this->connection->createSchema();
457
-				$this->ensureOracleIdentifierLengthLimit($beforeSchema, $targetSchema, strlen($this->connection->getPrefix()));
458
-			}
459
-			$this->connection->migrateToSchema($targetSchema);
460
-			$toSchema->performDropTableCalls();
461
-		}
462
-	}
463
-
464
-	/**
465
-	 * Get the human readable descriptions for the migration steps to run
466
-	 *
467
-	 * @param string $to
468
-	 * @return string[] [$name => $description]
469
-	 */
470
-	public function describeMigrationStep($to = 'latest') {
471
-		$toBeExecuted = $this->getMigrationsToExecute($to);
472
-		$description = [];
473
-		foreach ($toBeExecuted as $version) {
474
-			$migration = $this->createInstance($version);
475
-			if ($migration->name()) {
476
-				$description[$migration->name()] = $migration->description();
477
-			}
478
-		}
479
-		return $description;
480
-	}
481
-
482
-	/**
483
-	 * @param string $version
484
-	 * @return IMigrationStep
485
-	 * @throws \InvalidArgumentException
486
-	 */
487
-	protected function createInstance($version) {
488
-		$class = $this->getClass($version);
489
-		try {
490
-			$s = \OC::$server->query($class);
491
-
492
-			if (!$s instanceof IMigrationStep) {
493
-				throw new \InvalidArgumentException('Not a valid migration');
494
-			}
495
-		} catch (QueryException $e) {
496
-			if (class_exists($class)) {
497
-				$s = new $class();
498
-			} else {
499
-				throw new \InvalidArgumentException("Migration step '$class' is unknown");
500
-			}
501
-		}
502
-
503
-		return $s;
504
-	}
505
-
506
-	/**
507
-	 * Executes one explicit version
508
-	 *
509
-	 * @param string $version
510
-	 * @param bool $schemaOnly
511
-	 * @throws \InvalidArgumentException
512
-	 */
513
-	public function executeStep($version, $schemaOnly = false) {
514
-		$instance = $this->createInstance($version);
515
-
516
-		if (!$schemaOnly) {
517
-			$instance->preSchemaChange($this->output, function () {
518
-				return new SchemaWrapper($this->connection);
519
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
520
-		}
521
-
522
-		$toSchema = $instance->changeSchema($this->output, function () {
523
-			return new SchemaWrapper($this->connection);
524
-		}, ['tablePrefix' => $this->connection->getPrefix()]);
525
-
526
-		if ($toSchema instanceof SchemaWrapper) {
527
-			$targetSchema = $toSchema->getWrappedSchema();
528
-			if ($this->checkOracle) {
529
-				$sourceSchema = $this->connection->createSchema();
530
-				$this->ensureOracleIdentifierLengthLimit($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
531
-			}
532
-			$this->connection->migrateToSchema($targetSchema);
533
-			$toSchema->performDropTableCalls();
534
-		}
535
-
536
-		if (!$schemaOnly) {
537
-			$instance->postSchemaChange($this->output, function () {
538
-				return new SchemaWrapper($this->connection);
539
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
540
-		}
541
-
542
-		$this->markAsExecuted($version);
543
-	}
544
-
545
-	public function ensureOracleIdentifierLengthLimit(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
546
-		$sequences = $targetSchema->getSequences();
547
-
548
-		foreach ($targetSchema->getTables() as $table) {
549
-			try {
550
-				$sourceTable = $sourceSchema->getTable($table->getName());
551
-			} catch (SchemaException $e) {
552
-				if (\strlen($table->getName()) - $prefixLength > 27) {
553
-					throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
554
-				}
555
-				$sourceTable = null;
556
-			}
557
-
558
-			foreach ($table->getColumns() as $thing) {
559
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
560
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
561
-				}
562
-
563
-				if ($thing->getNotnull() && $thing->getDefault() === ''
564
-					&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
565
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
566
-				}
567
-			}
568
-
569
-			foreach ($table->getIndexes() as $thing) {
570
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
571
-					throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
572
-				}
573
-			}
574
-
575
-			foreach ($table->getForeignKeys() as $thing) {
576
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
577
-					throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
578
-				}
579
-			}
580
-
581
-			$primaryKey = $table->getPrimaryKey();
582
-			if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
583
-				$indexName = strtolower($primaryKey->getName());
584
-				$isUsingDefaultName = $indexName === 'primary';
585
-
586
-				if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
587
-					$defaultName = $table->getName() . '_pkey';
588
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
589
-
590
-					if ($isUsingDefaultName) {
591
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
592
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
593
-							return $sequence->getName() !== $sequenceName;
594
-						});
595
-					}
596
-				} elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
597
-					$defaultName = $table->getName() . '_seq';
598
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
599
-				}
600
-
601
-				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
602
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
603
-				}
604
-				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
605
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
606
-				}
607
-			}
608
-		}
609
-
610
-		foreach ($sequences as $sequence) {
611
-			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
612
-				throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
613
-			}
614
-		}
615
-	}
616
-
617
-	private function ensureMigrationsAreLoaded() {
618
-		if (empty($this->migrations)) {
619
-			$this->migrations = $this->findMigrations();
620
-		}
621
-	}
49
+    /** @var boolean */
50
+    private $migrationTableCreated;
51
+    /** @var array */
52
+    private $migrations;
53
+    /** @var IOutput */
54
+    private $output;
55
+    /** @var Connection */
56
+    private $connection;
57
+    /** @var string */
58
+    private $appName;
59
+    /** @var bool */
60
+    private $checkOracle;
61
+
62
+    /**
63
+     * MigrationService constructor.
64
+     *
65
+     * @param $appName
66
+     * @param IDBConnection $connection
67
+     * @param AppLocator $appLocator
68
+     * @param IOutput|null $output
69
+     * @throws \Exception
70
+     */
71
+    public function __construct($appName, IDBConnection $connection, IOutput $output = null, AppLocator $appLocator = null) {
72
+        $this->appName = $appName;
73
+        $this->connection = $connection;
74
+        $this->output = $output;
75
+        if (null === $this->output) {
76
+            $this->output = new SimpleOutput(\OC::$server->getLogger(), $appName);
77
+        }
78
+
79
+        if ($appName === 'core') {
80
+            $this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
81
+            $this->migrationsNamespace = 'OC\\Core\\Migrations';
82
+            $this->checkOracle = true;
83
+        } else {
84
+            if (null === $appLocator) {
85
+                $appLocator = new AppLocator();
86
+            }
87
+            $appPath = $appLocator->getAppPath($appName);
88
+            $namespace = App::buildAppNamespace($appName);
89
+            $this->migrationsPath = "$appPath/lib/Migration";
90
+            $this->migrationsNamespace = $namespace . '\\Migration';
91
+
92
+            $infoParser = new InfoParser();
93
+            $info = $infoParser->parse($appPath . '/appinfo/info.xml');
94
+            if (!isset($info['dependencies']['database'])) {
95
+                $this->checkOracle = true;
96
+            } else {
97
+                $this->checkOracle = false;
98
+                foreach ($info['dependencies']['database'] as $database) {
99
+                    if (\is_string($database) && $database === 'oci') {
100
+                        $this->checkOracle = true;
101
+                    } elseif (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
102
+                        $this->checkOracle = true;
103
+                    }
104
+                }
105
+            }
106
+        }
107
+    }
108
+
109
+    /**
110
+     * Returns the name of the app for which this migration is executed
111
+     *
112
+     * @return string
113
+     */
114
+    public function getApp() {
115
+        return $this->appName;
116
+    }
117
+
118
+    /**
119
+     * @return bool
120
+     * @codeCoverageIgnore - this will implicitly tested on installation
121
+     */
122
+    private function createMigrationTable() {
123
+        if ($this->migrationTableCreated) {
124
+            return false;
125
+        }
126
+
127
+        if ($this->connection->tableExists('migrations')) {
128
+            $this->migrationTableCreated = true;
129
+            return false;
130
+        }
131
+
132
+        $schema = new SchemaWrapper($this->connection);
133
+
134
+        /**
135
+         * We drop the table when it has different columns or the definition does not
136
+         * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
137
+         */
138
+        try {
139
+            $table = $schema->getTable('migrations');
140
+            $columns = $table->getColumns();
141
+
142
+            if (count($columns) === 2) {
143
+                try {
144
+                    $column = $table->getColumn('app');
145
+                    $schemaMismatch = $column->getLength() !== 255;
146
+
147
+                    if (!$schemaMismatch) {
148
+                        $column = $table->getColumn('version');
149
+                        $schemaMismatch = $column->getLength() !== 255;
150
+                    }
151
+                } catch (SchemaException $e) {
152
+                    // One of the columns is missing
153
+                    $schemaMismatch = true;
154
+                }
155
+
156
+                if (!$schemaMismatch) {
157
+                    // Table exists and schema matches: return back!
158
+                    $this->migrationTableCreated = true;
159
+                    return false;
160
+                }
161
+            }
162
+
163
+            // Drop the table, when it didn't match our expectations.
164
+            $this->connection->dropTable('migrations');
165
+
166
+            // Recreate the schema after the table was dropped.
167
+            $schema = new SchemaWrapper($this->connection);
168
+        } catch (SchemaException $e) {
169
+            // Table not found, no need to panic, we will create it.
170
+        }
171
+
172
+        $table = $schema->createTable('migrations');
173
+        $table->addColumn('app', Types::STRING, ['length' => 255]);
174
+        $table->addColumn('version', Types::STRING, ['length' => 255]);
175
+        $table->setPrimaryKey(['app', 'version']);
176
+
177
+        $this->connection->migrateToSchema($schema->getWrappedSchema());
178
+
179
+        $this->migrationTableCreated = true;
180
+
181
+        return true;
182
+    }
183
+
184
+    /**
185
+     * Returns all versions which have already been applied
186
+     *
187
+     * @return string[]
188
+     * @codeCoverageIgnore - no need to test this
189
+     */
190
+    public function getMigratedVersions() {
191
+        $this->createMigrationTable();
192
+        $qb = $this->connection->getQueryBuilder();
193
+
194
+        $qb->select('version')
195
+            ->from('migrations')
196
+            ->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
197
+            ->orderBy('version');
198
+
199
+        $result = $qb->execute();
200
+        $rows = $result->fetchAll(\PDO::FETCH_COLUMN);
201
+        $result->closeCursor();
202
+
203
+        return $rows;
204
+    }
205
+
206
+    /**
207
+     * Returns all versions which are available in the migration folder
208
+     *
209
+     * @return array
210
+     */
211
+    public function getAvailableVersions() {
212
+        $this->ensureMigrationsAreLoaded();
213
+        return array_map('strval', array_keys($this->migrations));
214
+    }
215
+
216
+    protected function findMigrations() {
217
+        $directory = realpath($this->migrationsPath);
218
+        if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
219
+            return [];
220
+        }
221
+
222
+        $iterator = new \RegexIterator(
223
+            new \RecursiveIteratorIterator(
224
+                new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
225
+                \RecursiveIteratorIterator::LEAVES_ONLY
226
+            ),
227
+            '#^.+\\/Version[^\\/]{1,255}\\.php$#i',
228
+            \RegexIterator::GET_MATCH);
229
+
230
+        $files = array_keys(iterator_to_array($iterator));
231
+        uasort($files, function ($a, $b) {
232
+            preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
233
+            preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
234
+            if (!empty($matchA) && !empty($matchB)) {
235
+                if ($matchA[1] !== $matchB[1]) {
236
+                    return ($matchA[1] < $matchB[1]) ? -1 : 1;
237
+                }
238
+                return ($matchA[2] < $matchB[2]) ? -1 : 1;
239
+            }
240
+            return (basename($a) < basename($b)) ? -1 : 1;
241
+        });
242
+
243
+        $migrations = [];
244
+
245
+        foreach ($files as $file) {
246
+            $className = basename($file, '.php');
247
+            $version = (string) substr($className, 7);
248
+            if ($version === '0') {
249
+                throw new \InvalidArgumentException(
250
+                    "Cannot load a migrations with the name '$version' because it is a reserved number"
251
+                );
252
+            }
253
+            $migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
254
+        }
255
+
256
+        return $migrations;
257
+    }
258
+
259
+    /**
260
+     * @param string $to
261
+     * @return string[]
262
+     */
263
+    private function getMigrationsToExecute($to) {
264
+        $knownMigrations = $this->getMigratedVersions();
265
+        $availableMigrations = $this->getAvailableVersions();
266
+
267
+        $toBeExecuted = [];
268
+        foreach ($availableMigrations as $v) {
269
+            if ($to !== 'latest' && $v > $to) {
270
+                continue;
271
+            }
272
+            if ($this->shallBeExecuted($v, $knownMigrations)) {
273
+                $toBeExecuted[] = $v;
274
+            }
275
+        }
276
+
277
+        return $toBeExecuted;
278
+    }
279
+
280
+    /**
281
+     * @param string $m
282
+     * @param string[] $knownMigrations
283
+     * @return bool
284
+     */
285
+    private function shallBeExecuted($m, $knownMigrations) {
286
+        if (in_array($m, $knownMigrations)) {
287
+            return false;
288
+        }
289
+
290
+        return true;
291
+    }
292
+
293
+    /**
294
+     * @param string $version
295
+     */
296
+    private function markAsExecuted($version) {
297
+        $this->connection->insertIfNotExist('*PREFIX*migrations', [
298
+            'app' => $this->appName,
299
+            'version' => $version
300
+        ]);
301
+    }
302
+
303
+    /**
304
+     * Returns the name of the table which holds the already applied versions
305
+     *
306
+     * @return string
307
+     */
308
+    public function getMigrationsTableName() {
309
+        return $this->connection->getPrefix() . 'migrations';
310
+    }
311
+
312
+    /**
313
+     * Returns the namespace of the version classes
314
+     *
315
+     * @return string
316
+     */
317
+    public function getMigrationsNamespace() {
318
+        return $this->migrationsNamespace;
319
+    }
320
+
321
+    /**
322
+     * Returns the directory which holds the versions
323
+     *
324
+     * @return string
325
+     */
326
+    public function getMigrationsDirectory() {
327
+        return $this->migrationsPath;
328
+    }
329
+
330
+    /**
331
+     * Return the explicit version for the aliases; current, next, prev, latest
332
+     *
333
+     * @param string $alias
334
+     * @return mixed|null|string
335
+     */
336
+    public function getMigration($alias) {
337
+        switch ($alias) {
338
+            case 'current':
339
+                return $this->getCurrentVersion();
340
+            case 'next':
341
+                return $this->getRelativeVersion($this->getCurrentVersion(), 1);
342
+            case 'prev':
343
+                return $this->getRelativeVersion($this->getCurrentVersion(), -1);
344
+            case 'latest':
345
+                $this->ensureMigrationsAreLoaded();
346
+
347
+                $migrations = $this->getAvailableVersions();
348
+                return @end($migrations);
349
+        }
350
+        return '0';
351
+    }
352
+
353
+    /**
354
+     * @param string $version
355
+     * @param int $delta
356
+     * @return null|string
357
+     */
358
+    private function getRelativeVersion($version, $delta) {
359
+        $this->ensureMigrationsAreLoaded();
360
+
361
+        $versions = $this->getAvailableVersions();
362
+        array_unshift($versions, 0);
363
+        $offset = array_search($version, $versions, true);
364
+        if ($offset === false || !isset($versions[$offset + $delta])) {
365
+            // Unknown version or delta out of bounds.
366
+            return null;
367
+        }
368
+
369
+        return (string) $versions[$offset + $delta];
370
+    }
371
+
372
+    /**
373
+     * @return string
374
+     */
375
+    private function getCurrentVersion() {
376
+        $m = $this->getMigratedVersions();
377
+        if (count($m) === 0) {
378
+            return '0';
379
+        }
380
+        $migrations = array_values($m);
381
+        return @end($migrations);
382
+    }
383
+
384
+    /**
385
+     * @param string $version
386
+     * @return string
387
+     * @throws \InvalidArgumentException
388
+     */
389
+    private function getClass($version) {
390
+        $this->ensureMigrationsAreLoaded();
391
+
392
+        if (isset($this->migrations[$version])) {
393
+            return $this->migrations[$version];
394
+        }
395
+
396
+        throw new \InvalidArgumentException("Version $version is unknown.");
397
+    }
398
+
399
+    /**
400
+     * Allows to set an IOutput implementation which is used for logging progress and messages
401
+     *
402
+     * @param IOutput $output
403
+     */
404
+    public function setOutput(IOutput $output) {
405
+        $this->output = $output;
406
+    }
407
+
408
+    /**
409
+     * Applies all not yet applied versions up to $to
410
+     *
411
+     * @param string $to
412
+     * @param bool $schemaOnly
413
+     * @throws \InvalidArgumentException
414
+     */
415
+    public function migrate($to = 'latest', $schemaOnly = false) {
416
+        if ($schemaOnly) {
417
+            $this->migrateSchemaOnly($to);
418
+            return;
419
+        }
420
+
421
+        // read known migrations
422
+        $toBeExecuted = $this->getMigrationsToExecute($to);
423
+        foreach ($toBeExecuted as $version) {
424
+            $this->executeStep($version, $schemaOnly);
425
+        }
426
+    }
427
+
428
+    /**
429
+     * Applies all not yet applied versions up to $to
430
+     *
431
+     * @param string $to
432
+     * @throws \InvalidArgumentException
433
+     */
434
+    public function migrateSchemaOnly($to = 'latest') {
435
+        // read known migrations
436
+        $toBeExecuted = $this->getMigrationsToExecute($to);
437
+
438
+        if (empty($toBeExecuted)) {
439
+            return;
440
+        }
441
+
442
+        $toSchema = null;
443
+        foreach ($toBeExecuted as $version) {
444
+            $instance = $this->createInstance($version);
445
+
446
+            $toSchema = $instance->changeSchema($this->output, function () use ($toSchema) {
447
+                return $toSchema ?: new SchemaWrapper($this->connection);
448
+            }, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
449
+
450
+            $this->markAsExecuted($version);
451
+        }
452
+
453
+        if ($toSchema instanceof SchemaWrapper) {
454
+            $targetSchema = $toSchema->getWrappedSchema();
455
+            if ($this->checkOracle) {
456
+                $beforeSchema = $this->connection->createSchema();
457
+                $this->ensureOracleIdentifierLengthLimit($beforeSchema, $targetSchema, strlen($this->connection->getPrefix()));
458
+            }
459
+            $this->connection->migrateToSchema($targetSchema);
460
+            $toSchema->performDropTableCalls();
461
+        }
462
+    }
463
+
464
+    /**
465
+     * Get the human readable descriptions for the migration steps to run
466
+     *
467
+     * @param string $to
468
+     * @return string[] [$name => $description]
469
+     */
470
+    public function describeMigrationStep($to = 'latest') {
471
+        $toBeExecuted = $this->getMigrationsToExecute($to);
472
+        $description = [];
473
+        foreach ($toBeExecuted as $version) {
474
+            $migration = $this->createInstance($version);
475
+            if ($migration->name()) {
476
+                $description[$migration->name()] = $migration->description();
477
+            }
478
+        }
479
+        return $description;
480
+    }
481
+
482
+    /**
483
+     * @param string $version
484
+     * @return IMigrationStep
485
+     * @throws \InvalidArgumentException
486
+     */
487
+    protected function createInstance($version) {
488
+        $class = $this->getClass($version);
489
+        try {
490
+            $s = \OC::$server->query($class);
491
+
492
+            if (!$s instanceof IMigrationStep) {
493
+                throw new \InvalidArgumentException('Not a valid migration');
494
+            }
495
+        } catch (QueryException $e) {
496
+            if (class_exists($class)) {
497
+                $s = new $class();
498
+            } else {
499
+                throw new \InvalidArgumentException("Migration step '$class' is unknown");
500
+            }
501
+        }
502
+
503
+        return $s;
504
+    }
505
+
506
+    /**
507
+     * Executes one explicit version
508
+     *
509
+     * @param string $version
510
+     * @param bool $schemaOnly
511
+     * @throws \InvalidArgumentException
512
+     */
513
+    public function executeStep($version, $schemaOnly = false) {
514
+        $instance = $this->createInstance($version);
515
+
516
+        if (!$schemaOnly) {
517
+            $instance->preSchemaChange($this->output, function () {
518
+                return new SchemaWrapper($this->connection);
519
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
520
+        }
521
+
522
+        $toSchema = $instance->changeSchema($this->output, function () {
523
+            return new SchemaWrapper($this->connection);
524
+        }, ['tablePrefix' => $this->connection->getPrefix()]);
525
+
526
+        if ($toSchema instanceof SchemaWrapper) {
527
+            $targetSchema = $toSchema->getWrappedSchema();
528
+            if ($this->checkOracle) {
529
+                $sourceSchema = $this->connection->createSchema();
530
+                $this->ensureOracleIdentifierLengthLimit($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
531
+            }
532
+            $this->connection->migrateToSchema($targetSchema);
533
+            $toSchema->performDropTableCalls();
534
+        }
535
+
536
+        if (!$schemaOnly) {
537
+            $instance->postSchemaChange($this->output, function () {
538
+                return new SchemaWrapper($this->connection);
539
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
540
+        }
541
+
542
+        $this->markAsExecuted($version);
543
+    }
544
+
545
+    public function ensureOracleIdentifierLengthLimit(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
546
+        $sequences = $targetSchema->getSequences();
547
+
548
+        foreach ($targetSchema->getTables() as $table) {
549
+            try {
550
+                $sourceTable = $sourceSchema->getTable($table->getName());
551
+            } catch (SchemaException $e) {
552
+                if (\strlen($table->getName()) - $prefixLength > 27) {
553
+                    throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
554
+                }
555
+                $sourceTable = null;
556
+            }
557
+
558
+            foreach ($table->getColumns() as $thing) {
559
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
560
+                    throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
561
+                }
562
+
563
+                if ($thing->getNotnull() && $thing->getDefault() === ''
564
+                    && $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
565
+                    throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
566
+                }
567
+            }
568
+
569
+            foreach ($table->getIndexes() as $thing) {
570
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
571
+                    throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
572
+                }
573
+            }
574
+
575
+            foreach ($table->getForeignKeys() as $thing) {
576
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
577
+                    throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
578
+                }
579
+            }
580
+
581
+            $primaryKey = $table->getPrimaryKey();
582
+            if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
583
+                $indexName = strtolower($primaryKey->getName());
584
+                $isUsingDefaultName = $indexName === 'primary';
585
+
586
+                if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
587
+                    $defaultName = $table->getName() . '_pkey';
588
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
589
+
590
+                    if ($isUsingDefaultName) {
591
+                        $sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
592
+                        $sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
593
+                            return $sequence->getName() !== $sequenceName;
594
+                        });
595
+                    }
596
+                } elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
597
+                    $defaultName = $table->getName() . '_seq';
598
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
599
+                }
600
+
601
+                if (!$isUsingDefaultName && \strlen($indexName) > 30) {
602
+                    throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
603
+                }
604
+                if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
605
+                    throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
606
+                }
607
+            }
608
+        }
609
+
610
+        foreach ($sequences as $sequence) {
611
+            if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
612
+                throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
613
+            }
614
+        }
615
+    }
616
+
617
+    private function ensureMigrationsAreLoaded() {
618
+        if (empty($this->migrations)) {
619
+            $this->migrations = $this->findMigrations();
620
+        }
621
+    }
622 622
 }
Please login to merge, or discard this patch.
Spacing   +21 added lines, -21 removed lines patch added patch discarded remove patch
@@ -77,7 +77,7 @@  discard block
 block discarded – undo
77 77
 		}
78 78
 
79 79
 		if ($appName === 'core') {
80
-			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
80
+			$this->migrationsPath = \OC::$SERVERROOT.'/core/Migrations';
81 81
 			$this->migrationsNamespace = 'OC\\Core\\Migrations';
82 82
 			$this->checkOracle = true;
83 83
 		} else {
@@ -87,10 +87,10 @@  discard block
 block discarded – undo
87 87
 			$appPath = $appLocator->getAppPath($appName);
88 88
 			$namespace = App::buildAppNamespace($appName);
89 89
 			$this->migrationsPath = "$appPath/lib/Migration";
90
-			$this->migrationsNamespace = $namespace . '\\Migration';
90
+			$this->migrationsNamespace = $namespace.'\\Migration';
91 91
 
92 92
 			$infoParser = new InfoParser();
93
-			$info = $infoParser->parse($appPath . '/appinfo/info.xml');
93
+			$info = $infoParser->parse($appPath.'/appinfo/info.xml');
94 94
 			if (!isset($info['dependencies']['database'])) {
95 95
 				$this->checkOracle = true;
96 96
 			} else {
@@ -228,7 +228,7 @@  discard block
 block discarded – undo
228 228
 			\RegexIterator::GET_MATCH);
229 229
 
230 230
 		$files = array_keys(iterator_to_array($iterator));
231
-		uasort($files, function ($a, $b) {
231
+		uasort($files, function($a, $b) {
232 232
 			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
233 233
 			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
234 234
 			if (!empty($matchA) && !empty($matchB)) {
@@ -306,7 +306,7 @@  discard block
 block discarded – undo
306 306
 	 * @return string
307 307
 	 */
308 308
 	public function getMigrationsTableName() {
309
-		return $this->connection->getPrefix() . 'migrations';
309
+		return $this->connection->getPrefix().'migrations';
310 310
 	}
311 311
 
312 312
 	/**
@@ -443,7 +443,7 @@  discard block
 block discarded – undo
443 443
 		foreach ($toBeExecuted as $version) {
444 444
 			$instance = $this->createInstance($version);
445 445
 
446
-			$toSchema = $instance->changeSchema($this->output, function () use ($toSchema) {
446
+			$toSchema = $instance->changeSchema($this->output, function() use ($toSchema) {
447 447
 				return $toSchema ?: new SchemaWrapper($this->connection);
448 448
 			}, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
449 449
 
@@ -514,12 +514,12 @@  discard block
 block discarded – undo
514 514
 		$instance = $this->createInstance($version);
515 515
 
516 516
 		if (!$schemaOnly) {
517
-			$instance->preSchemaChange($this->output, function () {
517
+			$instance->preSchemaChange($this->output, function() {
518 518
 				return new SchemaWrapper($this->connection);
519 519
 			}, ['tablePrefix' => $this->connection->getPrefix()]);
520 520
 		}
521 521
 
522
-		$toSchema = $instance->changeSchema($this->output, function () {
522
+		$toSchema = $instance->changeSchema($this->output, function() {
523 523
 			return new SchemaWrapper($this->connection);
524 524
 		}, ['tablePrefix' => $this->connection->getPrefix()]);
525 525
 
@@ -534,7 +534,7 @@  discard block
 block discarded – undo
534 534
 		}
535 535
 
536 536
 		if (!$schemaOnly) {
537
-			$instance->postSchemaChange($this->output, function () {
537
+			$instance->postSchemaChange($this->output, function() {
538 538
 				return new SchemaWrapper($this->connection);
539 539
 			}, ['tablePrefix' => $this->connection->getPrefix()]);
540 540
 		}
@@ -550,31 +550,31 @@  discard block
 block discarded – undo
550 550
 				$sourceTable = $sourceSchema->getTable($table->getName());
551 551
 			} catch (SchemaException $e) {
552 552
 				if (\strlen($table->getName()) - $prefixLength > 27) {
553
-					throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
553
+					throw new \InvalidArgumentException('Table name "'.$table->getName().'" is too long.');
554 554
 				}
555 555
 				$sourceTable = null;
556 556
 			}
557 557
 
558 558
 			foreach ($table->getColumns() as $thing) {
559 559
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
560
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
560
+					throw new \InvalidArgumentException('Column name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
561 561
 				}
562 562
 
563 563
 				if ($thing->getNotnull() && $thing->getDefault() === ''
564 564
 					&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
565
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
565
+					throw new \InvalidArgumentException('Column name "'.$table->getName().'"."'.$thing->getName().'" is NotNull, but has empty string or null as default.');
566 566
 				}
567 567
 			}
568 568
 
569 569
 			foreach ($table->getIndexes() as $thing) {
570 570
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
571
-					throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
571
+					throw new \InvalidArgumentException('Index name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
572 572
 				}
573 573
 			}
574 574
 
575 575
 			foreach ($table->getForeignKeys() as $thing) {
576 576
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
577
-					throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
577
+					throw new \InvalidArgumentException('Foreign key name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
578 578
 				}
579 579
 			}
580 580
 
@@ -584,32 +584,32 @@  discard block
 block discarded – undo
584 584
 				$isUsingDefaultName = $indexName === 'primary';
585 585
 
586 586
 				if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
587
-					$defaultName = $table->getName() . '_pkey';
587
+					$defaultName = $table->getName().'_pkey';
588 588
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
589 589
 
590 590
 					if ($isUsingDefaultName) {
591
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
592
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
591
+						$sequenceName = $table->getName().'_'.implode('_', $primaryKey->getColumns()).'_seq';
592
+						$sequences = array_filter($sequences, function(Sequence $sequence) use ($sequenceName) {
593 593
 							return $sequence->getName() !== $sequenceName;
594 594
 						});
595 595
 					}
596 596
 				} elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
597
-					$defaultName = $table->getName() . '_seq';
597
+					$defaultName = $table->getName().'_seq';
598 598
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
599 599
 				}
600 600
 
601 601
 				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
602
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
602
+					throw new \InvalidArgumentException('Primary index name  on "'.$table->getName().'" is too long.');
603 603
 				}
604 604
 				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
605
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
605
+					throw new \InvalidArgumentException('Primary index name  on "'.$table->getName().'" is too long.');
606 606
 				}
607 607
 			}
608 608
 		}
609 609
 
610 610
 		foreach ($sequences as $sequence) {
611 611
 			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
612
-				throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
612
+				throw new \InvalidArgumentException('Sequence name "'.$sequence->getName().'" is too long.');
613 613
 			}
614 614
 		}
615 615
 	}
Please login to merge, or discard this patch.