Completed
Push — master ( 2ea30f...65f52c )
by
unknown
28:59
created
lib/public/Migration/BigIntMigration.php 1 patch
Indentation   +34 added lines, -34 removed lines patch added patch discarded remove patch
@@ -14,38 +14,38 @@
 block discarded – undo
14 14
  * @since 13.0.0
15 15
  */
16 16
 abstract class BigIntMigration extends SimpleMigrationStep {
17
-	/**
18
-	 * @return array Returns an array with the following structure
19
-	 *               ['table1' => ['column1', 'column2'], ...]
20
-	 * @since 13.0.0
21
-	 */
22
-	abstract protected function getColumnsByTable();
23
-
24
-	/**
25
-	 * @param IOutput $output
26
-	 * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
27
-	 * @param array $options
28
-	 * @return null|ISchemaWrapper
29
-	 * @since 13.0.0
30
-	 */
31
-	public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
32
-		/** @var ISchemaWrapper $schema */
33
-		$schema = $schemaClosure();
34
-
35
-		$tables = $this->getColumnsByTable();
36
-
37
-		foreach ($tables as $tableName => $columns) {
38
-			$table = $schema->getTable($tableName);
39
-
40
-			foreach ($columns as $columnName) {
41
-				$column = $table->getColumn($columnName);
42
-				if (Type::lookupName($column->getType()) !== Types::BIGINT) {
43
-					$column->setType(Type::getType(Types::BIGINT));
44
-					$column->setOptions(['length' => 20]);
45
-				}
46
-			}
47
-		}
48
-
49
-		return $schema;
50
-	}
17
+    /**
18
+     * @return array Returns an array with the following structure
19
+     *               ['table1' => ['column1', 'column2'], ...]
20
+     * @since 13.0.0
21
+     */
22
+    abstract protected function getColumnsByTable();
23
+
24
+    /**
25
+     * @param IOutput $output
26
+     * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
27
+     * @param array $options
28
+     * @return null|ISchemaWrapper
29
+     * @since 13.0.0
30
+     */
31
+    public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
32
+        /** @var ISchemaWrapper $schema */
33
+        $schema = $schemaClosure();
34
+
35
+        $tables = $this->getColumnsByTable();
36
+
37
+        foreach ($tables as $tableName => $columns) {
38
+            $table = $schema->getTable($tableName);
39
+
40
+            foreach ($columns as $columnName) {
41
+                $column = $table->getColumn($columnName);
42
+                if (Type::lookupName($column->getType()) !== Types::BIGINT) {
43
+                    $column->setType(Type::getType(Types::BIGINT));
44
+                    $column->setOptions(['length' => 20]);
45
+                }
46
+            }
47
+        }
48
+
49
+        return $schema;
50
+    }
51 51
 }
Please login to merge, or discard this patch.
lib/private/DB/MigrationService.php 2 patches
Indentation   +713 added lines, -713 removed lines patch added patch discarded remove patch
@@ -27,717 +27,717 @@
 block discarded – undo
27 27
 use Psr\Log\LoggerInterface;
28 28
 
29 29
 class MigrationService {
30
-	private bool $migrationTableCreated;
31
-	private array $migrations;
32
-	private string $migrationsPath;
33
-	private string $migrationsNamespace;
34
-	private IOutput $output;
35
-	private LoggerInterface $logger;
36
-	private Connection $connection;
37
-	private string $appName;
38
-	private bool $checkOracle;
39
-
40
-	/**
41
-	 * @throws \Exception
42
-	 */
43
-	public function __construct(
44
-		string $appName,
45
-		Connection $connection,
46
-		?IOutput $output = null,
47
-		?LoggerInterface $logger = null,
48
-	) {
49
-		$this->appName = $appName;
50
-		$this->connection = $connection;
51
-		if ($logger === null) {
52
-			$this->logger = Server::get(LoggerInterface::class);
53
-		} else {
54
-			$this->logger = $logger;
55
-		}
56
-		if ($output === null) {
57
-			$this->output = new SimpleOutput($this->logger, $appName);
58
-		} else {
59
-			$this->output = $output;
60
-		}
61
-
62
-		if ($appName === 'core') {
63
-			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
64
-			$this->migrationsNamespace = 'OC\\Core\\Migrations';
65
-			$this->checkOracle = true;
66
-		} else {
67
-			$appManager = Server::get(IAppManager::class);
68
-			$appPath = $appManager->getAppPath($appName);
69
-			$namespace = App::buildAppNamespace($appName);
70
-			$this->migrationsPath = "$appPath/lib/Migration";
71
-			$this->migrationsNamespace = $namespace . '\\Migration';
72
-
73
-			$infoParser = new InfoParser();
74
-			$info = $infoParser->parse($appPath . '/appinfo/info.xml');
75
-			if (!isset($info['dependencies']['database'])) {
76
-				$this->checkOracle = true;
77
-			} else {
78
-				$this->checkOracle = false;
79
-				foreach ($info['dependencies']['database'] as $database) {
80
-					if (\is_string($database) && $database === 'oci') {
81
-						$this->checkOracle = true;
82
-					} elseif (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
83
-						$this->checkOracle = true;
84
-					}
85
-				}
86
-			}
87
-		}
88
-		$this->migrationTableCreated = false;
89
-	}
90
-
91
-	/**
92
-	 * Returns the name of the app for which this migration is executed
93
-	 */
94
-	public function getApp(): string {
95
-		return $this->appName;
96
-	}
97
-
98
-	/**
99
-	 * @codeCoverageIgnore - this will implicitly tested on installation
100
-	 */
101
-	private function createMigrationTable(): bool {
102
-		if ($this->migrationTableCreated) {
103
-			return false;
104
-		}
105
-
106
-		if ($this->connection->tableExists('migrations') && \OC::$server->getConfig()->getAppValue('core', 'vendor', '') !== 'owncloud') {
107
-			$this->migrationTableCreated = true;
108
-			return false;
109
-		}
110
-
111
-		$schema = new SchemaWrapper($this->connection);
112
-
113
-		/**
114
-		 * We drop the table when it has different columns or the definition does not
115
-		 * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
116
-		 */
117
-		try {
118
-			$table = $schema->getTable('migrations');
119
-			$columns = $table->getColumns();
120
-
121
-			if (count($columns) === 2) {
122
-				try {
123
-					$column = $table->getColumn('app');
124
-					$schemaMismatch = $column->getLength() !== 255;
125
-
126
-					if (!$schemaMismatch) {
127
-						$column = $table->getColumn('version');
128
-						$schemaMismatch = $column->getLength() !== 255;
129
-					}
130
-				} catch (SchemaException $e) {
131
-					// One of the columns is missing
132
-					$schemaMismatch = true;
133
-				}
134
-
135
-				if (!$schemaMismatch) {
136
-					// Table exists and schema matches: return back!
137
-					$this->migrationTableCreated = true;
138
-					return false;
139
-				}
140
-			}
141
-
142
-			// Drop the table, when it didn't match our expectations.
143
-			$this->connection->dropTable('migrations');
144
-
145
-			// Recreate the schema after the table was dropped.
146
-			$schema = new SchemaWrapper($this->connection);
147
-		} catch (SchemaException $e) {
148
-			// Table not found, no need to panic, we will create it.
149
-		}
150
-
151
-		$table = $schema->createTable('migrations');
152
-		$table->addColumn('app', Types::STRING, ['length' => 255]);
153
-		$table->addColumn('version', Types::STRING, ['length' => 255]);
154
-		$table->setPrimaryKey(['app', 'version']);
155
-
156
-		$this->connection->migrateToSchema($schema->getWrappedSchema());
157
-
158
-		$this->migrationTableCreated = true;
159
-
160
-		return true;
161
-	}
162
-
163
-	/**
164
-	 * Returns all versions which have already been applied
165
-	 *
166
-	 * @return list<string>
167
-	 * @codeCoverageIgnore - no need to test this
168
-	 */
169
-	public function getMigratedVersions() {
170
-		$this->createMigrationTable();
171
-		$qb = $this->connection->getQueryBuilder();
172
-
173
-		$qb->select('version')
174
-			->from('migrations')
175
-			->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
176
-			->orderBy('version');
177
-
178
-		$result = $qb->executeQuery();
179
-		$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
180
-		$result->closeCursor();
181
-
182
-		usort($rows, $this->sortMigrations(...));
183
-
184
-		return $rows;
185
-	}
186
-
187
-	/**
188
-	 * Returns all versions which are available in the migration folder
189
-	 * @return list<string>
190
-	 */
191
-	public function getAvailableVersions(): array {
192
-		$this->ensureMigrationsAreLoaded();
193
-		$versions = array_map('strval', array_keys($this->migrations));
194
-		usort($versions, $this->sortMigrations(...));
195
-		return $versions;
196
-	}
197
-
198
-	protected function sortMigrations(string $a, string $b): int {
199
-		preg_match('/(\d+)Date(\d+)/', basename($a), $matchA);
200
-		preg_match('/(\d+)Date(\d+)/', basename($b), $matchB);
201
-		if (!empty($matchA) && !empty($matchB)) {
202
-			$versionA = (int)$matchA[1];
203
-			$versionB = (int)$matchB[1];
204
-			if ($versionA !== $versionB) {
205
-				return ($versionA < $versionB) ? -1 : 1;
206
-			}
207
-			return strnatcmp($matchA[2], $matchB[2]);
208
-		}
209
-		return strnatcmp(basename($a), basename($b));
210
-	}
211
-
212
-	/**
213
-	 * @return array<string, string>
214
-	 */
215
-	protected function findMigrations(): array {
216
-		$directory = realpath($this->migrationsPath);
217
-		if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
218
-			return [];
219
-		}
220
-
221
-		$iterator = new \RegexIterator(
222
-			new \RecursiveIteratorIterator(
223
-				new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
224
-				\RecursiveIteratorIterator::LEAVES_ONLY
225
-			),
226
-			'#^.+\\/Version[^\\/]{1,255}\\.php$#i',
227
-			\RegexIterator::GET_MATCH);
228
-
229
-		$files = array_keys(iterator_to_array($iterator));
230
-		usort($files, $this->sortMigrations(...));
231
-
232
-		$migrations = [];
233
-
234
-		foreach ($files as $file) {
235
-			$className = basename($file, '.php');
236
-			$version = (string)substr($className, 7);
237
-			if ($version === '0') {
238
-				throw new \InvalidArgumentException(
239
-					"Cannot load a migrations with the name '$version' because it is a reserved number"
240
-				);
241
-			}
242
-			$migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
243
-		}
244
-
245
-		return $migrations;
246
-	}
247
-
248
-	/**
249
-	 * @param string $to
250
-	 * @return string[]
251
-	 */
252
-	private function getMigrationsToExecute($to) {
253
-		$knownMigrations = $this->getMigratedVersions();
254
-		$availableMigrations = $this->getAvailableVersions();
255
-
256
-		$toBeExecuted = [];
257
-		foreach ($availableMigrations as $v) {
258
-			if ($to !== 'latest' && ($this->sortMigrations($v, $to) > 0)) {
259
-				continue;
260
-			}
261
-			if ($this->shallBeExecuted($v, $knownMigrations)) {
262
-				$toBeExecuted[] = $v;
263
-			}
264
-		}
265
-
266
-		return $toBeExecuted;
267
-	}
268
-
269
-	/**
270
-	 * @param string $m
271
-	 * @param string[] $knownMigrations
272
-	 * @return bool
273
-	 */
274
-	private function shallBeExecuted($m, $knownMigrations) {
275
-		if (in_array($m, $knownMigrations)) {
276
-			return false;
277
-		}
278
-
279
-		return true;
280
-	}
281
-
282
-	/**
283
-	 * @param string $version
284
-	 */
285
-	private function markAsExecuted($version) {
286
-		$this->connection->insertIfNotExist('*PREFIX*migrations', [
287
-			'app' => $this->appName,
288
-			'version' => $version
289
-		]);
290
-	}
291
-
292
-	/**
293
-	 * Returns the name of the table which holds the already applied versions
294
-	 *
295
-	 * @return string
296
-	 */
297
-	public function getMigrationsTableName() {
298
-		return $this->connection->getPrefix() . 'migrations';
299
-	}
300
-
301
-	/**
302
-	 * Returns the namespace of the version classes
303
-	 *
304
-	 * @return string
305
-	 */
306
-	public function getMigrationsNamespace() {
307
-		return $this->migrationsNamespace;
308
-	}
309
-
310
-	/**
311
-	 * Returns the directory which holds the versions
312
-	 *
313
-	 * @return string
314
-	 */
315
-	public function getMigrationsDirectory() {
316
-		return $this->migrationsPath;
317
-	}
318
-
319
-	/**
320
-	 * Return the explicit version for the aliases; current, next, prev, latest
321
-	 *
322
-	 * @return mixed|null|string
323
-	 */
324
-	public function getMigration(string $alias) {
325
-		switch ($alias) {
326
-			case 'current':
327
-				return $this->getCurrentVersion();
328
-			case 'next':
329
-				return $this->getRelativeVersion($this->getCurrentVersion(), 1);
330
-			case 'prev':
331
-				return $this->getRelativeVersion($this->getCurrentVersion(), -1);
332
-			case 'latest':
333
-				$this->ensureMigrationsAreLoaded();
334
-
335
-				$migrations = $this->getAvailableVersions();
336
-				return @end($migrations);
337
-		}
338
-		return '0';
339
-	}
340
-
341
-	private function getRelativeVersion(string $version, int $delta): ?string {
342
-		$this->ensureMigrationsAreLoaded();
343
-
344
-		$versions = $this->getAvailableVersions();
345
-		array_unshift($versions, '0');
346
-		/** @var int $offset */
347
-		$offset = array_search($version, $versions, true);
348
-		if ($offset === false || !isset($versions[$offset + $delta])) {
349
-			// Unknown version or delta out of bounds.
350
-			return null;
351
-		}
352
-
353
-		return (string)$versions[$offset + $delta];
354
-	}
355
-
356
-	private function getCurrentVersion(): string {
357
-		$m = $this->getMigratedVersions();
358
-		if (count($m) === 0) {
359
-			return '0';
360
-		}
361
-		$migrations = array_values($m);
362
-		return @end($migrations);
363
-	}
364
-
365
-	/**
366
-	 * @throws \InvalidArgumentException
367
-	 */
368
-	private function getClass(string $version): string {
369
-		$this->ensureMigrationsAreLoaded();
370
-
371
-		if (isset($this->migrations[$version])) {
372
-			return $this->migrations[$version];
373
-		}
374
-
375
-		throw new \InvalidArgumentException("Version $version is unknown.");
376
-	}
377
-
378
-	/**
379
-	 * Allows to set an IOutput implementation which is used for logging progress and messages
380
-	 */
381
-	public function setOutput(IOutput $output): void {
382
-		$this->output = $output;
383
-	}
384
-
385
-	/**
386
-	 * Applies all not yet applied versions up to $to
387
-	 * @throws \InvalidArgumentException
388
-	 */
389
-	public function migrate(string $to = 'latest', bool $schemaOnly = false): void {
390
-		if ($schemaOnly) {
391
-			$this->output->debug('Migrating schema only');
392
-			$this->migrateSchemaOnly($to);
393
-			return;
394
-		}
395
-
396
-		// read known migrations
397
-		$toBeExecuted = $this->getMigrationsToExecute($to);
398
-		foreach ($toBeExecuted as $version) {
399
-			try {
400
-				$this->executeStep($version, $schemaOnly);
401
-			} catch (\Exception $e) {
402
-				// The exception itself does not contain the name of the migration,
403
-				// so we wrap it here, to make debugging easier.
404
-				throw new \Exception('Database error when running migration ' . $version . ' for app ' . $this->getApp() . PHP_EOL . $e->getMessage(), 0, $e);
405
-			}
406
-		}
407
-	}
408
-
409
-	/**
410
-	 * Applies all not yet applied versions up to $to
411
-	 * @throws \InvalidArgumentException
412
-	 */
413
-	public function migrateSchemaOnly(string $to = 'latest'): void {
414
-		// read known migrations
415
-		$toBeExecuted = $this->getMigrationsToExecute($to);
416
-
417
-		if (empty($toBeExecuted)) {
418
-			return;
419
-		}
420
-
421
-		$toSchema = null;
422
-		foreach ($toBeExecuted as $version) {
423
-			$this->output->debug('- Reading ' . $version);
424
-			$instance = $this->createInstance($version);
425
-
426
-			$toSchema = $instance->changeSchema($this->output, function () use ($toSchema): ISchemaWrapper {
427
-				return $toSchema ?: new SchemaWrapper($this->connection);
428
-			}, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
429
-		}
430
-
431
-		if ($toSchema instanceof SchemaWrapper) {
432
-			$this->output->debug('- Checking target database schema');
433
-			$targetSchema = $toSchema->getWrappedSchema();
434
-			$this->ensureUniqueNamesConstraints($targetSchema, true);
435
-			if ($this->checkOracle) {
436
-				$beforeSchema = $this->connection->createSchema();
437
-				$this->ensureOracleConstraints($beforeSchema, $targetSchema, strlen($this->connection->getPrefix()));
438
-			}
439
-
440
-			$this->output->debug('- Migrate database schema');
441
-			$this->connection->migrateToSchema($targetSchema);
442
-			$toSchema->performDropTableCalls();
443
-		}
444
-
445
-		$this->output->debug('- Mark migrations as executed');
446
-		foreach ($toBeExecuted as $version) {
447
-			$this->markAsExecuted($version);
448
-		}
449
-	}
450
-
451
-	/**
452
-	 * Get the human readable descriptions for the migration steps to run
453
-	 *
454
-	 * @param string $to
455
-	 * @return string[] [$name => $description]
456
-	 */
457
-	public function describeMigrationStep($to = 'latest') {
458
-		$toBeExecuted = $this->getMigrationsToExecute($to);
459
-		$description = [];
460
-		foreach ($toBeExecuted as $version) {
461
-			$migration = $this->createInstance($version);
462
-			if ($migration->name()) {
463
-				$description[$migration->name()] = $migration->description();
464
-			}
465
-		}
466
-		return $description;
467
-	}
468
-
469
-	/**
470
-	 * @param string $version
471
-	 * @return IMigrationStep
472
-	 * @throws \InvalidArgumentException
473
-	 */
474
-	public function createInstance($version) {
475
-		$class = $this->getClass($version);
476
-		try {
477
-			$s = \OCP\Server::get($class);
478
-
479
-			if (!$s instanceof IMigrationStep) {
480
-				throw new \InvalidArgumentException('Not a valid migration');
481
-			}
482
-		} catch (QueryException $e) {
483
-			if (class_exists($class)) {
484
-				$s = new $class();
485
-			} else {
486
-				throw new \InvalidArgumentException("Migration step '$class' is unknown");
487
-			}
488
-		}
489
-
490
-		return $s;
491
-	}
492
-
493
-	/**
494
-	 * Executes one explicit version
495
-	 *
496
-	 * @param string $version
497
-	 * @param bool $schemaOnly
498
-	 * @throws \InvalidArgumentException
499
-	 */
500
-	public function executeStep($version, $schemaOnly = false) {
501
-		$instance = $this->createInstance($version);
502
-
503
-		if (!$schemaOnly) {
504
-			$instance->preSchemaChange($this->output, function (): ISchemaWrapper {
505
-				return new SchemaWrapper($this->connection);
506
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
507
-		}
508
-
509
-		$toSchema = $instance->changeSchema($this->output, function (): ISchemaWrapper {
510
-			return new SchemaWrapper($this->connection);
511
-		}, ['tablePrefix' => $this->connection->getPrefix()]);
512
-
513
-		if ($toSchema instanceof SchemaWrapper) {
514
-			$targetSchema = $toSchema->getWrappedSchema();
515
-			$this->ensureUniqueNamesConstraints($targetSchema, $schemaOnly);
516
-			if ($this->checkOracle) {
517
-				$sourceSchema = $this->connection->createSchema();
518
-				$this->ensureOracleConstraints($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
519
-			}
520
-			$this->connection->migrateToSchema($targetSchema);
521
-			$toSchema->performDropTableCalls();
522
-		}
523
-
524
-		if (!$schemaOnly) {
525
-			$instance->postSchemaChange($this->output, function (): ISchemaWrapper {
526
-				return new SchemaWrapper($this->connection);
527
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
528
-		}
529
-
530
-		$this->markAsExecuted($version);
531
-	}
532
-
533
-	/**
534
-	 * Naming constraints:
535
-	 * - Tables names must be 30 chars or shorter (27 + oc_ prefix)
536
-	 * - Column names must be 30 chars or shorter
537
-	 * - Index names must be 30 chars or shorter
538
-	 * - Sequence names must be 30 chars or shorter
539
-	 * - Primary key names must be set or the table name 23 chars or shorter
540
-	 *
541
-	 * Data constraints:
542
-	 * - Tables need a primary key (Not specific to Oracle, but required for performant clustering support)
543
-	 * - Columns with "NotNull" can not have empty string as default value
544
-	 * - Columns with "NotNull" can not have number 0 as default value
545
-	 * - Columns with type "bool" (which is in fact integer of length 1) can not be "NotNull" as it can not store 0/false
546
-	 * - Columns with type "string" can not be longer than 4.000 characters, use "text" instead
547
-	 *
548
-	 * @see https://github.com/nextcloud/documentation/blob/master/developer_manual/basics/storage/database.rst
549
-	 *
550
-	 * @param Schema $sourceSchema
551
-	 * @param Schema $targetSchema
552
-	 * @param int $prefixLength
553
-	 * @throws \Doctrine\DBAL\Exception
554
-	 */
555
-	public function ensureOracleConstraints(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
556
-		$sequences = $targetSchema->getSequences();
557
-
558
-		foreach ($targetSchema->getTables() as $table) {
559
-			try {
560
-				$sourceTable = $sourceSchema->getTable($table->getName());
561
-			} catch (SchemaException $e) {
562
-				if (\strlen($table->getName()) - $prefixLength > 27) {
563
-					throw new \InvalidArgumentException('Table name "' . $table->getName() . '" is too long.');
564
-				}
565
-				$sourceTable = null;
566
-			}
567
-
568
-			foreach ($table->getColumns() as $thing) {
569
-				// If the table doesn't exist OR if the column doesn't exist in the table
570
-				if (!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) {
571
-					if (\strlen($thing->getName()) > 30) {
572
-						throw new \InvalidArgumentException('Column name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
573
-					}
574
-
575
-					if ($thing->getNotnull() && $thing->getDefault() === ''
576
-						&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
577
-						throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
578
-					}
579
-
580
-					if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
581
-						// Oracle doesn't support boolean column with non-null value
582
-						if ($thing->getNotnull() && Type::lookupName($thing->getType()) === Types::BOOLEAN) {
583
-							$thing->setNotnull(false);
584
-						}
585
-					}
586
-
587
-					$sourceColumn = null;
588
-				} else {
589
-					$sourceColumn = $sourceTable->getColumn($thing->getName());
590
-				}
591
-
592
-				// If the column was just created OR the length changed OR the type changed
593
-				// we will NOT detect invalid length if the column is not modified
594
-				if (($sourceColumn === null || $sourceColumn->getLength() !== $thing->getLength() || Type::lookupName($sourceColumn->getType()) !== Types::STRING)
595
-					&& $thing->getLength() > 4000 && Type::lookupName($thing->getType()) === Types::STRING) {
596
-					throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type String, but exceeding the 4.000 length limit.');
597
-				}
598
-			}
599
-
600
-			foreach ($table->getIndexes() as $thing) {
601
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
602
-					throw new \InvalidArgumentException('Index name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
603
-				}
604
-			}
605
-
606
-			foreach ($table->getForeignKeys() as $thing) {
607
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
608
-					throw new \InvalidArgumentException('Foreign key name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
609
-				}
610
-			}
611
-
612
-			$primaryKey = $table->getPrimaryKey();
613
-			if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || $sourceTable->getPrimaryKey() === null)) {
614
-				$indexName = strtolower($primaryKey->getName());
615
-				$isUsingDefaultName = $indexName === 'primary';
616
-
617
-				if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_POSTGRES) {
618
-					$defaultName = $table->getName() . '_pkey';
619
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
620
-
621
-					if ($isUsingDefaultName) {
622
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
623
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
624
-							return $sequence->getName() !== $sequenceName;
625
-						});
626
-					}
627
-				} elseif ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
628
-					$defaultName = $table->getName() . '_seq';
629
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
630
-				}
631
-
632
-				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
633
-					throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
634
-				}
635
-				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
636
-					throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
637
-				}
638
-			} elseif (!$primaryKey instanceof Index && !$sourceTable instanceof Table) {
639
-				/** @var LoggerInterface $logger */
640
-				$logger = \OC::$server->get(LoggerInterface::class);
641
-				$logger->error('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
642
-				// throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
643
-			}
644
-		}
645
-
646
-		foreach ($sequences as $sequence) {
647
-			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
648
-				throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" is too long.');
649
-			}
650
-		}
651
-	}
652
-
653
-	/**
654
-	 * Ensure naming constraints
655
-	 *
656
-	 * Naming constraints:
657
-	 * - Index, sequence and primary key names must be unique within a Postgres Schema
658
-	 *
659
-	 * Only on installation we want to break hard, so that all developers notice
660
-	 * the bugs when installing the app on any database or CI, and can work on
661
-	 * fixing their migrations before releasing a version incompatible with Postgres.
662
-	 *
663
-	 * In case of updates we might be running on production instances and the
664
-	 * administrators being faced with the error would not know how to resolve it
665
-	 * anyway. This can also happen with instances, that had the issue before the
666
-	 * current update, so we don't want to make their life more complicated
667
-	 * than needed.
668
-	 *
669
-	 * @param Schema $targetSchema
670
-	 * @param bool $isInstalling
671
-	 */
672
-	public function ensureUniqueNamesConstraints(Schema $targetSchema, bool $isInstalling): void {
673
-		$constraintNames = [];
674
-		$sequences = $targetSchema->getSequences();
675
-
676
-		foreach ($targetSchema->getTables() as $table) {
677
-			foreach ($table->getIndexes() as $thing) {
678
-				$indexName = strtolower($thing->getName());
679
-				if ($indexName === 'primary' || $thing->isPrimary()) {
680
-					continue;
681
-				}
682
-
683
-				if (isset($constraintNames[$thing->getName()])) {
684
-					if ($isInstalling) {
685
-						throw new \InvalidArgumentException('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
686
-					}
687
-					$this->logErrorOrWarning('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
688
-				}
689
-				$constraintNames[$thing->getName()] = $table->getName();
690
-			}
691
-
692
-			foreach ($table->getForeignKeys() as $thing) {
693
-				if (isset($constraintNames[$thing->getName()])) {
694
-					if ($isInstalling) {
695
-						throw new \InvalidArgumentException('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
696
-					}
697
-					$this->logErrorOrWarning('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
698
-				}
699
-				$constraintNames[$thing->getName()] = $table->getName();
700
-			}
701
-
702
-			$primaryKey = $table->getPrimaryKey();
703
-			if ($primaryKey instanceof Index) {
704
-				$indexName = strtolower($primaryKey->getName());
705
-				if ($indexName === 'primary') {
706
-					continue;
707
-				}
708
-
709
-				if (isset($constraintNames[$indexName])) {
710
-					if ($isInstalling) {
711
-						throw new \InvalidArgumentException('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
712
-					}
713
-					$this->logErrorOrWarning('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
714
-				}
715
-				$constraintNames[$indexName] = $table->getName();
716
-			}
717
-		}
718
-
719
-		foreach ($sequences as $sequence) {
720
-			if (isset($constraintNames[$sequence->getName()])) {
721
-				if ($isInstalling) {
722
-					throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
723
-				}
724
-				$this->logErrorOrWarning('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
725
-			}
726
-			$constraintNames[$sequence->getName()] = 'sequence';
727
-		}
728
-	}
729
-
730
-	protected function logErrorOrWarning(string $log): void {
731
-		if ($this->output instanceof SimpleOutput) {
732
-			$this->output->warning($log);
733
-		} else {
734
-			$this->logger->error($log);
735
-		}
736
-	}
737
-
738
-	private function ensureMigrationsAreLoaded(): void {
739
-		if (empty($this->migrations)) {
740
-			$this->migrations = $this->findMigrations();
741
-		}
742
-	}
30
+    private bool $migrationTableCreated;
31
+    private array $migrations;
32
+    private string $migrationsPath;
33
+    private string $migrationsNamespace;
34
+    private IOutput $output;
35
+    private LoggerInterface $logger;
36
+    private Connection $connection;
37
+    private string $appName;
38
+    private bool $checkOracle;
39
+
40
+    /**
41
+     * @throws \Exception
42
+     */
43
+    public function __construct(
44
+        string $appName,
45
+        Connection $connection,
46
+        ?IOutput $output = null,
47
+        ?LoggerInterface $logger = null,
48
+    ) {
49
+        $this->appName = $appName;
50
+        $this->connection = $connection;
51
+        if ($logger === null) {
52
+            $this->logger = Server::get(LoggerInterface::class);
53
+        } else {
54
+            $this->logger = $logger;
55
+        }
56
+        if ($output === null) {
57
+            $this->output = new SimpleOutput($this->logger, $appName);
58
+        } else {
59
+            $this->output = $output;
60
+        }
61
+
62
+        if ($appName === 'core') {
63
+            $this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
64
+            $this->migrationsNamespace = 'OC\\Core\\Migrations';
65
+            $this->checkOracle = true;
66
+        } else {
67
+            $appManager = Server::get(IAppManager::class);
68
+            $appPath = $appManager->getAppPath($appName);
69
+            $namespace = App::buildAppNamespace($appName);
70
+            $this->migrationsPath = "$appPath/lib/Migration";
71
+            $this->migrationsNamespace = $namespace . '\\Migration';
72
+
73
+            $infoParser = new InfoParser();
74
+            $info = $infoParser->parse($appPath . '/appinfo/info.xml');
75
+            if (!isset($info['dependencies']['database'])) {
76
+                $this->checkOracle = true;
77
+            } else {
78
+                $this->checkOracle = false;
79
+                foreach ($info['dependencies']['database'] as $database) {
80
+                    if (\is_string($database) && $database === 'oci') {
81
+                        $this->checkOracle = true;
82
+                    } elseif (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
83
+                        $this->checkOracle = true;
84
+                    }
85
+                }
86
+            }
87
+        }
88
+        $this->migrationTableCreated = false;
89
+    }
90
+
91
+    /**
92
+     * Returns the name of the app for which this migration is executed
93
+     */
94
+    public function getApp(): string {
95
+        return $this->appName;
96
+    }
97
+
98
+    /**
99
+     * @codeCoverageIgnore - this will implicitly tested on installation
100
+     */
101
+    private function createMigrationTable(): bool {
102
+        if ($this->migrationTableCreated) {
103
+            return false;
104
+        }
105
+
106
+        if ($this->connection->tableExists('migrations') && \OC::$server->getConfig()->getAppValue('core', 'vendor', '') !== 'owncloud') {
107
+            $this->migrationTableCreated = true;
108
+            return false;
109
+        }
110
+
111
+        $schema = new SchemaWrapper($this->connection);
112
+
113
+        /**
114
+         * We drop the table when it has different columns or the definition does not
115
+         * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
116
+         */
117
+        try {
118
+            $table = $schema->getTable('migrations');
119
+            $columns = $table->getColumns();
120
+
121
+            if (count($columns) === 2) {
122
+                try {
123
+                    $column = $table->getColumn('app');
124
+                    $schemaMismatch = $column->getLength() !== 255;
125
+
126
+                    if (!$schemaMismatch) {
127
+                        $column = $table->getColumn('version');
128
+                        $schemaMismatch = $column->getLength() !== 255;
129
+                    }
130
+                } catch (SchemaException $e) {
131
+                    // One of the columns is missing
132
+                    $schemaMismatch = true;
133
+                }
134
+
135
+                if (!$schemaMismatch) {
136
+                    // Table exists and schema matches: return back!
137
+                    $this->migrationTableCreated = true;
138
+                    return false;
139
+                }
140
+            }
141
+
142
+            // Drop the table, when it didn't match our expectations.
143
+            $this->connection->dropTable('migrations');
144
+
145
+            // Recreate the schema after the table was dropped.
146
+            $schema = new SchemaWrapper($this->connection);
147
+        } catch (SchemaException $e) {
148
+            // Table not found, no need to panic, we will create it.
149
+        }
150
+
151
+        $table = $schema->createTable('migrations');
152
+        $table->addColumn('app', Types::STRING, ['length' => 255]);
153
+        $table->addColumn('version', Types::STRING, ['length' => 255]);
154
+        $table->setPrimaryKey(['app', 'version']);
155
+
156
+        $this->connection->migrateToSchema($schema->getWrappedSchema());
157
+
158
+        $this->migrationTableCreated = true;
159
+
160
+        return true;
161
+    }
162
+
163
+    /**
164
+     * Returns all versions which have already been applied
165
+     *
166
+     * @return list<string>
167
+     * @codeCoverageIgnore - no need to test this
168
+     */
169
+    public function getMigratedVersions() {
170
+        $this->createMigrationTable();
171
+        $qb = $this->connection->getQueryBuilder();
172
+
173
+        $qb->select('version')
174
+            ->from('migrations')
175
+            ->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
176
+            ->orderBy('version');
177
+
178
+        $result = $qb->executeQuery();
179
+        $rows = $result->fetchAll(\PDO::FETCH_COLUMN);
180
+        $result->closeCursor();
181
+
182
+        usort($rows, $this->sortMigrations(...));
183
+
184
+        return $rows;
185
+    }
186
+
187
+    /**
188
+     * Returns all versions which are available in the migration folder
189
+     * @return list<string>
190
+     */
191
+    public function getAvailableVersions(): array {
192
+        $this->ensureMigrationsAreLoaded();
193
+        $versions = array_map('strval', array_keys($this->migrations));
194
+        usort($versions, $this->sortMigrations(...));
195
+        return $versions;
196
+    }
197
+
198
+    protected function sortMigrations(string $a, string $b): int {
199
+        preg_match('/(\d+)Date(\d+)/', basename($a), $matchA);
200
+        preg_match('/(\d+)Date(\d+)/', basename($b), $matchB);
201
+        if (!empty($matchA) && !empty($matchB)) {
202
+            $versionA = (int)$matchA[1];
203
+            $versionB = (int)$matchB[1];
204
+            if ($versionA !== $versionB) {
205
+                return ($versionA < $versionB) ? -1 : 1;
206
+            }
207
+            return strnatcmp($matchA[2], $matchB[2]);
208
+        }
209
+        return strnatcmp(basename($a), basename($b));
210
+    }
211
+
212
+    /**
213
+     * @return array<string, string>
214
+     */
215
+    protected function findMigrations(): array {
216
+        $directory = realpath($this->migrationsPath);
217
+        if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
218
+            return [];
219
+        }
220
+
221
+        $iterator = new \RegexIterator(
222
+            new \RecursiveIteratorIterator(
223
+                new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
224
+                \RecursiveIteratorIterator::LEAVES_ONLY
225
+            ),
226
+            '#^.+\\/Version[^\\/]{1,255}\\.php$#i',
227
+            \RegexIterator::GET_MATCH);
228
+
229
+        $files = array_keys(iterator_to_array($iterator));
230
+        usort($files, $this->sortMigrations(...));
231
+
232
+        $migrations = [];
233
+
234
+        foreach ($files as $file) {
235
+            $className = basename($file, '.php');
236
+            $version = (string)substr($className, 7);
237
+            if ($version === '0') {
238
+                throw new \InvalidArgumentException(
239
+                    "Cannot load a migrations with the name '$version' because it is a reserved number"
240
+                );
241
+            }
242
+            $migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
243
+        }
244
+
245
+        return $migrations;
246
+    }
247
+
248
+    /**
249
+     * @param string $to
250
+     * @return string[]
251
+     */
252
+    private function getMigrationsToExecute($to) {
253
+        $knownMigrations = $this->getMigratedVersions();
254
+        $availableMigrations = $this->getAvailableVersions();
255
+
256
+        $toBeExecuted = [];
257
+        foreach ($availableMigrations as $v) {
258
+            if ($to !== 'latest' && ($this->sortMigrations($v, $to) > 0)) {
259
+                continue;
260
+            }
261
+            if ($this->shallBeExecuted($v, $knownMigrations)) {
262
+                $toBeExecuted[] = $v;
263
+            }
264
+        }
265
+
266
+        return $toBeExecuted;
267
+    }
268
+
269
+    /**
270
+     * @param string $m
271
+     * @param string[] $knownMigrations
272
+     * @return bool
273
+     */
274
+    private function shallBeExecuted($m, $knownMigrations) {
275
+        if (in_array($m, $knownMigrations)) {
276
+            return false;
277
+        }
278
+
279
+        return true;
280
+    }
281
+
282
+    /**
283
+     * @param string $version
284
+     */
285
+    private function markAsExecuted($version) {
286
+        $this->connection->insertIfNotExist('*PREFIX*migrations', [
287
+            'app' => $this->appName,
288
+            'version' => $version
289
+        ]);
290
+    }
291
+
292
+    /**
293
+     * Returns the name of the table which holds the already applied versions
294
+     *
295
+     * @return string
296
+     */
297
+    public function getMigrationsTableName() {
298
+        return $this->connection->getPrefix() . 'migrations';
299
+    }
300
+
301
+    /**
302
+     * Returns the namespace of the version classes
303
+     *
304
+     * @return string
305
+     */
306
+    public function getMigrationsNamespace() {
307
+        return $this->migrationsNamespace;
308
+    }
309
+
310
+    /**
311
+     * Returns the directory which holds the versions
312
+     *
313
+     * @return string
314
+     */
315
+    public function getMigrationsDirectory() {
316
+        return $this->migrationsPath;
317
+    }
318
+
319
+    /**
320
+     * Return the explicit version for the aliases; current, next, prev, latest
321
+     *
322
+     * @return mixed|null|string
323
+     */
324
+    public function getMigration(string $alias) {
325
+        switch ($alias) {
326
+            case 'current':
327
+                return $this->getCurrentVersion();
328
+            case 'next':
329
+                return $this->getRelativeVersion($this->getCurrentVersion(), 1);
330
+            case 'prev':
331
+                return $this->getRelativeVersion($this->getCurrentVersion(), -1);
332
+            case 'latest':
333
+                $this->ensureMigrationsAreLoaded();
334
+
335
+                $migrations = $this->getAvailableVersions();
336
+                return @end($migrations);
337
+        }
338
+        return '0';
339
+    }
340
+
341
+    private function getRelativeVersion(string $version, int $delta): ?string {
342
+        $this->ensureMigrationsAreLoaded();
343
+
344
+        $versions = $this->getAvailableVersions();
345
+        array_unshift($versions, '0');
346
+        /** @var int $offset */
347
+        $offset = array_search($version, $versions, true);
348
+        if ($offset === false || !isset($versions[$offset + $delta])) {
349
+            // Unknown version or delta out of bounds.
350
+            return null;
351
+        }
352
+
353
+        return (string)$versions[$offset + $delta];
354
+    }
355
+
356
+    private function getCurrentVersion(): string {
357
+        $m = $this->getMigratedVersions();
358
+        if (count($m) === 0) {
359
+            return '0';
360
+        }
361
+        $migrations = array_values($m);
362
+        return @end($migrations);
363
+    }
364
+
365
+    /**
366
+     * @throws \InvalidArgumentException
367
+     */
368
+    private function getClass(string $version): string {
369
+        $this->ensureMigrationsAreLoaded();
370
+
371
+        if (isset($this->migrations[$version])) {
372
+            return $this->migrations[$version];
373
+        }
374
+
375
+        throw new \InvalidArgumentException("Version $version is unknown.");
376
+    }
377
+
378
+    /**
379
+     * Allows to set an IOutput implementation which is used for logging progress and messages
380
+     */
381
+    public function setOutput(IOutput $output): void {
382
+        $this->output = $output;
383
+    }
384
+
385
+    /**
386
+     * Applies all not yet applied versions up to $to
387
+     * @throws \InvalidArgumentException
388
+     */
389
+    public function migrate(string $to = 'latest', bool $schemaOnly = false): void {
390
+        if ($schemaOnly) {
391
+            $this->output->debug('Migrating schema only');
392
+            $this->migrateSchemaOnly($to);
393
+            return;
394
+        }
395
+
396
+        // read known migrations
397
+        $toBeExecuted = $this->getMigrationsToExecute($to);
398
+        foreach ($toBeExecuted as $version) {
399
+            try {
400
+                $this->executeStep($version, $schemaOnly);
401
+            } catch (\Exception $e) {
402
+                // The exception itself does not contain the name of the migration,
403
+                // so we wrap it here, to make debugging easier.
404
+                throw new \Exception('Database error when running migration ' . $version . ' for app ' . $this->getApp() . PHP_EOL . $e->getMessage(), 0, $e);
405
+            }
406
+        }
407
+    }
408
+
409
+    /**
410
+     * Applies all not yet applied versions up to $to
411
+     * @throws \InvalidArgumentException
412
+     */
413
+    public function migrateSchemaOnly(string $to = 'latest'): void {
414
+        // read known migrations
415
+        $toBeExecuted = $this->getMigrationsToExecute($to);
416
+
417
+        if (empty($toBeExecuted)) {
418
+            return;
419
+        }
420
+
421
+        $toSchema = null;
422
+        foreach ($toBeExecuted as $version) {
423
+            $this->output->debug('- Reading ' . $version);
424
+            $instance = $this->createInstance($version);
425
+
426
+            $toSchema = $instance->changeSchema($this->output, function () use ($toSchema): ISchemaWrapper {
427
+                return $toSchema ?: new SchemaWrapper($this->connection);
428
+            }, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
429
+        }
430
+
431
+        if ($toSchema instanceof SchemaWrapper) {
432
+            $this->output->debug('- Checking target database schema');
433
+            $targetSchema = $toSchema->getWrappedSchema();
434
+            $this->ensureUniqueNamesConstraints($targetSchema, true);
435
+            if ($this->checkOracle) {
436
+                $beforeSchema = $this->connection->createSchema();
437
+                $this->ensureOracleConstraints($beforeSchema, $targetSchema, strlen($this->connection->getPrefix()));
438
+            }
439
+
440
+            $this->output->debug('- Migrate database schema');
441
+            $this->connection->migrateToSchema($targetSchema);
442
+            $toSchema->performDropTableCalls();
443
+        }
444
+
445
+        $this->output->debug('- Mark migrations as executed');
446
+        foreach ($toBeExecuted as $version) {
447
+            $this->markAsExecuted($version);
448
+        }
449
+    }
450
+
451
+    /**
452
+     * Get the human readable descriptions for the migration steps to run
453
+     *
454
+     * @param string $to
455
+     * @return string[] [$name => $description]
456
+     */
457
+    public function describeMigrationStep($to = 'latest') {
458
+        $toBeExecuted = $this->getMigrationsToExecute($to);
459
+        $description = [];
460
+        foreach ($toBeExecuted as $version) {
461
+            $migration = $this->createInstance($version);
462
+            if ($migration->name()) {
463
+                $description[$migration->name()] = $migration->description();
464
+            }
465
+        }
466
+        return $description;
467
+    }
468
+
469
+    /**
470
+     * @param string $version
471
+     * @return IMigrationStep
472
+     * @throws \InvalidArgumentException
473
+     */
474
+    public function createInstance($version) {
475
+        $class = $this->getClass($version);
476
+        try {
477
+            $s = \OCP\Server::get($class);
478
+
479
+            if (!$s instanceof IMigrationStep) {
480
+                throw new \InvalidArgumentException('Not a valid migration');
481
+            }
482
+        } catch (QueryException $e) {
483
+            if (class_exists($class)) {
484
+                $s = new $class();
485
+            } else {
486
+                throw new \InvalidArgumentException("Migration step '$class' is unknown");
487
+            }
488
+        }
489
+
490
+        return $s;
491
+    }
492
+
493
+    /**
494
+     * Executes one explicit version
495
+     *
496
+     * @param string $version
497
+     * @param bool $schemaOnly
498
+     * @throws \InvalidArgumentException
499
+     */
500
+    public function executeStep($version, $schemaOnly = false) {
501
+        $instance = $this->createInstance($version);
502
+
503
+        if (!$schemaOnly) {
504
+            $instance->preSchemaChange($this->output, function (): ISchemaWrapper {
505
+                return new SchemaWrapper($this->connection);
506
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
507
+        }
508
+
509
+        $toSchema = $instance->changeSchema($this->output, function (): ISchemaWrapper {
510
+            return new SchemaWrapper($this->connection);
511
+        }, ['tablePrefix' => $this->connection->getPrefix()]);
512
+
513
+        if ($toSchema instanceof SchemaWrapper) {
514
+            $targetSchema = $toSchema->getWrappedSchema();
515
+            $this->ensureUniqueNamesConstraints($targetSchema, $schemaOnly);
516
+            if ($this->checkOracle) {
517
+                $sourceSchema = $this->connection->createSchema();
518
+                $this->ensureOracleConstraints($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
519
+            }
520
+            $this->connection->migrateToSchema($targetSchema);
521
+            $toSchema->performDropTableCalls();
522
+        }
523
+
524
+        if (!$schemaOnly) {
525
+            $instance->postSchemaChange($this->output, function (): ISchemaWrapper {
526
+                return new SchemaWrapper($this->connection);
527
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
528
+        }
529
+
530
+        $this->markAsExecuted($version);
531
+    }
532
+
533
+    /**
534
+     * Naming constraints:
535
+     * - Tables names must be 30 chars or shorter (27 + oc_ prefix)
536
+     * - Column names must be 30 chars or shorter
537
+     * - Index names must be 30 chars or shorter
538
+     * - Sequence names must be 30 chars or shorter
539
+     * - Primary key names must be set or the table name 23 chars or shorter
540
+     *
541
+     * Data constraints:
542
+     * - Tables need a primary key (Not specific to Oracle, but required for performant clustering support)
543
+     * - Columns with "NotNull" can not have empty string as default value
544
+     * - Columns with "NotNull" can not have number 0 as default value
545
+     * - Columns with type "bool" (which is in fact integer of length 1) can not be "NotNull" as it can not store 0/false
546
+     * - Columns with type "string" can not be longer than 4.000 characters, use "text" instead
547
+     *
548
+     * @see https://github.com/nextcloud/documentation/blob/master/developer_manual/basics/storage/database.rst
549
+     *
550
+     * @param Schema $sourceSchema
551
+     * @param Schema $targetSchema
552
+     * @param int $prefixLength
553
+     * @throws \Doctrine\DBAL\Exception
554
+     */
555
+    public function ensureOracleConstraints(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
556
+        $sequences = $targetSchema->getSequences();
557
+
558
+        foreach ($targetSchema->getTables() as $table) {
559
+            try {
560
+                $sourceTable = $sourceSchema->getTable($table->getName());
561
+            } catch (SchemaException $e) {
562
+                if (\strlen($table->getName()) - $prefixLength > 27) {
563
+                    throw new \InvalidArgumentException('Table name "' . $table->getName() . '" is too long.');
564
+                }
565
+                $sourceTable = null;
566
+            }
567
+
568
+            foreach ($table->getColumns() as $thing) {
569
+                // If the table doesn't exist OR if the column doesn't exist in the table
570
+                if (!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) {
571
+                    if (\strlen($thing->getName()) > 30) {
572
+                        throw new \InvalidArgumentException('Column name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
573
+                    }
574
+
575
+                    if ($thing->getNotnull() && $thing->getDefault() === ''
576
+                        && $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
577
+                        throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
578
+                    }
579
+
580
+                    if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
581
+                        // Oracle doesn't support boolean column with non-null value
582
+                        if ($thing->getNotnull() && Type::lookupName($thing->getType()) === Types::BOOLEAN) {
583
+                            $thing->setNotnull(false);
584
+                        }
585
+                    }
586
+
587
+                    $sourceColumn = null;
588
+                } else {
589
+                    $sourceColumn = $sourceTable->getColumn($thing->getName());
590
+                }
591
+
592
+                // If the column was just created OR the length changed OR the type changed
593
+                // we will NOT detect invalid length if the column is not modified
594
+                if (($sourceColumn === null || $sourceColumn->getLength() !== $thing->getLength() || Type::lookupName($sourceColumn->getType()) !== Types::STRING)
595
+                    && $thing->getLength() > 4000 && Type::lookupName($thing->getType()) === Types::STRING) {
596
+                    throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type String, but exceeding the 4.000 length limit.');
597
+                }
598
+            }
599
+
600
+            foreach ($table->getIndexes() as $thing) {
601
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
602
+                    throw new \InvalidArgumentException('Index name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
603
+                }
604
+            }
605
+
606
+            foreach ($table->getForeignKeys() as $thing) {
607
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
608
+                    throw new \InvalidArgumentException('Foreign key name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
609
+                }
610
+            }
611
+
612
+            $primaryKey = $table->getPrimaryKey();
613
+            if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || $sourceTable->getPrimaryKey() === null)) {
614
+                $indexName = strtolower($primaryKey->getName());
615
+                $isUsingDefaultName = $indexName === 'primary';
616
+
617
+                if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_POSTGRES) {
618
+                    $defaultName = $table->getName() . '_pkey';
619
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
620
+
621
+                    if ($isUsingDefaultName) {
622
+                        $sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
623
+                        $sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
624
+                            return $sequence->getName() !== $sequenceName;
625
+                        });
626
+                    }
627
+                } elseif ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
628
+                    $defaultName = $table->getName() . '_seq';
629
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
630
+                }
631
+
632
+                if (!$isUsingDefaultName && \strlen($indexName) > 30) {
633
+                    throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
634
+                }
635
+                if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
636
+                    throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
637
+                }
638
+            } elseif (!$primaryKey instanceof Index && !$sourceTable instanceof Table) {
639
+                /** @var LoggerInterface $logger */
640
+                $logger = \OC::$server->get(LoggerInterface::class);
641
+                $logger->error('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
642
+                // throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
643
+            }
644
+        }
645
+
646
+        foreach ($sequences as $sequence) {
647
+            if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
648
+                throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" is too long.');
649
+            }
650
+        }
651
+    }
652
+
653
+    /**
654
+     * Ensure naming constraints
655
+     *
656
+     * Naming constraints:
657
+     * - Index, sequence and primary key names must be unique within a Postgres Schema
658
+     *
659
+     * Only on installation we want to break hard, so that all developers notice
660
+     * the bugs when installing the app on any database or CI, and can work on
661
+     * fixing their migrations before releasing a version incompatible with Postgres.
662
+     *
663
+     * In case of updates we might be running on production instances and the
664
+     * administrators being faced with the error would not know how to resolve it
665
+     * anyway. This can also happen with instances, that had the issue before the
666
+     * current update, so we don't want to make their life more complicated
667
+     * than needed.
668
+     *
669
+     * @param Schema $targetSchema
670
+     * @param bool $isInstalling
671
+     */
672
+    public function ensureUniqueNamesConstraints(Schema $targetSchema, bool $isInstalling): void {
673
+        $constraintNames = [];
674
+        $sequences = $targetSchema->getSequences();
675
+
676
+        foreach ($targetSchema->getTables() as $table) {
677
+            foreach ($table->getIndexes() as $thing) {
678
+                $indexName = strtolower($thing->getName());
679
+                if ($indexName === 'primary' || $thing->isPrimary()) {
680
+                    continue;
681
+                }
682
+
683
+                if (isset($constraintNames[$thing->getName()])) {
684
+                    if ($isInstalling) {
685
+                        throw new \InvalidArgumentException('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
686
+                    }
687
+                    $this->logErrorOrWarning('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
688
+                }
689
+                $constraintNames[$thing->getName()] = $table->getName();
690
+            }
691
+
692
+            foreach ($table->getForeignKeys() as $thing) {
693
+                if (isset($constraintNames[$thing->getName()])) {
694
+                    if ($isInstalling) {
695
+                        throw new \InvalidArgumentException('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
696
+                    }
697
+                    $this->logErrorOrWarning('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
698
+                }
699
+                $constraintNames[$thing->getName()] = $table->getName();
700
+            }
701
+
702
+            $primaryKey = $table->getPrimaryKey();
703
+            if ($primaryKey instanceof Index) {
704
+                $indexName = strtolower($primaryKey->getName());
705
+                if ($indexName === 'primary') {
706
+                    continue;
707
+                }
708
+
709
+                if (isset($constraintNames[$indexName])) {
710
+                    if ($isInstalling) {
711
+                        throw new \InvalidArgumentException('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
712
+                    }
713
+                    $this->logErrorOrWarning('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
714
+                }
715
+                $constraintNames[$indexName] = $table->getName();
716
+            }
717
+        }
718
+
719
+        foreach ($sequences as $sequence) {
720
+            if (isset($constraintNames[$sequence->getName()])) {
721
+                if ($isInstalling) {
722
+                    throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
723
+                }
724
+                $this->logErrorOrWarning('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
725
+            }
726
+            $constraintNames[$sequence->getName()] = 'sequence';
727
+        }
728
+    }
729
+
730
+    protected function logErrorOrWarning(string $log): void {
731
+        if ($this->output instanceof SimpleOutput) {
732
+            $this->output->warning($log);
733
+        } else {
734
+            $this->logger->error($log);
735
+        }
736
+    }
737
+
738
+    private function ensureMigrationsAreLoaded(): void {
739
+        if (empty($this->migrations)) {
740
+            $this->migrations = $this->findMigrations();
741
+        }
742
+    }
743 743
 }
Please login to merge, or discard this patch.
Spacing   +36 added lines, -36 removed lines patch added patch discarded remove patch
@@ -60,7 +60,7 @@  discard block
 block discarded – undo
60 60
 		}
61 61
 
62 62
 		if ($appName === 'core') {
63
-			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
63
+			$this->migrationsPath = \OC::$SERVERROOT.'/core/Migrations';
64 64
 			$this->migrationsNamespace = 'OC\\Core\\Migrations';
65 65
 			$this->checkOracle = true;
66 66
 		} else {
@@ -68,10 +68,10 @@  discard block
 block discarded – undo
68 68
 			$appPath = $appManager->getAppPath($appName);
69 69
 			$namespace = App::buildAppNamespace($appName);
70 70
 			$this->migrationsPath = "$appPath/lib/Migration";
71
-			$this->migrationsNamespace = $namespace . '\\Migration';
71
+			$this->migrationsNamespace = $namespace.'\\Migration';
72 72
 
73 73
 			$infoParser = new InfoParser();
74
-			$info = $infoParser->parse($appPath . '/appinfo/info.xml');
74
+			$info = $infoParser->parse($appPath.'/appinfo/info.xml');
75 75
 			if (!isset($info['dependencies']['database'])) {
76 76
 				$this->checkOracle = true;
77 77
 			} else {
@@ -199,8 +199,8 @@  discard block
 block discarded – undo
199 199
 		preg_match('/(\d+)Date(\d+)/', basename($a), $matchA);
200 200
 		preg_match('/(\d+)Date(\d+)/', basename($b), $matchB);
201 201
 		if (!empty($matchA) && !empty($matchB)) {
202
-			$versionA = (int)$matchA[1];
203
-			$versionB = (int)$matchB[1];
202
+			$versionA = (int) $matchA[1];
203
+			$versionB = (int) $matchB[1];
204 204
 			if ($versionA !== $versionB) {
205 205
 				return ($versionA < $versionB) ? -1 : 1;
206 206
 			}
@@ -233,7 +233,7 @@  discard block
 block discarded – undo
233 233
 
234 234
 		foreach ($files as $file) {
235 235
 			$className = basename($file, '.php');
236
-			$version = (string)substr($className, 7);
236
+			$version = (string) substr($className, 7);
237 237
 			if ($version === '0') {
238 238
 				throw new \InvalidArgumentException(
239 239
 					"Cannot load a migrations with the name '$version' because it is a reserved number"
@@ -295,7 +295,7 @@  discard block
 block discarded – undo
295 295
 	 * @return string
296 296
 	 */
297 297
 	public function getMigrationsTableName() {
298
-		return $this->connection->getPrefix() . 'migrations';
298
+		return $this->connection->getPrefix().'migrations';
299 299
 	}
300 300
 
301 301
 	/**
@@ -350,7 +350,7 @@  discard block
 block discarded – undo
350 350
 			return null;
351 351
 		}
352 352
 
353
-		return (string)$versions[$offset + $delta];
353
+		return (string) $versions[$offset + $delta];
354 354
 	}
355 355
 
356 356
 	private function getCurrentVersion(): string {
@@ -401,7 +401,7 @@  discard block
 block discarded – undo
401 401
 			} catch (\Exception $e) {
402 402
 				// The exception itself does not contain the name of the migration,
403 403
 				// so we wrap it here, to make debugging easier.
404
-				throw new \Exception('Database error when running migration ' . $version . ' for app ' . $this->getApp() . PHP_EOL . $e->getMessage(), 0, $e);
404
+				throw new \Exception('Database error when running migration '.$version.' for app '.$this->getApp().PHP_EOL.$e->getMessage(), 0, $e);
405 405
 			}
406 406
 		}
407 407
 	}
@@ -420,10 +420,10 @@  discard block
 block discarded – undo
420 420
 
421 421
 		$toSchema = null;
422 422
 		foreach ($toBeExecuted as $version) {
423
-			$this->output->debug('- Reading ' . $version);
423
+			$this->output->debug('- Reading '.$version);
424 424
 			$instance = $this->createInstance($version);
425 425
 
426
-			$toSchema = $instance->changeSchema($this->output, function () use ($toSchema): ISchemaWrapper {
426
+			$toSchema = $instance->changeSchema($this->output, function() use ($toSchema): ISchemaWrapper {
427 427
 				return $toSchema ?: new SchemaWrapper($this->connection);
428 428
 			}, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
429 429
 		}
@@ -501,12 +501,12 @@  discard block
 block discarded – undo
501 501
 		$instance = $this->createInstance($version);
502 502
 
503 503
 		if (!$schemaOnly) {
504
-			$instance->preSchemaChange($this->output, function (): ISchemaWrapper {
504
+			$instance->preSchemaChange($this->output, function(): ISchemaWrapper {
505 505
 				return new SchemaWrapper($this->connection);
506 506
 			}, ['tablePrefix' => $this->connection->getPrefix()]);
507 507
 		}
508 508
 
509
-		$toSchema = $instance->changeSchema($this->output, function (): ISchemaWrapper {
509
+		$toSchema = $instance->changeSchema($this->output, function(): ISchemaWrapper {
510 510
 			return new SchemaWrapper($this->connection);
511 511
 		}, ['tablePrefix' => $this->connection->getPrefix()]);
512 512
 
@@ -522,7 +522,7 @@  discard block
 block discarded – undo
522 522
 		}
523 523
 
524 524
 		if (!$schemaOnly) {
525
-			$instance->postSchemaChange($this->output, function (): ISchemaWrapper {
525
+			$instance->postSchemaChange($this->output, function(): ISchemaWrapper {
526 526
 				return new SchemaWrapper($this->connection);
527 527
 			}, ['tablePrefix' => $this->connection->getPrefix()]);
528 528
 		}
@@ -560,7 +560,7 @@  discard block
 block discarded – undo
560 560
 				$sourceTable = $sourceSchema->getTable($table->getName());
561 561
 			} catch (SchemaException $e) {
562 562
 				if (\strlen($table->getName()) - $prefixLength > 27) {
563
-					throw new \InvalidArgumentException('Table name "' . $table->getName() . '" is too long.');
563
+					throw new \InvalidArgumentException('Table name "'.$table->getName().'" is too long.');
564 564
 				}
565 565
 				$sourceTable = null;
566 566
 			}
@@ -569,12 +569,12 @@  discard block
 block discarded – undo
569 569
 				// If the table doesn't exist OR if the column doesn't exist in the table
570 570
 				if (!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) {
571 571
 					if (\strlen($thing->getName()) > 30) {
572
-						throw new \InvalidArgumentException('Column name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
572
+						throw new \InvalidArgumentException('Column name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
573 573
 					}
574 574
 
575 575
 					if ($thing->getNotnull() && $thing->getDefault() === ''
576 576
 						&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
577
-						throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
577
+						throw new \InvalidArgumentException('Column "'.$table->getName().'"."'.$thing->getName().'" is NotNull, but has empty string or null as default.');
578 578
 					}
579 579
 
580 580
 					if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
@@ -593,19 +593,19 @@  discard block
 block discarded – undo
593 593
 				// we will NOT detect invalid length if the column is not modified
594 594
 				if (($sourceColumn === null || $sourceColumn->getLength() !== $thing->getLength() || Type::lookupName($sourceColumn->getType()) !== Types::STRING)
595 595
 					&& $thing->getLength() > 4000 && Type::lookupName($thing->getType()) === Types::STRING) {
596
-					throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type String, but exceeding the 4.000 length limit.');
596
+					throw new \InvalidArgumentException('Column "'.$table->getName().'"."'.$thing->getName().'" is type String, but exceeding the 4.000 length limit.');
597 597
 				}
598 598
 			}
599 599
 
600 600
 			foreach ($table->getIndexes() as $thing) {
601 601
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
602
-					throw new \InvalidArgumentException('Index name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
602
+					throw new \InvalidArgumentException('Index name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
603 603
 				}
604 604
 			}
605 605
 
606 606
 			foreach ($table->getForeignKeys() as $thing) {
607 607
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
608
-					throw new \InvalidArgumentException('Foreign key name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
608
+					throw new \InvalidArgumentException('Foreign key name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
609 609
 				}
610 610
 			}
611 611
 
@@ -615,37 +615,37 @@  discard block
 block discarded – undo
615 615
 				$isUsingDefaultName = $indexName === 'primary';
616 616
 
617 617
 				if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_POSTGRES) {
618
-					$defaultName = $table->getName() . '_pkey';
618
+					$defaultName = $table->getName().'_pkey';
619 619
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
620 620
 
621 621
 					if ($isUsingDefaultName) {
622
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
623
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
622
+						$sequenceName = $table->getName().'_'.implode('_', $primaryKey->getColumns()).'_seq';
623
+						$sequences = array_filter($sequences, function(Sequence $sequence) use ($sequenceName) {
624 624
 							return $sequence->getName() !== $sequenceName;
625 625
 						});
626 626
 					}
627 627
 				} elseif ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
628
-					$defaultName = $table->getName() . '_seq';
628
+					$defaultName = $table->getName().'_seq';
629 629
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
630 630
 				}
631 631
 
632 632
 				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
633
-					throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
633
+					throw new \InvalidArgumentException('Primary index name on "'.$table->getName().'" is too long.');
634 634
 				}
635 635
 				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
636
-					throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
636
+					throw new \InvalidArgumentException('Primary index name on "'.$table->getName().'" is too long.');
637 637
 				}
638 638
 			} elseif (!$primaryKey instanceof Index && !$sourceTable instanceof Table) {
639 639
 				/** @var LoggerInterface $logger */
640 640
 				$logger = \OC::$server->get(LoggerInterface::class);
641
-				$logger->error('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
641
+				$logger->error('Table "'.$table->getName().'" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
642 642
 				// throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
643 643
 			}
644 644
 		}
645 645
 
646 646
 		foreach ($sequences as $sequence) {
647 647
 			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
648
-				throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" is too long.');
648
+				throw new \InvalidArgumentException('Sequence name "'.$sequence->getName().'" is too long.');
649 649
 			}
650 650
 		}
651 651
 	}
@@ -682,9 +682,9 @@  discard block
 block discarded – undo
682 682
 
683 683
 				if (isset($constraintNames[$thing->getName()])) {
684 684
 					if ($isInstalling) {
685
-						throw new \InvalidArgumentException('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
685
+						throw new \InvalidArgumentException('Index name "'.$thing->getName().'" for table "'.$table->getName().'" collides with the constraint on table "'.$constraintNames[$thing->getName()].'".');
686 686
 					}
687
-					$this->logErrorOrWarning('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
687
+					$this->logErrorOrWarning('Index name "'.$thing->getName().'" for table "'.$table->getName().'" collides with the constraint on table "'.$constraintNames[$thing->getName()].'".');
688 688
 				}
689 689
 				$constraintNames[$thing->getName()] = $table->getName();
690 690
 			}
@@ -692,9 +692,9 @@  discard block
 block discarded – undo
692 692
 			foreach ($table->getForeignKeys() as $thing) {
693 693
 				if (isset($constraintNames[$thing->getName()])) {
694 694
 					if ($isInstalling) {
695
-						throw new \InvalidArgumentException('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
695
+						throw new \InvalidArgumentException('Foreign key name "'.$thing->getName().'" for table "'.$table->getName().'" collides with the constraint on table "'.$constraintNames[$thing->getName()].'".');
696 696
 					}
697
-					$this->logErrorOrWarning('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
697
+					$this->logErrorOrWarning('Foreign key name "'.$thing->getName().'" for table "'.$table->getName().'" collides with the constraint on table "'.$constraintNames[$thing->getName()].'".');
698 698
 				}
699 699
 				$constraintNames[$thing->getName()] = $table->getName();
700 700
 			}
@@ -708,9 +708,9 @@  discard block
 block discarded – undo
708 708
 
709 709
 				if (isset($constraintNames[$indexName])) {
710 710
 					if ($isInstalling) {
711
-						throw new \InvalidArgumentException('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
711
+						throw new \InvalidArgumentException('Primary index name "'.$indexName.'" for table "'.$table->getName().'" collides with the constraint on table "'.$constraintNames[$thing->getName()].'".');
712 712
 					}
713
-					$this->logErrorOrWarning('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
713
+					$this->logErrorOrWarning('Primary index name "'.$indexName.'" for table "'.$table->getName().'" collides with the constraint on table "'.$constraintNames[$thing->getName()].'".');
714 714
 				}
715 715
 				$constraintNames[$indexName] = $table->getName();
716 716
 			}
@@ -719,9 +719,9 @@  discard block
 block discarded – undo
719 719
 		foreach ($sequences as $sequence) {
720 720
 			if (isset($constraintNames[$sequence->getName()])) {
721 721
 				if ($isInstalling) {
722
-					throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
722
+					throw new \InvalidArgumentException('Sequence name "'.$sequence->getName().'" for table "'.$table->getName().'" collides with the constraint on table "'.$constraintNames[$thing->getName()].'".');
723 723
 				}
724
-				$this->logErrorOrWarning('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
724
+				$this->logErrorOrWarning('Sequence name "'.$sequence->getName().'" for table "'.$table->getName().'" collides with the constraint on table "'.$constraintNames[$thing->getName()].'".');
725 725
 			}
726 726
 			$constraintNames[$sequence->getName()] = 'sequence';
727 727
 		}
Please login to merge, or discard this patch.
lib/private/User/Manager.php 1 patch
Indentation   +768 added lines, -768 removed lines patch added patch discarded remove patch
@@ -53,772 +53,772 @@
 block discarded – undo
53 53
  * @package OC\User
54 54
  */
55 55
 class Manager extends PublicEmitter implements IUserManager {
56
-	/**
57
-	 * @var UserInterface[] $backends
58
-	 */
59
-	private array $backends = [];
60
-
61
-	/**
62
-	 * @var array<string,\OC\User\User> $cachedUsers
63
-	 */
64
-	private array $cachedUsers = [];
65
-
66
-	private ICache $cache;
67
-
68
-	private DisplayNameCache $displayNameCache;
69
-
70
-	public function __construct(
71
-		private IConfig $config,
72
-		ICacheFactory $cacheFactory,
73
-		private IEventDispatcher $eventDispatcher,
74
-		private LoggerInterface $logger,
75
-	) {
76
-		$this->cache = new WithLocalCache($cacheFactory->createDistributed('user_backend_map'));
77
-		$this->listen('\OC\User', 'postDelete', function (IUser $user): void {
78
-			unset($this->cachedUsers[$user->getUID()]);
79
-		});
80
-		$this->displayNameCache = new DisplayNameCache($cacheFactory, $this);
81
-	}
82
-
83
-	/**
84
-	 * Get the active backends
85
-	 * @return UserInterface[]
86
-	 */
87
-	public function getBackends(): array {
88
-		return $this->backends;
89
-	}
90
-
91
-	public function registerBackend(UserInterface $backend): void {
92
-		$this->backends[] = $backend;
93
-	}
94
-
95
-	public function removeBackend(UserInterface $backend): void {
96
-		$this->cachedUsers = [];
97
-		if (($i = array_search($backend, $this->backends)) !== false) {
98
-			unset($this->backends[$i]);
99
-		}
100
-	}
101
-
102
-	public function clearBackends(): void {
103
-		$this->cachedUsers = [];
104
-		$this->backends = [];
105
-	}
106
-
107
-	/**
108
-	 * get a user by user id
109
-	 *
110
-	 * @param string $uid
111
-	 * @return \OC\User\User|null Either the user or null if the specified user does not exist
112
-	 */
113
-	public function get($uid) {
114
-		if (is_null($uid) || $uid === '' || $uid === false) {
115
-			return null;
116
-		}
117
-		if (isset($this->cachedUsers[$uid])) { //check the cache first to prevent having to loop over the backends
118
-			return $this->cachedUsers[$uid];
119
-		}
120
-
121
-		if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
122
-			return null;
123
-		}
124
-
125
-		$cachedBackend = $this->cache->get(sha1($uid));
126
-		if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
127
-			// Cache has the info of the user backend already, so ask that one directly
128
-			$backend = $this->backends[$cachedBackend];
129
-			if ($backend->userExists($uid)) {
130
-				return $this->getUserObject($uid, $backend);
131
-			}
132
-		}
133
-
134
-		foreach ($this->backends as $i => $backend) {
135
-			if ($i === $cachedBackend) {
136
-				// Tried that one already
137
-				continue;
138
-			}
139
-
140
-			if ($backend->userExists($uid)) {
141
-				// Hash $uid to ensure that only valid characters are used for the cache key
142
-				$this->cache->set(sha1($uid), $i, 300);
143
-				return $this->getUserObject($uid, $backend);
144
-			}
145
-		}
146
-		return null;
147
-	}
148
-
149
-	public function getDisplayName(string $uid): ?string {
150
-		return $this->displayNameCache->getDisplayName($uid);
151
-	}
152
-
153
-	/**
154
-	 * get or construct the user object
155
-	 *
156
-	 * @param string $uid
157
-	 * @param \OCP\UserInterface $backend
158
-	 * @param bool $cacheUser If false the newly created user object will not be cached
159
-	 * @return \OC\User\User
160
-	 */
161
-	public function getUserObject($uid, $backend, $cacheUser = true) {
162
-		if ($backend instanceof IGetRealUIDBackend) {
163
-			$uid = $backend->getRealUID($uid);
164
-		}
165
-
166
-		if (isset($this->cachedUsers[$uid])) {
167
-			return $this->cachedUsers[$uid];
168
-		}
169
-
170
-		$user = new User($uid, $backend, $this->eventDispatcher, $this, $this->config);
171
-		if ($cacheUser) {
172
-			$this->cachedUsers[$uid] = $user;
173
-		}
174
-		return $user;
175
-	}
176
-
177
-	/**
178
-	 * check if a user exists
179
-	 *
180
-	 * @param string $uid
181
-	 * @return bool
182
-	 */
183
-	public function userExists($uid) {
184
-		if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
185
-			return false;
186
-		}
187
-
188
-		$user = $this->get($uid);
189
-		return ($user !== null);
190
-	}
191
-
192
-	/**
193
-	 * Check if the password is valid for the user
194
-	 *
195
-	 * @param string $loginName
196
-	 * @param string $password
197
-	 * @return IUser|false the User object on success, false otherwise
198
-	 */
199
-	public function checkPassword($loginName, $password) {
200
-		$result = $this->checkPasswordNoLogging($loginName, $password);
201
-
202
-		if ($result === false) {
203
-			$this->logger->warning('Login failed: \'' . $loginName . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
204
-		}
205
-
206
-		return $result;
207
-	}
208
-
209
-	/**
210
-	 * Check if the password is valid for the user
211
-	 *
212
-	 * @internal
213
-	 * @param string $loginName
214
-	 * @param string $password
215
-	 * @return IUser|false the User object on success, false otherwise
216
-	 */
217
-	public function checkPasswordNoLogging($loginName, $password) {
218
-		$loginName = str_replace("\0", '', $loginName);
219
-		$password = str_replace("\0", '', $password);
220
-
221
-		$cachedBackend = $this->cache->get($loginName);
222
-		if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
223
-			$backends = [$this->backends[$cachedBackend]];
224
-		} else {
225
-			$backends = $this->backends;
226
-		}
227
-		foreach ($backends as $backend) {
228
-			if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
229
-				/** @var ICheckPasswordBackend $backend */
230
-				$uid = $backend->checkPassword($loginName, $password);
231
-				if ($uid !== false) {
232
-					return $this->getUserObject($uid, $backend);
233
-				}
234
-			}
235
-		}
236
-
237
-		// since http basic auth doesn't provide a standard way of handling non ascii password we allow password to be urlencoded
238
-		// we only do this decoding after using the plain password fails to maintain compatibility with any password that happens
239
-		// to contain urlencoded patterns by "accident".
240
-		$password = urldecode($password);
241
-
242
-		foreach ($backends as $backend) {
243
-			if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
244
-				/** @var ICheckPasswordBackend|UserInterface $backend */
245
-				$uid = $backend->checkPassword($loginName, $password);
246
-				if ($uid !== false) {
247
-					return $this->getUserObject($uid, $backend);
248
-				}
249
-			}
250
-		}
251
-
252
-		return false;
253
-	}
254
-
255
-	public function search($pattern, $limit = null, $offset = null) {
256
-		$users = [];
257
-		foreach ($this->backends as $backend) {
258
-			$backendUsers = $backend->getUsers($pattern, $limit, $offset);
259
-			if (is_array($backendUsers)) {
260
-				foreach ($backendUsers as $uid) {
261
-					$users[$uid] = new LazyUser($uid, $this, null, $backend);
262
-				}
263
-			}
264
-		}
265
-
266
-		uasort($users, function (IUser $a, IUser $b) {
267
-			return strcasecmp($a->getUID(), $b->getUID());
268
-		});
269
-		return $users;
270
-	}
271
-
272
-	public function searchDisplayName($pattern, $limit = null, $offset = null) {
273
-		$users = [];
274
-		foreach ($this->backends as $backend) {
275
-			$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
276
-			if (is_array($backendUsers)) {
277
-				foreach ($backendUsers as $uid => $displayName) {
278
-					$users[] = new LazyUser($uid, $this, $displayName, $backend);
279
-				}
280
-			}
281
-		}
282
-
283
-		usort($users, function (IUser $a, IUser $b) {
284
-			return strcasecmp($a->getDisplayName(), $b->getDisplayName());
285
-		});
286
-		return $users;
287
-	}
288
-
289
-	/**
290
-	 * @return IUser[]
291
-	 */
292
-	public function getDisabledUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
293
-		$users = $this->config->getUsersForUserValue('core', 'enabled', 'false');
294
-		$users = array_combine(
295
-			$users,
296
-			array_map(
297
-				fn (string $uid): IUser => new LazyUser($uid, $this),
298
-				$users
299
-			)
300
-		);
301
-		if ($search !== '') {
302
-			$users = array_filter(
303
-				$users,
304
-				function (IUser $user) use ($search): bool {
305
-					try {
306
-						return mb_stripos($user->getUID(), $search) !== false
307
-						|| mb_stripos($user->getDisplayName(), $search) !== false
308
-						|| mb_stripos($user->getEMailAddress() ?? '', $search) !== false;
309
-					} catch (NoUserException $ex) {
310
-						$this->logger->error('Error while filtering disabled users', ['exception' => $ex, 'userUID' => $user->getUID()]);
311
-						return false;
312
-					}
313
-				});
314
-		}
315
-
316
-		$tempLimit = ($limit === null ? null : $limit + $offset);
317
-		foreach ($this->backends as $backend) {
318
-			if (($tempLimit !== null) && (count($users) >= $tempLimit)) {
319
-				break;
320
-			}
321
-			if ($backend instanceof IProvideEnabledStateBackend) {
322
-				$backendUsers = $backend->getDisabledUserList(($tempLimit === null ? null : $tempLimit - count($users)), 0, $search);
323
-				foreach ($backendUsers as $uid) {
324
-					$users[$uid] = new LazyUser($uid, $this, null, $backend);
325
-				}
326
-			}
327
-		}
328
-
329
-		return array_slice($users, $offset, $limit);
330
-	}
331
-
332
-	/**
333
-	 * Search known users (from phonebook sync) by displayName
334
-	 *
335
-	 * @param string $searcher
336
-	 * @param string $pattern
337
-	 * @param int|null $limit
338
-	 * @param int|null $offset
339
-	 * @return IUser[]
340
-	 */
341
-	public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
342
-		$users = [];
343
-		foreach ($this->backends as $backend) {
344
-			if ($backend instanceof ISearchKnownUsersBackend) {
345
-				$backendUsers = $backend->searchKnownUsersByDisplayName($searcher, $pattern, $limit, $offset);
346
-			} else {
347
-				// Better than nothing, but filtering after pagination can remove lots of results.
348
-				$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
349
-			}
350
-			if (is_array($backendUsers)) {
351
-				foreach ($backendUsers as $uid => $displayName) {
352
-					$users[] = $this->getUserObject($uid, $backend);
353
-				}
354
-			}
355
-		}
356
-
357
-		usort($users, function ($a, $b) {
358
-			/**
359
-			 * @var IUser $a
360
-			 * @var IUser $b
361
-			 */
362
-			return strcasecmp($a->getDisplayName(), $b->getDisplayName());
363
-		});
364
-		return $users;
365
-	}
366
-
367
-	/**
368
-	 * @param string $uid
369
-	 * @param string $password
370
-	 * @return false|IUser the created user or false
371
-	 * @throws \InvalidArgumentException
372
-	 * @throws HintException
373
-	 */
374
-	public function createUser($uid, $password) {
375
-		// DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
376
-		/** @var IAssertion $assertion */
377
-		$assertion = \OC::$server->get(IAssertion::class);
378
-		$assertion->createUserIsLegit();
379
-
380
-		$localBackends = [];
381
-		foreach ($this->backends as $backend) {
382
-			if ($backend instanceof Database) {
383
-				// First check if there is another user backend
384
-				$localBackends[] = $backend;
385
-				continue;
386
-			}
387
-
388
-			if ($backend->implementsActions(Backend::CREATE_USER)) {
389
-				return $this->createUserFromBackend($uid, $password, $backend);
390
-			}
391
-		}
392
-
393
-		foreach ($localBackends as $backend) {
394
-			if ($backend->implementsActions(Backend::CREATE_USER)) {
395
-				return $this->createUserFromBackend($uid, $password, $backend);
396
-			}
397
-		}
398
-
399
-		return false;
400
-	}
401
-
402
-	/**
403
-	 * @param string $uid
404
-	 * @param string $password
405
-	 * @param UserInterface $backend
406
-	 * @return IUser|false
407
-	 * @throws \InvalidArgumentException
408
-	 */
409
-	public function createUserFromBackend($uid, $password, UserInterface $backend) {
410
-		$l = \OCP\Util::getL10N('lib');
411
-
412
-		$this->validateUserId($uid, true);
413
-
414
-		// No empty password
415
-		if (trim($password) === '') {
416
-			throw new \InvalidArgumentException($l->t('A valid password must be provided'));
417
-		}
418
-
419
-		// Check if user already exists
420
-		if ($this->userExists($uid)) {
421
-			throw new \InvalidArgumentException($l->t('The Login is already being used'));
422
-		}
423
-
424
-		/** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
425
-		$this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
426
-		$this->eventDispatcher->dispatchTyped(new BeforeUserCreatedEvent($uid, $password));
427
-		$state = $backend->createUser($uid, $password);
428
-		if ($state === false) {
429
-			throw new \InvalidArgumentException($l->t('Could not create account'));
430
-		}
431
-		$user = $this->getUserObject($uid, $backend);
432
-		if ($user instanceof IUser) {
433
-			/** @deprecated 21.0.0 use UserCreatedEvent event with the IEventDispatcher instead */
434
-			$this->emit('\OC\User', 'postCreateUser', [$user, $password]);
435
-			$this->eventDispatcher->dispatchTyped(new UserCreatedEvent($user, $password));
436
-			return $user;
437
-		}
438
-		return false;
439
-	}
440
-
441
-	/**
442
-	 * returns how many users per backend exist (if supported by backend)
443
-	 *
444
-	 * @param boolean $hasLoggedIn when true only users that have a lastLogin
445
-	 *                             entry in the preferences table will be affected
446
-	 * @return array<string, int> an array of backend class as key and count number as value
447
-	 */
448
-	public function countUsers() {
449
-		$userCountStatistics = [];
450
-		foreach ($this->backends as $backend) {
451
-			if ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
452
-				/** @var ICountUsersBackend|IUserBackend $backend */
453
-				$backendUsers = $backend->countUsers();
454
-				if ($backendUsers !== false) {
455
-					if ($backend instanceof IUserBackend) {
456
-						$name = $backend->getBackendName();
457
-					} else {
458
-						$name = get_class($backend);
459
-					}
460
-					if (isset($userCountStatistics[$name])) {
461
-						$userCountStatistics[$name] += $backendUsers;
462
-					} else {
463
-						$userCountStatistics[$name] = $backendUsers;
464
-					}
465
-				}
466
-			}
467
-		}
468
-		return $userCountStatistics;
469
-	}
470
-
471
-	public function countUsersTotal(int $limit = 0, bool $onlyMappedUsers = false): int|false {
472
-		$userCount = false;
473
-
474
-		foreach ($this->backends as $backend) {
475
-			if ($onlyMappedUsers && $backend instanceof ICountMappedUsersBackend) {
476
-				$backendUsers = $backend->countMappedUsers();
477
-			} elseif ($backend instanceof ILimitAwareCountUsersBackend) {
478
-				$backendUsers = $backend->countUsers($limit);
479
-			} elseif ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
480
-				/** @var ICountUsersBackend $backend */
481
-				$backendUsers = $backend->countUsers();
482
-			} else {
483
-				$this->logger->debug('Skip backend for user count: ' . get_class($backend));
484
-				continue;
485
-			}
486
-			if ($backendUsers !== false) {
487
-				$userCount = (int)$userCount + $backendUsers;
488
-				if ($limit > 0) {
489
-					if ($userCount >= $limit) {
490
-						break;
491
-					}
492
-					$limit -= $userCount;
493
-				}
494
-			} else {
495
-				$this->logger->warning('Can not determine user count for ' . get_class($backend));
496
-			}
497
-		}
498
-		return $userCount;
499
-	}
500
-
501
-	/**
502
-	 * returns how many users per backend exist in the requested groups (if supported by backend)
503
-	 *
504
-	 * @param IGroup[] $groups an array of groups to search in
505
-	 * @param int $limit limit to stop counting
506
-	 * @return array{int,int} total number of users, and number of disabled users in the given groups, below $limit. If limit is reached, -1 is returned for number of disabled users
507
-	 */
508
-	public function countUsersAndDisabledUsersOfGroups(array $groups, int $limit): array {
509
-		$users = [];
510
-		$disabled = [];
511
-		foreach ($groups as $group) {
512
-			foreach ($group->getUsers() as $user) {
513
-				$users[$user->getUID()] = 1;
514
-				if (!$user->isEnabled()) {
515
-					$disabled[$user->getUID()] = 1;
516
-				}
517
-				if (count($users) >= $limit) {
518
-					return [count($users),-1];
519
-				}
520
-			}
521
-		}
522
-		return [count($users),count($disabled)];
523
-	}
524
-
525
-	/**
526
-	 * The callback is executed for each user on each backend.
527
-	 * If the callback returns false no further users will be retrieved.
528
-	 *
529
-	 * @psalm-param \Closure(\OCP\IUser):?bool $callback
530
-	 * @param string $search
531
-	 * @param boolean $onlySeen when true only users that have a lastLogin entry
532
-	 *                          in the preferences table will be affected
533
-	 * @since 9.0.0
534
-	 */
535
-	public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
536
-		if ($onlySeen) {
537
-			$this->callForSeenUsers($callback);
538
-		} else {
539
-			foreach ($this->getBackends() as $backend) {
540
-				$limit = 500;
541
-				$offset = 0;
542
-				do {
543
-					$users = $backend->getUsers($search, $limit, $offset);
544
-					foreach ($users as $uid) {
545
-						if (!$backend->userExists($uid)) {
546
-							continue;
547
-						}
548
-						$user = $this->getUserObject($uid, $backend, false);
549
-						$return = $callback($user);
550
-						if ($return === false) {
551
-							break;
552
-						}
553
-					}
554
-					$offset += $limit;
555
-				} while (count($users) >= $limit);
556
-			}
557
-		}
558
-	}
559
-
560
-	/**
561
-	 * returns how many users are disabled
562
-	 *
563
-	 * @return int
564
-	 * @since 12.0.0
565
-	 */
566
-	public function countDisabledUsers(): int {
567
-		$queryBuilder = Server::get(IDBConnection::class)->getQueryBuilder();
568
-		$queryBuilder->select($queryBuilder->func()->count('*'))
569
-			->from('preferences')
570
-			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
571
-			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
572
-			->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR));
573
-
574
-
575
-		$result = $queryBuilder->executeQuery();
576
-		$count = $result->fetchOne();
577
-		$result->closeCursor();
578
-
579
-		if ($count !== false) {
580
-			$count = (int)$count;
581
-		} else {
582
-			$count = 0;
583
-		}
584
-
585
-		return $count;
586
-	}
587
-
588
-	/**
589
-	 * returns how many users have logged in once
590
-	 *
591
-	 * @return int
592
-	 * @since 11.0.0
593
-	 */
594
-	public function countSeenUsers() {
595
-		$queryBuilder = Server::get(IDBConnection::class)->getQueryBuilder();
596
-		$queryBuilder->select($queryBuilder->func()->count('*'))
597
-			->from('preferences')
598
-			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('login')))
599
-			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('lastLogin')));
600
-
601
-		$query = $queryBuilder->executeQuery();
602
-
603
-		$result = (int)$query->fetchOne();
604
-		$query->closeCursor();
605
-
606
-		return $result;
607
-	}
608
-
609
-	public function callForSeenUsers(\Closure $callback) {
610
-		$users = $this->getSeenUsers();
611
-		foreach ($users as $user) {
612
-			$return = $callback($user);
613
-			if ($return === false) {
614
-				return;
615
-			}
616
-		}
617
-	}
618
-
619
-	/**
620
-	 * Getting all userIds that have a lastLogin value requires checking the
621
-	 * value in php because on oracle you cannot use a clob in a where clause,
622
-	 * preventing us from doing a not null or length(value) > 0 check.
623
-	 *
624
-	 * @param int $limit
625
-	 * @param int $offset
626
-	 * @return string[] with user ids
627
-	 */
628
-	private function getSeenUserIds($limit = null, $offset = null) {
629
-		$queryBuilder = Server::get(IDBConnection::class)->getQueryBuilder();
630
-		$queryBuilder->select(['userid'])
631
-			->from('preferences')
632
-			->where($queryBuilder->expr()->eq(
633
-				'appid', $queryBuilder->createNamedParameter('login'))
634
-			)
635
-			->andWhere($queryBuilder->expr()->eq(
636
-				'configkey', $queryBuilder->createNamedParameter('lastLogin'))
637
-			)
638
-			->andWhere($queryBuilder->expr()->isNotNull('configvalue')
639
-			);
640
-
641
-		if ($limit !== null) {
642
-			$queryBuilder->setMaxResults($limit);
643
-		}
644
-		if ($offset !== null) {
645
-			$queryBuilder->setFirstResult($offset);
646
-		}
647
-		$query = $queryBuilder->executeQuery();
648
-		$result = [];
649
-
650
-		while ($row = $query->fetch()) {
651
-			$result[] = $row['userid'];
652
-		}
653
-
654
-		$query->closeCursor();
655
-
656
-		return $result;
657
-	}
658
-
659
-	/**
660
-	 * @param string $email
661
-	 * @return IUser[]
662
-	 * @since 9.1.0
663
-	 */
664
-	public function getByEmail($email) {
665
-		// looking for 'email' only (and not primary_mail) is intentional
666
-		$userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email);
667
-
668
-		$users = array_map(function ($uid) {
669
-			return $this->get($uid);
670
-		}, $userIds);
671
-
672
-		return array_values(array_filter($users, function ($u) {
673
-			return ($u instanceof IUser);
674
-		}));
675
-	}
676
-
677
-	/**
678
-	 * @param string $uid
679
-	 * @param bool $checkDataDirectory
680
-	 * @throws \InvalidArgumentException Message is an already translated string with a reason why the id is not valid
681
-	 * @since 26.0.0
682
-	 */
683
-	public function validateUserId(string $uid, bool $checkDataDirectory = false): void {
684
-		$l = Server::get(IFactory::class)->get('lib');
685
-
686
-		// Check the ID for bad characters
687
-		// Allowed are: "a-z", "A-Z", "0-9", spaces and "_.@-'"
688
-		if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) {
689
-			throw new \InvalidArgumentException($l->t('Only the following characters are allowed in an Login:'
690
-				. ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'));
691
-		}
692
-
693
-		// No empty user ID
694
-		if (trim($uid) === '') {
695
-			throw new \InvalidArgumentException($l->t('A valid Login must be provided'));
696
-		}
697
-
698
-		// No whitespace at the beginning or at the end
699
-		if (trim($uid) !== $uid) {
700
-			throw new \InvalidArgumentException($l->t('Login contains whitespace at the beginning or at the end'));
701
-		}
702
-
703
-		// User ID only consists of 1 or 2 dots (directory traversal)
704
-		if ($uid === '.' || $uid === '..') {
705
-			throw new \InvalidArgumentException($l->t('Login must not consist of dots only'));
706
-		}
707
-
708
-		// User ID is too long
709
-		if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
710
-			// TRANSLATORS User ID is too long
711
-			throw new \InvalidArgumentException($l->t('Username is too long'));
712
-		}
713
-
714
-		if (!$this->verifyUid($uid, $checkDataDirectory)) {
715
-			throw new \InvalidArgumentException($l->t('Login is invalid because files already exist for this user'));
716
-		}
717
-	}
718
-
719
-	/**
720
-	 * Gets the list of user ids sorted by lastLogin, from most recent to least recent
721
-	 *
722
-	 * @param int|null $limit how many users to fetch (default: 25, max: 100)
723
-	 * @param int $offset from which offset to fetch
724
-	 * @param string $search search users based on search params
725
-	 * @return list<string> list of user IDs
726
-	 */
727
-	public function getLastLoggedInUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
728
-		// We can't load all users who already logged in
729
-		$limit = min(100, $limit ?: 25);
730
-
731
-		$connection = Server::get(IDBConnection::class);
732
-		$queryBuilder = $connection->getQueryBuilder();
733
-		$queryBuilder->select('pref_login.userid')
734
-			->from('preferences', 'pref_login')
735
-			->where($queryBuilder->expr()->eq('pref_login.appid', $queryBuilder->expr()->literal('login')))
736
-			->andWhere($queryBuilder->expr()->eq('pref_login.configkey', $queryBuilder->expr()->literal('lastLogin')))
737
-			->setFirstResult($offset)
738
-			->setMaxResults($limit)
739
-		;
740
-
741
-		// Oracle don't want to run ORDER BY on CLOB column
742
-		$loginOrder = $connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE
743
-			? $queryBuilder->expr()->castColumn('pref_login.configvalue', IQueryBuilder::PARAM_INT)
744
-			: 'pref_login.configvalue';
745
-		$queryBuilder
746
-			->orderBy($loginOrder, 'DESC')
747
-			->addOrderBy($queryBuilder->func()->lower('pref_login.userid'), 'ASC');
748
-
749
-		if ($search !== '') {
750
-			$displayNameMatches = $this->searchDisplayName($search);
751
-			$matchedUids = array_map(static fn (IUser $u): string => $u->getUID(), $displayNameMatches);
752
-
753
-			$queryBuilder
754
-				->leftJoin('pref_login', 'preferences', 'pref_email', $queryBuilder->expr()->andX(
755
-					$queryBuilder->expr()->eq('pref_login.userid', 'pref_email.userid'),
756
-					$queryBuilder->expr()->eq('pref_email.appid', $queryBuilder->expr()->literal('settings')),
757
-					$queryBuilder->expr()->eq('pref_email.configkey', $queryBuilder->expr()->literal('email')),
758
-				))
759
-				->andWhere($queryBuilder->expr()->orX(
760
-					$queryBuilder->expr()->in('pref_login.userid', $queryBuilder->createNamedParameter($matchedUids, IQueryBuilder::PARAM_STR_ARRAY)),
761
-				));
762
-		}
763
-
764
-		/** @var list<string> */
765
-		$list = $queryBuilder->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
766
-
767
-		return $list;
768
-	}
769
-
770
-	private function verifyUid(string $uid, bool $checkDataDirectory = false): bool {
771
-		$appdata = 'appdata_' . $this->config->getSystemValueString('instanceid');
772
-
773
-		if (\in_array($uid, [
774
-			'.htaccess',
775
-			'files_external',
776
-			'__groupfolders',
777
-			'.ncdata',
778
-			'owncloud.log',
779
-			'nextcloud.log',
780
-			'updater.log',
781
-			'audit.log',
782
-			$appdata], true)) {
783
-			return false;
784
-		}
785
-
786
-		if (!$checkDataDirectory) {
787
-			return true;
788
-		}
789
-
790
-		$dataDirectory = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
791
-
792
-		return !file_exists(rtrim($dataDirectory, '/') . '/' . $uid);
793
-	}
794
-
795
-	public function getDisplayNameCache(): DisplayNameCache {
796
-		return $this->displayNameCache;
797
-	}
798
-
799
-	public function getSeenUsers(int $offset = 0, ?int $limit = null): \Iterator {
800
-		$maxBatchSize = 1000;
801
-
802
-		do {
803
-			if ($limit !== null) {
804
-				$batchSize = min($limit, $maxBatchSize);
805
-				$limit -= $batchSize;
806
-			} else {
807
-				$batchSize = $maxBatchSize;
808
-			}
809
-
810
-			$userIds = $this->getSeenUserIds($batchSize, $offset);
811
-			$offset += $batchSize;
812
-
813
-			foreach ($userIds as $userId) {
814
-				foreach ($this->backends as $backend) {
815
-					if ($backend->userExists($userId)) {
816
-						$user = new LazyUser($userId, $this, null, $backend);
817
-						yield $user;
818
-						break;
819
-					}
820
-				}
821
-			}
822
-		} while (count($userIds) === $batchSize && $limit !== 0);
823
-	}
56
+    /**
57
+     * @var UserInterface[] $backends
58
+     */
59
+    private array $backends = [];
60
+
61
+    /**
62
+     * @var array<string,\OC\User\User> $cachedUsers
63
+     */
64
+    private array $cachedUsers = [];
65
+
66
+    private ICache $cache;
67
+
68
+    private DisplayNameCache $displayNameCache;
69
+
70
+    public function __construct(
71
+        private IConfig $config,
72
+        ICacheFactory $cacheFactory,
73
+        private IEventDispatcher $eventDispatcher,
74
+        private LoggerInterface $logger,
75
+    ) {
76
+        $this->cache = new WithLocalCache($cacheFactory->createDistributed('user_backend_map'));
77
+        $this->listen('\OC\User', 'postDelete', function (IUser $user): void {
78
+            unset($this->cachedUsers[$user->getUID()]);
79
+        });
80
+        $this->displayNameCache = new DisplayNameCache($cacheFactory, $this);
81
+    }
82
+
83
+    /**
84
+     * Get the active backends
85
+     * @return UserInterface[]
86
+     */
87
+    public function getBackends(): array {
88
+        return $this->backends;
89
+    }
90
+
91
+    public function registerBackend(UserInterface $backend): void {
92
+        $this->backends[] = $backend;
93
+    }
94
+
95
+    public function removeBackend(UserInterface $backend): void {
96
+        $this->cachedUsers = [];
97
+        if (($i = array_search($backend, $this->backends)) !== false) {
98
+            unset($this->backends[$i]);
99
+        }
100
+    }
101
+
102
+    public function clearBackends(): void {
103
+        $this->cachedUsers = [];
104
+        $this->backends = [];
105
+    }
106
+
107
+    /**
108
+     * get a user by user id
109
+     *
110
+     * @param string $uid
111
+     * @return \OC\User\User|null Either the user or null if the specified user does not exist
112
+     */
113
+    public function get($uid) {
114
+        if (is_null($uid) || $uid === '' || $uid === false) {
115
+            return null;
116
+        }
117
+        if (isset($this->cachedUsers[$uid])) { //check the cache first to prevent having to loop over the backends
118
+            return $this->cachedUsers[$uid];
119
+        }
120
+
121
+        if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
122
+            return null;
123
+        }
124
+
125
+        $cachedBackend = $this->cache->get(sha1($uid));
126
+        if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
127
+            // Cache has the info of the user backend already, so ask that one directly
128
+            $backend = $this->backends[$cachedBackend];
129
+            if ($backend->userExists($uid)) {
130
+                return $this->getUserObject($uid, $backend);
131
+            }
132
+        }
133
+
134
+        foreach ($this->backends as $i => $backend) {
135
+            if ($i === $cachedBackend) {
136
+                // Tried that one already
137
+                continue;
138
+            }
139
+
140
+            if ($backend->userExists($uid)) {
141
+                // Hash $uid to ensure that only valid characters are used for the cache key
142
+                $this->cache->set(sha1($uid), $i, 300);
143
+                return $this->getUserObject($uid, $backend);
144
+            }
145
+        }
146
+        return null;
147
+    }
148
+
149
+    public function getDisplayName(string $uid): ?string {
150
+        return $this->displayNameCache->getDisplayName($uid);
151
+    }
152
+
153
+    /**
154
+     * get or construct the user object
155
+     *
156
+     * @param string $uid
157
+     * @param \OCP\UserInterface $backend
158
+     * @param bool $cacheUser If false the newly created user object will not be cached
159
+     * @return \OC\User\User
160
+     */
161
+    public function getUserObject($uid, $backend, $cacheUser = true) {
162
+        if ($backend instanceof IGetRealUIDBackend) {
163
+            $uid = $backend->getRealUID($uid);
164
+        }
165
+
166
+        if (isset($this->cachedUsers[$uid])) {
167
+            return $this->cachedUsers[$uid];
168
+        }
169
+
170
+        $user = new User($uid, $backend, $this->eventDispatcher, $this, $this->config);
171
+        if ($cacheUser) {
172
+            $this->cachedUsers[$uid] = $user;
173
+        }
174
+        return $user;
175
+    }
176
+
177
+    /**
178
+     * check if a user exists
179
+     *
180
+     * @param string $uid
181
+     * @return bool
182
+     */
183
+    public function userExists($uid) {
184
+        if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
185
+            return false;
186
+        }
187
+
188
+        $user = $this->get($uid);
189
+        return ($user !== null);
190
+    }
191
+
192
+    /**
193
+     * Check if the password is valid for the user
194
+     *
195
+     * @param string $loginName
196
+     * @param string $password
197
+     * @return IUser|false the User object on success, false otherwise
198
+     */
199
+    public function checkPassword($loginName, $password) {
200
+        $result = $this->checkPasswordNoLogging($loginName, $password);
201
+
202
+        if ($result === false) {
203
+            $this->logger->warning('Login failed: \'' . $loginName . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
204
+        }
205
+
206
+        return $result;
207
+    }
208
+
209
+    /**
210
+     * Check if the password is valid for the user
211
+     *
212
+     * @internal
213
+     * @param string $loginName
214
+     * @param string $password
215
+     * @return IUser|false the User object on success, false otherwise
216
+     */
217
+    public function checkPasswordNoLogging($loginName, $password) {
218
+        $loginName = str_replace("\0", '', $loginName);
219
+        $password = str_replace("\0", '', $password);
220
+
221
+        $cachedBackend = $this->cache->get($loginName);
222
+        if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
223
+            $backends = [$this->backends[$cachedBackend]];
224
+        } else {
225
+            $backends = $this->backends;
226
+        }
227
+        foreach ($backends as $backend) {
228
+            if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
229
+                /** @var ICheckPasswordBackend $backend */
230
+                $uid = $backend->checkPassword($loginName, $password);
231
+                if ($uid !== false) {
232
+                    return $this->getUserObject($uid, $backend);
233
+                }
234
+            }
235
+        }
236
+
237
+        // since http basic auth doesn't provide a standard way of handling non ascii password we allow password to be urlencoded
238
+        // we only do this decoding after using the plain password fails to maintain compatibility with any password that happens
239
+        // to contain urlencoded patterns by "accident".
240
+        $password = urldecode($password);
241
+
242
+        foreach ($backends as $backend) {
243
+            if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
244
+                /** @var ICheckPasswordBackend|UserInterface $backend */
245
+                $uid = $backend->checkPassword($loginName, $password);
246
+                if ($uid !== false) {
247
+                    return $this->getUserObject($uid, $backend);
248
+                }
249
+            }
250
+        }
251
+
252
+        return false;
253
+    }
254
+
255
+    public function search($pattern, $limit = null, $offset = null) {
256
+        $users = [];
257
+        foreach ($this->backends as $backend) {
258
+            $backendUsers = $backend->getUsers($pattern, $limit, $offset);
259
+            if (is_array($backendUsers)) {
260
+                foreach ($backendUsers as $uid) {
261
+                    $users[$uid] = new LazyUser($uid, $this, null, $backend);
262
+                }
263
+            }
264
+        }
265
+
266
+        uasort($users, function (IUser $a, IUser $b) {
267
+            return strcasecmp($a->getUID(), $b->getUID());
268
+        });
269
+        return $users;
270
+    }
271
+
272
+    public function searchDisplayName($pattern, $limit = null, $offset = null) {
273
+        $users = [];
274
+        foreach ($this->backends as $backend) {
275
+            $backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
276
+            if (is_array($backendUsers)) {
277
+                foreach ($backendUsers as $uid => $displayName) {
278
+                    $users[] = new LazyUser($uid, $this, $displayName, $backend);
279
+                }
280
+            }
281
+        }
282
+
283
+        usort($users, function (IUser $a, IUser $b) {
284
+            return strcasecmp($a->getDisplayName(), $b->getDisplayName());
285
+        });
286
+        return $users;
287
+    }
288
+
289
+    /**
290
+     * @return IUser[]
291
+     */
292
+    public function getDisabledUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
293
+        $users = $this->config->getUsersForUserValue('core', 'enabled', 'false');
294
+        $users = array_combine(
295
+            $users,
296
+            array_map(
297
+                fn (string $uid): IUser => new LazyUser($uid, $this),
298
+                $users
299
+            )
300
+        );
301
+        if ($search !== '') {
302
+            $users = array_filter(
303
+                $users,
304
+                function (IUser $user) use ($search): bool {
305
+                    try {
306
+                        return mb_stripos($user->getUID(), $search) !== false
307
+                        || mb_stripos($user->getDisplayName(), $search) !== false
308
+                        || mb_stripos($user->getEMailAddress() ?? '', $search) !== false;
309
+                    } catch (NoUserException $ex) {
310
+                        $this->logger->error('Error while filtering disabled users', ['exception' => $ex, 'userUID' => $user->getUID()]);
311
+                        return false;
312
+                    }
313
+                });
314
+        }
315
+
316
+        $tempLimit = ($limit === null ? null : $limit + $offset);
317
+        foreach ($this->backends as $backend) {
318
+            if (($tempLimit !== null) && (count($users) >= $tempLimit)) {
319
+                break;
320
+            }
321
+            if ($backend instanceof IProvideEnabledStateBackend) {
322
+                $backendUsers = $backend->getDisabledUserList(($tempLimit === null ? null : $tempLimit - count($users)), 0, $search);
323
+                foreach ($backendUsers as $uid) {
324
+                    $users[$uid] = new LazyUser($uid, $this, null, $backend);
325
+                }
326
+            }
327
+        }
328
+
329
+        return array_slice($users, $offset, $limit);
330
+    }
331
+
332
+    /**
333
+     * Search known users (from phonebook sync) by displayName
334
+     *
335
+     * @param string $searcher
336
+     * @param string $pattern
337
+     * @param int|null $limit
338
+     * @param int|null $offset
339
+     * @return IUser[]
340
+     */
341
+    public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
342
+        $users = [];
343
+        foreach ($this->backends as $backend) {
344
+            if ($backend instanceof ISearchKnownUsersBackend) {
345
+                $backendUsers = $backend->searchKnownUsersByDisplayName($searcher, $pattern, $limit, $offset);
346
+            } else {
347
+                // Better than nothing, but filtering after pagination can remove lots of results.
348
+                $backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
349
+            }
350
+            if (is_array($backendUsers)) {
351
+                foreach ($backendUsers as $uid => $displayName) {
352
+                    $users[] = $this->getUserObject($uid, $backend);
353
+                }
354
+            }
355
+        }
356
+
357
+        usort($users, function ($a, $b) {
358
+            /**
359
+             * @var IUser $a
360
+             * @var IUser $b
361
+             */
362
+            return strcasecmp($a->getDisplayName(), $b->getDisplayName());
363
+        });
364
+        return $users;
365
+    }
366
+
367
+    /**
368
+     * @param string $uid
369
+     * @param string $password
370
+     * @return false|IUser the created user or false
371
+     * @throws \InvalidArgumentException
372
+     * @throws HintException
373
+     */
374
+    public function createUser($uid, $password) {
375
+        // DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
376
+        /** @var IAssertion $assertion */
377
+        $assertion = \OC::$server->get(IAssertion::class);
378
+        $assertion->createUserIsLegit();
379
+
380
+        $localBackends = [];
381
+        foreach ($this->backends as $backend) {
382
+            if ($backend instanceof Database) {
383
+                // First check if there is another user backend
384
+                $localBackends[] = $backend;
385
+                continue;
386
+            }
387
+
388
+            if ($backend->implementsActions(Backend::CREATE_USER)) {
389
+                return $this->createUserFromBackend($uid, $password, $backend);
390
+            }
391
+        }
392
+
393
+        foreach ($localBackends as $backend) {
394
+            if ($backend->implementsActions(Backend::CREATE_USER)) {
395
+                return $this->createUserFromBackend($uid, $password, $backend);
396
+            }
397
+        }
398
+
399
+        return false;
400
+    }
401
+
402
+    /**
403
+     * @param string $uid
404
+     * @param string $password
405
+     * @param UserInterface $backend
406
+     * @return IUser|false
407
+     * @throws \InvalidArgumentException
408
+     */
409
+    public function createUserFromBackend($uid, $password, UserInterface $backend) {
410
+        $l = \OCP\Util::getL10N('lib');
411
+
412
+        $this->validateUserId($uid, true);
413
+
414
+        // No empty password
415
+        if (trim($password) === '') {
416
+            throw new \InvalidArgumentException($l->t('A valid password must be provided'));
417
+        }
418
+
419
+        // Check if user already exists
420
+        if ($this->userExists($uid)) {
421
+            throw new \InvalidArgumentException($l->t('The Login is already being used'));
422
+        }
423
+
424
+        /** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
425
+        $this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
426
+        $this->eventDispatcher->dispatchTyped(new BeforeUserCreatedEvent($uid, $password));
427
+        $state = $backend->createUser($uid, $password);
428
+        if ($state === false) {
429
+            throw new \InvalidArgumentException($l->t('Could not create account'));
430
+        }
431
+        $user = $this->getUserObject($uid, $backend);
432
+        if ($user instanceof IUser) {
433
+            /** @deprecated 21.0.0 use UserCreatedEvent event with the IEventDispatcher instead */
434
+            $this->emit('\OC\User', 'postCreateUser', [$user, $password]);
435
+            $this->eventDispatcher->dispatchTyped(new UserCreatedEvent($user, $password));
436
+            return $user;
437
+        }
438
+        return false;
439
+    }
440
+
441
+    /**
442
+     * returns how many users per backend exist (if supported by backend)
443
+     *
444
+     * @param boolean $hasLoggedIn when true only users that have a lastLogin
445
+     *                             entry in the preferences table will be affected
446
+     * @return array<string, int> an array of backend class as key and count number as value
447
+     */
448
+    public function countUsers() {
449
+        $userCountStatistics = [];
450
+        foreach ($this->backends as $backend) {
451
+            if ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
452
+                /** @var ICountUsersBackend|IUserBackend $backend */
453
+                $backendUsers = $backend->countUsers();
454
+                if ($backendUsers !== false) {
455
+                    if ($backend instanceof IUserBackend) {
456
+                        $name = $backend->getBackendName();
457
+                    } else {
458
+                        $name = get_class($backend);
459
+                    }
460
+                    if (isset($userCountStatistics[$name])) {
461
+                        $userCountStatistics[$name] += $backendUsers;
462
+                    } else {
463
+                        $userCountStatistics[$name] = $backendUsers;
464
+                    }
465
+                }
466
+            }
467
+        }
468
+        return $userCountStatistics;
469
+    }
470
+
471
+    public function countUsersTotal(int $limit = 0, bool $onlyMappedUsers = false): int|false {
472
+        $userCount = false;
473
+
474
+        foreach ($this->backends as $backend) {
475
+            if ($onlyMappedUsers && $backend instanceof ICountMappedUsersBackend) {
476
+                $backendUsers = $backend->countMappedUsers();
477
+            } elseif ($backend instanceof ILimitAwareCountUsersBackend) {
478
+                $backendUsers = $backend->countUsers($limit);
479
+            } elseif ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
480
+                /** @var ICountUsersBackend $backend */
481
+                $backendUsers = $backend->countUsers();
482
+            } else {
483
+                $this->logger->debug('Skip backend for user count: ' . get_class($backend));
484
+                continue;
485
+            }
486
+            if ($backendUsers !== false) {
487
+                $userCount = (int)$userCount + $backendUsers;
488
+                if ($limit > 0) {
489
+                    if ($userCount >= $limit) {
490
+                        break;
491
+                    }
492
+                    $limit -= $userCount;
493
+                }
494
+            } else {
495
+                $this->logger->warning('Can not determine user count for ' . get_class($backend));
496
+            }
497
+        }
498
+        return $userCount;
499
+    }
500
+
501
+    /**
502
+     * returns how many users per backend exist in the requested groups (if supported by backend)
503
+     *
504
+     * @param IGroup[] $groups an array of groups to search in
505
+     * @param int $limit limit to stop counting
506
+     * @return array{int,int} total number of users, and number of disabled users in the given groups, below $limit. If limit is reached, -1 is returned for number of disabled users
507
+     */
508
+    public function countUsersAndDisabledUsersOfGroups(array $groups, int $limit): array {
509
+        $users = [];
510
+        $disabled = [];
511
+        foreach ($groups as $group) {
512
+            foreach ($group->getUsers() as $user) {
513
+                $users[$user->getUID()] = 1;
514
+                if (!$user->isEnabled()) {
515
+                    $disabled[$user->getUID()] = 1;
516
+                }
517
+                if (count($users) >= $limit) {
518
+                    return [count($users),-1];
519
+                }
520
+            }
521
+        }
522
+        return [count($users),count($disabled)];
523
+    }
524
+
525
+    /**
526
+     * The callback is executed for each user on each backend.
527
+     * If the callback returns false no further users will be retrieved.
528
+     *
529
+     * @psalm-param \Closure(\OCP\IUser):?bool $callback
530
+     * @param string $search
531
+     * @param boolean $onlySeen when true only users that have a lastLogin entry
532
+     *                          in the preferences table will be affected
533
+     * @since 9.0.0
534
+     */
535
+    public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
536
+        if ($onlySeen) {
537
+            $this->callForSeenUsers($callback);
538
+        } else {
539
+            foreach ($this->getBackends() as $backend) {
540
+                $limit = 500;
541
+                $offset = 0;
542
+                do {
543
+                    $users = $backend->getUsers($search, $limit, $offset);
544
+                    foreach ($users as $uid) {
545
+                        if (!$backend->userExists($uid)) {
546
+                            continue;
547
+                        }
548
+                        $user = $this->getUserObject($uid, $backend, false);
549
+                        $return = $callback($user);
550
+                        if ($return === false) {
551
+                            break;
552
+                        }
553
+                    }
554
+                    $offset += $limit;
555
+                } while (count($users) >= $limit);
556
+            }
557
+        }
558
+    }
559
+
560
+    /**
561
+     * returns how many users are disabled
562
+     *
563
+     * @return int
564
+     * @since 12.0.0
565
+     */
566
+    public function countDisabledUsers(): int {
567
+        $queryBuilder = Server::get(IDBConnection::class)->getQueryBuilder();
568
+        $queryBuilder->select($queryBuilder->func()->count('*'))
569
+            ->from('preferences')
570
+            ->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
571
+            ->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
572
+            ->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR));
573
+
574
+
575
+        $result = $queryBuilder->executeQuery();
576
+        $count = $result->fetchOne();
577
+        $result->closeCursor();
578
+
579
+        if ($count !== false) {
580
+            $count = (int)$count;
581
+        } else {
582
+            $count = 0;
583
+        }
584
+
585
+        return $count;
586
+    }
587
+
588
+    /**
589
+     * returns how many users have logged in once
590
+     *
591
+     * @return int
592
+     * @since 11.0.0
593
+     */
594
+    public function countSeenUsers() {
595
+        $queryBuilder = Server::get(IDBConnection::class)->getQueryBuilder();
596
+        $queryBuilder->select($queryBuilder->func()->count('*'))
597
+            ->from('preferences')
598
+            ->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('login')))
599
+            ->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('lastLogin')));
600
+
601
+        $query = $queryBuilder->executeQuery();
602
+
603
+        $result = (int)$query->fetchOne();
604
+        $query->closeCursor();
605
+
606
+        return $result;
607
+    }
608
+
609
+    public function callForSeenUsers(\Closure $callback) {
610
+        $users = $this->getSeenUsers();
611
+        foreach ($users as $user) {
612
+            $return = $callback($user);
613
+            if ($return === false) {
614
+                return;
615
+            }
616
+        }
617
+    }
618
+
619
+    /**
620
+     * Getting all userIds that have a lastLogin value requires checking the
621
+     * value in php because on oracle you cannot use a clob in a where clause,
622
+     * preventing us from doing a not null or length(value) > 0 check.
623
+     *
624
+     * @param int $limit
625
+     * @param int $offset
626
+     * @return string[] with user ids
627
+     */
628
+    private function getSeenUserIds($limit = null, $offset = null) {
629
+        $queryBuilder = Server::get(IDBConnection::class)->getQueryBuilder();
630
+        $queryBuilder->select(['userid'])
631
+            ->from('preferences')
632
+            ->where($queryBuilder->expr()->eq(
633
+                'appid', $queryBuilder->createNamedParameter('login'))
634
+            )
635
+            ->andWhere($queryBuilder->expr()->eq(
636
+                'configkey', $queryBuilder->createNamedParameter('lastLogin'))
637
+            )
638
+            ->andWhere($queryBuilder->expr()->isNotNull('configvalue')
639
+            );
640
+
641
+        if ($limit !== null) {
642
+            $queryBuilder->setMaxResults($limit);
643
+        }
644
+        if ($offset !== null) {
645
+            $queryBuilder->setFirstResult($offset);
646
+        }
647
+        $query = $queryBuilder->executeQuery();
648
+        $result = [];
649
+
650
+        while ($row = $query->fetch()) {
651
+            $result[] = $row['userid'];
652
+        }
653
+
654
+        $query->closeCursor();
655
+
656
+        return $result;
657
+    }
658
+
659
+    /**
660
+     * @param string $email
661
+     * @return IUser[]
662
+     * @since 9.1.0
663
+     */
664
+    public function getByEmail($email) {
665
+        // looking for 'email' only (and not primary_mail) is intentional
666
+        $userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email);
667
+
668
+        $users = array_map(function ($uid) {
669
+            return $this->get($uid);
670
+        }, $userIds);
671
+
672
+        return array_values(array_filter($users, function ($u) {
673
+            return ($u instanceof IUser);
674
+        }));
675
+    }
676
+
677
+    /**
678
+     * @param string $uid
679
+     * @param bool $checkDataDirectory
680
+     * @throws \InvalidArgumentException Message is an already translated string with a reason why the id is not valid
681
+     * @since 26.0.0
682
+     */
683
+    public function validateUserId(string $uid, bool $checkDataDirectory = false): void {
684
+        $l = Server::get(IFactory::class)->get('lib');
685
+
686
+        // Check the ID for bad characters
687
+        // Allowed are: "a-z", "A-Z", "0-9", spaces and "_.@-'"
688
+        if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) {
689
+            throw new \InvalidArgumentException($l->t('Only the following characters are allowed in an Login:'
690
+                . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'));
691
+        }
692
+
693
+        // No empty user ID
694
+        if (trim($uid) === '') {
695
+            throw new \InvalidArgumentException($l->t('A valid Login must be provided'));
696
+        }
697
+
698
+        // No whitespace at the beginning or at the end
699
+        if (trim($uid) !== $uid) {
700
+            throw new \InvalidArgumentException($l->t('Login contains whitespace at the beginning or at the end'));
701
+        }
702
+
703
+        // User ID only consists of 1 or 2 dots (directory traversal)
704
+        if ($uid === '.' || $uid === '..') {
705
+            throw new \InvalidArgumentException($l->t('Login must not consist of dots only'));
706
+        }
707
+
708
+        // User ID is too long
709
+        if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
710
+            // TRANSLATORS User ID is too long
711
+            throw new \InvalidArgumentException($l->t('Username is too long'));
712
+        }
713
+
714
+        if (!$this->verifyUid($uid, $checkDataDirectory)) {
715
+            throw new \InvalidArgumentException($l->t('Login is invalid because files already exist for this user'));
716
+        }
717
+    }
718
+
719
+    /**
720
+     * Gets the list of user ids sorted by lastLogin, from most recent to least recent
721
+     *
722
+     * @param int|null $limit how many users to fetch (default: 25, max: 100)
723
+     * @param int $offset from which offset to fetch
724
+     * @param string $search search users based on search params
725
+     * @return list<string> list of user IDs
726
+     */
727
+    public function getLastLoggedInUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
728
+        // We can't load all users who already logged in
729
+        $limit = min(100, $limit ?: 25);
730
+
731
+        $connection = Server::get(IDBConnection::class);
732
+        $queryBuilder = $connection->getQueryBuilder();
733
+        $queryBuilder->select('pref_login.userid')
734
+            ->from('preferences', 'pref_login')
735
+            ->where($queryBuilder->expr()->eq('pref_login.appid', $queryBuilder->expr()->literal('login')))
736
+            ->andWhere($queryBuilder->expr()->eq('pref_login.configkey', $queryBuilder->expr()->literal('lastLogin')))
737
+            ->setFirstResult($offset)
738
+            ->setMaxResults($limit)
739
+        ;
740
+
741
+        // Oracle don't want to run ORDER BY on CLOB column
742
+        $loginOrder = $connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE
743
+            ? $queryBuilder->expr()->castColumn('pref_login.configvalue', IQueryBuilder::PARAM_INT)
744
+            : 'pref_login.configvalue';
745
+        $queryBuilder
746
+            ->orderBy($loginOrder, 'DESC')
747
+            ->addOrderBy($queryBuilder->func()->lower('pref_login.userid'), 'ASC');
748
+
749
+        if ($search !== '') {
750
+            $displayNameMatches = $this->searchDisplayName($search);
751
+            $matchedUids = array_map(static fn (IUser $u): string => $u->getUID(), $displayNameMatches);
752
+
753
+            $queryBuilder
754
+                ->leftJoin('pref_login', 'preferences', 'pref_email', $queryBuilder->expr()->andX(
755
+                    $queryBuilder->expr()->eq('pref_login.userid', 'pref_email.userid'),
756
+                    $queryBuilder->expr()->eq('pref_email.appid', $queryBuilder->expr()->literal('settings')),
757
+                    $queryBuilder->expr()->eq('pref_email.configkey', $queryBuilder->expr()->literal('email')),
758
+                ))
759
+                ->andWhere($queryBuilder->expr()->orX(
760
+                    $queryBuilder->expr()->in('pref_login.userid', $queryBuilder->createNamedParameter($matchedUids, IQueryBuilder::PARAM_STR_ARRAY)),
761
+                ));
762
+        }
763
+
764
+        /** @var list<string> */
765
+        $list = $queryBuilder->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
766
+
767
+        return $list;
768
+    }
769
+
770
+    private function verifyUid(string $uid, bool $checkDataDirectory = false): bool {
771
+        $appdata = 'appdata_' . $this->config->getSystemValueString('instanceid');
772
+
773
+        if (\in_array($uid, [
774
+            '.htaccess',
775
+            'files_external',
776
+            '__groupfolders',
777
+            '.ncdata',
778
+            'owncloud.log',
779
+            'nextcloud.log',
780
+            'updater.log',
781
+            'audit.log',
782
+            $appdata], true)) {
783
+            return false;
784
+        }
785
+
786
+        if (!$checkDataDirectory) {
787
+            return true;
788
+        }
789
+
790
+        $dataDirectory = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
791
+
792
+        return !file_exists(rtrim($dataDirectory, '/') . '/' . $uid);
793
+    }
794
+
795
+    public function getDisplayNameCache(): DisplayNameCache {
796
+        return $this->displayNameCache;
797
+    }
798
+
799
+    public function getSeenUsers(int $offset = 0, ?int $limit = null): \Iterator {
800
+        $maxBatchSize = 1000;
801
+
802
+        do {
803
+            if ($limit !== null) {
804
+                $batchSize = min($limit, $maxBatchSize);
805
+                $limit -= $batchSize;
806
+            } else {
807
+                $batchSize = $maxBatchSize;
808
+            }
809
+
810
+            $userIds = $this->getSeenUserIds($batchSize, $offset);
811
+            $offset += $batchSize;
812
+
813
+            foreach ($userIds as $userId) {
814
+                foreach ($this->backends as $backend) {
815
+                    if ($backend->userExists($userId)) {
816
+                        $user = new LazyUser($userId, $this, null, $backend);
817
+                        yield $user;
818
+                        break;
819
+                    }
820
+                }
821
+            }
822
+        } while (count($userIds) === $batchSize && $limit !== 0);
823
+    }
824 824
 }
Please login to merge, or discard this patch.
apps/settings/lib/SetupChecks/MysqlRowFormat.php 1 patch
Indentation   +40 added lines, -40 removed lines patch added patch discarded remove patch
@@ -17,55 +17,55 @@
 block discarded – undo
17 17
 use OCP\SetupCheck\SetupResult;
18 18
 
19 19
 class MysqlRowFormat implements ISetupCheck {
20
-	public function __construct(
21
-		private IL10N $l10n,
22
-		private IConfig $config,
23
-		private Connection $connection,
24
-		private IURLGenerator $urlGenerator,
25
-	) {
26
-	}
20
+    public function __construct(
21
+        private IL10N $l10n,
22
+        private IConfig $config,
23
+        private Connection $connection,
24
+        private IURLGenerator $urlGenerator,
25
+    ) {
26
+    }
27 27
 
28
-	public function getName(): string {
29
-		return $this->l10n->t('MySQL row format');
30
-	}
28
+    public function getName(): string {
29
+        return $this->l10n->t('MySQL row format');
30
+    }
31 31
 
32
-	public function getCategory(): string {
33
-		return 'database';
34
-	}
32
+    public function getCategory(): string {
33
+        return 'database';
34
+    }
35 35
 
36
-	public function run(): SetupResult {
37
-		if (!$this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_MYSQL
38
-			&& !$this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_MARIADB) {
39
-			return SetupResult::success($this->l10n->t('You are not using MySQL'));
40
-		}
36
+    public function run(): SetupResult {
37
+        if (!$this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_MYSQL
38
+            && !$this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_MARIADB) {
39
+            return SetupResult::success($this->l10n->t('You are not using MySQL'));
40
+        }
41 41
 
42
-		$wrongRowFormatTables = $this->getRowNotDynamicTables();
43
-		if (empty($wrongRowFormatTables)) {
44
-			return SetupResult::success($this->l10n->t('None of your tables use ROW_FORMAT=Compressed'));
45
-		}
42
+        $wrongRowFormatTables = $this->getRowNotDynamicTables();
43
+        if (empty($wrongRowFormatTables)) {
44
+            return SetupResult::success($this->l10n->t('None of your tables use ROW_FORMAT=Compressed'));
45
+        }
46 46
 
47
-		return SetupResult::warning(
48
-			$this->l10n->t(
49
-				'Incorrect row format found in your database. ROW_FORMAT=Dynamic offers the best database performances for Nextcloud. Please update row format on the following list: %s.',
50
-				[implode(', ', $wrongRowFormatTables)],
51
-			),
52
-			'https://dev.mysql.com/doc/refman/en/innodb-row-format.html',
53
-		);
54
-	}
47
+        return SetupResult::warning(
48
+            $this->l10n->t(
49
+                'Incorrect row format found in your database. ROW_FORMAT=Dynamic offers the best database performances for Nextcloud. Please update row format on the following list: %s.',
50
+                [implode(', ', $wrongRowFormatTables)],
51
+            ),
52
+            'https://dev.mysql.com/doc/refman/en/innodb-row-format.html',
53
+        );
54
+    }
55 55
 
56
-	/**
57
-	 * @return string[]
58
-	 */
59
-	private function getRowNotDynamicTables(): array {
60
-		$sql = "SELECT table_name
56
+    /**
57
+     * @return string[]
58
+     */
59
+    private function getRowNotDynamicTables(): array {
60
+        $sql = "SELECT table_name
61 61
 			FROM information_schema.tables
62 62
 			WHERE table_schema = ?
63 63
 			  AND table_name LIKE '*PREFIX*%'
64 64
 			  AND row_format != 'Dynamic';";
65 65
 
66
-		return $this->connection->executeQuery(
67
-			$sql,
68
-			[$this->config->getSystemValueString('dbname')],
69
-		)->fetchFirstColumn();
70
-	}
66
+        return $this->connection->executeQuery(
67
+            $sql,
68
+            [$this->config->getSystemValueString('dbname')],
69
+        )->fetchFirstColumn();
70
+    }
71 71
 }
Please login to merge, or discard this patch.
apps/settings/lib/SetupChecks/SupportedDatabase.php 1 patch
Indentation   +98 added lines, -98 removed lines patch added patch discarded remove patch
@@ -16,106 +16,106 @@
 block discarded – undo
16 16
 
17 17
 class SupportedDatabase implements ISetupCheck {
18 18
 
19
-	private const MIN_MARIADB = '10.6';
20
-	private const MAX_MARIADB = '11.8';
21
-	private const MIN_MYSQL = '8.0';
22
-	private const MAX_MYSQL = '8.4';
23
-	private const MIN_POSTGRES = '13';
24
-	private const MAX_POSTGRES = '17';
19
+    private const MIN_MARIADB = '10.6';
20
+    private const MAX_MARIADB = '11.8';
21
+    private const MIN_MYSQL = '8.0';
22
+    private const MAX_MYSQL = '8.4';
23
+    private const MIN_POSTGRES = '13';
24
+    private const MAX_POSTGRES = '17';
25 25
 
26
-	public function __construct(
27
-		private IL10N $l10n,
28
-		private IURLGenerator $urlGenerator,
29
-		private IDBConnection $connection,
30
-	) {
31
-	}
26
+    public function __construct(
27
+        private IL10N $l10n,
28
+        private IURLGenerator $urlGenerator,
29
+        private IDBConnection $connection,
30
+    ) {
31
+    }
32 32
 
33
-	public function getCategory(): string {
34
-		return 'database';
35
-	}
33
+    public function getCategory(): string {
34
+        return 'database';
35
+    }
36 36
 
37
-	public function getName(): string {
38
-		return $this->l10n->t('Database version');
39
-	}
37
+    public function getName(): string {
38
+        return $this->l10n->t('Database version');
39
+    }
40 40
 
41
-	public function run(): SetupResult {
42
-		$databasePlatform = $this->connection->getDatabaseProvider();
43
-		if ($databasePlatform === IDBConnection::PLATFORM_MYSQL || $databasePlatform === IDBConnection::PLATFORM_MARIADB) {
44
-			$statement = $this->connection->prepare("SHOW VARIABLES LIKE 'version';");
45
-			$result = $statement->execute();
46
-			$row = $result->fetch();
47
-			$version = $row['Value'];
48
-			$versionlc = strtolower($version);
49
-			// we only care about X.Y not X.Y.Z differences
50
-			[$major, $minor, ] = explode('.', $versionlc);
51
-			$versionConcern = $major . '.' . $minor;
52
-			if (str_contains($versionlc, 'mariadb')) {
53
-				if (version_compare($versionConcern, '10.3', '=')) {
54
-					return SetupResult::info(
55
-						$this->l10n->t(
56
-							'MariaDB version 10.3 detected, this version is end-of-life and only supported as part of Ubuntu 20.04. MariaDB >=%1$s and <=%2$s is suggested for best performance, stability and functionality with this version of Nextcloud.',
57
-							[
58
-								self::MIN_MARIADB,
59
-								self::MAX_MARIADB,
60
-							]
61
-						),
62
-					);
63
-				} elseif (version_compare($versionConcern, self::MIN_MARIADB, '<') || version_compare($versionConcern, self::MAX_MARIADB, '>')) {
64
-					return SetupResult::warning(
65
-						$this->l10n->t(
66
-							'MariaDB version "%1$s" detected. MariaDB >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.',
67
-							[
68
-								$version,
69
-								self::MIN_MARIADB,
70
-								self::MAX_MARIADB,
71
-							],
72
-						),
73
-					);
74
-				}
75
-			} else {
76
-				if (version_compare($versionConcern, self::MIN_MYSQL, '<') || version_compare($versionConcern, self::MAX_MYSQL, '>')) {
77
-					return SetupResult::warning(
78
-						$this->l10n->t(
79
-							'MySQL version "%1$s" detected. MySQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.',
80
-							[
81
-								$version,
82
-								self::MIN_MYSQL,
83
-								self::MAX_MYSQL,
84
-							],
85
-						),
86
-					);
87
-				}
88
-			}
89
-		} elseif ($databasePlatform === IDBConnection::PLATFORM_POSTGRES) {
90
-			$statement = $this->connection->prepare('SHOW server_version;');
91
-			$result = $statement->execute();
92
-			$row = $result->fetch();
93
-			$version = $row['server_version'];
94
-			$versionlc = strtolower($version);
95
-			// we only care about X not X.Y or X.Y.Z differences
96
-			[$major, ] = explode('.', $versionlc);
97
-			$versionConcern = $major;
98
-			if (version_compare($versionConcern, self::MIN_POSTGRES, '<') || version_compare($versionConcern, self::MAX_POSTGRES, '>')) {
99
-				return SetupResult::warning(
100
-					$this->l10n->t(
101
-						'PostgreSQL version "%1$s" detected. PostgreSQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.',
102
-						[
103
-							$version,
104
-							self::MIN_POSTGRES,
105
-							self::MAX_POSTGRES,
106
-						])
107
-				);
108
-			}
109
-		} elseif ($databasePlatform === IDBConnection::PLATFORM_ORACLE) {
110
-			$version = 'Oracle';
111
-		} elseif ($databasePlatform === IDBConnection::PLATFORM_SQLITE) {
112
-			return SetupResult::warning(
113
-				$this->l10n->t('SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend. This is particularly recommended when using the desktop client for file synchronisation. To migrate to another database use the command line tool: "occ db:convert-type".'),
114
-				$this->urlGenerator->linkToDocs('admin-db-conversion')
115
-			);
116
-		} else {
117
-			return SetupResult::error($this->l10n->t('Unknown database platform'));
118
-		}
119
-		return SetupResult::success($version);
120
-	}
41
+    public function run(): SetupResult {
42
+        $databasePlatform = $this->connection->getDatabaseProvider();
43
+        if ($databasePlatform === IDBConnection::PLATFORM_MYSQL || $databasePlatform === IDBConnection::PLATFORM_MARIADB) {
44
+            $statement = $this->connection->prepare("SHOW VARIABLES LIKE 'version';");
45
+            $result = $statement->execute();
46
+            $row = $result->fetch();
47
+            $version = $row['Value'];
48
+            $versionlc = strtolower($version);
49
+            // we only care about X.Y not X.Y.Z differences
50
+            [$major, $minor, ] = explode('.', $versionlc);
51
+            $versionConcern = $major . '.' . $minor;
52
+            if (str_contains($versionlc, 'mariadb')) {
53
+                if (version_compare($versionConcern, '10.3', '=')) {
54
+                    return SetupResult::info(
55
+                        $this->l10n->t(
56
+                            'MariaDB version 10.3 detected, this version is end-of-life and only supported as part of Ubuntu 20.04. MariaDB >=%1$s and <=%2$s is suggested for best performance, stability and functionality with this version of Nextcloud.',
57
+                            [
58
+                                self::MIN_MARIADB,
59
+                                self::MAX_MARIADB,
60
+                            ]
61
+                        ),
62
+                    );
63
+                } elseif (version_compare($versionConcern, self::MIN_MARIADB, '<') || version_compare($versionConcern, self::MAX_MARIADB, '>')) {
64
+                    return SetupResult::warning(
65
+                        $this->l10n->t(
66
+                            'MariaDB version "%1$s" detected. MariaDB >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.',
67
+                            [
68
+                                $version,
69
+                                self::MIN_MARIADB,
70
+                                self::MAX_MARIADB,
71
+                            ],
72
+                        ),
73
+                    );
74
+                }
75
+            } else {
76
+                if (version_compare($versionConcern, self::MIN_MYSQL, '<') || version_compare($versionConcern, self::MAX_MYSQL, '>')) {
77
+                    return SetupResult::warning(
78
+                        $this->l10n->t(
79
+                            'MySQL version "%1$s" detected. MySQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.',
80
+                            [
81
+                                $version,
82
+                                self::MIN_MYSQL,
83
+                                self::MAX_MYSQL,
84
+                            ],
85
+                        ),
86
+                    );
87
+                }
88
+            }
89
+        } elseif ($databasePlatform === IDBConnection::PLATFORM_POSTGRES) {
90
+            $statement = $this->connection->prepare('SHOW server_version;');
91
+            $result = $statement->execute();
92
+            $row = $result->fetch();
93
+            $version = $row['server_version'];
94
+            $versionlc = strtolower($version);
95
+            // we only care about X not X.Y or X.Y.Z differences
96
+            [$major, ] = explode('.', $versionlc);
97
+            $versionConcern = $major;
98
+            if (version_compare($versionConcern, self::MIN_POSTGRES, '<') || version_compare($versionConcern, self::MAX_POSTGRES, '>')) {
99
+                return SetupResult::warning(
100
+                    $this->l10n->t(
101
+                        'PostgreSQL version "%1$s" detected. PostgreSQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.',
102
+                        [
103
+                            $version,
104
+                            self::MIN_POSTGRES,
105
+                            self::MAX_POSTGRES,
106
+                        ])
107
+                );
108
+            }
109
+        } elseif ($databasePlatform === IDBConnection::PLATFORM_ORACLE) {
110
+            $version = 'Oracle';
111
+        } elseif ($databasePlatform === IDBConnection::PLATFORM_SQLITE) {
112
+            return SetupResult::warning(
113
+                $this->l10n->t('SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend. This is particularly recommended when using the desktop client for file synchronisation. To migrate to another database use the command line tool: "occ db:convert-type".'),
114
+                $this->urlGenerator->linkToDocs('admin-db-conversion')
115
+            );
116
+        } else {
117
+            return SetupResult::error($this->l10n->t('Unknown database platform'));
118
+        }
119
+        return SetupResult::success($version);
120
+    }
121 121
 }
Please login to merge, or discard this patch.
apps/oauth2/tests/Db/ClientMapperTest.php 1 patch
Indentation   +51 added lines, -51 removed lines patch added patch discarded remove patch
@@ -17,66 +17,66 @@
 block discarded – undo
17 17
  * @group DB
18 18
  */
19 19
 class ClientMapperTest extends TestCase {
20
-	/** @var ClientMapper */
21
-	private $clientMapper;
20
+    /** @var ClientMapper */
21
+    private $clientMapper;
22 22
 
23
-	protected function setUp(): void {
24
-		parent::setUp();
25
-		$this->clientMapper = new ClientMapper(Server::get(IDBConnection::class));
26
-	}
23
+    protected function setUp(): void {
24
+        parent::setUp();
25
+        $this->clientMapper = new ClientMapper(Server::get(IDBConnection::class));
26
+    }
27 27
 
28
-	protected function tearDown(): void {
29
-		$query = Server::get(IDBConnection::class)->getQueryBuilder();
30
-		$query->delete('oauth2_clients')->executeStatement();
28
+    protected function tearDown(): void {
29
+        $query = Server::get(IDBConnection::class)->getQueryBuilder();
30
+        $query->delete('oauth2_clients')->executeStatement();
31 31
 
32
-		parent::tearDown();
33
-	}
32
+        parent::tearDown();
33
+    }
34 34
 
35
-	public function testGetByIdentifier(): void {
36
-		$client = new Client();
37
-		$client->setClientIdentifier('MyAwesomeClientIdentifier');
38
-		$client->setName('Client Name');
39
-		$client->setRedirectUri('https://example.com/');
40
-		$client->setSecret('TotallyNotSecret');
41
-		$this->clientMapper->insert($client);
42
-		$client->resetUpdatedFields();
43
-		$this->assertEquals($client, $this->clientMapper->getByIdentifier('MyAwesomeClientIdentifier'));
44
-	}
35
+    public function testGetByIdentifier(): void {
36
+        $client = new Client();
37
+        $client->setClientIdentifier('MyAwesomeClientIdentifier');
38
+        $client->setName('Client Name');
39
+        $client->setRedirectUri('https://example.com/');
40
+        $client->setSecret('TotallyNotSecret');
41
+        $this->clientMapper->insert($client);
42
+        $client->resetUpdatedFields();
43
+        $this->assertEquals($client, $this->clientMapper->getByIdentifier('MyAwesomeClientIdentifier'));
44
+    }
45 45
 
46
-	public function testGetByIdentifierNotExisting(): void {
47
-		$this->expectException(ClientNotFoundException::class);
46
+    public function testGetByIdentifierNotExisting(): void {
47
+        $this->expectException(ClientNotFoundException::class);
48 48
 
49
-		$this->clientMapper->getByIdentifier('MyTotallyNotExistingClient');
50
-	}
49
+        $this->clientMapper->getByIdentifier('MyTotallyNotExistingClient');
50
+    }
51 51
 
52
-	public function testGetByUid(): void {
53
-		$client = new Client();
54
-		$client->setClientIdentifier('MyNewClient');
55
-		$client->setName('Client Name');
56
-		$client->setRedirectUri('https://example.com/');
57
-		$client->setSecret('TotallyNotSecret');
58
-		$this->clientMapper->insert($client);
59
-		$client->resetUpdatedFields();
60
-		$this->assertEquals($client, $this->clientMapper->getByUid($client->getId()));
61
-	}
52
+    public function testGetByUid(): void {
53
+        $client = new Client();
54
+        $client->setClientIdentifier('MyNewClient');
55
+        $client->setName('Client Name');
56
+        $client->setRedirectUri('https://example.com/');
57
+        $client->setSecret('TotallyNotSecret');
58
+        $this->clientMapper->insert($client);
59
+        $client->resetUpdatedFields();
60
+        $this->assertEquals($client, $this->clientMapper->getByUid($client->getId()));
61
+    }
62 62
 
63
-	public function testGetByUidNotExisting(): void {
64
-		$this->expectException(ClientNotFoundException::class);
63
+    public function testGetByUidNotExisting(): void {
64
+        $this->expectException(ClientNotFoundException::class);
65 65
 
66
-		$this->clientMapper->getByUid(1234);
67
-	}
66
+        $this->clientMapper->getByUid(1234);
67
+    }
68 68
 
69
-	public function testGetClients(): void {
70
-		$this->assertSame('array', gettype($this->clientMapper->getClients()));
71
-	}
69
+    public function testGetClients(): void {
70
+        $this->assertSame('array', gettype($this->clientMapper->getClients()));
71
+    }
72 72
 
73
-	public function testInsertLongEncryptedSecret(): void {
74
-		$client = new Client();
75
-		$client->setClientIdentifier('MyNewClient');
76
-		$client->setName('Client Name');
77
-		$client->setRedirectUri('https://example.com/');
78
-		$client->setSecret('b81dc8e2dc178817bf28ca7b37265aa96559ca02e6dcdeb74b42221d096ed5ef63681e836ae0ba1077b5fb5e6c2fa7748c78463f66fe0110c8dcb8dd7eb0305b16d0cd993e2ae275879994a2abf88c68|e466d9befa6b0102341458e45ecd551a|013af9e277374483123437f180a3b0371a411ad4f34c451547909769181a7d7cc191f0f5c2de78376d124dd7751b8c9660aabdd913f5e071fc6b819ba2e3d919|3');
79
-		$this->clientMapper->insert($client);
80
-		$this->assertTrue(true);
81
-	}
73
+    public function testInsertLongEncryptedSecret(): void {
74
+        $client = new Client();
75
+        $client->setClientIdentifier('MyNewClient');
76
+        $client->setName('Client Name');
77
+        $client->setRedirectUri('https://example.com/');
78
+        $client->setSecret('b81dc8e2dc178817bf28ca7b37265aa96559ca02e6dcdeb74b42221d096ed5ef63681e836ae0ba1077b5fb5e6c2fa7748c78463f66fe0110c8dcb8dd7eb0305b16d0cd993e2ae275879994a2abf88c68|e466d9befa6b0102341458e45ecd551a|013af9e277374483123437f180a3b0371a411ad4f34c451547909769181a7d7cc191f0f5c2de78376d124dd7751b8c9660aabdd913f5e071fc6b819ba2e3d919|3');
79
+        $this->clientMapper->insert($client);
80
+        $this->assertTrue(true);
81
+    }
82 82
 }
Please login to merge, or discard this patch.
apps/workflowengine/tests/ManagerTest.php 1 patch
Indentation   +733 added lines, -733 removed lines patch added patch discarded remove patch
@@ -43,737 +43,737 @@
 block discarded – undo
43 43
  * @group DB
44 44
  */
45 45
 class ManagerTest extends TestCase {
46
-	protected Manager $manager;
47
-	protected IDBConnection $db;
48
-	protected LoggerInterface&MockObject $logger;
49
-	protected ContainerInterface&MockObject $container;
50
-	protected IUserSession&MockObject $session;
51
-	protected IL10N&MockObject $l;
52
-	protected IEventDispatcher&MockObject $dispatcher;
53
-	protected IAppConfig&MockObject $config;
54
-	protected ICacheFactory&MockObject $cacheFactory;
55
-
56
-	protected function setUp(): void {
57
-		parent::setUp();
58
-
59
-		$this->db = Server::get(IDBConnection::class);
60
-		$this->container = $this->createMock(ContainerInterface::class);
61
-		$this->l = $this->createMock(IL10N::class);
62
-		$this->l->method('t')
63
-			->willReturnCallback(function ($text, $parameters = []) {
64
-				return vsprintf($text, $parameters);
65
-			});
66
-
67
-		$this->logger = $this->createMock(LoggerInterface::class);
68
-		$this->session = $this->createMock(IUserSession::class);
69
-		$this->dispatcher = $this->createMock(IEventDispatcher::class);
70
-		$this->config = $this->createMock(IAppConfig::class);
71
-		$this->cacheFactory = $this->createMock(ICacheFactory::class);
72
-
73
-		$this->manager = new Manager(
74
-			$this->db,
75
-			$this->container,
76
-			$this->l,
77
-			$this->logger,
78
-			$this->session,
79
-			$this->dispatcher,
80
-			$this->config,
81
-			$this->cacheFactory
82
-		);
83
-		$this->clearTables();
84
-	}
85
-
86
-	protected function tearDown(): void {
87
-		$this->clearTables();
88
-		parent::tearDown();
89
-	}
90
-
91
-	protected function buildScope(?string $scopeId = null): MockObject&ScopeContext {
92
-		$scopeContext = $this->createMock(ScopeContext::class);
93
-		$scopeContext->expects($this->any())
94
-			->method('getScope')
95
-			->willReturn($scopeId ? IManager::SCOPE_USER : IManager::SCOPE_ADMIN);
96
-		$scopeContext->expects($this->any())
97
-			->method('getScopeId')
98
-			->willReturn($scopeId ?? '');
99
-		$scopeContext->expects($this->any())
100
-			->method('getHash')
101
-			->willReturn(md5($scopeId ?? ''));
102
-
103
-		return $scopeContext;
104
-	}
105
-
106
-	public function clearTables() {
107
-		$query = $this->db->getQueryBuilder();
108
-		foreach (['flow_checks', 'flow_operations', 'flow_operations_scope'] as $table) {
109
-			$query->delete($table)
110
-				->executeStatement();
111
-		}
112
-	}
113
-
114
-	public function testChecks(): void {
115
-		$check1 = $this->invokePrivate($this->manager, 'addCheck', ['Test', 'equal', 1]);
116
-		$check2 = $this->invokePrivate($this->manager, 'addCheck', ['Test', '!equal', 2]);
117
-
118
-		$data = $this->manager->getChecks([$check1]);
119
-		$this->assertArrayHasKey($check1, $data);
120
-		$this->assertArrayNotHasKey($check2, $data);
121
-
122
-		$data = $this->manager->getChecks([$check1, $check2]);
123
-		$this->assertArrayHasKey($check1, $data);
124
-		$this->assertArrayHasKey($check2, $data);
125
-
126
-		$data = $this->manager->getChecks([$check2, $check1]);
127
-		$this->assertArrayHasKey($check1, $data);
128
-		$this->assertArrayHasKey($check2, $data);
129
-
130
-		$data = $this->manager->getChecks([$check2]);
131
-		$this->assertArrayNotHasKey($check1, $data);
132
-		$this->assertArrayHasKey($check2, $data);
133
-	}
134
-
135
-	public function testScope(): void {
136
-		$adminScope = $this->buildScope();
137
-		$userScope = $this->buildScope('jackie');
138
-		$entity = File::class;
139
-
140
-		$opId1 = $this->invokePrivate(
141
-			$this->manager,
142
-			'insertOperation',
143
-			['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo', $entity, []]
144
-		);
145
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
146
-
147
-		$opId2 = $this->invokePrivate(
148
-			$this->manager,
149
-			'insertOperation',
150
-			['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar', $entity, []]
151
-		);
152
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
153
-		$opId3 = $this->invokePrivate(
154
-			$this->manager,
155
-			'insertOperation',
156
-			['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar', $entity, []]
157
-		);
158
-		$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
159
-
160
-		$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId1, $adminScope]));
161
-		$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId2, $adminScope]));
162
-		$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId3, $adminScope]));
163
-
164
-		$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId1, $userScope]));
165
-		$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId2, $userScope]));
166
-		$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId3, $userScope]));
167
-	}
168
-
169
-	public function testGetAllOperations(): void {
170
-		$adminScope = $this->buildScope();
171
-		$userScope = $this->buildScope('jackie');
172
-		$entity = File::class;
173
-
174
-		$adminOperation = $this->createMock(IOperation::class);
175
-		$adminOperation->expects($this->any())
176
-			->method('isAvailableForScope')
177
-			->willReturnMap([
178
-				[IManager::SCOPE_ADMIN, true],
179
-				[IManager::SCOPE_USER, false],
180
-			]);
181
-		$userOperation = $this->createMock(IOperation::class);
182
-		$userOperation->expects($this->any())
183
-			->method('isAvailableForScope')
184
-			->willReturnMap([
185
-				[IManager::SCOPE_ADMIN, false],
186
-				[IManager::SCOPE_USER, true],
187
-			]);
188
-
189
-		$this->container->expects($this->any())
190
-			->method('get')
191
-			->willReturnCallback(function ($className) use ($adminOperation, $userOperation) {
192
-				switch ($className) {
193
-					case 'OCA\WFE\TestAdminOp':
194
-						return $adminOperation;
195
-					case 'OCA\WFE\TestUserOp':
196
-						return $userOperation;
197
-				}
198
-			});
199
-
200
-		$opId1 = $this->invokePrivate(
201
-			$this->manager,
202
-			'insertOperation',
203
-			['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
204
-		);
205
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
206
-
207
-		$opId2 = $this->invokePrivate(
208
-			$this->manager,
209
-			'insertOperation',
210
-			['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
211
-		);
212
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
213
-		$opId3 = $this->invokePrivate(
214
-			$this->manager,
215
-			'insertOperation',
216
-			['OCA\WFE\TestUserOp', 'Test03', [11, 44], 'foobar', $entity, []]
217
-		);
218
-		$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
219
-
220
-		$opId4 = $this->invokePrivate(
221
-			$this->manager,
222
-			'insertOperation',
223
-			['OCA\WFE\TestAdminOp', 'Test04', [41, 10, 4], 'NoBar', $entity, []]
224
-		);
225
-		$this->invokePrivate($this->manager, 'addScope', [$opId4, $userScope]);
226
-
227
-		$adminOps = $this->manager->getAllOperations($adminScope);
228
-		$userOps = $this->manager->getAllOperations($userScope);
229
-
230
-		$this->assertSame(1, count($adminOps));
231
-		$this->assertTrue(array_key_exists('OCA\WFE\TestAdminOp', $adminOps));
232
-		$this->assertFalse(array_key_exists('OCA\WFE\TestUserOp', $adminOps));
233
-
234
-		$this->assertSame(1, count($userOps));
235
-		$this->assertFalse(array_key_exists('OCA\WFE\TestAdminOp', $userOps));
236
-		$this->assertTrue(array_key_exists('OCA\WFE\TestUserOp', $userOps));
237
-		$this->assertSame(2, count($userOps['OCA\WFE\TestUserOp']));
238
-	}
239
-
240
-	public function testGetOperations(): void {
241
-		$adminScope = $this->buildScope();
242
-		$userScope = $this->buildScope('jackie');
243
-		$entity = File::class;
244
-
245
-		$opId1 = $this->invokePrivate(
246
-			$this->manager,
247
-			'insertOperation',
248
-			['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo', $entity, []]
249
-		);
250
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
251
-		$opId4 = $this->invokePrivate(
252
-			$this->manager,
253
-			'insertOperation',
254
-			['OCA\WFE\OtherTestOp', 'Test04', [5], 'foo', $entity, []]
255
-		);
256
-		$this->invokePrivate($this->manager, 'addScope', [$opId4, $adminScope]);
257
-
258
-		$opId2 = $this->invokePrivate(
259
-			$this->manager,
260
-			'insertOperation',
261
-			['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar', $entity, []]
262
-		);
263
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
264
-		$opId3 = $this->invokePrivate(
265
-			$this->manager,
266
-			'insertOperation',
267
-			['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar', $entity, []]
268
-		);
269
-		$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
270
-		$opId5 = $this->invokePrivate(
271
-			$this->manager,
272
-			'insertOperation',
273
-			['OCA\WFE\OtherTestOp', 'Test05', [5], 'foobar', $entity, []]
274
-		);
275
-		$this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
276
-
277
-		$operation = $this->createMock(IOperation::class);
278
-		$operation->expects($this->any())
279
-			->method('isAvailableForScope')
280
-			->willReturnMap([
281
-				[IManager::SCOPE_ADMIN, true],
282
-				[IManager::SCOPE_USER, true],
283
-			]);
284
-
285
-		$this->container->expects($this->any())
286
-			->method('get')
287
-			->willReturnCallback(function ($className) use ($operation) {
288
-				switch ($className) {
289
-					case 'OCA\WFE\TestOp':
290
-						return $operation;
291
-					case 'OCA\WFE\OtherTestOp':
292
-						throw new QueryException();
293
-				}
294
-			});
295
-
296
-		$adminOps = $this->manager->getOperations('OCA\WFE\TestOp', $adminScope);
297
-		$userOps = $this->manager->getOperations('OCA\WFE\TestOp', $userScope);
298
-
299
-		$this->assertSame(1, count($adminOps));
300
-		array_walk($adminOps, function ($op): void {
301
-			$this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
302
-		});
303
-
304
-		$this->assertSame(2, count($userOps));
305
-		array_walk($userOps, function ($op): void {
306
-			$this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
307
-		});
308
-	}
309
-
310
-	public function testGetAllConfiguredEvents(): void {
311
-		$adminScope = $this->buildScope();
312
-		$userScope = $this->buildScope('jackie');
313
-		$entity = File::class;
314
-
315
-		$opId5 = $this->invokePrivate(
316
-			$this->manager,
317
-			'insertOperation',
318
-			['OCA\WFE\OtherTestOp', 'Test04', [], 'foo', $entity, [NodeCreatedEvent::class]]
319
-		);
320
-		$this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
321
-
322
-		$allOperations = null;
323
-
324
-		$cache = $this->createMock(ICache::class);
325
-		$cache
326
-			->method('get')
327
-			->willReturnCallback(function () use (&$allOperations) {
328
-				if ($allOperations) {
329
-					return $allOperations;
330
-				}
331
-
332
-				return null;
333
-			});
334
-
335
-		$this->cacheFactory->method('createDistributed')->willReturn($cache);
336
-		$allOperations = $this->manager->getAllConfiguredEvents();
337
-		$this->assertCount(1, $allOperations);
338
-
339
-		$allOperationsCached = $this->manager->getAllConfiguredEvents();
340
-		$this->assertCount(1, $allOperationsCached);
341
-		$this->assertEquals($allOperationsCached, $allOperations);
342
-	}
343
-
344
-	public function testUpdateOperation(): void {
345
-		$adminScope = $this->buildScope();
346
-		$userScope = $this->buildScope('jackie');
347
-		$entity = File::class;
348
-
349
-		$cache = $this->createMock(ICache::class);
350
-		$cache->expects($this->exactly(4))
351
-			->method('remove')
352
-			->with('events');
353
-		$this->cacheFactory->method('createDistributed')
354
-			->willReturn($cache);
355
-
356
-		$expectedCalls = [
357
-			[IManager::SCOPE_ADMIN],
358
-			[IManager::SCOPE_USER],
359
-		];
360
-		$i = 0;
361
-		$operationMock = $this->createMock(IOperation::class);
362
-		$operationMock->expects($this->any())
363
-			->method('isAvailableForScope')
364
-			->willReturnCallback(function () use (&$expectedCalls, &$i): bool {
365
-				$this->assertEquals($expectedCalls[$i], func_get_args());
366
-				$i++;
367
-				return true;
368
-			});
369
-
370
-		$this->container->expects($this->any())
371
-			->method('get')
372
-			->willReturnCallback(function ($class) use ($operationMock) {
373
-				if (substr($class, -2) === 'Op') {
374
-					return $operationMock;
375
-				} elseif ($class === File::class) {
376
-					return $this->getMockBuilder(File::class)
377
-						->setConstructorArgs([
378
-							$this->l,
379
-							$this->createMock(IURLGenerator::class),
380
-							$this->createMock(IRootFolder::class),
381
-							$this->createMock(IUserSession::class),
382
-							$this->createMock(ISystemTagManager::class),
383
-							$this->createMock(IUserManager::class),
384
-							$this->createMock(UserMountCache::class),
385
-							$this->createMock(IMountManager::class),
386
-						])
387
-						->onlyMethods($this->filterClassMethods(File::class, ['getEvents']))
388
-						->getMock();
389
-				}
390
-				return $this->createMock(ICheck::class);
391
-			});
392
-
393
-		$opId1 = $this->invokePrivate(
394
-			$this->manager,
395
-			'insertOperation',
396
-			['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
397
-		);
398
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
399
-
400
-		$opId2 = $this->invokePrivate(
401
-			$this->manager,
402
-			'insertOperation',
403
-			['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
404
-		);
405
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
406
-
407
-		$check1 = ['class' => 'OCA\WFE\C22', 'operator' => 'eq', 'value' => 'asdf'];
408
-		$check2 = ['class' => 'OCA\WFE\C33', 'operator' => 'eq', 'value' => 23456];
409
-
410
-		/** @noinspection PhpUnhandledExceptionInspection */
411
-		$op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope, $entity, ['\OCP\Files::postDelete']);
412
-		$this->assertSame('Test01a', $op['name']);
413
-		$this->assertSame('foohur', $op['operation']);
414
-
415
-		/** @noinspection PhpUnhandledExceptionInspection */
416
-		$op = $this->manager->updateOperation($opId2, 'Test02a', [$check1], 'barfoo', $userScope, $entity, ['\OCP\Files::postDelete']);
417
-		$this->assertSame('Test02a', $op['name']);
418
-		$this->assertSame('barfoo', $op['operation']);
419
-
420
-		foreach ([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
421
-			try {
422
-				/** @noinspection PhpUnhandledExceptionInspection */
423
-				$this->manager->updateOperation($run[1], 'Evil', [$check2], 'hackx0r', $run[0], $entity, []);
424
-				$this->assertTrue(false, 'DomainException not thrown');
425
-			} catch (\DomainException $e) {
426
-				$this->assertTrue(true);
427
-			}
428
-		}
429
-	}
430
-
431
-	public function testDeleteOperation(): void {
432
-		$adminScope = $this->buildScope();
433
-		$userScope = $this->buildScope('jackie');
434
-		$entity = File::class;
435
-
436
-		$cache = $this->createMock(ICache::class);
437
-		$cache->expects($this->exactly(4))
438
-			->method('remove')
439
-			->with('events');
440
-		$this->cacheFactory->method('createDistributed')->willReturn($cache);
441
-
442
-		$opId1 = $this->invokePrivate(
443
-			$this->manager,
444
-			'insertOperation',
445
-			['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
446
-		);
447
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
448
-
449
-		$opId2 = $this->invokePrivate(
450
-			$this->manager,
451
-			'insertOperation',
452
-			['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
453
-		);
454
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
455
-
456
-		foreach ([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
457
-			try {
458
-				/** @noinspection PhpUnhandledExceptionInspection */
459
-				$this->manager->deleteOperation($run[1], $run[0]);
460
-				$this->assertTrue(false, 'DomainException not thrown');
461
-			} catch (\Exception $e) {
462
-				$this->assertInstanceOf(\DomainException::class, $e);
463
-			}
464
-		}
465
-
466
-		/** @noinspection PhpUnhandledExceptionInspection */
467
-		$this->manager->deleteOperation($opId1, $adminScope);
468
-		/** @noinspection PhpUnhandledExceptionInspection */
469
-		$this->manager->deleteOperation($opId2, $userScope);
470
-
471
-		foreach ([$opId1, $opId2] as $opId) {
472
-			try {
473
-				$this->invokePrivate($this->manager, 'getOperation', [$opId]);
474
-				$this->assertTrue(false, 'UnexpectedValueException not thrown');
475
-			} catch (\Exception $e) {
476
-				$this->assertInstanceOf(\UnexpectedValueException::class, $e);
477
-			}
478
-		}
479
-	}
480
-
481
-	public function testGetEntitiesListBuildInOnly(): void {
482
-		$fileEntityMock = $this->createMock(File::class);
483
-
484
-		$this->container->expects($this->once())
485
-			->method('get')
486
-			->with(File::class)
487
-			->willReturn($fileEntityMock);
488
-
489
-		$entities = $this->manager->getEntitiesList();
490
-
491
-		$this->assertCount(1, $entities);
492
-		$this->assertInstanceOf(IEntity::class, $entities[0]);
493
-	}
494
-
495
-	public function testGetEntitiesList(): void {
496
-		$fileEntityMock = $this->createMock(File::class);
497
-
498
-		$this->container->expects($this->once())
499
-			->method('get')
500
-			->with(File::class)
501
-			->willReturn($fileEntityMock);
502
-
503
-		$extraEntity = $this->createMock(IEntity::class);
504
-
505
-		$this->dispatcher->expects($this->once())
506
-			->method('dispatchTyped')
507
-			->willReturnCallback(function (RegisterEntitiesEvent $e) use ($extraEntity): void {
508
-				$this->manager->registerEntity($extraEntity);
509
-			});
510
-
511
-		$entities = $this->manager->getEntitiesList();
512
-
513
-		$this->assertCount(2, $entities);
514
-
515
-		$entityTypeCounts = array_reduce($entities, function (array $carry, IEntity $entity) {
516
-			if ($entity instanceof File) {
517
-				$carry[0]++;
518
-			} elseif ($entity instanceof IEntity) {
519
-				$carry[1]++;
520
-			}
521
-			return $carry;
522
-		}, [0, 0]);
523
-
524
-		$this->assertSame(1, $entityTypeCounts[0]);
525
-		$this->assertSame(1, $entityTypeCounts[1]);
526
-	}
527
-
528
-	public function testValidateOperationOK(): void {
529
-		$check = [
530
-			'class' => ICheck::class,
531
-			'operator' => 'is',
532
-			'value' => 'barfoo',
533
-		];
534
-
535
-		$operationMock = $this->createMock(IOperation::class);
536
-		$entityMock = $this->createMock(IEntity::class);
537
-		$eventEntityMock = $this->createMock(IEntityEvent::class);
538
-		$checkMock = $this->createMock(ICheck::class);
539
-		$scopeMock = $this->createMock(ScopeContext::class);
540
-
541
-		$scopeMock->expects($this->any())
542
-			->method('getScope')
543
-			->willReturn(IManager::SCOPE_ADMIN);
544
-
545
-		$operationMock->expects($this->once())
546
-			->method('isAvailableForScope')
547
-			->with(IManager::SCOPE_ADMIN)
548
-			->willReturn(true);
549
-
550
-		$operationMock->expects($this->once())
551
-			->method('validateOperation')
552
-			->with('test', [$check], 'operationData');
553
-
554
-		$entityMock->expects($this->any())
555
-			->method('getEvents')
556
-			->willReturn([$eventEntityMock]);
557
-
558
-		$eventEntityMock->expects($this->any())
559
-			->method('getEventName')
560
-			->willReturn('MyEvent');
561
-
562
-		$checkMock->expects($this->any())
563
-			->method('supportedEntities')
564
-			->willReturn([IEntity::class]);
565
-		$checkMock->expects($this->atLeastOnce())
566
-			->method('validateCheck');
567
-
568
-		$this->container->expects($this->any())
569
-			->method('get')
570
-			->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
571
-				switch ($className) {
572
-					case IOperation::class:
573
-						return $operationMock;
574
-					case IEntity::class:
575
-						return $entityMock;
576
-					case IEntityEvent::class:
577
-						return $eventEntityMock;
578
-					case ICheck::class:
579
-						return $checkMock;
580
-					default:
581
-						return $this->createMock($className);
582
-				}
583
-			});
584
-
585
-		$this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
586
-	}
587
-
588
-	public function testValidateOperationCheckInputLengthError(): void {
589
-		$check = [
590
-			'class' => ICheck::class,
591
-			'operator' => 'is',
592
-			'value' => str_pad('', IManager::MAX_CHECK_VALUE_BYTES + 1, 'FooBar'),
593
-		];
594
-
595
-		$operationMock = $this->createMock(IOperation::class);
596
-		$entityMock = $this->createMock(IEntity::class);
597
-		$eventEntityMock = $this->createMock(IEntityEvent::class);
598
-		$checkMock = $this->createMock(ICheck::class);
599
-		$scopeMock = $this->createMock(ScopeContext::class);
600
-
601
-		$scopeMock->expects($this->any())
602
-			->method('getScope')
603
-			->willReturn(IManager::SCOPE_ADMIN);
604
-
605
-		$operationMock->expects($this->once())
606
-			->method('isAvailableForScope')
607
-			->with(IManager::SCOPE_ADMIN)
608
-			->willReturn(true);
609
-
610
-		$operationMock->expects($this->once())
611
-			->method('validateOperation')
612
-			->with('test', [$check], 'operationData');
613
-
614
-		$entityMock->expects($this->any())
615
-			->method('getEvents')
616
-			->willReturn([$eventEntityMock]);
617
-
618
-		$eventEntityMock->expects($this->any())
619
-			->method('getEventName')
620
-			->willReturn('MyEvent');
621
-
622
-		$checkMock->expects($this->any())
623
-			->method('supportedEntities')
624
-			->willReturn([IEntity::class]);
625
-		$checkMock->expects($this->never())
626
-			->method('validateCheck');
627
-
628
-		$this->container->expects($this->any())
629
-			->method('get')
630
-			->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
631
-				switch ($className) {
632
-					case IOperation::class:
633
-						return $operationMock;
634
-					case IEntity::class:
635
-						return $entityMock;
636
-					case IEntityEvent::class:
637
-						return $eventEntityMock;
638
-					case ICheck::class:
639
-						return $checkMock;
640
-					default:
641
-						return $this->createMock($className);
642
-				}
643
-			});
644
-
645
-		try {
646
-			$this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
647
-		} catch (\UnexpectedValueException $e) {
648
-			$this->assertSame('The provided check value is too long', $e->getMessage());
649
-		}
650
-	}
651
-
652
-	public function testValidateOperationDataLengthError(): void {
653
-		$check = [
654
-			'class' => ICheck::class,
655
-			'operator' => 'is',
656
-			'value' => 'barfoo',
657
-		];
658
-		$operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES + 1, 'FooBar');
659
-
660
-		$operationMock = $this->createMock(IOperation::class);
661
-		$entityMock = $this->createMock(IEntity::class);
662
-		$eventEntityMock = $this->createMock(IEntityEvent::class);
663
-		$checkMock = $this->createMock(ICheck::class);
664
-		$scopeMock = $this->createMock(ScopeContext::class);
665
-
666
-		$scopeMock->expects($this->any())
667
-			->method('getScope')
668
-			->willReturn(IManager::SCOPE_ADMIN);
669
-
670
-		$operationMock->expects($this->once())
671
-			->method('isAvailableForScope')
672
-			->with(IManager::SCOPE_ADMIN)
673
-			->willReturn(true);
674
-
675
-		$operationMock->expects($this->never())
676
-			->method('validateOperation');
677
-
678
-		$entityMock->expects($this->any())
679
-			->method('getEvents')
680
-			->willReturn([$eventEntityMock]);
681
-
682
-		$eventEntityMock->expects($this->any())
683
-			->method('getEventName')
684
-			->willReturn('MyEvent');
685
-
686
-		$checkMock->expects($this->any())
687
-			->method('supportedEntities')
688
-			->willReturn([IEntity::class]);
689
-		$checkMock->expects($this->never())
690
-			->method('validateCheck');
691
-
692
-		$this->container->expects($this->any())
693
-			->method('get')
694
-			->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
695
-				switch ($className) {
696
-					case IOperation::class:
697
-						return $operationMock;
698
-					case IEntity::class:
699
-						return $entityMock;
700
-					case IEntityEvent::class:
701
-						return $eventEntityMock;
702
-					case ICheck::class:
703
-						return $checkMock;
704
-					default:
705
-						return $this->createMock($className);
706
-				}
707
-			});
708
-
709
-		try {
710
-			$this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
711
-		} catch (\UnexpectedValueException $e) {
712
-			$this->assertSame('The provided operation data is too long', $e->getMessage());
713
-		}
714
-	}
715
-
716
-	public function testValidateOperationScopeNotAvailable(): void {
717
-		$check = [
718
-			'class' => ICheck::class,
719
-			'operator' => 'is',
720
-			'value' => 'barfoo',
721
-		];
722
-		$operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES + 1, 'FooBar');
723
-
724
-		$operationMock = $this->createMock(IOperation::class);
725
-		$entityMock = $this->createMock(IEntity::class);
726
-		$eventEntityMock = $this->createMock(IEntityEvent::class);
727
-		$checkMock = $this->createMock(ICheck::class);
728
-		$scopeMock = $this->createMock(ScopeContext::class);
729
-
730
-		$scopeMock->expects($this->any())
731
-			->method('getScope')
732
-			->willReturn(IManager::SCOPE_ADMIN);
733
-
734
-		$operationMock->expects($this->once())
735
-			->method('isAvailableForScope')
736
-			->with(IManager::SCOPE_ADMIN)
737
-			->willReturn(false);
738
-
739
-		$operationMock->expects($this->never())
740
-			->method('validateOperation');
741
-
742
-		$entityMock->expects($this->any())
743
-			->method('getEvents')
744
-			->willReturn([$eventEntityMock]);
745
-
746
-		$eventEntityMock->expects($this->any())
747
-			->method('getEventName')
748
-			->willReturn('MyEvent');
749
-
750
-		$checkMock->expects($this->any())
751
-			->method('supportedEntities')
752
-			->willReturn([IEntity::class]);
753
-		$checkMock->expects($this->never())
754
-			->method('validateCheck');
755
-
756
-		$this->container->expects($this->any())
757
-			->method('get')
758
-			->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
759
-				switch ($className) {
760
-					case IOperation::class:
761
-						return $operationMock;
762
-					case IEntity::class:
763
-						return $entityMock;
764
-					case IEntityEvent::class:
765
-						return $eventEntityMock;
766
-					case ICheck::class:
767
-						return $checkMock;
768
-					default:
769
-						return $this->createMock($className);
770
-				}
771
-			});
772
-
773
-		try {
774
-			$this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
775
-		} catch (\UnexpectedValueException $e) {
776
-			$this->assertSame('Operation OCP\WorkflowEngine\IOperation is invalid', $e->getMessage());
777
-		}
778
-	}
46
+    protected Manager $manager;
47
+    protected IDBConnection $db;
48
+    protected LoggerInterface&MockObject $logger;
49
+    protected ContainerInterface&MockObject $container;
50
+    protected IUserSession&MockObject $session;
51
+    protected IL10N&MockObject $l;
52
+    protected IEventDispatcher&MockObject $dispatcher;
53
+    protected IAppConfig&MockObject $config;
54
+    protected ICacheFactory&MockObject $cacheFactory;
55
+
56
+    protected function setUp(): void {
57
+        parent::setUp();
58
+
59
+        $this->db = Server::get(IDBConnection::class);
60
+        $this->container = $this->createMock(ContainerInterface::class);
61
+        $this->l = $this->createMock(IL10N::class);
62
+        $this->l->method('t')
63
+            ->willReturnCallback(function ($text, $parameters = []) {
64
+                return vsprintf($text, $parameters);
65
+            });
66
+
67
+        $this->logger = $this->createMock(LoggerInterface::class);
68
+        $this->session = $this->createMock(IUserSession::class);
69
+        $this->dispatcher = $this->createMock(IEventDispatcher::class);
70
+        $this->config = $this->createMock(IAppConfig::class);
71
+        $this->cacheFactory = $this->createMock(ICacheFactory::class);
72
+
73
+        $this->manager = new Manager(
74
+            $this->db,
75
+            $this->container,
76
+            $this->l,
77
+            $this->logger,
78
+            $this->session,
79
+            $this->dispatcher,
80
+            $this->config,
81
+            $this->cacheFactory
82
+        );
83
+        $this->clearTables();
84
+    }
85
+
86
+    protected function tearDown(): void {
87
+        $this->clearTables();
88
+        parent::tearDown();
89
+    }
90
+
91
+    protected function buildScope(?string $scopeId = null): MockObject&ScopeContext {
92
+        $scopeContext = $this->createMock(ScopeContext::class);
93
+        $scopeContext->expects($this->any())
94
+            ->method('getScope')
95
+            ->willReturn($scopeId ? IManager::SCOPE_USER : IManager::SCOPE_ADMIN);
96
+        $scopeContext->expects($this->any())
97
+            ->method('getScopeId')
98
+            ->willReturn($scopeId ?? '');
99
+        $scopeContext->expects($this->any())
100
+            ->method('getHash')
101
+            ->willReturn(md5($scopeId ?? ''));
102
+
103
+        return $scopeContext;
104
+    }
105
+
106
+    public function clearTables() {
107
+        $query = $this->db->getQueryBuilder();
108
+        foreach (['flow_checks', 'flow_operations', 'flow_operations_scope'] as $table) {
109
+            $query->delete($table)
110
+                ->executeStatement();
111
+        }
112
+    }
113
+
114
+    public function testChecks(): void {
115
+        $check1 = $this->invokePrivate($this->manager, 'addCheck', ['Test', 'equal', 1]);
116
+        $check2 = $this->invokePrivate($this->manager, 'addCheck', ['Test', '!equal', 2]);
117
+
118
+        $data = $this->manager->getChecks([$check1]);
119
+        $this->assertArrayHasKey($check1, $data);
120
+        $this->assertArrayNotHasKey($check2, $data);
121
+
122
+        $data = $this->manager->getChecks([$check1, $check2]);
123
+        $this->assertArrayHasKey($check1, $data);
124
+        $this->assertArrayHasKey($check2, $data);
125
+
126
+        $data = $this->manager->getChecks([$check2, $check1]);
127
+        $this->assertArrayHasKey($check1, $data);
128
+        $this->assertArrayHasKey($check2, $data);
129
+
130
+        $data = $this->manager->getChecks([$check2]);
131
+        $this->assertArrayNotHasKey($check1, $data);
132
+        $this->assertArrayHasKey($check2, $data);
133
+    }
134
+
135
+    public function testScope(): void {
136
+        $adminScope = $this->buildScope();
137
+        $userScope = $this->buildScope('jackie');
138
+        $entity = File::class;
139
+
140
+        $opId1 = $this->invokePrivate(
141
+            $this->manager,
142
+            'insertOperation',
143
+            ['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo', $entity, []]
144
+        );
145
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
146
+
147
+        $opId2 = $this->invokePrivate(
148
+            $this->manager,
149
+            'insertOperation',
150
+            ['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar', $entity, []]
151
+        );
152
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
153
+        $opId3 = $this->invokePrivate(
154
+            $this->manager,
155
+            'insertOperation',
156
+            ['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar', $entity, []]
157
+        );
158
+        $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
159
+
160
+        $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId1, $adminScope]));
161
+        $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId2, $adminScope]));
162
+        $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId3, $adminScope]));
163
+
164
+        $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId1, $userScope]));
165
+        $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId2, $userScope]));
166
+        $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId3, $userScope]));
167
+    }
168
+
169
+    public function testGetAllOperations(): void {
170
+        $adminScope = $this->buildScope();
171
+        $userScope = $this->buildScope('jackie');
172
+        $entity = File::class;
173
+
174
+        $adminOperation = $this->createMock(IOperation::class);
175
+        $adminOperation->expects($this->any())
176
+            ->method('isAvailableForScope')
177
+            ->willReturnMap([
178
+                [IManager::SCOPE_ADMIN, true],
179
+                [IManager::SCOPE_USER, false],
180
+            ]);
181
+        $userOperation = $this->createMock(IOperation::class);
182
+        $userOperation->expects($this->any())
183
+            ->method('isAvailableForScope')
184
+            ->willReturnMap([
185
+                [IManager::SCOPE_ADMIN, false],
186
+                [IManager::SCOPE_USER, true],
187
+            ]);
188
+
189
+        $this->container->expects($this->any())
190
+            ->method('get')
191
+            ->willReturnCallback(function ($className) use ($adminOperation, $userOperation) {
192
+                switch ($className) {
193
+                    case 'OCA\WFE\TestAdminOp':
194
+                        return $adminOperation;
195
+                    case 'OCA\WFE\TestUserOp':
196
+                        return $userOperation;
197
+                }
198
+            });
199
+
200
+        $opId1 = $this->invokePrivate(
201
+            $this->manager,
202
+            'insertOperation',
203
+            ['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
204
+        );
205
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
206
+
207
+        $opId2 = $this->invokePrivate(
208
+            $this->manager,
209
+            'insertOperation',
210
+            ['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
211
+        );
212
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
213
+        $opId3 = $this->invokePrivate(
214
+            $this->manager,
215
+            'insertOperation',
216
+            ['OCA\WFE\TestUserOp', 'Test03', [11, 44], 'foobar', $entity, []]
217
+        );
218
+        $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
219
+
220
+        $opId4 = $this->invokePrivate(
221
+            $this->manager,
222
+            'insertOperation',
223
+            ['OCA\WFE\TestAdminOp', 'Test04', [41, 10, 4], 'NoBar', $entity, []]
224
+        );
225
+        $this->invokePrivate($this->manager, 'addScope', [$opId4, $userScope]);
226
+
227
+        $adminOps = $this->manager->getAllOperations($adminScope);
228
+        $userOps = $this->manager->getAllOperations($userScope);
229
+
230
+        $this->assertSame(1, count($adminOps));
231
+        $this->assertTrue(array_key_exists('OCA\WFE\TestAdminOp', $adminOps));
232
+        $this->assertFalse(array_key_exists('OCA\WFE\TestUserOp', $adminOps));
233
+
234
+        $this->assertSame(1, count($userOps));
235
+        $this->assertFalse(array_key_exists('OCA\WFE\TestAdminOp', $userOps));
236
+        $this->assertTrue(array_key_exists('OCA\WFE\TestUserOp', $userOps));
237
+        $this->assertSame(2, count($userOps['OCA\WFE\TestUserOp']));
238
+    }
239
+
240
+    public function testGetOperations(): void {
241
+        $adminScope = $this->buildScope();
242
+        $userScope = $this->buildScope('jackie');
243
+        $entity = File::class;
244
+
245
+        $opId1 = $this->invokePrivate(
246
+            $this->manager,
247
+            'insertOperation',
248
+            ['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo', $entity, []]
249
+        );
250
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
251
+        $opId4 = $this->invokePrivate(
252
+            $this->manager,
253
+            'insertOperation',
254
+            ['OCA\WFE\OtherTestOp', 'Test04', [5], 'foo', $entity, []]
255
+        );
256
+        $this->invokePrivate($this->manager, 'addScope', [$opId4, $adminScope]);
257
+
258
+        $opId2 = $this->invokePrivate(
259
+            $this->manager,
260
+            'insertOperation',
261
+            ['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar', $entity, []]
262
+        );
263
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
264
+        $opId3 = $this->invokePrivate(
265
+            $this->manager,
266
+            'insertOperation',
267
+            ['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar', $entity, []]
268
+        );
269
+        $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
270
+        $opId5 = $this->invokePrivate(
271
+            $this->manager,
272
+            'insertOperation',
273
+            ['OCA\WFE\OtherTestOp', 'Test05', [5], 'foobar', $entity, []]
274
+        );
275
+        $this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
276
+
277
+        $operation = $this->createMock(IOperation::class);
278
+        $operation->expects($this->any())
279
+            ->method('isAvailableForScope')
280
+            ->willReturnMap([
281
+                [IManager::SCOPE_ADMIN, true],
282
+                [IManager::SCOPE_USER, true],
283
+            ]);
284
+
285
+        $this->container->expects($this->any())
286
+            ->method('get')
287
+            ->willReturnCallback(function ($className) use ($operation) {
288
+                switch ($className) {
289
+                    case 'OCA\WFE\TestOp':
290
+                        return $operation;
291
+                    case 'OCA\WFE\OtherTestOp':
292
+                        throw new QueryException();
293
+                }
294
+            });
295
+
296
+        $adminOps = $this->manager->getOperations('OCA\WFE\TestOp', $adminScope);
297
+        $userOps = $this->manager->getOperations('OCA\WFE\TestOp', $userScope);
298
+
299
+        $this->assertSame(1, count($adminOps));
300
+        array_walk($adminOps, function ($op): void {
301
+            $this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
302
+        });
303
+
304
+        $this->assertSame(2, count($userOps));
305
+        array_walk($userOps, function ($op): void {
306
+            $this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
307
+        });
308
+    }
309
+
310
+    public function testGetAllConfiguredEvents(): void {
311
+        $adminScope = $this->buildScope();
312
+        $userScope = $this->buildScope('jackie');
313
+        $entity = File::class;
314
+
315
+        $opId5 = $this->invokePrivate(
316
+            $this->manager,
317
+            'insertOperation',
318
+            ['OCA\WFE\OtherTestOp', 'Test04', [], 'foo', $entity, [NodeCreatedEvent::class]]
319
+        );
320
+        $this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
321
+
322
+        $allOperations = null;
323
+
324
+        $cache = $this->createMock(ICache::class);
325
+        $cache
326
+            ->method('get')
327
+            ->willReturnCallback(function () use (&$allOperations) {
328
+                if ($allOperations) {
329
+                    return $allOperations;
330
+                }
331
+
332
+                return null;
333
+            });
334
+
335
+        $this->cacheFactory->method('createDistributed')->willReturn($cache);
336
+        $allOperations = $this->manager->getAllConfiguredEvents();
337
+        $this->assertCount(1, $allOperations);
338
+
339
+        $allOperationsCached = $this->manager->getAllConfiguredEvents();
340
+        $this->assertCount(1, $allOperationsCached);
341
+        $this->assertEquals($allOperationsCached, $allOperations);
342
+    }
343
+
344
+    public function testUpdateOperation(): void {
345
+        $adminScope = $this->buildScope();
346
+        $userScope = $this->buildScope('jackie');
347
+        $entity = File::class;
348
+
349
+        $cache = $this->createMock(ICache::class);
350
+        $cache->expects($this->exactly(4))
351
+            ->method('remove')
352
+            ->with('events');
353
+        $this->cacheFactory->method('createDistributed')
354
+            ->willReturn($cache);
355
+
356
+        $expectedCalls = [
357
+            [IManager::SCOPE_ADMIN],
358
+            [IManager::SCOPE_USER],
359
+        ];
360
+        $i = 0;
361
+        $operationMock = $this->createMock(IOperation::class);
362
+        $operationMock->expects($this->any())
363
+            ->method('isAvailableForScope')
364
+            ->willReturnCallback(function () use (&$expectedCalls, &$i): bool {
365
+                $this->assertEquals($expectedCalls[$i], func_get_args());
366
+                $i++;
367
+                return true;
368
+            });
369
+
370
+        $this->container->expects($this->any())
371
+            ->method('get')
372
+            ->willReturnCallback(function ($class) use ($operationMock) {
373
+                if (substr($class, -2) === 'Op') {
374
+                    return $operationMock;
375
+                } elseif ($class === File::class) {
376
+                    return $this->getMockBuilder(File::class)
377
+                        ->setConstructorArgs([
378
+                            $this->l,
379
+                            $this->createMock(IURLGenerator::class),
380
+                            $this->createMock(IRootFolder::class),
381
+                            $this->createMock(IUserSession::class),
382
+                            $this->createMock(ISystemTagManager::class),
383
+                            $this->createMock(IUserManager::class),
384
+                            $this->createMock(UserMountCache::class),
385
+                            $this->createMock(IMountManager::class),
386
+                        ])
387
+                        ->onlyMethods($this->filterClassMethods(File::class, ['getEvents']))
388
+                        ->getMock();
389
+                }
390
+                return $this->createMock(ICheck::class);
391
+            });
392
+
393
+        $opId1 = $this->invokePrivate(
394
+            $this->manager,
395
+            'insertOperation',
396
+            ['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
397
+        );
398
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
399
+
400
+        $opId2 = $this->invokePrivate(
401
+            $this->manager,
402
+            'insertOperation',
403
+            ['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
404
+        );
405
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
406
+
407
+        $check1 = ['class' => 'OCA\WFE\C22', 'operator' => 'eq', 'value' => 'asdf'];
408
+        $check2 = ['class' => 'OCA\WFE\C33', 'operator' => 'eq', 'value' => 23456];
409
+
410
+        /** @noinspection PhpUnhandledExceptionInspection */
411
+        $op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope, $entity, ['\OCP\Files::postDelete']);
412
+        $this->assertSame('Test01a', $op['name']);
413
+        $this->assertSame('foohur', $op['operation']);
414
+
415
+        /** @noinspection PhpUnhandledExceptionInspection */
416
+        $op = $this->manager->updateOperation($opId2, 'Test02a', [$check1], 'barfoo', $userScope, $entity, ['\OCP\Files::postDelete']);
417
+        $this->assertSame('Test02a', $op['name']);
418
+        $this->assertSame('barfoo', $op['operation']);
419
+
420
+        foreach ([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
421
+            try {
422
+                /** @noinspection PhpUnhandledExceptionInspection */
423
+                $this->manager->updateOperation($run[1], 'Evil', [$check2], 'hackx0r', $run[0], $entity, []);
424
+                $this->assertTrue(false, 'DomainException not thrown');
425
+            } catch (\DomainException $e) {
426
+                $this->assertTrue(true);
427
+            }
428
+        }
429
+    }
430
+
431
+    public function testDeleteOperation(): void {
432
+        $adminScope = $this->buildScope();
433
+        $userScope = $this->buildScope('jackie');
434
+        $entity = File::class;
435
+
436
+        $cache = $this->createMock(ICache::class);
437
+        $cache->expects($this->exactly(4))
438
+            ->method('remove')
439
+            ->with('events');
440
+        $this->cacheFactory->method('createDistributed')->willReturn($cache);
441
+
442
+        $opId1 = $this->invokePrivate(
443
+            $this->manager,
444
+            'insertOperation',
445
+            ['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
446
+        );
447
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
448
+
449
+        $opId2 = $this->invokePrivate(
450
+            $this->manager,
451
+            'insertOperation',
452
+            ['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
453
+        );
454
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
455
+
456
+        foreach ([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
457
+            try {
458
+                /** @noinspection PhpUnhandledExceptionInspection */
459
+                $this->manager->deleteOperation($run[1], $run[0]);
460
+                $this->assertTrue(false, 'DomainException not thrown');
461
+            } catch (\Exception $e) {
462
+                $this->assertInstanceOf(\DomainException::class, $e);
463
+            }
464
+        }
465
+
466
+        /** @noinspection PhpUnhandledExceptionInspection */
467
+        $this->manager->deleteOperation($opId1, $adminScope);
468
+        /** @noinspection PhpUnhandledExceptionInspection */
469
+        $this->manager->deleteOperation($opId2, $userScope);
470
+
471
+        foreach ([$opId1, $opId2] as $opId) {
472
+            try {
473
+                $this->invokePrivate($this->manager, 'getOperation', [$opId]);
474
+                $this->assertTrue(false, 'UnexpectedValueException not thrown');
475
+            } catch (\Exception $e) {
476
+                $this->assertInstanceOf(\UnexpectedValueException::class, $e);
477
+            }
478
+        }
479
+    }
480
+
481
+    public function testGetEntitiesListBuildInOnly(): void {
482
+        $fileEntityMock = $this->createMock(File::class);
483
+
484
+        $this->container->expects($this->once())
485
+            ->method('get')
486
+            ->with(File::class)
487
+            ->willReturn($fileEntityMock);
488
+
489
+        $entities = $this->manager->getEntitiesList();
490
+
491
+        $this->assertCount(1, $entities);
492
+        $this->assertInstanceOf(IEntity::class, $entities[0]);
493
+    }
494
+
495
+    public function testGetEntitiesList(): void {
496
+        $fileEntityMock = $this->createMock(File::class);
497
+
498
+        $this->container->expects($this->once())
499
+            ->method('get')
500
+            ->with(File::class)
501
+            ->willReturn($fileEntityMock);
502
+
503
+        $extraEntity = $this->createMock(IEntity::class);
504
+
505
+        $this->dispatcher->expects($this->once())
506
+            ->method('dispatchTyped')
507
+            ->willReturnCallback(function (RegisterEntitiesEvent $e) use ($extraEntity): void {
508
+                $this->manager->registerEntity($extraEntity);
509
+            });
510
+
511
+        $entities = $this->manager->getEntitiesList();
512
+
513
+        $this->assertCount(2, $entities);
514
+
515
+        $entityTypeCounts = array_reduce($entities, function (array $carry, IEntity $entity) {
516
+            if ($entity instanceof File) {
517
+                $carry[0]++;
518
+            } elseif ($entity instanceof IEntity) {
519
+                $carry[1]++;
520
+            }
521
+            return $carry;
522
+        }, [0, 0]);
523
+
524
+        $this->assertSame(1, $entityTypeCounts[0]);
525
+        $this->assertSame(1, $entityTypeCounts[1]);
526
+    }
527
+
528
+    public function testValidateOperationOK(): void {
529
+        $check = [
530
+            'class' => ICheck::class,
531
+            'operator' => 'is',
532
+            'value' => 'barfoo',
533
+        ];
534
+
535
+        $operationMock = $this->createMock(IOperation::class);
536
+        $entityMock = $this->createMock(IEntity::class);
537
+        $eventEntityMock = $this->createMock(IEntityEvent::class);
538
+        $checkMock = $this->createMock(ICheck::class);
539
+        $scopeMock = $this->createMock(ScopeContext::class);
540
+
541
+        $scopeMock->expects($this->any())
542
+            ->method('getScope')
543
+            ->willReturn(IManager::SCOPE_ADMIN);
544
+
545
+        $operationMock->expects($this->once())
546
+            ->method('isAvailableForScope')
547
+            ->with(IManager::SCOPE_ADMIN)
548
+            ->willReturn(true);
549
+
550
+        $operationMock->expects($this->once())
551
+            ->method('validateOperation')
552
+            ->with('test', [$check], 'operationData');
553
+
554
+        $entityMock->expects($this->any())
555
+            ->method('getEvents')
556
+            ->willReturn([$eventEntityMock]);
557
+
558
+        $eventEntityMock->expects($this->any())
559
+            ->method('getEventName')
560
+            ->willReturn('MyEvent');
561
+
562
+        $checkMock->expects($this->any())
563
+            ->method('supportedEntities')
564
+            ->willReturn([IEntity::class]);
565
+        $checkMock->expects($this->atLeastOnce())
566
+            ->method('validateCheck');
567
+
568
+        $this->container->expects($this->any())
569
+            ->method('get')
570
+            ->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
571
+                switch ($className) {
572
+                    case IOperation::class:
573
+                        return $operationMock;
574
+                    case IEntity::class:
575
+                        return $entityMock;
576
+                    case IEntityEvent::class:
577
+                        return $eventEntityMock;
578
+                    case ICheck::class:
579
+                        return $checkMock;
580
+                    default:
581
+                        return $this->createMock($className);
582
+                }
583
+            });
584
+
585
+        $this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
586
+    }
587
+
588
+    public function testValidateOperationCheckInputLengthError(): void {
589
+        $check = [
590
+            'class' => ICheck::class,
591
+            'operator' => 'is',
592
+            'value' => str_pad('', IManager::MAX_CHECK_VALUE_BYTES + 1, 'FooBar'),
593
+        ];
594
+
595
+        $operationMock = $this->createMock(IOperation::class);
596
+        $entityMock = $this->createMock(IEntity::class);
597
+        $eventEntityMock = $this->createMock(IEntityEvent::class);
598
+        $checkMock = $this->createMock(ICheck::class);
599
+        $scopeMock = $this->createMock(ScopeContext::class);
600
+
601
+        $scopeMock->expects($this->any())
602
+            ->method('getScope')
603
+            ->willReturn(IManager::SCOPE_ADMIN);
604
+
605
+        $operationMock->expects($this->once())
606
+            ->method('isAvailableForScope')
607
+            ->with(IManager::SCOPE_ADMIN)
608
+            ->willReturn(true);
609
+
610
+        $operationMock->expects($this->once())
611
+            ->method('validateOperation')
612
+            ->with('test', [$check], 'operationData');
613
+
614
+        $entityMock->expects($this->any())
615
+            ->method('getEvents')
616
+            ->willReturn([$eventEntityMock]);
617
+
618
+        $eventEntityMock->expects($this->any())
619
+            ->method('getEventName')
620
+            ->willReturn('MyEvent');
621
+
622
+        $checkMock->expects($this->any())
623
+            ->method('supportedEntities')
624
+            ->willReturn([IEntity::class]);
625
+        $checkMock->expects($this->never())
626
+            ->method('validateCheck');
627
+
628
+        $this->container->expects($this->any())
629
+            ->method('get')
630
+            ->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
631
+                switch ($className) {
632
+                    case IOperation::class:
633
+                        return $operationMock;
634
+                    case IEntity::class:
635
+                        return $entityMock;
636
+                    case IEntityEvent::class:
637
+                        return $eventEntityMock;
638
+                    case ICheck::class:
639
+                        return $checkMock;
640
+                    default:
641
+                        return $this->createMock($className);
642
+                }
643
+            });
644
+
645
+        try {
646
+            $this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
647
+        } catch (\UnexpectedValueException $e) {
648
+            $this->assertSame('The provided check value is too long', $e->getMessage());
649
+        }
650
+    }
651
+
652
+    public function testValidateOperationDataLengthError(): void {
653
+        $check = [
654
+            'class' => ICheck::class,
655
+            'operator' => 'is',
656
+            'value' => 'barfoo',
657
+        ];
658
+        $operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES + 1, 'FooBar');
659
+
660
+        $operationMock = $this->createMock(IOperation::class);
661
+        $entityMock = $this->createMock(IEntity::class);
662
+        $eventEntityMock = $this->createMock(IEntityEvent::class);
663
+        $checkMock = $this->createMock(ICheck::class);
664
+        $scopeMock = $this->createMock(ScopeContext::class);
665
+
666
+        $scopeMock->expects($this->any())
667
+            ->method('getScope')
668
+            ->willReturn(IManager::SCOPE_ADMIN);
669
+
670
+        $operationMock->expects($this->once())
671
+            ->method('isAvailableForScope')
672
+            ->with(IManager::SCOPE_ADMIN)
673
+            ->willReturn(true);
674
+
675
+        $operationMock->expects($this->never())
676
+            ->method('validateOperation');
677
+
678
+        $entityMock->expects($this->any())
679
+            ->method('getEvents')
680
+            ->willReturn([$eventEntityMock]);
681
+
682
+        $eventEntityMock->expects($this->any())
683
+            ->method('getEventName')
684
+            ->willReturn('MyEvent');
685
+
686
+        $checkMock->expects($this->any())
687
+            ->method('supportedEntities')
688
+            ->willReturn([IEntity::class]);
689
+        $checkMock->expects($this->never())
690
+            ->method('validateCheck');
691
+
692
+        $this->container->expects($this->any())
693
+            ->method('get')
694
+            ->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
695
+                switch ($className) {
696
+                    case IOperation::class:
697
+                        return $operationMock;
698
+                    case IEntity::class:
699
+                        return $entityMock;
700
+                    case IEntityEvent::class:
701
+                        return $eventEntityMock;
702
+                    case ICheck::class:
703
+                        return $checkMock;
704
+                    default:
705
+                        return $this->createMock($className);
706
+                }
707
+            });
708
+
709
+        try {
710
+            $this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
711
+        } catch (\UnexpectedValueException $e) {
712
+            $this->assertSame('The provided operation data is too long', $e->getMessage());
713
+        }
714
+    }
715
+
716
+    public function testValidateOperationScopeNotAvailable(): void {
717
+        $check = [
718
+            'class' => ICheck::class,
719
+            'operator' => 'is',
720
+            'value' => 'barfoo',
721
+        ];
722
+        $operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES + 1, 'FooBar');
723
+
724
+        $operationMock = $this->createMock(IOperation::class);
725
+        $entityMock = $this->createMock(IEntity::class);
726
+        $eventEntityMock = $this->createMock(IEntityEvent::class);
727
+        $checkMock = $this->createMock(ICheck::class);
728
+        $scopeMock = $this->createMock(ScopeContext::class);
729
+
730
+        $scopeMock->expects($this->any())
731
+            ->method('getScope')
732
+            ->willReturn(IManager::SCOPE_ADMIN);
733
+
734
+        $operationMock->expects($this->once())
735
+            ->method('isAvailableForScope')
736
+            ->with(IManager::SCOPE_ADMIN)
737
+            ->willReturn(false);
738
+
739
+        $operationMock->expects($this->never())
740
+            ->method('validateOperation');
741
+
742
+        $entityMock->expects($this->any())
743
+            ->method('getEvents')
744
+            ->willReturn([$eventEntityMock]);
745
+
746
+        $eventEntityMock->expects($this->any())
747
+            ->method('getEventName')
748
+            ->willReturn('MyEvent');
749
+
750
+        $checkMock->expects($this->any())
751
+            ->method('supportedEntities')
752
+            ->willReturn([IEntity::class]);
753
+        $checkMock->expects($this->never())
754
+            ->method('validateCheck');
755
+
756
+        $this->container->expects($this->any())
757
+            ->method('get')
758
+            ->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
759
+                switch ($className) {
760
+                    case IOperation::class:
761
+                        return $operationMock;
762
+                    case IEntity::class:
763
+                        return $entityMock;
764
+                    case IEntityEvent::class:
765
+                        return $eventEntityMock;
766
+                    case ICheck::class:
767
+                        return $checkMock;
768
+                    default:
769
+                        return $this->createMock($className);
770
+                }
771
+            });
772
+
773
+        try {
774
+            $this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
775
+        } catch (\UnexpectedValueException $e) {
776
+            $this->assertSame('Operation OCP\WorkflowEngine\IOperation is invalid', $e->getMessage());
777
+        }
778
+    }
779 779
 }
Please login to merge, or discard this patch.
apps/federatedfilesharing/lib/Migration/Version1011Date20201120125158.php 1 patch
Indentation   +31 added lines, -31 removed lines patch added patch discarded remove patch
@@ -18,35 +18,35 @@
 block discarded – undo
18 18
 
19 19
 class Version1011Date20201120125158 extends SimpleMigrationStep {
20 20
 
21
-	public function __construct(
22
-		private IDBConnection $connection,
23
-	) {
24
-	}
25
-
26
-	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
27
-		/** @var ISchemaWrapper $schema */
28
-		$schema = $schemaClosure();
29
-
30
-		if ($schema->hasTable('federated_reshares')) {
31
-			$table = $schema->getTable('federated_reshares');
32
-			$remoteIdColumn = $table->getColumn('remote_id');
33
-			if ($remoteIdColumn && Type::lookupName($remoteIdColumn->getType()) !== Types::STRING) {
34
-				$remoteIdColumn->setNotnull(false);
35
-				$remoteIdColumn->setType(Type::getType(Types::STRING));
36
-				$remoteIdColumn->setOptions(['length' => 255]);
37
-				$remoteIdColumn->setDefault('');
38
-				return $schema;
39
-			}
40
-		}
41
-
42
-		return null;
43
-	}
44
-
45
-	public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
46
-		$qb = $this->connection->getQueryBuilder();
47
-		$qb->update('federated_reshares')
48
-			->set('remote_id', $qb->createNamedParameter(''))
49
-			->where($qb->expr()->eq('remote_id', $qb->createNamedParameter('-1')));
50
-		$qb->executeStatement();
51
-	}
21
+    public function __construct(
22
+        private IDBConnection $connection,
23
+    ) {
24
+    }
25
+
26
+    public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
27
+        /** @var ISchemaWrapper $schema */
28
+        $schema = $schemaClosure();
29
+
30
+        if ($schema->hasTable('federated_reshares')) {
31
+            $table = $schema->getTable('federated_reshares');
32
+            $remoteIdColumn = $table->getColumn('remote_id');
33
+            if ($remoteIdColumn && Type::lookupName($remoteIdColumn->getType()) !== Types::STRING) {
34
+                $remoteIdColumn->setNotnull(false);
35
+                $remoteIdColumn->setType(Type::getType(Types::STRING));
36
+                $remoteIdColumn->setOptions(['length' => 255]);
37
+                $remoteIdColumn->setDefault('');
38
+                return $schema;
39
+            }
40
+        }
41
+
42
+        return null;
43
+    }
44
+
45
+    public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
46
+        $qb = $this->connection->getQueryBuilder();
47
+        $qb->update('federated_reshares')
48
+            ->set('remote_id', $qb->createNamedParameter(''))
49
+            ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter('-1')));
50
+        $qb->executeStatement();
51
+    }
52 52
 }
Please login to merge, or discard this patch.
apps/files_external/tests/Service/StoragesServiceTestCase.php 2 patches
Indentation   +455 added lines, -455 removed lines patch added patch discarded remove patch
@@ -38,465 +38,465 @@
 block discarded – undo
38 38
 use PHPUnit\Framework\MockObject\MockObject;
39 39
 
40 40
 class CleaningDBConfig extends DBConfigService {
41
-	private array $mountIds = [];
42
-
43
-	public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type) {
44
-		$id = parent::addMount($mountPoint, $storageBackend, $authBackend, $priority, $type); // TODO: Change the autogenerated stub
45
-		$this->mountIds[] = $id;
46
-		return $id;
47
-	}
48
-
49
-	public function clean() {
50
-		foreach ($this->mountIds as $id) {
51
-			$this->removeMount($id);
52
-		}
53
-	}
41
+    private array $mountIds = [];
42
+
43
+    public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type) {
44
+        $id = parent::addMount($mountPoint, $storageBackend, $authBackend, $priority, $type); // TODO: Change the autogenerated stub
45
+        $this->mountIds[] = $id;
46
+        return $id;
47
+    }
48
+
49
+    public function clean() {
50
+        foreach ($this->mountIds as $id) {
51
+            $this->removeMount($id);
52
+        }
53
+    }
54 54
 }
55 55
 
56 56
 /**
57 57
  * @group DB
58 58
  */
59 59
 abstract class StoragesServiceTestCase extends \Test\TestCase {
60
-	protected StoragesService $service;
61
-	protected BackendService&MockObject $backendService;
62
-	protected string $dataDir;
63
-	protected CleaningDBConfig $dbConfig;
64
-	protected static array $hookCalls;
65
-	protected IUserMountCache&MockObject $mountCache;
66
-	protected IEventDispatcher&MockObject $eventDispatcher;
67
-	protected IAppConfig&MockObject $appConfig;
68
-
69
-	protected function setUp(): void {
70
-		parent::setUp();
71
-		$this->dbConfig = new CleaningDBConfig(Server::get(IDBConnection::class), Server::get(ICrypto::class));
72
-		self::$hookCalls = [];
73
-		$config = Server::get(IConfig::class);
74
-		$this->dataDir = $config->getSystemValue(
75
-			'datadirectory',
76
-			\OC::$SERVERROOT . '/data/'
77
-		);
78
-		MountConfig::$skipTest = true;
79
-
80
-		$this->mountCache = $this->createMock(IUserMountCache::class);
81
-		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
82
-		$this->appConfig = $this->createMock(IAppConfig::class);
83
-
84
-		// prepare BackendService mock
85
-		$this->backendService = $this->createMock(BackendService::class);
86
-
87
-		$authMechanisms = [
88
-			'identifier:\Auth\Mechanism' => $this->getAuthMechMock('null', '\Auth\Mechanism'),
89
-			'identifier:\Other\Auth\Mechanism' => $this->getAuthMechMock('null', '\Other\Auth\Mechanism'),
90
-			'identifier:\OCA\Files_External\Lib\Auth\NullMechanism' => $this->getAuthMechMock(),
91
-		];
92
-		$this->backendService->method('getAuthMechanism')
93
-			->willReturnCallback(function ($class) use ($authMechanisms) {
94
-				if (isset($authMechanisms[$class])) {
95
-					return $authMechanisms[$class];
96
-				}
97
-				return null;
98
-			});
99
-		$this->backendService->method('getAuthMechanismsByScheme')
100
-			->willReturnCallback(function ($schemes) use ($authMechanisms) {
101
-				return array_filter($authMechanisms, function ($authMech) use ($schemes) {
102
-					return in_array($authMech->getScheme(), $schemes, true);
103
-				});
104
-			});
105
-		$this->backendService->method('getAuthMechanisms')
106
-			->willReturn($authMechanisms);
107
-
108
-		$sftpBackend = $this->getBackendMock('\OCA\Files_External\Lib\Backend\SFTP', '\OCA\Files_External\Lib\Storage\SFTP');
109
-		$backends = [
110
-			'identifier:\OCA\Files_External\Lib\Backend\DAV' => $this->getBackendMock('\OCA\Files_External\Lib\Backend\DAV', '\OC\Files\Storage\DAV'),
111
-			'identifier:\OCA\Files_External\Lib\Backend\SMB' => $this->getBackendMock('\OCA\Files_External\Lib\Backend\SMB', '\OCA\Files_External\Lib\Storage\SMB'),
112
-			'identifier:\OCA\Files_External\Lib\Backend\SFTP' => $sftpBackend,
113
-			'identifier:sftp_alias' => $sftpBackend,
114
-		];
115
-		$backends['identifier:\OCA\Files_External\Lib\Backend\SFTP']->method('getLegacyAuthMechanism')
116
-			->willReturn($authMechanisms['identifier:\Other\Auth\Mechanism']);
117
-		$this->backendService->method('getBackend')
118
-			->willReturnCallback(function ($backendClass) use ($backends) {
119
-				if (isset($backends[$backendClass])) {
120
-					return $backends[$backendClass];
121
-				}
122
-				return null;
123
-			});
124
-		$this->backendService->method('getBackends')
125
-			->willReturn($backends);
126
-		$this->overwriteService(BackendService::class, $this->backendService);
127
-
128
-		Util::connectHook(
129
-			Filesystem::CLASSNAME,
130
-			Filesystem::signal_create_mount,
131
-			get_class($this), 'createHookCallback');
132
-		Util::connectHook(
133
-			Filesystem::CLASSNAME,
134
-			Filesystem::signal_delete_mount,
135
-			get_class($this), 'deleteHookCallback');
136
-
137
-		$containerMock = $this->createMock(IAppContainer::class);
138
-		$containerMock->method('query')
139
-			->willReturnCallback(function ($name) {
140
-				if ($name === 'OCA\Files_External\Service\BackendService') {
141
-					return $this->backendService;
142
-				}
143
-			});
144
-	}
145
-
146
-	protected function tearDown(): void {
147
-		MountConfig::$skipTest = false;
148
-		self::$hookCalls = [];
149
-		if ($this->dbConfig) {
150
-			$this->dbConfig->clean();
151
-		}
152
-		parent::tearDown();
153
-	}
154
-
155
-	protected function getBackendMock($class = SMB::class, $storageClass = \OCA\Files_External\Lib\Storage\SMB::class) {
156
-		$backend = $this->createMock(Backend::class);
157
-		$backend->method('getStorageClass')
158
-			->willReturn($storageClass);
159
-		$backend->method('getIdentifier')
160
-			->willReturn('identifier:' . $class);
161
-		return $backend;
162
-	}
163
-
164
-	protected function getAuthMechMock($scheme = 'null', $class = NullMechanism::class) {
165
-		$authMech = $this->createMock(AuthMechanism::class);
166
-		$authMech->method('getScheme')
167
-			->willReturn($scheme);
168
-		$authMech->method('getIdentifier')
169
-			->willReturn('identifier:' . $class);
170
-
171
-		return $authMech;
172
-	}
173
-
174
-	/**
175
-	 * Creates a StorageConfig instance based on array data
176
-	 */
177
-	protected function makeStorageConfig(array $data): StorageConfig {
178
-		$storage = new StorageConfig();
179
-		if (isset($data['id'])) {
180
-			$storage->setId($data['id']);
181
-		}
182
-		$storage->setMountPoint($data['mountPoint']);
183
-		if (!isset($data['backend'])) {
184
-			// data providers are run before $this->backendService is initialised
185
-			// so $data['backend'] can be specified directly
186
-			$data['backend'] = $this->backendService->getBackend($data['backendIdentifier']);
187
-		}
188
-		if (!isset($data['backend'])) {
189
-			throw new \Exception('oops, no backend');
190
-		}
191
-		if (!isset($data['authMechanism'])) {
192
-			$data['authMechanism'] = $this->backendService->getAuthMechanism($data['authMechanismIdentifier']);
193
-		}
194
-		if (!isset($data['authMechanism'])) {
195
-			throw new \Exception('oops, no auth mechanism');
196
-		}
197
-		$storage->setBackend($data['backend']);
198
-		$storage->setAuthMechanism($data['authMechanism']);
199
-		$storage->setBackendOptions($data['backendOptions']);
200
-		if (isset($data['applicableUsers'])) {
201
-			$storage->setApplicableUsers($data['applicableUsers']);
202
-		}
203
-		if (isset($data['applicableGroups'])) {
204
-			$storage->setApplicableGroups($data['applicableGroups']);
205
-		}
206
-		if (isset($data['priority'])) {
207
-			$storage->setPriority($data['priority']);
208
-		}
209
-		if (isset($data['mountOptions'])) {
210
-			$storage->setMountOptions($data['mountOptions']);
211
-		}
212
-		return $storage;
213
-	}
214
-
215
-
216
-	protected function ActualNonExistingStorageTest() {
217
-		$backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\SMB');
218
-		$authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
219
-		$storage = new StorageConfig(255);
220
-		$storage->setMountPoint('mountpoint');
221
-		$storage->setBackend($backend);
222
-		$storage->setAuthMechanism($authMechanism);
223
-		$this->service->updateStorage($storage);
224
-	}
225
-
226
-	public function testNonExistingStorage(): void {
227
-		$this->expectException(NotFoundException::class);
228
-
229
-		$this->ActualNonExistingStorageTest();
230
-	}
231
-
232
-	public static function deleteStorageDataProvider(): array {
233
-		return [
234
-			// regular case, can properly delete the oc_storages entry
235
-			[
236
-				[
237
-					'host' => 'example.com',
238
-					'user' => 'test',
239
-					'password' => 'testPassword',
240
-					'root' => 'someroot',
241
-				],
242
-				'webdav::[email protected]//someroot/'
243
-			],
244
-			[
245
-				[
246
-					'host' => 'example.com',
247
-					'user' => '$user',
248
-					'password' => 'testPassword',
249
-					'root' => 'someroot',
250
-				],
251
-				'webdav::[email protected]//someroot/'
252
-			],
253
-		];
254
-	}
255
-
256
-	#[\PHPUnit\Framework\Attributes\DataProvider('deleteStorageDataProvider')]
257
-	public function testDeleteStorage(array $backendOptions, string $rustyStorageId): void {
258
-		$backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\DAV');
259
-		$authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
260
-		$storage = new StorageConfig(255);
261
-		$storage->setMountPoint('mountpoint');
262
-		$storage->setBackend($backend);
263
-		$storage->setAuthMechanism($authMechanism);
264
-		$storage->setBackendOptions($backendOptions);
265
-
266
-		$newStorage = $this->service->addStorage($storage);
267
-		$id = $newStorage->getId();
268
-
269
-		// manually trigger storage entry because normally it happens on first
270
-		// access, which isn't possible within this test
271
-		$storageCache = new Storage($rustyStorageId, true, Server::get(IDBConnection::class));
272
-
273
-		/** @var IUserMountCache $mountCache */
274
-		$mountCache = Server::get(IUserMountCache::class);
275
-		$mountCache->clear();
276
-		$user = $this->createMock(IUser::class);
277
-		$user->method('getUID')->willReturn('test');
278
-		$cache = $this->createMock(ICache::class);
279
-		$storage = $this->createMock(IStorage::class);
280
-		$storage->method('getCache')->willReturn($cache);
281
-		$mount = $this->createMock(IMountPoint::class);
282
-		$mount->method('getStorage')
283
-			->willReturn($storage);
284
-		$mount->method('getStorageId')
285
-			->willReturn($rustyStorageId);
286
-		$mount->method('getNumericStorageId')
287
-			->willReturn($storageCache->getNumericId());
288
-		$mount->method('getStorageRootId')
289
-			->willReturn(1);
290
-		$mount->method('getMountPoint')
291
-			->willReturn('dummy');
292
-		$mount->method('getMountId')
293
-			->willReturn($id);
294
-		$mountCache->registerMounts($user, [
295
-			$mount
296
-		]);
297
-
298
-		// get numeric id for later check
299
-		$numericId = $storageCache->getNumericId();
300
-
301
-		$this->service->removeStorage($id);
302
-
303
-		$caught = false;
304
-		try {
305
-			$this->service->getStorage(1);
306
-		} catch (NotFoundException $e) {
307
-			$caught = true;
308
-		}
309
-
310
-		$this->assertTrue($caught);
311
-
312
-		// storage id was removed from oc_storages
313
-		$qb = Server::get(IDBConnection::class)->getQueryBuilder();
314
-		$storageCheckQuery = $qb->select('*')
315
-			->from('storages')
316
-			->where($qb->expr()->eq('numeric_id', $qb->expr()->literal($numericId)));
317
-
318
-		$result = $storageCheckQuery->executeQuery();
319
-		$storages = $result->fetchAll();
320
-		$result->closeCursor();
321
-		$this->assertCount(0, $storages, 'expected 0 storages, got ' . json_encode($storages));
322
-	}
323
-
324
-	protected function actualDeletedUnexistingStorageTest() {
325
-		$this->service->removeStorage(255);
326
-	}
327
-
328
-	public function testDeleteUnexistingStorage(): void {
329
-		$this->expectException(NotFoundException::class);
330
-
331
-		$this->actualDeletedUnexistingStorageTest();
332
-	}
333
-
334
-	public function testCreateStorage(): void {
335
-		$mountPoint = 'mount';
336
-		$backendIdentifier = 'identifier:\OCA\Files_External\Lib\Backend\SMB';
337
-		$authMechanismIdentifier = 'identifier:\Auth\Mechanism';
338
-		$backendOptions = ['param' => 'foo', 'param2' => 'bar'];
339
-		$mountOptions = ['option' => 'foobar'];
340
-		$applicableUsers = ['user1', 'user2'];
341
-		$applicableGroups = ['group'];
342
-		$priority = 123;
343
-
344
-		$backend = $this->backendService->getBackend($backendIdentifier);
345
-		$authMechanism = $this->backendService->getAuthMechanism($authMechanismIdentifier);
346
-
347
-		$storage = $this->service->createStorage(
348
-			$mountPoint,
349
-			$backendIdentifier,
350
-			$authMechanismIdentifier,
351
-			$backendOptions,
352
-			$mountOptions,
353
-			$applicableUsers,
354
-			$applicableGroups,
355
-			$priority
356
-		);
357
-
358
-		$this->assertEquals('/' . $mountPoint, $storage->getMountPoint());
359
-		$this->assertEquals($backend, $storage->getBackend());
360
-		$this->assertEquals($authMechanism, $storage->getAuthMechanism());
361
-		$this->assertEquals($backendOptions, $storage->getBackendOptions());
362
-		$this->assertEquals($mountOptions, $storage->getMountOptions());
363
-		$this->assertEquals($applicableUsers, $storage->getApplicableUsers());
364
-		$this->assertEquals($applicableGroups, $storage->getApplicableGroups());
365
-		$this->assertEquals($priority, $storage->getPriority());
366
-	}
367
-
368
-	public function testCreateStorageInvalidClass(): void {
369
-		$storage = $this->service->createStorage(
370
-			'mount',
371
-			'identifier:\OC\Not\A\Backend',
372
-			'identifier:\Auth\Mechanism',
373
-			[]
374
-		);
375
-		$this->assertInstanceOf(InvalidBackend::class, $storage->getBackend());
376
-	}
377
-
378
-	public function testCreateStorageInvalidAuthMechanismClass(): void {
379
-		$storage = $this->service->createStorage(
380
-			'mount',
381
-			'identifier:\OCA\Files_External\Lib\Backend\SMB',
382
-			'identifier:\Not\An\Auth\Mechanism',
383
-			[]
384
-		);
385
-		$this->assertInstanceOf(InvalidAuth::class, $storage->getAuthMechanism());
386
-	}
387
-
388
-	public function testGetStoragesBackendNotVisible(): void {
389
-		$backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\SMB');
390
-		$backend->expects($this->once())
391
-			->method('isVisibleFor')
392
-			->with($this->service->getVisibilityType())
393
-			->willReturn(false);
394
-		$authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
395
-		$authMechanism->method('isVisibleFor')
396
-			->with($this->service->getVisibilityType())
397
-			->willReturn(true);
398
-
399
-		$storage = new StorageConfig(255);
400
-		$storage->setMountPoint('mountpoint');
401
-		$storage->setBackend($backend);
402
-		$storage->setAuthMechanism($authMechanism);
403
-		$storage->setBackendOptions(['password' => 'testPassword']);
404
-
405
-		$newStorage = $this->service->addStorage($storage);
406
-
407
-		$this->assertCount(1, $this->service->getAllStorages());
408
-		$this->assertEmpty($this->service->getStorages());
409
-	}
410
-
411
-	public function testGetStoragesAuthMechanismNotVisible(): void {
412
-		$backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\SMB');
413
-		$backend->method('isVisibleFor')
414
-			->with($this->service->getVisibilityType())
415
-			->willReturn(true);
416
-		$authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
417
-		$authMechanism->expects($this->once())
418
-			->method('isVisibleFor')
419
-			->with($this->service->getVisibilityType())
420
-			->willReturn(false);
421
-
422
-		$storage = new StorageConfig(255);
423
-		$storage->setMountPoint('mountpoint');
424
-		$storage->setBackend($backend);
425
-		$storage->setAuthMechanism($authMechanism);
426
-		$storage->setBackendOptions(['password' => 'testPassword']);
427
-
428
-		$newStorage = $this->service->addStorage($storage);
429
-
430
-		$this->assertCount(1, $this->service->getAllStorages());
431
-		$this->assertEmpty($this->service->getStorages());
432
-	}
433
-
434
-	public static function createHookCallback($params): void {
435
-		self::$hookCalls[] = [
436
-			'signal' => Filesystem::signal_create_mount,
437
-			'params' => $params
438
-		];
439
-	}
440
-
441
-	public static function deleteHookCallback($params): void {
442
-		self::$hookCalls[] = [
443
-			'signal' => Filesystem::signal_delete_mount,
444
-			'params' => $params
445
-		];
446
-	}
447
-
448
-	/**
449
-	 * Asserts hook call
450
-	 *
451
-	 * @param array $callData hook call data to check
452
-	 * @param string $signal signal name
453
-	 * @param string $mountPath mount path
454
-	 * @param string $mountType mount type
455
-	 * @param string $applicable applicable users
456
-	 */
457
-	protected function assertHookCall($callData, $signal, $mountPath, $mountType, $applicable) {
458
-		$this->assertEquals($signal, $callData['signal']);
459
-		$params = $callData['params'];
460
-		$this->assertEquals(
461
-			$mountPath,
462
-			$params[Filesystem::signal_param_path]
463
-		);
464
-		$this->assertEquals(
465
-			$mountType,
466
-			$params[Filesystem::signal_param_mount_type]
467
-		);
468
-		$this->assertEquals(
469
-			$applicable,
470
-			$params[Filesystem::signal_param_users]
471
-		);
472
-	}
473
-
474
-	public function testUpdateStorageMountPoint(): void {
475
-		$backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\SMB');
476
-		$authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
477
-
478
-		$storage = new StorageConfig();
479
-		$storage->setMountPoint('mountpoint');
480
-		$storage->setBackend($backend);
481
-		$storage->setAuthMechanism($authMechanism);
482
-		$storage->setBackendOptions(['password' => 'testPassword']);
483
-
484
-		$savedStorage = $this->service->addStorage($storage);
485
-
486
-		$newAuthMechanism = $this->backendService->getAuthMechanism('identifier:\Other\Auth\Mechanism');
487
-
488
-		$updatedStorage = new StorageConfig($savedStorage->getId());
489
-		$updatedStorage->setMountPoint('mountpoint2');
490
-		$updatedStorage->setBackend($backend);
491
-		$updatedStorage->setAuthMechanism($newAuthMechanism);
492
-		$updatedStorage->setBackendOptions(['password' => 'password2']);
493
-
494
-		$this->service->updateStorage($updatedStorage);
495
-
496
-		$savedStorage = $this->service->getStorage($updatedStorage->getId());
497
-
498
-		$this->assertEquals('/mountpoint2', $savedStorage->getMountPoint());
499
-		$this->assertEquals($newAuthMechanism, $savedStorage->getAuthMechanism());
500
-		$this->assertEquals('password2', $savedStorage->getBackendOption('password'));
501
-	}
60
+    protected StoragesService $service;
61
+    protected BackendService&MockObject $backendService;
62
+    protected string $dataDir;
63
+    protected CleaningDBConfig $dbConfig;
64
+    protected static array $hookCalls;
65
+    protected IUserMountCache&MockObject $mountCache;
66
+    protected IEventDispatcher&MockObject $eventDispatcher;
67
+    protected IAppConfig&MockObject $appConfig;
68
+
69
+    protected function setUp(): void {
70
+        parent::setUp();
71
+        $this->dbConfig = new CleaningDBConfig(Server::get(IDBConnection::class), Server::get(ICrypto::class));
72
+        self::$hookCalls = [];
73
+        $config = Server::get(IConfig::class);
74
+        $this->dataDir = $config->getSystemValue(
75
+            'datadirectory',
76
+            \OC::$SERVERROOT . '/data/'
77
+        );
78
+        MountConfig::$skipTest = true;
79
+
80
+        $this->mountCache = $this->createMock(IUserMountCache::class);
81
+        $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
82
+        $this->appConfig = $this->createMock(IAppConfig::class);
83
+
84
+        // prepare BackendService mock
85
+        $this->backendService = $this->createMock(BackendService::class);
86
+
87
+        $authMechanisms = [
88
+            'identifier:\Auth\Mechanism' => $this->getAuthMechMock('null', '\Auth\Mechanism'),
89
+            'identifier:\Other\Auth\Mechanism' => $this->getAuthMechMock('null', '\Other\Auth\Mechanism'),
90
+            'identifier:\OCA\Files_External\Lib\Auth\NullMechanism' => $this->getAuthMechMock(),
91
+        ];
92
+        $this->backendService->method('getAuthMechanism')
93
+            ->willReturnCallback(function ($class) use ($authMechanisms) {
94
+                if (isset($authMechanisms[$class])) {
95
+                    return $authMechanisms[$class];
96
+                }
97
+                return null;
98
+            });
99
+        $this->backendService->method('getAuthMechanismsByScheme')
100
+            ->willReturnCallback(function ($schemes) use ($authMechanisms) {
101
+                return array_filter($authMechanisms, function ($authMech) use ($schemes) {
102
+                    return in_array($authMech->getScheme(), $schemes, true);
103
+                });
104
+            });
105
+        $this->backendService->method('getAuthMechanisms')
106
+            ->willReturn($authMechanisms);
107
+
108
+        $sftpBackend = $this->getBackendMock('\OCA\Files_External\Lib\Backend\SFTP', '\OCA\Files_External\Lib\Storage\SFTP');
109
+        $backends = [
110
+            'identifier:\OCA\Files_External\Lib\Backend\DAV' => $this->getBackendMock('\OCA\Files_External\Lib\Backend\DAV', '\OC\Files\Storage\DAV'),
111
+            'identifier:\OCA\Files_External\Lib\Backend\SMB' => $this->getBackendMock('\OCA\Files_External\Lib\Backend\SMB', '\OCA\Files_External\Lib\Storage\SMB'),
112
+            'identifier:\OCA\Files_External\Lib\Backend\SFTP' => $sftpBackend,
113
+            'identifier:sftp_alias' => $sftpBackend,
114
+        ];
115
+        $backends['identifier:\OCA\Files_External\Lib\Backend\SFTP']->method('getLegacyAuthMechanism')
116
+            ->willReturn($authMechanisms['identifier:\Other\Auth\Mechanism']);
117
+        $this->backendService->method('getBackend')
118
+            ->willReturnCallback(function ($backendClass) use ($backends) {
119
+                if (isset($backends[$backendClass])) {
120
+                    return $backends[$backendClass];
121
+                }
122
+                return null;
123
+            });
124
+        $this->backendService->method('getBackends')
125
+            ->willReturn($backends);
126
+        $this->overwriteService(BackendService::class, $this->backendService);
127
+
128
+        Util::connectHook(
129
+            Filesystem::CLASSNAME,
130
+            Filesystem::signal_create_mount,
131
+            get_class($this), 'createHookCallback');
132
+        Util::connectHook(
133
+            Filesystem::CLASSNAME,
134
+            Filesystem::signal_delete_mount,
135
+            get_class($this), 'deleteHookCallback');
136
+
137
+        $containerMock = $this->createMock(IAppContainer::class);
138
+        $containerMock->method('query')
139
+            ->willReturnCallback(function ($name) {
140
+                if ($name === 'OCA\Files_External\Service\BackendService') {
141
+                    return $this->backendService;
142
+                }
143
+            });
144
+    }
145
+
146
+    protected function tearDown(): void {
147
+        MountConfig::$skipTest = false;
148
+        self::$hookCalls = [];
149
+        if ($this->dbConfig) {
150
+            $this->dbConfig->clean();
151
+        }
152
+        parent::tearDown();
153
+    }
154
+
155
+    protected function getBackendMock($class = SMB::class, $storageClass = \OCA\Files_External\Lib\Storage\SMB::class) {
156
+        $backend = $this->createMock(Backend::class);
157
+        $backend->method('getStorageClass')
158
+            ->willReturn($storageClass);
159
+        $backend->method('getIdentifier')
160
+            ->willReturn('identifier:' . $class);
161
+        return $backend;
162
+    }
163
+
164
+    protected function getAuthMechMock($scheme = 'null', $class = NullMechanism::class) {
165
+        $authMech = $this->createMock(AuthMechanism::class);
166
+        $authMech->method('getScheme')
167
+            ->willReturn($scheme);
168
+        $authMech->method('getIdentifier')
169
+            ->willReturn('identifier:' . $class);
170
+
171
+        return $authMech;
172
+    }
173
+
174
+    /**
175
+     * Creates a StorageConfig instance based on array data
176
+     */
177
+    protected function makeStorageConfig(array $data): StorageConfig {
178
+        $storage = new StorageConfig();
179
+        if (isset($data['id'])) {
180
+            $storage->setId($data['id']);
181
+        }
182
+        $storage->setMountPoint($data['mountPoint']);
183
+        if (!isset($data['backend'])) {
184
+            // data providers are run before $this->backendService is initialised
185
+            // so $data['backend'] can be specified directly
186
+            $data['backend'] = $this->backendService->getBackend($data['backendIdentifier']);
187
+        }
188
+        if (!isset($data['backend'])) {
189
+            throw new \Exception('oops, no backend');
190
+        }
191
+        if (!isset($data['authMechanism'])) {
192
+            $data['authMechanism'] = $this->backendService->getAuthMechanism($data['authMechanismIdentifier']);
193
+        }
194
+        if (!isset($data['authMechanism'])) {
195
+            throw new \Exception('oops, no auth mechanism');
196
+        }
197
+        $storage->setBackend($data['backend']);
198
+        $storage->setAuthMechanism($data['authMechanism']);
199
+        $storage->setBackendOptions($data['backendOptions']);
200
+        if (isset($data['applicableUsers'])) {
201
+            $storage->setApplicableUsers($data['applicableUsers']);
202
+        }
203
+        if (isset($data['applicableGroups'])) {
204
+            $storage->setApplicableGroups($data['applicableGroups']);
205
+        }
206
+        if (isset($data['priority'])) {
207
+            $storage->setPriority($data['priority']);
208
+        }
209
+        if (isset($data['mountOptions'])) {
210
+            $storage->setMountOptions($data['mountOptions']);
211
+        }
212
+        return $storage;
213
+    }
214
+
215
+
216
+    protected function ActualNonExistingStorageTest() {
217
+        $backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\SMB');
218
+        $authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
219
+        $storage = new StorageConfig(255);
220
+        $storage->setMountPoint('mountpoint');
221
+        $storage->setBackend($backend);
222
+        $storage->setAuthMechanism($authMechanism);
223
+        $this->service->updateStorage($storage);
224
+    }
225
+
226
+    public function testNonExistingStorage(): void {
227
+        $this->expectException(NotFoundException::class);
228
+
229
+        $this->ActualNonExistingStorageTest();
230
+    }
231
+
232
+    public static function deleteStorageDataProvider(): array {
233
+        return [
234
+            // regular case, can properly delete the oc_storages entry
235
+            [
236
+                [
237
+                    'host' => 'example.com',
238
+                    'user' => 'test',
239
+                    'password' => 'testPassword',
240
+                    'root' => 'someroot',
241
+                ],
242
+                'webdav::[email protected]//someroot/'
243
+            ],
244
+            [
245
+                [
246
+                    'host' => 'example.com',
247
+                    'user' => '$user',
248
+                    'password' => 'testPassword',
249
+                    'root' => 'someroot',
250
+                ],
251
+                'webdav::[email protected]//someroot/'
252
+            ],
253
+        ];
254
+    }
255
+
256
+    #[\PHPUnit\Framework\Attributes\DataProvider('deleteStorageDataProvider')]
257
+    public function testDeleteStorage(array $backendOptions, string $rustyStorageId): void {
258
+        $backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\DAV');
259
+        $authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
260
+        $storage = new StorageConfig(255);
261
+        $storage->setMountPoint('mountpoint');
262
+        $storage->setBackend($backend);
263
+        $storage->setAuthMechanism($authMechanism);
264
+        $storage->setBackendOptions($backendOptions);
265
+
266
+        $newStorage = $this->service->addStorage($storage);
267
+        $id = $newStorage->getId();
268
+
269
+        // manually trigger storage entry because normally it happens on first
270
+        // access, which isn't possible within this test
271
+        $storageCache = new Storage($rustyStorageId, true, Server::get(IDBConnection::class));
272
+
273
+        /** @var IUserMountCache $mountCache */
274
+        $mountCache = Server::get(IUserMountCache::class);
275
+        $mountCache->clear();
276
+        $user = $this->createMock(IUser::class);
277
+        $user->method('getUID')->willReturn('test');
278
+        $cache = $this->createMock(ICache::class);
279
+        $storage = $this->createMock(IStorage::class);
280
+        $storage->method('getCache')->willReturn($cache);
281
+        $mount = $this->createMock(IMountPoint::class);
282
+        $mount->method('getStorage')
283
+            ->willReturn($storage);
284
+        $mount->method('getStorageId')
285
+            ->willReturn($rustyStorageId);
286
+        $mount->method('getNumericStorageId')
287
+            ->willReturn($storageCache->getNumericId());
288
+        $mount->method('getStorageRootId')
289
+            ->willReturn(1);
290
+        $mount->method('getMountPoint')
291
+            ->willReturn('dummy');
292
+        $mount->method('getMountId')
293
+            ->willReturn($id);
294
+        $mountCache->registerMounts($user, [
295
+            $mount
296
+        ]);
297
+
298
+        // get numeric id for later check
299
+        $numericId = $storageCache->getNumericId();
300
+
301
+        $this->service->removeStorage($id);
302
+
303
+        $caught = false;
304
+        try {
305
+            $this->service->getStorage(1);
306
+        } catch (NotFoundException $e) {
307
+            $caught = true;
308
+        }
309
+
310
+        $this->assertTrue($caught);
311
+
312
+        // storage id was removed from oc_storages
313
+        $qb = Server::get(IDBConnection::class)->getQueryBuilder();
314
+        $storageCheckQuery = $qb->select('*')
315
+            ->from('storages')
316
+            ->where($qb->expr()->eq('numeric_id', $qb->expr()->literal($numericId)));
317
+
318
+        $result = $storageCheckQuery->executeQuery();
319
+        $storages = $result->fetchAll();
320
+        $result->closeCursor();
321
+        $this->assertCount(0, $storages, 'expected 0 storages, got ' . json_encode($storages));
322
+    }
323
+
324
+    protected function actualDeletedUnexistingStorageTest() {
325
+        $this->service->removeStorage(255);
326
+    }
327
+
328
+    public function testDeleteUnexistingStorage(): void {
329
+        $this->expectException(NotFoundException::class);
330
+
331
+        $this->actualDeletedUnexistingStorageTest();
332
+    }
333
+
334
+    public function testCreateStorage(): void {
335
+        $mountPoint = 'mount';
336
+        $backendIdentifier = 'identifier:\OCA\Files_External\Lib\Backend\SMB';
337
+        $authMechanismIdentifier = 'identifier:\Auth\Mechanism';
338
+        $backendOptions = ['param' => 'foo', 'param2' => 'bar'];
339
+        $mountOptions = ['option' => 'foobar'];
340
+        $applicableUsers = ['user1', 'user2'];
341
+        $applicableGroups = ['group'];
342
+        $priority = 123;
343
+
344
+        $backend = $this->backendService->getBackend($backendIdentifier);
345
+        $authMechanism = $this->backendService->getAuthMechanism($authMechanismIdentifier);
346
+
347
+        $storage = $this->service->createStorage(
348
+            $mountPoint,
349
+            $backendIdentifier,
350
+            $authMechanismIdentifier,
351
+            $backendOptions,
352
+            $mountOptions,
353
+            $applicableUsers,
354
+            $applicableGroups,
355
+            $priority
356
+        );
357
+
358
+        $this->assertEquals('/' . $mountPoint, $storage->getMountPoint());
359
+        $this->assertEquals($backend, $storage->getBackend());
360
+        $this->assertEquals($authMechanism, $storage->getAuthMechanism());
361
+        $this->assertEquals($backendOptions, $storage->getBackendOptions());
362
+        $this->assertEquals($mountOptions, $storage->getMountOptions());
363
+        $this->assertEquals($applicableUsers, $storage->getApplicableUsers());
364
+        $this->assertEquals($applicableGroups, $storage->getApplicableGroups());
365
+        $this->assertEquals($priority, $storage->getPriority());
366
+    }
367
+
368
+    public function testCreateStorageInvalidClass(): void {
369
+        $storage = $this->service->createStorage(
370
+            'mount',
371
+            'identifier:\OC\Not\A\Backend',
372
+            'identifier:\Auth\Mechanism',
373
+            []
374
+        );
375
+        $this->assertInstanceOf(InvalidBackend::class, $storage->getBackend());
376
+    }
377
+
378
+    public function testCreateStorageInvalidAuthMechanismClass(): void {
379
+        $storage = $this->service->createStorage(
380
+            'mount',
381
+            'identifier:\OCA\Files_External\Lib\Backend\SMB',
382
+            'identifier:\Not\An\Auth\Mechanism',
383
+            []
384
+        );
385
+        $this->assertInstanceOf(InvalidAuth::class, $storage->getAuthMechanism());
386
+    }
387
+
388
+    public function testGetStoragesBackendNotVisible(): void {
389
+        $backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\SMB');
390
+        $backend->expects($this->once())
391
+            ->method('isVisibleFor')
392
+            ->with($this->service->getVisibilityType())
393
+            ->willReturn(false);
394
+        $authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
395
+        $authMechanism->method('isVisibleFor')
396
+            ->with($this->service->getVisibilityType())
397
+            ->willReturn(true);
398
+
399
+        $storage = new StorageConfig(255);
400
+        $storage->setMountPoint('mountpoint');
401
+        $storage->setBackend($backend);
402
+        $storage->setAuthMechanism($authMechanism);
403
+        $storage->setBackendOptions(['password' => 'testPassword']);
404
+
405
+        $newStorage = $this->service->addStorage($storage);
406
+
407
+        $this->assertCount(1, $this->service->getAllStorages());
408
+        $this->assertEmpty($this->service->getStorages());
409
+    }
410
+
411
+    public function testGetStoragesAuthMechanismNotVisible(): void {
412
+        $backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\SMB');
413
+        $backend->method('isVisibleFor')
414
+            ->with($this->service->getVisibilityType())
415
+            ->willReturn(true);
416
+        $authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
417
+        $authMechanism->expects($this->once())
418
+            ->method('isVisibleFor')
419
+            ->with($this->service->getVisibilityType())
420
+            ->willReturn(false);
421
+
422
+        $storage = new StorageConfig(255);
423
+        $storage->setMountPoint('mountpoint');
424
+        $storage->setBackend($backend);
425
+        $storage->setAuthMechanism($authMechanism);
426
+        $storage->setBackendOptions(['password' => 'testPassword']);
427
+
428
+        $newStorage = $this->service->addStorage($storage);
429
+
430
+        $this->assertCount(1, $this->service->getAllStorages());
431
+        $this->assertEmpty($this->service->getStorages());
432
+    }
433
+
434
+    public static function createHookCallback($params): void {
435
+        self::$hookCalls[] = [
436
+            'signal' => Filesystem::signal_create_mount,
437
+            'params' => $params
438
+        ];
439
+    }
440
+
441
+    public static function deleteHookCallback($params): void {
442
+        self::$hookCalls[] = [
443
+            'signal' => Filesystem::signal_delete_mount,
444
+            'params' => $params
445
+        ];
446
+    }
447
+
448
+    /**
449
+     * Asserts hook call
450
+     *
451
+     * @param array $callData hook call data to check
452
+     * @param string $signal signal name
453
+     * @param string $mountPath mount path
454
+     * @param string $mountType mount type
455
+     * @param string $applicable applicable users
456
+     */
457
+    protected function assertHookCall($callData, $signal, $mountPath, $mountType, $applicable) {
458
+        $this->assertEquals($signal, $callData['signal']);
459
+        $params = $callData['params'];
460
+        $this->assertEquals(
461
+            $mountPath,
462
+            $params[Filesystem::signal_param_path]
463
+        );
464
+        $this->assertEquals(
465
+            $mountType,
466
+            $params[Filesystem::signal_param_mount_type]
467
+        );
468
+        $this->assertEquals(
469
+            $applicable,
470
+            $params[Filesystem::signal_param_users]
471
+        );
472
+    }
473
+
474
+    public function testUpdateStorageMountPoint(): void {
475
+        $backend = $this->backendService->getBackend('identifier:\OCA\Files_External\Lib\Backend\SMB');
476
+        $authMechanism = $this->backendService->getAuthMechanism('identifier:\Auth\Mechanism');
477
+
478
+        $storage = new StorageConfig();
479
+        $storage->setMountPoint('mountpoint');
480
+        $storage->setBackend($backend);
481
+        $storage->setAuthMechanism($authMechanism);
482
+        $storage->setBackendOptions(['password' => 'testPassword']);
483
+
484
+        $savedStorage = $this->service->addStorage($storage);
485
+
486
+        $newAuthMechanism = $this->backendService->getAuthMechanism('identifier:\Other\Auth\Mechanism');
487
+
488
+        $updatedStorage = new StorageConfig($savedStorage->getId());
489
+        $updatedStorage->setMountPoint('mountpoint2');
490
+        $updatedStorage->setBackend($backend);
491
+        $updatedStorage->setAuthMechanism($newAuthMechanism);
492
+        $updatedStorage->setBackendOptions(['password' => 'password2']);
493
+
494
+        $this->service->updateStorage($updatedStorage);
495
+
496
+        $savedStorage = $this->service->getStorage($updatedStorage->getId());
497
+
498
+        $this->assertEquals('/mountpoint2', $savedStorage->getMountPoint());
499
+        $this->assertEquals($newAuthMechanism, $savedStorage->getAuthMechanism());
500
+        $this->assertEquals('password2', $savedStorage->getBackendOption('password'));
501
+    }
502 502
 }
Please login to merge, or discard this patch.
Spacing   +10 added lines, -10 removed lines patch added patch discarded remove patch
@@ -73,7 +73,7 @@  discard block
 block discarded – undo
73 73
 		$config = Server::get(IConfig::class);
74 74
 		$this->dataDir = $config->getSystemValue(
75 75
 			'datadirectory',
76
-			\OC::$SERVERROOT . '/data/'
76
+			\OC::$SERVERROOT.'/data/'
77 77
 		);
78 78
 		MountConfig::$skipTest = true;
79 79
 
@@ -90,15 +90,15 @@  discard block
 block discarded – undo
90 90
 			'identifier:\OCA\Files_External\Lib\Auth\NullMechanism' => $this->getAuthMechMock(),
91 91
 		];
92 92
 		$this->backendService->method('getAuthMechanism')
93
-			->willReturnCallback(function ($class) use ($authMechanisms) {
93
+			->willReturnCallback(function($class) use ($authMechanisms) {
94 94
 				if (isset($authMechanisms[$class])) {
95 95
 					return $authMechanisms[$class];
96 96
 				}
97 97
 				return null;
98 98
 			});
99 99
 		$this->backendService->method('getAuthMechanismsByScheme')
100
-			->willReturnCallback(function ($schemes) use ($authMechanisms) {
101
-				return array_filter($authMechanisms, function ($authMech) use ($schemes) {
100
+			->willReturnCallback(function($schemes) use ($authMechanisms) {
101
+				return array_filter($authMechanisms, function($authMech) use ($schemes) {
102 102
 					return in_array($authMech->getScheme(), $schemes, true);
103 103
 				});
104 104
 			});
@@ -115,7 +115,7 @@  discard block
 block discarded – undo
115 115
 		$backends['identifier:\OCA\Files_External\Lib\Backend\SFTP']->method('getLegacyAuthMechanism')
116 116
 			->willReturn($authMechanisms['identifier:\Other\Auth\Mechanism']);
117 117
 		$this->backendService->method('getBackend')
118
-			->willReturnCallback(function ($backendClass) use ($backends) {
118
+			->willReturnCallback(function($backendClass) use ($backends) {
119 119
 				if (isset($backends[$backendClass])) {
120 120
 					return $backends[$backendClass];
121 121
 				}
@@ -136,7 +136,7 @@  discard block
 block discarded – undo
136 136
 
137 137
 		$containerMock = $this->createMock(IAppContainer::class);
138 138
 		$containerMock->method('query')
139
-			->willReturnCallback(function ($name) {
139
+			->willReturnCallback(function($name) {
140 140
 				if ($name === 'OCA\Files_External\Service\BackendService') {
141 141
 					return $this->backendService;
142 142
 				}
@@ -157,7 +157,7 @@  discard block
 block discarded – undo
157 157
 		$backend->method('getStorageClass')
158 158
 			->willReturn($storageClass);
159 159
 		$backend->method('getIdentifier')
160
-			->willReturn('identifier:' . $class);
160
+			->willReturn('identifier:'.$class);
161 161
 		return $backend;
162 162
 	}
163 163
 
@@ -166,7 +166,7 @@  discard block
 block discarded – undo
166 166
 		$authMech->method('getScheme')
167 167
 			->willReturn($scheme);
168 168
 		$authMech->method('getIdentifier')
169
-			->willReturn('identifier:' . $class);
169
+			->willReturn('identifier:'.$class);
170 170
 
171 171
 		return $authMech;
172 172
 	}
@@ -318,7 +318,7 @@  discard block
 block discarded – undo
318 318
 		$result = $storageCheckQuery->executeQuery();
319 319
 		$storages = $result->fetchAll();
320 320
 		$result->closeCursor();
321
-		$this->assertCount(0, $storages, 'expected 0 storages, got ' . json_encode($storages));
321
+		$this->assertCount(0, $storages, 'expected 0 storages, got '.json_encode($storages));
322 322
 	}
323 323
 
324 324
 	protected function actualDeletedUnexistingStorageTest() {
@@ -355,7 +355,7 @@  discard block
 block discarded – undo
355 355
 			$priority
356 356
 		);
357 357
 
358
-		$this->assertEquals('/' . $mountPoint, $storage->getMountPoint());
358
+		$this->assertEquals('/'.$mountPoint, $storage->getMountPoint());
359 359
 		$this->assertEquals($backend, $storage->getBackend());
360 360
 		$this->assertEquals($authMechanism, $storage->getAuthMechanism());
361 361
 		$this->assertEquals($backendOptions, $storage->getBackendOptions());
Please login to merge, or discard this patch.