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