Passed
Push — master ( 9db321...fec679 )
by Morris
10:50 queued 11s
created
lib/private/Setup/AbstractDatabase.php 1 patch
Indentation   +104 added lines, -104 removed lines patch added patch discarded remove patch
@@ -35,118 +35,118 @@
 block discarded – undo
35 35
 
36 36
 abstract class AbstractDatabase {
37 37
 
38
-	/** @var IL10N */
39
-	protected $trans;
40
-	/** @var string */
41
-	protected $dbUser;
42
-	/** @var string */
43
-	protected $dbPassword;
44
-	/** @var string */
45
-	protected $dbName;
46
-	/** @var string */
47
-	protected $dbHost;
48
-	/** @var string */
49
-	protected $dbPort;
50
-	/** @var string */
51
-	protected $tablePrefix;
52
-	/** @var SystemConfig */
53
-	protected $config;
54
-	/** @var ILogger */
55
-	protected $logger;
56
-	/** @var ISecureRandom */
57
-	protected $random;
38
+    /** @var IL10N */
39
+    protected $trans;
40
+    /** @var string */
41
+    protected $dbUser;
42
+    /** @var string */
43
+    protected $dbPassword;
44
+    /** @var string */
45
+    protected $dbName;
46
+    /** @var string */
47
+    protected $dbHost;
48
+    /** @var string */
49
+    protected $dbPort;
50
+    /** @var string */
51
+    protected $tablePrefix;
52
+    /** @var SystemConfig */
53
+    protected $config;
54
+    /** @var ILogger */
55
+    protected $logger;
56
+    /** @var ISecureRandom */
57
+    protected $random;
58 58
 
59
-	public function __construct(IL10N $trans, SystemConfig $config, ILogger $logger, ISecureRandom $random) {
60
-		$this->trans = $trans;
61
-		$this->config = $config;
62
-		$this->logger = $logger;
63
-		$this->random = $random;
64
-	}
59
+    public function __construct(IL10N $trans, SystemConfig $config, ILogger $logger, ISecureRandom $random) {
60
+        $this->trans = $trans;
61
+        $this->config = $config;
62
+        $this->logger = $logger;
63
+        $this->random = $random;
64
+    }
65 65
 
66
-	public function validate($config) {
67
-		$errors = [];
68
-		if (empty($config['dbuser']) && empty($config['dbname'])) {
69
-			$errors[] = $this->trans->t("%s enter the database username and name.", [$this->dbprettyname]);
70
-		} elseif (empty($config['dbuser'])) {
71
-			$errors[] = $this->trans->t("%s enter the database username.", [$this->dbprettyname]);
72
-		} elseif (empty($config['dbname'])) {
73
-			$errors[] = $this->trans->t("%s enter the database name.", [$this->dbprettyname]);
74
-		}
75
-		if (substr_count($config['dbname'], '.') >= 1) {
76
-			$errors[] = $this->trans->t("%s you may not use dots in the database name", [$this->dbprettyname]);
77
-		}
78
-		return $errors;
79
-	}
66
+    public function validate($config) {
67
+        $errors = [];
68
+        if (empty($config['dbuser']) && empty($config['dbname'])) {
69
+            $errors[] = $this->trans->t("%s enter the database username and name.", [$this->dbprettyname]);
70
+        } elseif (empty($config['dbuser'])) {
71
+            $errors[] = $this->trans->t("%s enter the database username.", [$this->dbprettyname]);
72
+        } elseif (empty($config['dbname'])) {
73
+            $errors[] = $this->trans->t("%s enter the database name.", [$this->dbprettyname]);
74
+        }
75
+        if (substr_count($config['dbname'], '.') >= 1) {
76
+            $errors[] = $this->trans->t("%s you may not use dots in the database name", [$this->dbprettyname]);
77
+        }
78
+        return $errors;
79
+    }
80 80
 
81
-	public function initialize($config) {
82
-		$dbUser = $config['dbuser'];
83
-		$dbPass = $config['dbpass'];
84
-		$dbName = $config['dbname'];
85
-		$dbHost = !empty($config['dbhost']) ? $config['dbhost'] : 'localhost';
86
-		$dbPort = !empty($config['dbport']) ? $config['dbport'] : '';
87
-		$dbTablePrefix = isset($config['dbtableprefix']) ? $config['dbtableprefix'] : 'oc_';
81
+    public function initialize($config) {
82
+        $dbUser = $config['dbuser'];
83
+        $dbPass = $config['dbpass'];
84
+        $dbName = $config['dbname'];
85
+        $dbHost = !empty($config['dbhost']) ? $config['dbhost'] : 'localhost';
86
+        $dbPort = !empty($config['dbport']) ? $config['dbport'] : '';
87
+        $dbTablePrefix = isset($config['dbtableprefix']) ? $config['dbtableprefix'] : 'oc_';
88 88
 
89
-		$this->config->setValues([
90
-			'dbname' => $dbName,
91
-			'dbhost' => $dbHost,
92
-			'dbport' => $dbPort,
93
-			'dbtableprefix' => $dbTablePrefix,
94
-		]);
89
+        $this->config->setValues([
90
+            'dbname' => $dbName,
91
+            'dbhost' => $dbHost,
92
+            'dbport' => $dbPort,
93
+            'dbtableprefix' => $dbTablePrefix,
94
+        ]);
95 95
 
96
-		$this->dbUser = $dbUser;
97
-		$this->dbPassword = $dbPass;
98
-		$this->dbName = $dbName;
99
-		$this->dbHost = $dbHost;
100
-		$this->dbPort = $dbPort;
101
-		$this->tablePrefix = $dbTablePrefix;
102
-	}
96
+        $this->dbUser = $dbUser;
97
+        $this->dbPassword = $dbPass;
98
+        $this->dbName = $dbName;
99
+        $this->dbHost = $dbHost;
100
+        $this->dbPort = $dbPort;
101
+        $this->tablePrefix = $dbTablePrefix;
102
+    }
103 103
 
104
-	/**
105
-	 * @param array $configOverwrite
106
-	 * @return \OC\DB\Connection
107
-	 */
108
-	protected function connect(array $configOverwrite = []) {
109
-		$connectionParams = [
110
-			'host' => $this->dbHost,
111
-			'user' => $this->dbUser,
112
-			'password' => $this->dbPassword,
113
-			'tablePrefix' => $this->tablePrefix,
114
-			'dbname' => $this->dbName
115
-		];
104
+    /**
105
+     * @param array $configOverwrite
106
+     * @return \OC\DB\Connection
107
+     */
108
+    protected function connect(array $configOverwrite = []) {
109
+        $connectionParams = [
110
+            'host' => $this->dbHost,
111
+            'user' => $this->dbUser,
112
+            'password' => $this->dbPassword,
113
+            'tablePrefix' => $this->tablePrefix,
114
+            'dbname' => $this->dbName
115
+        ];
116 116
 
117
-		// adding port support through installer
118
-		if (!empty($this->dbPort)) {
119
-			if (ctype_digit($this->dbPort)) {
120
-				$connectionParams['port'] = $this->dbPort;
121
-			} else {
122
-				$connectionParams['unix_socket'] = $this->dbPort;
123
-			}
124
-		} elseif (strpos($this->dbHost, ':')) {
125
-			// Host variable may carry a port or socket.
126
-			list($host, $portOrSocket) = explode(':', $this->dbHost, 2);
127
-			if (ctype_digit($portOrSocket)) {
128
-				$connectionParams['port'] = $portOrSocket;
129
-			} else {
130
-				$connectionParams['unix_socket'] = $portOrSocket;
131
-			}
132
-			$connectionParams['host'] = $host;
133
-		}
117
+        // adding port support through installer
118
+        if (!empty($this->dbPort)) {
119
+            if (ctype_digit($this->dbPort)) {
120
+                $connectionParams['port'] = $this->dbPort;
121
+            } else {
122
+                $connectionParams['unix_socket'] = $this->dbPort;
123
+            }
124
+        } elseif (strpos($this->dbHost, ':')) {
125
+            // Host variable may carry a port or socket.
126
+            list($host, $portOrSocket) = explode(':', $this->dbHost, 2);
127
+            if (ctype_digit($portOrSocket)) {
128
+                $connectionParams['port'] = $portOrSocket;
129
+            } else {
130
+                $connectionParams['unix_socket'] = $portOrSocket;
131
+            }
132
+            $connectionParams['host'] = $host;
133
+        }
134 134
 
135
-		$connectionParams = array_merge($connectionParams, $configOverwrite);
136
-		$cf = new ConnectionFactory($this->config);
137
-		return $cf->getConnection($this->config->getValue('dbtype', 'sqlite'), $connectionParams);
138
-	}
135
+        $connectionParams = array_merge($connectionParams, $configOverwrite);
136
+        $cf = new ConnectionFactory($this->config);
137
+        return $cf->getConnection($this->config->getValue('dbtype', 'sqlite'), $connectionParams);
138
+    }
139 139
 
140
-	/**
141
-	 * @param string $userName
142
-	 */
143
-	abstract public function setupDatabase($userName);
140
+    /**
141
+     * @param string $userName
142
+     */
143
+    abstract public function setupDatabase($userName);
144 144
 
145
-	public function runMigrations() {
146
-		if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) {
147
-			return;
148
-		}
149
-		$ms = new MigrationService('core', \OC::$server->getDatabaseConnection());
150
-		$ms->migrate();
151
-	}
145
+    public function runMigrations() {
146
+        if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) {
147
+            return;
148
+        }
149
+        $ms = new MigrationService('core', \OC::$server->getDatabaseConnection());
150
+        $ms->migrate();
151
+    }
152 152
 }
Please login to merge, or discard this patch.
lib/private/DB/MigrationService.php 2 patches
Indentation   +527 added lines, -527 removed lines patch added patch discarded remove patch
@@ -46,531 +46,531 @@
 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
-		$schema = new SchemaWrapper($this->connection);
128
-
129
-		/**
130
-		 * We drop the table when it has different columns or the definition does not
131
-		 * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
132
-		 */
133
-		try {
134
-			$table = $schema->getTable('migrations');
135
-			$columns = $table->getColumns();
136
-
137
-			if (count($columns) === 2) {
138
-				try {
139
-					$column = $table->getColumn('app');
140
-					$schemaMismatch = $column->getLength() !== 255;
141
-
142
-					if (!$schemaMismatch) {
143
-						$column = $table->getColumn('version');
144
-						$schemaMismatch = $column->getLength() !== 255;
145
-					}
146
-				} catch (SchemaException $e) {
147
-					// One of the columns is missing
148
-					$schemaMismatch = true;
149
-				}
150
-
151
-				if (!$schemaMismatch) {
152
-					// Table exists and schema matches: return back!
153
-					$this->migrationTableCreated = true;
154
-					return false;
155
-				}
156
-			}
157
-
158
-			// Drop the table, when it didn't match our expectations.
159
-			$this->connection->dropTable('migrations');
160
-
161
-			// Recreate the schema after the table was dropped.
162
-			$schema = new SchemaWrapper($this->connection);
163
-		} catch (SchemaException $e) {
164
-			// Table not found, no need to panic, we will create it.
165
-		}
166
-
167
-		$table = $schema->createTable('migrations');
168
-		$table->addColumn('app', Types::STRING, ['length' => 255]);
169
-		$table->addColumn('version', Types::STRING, ['length' => 255]);
170
-		$table->setPrimaryKey(['app', 'version']);
171
-
172
-		$this->connection->migrateToSchema($schema->getWrappedSchema());
173
-
174
-		$this->migrationTableCreated = true;
175
-
176
-		return true;
177
-	}
178
-
179
-	/**
180
-	 * Returns all versions which have already been applied
181
-	 *
182
-	 * @return string[]
183
-	 * @codeCoverageIgnore - no need to test this
184
-	 */
185
-	public function getMigratedVersions() {
186
-		$this->createMigrationTable();
187
-		$qb = $this->connection->getQueryBuilder();
188
-
189
-		$qb->select('version')
190
-			->from('migrations')
191
-			->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
192
-			->orderBy('version');
193
-
194
-		$result = $qb->execute();
195
-		$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
196
-		$result->closeCursor();
197
-
198
-		return $rows;
199
-	}
200
-
201
-	/**
202
-	 * Returns all versions which are available in the migration folder
203
-	 *
204
-	 * @return array
205
-	 */
206
-	public function getAvailableVersions() {
207
-		$this->ensureMigrationsAreLoaded();
208
-		return array_map('strval', array_keys($this->migrations));
209
-	}
210
-
211
-	protected function findMigrations() {
212
-		$directory = realpath($this->migrationsPath);
213
-		if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
214
-			return [];
215
-		}
216
-
217
-		$iterator = new \RegexIterator(
218
-			new \RecursiveIteratorIterator(
219
-				new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
220
-				\RecursiveIteratorIterator::LEAVES_ONLY
221
-			),
222
-			'#^.+\\/Version[^\\/]{1,255}\\.php$#i',
223
-			\RegexIterator::GET_MATCH);
224
-
225
-		$files = array_keys(iterator_to_array($iterator));
226
-		uasort($files, function ($a, $b) {
227
-			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
228
-			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
229
-			if (!empty($matchA) && !empty($matchB)) {
230
-				if ($matchA[1] !== $matchB[1]) {
231
-					return ($matchA[1] < $matchB[1]) ? -1 : 1;
232
-				}
233
-				return ($matchA[2] < $matchB[2]) ? -1 : 1;
234
-			}
235
-			return (basename($a) < basename($b)) ? -1 : 1;
236
-		});
237
-
238
-		$migrations = [];
239
-
240
-		foreach ($files as $file) {
241
-			$className = basename($file, '.php');
242
-			$version = (string) substr($className, 7);
243
-			if ($version === '0') {
244
-				throw new \InvalidArgumentException(
245
-					"Cannot load a migrations with the name '$version' because it is a reserved number"
246
-				);
247
-			}
248
-			$migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
249
-		}
250
-
251
-		return $migrations;
252
-	}
253
-
254
-	/**
255
-	 * @param string $to
256
-	 * @return string[]
257
-	 */
258
-	private function getMigrationsToExecute($to) {
259
-		$knownMigrations = $this->getMigratedVersions();
260
-		$availableMigrations = $this->getAvailableVersions();
261
-
262
-		$toBeExecuted = [];
263
-		foreach ($availableMigrations as $v) {
264
-			if ($to !== 'latest' && $v > $to) {
265
-				continue;
266
-			}
267
-			if ($this->shallBeExecuted($v, $knownMigrations)) {
268
-				$toBeExecuted[] = $v;
269
-			}
270
-		}
271
-
272
-		return $toBeExecuted;
273
-	}
274
-
275
-	/**
276
-	 * @param string $m
277
-	 * @param string[] $knownMigrations
278
-	 * @return bool
279
-	 */
280
-	private function shallBeExecuted($m, $knownMigrations) {
281
-		if (in_array($m, $knownMigrations)) {
282
-			return false;
283
-		}
284
-
285
-		return true;
286
-	}
287
-
288
-	/**
289
-	 * @param string $version
290
-	 */
291
-	private function markAsExecuted($version) {
292
-		$this->connection->insertIfNotExist('*PREFIX*migrations', [
293
-			'app' => $this->appName,
294
-			'version' => $version
295
-		]);
296
-	}
297
-
298
-	/**
299
-	 * Returns the name of the table which holds the already applied versions
300
-	 *
301
-	 * @return string
302
-	 */
303
-	public function getMigrationsTableName() {
304
-		return $this->connection->getPrefix() . 'migrations';
305
-	}
306
-
307
-	/**
308
-	 * Returns the namespace of the version classes
309
-	 *
310
-	 * @return string
311
-	 */
312
-	public function getMigrationsNamespace() {
313
-		return $this->migrationsNamespace;
314
-	}
315
-
316
-	/**
317
-	 * Returns the directory which holds the versions
318
-	 *
319
-	 * @return string
320
-	 */
321
-	public function getMigrationsDirectory() {
322
-		return $this->migrationsPath;
323
-	}
324
-
325
-	/**
326
-	 * Return the explicit version for the aliases; current, next, prev, latest
327
-	 *
328
-	 * @param string $alias
329
-	 * @return mixed|null|string
330
-	 */
331
-	public function getMigration($alias) {
332
-		switch ($alias) {
333
-			case 'current':
334
-				return $this->getCurrentVersion();
335
-			case 'next':
336
-				return $this->getRelativeVersion($this->getCurrentVersion(), 1);
337
-			case 'prev':
338
-				return $this->getRelativeVersion($this->getCurrentVersion(), -1);
339
-			case 'latest':
340
-				$this->ensureMigrationsAreLoaded();
341
-
342
-				$migrations = $this->getAvailableVersions();
343
-				return @end($migrations);
344
-		}
345
-		return '0';
346
-	}
347
-
348
-	/**
349
-	 * @param string $version
350
-	 * @param int $delta
351
-	 * @return null|string
352
-	 */
353
-	private function getRelativeVersion($version, $delta) {
354
-		$this->ensureMigrationsAreLoaded();
355
-
356
-		$versions = $this->getAvailableVersions();
357
-		array_unshift($versions, 0);
358
-		$offset = array_search($version, $versions, true);
359
-		if ($offset === false || !isset($versions[$offset + $delta])) {
360
-			// Unknown version or delta out of bounds.
361
-			return null;
362
-		}
363
-
364
-		return (string) $versions[$offset + $delta];
365
-	}
366
-
367
-	/**
368
-	 * @return string
369
-	 */
370
-	private function getCurrentVersion() {
371
-		$m = $this->getMigratedVersions();
372
-		if (count($m) === 0) {
373
-			return '0';
374
-		}
375
-		$migrations = array_values($m);
376
-		return @end($migrations);
377
-	}
378
-
379
-	/**
380
-	 * @param string $version
381
-	 * @return string
382
-	 * @throws \InvalidArgumentException
383
-	 */
384
-	private function getClass($version) {
385
-		$this->ensureMigrationsAreLoaded();
386
-
387
-		if (isset($this->migrations[$version])) {
388
-			return $this->migrations[$version];
389
-		}
390
-
391
-		throw new \InvalidArgumentException("Version $version is unknown.");
392
-	}
393
-
394
-	/**
395
-	 * Allows to set an IOutput implementation which is used for logging progress and messages
396
-	 *
397
-	 * @param IOutput $output
398
-	 */
399
-	public function setOutput(IOutput $output) {
400
-		$this->output = $output;
401
-	}
402
-
403
-	/**
404
-	 * Applies all not yet applied versions up to $to
405
-	 *
406
-	 * @param string $to
407
-	 * @param bool $schemaOnly
408
-	 * @throws \InvalidArgumentException
409
-	 */
410
-	public function migrate($to = 'latest', $schemaOnly = false) {
411
-		// read known migrations
412
-		$toBeExecuted = $this->getMigrationsToExecute($to);
413
-		foreach ($toBeExecuted as $version) {
414
-			$this->executeStep($version, $schemaOnly);
415
-		}
416
-	}
417
-
418
-	/**
419
-	 * Get the human readable descriptions for the migration steps to run
420
-	 *
421
-	 * @param string $to
422
-	 * @return string[] [$name => $description]
423
-	 */
424
-	public function describeMigrationStep($to = 'latest') {
425
-		$toBeExecuted = $this->getMigrationsToExecute($to);
426
-		$description = [];
427
-		foreach ($toBeExecuted as $version) {
428
-			$migration = $this->createInstance($version);
429
-			if ($migration->name()) {
430
-				$description[$migration->name()] = $migration->description();
431
-			}
432
-		}
433
-		return $description;
434
-	}
435
-
436
-	/**
437
-	 * @param string $version
438
-	 * @return IMigrationStep
439
-	 * @throws \InvalidArgumentException
440
-	 */
441
-	protected function createInstance($version) {
442
-		$class = $this->getClass($version);
443
-		try {
444
-			$s = \OC::$server->query($class);
445
-
446
-			if (!$s instanceof IMigrationStep) {
447
-				throw new \InvalidArgumentException('Not a valid migration');
448
-			}
449
-		} catch (QueryException $e) {
450
-			if (class_exists($class)) {
451
-				$s = new $class();
452
-			} else {
453
-				throw new \InvalidArgumentException("Migration step '$class' is unknown");
454
-			}
455
-		}
456
-
457
-		return $s;
458
-	}
459
-
460
-	/**
461
-	 * Executes one explicit version
462
-	 *
463
-	 * @param string $version
464
-	 * @param bool $schemaOnly
465
-	 * @throws \InvalidArgumentException
466
-	 */
467
-	public function executeStep($version, $schemaOnly = false) {
468
-		$instance = $this->createInstance($version);
469
-
470
-		if (!$schemaOnly) {
471
-			$instance->preSchemaChange($this->output, function () {
472
-				return new SchemaWrapper($this->connection);
473
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
474
-		}
475
-
476
-		$toSchema = $instance->changeSchema($this->output, function () {
477
-			return new SchemaWrapper($this->connection);
478
-		}, ['tablePrefix' => $this->connection->getPrefix()]);
479
-
480
-		if ($toSchema instanceof SchemaWrapper) {
481
-			$targetSchema = $toSchema->getWrappedSchema();
482
-			if ($this->checkOracle) {
483
-				$sourceSchema = $this->connection->createSchema();
484
-				$this->ensureOracleIdentifierLengthLimit($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
485
-			}
486
-			$this->connection->migrateToSchema($targetSchema);
487
-			$toSchema->performDropTableCalls();
488
-		}
489
-
490
-		if (!$schemaOnly) {
491
-			$instance->postSchemaChange($this->output, function () {
492
-				return new SchemaWrapper($this->connection);
493
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
494
-		}
495
-
496
-		$this->markAsExecuted($version);
497
-	}
498
-
499
-	public function ensureOracleIdentifierLengthLimit(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
500
-		$sequences = $targetSchema->getSequences();
501
-
502
-		foreach ($targetSchema->getTables() as $table) {
503
-			try {
504
-				$sourceTable = $sourceSchema->getTable($table->getName());
505
-			} catch (SchemaException $e) {
506
-				if (\strlen($table->getName()) - $prefixLength > 27) {
507
-					throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
508
-				}
509
-				$sourceTable = null;
510
-			}
511
-
512
-			foreach ($table->getColumns() as $thing) {
513
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
514
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
515
-				}
516
-
517
-				if ($thing->getNotnull() && $thing->getDefault() === ''
518
-					&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
519
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
520
-				}
521
-			}
522
-
523
-			foreach ($table->getIndexes() as $thing) {
524
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
525
-					throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
526
-				}
527
-			}
528
-
529
-			foreach ($table->getForeignKeys() as $thing) {
530
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
531
-					throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
532
-				}
533
-			}
534
-
535
-			$primaryKey = $table->getPrimaryKey();
536
-			if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
537
-				$indexName = strtolower($primaryKey->getName());
538
-				$isUsingDefaultName = $indexName === 'primary';
539
-
540
-				if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
541
-					$defaultName = $table->getName() . '_pkey';
542
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
543
-
544
-					if ($isUsingDefaultName) {
545
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
546
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
547
-							return $sequence->getName() !== $sequenceName;
548
-						});
549
-					}
550
-				} elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
551
-					$defaultName = $table->getName() . '_seq';
552
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
553
-				}
554
-
555
-				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
556
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
557
-				}
558
-				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
559
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
560
-				}
561
-			}
562
-		}
563
-
564
-		foreach ($sequences as $sequence) {
565
-			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
566
-				throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
567
-			}
568
-		}
569
-	}
570
-
571
-	private function ensureMigrationsAreLoaded() {
572
-		if (empty($this->migrations)) {
573
-			$this->migrations = $this->findMigrations();
574
-		}
575
-	}
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
+        $schema = new SchemaWrapper($this->connection);
128
+
129
+        /**
130
+         * We drop the table when it has different columns or the definition does not
131
+         * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
132
+         */
133
+        try {
134
+            $table = $schema->getTable('migrations');
135
+            $columns = $table->getColumns();
136
+
137
+            if (count($columns) === 2) {
138
+                try {
139
+                    $column = $table->getColumn('app');
140
+                    $schemaMismatch = $column->getLength() !== 255;
141
+
142
+                    if (!$schemaMismatch) {
143
+                        $column = $table->getColumn('version');
144
+                        $schemaMismatch = $column->getLength() !== 255;
145
+                    }
146
+                } catch (SchemaException $e) {
147
+                    // One of the columns is missing
148
+                    $schemaMismatch = true;
149
+                }
150
+
151
+                if (!$schemaMismatch) {
152
+                    // Table exists and schema matches: return back!
153
+                    $this->migrationTableCreated = true;
154
+                    return false;
155
+                }
156
+            }
157
+
158
+            // Drop the table, when it didn't match our expectations.
159
+            $this->connection->dropTable('migrations');
160
+
161
+            // Recreate the schema after the table was dropped.
162
+            $schema = new SchemaWrapper($this->connection);
163
+        } catch (SchemaException $e) {
164
+            // Table not found, no need to panic, we will create it.
165
+        }
166
+
167
+        $table = $schema->createTable('migrations');
168
+        $table->addColumn('app', Types::STRING, ['length' => 255]);
169
+        $table->addColumn('version', Types::STRING, ['length' => 255]);
170
+        $table->setPrimaryKey(['app', 'version']);
171
+
172
+        $this->connection->migrateToSchema($schema->getWrappedSchema());
173
+
174
+        $this->migrationTableCreated = true;
175
+
176
+        return true;
177
+    }
178
+
179
+    /**
180
+     * Returns all versions which have already been applied
181
+     *
182
+     * @return string[]
183
+     * @codeCoverageIgnore - no need to test this
184
+     */
185
+    public function getMigratedVersions() {
186
+        $this->createMigrationTable();
187
+        $qb = $this->connection->getQueryBuilder();
188
+
189
+        $qb->select('version')
190
+            ->from('migrations')
191
+            ->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
192
+            ->orderBy('version');
193
+
194
+        $result = $qb->execute();
195
+        $rows = $result->fetchAll(\PDO::FETCH_COLUMN);
196
+        $result->closeCursor();
197
+
198
+        return $rows;
199
+    }
200
+
201
+    /**
202
+     * Returns all versions which are available in the migration folder
203
+     *
204
+     * @return array
205
+     */
206
+    public function getAvailableVersions() {
207
+        $this->ensureMigrationsAreLoaded();
208
+        return array_map('strval', array_keys($this->migrations));
209
+    }
210
+
211
+    protected function findMigrations() {
212
+        $directory = realpath($this->migrationsPath);
213
+        if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
214
+            return [];
215
+        }
216
+
217
+        $iterator = new \RegexIterator(
218
+            new \RecursiveIteratorIterator(
219
+                new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
220
+                \RecursiveIteratorIterator::LEAVES_ONLY
221
+            ),
222
+            '#^.+\\/Version[^\\/]{1,255}\\.php$#i',
223
+            \RegexIterator::GET_MATCH);
224
+
225
+        $files = array_keys(iterator_to_array($iterator));
226
+        uasort($files, function ($a, $b) {
227
+            preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
228
+            preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
229
+            if (!empty($matchA) && !empty($matchB)) {
230
+                if ($matchA[1] !== $matchB[1]) {
231
+                    return ($matchA[1] < $matchB[1]) ? -1 : 1;
232
+                }
233
+                return ($matchA[2] < $matchB[2]) ? -1 : 1;
234
+            }
235
+            return (basename($a) < basename($b)) ? -1 : 1;
236
+        });
237
+
238
+        $migrations = [];
239
+
240
+        foreach ($files as $file) {
241
+            $className = basename($file, '.php');
242
+            $version = (string) substr($className, 7);
243
+            if ($version === '0') {
244
+                throw new \InvalidArgumentException(
245
+                    "Cannot load a migrations with the name '$version' because it is a reserved number"
246
+                );
247
+            }
248
+            $migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
249
+        }
250
+
251
+        return $migrations;
252
+    }
253
+
254
+    /**
255
+     * @param string $to
256
+     * @return string[]
257
+     */
258
+    private function getMigrationsToExecute($to) {
259
+        $knownMigrations = $this->getMigratedVersions();
260
+        $availableMigrations = $this->getAvailableVersions();
261
+
262
+        $toBeExecuted = [];
263
+        foreach ($availableMigrations as $v) {
264
+            if ($to !== 'latest' && $v > $to) {
265
+                continue;
266
+            }
267
+            if ($this->shallBeExecuted($v, $knownMigrations)) {
268
+                $toBeExecuted[] = $v;
269
+            }
270
+        }
271
+
272
+        return $toBeExecuted;
273
+    }
274
+
275
+    /**
276
+     * @param string $m
277
+     * @param string[] $knownMigrations
278
+     * @return bool
279
+     */
280
+    private function shallBeExecuted($m, $knownMigrations) {
281
+        if (in_array($m, $knownMigrations)) {
282
+            return false;
283
+        }
284
+
285
+        return true;
286
+    }
287
+
288
+    /**
289
+     * @param string $version
290
+     */
291
+    private function markAsExecuted($version) {
292
+        $this->connection->insertIfNotExist('*PREFIX*migrations', [
293
+            'app' => $this->appName,
294
+            'version' => $version
295
+        ]);
296
+    }
297
+
298
+    /**
299
+     * Returns the name of the table which holds the already applied versions
300
+     *
301
+     * @return string
302
+     */
303
+    public function getMigrationsTableName() {
304
+        return $this->connection->getPrefix() . 'migrations';
305
+    }
306
+
307
+    /**
308
+     * Returns the namespace of the version classes
309
+     *
310
+     * @return string
311
+     */
312
+    public function getMigrationsNamespace() {
313
+        return $this->migrationsNamespace;
314
+    }
315
+
316
+    /**
317
+     * Returns the directory which holds the versions
318
+     *
319
+     * @return string
320
+     */
321
+    public function getMigrationsDirectory() {
322
+        return $this->migrationsPath;
323
+    }
324
+
325
+    /**
326
+     * Return the explicit version for the aliases; current, next, prev, latest
327
+     *
328
+     * @param string $alias
329
+     * @return mixed|null|string
330
+     */
331
+    public function getMigration($alias) {
332
+        switch ($alias) {
333
+            case 'current':
334
+                return $this->getCurrentVersion();
335
+            case 'next':
336
+                return $this->getRelativeVersion($this->getCurrentVersion(), 1);
337
+            case 'prev':
338
+                return $this->getRelativeVersion($this->getCurrentVersion(), -1);
339
+            case 'latest':
340
+                $this->ensureMigrationsAreLoaded();
341
+
342
+                $migrations = $this->getAvailableVersions();
343
+                return @end($migrations);
344
+        }
345
+        return '0';
346
+    }
347
+
348
+    /**
349
+     * @param string $version
350
+     * @param int $delta
351
+     * @return null|string
352
+     */
353
+    private function getRelativeVersion($version, $delta) {
354
+        $this->ensureMigrationsAreLoaded();
355
+
356
+        $versions = $this->getAvailableVersions();
357
+        array_unshift($versions, 0);
358
+        $offset = array_search($version, $versions, true);
359
+        if ($offset === false || !isset($versions[$offset + $delta])) {
360
+            // Unknown version or delta out of bounds.
361
+            return null;
362
+        }
363
+
364
+        return (string) $versions[$offset + $delta];
365
+    }
366
+
367
+    /**
368
+     * @return string
369
+     */
370
+    private function getCurrentVersion() {
371
+        $m = $this->getMigratedVersions();
372
+        if (count($m) === 0) {
373
+            return '0';
374
+        }
375
+        $migrations = array_values($m);
376
+        return @end($migrations);
377
+    }
378
+
379
+    /**
380
+     * @param string $version
381
+     * @return string
382
+     * @throws \InvalidArgumentException
383
+     */
384
+    private function getClass($version) {
385
+        $this->ensureMigrationsAreLoaded();
386
+
387
+        if (isset($this->migrations[$version])) {
388
+            return $this->migrations[$version];
389
+        }
390
+
391
+        throw new \InvalidArgumentException("Version $version is unknown.");
392
+    }
393
+
394
+    /**
395
+     * Allows to set an IOutput implementation which is used for logging progress and messages
396
+     *
397
+     * @param IOutput $output
398
+     */
399
+    public function setOutput(IOutput $output) {
400
+        $this->output = $output;
401
+    }
402
+
403
+    /**
404
+     * Applies all not yet applied versions up to $to
405
+     *
406
+     * @param string $to
407
+     * @param bool $schemaOnly
408
+     * @throws \InvalidArgumentException
409
+     */
410
+    public function migrate($to = 'latest', $schemaOnly = false) {
411
+        // read known migrations
412
+        $toBeExecuted = $this->getMigrationsToExecute($to);
413
+        foreach ($toBeExecuted as $version) {
414
+            $this->executeStep($version, $schemaOnly);
415
+        }
416
+    }
417
+
418
+    /**
419
+     * Get the human readable descriptions for the migration steps to run
420
+     *
421
+     * @param string $to
422
+     * @return string[] [$name => $description]
423
+     */
424
+    public function describeMigrationStep($to = 'latest') {
425
+        $toBeExecuted = $this->getMigrationsToExecute($to);
426
+        $description = [];
427
+        foreach ($toBeExecuted as $version) {
428
+            $migration = $this->createInstance($version);
429
+            if ($migration->name()) {
430
+                $description[$migration->name()] = $migration->description();
431
+            }
432
+        }
433
+        return $description;
434
+    }
435
+
436
+    /**
437
+     * @param string $version
438
+     * @return IMigrationStep
439
+     * @throws \InvalidArgumentException
440
+     */
441
+    protected function createInstance($version) {
442
+        $class = $this->getClass($version);
443
+        try {
444
+            $s = \OC::$server->query($class);
445
+
446
+            if (!$s instanceof IMigrationStep) {
447
+                throw new \InvalidArgumentException('Not a valid migration');
448
+            }
449
+        } catch (QueryException $e) {
450
+            if (class_exists($class)) {
451
+                $s = new $class();
452
+            } else {
453
+                throw new \InvalidArgumentException("Migration step '$class' is unknown");
454
+            }
455
+        }
456
+
457
+        return $s;
458
+    }
459
+
460
+    /**
461
+     * Executes one explicit version
462
+     *
463
+     * @param string $version
464
+     * @param bool $schemaOnly
465
+     * @throws \InvalidArgumentException
466
+     */
467
+    public function executeStep($version, $schemaOnly = false) {
468
+        $instance = $this->createInstance($version);
469
+
470
+        if (!$schemaOnly) {
471
+            $instance->preSchemaChange($this->output, function () {
472
+                return new SchemaWrapper($this->connection);
473
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
474
+        }
475
+
476
+        $toSchema = $instance->changeSchema($this->output, function () {
477
+            return new SchemaWrapper($this->connection);
478
+        }, ['tablePrefix' => $this->connection->getPrefix()]);
479
+
480
+        if ($toSchema instanceof SchemaWrapper) {
481
+            $targetSchema = $toSchema->getWrappedSchema();
482
+            if ($this->checkOracle) {
483
+                $sourceSchema = $this->connection->createSchema();
484
+                $this->ensureOracleIdentifierLengthLimit($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
485
+            }
486
+            $this->connection->migrateToSchema($targetSchema);
487
+            $toSchema->performDropTableCalls();
488
+        }
489
+
490
+        if (!$schemaOnly) {
491
+            $instance->postSchemaChange($this->output, function () {
492
+                return new SchemaWrapper($this->connection);
493
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
494
+        }
495
+
496
+        $this->markAsExecuted($version);
497
+    }
498
+
499
+    public function ensureOracleIdentifierLengthLimit(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
500
+        $sequences = $targetSchema->getSequences();
501
+
502
+        foreach ($targetSchema->getTables() as $table) {
503
+            try {
504
+                $sourceTable = $sourceSchema->getTable($table->getName());
505
+            } catch (SchemaException $e) {
506
+                if (\strlen($table->getName()) - $prefixLength > 27) {
507
+                    throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
508
+                }
509
+                $sourceTable = null;
510
+            }
511
+
512
+            foreach ($table->getColumns() as $thing) {
513
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
514
+                    throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
515
+                }
516
+
517
+                if ($thing->getNotnull() && $thing->getDefault() === ''
518
+                    && $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
519
+                    throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
520
+                }
521
+            }
522
+
523
+            foreach ($table->getIndexes() as $thing) {
524
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
525
+                    throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
526
+                }
527
+            }
528
+
529
+            foreach ($table->getForeignKeys() as $thing) {
530
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
531
+                    throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
532
+                }
533
+            }
534
+
535
+            $primaryKey = $table->getPrimaryKey();
536
+            if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
537
+                $indexName = strtolower($primaryKey->getName());
538
+                $isUsingDefaultName = $indexName === 'primary';
539
+
540
+                if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
541
+                    $defaultName = $table->getName() . '_pkey';
542
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
543
+
544
+                    if ($isUsingDefaultName) {
545
+                        $sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
546
+                        $sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
547
+                            return $sequence->getName() !== $sequenceName;
548
+                        });
549
+                    }
550
+                } elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
551
+                    $defaultName = $table->getName() . '_seq';
552
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
553
+                }
554
+
555
+                if (!$isUsingDefaultName && \strlen($indexName) > 30) {
556
+                    throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
557
+                }
558
+                if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
559
+                    throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
560
+                }
561
+            }
562
+        }
563
+
564
+        foreach ($sequences as $sequence) {
565
+            if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
566
+                throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
567
+            }
568
+        }
569
+    }
570
+
571
+    private function ensureMigrationsAreLoaded() {
572
+        if (empty($this->migrations)) {
573
+            $this->migrations = $this->findMigrations();
574
+        }
575
+    }
576 576
 }
Please login to merge, or discard this patch.
Spacing   +20 added lines, -20 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 {
@@ -223,7 +223,7 @@  discard block
 block discarded – undo
223 223
 			\RegexIterator::GET_MATCH);
224 224
 
225 225
 		$files = array_keys(iterator_to_array($iterator));
226
-		uasort($files, function ($a, $b) {
226
+		uasort($files, function($a, $b) {
227 227
 			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
228 228
 			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
229 229
 			if (!empty($matchA) && !empty($matchB)) {
@@ -301,7 +301,7 @@  discard block
 block discarded – undo
301 301
 	 * @return string
302 302
 	 */
303 303
 	public function getMigrationsTableName() {
304
-		return $this->connection->getPrefix() . 'migrations';
304
+		return $this->connection->getPrefix().'migrations';
305 305
 	}
306 306
 
307 307
 	/**
@@ -468,12 +468,12 @@  discard block
 block discarded – undo
468 468
 		$instance = $this->createInstance($version);
469 469
 
470 470
 		if (!$schemaOnly) {
471
-			$instance->preSchemaChange($this->output, function () {
471
+			$instance->preSchemaChange($this->output, function() {
472 472
 				return new SchemaWrapper($this->connection);
473 473
 			}, ['tablePrefix' => $this->connection->getPrefix()]);
474 474
 		}
475 475
 
476
-		$toSchema = $instance->changeSchema($this->output, function () {
476
+		$toSchema = $instance->changeSchema($this->output, function() {
477 477
 			return new SchemaWrapper($this->connection);
478 478
 		}, ['tablePrefix' => $this->connection->getPrefix()]);
479 479
 
@@ -488,7 +488,7 @@  discard block
 block discarded – undo
488 488
 		}
489 489
 
490 490
 		if (!$schemaOnly) {
491
-			$instance->postSchemaChange($this->output, function () {
491
+			$instance->postSchemaChange($this->output, function() {
492 492
 				return new SchemaWrapper($this->connection);
493 493
 			}, ['tablePrefix' => $this->connection->getPrefix()]);
494 494
 		}
@@ -504,31 +504,31 @@  discard block
 block discarded – undo
504 504
 				$sourceTable = $sourceSchema->getTable($table->getName());
505 505
 			} catch (SchemaException $e) {
506 506
 				if (\strlen($table->getName()) - $prefixLength > 27) {
507
-					throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
507
+					throw new \InvalidArgumentException('Table name "'.$table->getName().'" is too long.');
508 508
 				}
509 509
 				$sourceTable = null;
510 510
 			}
511 511
 
512 512
 			foreach ($table->getColumns() as $thing) {
513 513
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
514
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
514
+					throw new \InvalidArgumentException('Column name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
515 515
 				}
516 516
 
517 517
 				if ($thing->getNotnull() && $thing->getDefault() === ''
518 518
 					&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
519
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
519
+					throw new \InvalidArgumentException('Column name "'.$table->getName().'"."'.$thing->getName().'" is NotNull, but has empty string or null as default.');
520 520
 				}
521 521
 			}
522 522
 
523 523
 			foreach ($table->getIndexes() as $thing) {
524 524
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
525
-					throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
525
+					throw new \InvalidArgumentException('Index name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
526 526
 				}
527 527
 			}
528 528
 
529 529
 			foreach ($table->getForeignKeys() as $thing) {
530 530
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
531
-					throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
531
+					throw new \InvalidArgumentException('Foreign key name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
532 532
 				}
533 533
 			}
534 534
 
@@ -538,32 +538,32 @@  discard block
 block discarded – undo
538 538
 				$isUsingDefaultName = $indexName === 'primary';
539 539
 
540 540
 				if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
541
-					$defaultName = $table->getName() . '_pkey';
541
+					$defaultName = $table->getName().'_pkey';
542 542
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
543 543
 
544 544
 					if ($isUsingDefaultName) {
545
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
546
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
545
+						$sequenceName = $table->getName().'_'.implode('_', $primaryKey->getColumns()).'_seq';
546
+						$sequences = array_filter($sequences, function(Sequence $sequence) use ($sequenceName) {
547 547
 							return $sequence->getName() !== $sequenceName;
548 548
 						});
549 549
 					}
550 550
 				} elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
551
-					$defaultName = $table->getName() . '_seq';
551
+					$defaultName = $table->getName().'_seq';
552 552
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
553 553
 				}
554 554
 
555 555
 				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
556
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
556
+					throw new \InvalidArgumentException('Primary index name  on "'.$table->getName().'" is too long.');
557 557
 				}
558 558
 				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
559
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
559
+					throw new \InvalidArgumentException('Primary index name  on "'.$table->getName().'" is too long.');
560 560
 				}
561 561
 			}
562 562
 		}
563 563
 
564 564
 		foreach ($sequences as $sequence) {
565 565
 			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
566
-				throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
566
+				throw new \InvalidArgumentException('Sequence name "'.$sequence->getName().'" is too long.');
567 567
 			}
568 568
 		}
569 569
 	}
Please login to merge, or discard this patch.
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();
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();
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();
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();
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.