Passed
Push — master ( f7c59f...a72edb )
by Morris
14:26 queued 11s
created
core/Migrations/Version14000Date20180404140050.php 1 patch
Indentation   +40 added lines, -40 removed lines patch added patch discarded remove patch
@@ -37,53 +37,53 @@
 block discarded – undo
37 37
  */
38 38
 class Version14000Date20180404140050 extends SimpleMigrationStep {
39 39
 
40
-	/** @var IDBConnection */
41
-	private $connection;
40
+    /** @var IDBConnection */
41
+    private $connection;
42 42
 
43
-	public function __construct(IDBConnection $connection) {
44
-		$this->connection = $connection;
45
-	}
43
+    public function __construct(IDBConnection $connection) {
44
+        $this->connection = $connection;
45
+    }
46 46
 
47
-	public function name(): string {
48
-		return 'Add lowercase user id column to users table';
49
-	}
47
+    public function name(): string {
48
+        return 'Add lowercase user id column to users table';
49
+    }
50 50
 
51
-	public function description(): string {
52
-		return 'Adds "uid_lower" column to the users table and fills the column to allow indexed case-insensitive searches';
53
-	}
51
+    public function description(): string {
52
+        return 'Adds "uid_lower" column to the users table and fills the column to allow indexed case-insensitive searches';
53
+    }
54 54
 
55
-	/**
56
-	 * @param IOutput $output
57
-	 * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
58
-	 * @param array $options
59
-	 * @return null|ISchemaWrapper
60
-	 */
61
-	public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
62
-		/** @var ISchemaWrapper $schema */
63
-		$schema = $schemaClosure();
55
+    /**
56
+     * @param IOutput $output
57
+     * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
58
+     * @param array $options
59
+     * @return null|ISchemaWrapper
60
+     */
61
+    public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
62
+        /** @var ISchemaWrapper $schema */
63
+        $schema = $schemaClosure();
64 64
 
65
-		$table = $schema->getTable('users');
65
+        $table = $schema->getTable('users');
66 66
 
67
-		$table->addColumn('uid_lower', 'string', [
68
-			'notnull' => false,
69
-			'length' => 64,
70
-			'default' => '',
71
-		]);
72
-		$table->addIndex(['uid_lower'], 'user_uid_lower');
67
+        $table->addColumn('uid_lower', 'string', [
68
+            'notnull' => false,
69
+            'length' => 64,
70
+            'default' => '',
71
+        ]);
72
+        $table->addIndex(['uid_lower'], 'user_uid_lower');
73 73
 
74
-		return $schema;
75
-	}
74
+        return $schema;
75
+    }
76 76
 
77
-	/**
78
-	 * @param IOutput $output
79
-	 * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
80
-	 * @param array $options
81
-	 */
82
-	public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
83
-		$qb = $this->connection->getQueryBuilder();
77
+    /**
78
+     * @param IOutput $output
79
+     * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
80
+     * @param array $options
81
+     */
82
+    public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
83
+        $qb = $this->connection->getQueryBuilder();
84 84
 
85
-		$qb->update('users')
86
-			->set('uid_lower', $qb->func()->lower('uid'));
87
-		$qb->execute();
88
-	}
85
+        $qb->update('users')
86
+            ->set('uid_lower', $qb->func()->lower('uid'));
87
+        $qb->execute();
88
+    }
89 89
 }
Please login to merge, or discard this patch.
core/Command/Db/ConvertType.php 2 patches
Indentation   +410 added lines, -410 removed lines patch added patch discarded remove patch
@@ -54,414 +54,414 @@
 block discarded – undo
54 54
 use Symfony\Component\Console\Question\Question;
55 55
 
56 56
 class ConvertType extends Command implements CompletionAwareInterface {
57
-	/**
58
-	 * @var \OCP\IConfig
59
-	 */
60
-	protected $config;
61
-
62
-	/**
63
-	 * @var \OC\DB\ConnectionFactory
64
-	 */
65
-	protected $connectionFactory;
66
-
67
-	/** @var array */
68
-	protected $columnTypes;
69
-
70
-	/**
71
-	 * @param \OCP\IConfig $config
72
-	 * @param \OC\DB\ConnectionFactory $connectionFactory
73
-	 */
74
-	public function __construct(IConfig $config, ConnectionFactory $connectionFactory) {
75
-		$this->config = $config;
76
-		$this->connectionFactory = $connectionFactory;
77
-		parent::__construct();
78
-	}
79
-
80
-	protected function configure() {
81
-		$this
82
-			->setName('db:convert-type')
83
-			->setDescription('Convert the Nextcloud database to the newly configured one')
84
-			->addArgument(
85
-				'type',
86
-				InputArgument::REQUIRED,
87
-				'the type of the database to convert to'
88
-			)
89
-			->addArgument(
90
-				'username',
91
-				InputArgument::REQUIRED,
92
-				'the username of the database to convert to'
93
-			)
94
-			->addArgument(
95
-				'hostname',
96
-				InputArgument::REQUIRED,
97
-				'the hostname of the database to convert to'
98
-			)
99
-			->addArgument(
100
-				'database',
101
-				InputArgument::REQUIRED,
102
-				'the name of the database to convert to'
103
-			)
104
-			->addOption(
105
-				'port',
106
-				null,
107
-				InputOption::VALUE_REQUIRED,
108
-				'the port of the database to convert to'
109
-			)
110
-			->addOption(
111
-				'password',
112
-				null,
113
-				InputOption::VALUE_REQUIRED,
114
-				'the password of the database to convert to. Will be asked when not specified. Can also be passed via stdin.'
115
-			)
116
-			->addOption(
117
-				'clear-schema',
118
-				null,
119
-				InputOption::VALUE_NONE,
120
-				'remove all tables from the destination database'
121
-			)
122
-			->addOption(
123
-				'all-apps',
124
-				null,
125
-				InputOption::VALUE_NONE,
126
-				'whether to create schema for all apps instead of only installed apps'
127
-			)
128
-			->addOption(
129
-				'chunk-size',
130
-				null,
131
-				InputOption::VALUE_REQUIRED,
132
-				'the maximum number of database rows to handle in a single query, bigger tables will be handled in chunks of this size. Lower this if the process runs out of memory during conversion.',
133
-				1000
134
-			)
135
-		;
136
-	}
137
-
138
-	protected function validateInput(InputInterface $input, OutputInterface $output) {
139
-		$type = $this->connectionFactory->normalizeType($input->getArgument('type'));
140
-		if ($type === 'sqlite3') {
141
-			throw new \InvalidArgumentException(
142
-				'Converting to SQLite (sqlite3) is currently not supported.'
143
-			);
144
-		}
145
-		if ($type === $this->config->getSystemValue('dbtype', '')) {
146
-			throw new \InvalidArgumentException(sprintf(
147
-				'Can not convert from %1$s to %1$s.',
148
-				$type
149
-			));
150
-		}
151
-		if ($type === 'oci' && $input->getOption('clear-schema')) {
152
-			// Doctrine unconditionally tries (at least in version 2.3)
153
-			// to drop sequence triggers when dropping a table, even though
154
-			// such triggers may not exist. This results in errors like
155
-			// "ORA-04080: trigger 'OC_STORAGES_AI_PK' does not exist".
156
-			throw new \InvalidArgumentException(
157
-				'The --clear-schema option is not supported when converting to Oracle (oci).'
158
-			);
159
-		}
160
-	}
161
-
162
-	protected function readPassword(InputInterface $input, OutputInterface $output) {
163
-		// Explicitly specified password
164
-		if ($input->getOption('password')) {
165
-			return;
166
-		}
167
-
168
-		// Read from stdin. stream_set_blocking is used to prevent blocking
169
-		// when nothing is passed via stdin.
170
-		stream_set_blocking(STDIN, 0);
171
-		$password = file_get_contents('php://stdin');
172
-		stream_set_blocking(STDIN, 1);
173
-		if (trim($password) !== '') {
174
-			$input->setOption('password', $password);
175
-			return;
176
-		}
177
-
178
-		// Read password by interacting
179
-		if ($input->isInteractive()) {
180
-			/** @var QuestionHelper $helper */
181
-			$helper = $this->getHelper('question');
182
-			$question = new Question('What is the database password?');
183
-			$question->setHidden(true);
184
-			$question->setHiddenFallback(false);
185
-			$password = $helper->ask($input, $output, $question);
186
-			$input->setOption('password', $password);
187
-			return;
188
-		}
189
-	}
190
-
191
-	protected function execute(InputInterface $input, OutputInterface $output): int {
192
-		$this->validateInput($input, $output);
193
-		$this->readPassword($input, $output);
194
-
195
-		$fromDB = \OC::$server->getDatabaseConnection();
196
-		$toDB = $this->getToDBConnection($input, $output);
197
-
198
-		if ($input->getOption('clear-schema')) {
199
-			$this->clearSchema($toDB, $input, $output);
200
-		}
201
-
202
-		$this->createSchema($fromDB, $toDB, $input, $output);
203
-
204
-		$toTables = $this->getTables($toDB);
205
-		$fromTables = $this->getTables($fromDB);
206
-
207
-		// warn/fail if there are more tables in 'from' database
208
-		$extraFromTables = array_diff($fromTables, $toTables);
209
-		if (!empty($extraFromTables)) {
210
-			$output->writeln('<comment>The following tables will not be converted:</comment>');
211
-			$output->writeln($extraFromTables);
212
-			if (!$input->getOption('all-apps')) {
213
-				$output->writeln('<comment>Please note that tables belonging to available but currently not installed apps</comment>');
214
-				$output->writeln('<comment>can be included by specifying the --all-apps option.</comment>');
215
-			}
216
-
217
-			$continueConversion = !$input->isInteractive(); // assume yes for --no-interaction and no otherwise.
218
-			$question = new ConfirmationQuestion('Continue with the conversion (y/n)? [n] ', $continueConversion);
219
-
220
-			/** @var QuestionHelper $helper */
221
-			$helper = $this->getHelper('question');
222
-
223
-			if (!$helper->ask($input, $output, $question)) {
224
-				return 1;
225
-			}
226
-		}
227
-		$intersectingTables = array_intersect($toTables, $fromTables);
228
-		$this->convertDB($fromDB, $toDB, $intersectingTables, $input, $output);
229
-		return 0;
230
-	}
231
-
232
-	protected function createSchema(Connection $fromDB, Connection $toDB, InputInterface $input, OutputInterface $output) {
233
-		$output->writeln('<info>Creating schema in new database</info>');
234
-
235
-		$fromMS = new MigrationService('core', $fromDB);
236
-		$currentMigration = $fromMS->getMigration('current');
237
-		if ($currentMigration !== '0') {
238
-			$toMS = new MigrationService('core', $toDB);
239
-			$toMS->migrate($currentMigration);
240
-		}
241
-
242
-		$schemaManager = new \OC\DB\MDB2SchemaManager($toDB);
243
-		$apps = $input->getOption('all-apps') ? \OC_App::getAllApps() : \OC_App::getEnabledApps();
244
-		foreach ($apps as $app) {
245
-			if (file_exists(\OC_App::getAppPath($app).'/appinfo/database.xml')) {
246
-				$schemaManager->createDbFromStructure(\OC_App::getAppPath($app).'/appinfo/database.xml');
247
-			} else {
248
-				// Make sure autoloading works...
249
-				\OC_App::loadApp($app);
250
-				$fromMS = new MigrationService($app, $fromDB);
251
-				$currentMigration = $fromMS->getMigration('current');
252
-				if ($currentMigration !== '0') {
253
-					$toMS = new MigrationService($app, $toDB);
254
-					$toMS->migrate($currentMigration, true);
255
-				}
256
-			}
257
-		}
258
-	}
259
-
260
-	protected function getToDBConnection(InputInterface $input, OutputInterface $output) {
261
-		$type = $input->getArgument('type');
262
-		$connectionParams = $this->connectionFactory->createConnectionParams();
263
-		$connectionParams = array_merge($connectionParams, [
264
-			'host' => $input->getArgument('hostname'),
265
-			'user' => $input->getArgument('username'),
266
-			'password' => $input->getOption('password'),
267
-			'dbname' => $input->getArgument('database'),
268
-		]);
269
-		if ($input->getOption('port')) {
270
-			$connectionParams['port'] = $input->getOption('port');
271
-		}
272
-		return $this->connectionFactory->getConnection($type, $connectionParams);
273
-	}
274
-
275
-	protected function clearSchema(Connection $db, InputInterface $input, OutputInterface $output) {
276
-		$toTables = $this->getTables($db);
277
-		if (!empty($toTables)) {
278
-			$output->writeln('<info>Clearing schema in new database</info>');
279
-		}
280
-		foreach ($toTables as $table) {
281
-			$db->getSchemaManager()->dropTable($table);
282
-		}
283
-	}
284
-
285
-	protected function getTables(Connection $db) {
286
-		$filterExpression = '/^' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/';
287
-		$db->getConfiguration()->
288
-			setFilterSchemaAssetsExpression($filterExpression);
289
-		return $db->getSchemaManager()->listTableNames();
290
-	}
291
-
292
-	/**
293
-	 * @param Connection $fromDB
294
-	 * @param Connection $toDB
295
-	 * @param Table $table
296
-	 * @param InputInterface $input
297
-	 * @param OutputInterface $output
298
-	 */
299
-	protected function copyTable(Connection $fromDB, Connection $toDB, Table $table, InputInterface $input, OutputInterface $output) {
300
-		if ($table->getName() === $toDB->getPrefix() . 'migrations') {
301
-			$output->writeln('<comment>Skipping migrations table because it was already filled by running the migrations</comment>');
302
-			return;
303
-		}
304
-
305
-		$chunkSize = $input->getOption('chunk-size');
306
-
307
-		$query = $fromDB->getQueryBuilder();
308
-		$query->automaticTablePrefix(false);
309
-		$query->select($query->func()->count('*', 'num_entries'))
310
-			->from($table->getName());
311
-		$result = $query->execute();
312
-		$count = $result->fetchColumn();
313
-		$result->closeCursor();
314
-
315
-		$numChunks = ceil($count/$chunkSize);
316
-		if ($numChunks > 1) {
317
-			$output->writeln('chunked query, ' . $numChunks . ' chunks');
318
-		}
319
-
320
-		$progress = new ProgressBar($output, $count);
321
-		$progress->start();
322
-		$redraw = $count > $chunkSize ? 100 : ($count > 100 ? 5 : 1);
323
-		$progress->setRedrawFrequency($redraw);
324
-
325
-		$query = $fromDB->getQueryBuilder();
326
-		$query->automaticTablePrefix(false);
327
-		$query->select('*')
328
-			->from($table->getName())
329
-			->setMaxResults($chunkSize);
330
-
331
-		try {
332
-			$orderColumns = $table->getPrimaryKeyColumns();
333
-		} catch (DBALException $e) {
334
-			$orderColumns = [];
335
-			foreach ($table->getColumns() as $column) {
336
-				$orderColumns[] = $column->getName();
337
-			}
338
-		}
339
-
340
-		foreach ($orderColumns as $column) {
341
-			$query->addOrderBy($column);
342
-		}
343
-
344
-		$insertQuery = $toDB->getQueryBuilder();
345
-		$insertQuery->automaticTablePrefix(false);
346
-		$insertQuery->insert($table->getName());
347
-		$parametersCreated = false;
348
-
349
-		for ($chunk = 0; $chunk < $numChunks; $chunk++) {
350
-			$query->setFirstResult($chunk * $chunkSize);
351
-
352
-			$result = $query->execute();
353
-
354
-			while ($row = $result->fetch()) {
355
-				$progress->advance();
356
-				if (!$parametersCreated) {
357
-					foreach ($row as $key => $value) {
358
-						$insertQuery->setValue($key, $insertQuery->createParameter($key));
359
-					}
360
-					$parametersCreated = true;
361
-				}
362
-
363
-				foreach ($row as $key => $value) {
364
-					$type = $this->getColumnType($table, $key);
365
-					if ($type !== false) {
366
-						$insertQuery->setParameter($key, $value, $type);
367
-					} else {
368
-						$insertQuery->setParameter($key, $value);
369
-					}
370
-				}
371
-				$insertQuery->execute();
372
-			}
373
-			$result->closeCursor();
374
-		}
375
-		$progress->finish();
376
-	}
377
-
378
-	protected function getColumnType(Table $table, $columnName) {
379
-		$tableName = $table->getName();
380
-		if (isset($this->columnTypes[$tableName][$columnName])) {
381
-			return $this->columnTypes[$tableName][$columnName];
382
-		}
383
-
384
-		$type = $table->getColumn($columnName)->getType()->getName();
385
-
386
-		switch ($type) {
387
-			case Type::BLOB:
388
-			case Type::TEXT:
389
-				$this->columnTypes[$tableName][$columnName] = IQueryBuilder::PARAM_LOB;
390
-				break;
391
-			case Type::BOOLEAN:
392
-				$this->columnTypes[$tableName][$columnName] = IQueryBuilder::PARAM_BOOL;
393
-				break;
394
-			default:
395
-				$this->columnTypes[$tableName][$columnName] = false;
396
-		}
397
-
398
-		return $this->columnTypes[$tableName][$columnName];
399
-	}
400
-
401
-	protected function convertDB(Connection $fromDB, Connection $toDB, array $tables, InputInterface $input, OutputInterface $output) {
402
-		$this->config->setSystemValue('maintenance', true);
403
-		$schema = $fromDB->createSchema();
404
-
405
-		try {
406
-			// copy table rows
407
-			foreach ($tables as $table) {
408
-				$output->writeln($table);
409
-				$this->copyTable($fromDB, $toDB, $schema->getTable($table), $input, $output);
410
-			}
411
-			if ($input->getArgument('type') === 'pgsql') {
412
-				$tools = new \OC\DB\PgSqlTools($this->config);
413
-				$tools->resynchronizeDatabaseSequences($toDB);
414
-			}
415
-			// save new database config
416
-			$this->saveDBInfo($input);
417
-		} catch (\Exception $e) {
418
-			$this->config->setSystemValue('maintenance', false);
419
-			throw $e;
420
-		}
421
-		$this->config->setSystemValue('maintenance', false);
422
-	}
423
-
424
-	protected function saveDBInfo(InputInterface $input) {
425
-		$type = $input->getArgument('type');
426
-		$username = $input->getArgument('username');
427
-		$dbHost = $input->getArgument('hostname');
428
-		$dbName = $input->getArgument('database');
429
-		$password = $input->getOption('password');
430
-		if ($input->getOption('port')) {
431
-			$dbHost .= ':'.$input->getOption('port');
432
-		}
433
-
434
-		$this->config->setSystemValues([
435
-			'dbtype'		=> $type,
436
-			'dbname'		=> $dbName,
437
-			'dbhost'		=> $dbHost,
438
-			'dbuser'		=> $username,
439
-			'dbpassword'	=> $password,
440
-		]);
441
-	}
442
-
443
-	/**
444
-	 * Return possible values for the named option
445
-	 *
446
-	 * @param string $optionName
447
-	 * @param CompletionContext $context
448
-	 * @return string[]
449
-	 */
450
-	public function completeOptionValues($optionName, CompletionContext $context) {
451
-		return [];
452
-	}
453
-
454
-	/**
455
-	 * Return possible values for the named argument
456
-	 *
457
-	 * @param string $argumentName
458
-	 * @param CompletionContext $context
459
-	 * @return string[]
460
-	 */
461
-	public function completeArgumentValues($argumentName, CompletionContext $context) {
462
-		if ($argumentName === 'type') {
463
-			return ['mysql', 'oci', 'pgsql'];
464
-		}
465
-		return [];
466
-	}
57
+    /**
58
+     * @var \OCP\IConfig
59
+     */
60
+    protected $config;
61
+
62
+    /**
63
+     * @var \OC\DB\ConnectionFactory
64
+     */
65
+    protected $connectionFactory;
66
+
67
+    /** @var array */
68
+    protected $columnTypes;
69
+
70
+    /**
71
+     * @param \OCP\IConfig $config
72
+     * @param \OC\DB\ConnectionFactory $connectionFactory
73
+     */
74
+    public function __construct(IConfig $config, ConnectionFactory $connectionFactory) {
75
+        $this->config = $config;
76
+        $this->connectionFactory = $connectionFactory;
77
+        parent::__construct();
78
+    }
79
+
80
+    protected function configure() {
81
+        $this
82
+            ->setName('db:convert-type')
83
+            ->setDescription('Convert the Nextcloud database to the newly configured one')
84
+            ->addArgument(
85
+                'type',
86
+                InputArgument::REQUIRED,
87
+                'the type of the database to convert to'
88
+            )
89
+            ->addArgument(
90
+                'username',
91
+                InputArgument::REQUIRED,
92
+                'the username of the database to convert to'
93
+            )
94
+            ->addArgument(
95
+                'hostname',
96
+                InputArgument::REQUIRED,
97
+                'the hostname of the database to convert to'
98
+            )
99
+            ->addArgument(
100
+                'database',
101
+                InputArgument::REQUIRED,
102
+                'the name of the database to convert to'
103
+            )
104
+            ->addOption(
105
+                'port',
106
+                null,
107
+                InputOption::VALUE_REQUIRED,
108
+                'the port of the database to convert to'
109
+            )
110
+            ->addOption(
111
+                'password',
112
+                null,
113
+                InputOption::VALUE_REQUIRED,
114
+                'the password of the database to convert to. Will be asked when not specified. Can also be passed via stdin.'
115
+            )
116
+            ->addOption(
117
+                'clear-schema',
118
+                null,
119
+                InputOption::VALUE_NONE,
120
+                'remove all tables from the destination database'
121
+            )
122
+            ->addOption(
123
+                'all-apps',
124
+                null,
125
+                InputOption::VALUE_NONE,
126
+                'whether to create schema for all apps instead of only installed apps'
127
+            )
128
+            ->addOption(
129
+                'chunk-size',
130
+                null,
131
+                InputOption::VALUE_REQUIRED,
132
+                'the maximum number of database rows to handle in a single query, bigger tables will be handled in chunks of this size. Lower this if the process runs out of memory during conversion.',
133
+                1000
134
+            )
135
+        ;
136
+    }
137
+
138
+    protected function validateInput(InputInterface $input, OutputInterface $output) {
139
+        $type = $this->connectionFactory->normalizeType($input->getArgument('type'));
140
+        if ($type === 'sqlite3') {
141
+            throw new \InvalidArgumentException(
142
+                'Converting to SQLite (sqlite3) is currently not supported.'
143
+            );
144
+        }
145
+        if ($type === $this->config->getSystemValue('dbtype', '')) {
146
+            throw new \InvalidArgumentException(sprintf(
147
+                'Can not convert from %1$s to %1$s.',
148
+                $type
149
+            ));
150
+        }
151
+        if ($type === 'oci' && $input->getOption('clear-schema')) {
152
+            // Doctrine unconditionally tries (at least in version 2.3)
153
+            // to drop sequence triggers when dropping a table, even though
154
+            // such triggers may not exist. This results in errors like
155
+            // "ORA-04080: trigger 'OC_STORAGES_AI_PK' does not exist".
156
+            throw new \InvalidArgumentException(
157
+                'The --clear-schema option is not supported when converting to Oracle (oci).'
158
+            );
159
+        }
160
+    }
161
+
162
+    protected function readPassword(InputInterface $input, OutputInterface $output) {
163
+        // Explicitly specified password
164
+        if ($input->getOption('password')) {
165
+            return;
166
+        }
167
+
168
+        // Read from stdin. stream_set_blocking is used to prevent blocking
169
+        // when nothing is passed via stdin.
170
+        stream_set_blocking(STDIN, 0);
171
+        $password = file_get_contents('php://stdin');
172
+        stream_set_blocking(STDIN, 1);
173
+        if (trim($password) !== '') {
174
+            $input->setOption('password', $password);
175
+            return;
176
+        }
177
+
178
+        // Read password by interacting
179
+        if ($input->isInteractive()) {
180
+            /** @var QuestionHelper $helper */
181
+            $helper = $this->getHelper('question');
182
+            $question = new Question('What is the database password?');
183
+            $question->setHidden(true);
184
+            $question->setHiddenFallback(false);
185
+            $password = $helper->ask($input, $output, $question);
186
+            $input->setOption('password', $password);
187
+            return;
188
+        }
189
+    }
190
+
191
+    protected function execute(InputInterface $input, OutputInterface $output): int {
192
+        $this->validateInput($input, $output);
193
+        $this->readPassword($input, $output);
194
+
195
+        $fromDB = \OC::$server->getDatabaseConnection();
196
+        $toDB = $this->getToDBConnection($input, $output);
197
+
198
+        if ($input->getOption('clear-schema')) {
199
+            $this->clearSchema($toDB, $input, $output);
200
+        }
201
+
202
+        $this->createSchema($fromDB, $toDB, $input, $output);
203
+
204
+        $toTables = $this->getTables($toDB);
205
+        $fromTables = $this->getTables($fromDB);
206
+
207
+        // warn/fail if there are more tables in 'from' database
208
+        $extraFromTables = array_diff($fromTables, $toTables);
209
+        if (!empty($extraFromTables)) {
210
+            $output->writeln('<comment>The following tables will not be converted:</comment>');
211
+            $output->writeln($extraFromTables);
212
+            if (!$input->getOption('all-apps')) {
213
+                $output->writeln('<comment>Please note that tables belonging to available but currently not installed apps</comment>');
214
+                $output->writeln('<comment>can be included by specifying the --all-apps option.</comment>');
215
+            }
216
+
217
+            $continueConversion = !$input->isInteractive(); // assume yes for --no-interaction and no otherwise.
218
+            $question = new ConfirmationQuestion('Continue with the conversion (y/n)? [n] ', $continueConversion);
219
+
220
+            /** @var QuestionHelper $helper */
221
+            $helper = $this->getHelper('question');
222
+
223
+            if (!$helper->ask($input, $output, $question)) {
224
+                return 1;
225
+            }
226
+        }
227
+        $intersectingTables = array_intersect($toTables, $fromTables);
228
+        $this->convertDB($fromDB, $toDB, $intersectingTables, $input, $output);
229
+        return 0;
230
+    }
231
+
232
+    protected function createSchema(Connection $fromDB, Connection $toDB, InputInterface $input, OutputInterface $output) {
233
+        $output->writeln('<info>Creating schema in new database</info>');
234
+
235
+        $fromMS = new MigrationService('core', $fromDB);
236
+        $currentMigration = $fromMS->getMigration('current');
237
+        if ($currentMigration !== '0') {
238
+            $toMS = new MigrationService('core', $toDB);
239
+            $toMS->migrate($currentMigration);
240
+        }
241
+
242
+        $schemaManager = new \OC\DB\MDB2SchemaManager($toDB);
243
+        $apps = $input->getOption('all-apps') ? \OC_App::getAllApps() : \OC_App::getEnabledApps();
244
+        foreach ($apps as $app) {
245
+            if (file_exists(\OC_App::getAppPath($app).'/appinfo/database.xml')) {
246
+                $schemaManager->createDbFromStructure(\OC_App::getAppPath($app).'/appinfo/database.xml');
247
+            } else {
248
+                // Make sure autoloading works...
249
+                \OC_App::loadApp($app);
250
+                $fromMS = new MigrationService($app, $fromDB);
251
+                $currentMigration = $fromMS->getMigration('current');
252
+                if ($currentMigration !== '0') {
253
+                    $toMS = new MigrationService($app, $toDB);
254
+                    $toMS->migrate($currentMigration, true);
255
+                }
256
+            }
257
+        }
258
+    }
259
+
260
+    protected function getToDBConnection(InputInterface $input, OutputInterface $output) {
261
+        $type = $input->getArgument('type');
262
+        $connectionParams = $this->connectionFactory->createConnectionParams();
263
+        $connectionParams = array_merge($connectionParams, [
264
+            'host' => $input->getArgument('hostname'),
265
+            'user' => $input->getArgument('username'),
266
+            'password' => $input->getOption('password'),
267
+            'dbname' => $input->getArgument('database'),
268
+        ]);
269
+        if ($input->getOption('port')) {
270
+            $connectionParams['port'] = $input->getOption('port');
271
+        }
272
+        return $this->connectionFactory->getConnection($type, $connectionParams);
273
+    }
274
+
275
+    protected function clearSchema(Connection $db, InputInterface $input, OutputInterface $output) {
276
+        $toTables = $this->getTables($db);
277
+        if (!empty($toTables)) {
278
+            $output->writeln('<info>Clearing schema in new database</info>');
279
+        }
280
+        foreach ($toTables as $table) {
281
+            $db->getSchemaManager()->dropTable($table);
282
+        }
283
+    }
284
+
285
+    protected function getTables(Connection $db) {
286
+        $filterExpression = '/^' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/';
287
+        $db->getConfiguration()->
288
+            setFilterSchemaAssetsExpression($filterExpression);
289
+        return $db->getSchemaManager()->listTableNames();
290
+    }
291
+
292
+    /**
293
+     * @param Connection $fromDB
294
+     * @param Connection $toDB
295
+     * @param Table $table
296
+     * @param InputInterface $input
297
+     * @param OutputInterface $output
298
+     */
299
+    protected function copyTable(Connection $fromDB, Connection $toDB, Table $table, InputInterface $input, OutputInterface $output) {
300
+        if ($table->getName() === $toDB->getPrefix() . 'migrations') {
301
+            $output->writeln('<comment>Skipping migrations table because it was already filled by running the migrations</comment>');
302
+            return;
303
+        }
304
+
305
+        $chunkSize = $input->getOption('chunk-size');
306
+
307
+        $query = $fromDB->getQueryBuilder();
308
+        $query->automaticTablePrefix(false);
309
+        $query->select($query->func()->count('*', 'num_entries'))
310
+            ->from($table->getName());
311
+        $result = $query->execute();
312
+        $count = $result->fetchColumn();
313
+        $result->closeCursor();
314
+
315
+        $numChunks = ceil($count/$chunkSize);
316
+        if ($numChunks > 1) {
317
+            $output->writeln('chunked query, ' . $numChunks . ' chunks');
318
+        }
319
+
320
+        $progress = new ProgressBar($output, $count);
321
+        $progress->start();
322
+        $redraw = $count > $chunkSize ? 100 : ($count > 100 ? 5 : 1);
323
+        $progress->setRedrawFrequency($redraw);
324
+
325
+        $query = $fromDB->getQueryBuilder();
326
+        $query->automaticTablePrefix(false);
327
+        $query->select('*')
328
+            ->from($table->getName())
329
+            ->setMaxResults($chunkSize);
330
+
331
+        try {
332
+            $orderColumns = $table->getPrimaryKeyColumns();
333
+        } catch (DBALException $e) {
334
+            $orderColumns = [];
335
+            foreach ($table->getColumns() as $column) {
336
+                $orderColumns[] = $column->getName();
337
+            }
338
+        }
339
+
340
+        foreach ($orderColumns as $column) {
341
+            $query->addOrderBy($column);
342
+        }
343
+
344
+        $insertQuery = $toDB->getQueryBuilder();
345
+        $insertQuery->automaticTablePrefix(false);
346
+        $insertQuery->insert($table->getName());
347
+        $parametersCreated = false;
348
+
349
+        for ($chunk = 0; $chunk < $numChunks; $chunk++) {
350
+            $query->setFirstResult($chunk * $chunkSize);
351
+
352
+            $result = $query->execute();
353
+
354
+            while ($row = $result->fetch()) {
355
+                $progress->advance();
356
+                if (!$parametersCreated) {
357
+                    foreach ($row as $key => $value) {
358
+                        $insertQuery->setValue($key, $insertQuery->createParameter($key));
359
+                    }
360
+                    $parametersCreated = true;
361
+                }
362
+
363
+                foreach ($row as $key => $value) {
364
+                    $type = $this->getColumnType($table, $key);
365
+                    if ($type !== false) {
366
+                        $insertQuery->setParameter($key, $value, $type);
367
+                    } else {
368
+                        $insertQuery->setParameter($key, $value);
369
+                    }
370
+                }
371
+                $insertQuery->execute();
372
+            }
373
+            $result->closeCursor();
374
+        }
375
+        $progress->finish();
376
+    }
377
+
378
+    protected function getColumnType(Table $table, $columnName) {
379
+        $tableName = $table->getName();
380
+        if (isset($this->columnTypes[$tableName][$columnName])) {
381
+            return $this->columnTypes[$tableName][$columnName];
382
+        }
383
+
384
+        $type = $table->getColumn($columnName)->getType()->getName();
385
+
386
+        switch ($type) {
387
+            case Type::BLOB:
388
+            case Type::TEXT:
389
+                $this->columnTypes[$tableName][$columnName] = IQueryBuilder::PARAM_LOB;
390
+                break;
391
+            case Type::BOOLEAN:
392
+                $this->columnTypes[$tableName][$columnName] = IQueryBuilder::PARAM_BOOL;
393
+                break;
394
+            default:
395
+                $this->columnTypes[$tableName][$columnName] = false;
396
+        }
397
+
398
+        return $this->columnTypes[$tableName][$columnName];
399
+    }
400
+
401
+    protected function convertDB(Connection $fromDB, Connection $toDB, array $tables, InputInterface $input, OutputInterface $output) {
402
+        $this->config->setSystemValue('maintenance', true);
403
+        $schema = $fromDB->createSchema();
404
+
405
+        try {
406
+            // copy table rows
407
+            foreach ($tables as $table) {
408
+                $output->writeln($table);
409
+                $this->copyTable($fromDB, $toDB, $schema->getTable($table), $input, $output);
410
+            }
411
+            if ($input->getArgument('type') === 'pgsql') {
412
+                $tools = new \OC\DB\PgSqlTools($this->config);
413
+                $tools->resynchronizeDatabaseSequences($toDB);
414
+            }
415
+            // save new database config
416
+            $this->saveDBInfo($input);
417
+        } catch (\Exception $e) {
418
+            $this->config->setSystemValue('maintenance', false);
419
+            throw $e;
420
+        }
421
+        $this->config->setSystemValue('maintenance', false);
422
+    }
423
+
424
+    protected function saveDBInfo(InputInterface $input) {
425
+        $type = $input->getArgument('type');
426
+        $username = $input->getArgument('username');
427
+        $dbHost = $input->getArgument('hostname');
428
+        $dbName = $input->getArgument('database');
429
+        $password = $input->getOption('password');
430
+        if ($input->getOption('port')) {
431
+            $dbHost .= ':'.$input->getOption('port');
432
+        }
433
+
434
+        $this->config->setSystemValues([
435
+            'dbtype'		=> $type,
436
+            'dbname'		=> $dbName,
437
+            'dbhost'		=> $dbHost,
438
+            'dbuser'		=> $username,
439
+            'dbpassword'	=> $password,
440
+        ]);
441
+    }
442
+
443
+    /**
444
+     * Return possible values for the named option
445
+     *
446
+     * @param string $optionName
447
+     * @param CompletionContext $context
448
+     * @return string[]
449
+     */
450
+    public function completeOptionValues($optionName, CompletionContext $context) {
451
+        return [];
452
+    }
453
+
454
+    /**
455
+     * Return possible values for the named argument
456
+     *
457
+     * @param string $argumentName
458
+     * @param CompletionContext $context
459
+     * @return string[]
460
+     */
461
+    public function completeArgumentValues($argumentName, CompletionContext $context) {
462
+        if ($argumentName === 'type') {
463
+            return ['mysql', 'oci', 'pgsql'];
464
+        }
465
+        return [];
466
+    }
467 467
 }
Please login to merge, or discard this patch.
Spacing   +4 added lines, -4 removed lines patch added patch discarded remove patch
@@ -283,7 +283,7 @@  discard block
 block discarded – undo
283 283
 	}
284 284
 
285 285
 	protected function getTables(Connection $db) {
286
-		$filterExpression = '/^' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/';
286
+		$filterExpression = '/^'.preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')).'/';
287 287
 		$db->getConfiguration()->
288 288
 			setFilterSchemaAssetsExpression($filterExpression);
289 289
 		return $db->getSchemaManager()->listTableNames();
@@ -297,7 +297,7 @@  discard block
 block discarded – undo
297 297
 	 * @param OutputInterface $output
298 298
 	 */
299 299
 	protected function copyTable(Connection $fromDB, Connection $toDB, Table $table, InputInterface $input, OutputInterface $output) {
300
-		if ($table->getName() === $toDB->getPrefix() . 'migrations') {
300
+		if ($table->getName() === $toDB->getPrefix().'migrations') {
301 301
 			$output->writeln('<comment>Skipping migrations table because it was already filled by running the migrations</comment>');
302 302
 			return;
303 303
 		}
@@ -312,9 +312,9 @@  discard block
 block discarded – undo
312 312
 		$count = $result->fetchColumn();
313 313
 		$result->closeCursor();
314 314
 
315
-		$numChunks = ceil($count/$chunkSize);
315
+		$numChunks = ceil($count / $chunkSize);
316 316
 		if ($numChunks > 1) {
317
-			$output->writeln('chunked query, ' . $numChunks . ' chunks');
317
+			$output->writeln('chunked query, '.$numChunks.' chunks');
318 318
 		}
319 319
 
320 320
 		$progress = new ProgressBar($output, $count);
Please login to merge, or discard this patch.
apps/files_external/lib/Service/DBConfigService.php 1 patch
Indentation   +493 added lines, -493 removed lines patch added patch discarded remove patch
@@ -37,497 +37,497 @@
 block discarded – undo
37 37
  * Stores the mount config in the database
38 38
  */
39 39
 class DBConfigService {
40
-	public const MOUNT_TYPE_ADMIN = 1;
41
-	public const MOUNT_TYPE_PERSONAl = 2;
42
-
43
-	public const APPLICABLE_TYPE_GLOBAL = 1;
44
-	public const APPLICABLE_TYPE_GROUP = 2;
45
-	public const APPLICABLE_TYPE_USER = 3;
46
-
47
-	/**
48
-	 * @var IDBConnection
49
-	 */
50
-	private $connection;
51
-
52
-	/**
53
-	 * @var ICrypto
54
-	 */
55
-	private $crypto;
56
-
57
-	/**
58
-	 * DBConfigService constructor.
59
-	 *
60
-	 * @param IDBConnection $connection
61
-	 * @param ICrypto $crypto
62
-	 */
63
-	public function __construct(IDBConnection $connection, ICrypto $crypto) {
64
-		$this->connection = $connection;
65
-		$this->crypto = $crypto;
66
-	}
67
-
68
-	/**
69
-	 * @param int $mountId
70
-	 * @return array
71
-	 */
72
-	public function getMountById($mountId) {
73
-		$builder = $this->connection->getQueryBuilder();
74
-		$query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
75
-			->from('external_mounts', 'm')
76
-			->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
77
-		$mounts = $this->getMountsFromQuery($query);
78
-		if (count($mounts) > 0) {
79
-			return $mounts[0];
80
-		} else {
81
-			return null;
82
-		}
83
-	}
84
-
85
-	/**
86
-	 * Get all configured mounts
87
-	 *
88
-	 * @return array
89
-	 */
90
-	public function getAllMounts() {
91
-		$builder = $this->connection->getQueryBuilder();
92
-		$query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
93
-			->from('external_mounts');
94
-		return $this->getMountsFromQuery($query);
95
-	}
96
-
97
-	public function getMountsForUser($userId, $groupIds) {
98
-		$builder = $this->connection->getQueryBuilder();
99
-		$query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
100
-			->from('external_mounts', 'm')
101
-			->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
102
-			->where($builder->expr()->orX(
103
-				$builder->expr()->andX( // global mounts
104
-					$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)),
105
-					$builder->expr()->isNull('a.value')
106
-				),
107
-				$builder->expr()->andX( // mounts for user
108
-					$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_USER, IQueryBuilder::PARAM_INT)),
109
-					$builder->expr()->eq('a.value', $builder->createNamedParameter($userId))
110
-				),
111
-				$builder->expr()->andX( // mounts for group
112
-					$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GROUP, IQueryBuilder::PARAM_INT)),
113
-					$builder->expr()->in('a.value', $builder->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY))
114
-				)
115
-			));
116
-
117
-		return $this->getMountsFromQuery($query);
118
-	}
119
-
120
-	public function modifyMountsOnUserDelete(string $uid): void {
121
-		$this->modifyMountsOnDelete($uid, self::APPLICABLE_TYPE_USER);
122
-	}
123
-
124
-	public function modifyMountsOnGroupDelete(string $gid): void {
125
-		$this->modifyMountsOnDelete($gid, self::APPLICABLE_TYPE_GROUP);
126
-	}
127
-
128
-	protected function modifyMountsOnDelete(string $applicableId, int $applicableType): void {
129
-		$builder = $this->connection->getQueryBuilder();
130
-		$query = $builder->select(['a.mount_id', $builder->func()->count('a.mount_id', 'count')])
131
-			->from('external_applicable', 'a')
132
-			->leftJoin('a', 'external_applicable', 'b', $builder->expr()->eq('a.mount_id', 'b.mount_id'))
133
-			->where($builder->expr()->andX(
134
-				$builder->expr()->eq('b.type', $builder->createNamedParameter($applicableType, IQueryBuilder::PARAM_INT)),
135
-				$builder->expr()->eq('b.value', $builder->createNamedParameter($applicableId))
136
-			)
137
-			)
138
-			->groupBy(['a.mount_id']);
139
-		$stmt = $query->execute();
140
-		$result = $stmt->fetchAll();
141
-		$stmt->closeCursor();
142
-
143
-		foreach ($result as $row) {
144
-			if ((int)$row['count'] > 1) {
145
-				$this->removeApplicable($row['mount_id'], $applicableType, $applicableId);
146
-			} else {
147
-				$this->removeMount($row['mount_id']);
148
-			}
149
-		}
150
-	}
151
-
152
-	/**
153
-	 * Get admin defined mounts
154
-	 *
155
-	 * @return array
156
-	 */
157
-	public function getAdminMounts() {
158
-		$builder = $this->connection->getQueryBuilder();
159
-		$query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
160
-			->from('external_mounts')
161
-			->where($builder->expr()->eq('type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
162
-		return $this->getMountsFromQuery($query);
163
-	}
164
-
165
-	protected function getForQuery(IQueryBuilder $builder, $type, $value) {
166
-		$query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
167
-			->from('external_mounts', 'm')
168
-			->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
169
-			->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
170
-
171
-		if (is_null($value)) {
172
-			$query = $query->andWhere($builder->expr()->isNull('a.value'));
173
-		} else {
174
-			$query = $query->andWhere($builder->expr()->eq('a.value', $builder->createNamedParameter($value)));
175
-		}
176
-
177
-		return $query;
178
-	}
179
-
180
-	/**
181
-	 * Get mounts by applicable
182
-	 *
183
-	 * @param int $type any of the self::APPLICABLE_TYPE_ constants
184
-	 * @param string|null $value user_id, group_id or null for global mounts
185
-	 * @return array
186
-	 */
187
-	public function getMountsFor($type, $value) {
188
-		$builder = $this->connection->getQueryBuilder();
189
-		$query = $this->getForQuery($builder, $type, $value);
190
-
191
-		return $this->getMountsFromQuery($query);
192
-	}
193
-
194
-	/**
195
-	 * Get admin defined mounts by applicable
196
-	 *
197
-	 * @param int $type any of the self::APPLICABLE_TYPE_ constants
198
-	 * @param string|null $value user_id, group_id or null for global mounts
199
-	 * @return array
200
-	 */
201
-	public function getAdminMountsFor($type, $value) {
202
-		$builder = $this->connection->getQueryBuilder();
203
-		$query = $this->getForQuery($builder, $type, $value);
204
-		$query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
205
-
206
-		return $this->getMountsFromQuery($query);
207
-	}
208
-
209
-	/**
210
-	 * Get admin defined mounts for multiple applicable
211
-	 *
212
-	 * @param int $type any of the self::APPLICABLE_TYPE_ constants
213
-	 * @param string[] $values user_ids or group_ids
214
-	 * @return array
215
-	 */
216
-	public function getAdminMountsForMultiple($type, array $values) {
217
-		$builder = $this->connection->getQueryBuilder();
218
-		$params = array_map(function ($value) use ($builder) {
219
-			return $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR);
220
-		}, $values);
221
-
222
-		$query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
223
-			->from('external_mounts', 'm')
224
-			->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
225
-			->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
226
-			->andWhere($builder->expr()->in('a.value', $params));
227
-		$query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
228
-
229
-		return $this->getMountsFromQuery($query);
230
-	}
231
-
232
-	/**
233
-	 * Get user defined mounts by applicable
234
-	 *
235
-	 * @param int $type any of the self::APPLICABLE_TYPE_ constants
236
-	 * @param string|null $value user_id, group_id or null for global mounts
237
-	 * @return array
238
-	 */
239
-	public function getUserMountsFor($type, $value) {
240
-		$builder = $this->connection->getQueryBuilder();
241
-		$query = $this->getForQuery($builder, $type, $value);
242
-		$query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_PERSONAl, IQueryBuilder::PARAM_INT)));
243
-
244
-		return $this->getMountsFromQuery($query);
245
-	}
246
-
247
-	/**
248
-	 * Add a mount to the database
249
-	 *
250
-	 * @param string $mountPoint
251
-	 * @param string $storageBackend
252
-	 * @param string $authBackend
253
-	 * @param int $priority
254
-	 * @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL
255
-	 * @return int the id of the new mount
256
-	 */
257
-	public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type) {
258
-		if (!$priority) {
259
-			$priority = 100;
260
-		}
261
-		$builder = $this->connection->getQueryBuilder();
262
-		$query = $builder->insert('external_mounts')
263
-			->values([
264
-				'mount_point' => $builder->createNamedParameter($mountPoint, IQueryBuilder::PARAM_STR),
265
-				'storage_backend' => $builder->createNamedParameter($storageBackend, IQueryBuilder::PARAM_STR),
266
-				'auth_backend' => $builder->createNamedParameter($authBackend, IQueryBuilder::PARAM_STR),
267
-				'priority' => $builder->createNamedParameter($priority, IQueryBuilder::PARAM_INT),
268
-				'type' => $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)
269
-			]);
270
-		$query->execute();
271
-		return (int)$this->connection->lastInsertId('*PREFIX*external_mounts');
272
-	}
273
-
274
-	/**
275
-	 * Remove a mount from the database
276
-	 *
277
-	 * @param int $mountId
278
-	 */
279
-	public function removeMount($mountId) {
280
-		$builder = $this->connection->getQueryBuilder();
281
-		$query = $builder->delete('external_mounts')
282
-			->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
283
-		$query->execute();
284
-
285
-		$query = $builder->delete('external_applicable')
286
-			->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
287
-		$query->execute();
288
-
289
-		$query = $builder->delete('external_config')
290
-			->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
291
-		$query->execute();
292
-
293
-		$query = $builder->delete('external_options')
294
-			->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
295
-		$query->execute();
296
-	}
297
-
298
-	/**
299
-	 * @param int $mountId
300
-	 * @param string $newMountPoint
301
-	 */
302
-	public function setMountPoint($mountId, $newMountPoint) {
303
-		$builder = $this->connection->getQueryBuilder();
304
-
305
-		$query = $builder->update('external_mounts')
306
-			->set('mount_point', $builder->createNamedParameter($newMountPoint))
307
-			->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
308
-
309
-		$query->execute();
310
-	}
311
-
312
-	/**
313
-	 * @param int $mountId
314
-	 * @param string $newAuthBackend
315
-	 */
316
-	public function setAuthBackend($mountId, $newAuthBackend) {
317
-		$builder = $this->connection->getQueryBuilder();
318
-
319
-		$query = $builder->update('external_mounts')
320
-			->set('auth_backend', $builder->createNamedParameter($newAuthBackend))
321
-			->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
322
-
323
-		$query->execute();
324
-	}
325
-
326
-	/**
327
-	 * @param int $mountId
328
-	 * @param string $key
329
-	 * @param string $value
330
-	 */
331
-	public function setConfig($mountId, $key, $value) {
332
-		if ($key === 'password') {
333
-			$value = $this->encryptValue($value);
334
-		}
335
-
336
-		try {
337
-			$builder = $this->connection->getQueryBuilder();
338
-			$builder->insert('external_config')
339
-				->setValue('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))
340
-				->setValue('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR))
341
-				->setValue('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR))
342
-				->execute();
343
-		} catch (UniqueConstraintViolationException $e) {
344
-			$builder = $this->connection->getQueryBuilder();
345
-			$query = $builder->update('external_config')
346
-				->set('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR))
347
-				->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
348
-				->andWhere($builder->expr()->eq('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR)));
349
-			$query->execute();
350
-		}
351
-	}
352
-
353
-	/**
354
-	 * @param int $mountId
355
-	 * @param string $key
356
-	 * @param string $value
357
-	 */
358
-	public function setOption($mountId, $key, $value) {
359
-		try {
360
-			$builder = $this->connection->getQueryBuilder();
361
-			$builder->insert('external_options')
362
-				->setValue('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))
363
-				->setValue('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR))
364
-				->setValue('value', $builder->createNamedParameter(json_encode($value), IQueryBuilder::PARAM_STR))
365
-				->execute();
366
-		} catch (UniqueConstraintViolationException $e) {
367
-			$builder = $this->connection->getQueryBuilder();
368
-			$query = $builder->update('external_options')
369
-				->set('value', $builder->createNamedParameter(json_encode($value), IQueryBuilder::PARAM_STR))
370
-				->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
371
-				->andWhere($builder->expr()->eq('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR)));
372
-			$query->execute();
373
-		}
374
-	}
375
-
376
-	public function addApplicable($mountId, $type, $value) {
377
-		try {
378
-			$builder = $this->connection->getQueryBuilder();
379
-			$builder->insert('external_applicable')
380
-				->setValue('mount_id', $builder->createNamedParameter($mountId))
381
-				->setValue('type', $builder->createNamedParameter($type))
382
-				->setValue('value', $builder->createNamedParameter($value))
383
-				->execute();
384
-		} catch (UniqueConstraintViolationException $e) {
385
-			// applicable exists already
386
-		}
387
-	}
388
-
389
-	public function removeApplicable($mountId, $type, $value) {
390
-		$builder = $this->connection->getQueryBuilder();
391
-		$query = $builder->delete('external_applicable')
392
-			->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
393
-			->andWhere($builder->expr()->eq('type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
394
-
395
-		if (is_null($value)) {
396
-			$query = $query->andWhere($builder->expr()->isNull('value'));
397
-		} else {
398
-			$query = $query->andWhere($builder->expr()->eq('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR)));
399
-		}
400
-
401
-		$query->execute();
402
-	}
403
-
404
-	private function getMountsFromQuery(IQueryBuilder $query) {
405
-		$result = $query->execute();
406
-		$mounts = $result->fetchAll();
407
-		$uniqueMounts = [];
408
-		foreach ($mounts as $mount) {
409
-			$id = $mount['mount_id'];
410
-			if (!isset($uniqueMounts[$id])) {
411
-				$uniqueMounts[$id] = $mount;
412
-			}
413
-		}
414
-		$uniqueMounts = array_values($uniqueMounts);
415
-
416
-		$mountIds = array_map(function ($mount) {
417
-			return $mount['mount_id'];
418
-		}, $uniqueMounts);
419
-		$mountIds = array_values(array_unique($mountIds));
420
-
421
-		$applicable = $this->getApplicableForMounts($mountIds);
422
-		$config = $this->getConfigForMounts($mountIds);
423
-		$options = $this->getOptionsForMounts($mountIds);
424
-
425
-		return array_map(function ($mount, $applicable, $config, $options) {
426
-			$mount['type'] = (int)$mount['type'];
427
-			$mount['priority'] = (int)$mount['priority'];
428
-			$mount['applicable'] = $applicable;
429
-			$mount['config'] = $config;
430
-			$mount['options'] = $options;
431
-			return $mount;
432
-		}, $uniqueMounts, $applicable, $config, $options);
433
-	}
434
-
435
-	/**
436
-	 * Get mount options from a table grouped by mount id
437
-	 *
438
-	 * @param string $table
439
-	 * @param string[] $fields
440
-	 * @param int[] $mountIds
441
-	 * @return array [$mountId => [['field1' => $value1, ...], ...], ...]
442
-	 */
443
-	private function selectForMounts($table, array $fields, array $mountIds) {
444
-		if (count($mountIds) === 0) {
445
-			return [];
446
-		}
447
-		$builder = $this->connection->getQueryBuilder();
448
-		$fields[] = 'mount_id';
449
-		$placeHolders = array_map(function ($id) use ($builder) {
450
-			return $builder->createPositionalParameter($id, IQueryBuilder::PARAM_INT);
451
-		}, $mountIds);
452
-		$query = $builder->select($fields)
453
-			->from($table)
454
-			->where($builder->expr()->in('mount_id', $placeHolders));
455
-		$rows = $query->execute()->fetchAll();
456
-
457
-		$result = [];
458
-		foreach ($mountIds as $mountId) {
459
-			$result[$mountId] = [];
460
-		}
461
-		foreach ($rows as $row) {
462
-			if (isset($row['type'])) {
463
-				$row['type'] = (int)$row['type'];
464
-			}
465
-			$result[$row['mount_id']][] = $row;
466
-		}
467
-		return $result;
468
-	}
469
-
470
-	/**
471
-	 * @param int[] $mountIds
472
-	 * @return array [$id => [['type' => $type, 'value' => $value], ...], ...]
473
-	 */
474
-	public function getApplicableForMounts($mountIds) {
475
-		return $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds);
476
-	}
477
-
478
-	/**
479
-	 * @param int[] $mountIds
480
-	 * @return array [$id => ['key1' => $value1, ...], ...]
481
-	 */
482
-	public function getConfigForMounts($mountIds) {
483
-		$mountConfigs = $this->selectForMounts('external_config', ['key', 'value'], $mountIds);
484
-		return array_map([$this, 'createKeyValueMap'], $mountConfigs);
485
-	}
486
-
487
-	/**
488
-	 * @param int[] $mountIds
489
-	 * @return array [$id => ['key1' => $value1, ...], ...]
490
-	 */
491
-	public function getOptionsForMounts($mountIds) {
492
-		$mountOptions = $this->selectForMounts('external_options', ['key', 'value'], $mountIds);
493
-		$optionsMap = array_map([$this, 'createKeyValueMap'], $mountOptions);
494
-		return array_map(function (array $options) {
495
-			return array_map(function ($option) {
496
-				return json_decode($option);
497
-			}, $options);
498
-		}, $optionsMap);
499
-	}
500
-
501
-	/**
502
-	 * @param array $keyValuePairs [['key'=>$key, 'value=>$value], ...]
503
-	 * @return array ['key1' => $value1, ...]
504
-	 */
505
-	private function createKeyValueMap(array $keyValuePairs) {
506
-		$decryptedPairts = array_map(function ($pair) {
507
-			if ($pair['key'] === 'password') {
508
-				$pair['value'] = $this->decryptValue($pair['value']);
509
-			}
510
-			return $pair;
511
-		}, $keyValuePairs);
512
-		$keys = array_map(function ($pair) {
513
-			return $pair['key'];
514
-		}, $decryptedPairts);
515
-		$values = array_map(function ($pair) {
516
-			return $pair['value'];
517
-		}, $decryptedPairts);
518
-
519
-		return array_combine($keys, $values);
520
-	}
521
-
522
-	private function encryptValue($value) {
523
-		return $this->crypto->encrypt($value);
524
-	}
525
-
526
-	private function decryptValue($value) {
527
-		try {
528
-			return $this->crypto->decrypt($value);
529
-		} catch (\Exception $e) {
530
-			return $value;
531
-		}
532
-	}
40
+    public const MOUNT_TYPE_ADMIN = 1;
41
+    public const MOUNT_TYPE_PERSONAl = 2;
42
+
43
+    public const APPLICABLE_TYPE_GLOBAL = 1;
44
+    public const APPLICABLE_TYPE_GROUP = 2;
45
+    public const APPLICABLE_TYPE_USER = 3;
46
+
47
+    /**
48
+     * @var IDBConnection
49
+     */
50
+    private $connection;
51
+
52
+    /**
53
+     * @var ICrypto
54
+     */
55
+    private $crypto;
56
+
57
+    /**
58
+     * DBConfigService constructor.
59
+     *
60
+     * @param IDBConnection $connection
61
+     * @param ICrypto $crypto
62
+     */
63
+    public function __construct(IDBConnection $connection, ICrypto $crypto) {
64
+        $this->connection = $connection;
65
+        $this->crypto = $crypto;
66
+    }
67
+
68
+    /**
69
+     * @param int $mountId
70
+     * @return array
71
+     */
72
+    public function getMountById($mountId) {
73
+        $builder = $this->connection->getQueryBuilder();
74
+        $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
75
+            ->from('external_mounts', 'm')
76
+            ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
77
+        $mounts = $this->getMountsFromQuery($query);
78
+        if (count($mounts) > 0) {
79
+            return $mounts[0];
80
+        } else {
81
+            return null;
82
+        }
83
+    }
84
+
85
+    /**
86
+     * Get all configured mounts
87
+     *
88
+     * @return array
89
+     */
90
+    public function getAllMounts() {
91
+        $builder = $this->connection->getQueryBuilder();
92
+        $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
93
+            ->from('external_mounts');
94
+        return $this->getMountsFromQuery($query);
95
+    }
96
+
97
+    public function getMountsForUser($userId, $groupIds) {
98
+        $builder = $this->connection->getQueryBuilder();
99
+        $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
100
+            ->from('external_mounts', 'm')
101
+            ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
102
+            ->where($builder->expr()->orX(
103
+                $builder->expr()->andX( // global mounts
104
+                    $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)),
105
+                    $builder->expr()->isNull('a.value')
106
+                ),
107
+                $builder->expr()->andX( // mounts for user
108
+                    $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_USER, IQueryBuilder::PARAM_INT)),
109
+                    $builder->expr()->eq('a.value', $builder->createNamedParameter($userId))
110
+                ),
111
+                $builder->expr()->andX( // mounts for group
112
+                    $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GROUP, IQueryBuilder::PARAM_INT)),
113
+                    $builder->expr()->in('a.value', $builder->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY))
114
+                )
115
+            ));
116
+
117
+        return $this->getMountsFromQuery($query);
118
+    }
119
+
120
+    public function modifyMountsOnUserDelete(string $uid): void {
121
+        $this->modifyMountsOnDelete($uid, self::APPLICABLE_TYPE_USER);
122
+    }
123
+
124
+    public function modifyMountsOnGroupDelete(string $gid): void {
125
+        $this->modifyMountsOnDelete($gid, self::APPLICABLE_TYPE_GROUP);
126
+    }
127
+
128
+    protected function modifyMountsOnDelete(string $applicableId, int $applicableType): void {
129
+        $builder = $this->connection->getQueryBuilder();
130
+        $query = $builder->select(['a.mount_id', $builder->func()->count('a.mount_id', 'count')])
131
+            ->from('external_applicable', 'a')
132
+            ->leftJoin('a', 'external_applicable', 'b', $builder->expr()->eq('a.mount_id', 'b.mount_id'))
133
+            ->where($builder->expr()->andX(
134
+                $builder->expr()->eq('b.type', $builder->createNamedParameter($applicableType, IQueryBuilder::PARAM_INT)),
135
+                $builder->expr()->eq('b.value', $builder->createNamedParameter($applicableId))
136
+            )
137
+            )
138
+            ->groupBy(['a.mount_id']);
139
+        $stmt = $query->execute();
140
+        $result = $stmt->fetchAll();
141
+        $stmt->closeCursor();
142
+
143
+        foreach ($result as $row) {
144
+            if ((int)$row['count'] > 1) {
145
+                $this->removeApplicable($row['mount_id'], $applicableType, $applicableId);
146
+            } else {
147
+                $this->removeMount($row['mount_id']);
148
+            }
149
+        }
150
+    }
151
+
152
+    /**
153
+     * Get admin defined mounts
154
+     *
155
+     * @return array
156
+     */
157
+    public function getAdminMounts() {
158
+        $builder = $this->connection->getQueryBuilder();
159
+        $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
160
+            ->from('external_mounts')
161
+            ->where($builder->expr()->eq('type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
162
+        return $this->getMountsFromQuery($query);
163
+    }
164
+
165
+    protected function getForQuery(IQueryBuilder $builder, $type, $value) {
166
+        $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
167
+            ->from('external_mounts', 'm')
168
+            ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
169
+            ->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
170
+
171
+        if (is_null($value)) {
172
+            $query = $query->andWhere($builder->expr()->isNull('a.value'));
173
+        } else {
174
+            $query = $query->andWhere($builder->expr()->eq('a.value', $builder->createNamedParameter($value)));
175
+        }
176
+
177
+        return $query;
178
+    }
179
+
180
+    /**
181
+     * Get mounts by applicable
182
+     *
183
+     * @param int $type any of the self::APPLICABLE_TYPE_ constants
184
+     * @param string|null $value user_id, group_id or null for global mounts
185
+     * @return array
186
+     */
187
+    public function getMountsFor($type, $value) {
188
+        $builder = $this->connection->getQueryBuilder();
189
+        $query = $this->getForQuery($builder, $type, $value);
190
+
191
+        return $this->getMountsFromQuery($query);
192
+    }
193
+
194
+    /**
195
+     * Get admin defined mounts by applicable
196
+     *
197
+     * @param int $type any of the self::APPLICABLE_TYPE_ constants
198
+     * @param string|null $value user_id, group_id or null for global mounts
199
+     * @return array
200
+     */
201
+    public function getAdminMountsFor($type, $value) {
202
+        $builder = $this->connection->getQueryBuilder();
203
+        $query = $this->getForQuery($builder, $type, $value);
204
+        $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
205
+
206
+        return $this->getMountsFromQuery($query);
207
+    }
208
+
209
+    /**
210
+     * Get admin defined mounts for multiple applicable
211
+     *
212
+     * @param int $type any of the self::APPLICABLE_TYPE_ constants
213
+     * @param string[] $values user_ids or group_ids
214
+     * @return array
215
+     */
216
+    public function getAdminMountsForMultiple($type, array $values) {
217
+        $builder = $this->connection->getQueryBuilder();
218
+        $params = array_map(function ($value) use ($builder) {
219
+            return $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR);
220
+        }, $values);
221
+
222
+        $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
223
+            ->from('external_mounts', 'm')
224
+            ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
225
+            ->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
226
+            ->andWhere($builder->expr()->in('a.value', $params));
227
+        $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
228
+
229
+        return $this->getMountsFromQuery($query);
230
+    }
231
+
232
+    /**
233
+     * Get user defined mounts by applicable
234
+     *
235
+     * @param int $type any of the self::APPLICABLE_TYPE_ constants
236
+     * @param string|null $value user_id, group_id or null for global mounts
237
+     * @return array
238
+     */
239
+    public function getUserMountsFor($type, $value) {
240
+        $builder = $this->connection->getQueryBuilder();
241
+        $query = $this->getForQuery($builder, $type, $value);
242
+        $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_PERSONAl, IQueryBuilder::PARAM_INT)));
243
+
244
+        return $this->getMountsFromQuery($query);
245
+    }
246
+
247
+    /**
248
+     * Add a mount to the database
249
+     *
250
+     * @param string $mountPoint
251
+     * @param string $storageBackend
252
+     * @param string $authBackend
253
+     * @param int $priority
254
+     * @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL
255
+     * @return int the id of the new mount
256
+     */
257
+    public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type) {
258
+        if (!$priority) {
259
+            $priority = 100;
260
+        }
261
+        $builder = $this->connection->getQueryBuilder();
262
+        $query = $builder->insert('external_mounts')
263
+            ->values([
264
+                'mount_point' => $builder->createNamedParameter($mountPoint, IQueryBuilder::PARAM_STR),
265
+                'storage_backend' => $builder->createNamedParameter($storageBackend, IQueryBuilder::PARAM_STR),
266
+                'auth_backend' => $builder->createNamedParameter($authBackend, IQueryBuilder::PARAM_STR),
267
+                'priority' => $builder->createNamedParameter($priority, IQueryBuilder::PARAM_INT),
268
+                'type' => $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)
269
+            ]);
270
+        $query->execute();
271
+        return (int)$this->connection->lastInsertId('*PREFIX*external_mounts');
272
+    }
273
+
274
+    /**
275
+     * Remove a mount from the database
276
+     *
277
+     * @param int $mountId
278
+     */
279
+    public function removeMount($mountId) {
280
+        $builder = $this->connection->getQueryBuilder();
281
+        $query = $builder->delete('external_mounts')
282
+            ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
283
+        $query->execute();
284
+
285
+        $query = $builder->delete('external_applicable')
286
+            ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
287
+        $query->execute();
288
+
289
+        $query = $builder->delete('external_config')
290
+            ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
291
+        $query->execute();
292
+
293
+        $query = $builder->delete('external_options')
294
+            ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
295
+        $query->execute();
296
+    }
297
+
298
+    /**
299
+     * @param int $mountId
300
+     * @param string $newMountPoint
301
+     */
302
+    public function setMountPoint($mountId, $newMountPoint) {
303
+        $builder = $this->connection->getQueryBuilder();
304
+
305
+        $query = $builder->update('external_mounts')
306
+            ->set('mount_point', $builder->createNamedParameter($newMountPoint))
307
+            ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
308
+
309
+        $query->execute();
310
+    }
311
+
312
+    /**
313
+     * @param int $mountId
314
+     * @param string $newAuthBackend
315
+     */
316
+    public function setAuthBackend($mountId, $newAuthBackend) {
317
+        $builder = $this->connection->getQueryBuilder();
318
+
319
+        $query = $builder->update('external_mounts')
320
+            ->set('auth_backend', $builder->createNamedParameter($newAuthBackend))
321
+            ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
322
+
323
+        $query->execute();
324
+    }
325
+
326
+    /**
327
+     * @param int $mountId
328
+     * @param string $key
329
+     * @param string $value
330
+     */
331
+    public function setConfig($mountId, $key, $value) {
332
+        if ($key === 'password') {
333
+            $value = $this->encryptValue($value);
334
+        }
335
+
336
+        try {
337
+            $builder = $this->connection->getQueryBuilder();
338
+            $builder->insert('external_config')
339
+                ->setValue('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))
340
+                ->setValue('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR))
341
+                ->setValue('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR))
342
+                ->execute();
343
+        } catch (UniqueConstraintViolationException $e) {
344
+            $builder = $this->connection->getQueryBuilder();
345
+            $query = $builder->update('external_config')
346
+                ->set('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR))
347
+                ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
348
+                ->andWhere($builder->expr()->eq('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR)));
349
+            $query->execute();
350
+        }
351
+    }
352
+
353
+    /**
354
+     * @param int $mountId
355
+     * @param string $key
356
+     * @param string $value
357
+     */
358
+    public function setOption($mountId, $key, $value) {
359
+        try {
360
+            $builder = $this->connection->getQueryBuilder();
361
+            $builder->insert('external_options')
362
+                ->setValue('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))
363
+                ->setValue('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR))
364
+                ->setValue('value', $builder->createNamedParameter(json_encode($value), IQueryBuilder::PARAM_STR))
365
+                ->execute();
366
+        } catch (UniqueConstraintViolationException $e) {
367
+            $builder = $this->connection->getQueryBuilder();
368
+            $query = $builder->update('external_options')
369
+                ->set('value', $builder->createNamedParameter(json_encode($value), IQueryBuilder::PARAM_STR))
370
+                ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
371
+                ->andWhere($builder->expr()->eq('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR)));
372
+            $query->execute();
373
+        }
374
+    }
375
+
376
+    public function addApplicable($mountId, $type, $value) {
377
+        try {
378
+            $builder = $this->connection->getQueryBuilder();
379
+            $builder->insert('external_applicable')
380
+                ->setValue('mount_id', $builder->createNamedParameter($mountId))
381
+                ->setValue('type', $builder->createNamedParameter($type))
382
+                ->setValue('value', $builder->createNamedParameter($value))
383
+                ->execute();
384
+        } catch (UniqueConstraintViolationException $e) {
385
+            // applicable exists already
386
+        }
387
+    }
388
+
389
+    public function removeApplicable($mountId, $type, $value) {
390
+        $builder = $this->connection->getQueryBuilder();
391
+        $query = $builder->delete('external_applicable')
392
+            ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
393
+            ->andWhere($builder->expr()->eq('type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
394
+
395
+        if (is_null($value)) {
396
+            $query = $query->andWhere($builder->expr()->isNull('value'));
397
+        } else {
398
+            $query = $query->andWhere($builder->expr()->eq('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR)));
399
+        }
400
+
401
+        $query->execute();
402
+    }
403
+
404
+    private function getMountsFromQuery(IQueryBuilder $query) {
405
+        $result = $query->execute();
406
+        $mounts = $result->fetchAll();
407
+        $uniqueMounts = [];
408
+        foreach ($mounts as $mount) {
409
+            $id = $mount['mount_id'];
410
+            if (!isset($uniqueMounts[$id])) {
411
+                $uniqueMounts[$id] = $mount;
412
+            }
413
+        }
414
+        $uniqueMounts = array_values($uniqueMounts);
415
+
416
+        $mountIds = array_map(function ($mount) {
417
+            return $mount['mount_id'];
418
+        }, $uniqueMounts);
419
+        $mountIds = array_values(array_unique($mountIds));
420
+
421
+        $applicable = $this->getApplicableForMounts($mountIds);
422
+        $config = $this->getConfigForMounts($mountIds);
423
+        $options = $this->getOptionsForMounts($mountIds);
424
+
425
+        return array_map(function ($mount, $applicable, $config, $options) {
426
+            $mount['type'] = (int)$mount['type'];
427
+            $mount['priority'] = (int)$mount['priority'];
428
+            $mount['applicable'] = $applicable;
429
+            $mount['config'] = $config;
430
+            $mount['options'] = $options;
431
+            return $mount;
432
+        }, $uniqueMounts, $applicable, $config, $options);
433
+    }
434
+
435
+    /**
436
+     * Get mount options from a table grouped by mount id
437
+     *
438
+     * @param string $table
439
+     * @param string[] $fields
440
+     * @param int[] $mountIds
441
+     * @return array [$mountId => [['field1' => $value1, ...], ...], ...]
442
+     */
443
+    private function selectForMounts($table, array $fields, array $mountIds) {
444
+        if (count($mountIds) === 0) {
445
+            return [];
446
+        }
447
+        $builder = $this->connection->getQueryBuilder();
448
+        $fields[] = 'mount_id';
449
+        $placeHolders = array_map(function ($id) use ($builder) {
450
+            return $builder->createPositionalParameter($id, IQueryBuilder::PARAM_INT);
451
+        }, $mountIds);
452
+        $query = $builder->select($fields)
453
+            ->from($table)
454
+            ->where($builder->expr()->in('mount_id', $placeHolders));
455
+        $rows = $query->execute()->fetchAll();
456
+
457
+        $result = [];
458
+        foreach ($mountIds as $mountId) {
459
+            $result[$mountId] = [];
460
+        }
461
+        foreach ($rows as $row) {
462
+            if (isset($row['type'])) {
463
+                $row['type'] = (int)$row['type'];
464
+            }
465
+            $result[$row['mount_id']][] = $row;
466
+        }
467
+        return $result;
468
+    }
469
+
470
+    /**
471
+     * @param int[] $mountIds
472
+     * @return array [$id => [['type' => $type, 'value' => $value], ...], ...]
473
+     */
474
+    public function getApplicableForMounts($mountIds) {
475
+        return $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds);
476
+    }
477
+
478
+    /**
479
+     * @param int[] $mountIds
480
+     * @return array [$id => ['key1' => $value1, ...], ...]
481
+     */
482
+    public function getConfigForMounts($mountIds) {
483
+        $mountConfigs = $this->selectForMounts('external_config', ['key', 'value'], $mountIds);
484
+        return array_map([$this, 'createKeyValueMap'], $mountConfigs);
485
+    }
486
+
487
+    /**
488
+     * @param int[] $mountIds
489
+     * @return array [$id => ['key1' => $value1, ...], ...]
490
+     */
491
+    public function getOptionsForMounts($mountIds) {
492
+        $mountOptions = $this->selectForMounts('external_options', ['key', 'value'], $mountIds);
493
+        $optionsMap = array_map([$this, 'createKeyValueMap'], $mountOptions);
494
+        return array_map(function (array $options) {
495
+            return array_map(function ($option) {
496
+                return json_decode($option);
497
+            }, $options);
498
+        }, $optionsMap);
499
+    }
500
+
501
+    /**
502
+     * @param array $keyValuePairs [['key'=>$key, 'value=>$value], ...]
503
+     * @return array ['key1' => $value1, ...]
504
+     */
505
+    private function createKeyValueMap(array $keyValuePairs) {
506
+        $decryptedPairts = array_map(function ($pair) {
507
+            if ($pair['key'] === 'password') {
508
+                $pair['value'] = $this->decryptValue($pair['value']);
509
+            }
510
+            return $pair;
511
+        }, $keyValuePairs);
512
+        $keys = array_map(function ($pair) {
513
+            return $pair['key'];
514
+        }, $decryptedPairts);
515
+        $values = array_map(function ($pair) {
516
+            return $pair['value'];
517
+        }, $decryptedPairts);
518
+
519
+        return array_combine($keys, $values);
520
+    }
521
+
522
+    private function encryptValue($value) {
523
+        return $this->crypto->encrypt($value);
524
+    }
525
+
526
+    private function decryptValue($value) {
527
+        try {
528
+            return $this->crypto->decrypt($value);
529
+        } catch (\Exception $e) {
530
+            return $value;
531
+        }
532
+    }
533 533
 }
Please login to merge, or discard this patch.
apps/dav/lib/CardDAV/CardDavBackend.php 2 patches
Indentation   +1299 added lines, -1299 removed lines patch added patch discarded remove patch
@@ -62,1303 +62,1303 @@
 block discarded – undo
62 62
 use Symfony\Component\EventDispatcher\GenericEvent;
63 63
 
64 64
 class CardDavBackend implements BackendInterface, SyncSupport {
65
-	public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
66
-	public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
67
-
68
-	/** @var Principal */
69
-	private $principalBackend;
70
-
71
-	/** @var string */
72
-	private $dbCardsTable = 'cards';
73
-
74
-	/** @var string */
75
-	private $dbCardsPropertiesTable = 'cards_properties';
76
-
77
-	/** @var IDBConnection */
78
-	private $db;
79
-
80
-	/** @var Backend */
81
-	private $sharingBackend;
82
-
83
-	/** @var array properties to index */
84
-	public static $indexProperties = [
85
-		'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
86
-		'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'];
87
-
88
-	/**
89
-	 * @var string[] Map of uid => display name
90
-	 */
91
-	protected $userDisplayNames;
92
-
93
-	/** @var IUserManager */
94
-	private $userManager;
95
-
96
-	/** @var IEventDispatcher */
97
-	private $dispatcher;
98
-
99
-	/** @var EventDispatcherInterface */
100
-	private $legacyDispatcher;
101
-
102
-	private $etagCache = [];
103
-
104
-	/**
105
-	 * CardDavBackend constructor.
106
-	 *
107
-	 * @param IDBConnection $db
108
-	 * @param Principal $principalBackend
109
-	 * @param IUserManager $userManager
110
-	 * @param IGroupManager $groupManager
111
-	 * @param IEventDispatcher $dispatcher
112
-	 * @param EventDispatcherInterface $legacyDispatcher
113
-	 */
114
-	public function __construct(IDBConnection $db,
115
-								Principal $principalBackend,
116
-								IUserManager $userManager,
117
-								IGroupManager $groupManager,
118
-								IEventDispatcher $dispatcher,
119
-								EventDispatcherInterface $legacyDispatcher) {
120
-		$this->db = $db;
121
-		$this->principalBackend = $principalBackend;
122
-		$this->userManager = $userManager;
123
-		$this->dispatcher = $dispatcher;
124
-		$this->legacyDispatcher = $legacyDispatcher;
125
-		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
126
-	}
127
-
128
-	/**
129
-	 * Return the number of address books for a principal
130
-	 *
131
-	 * @param $principalUri
132
-	 * @return int
133
-	 */
134
-	public function getAddressBooksForUserCount($principalUri) {
135
-		$principalUri = $this->convertPrincipal($principalUri, true);
136
-		$query = $this->db->getQueryBuilder();
137
-		$query->select($query->func()->count('*'))
138
-			->from('addressbooks')
139
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
140
-
141
-		return (int)$query->execute()->fetchColumn();
142
-	}
143
-
144
-	/**
145
-	 * Returns the list of address books for a specific user.
146
-	 *
147
-	 * Every addressbook should have the following properties:
148
-	 *   id - an arbitrary unique id
149
-	 *   uri - the 'basename' part of the url
150
-	 *   principaluri - Same as the passed parameter
151
-	 *
152
-	 * Any additional clark-notation property may be passed besides this. Some
153
-	 * common ones are :
154
-	 *   {DAV:}displayname
155
-	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
156
-	 *   {http://calendarserver.org/ns/}getctag
157
-	 *
158
-	 * @param string $principalUri
159
-	 * @return array
160
-	 */
161
-	public function getAddressBooksForUser($principalUri) {
162
-		$principalUriOriginal = $principalUri;
163
-		$principalUri = $this->convertPrincipal($principalUri, true);
164
-		$query = $this->db->getQueryBuilder();
165
-		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
166
-			->from('addressbooks')
167
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
168
-
169
-		$addressBooks = [];
170
-
171
-		$result = $query->execute();
172
-		while ($row = $result->fetch()) {
173
-			$addressBooks[$row['id']] = [
174
-				'id' => $row['id'],
175
-				'uri' => $row['uri'],
176
-				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
177
-				'{DAV:}displayname' => $row['displayname'],
178
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
179
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
180
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
181
-			];
182
-
183
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
184
-		}
185
-		$result->closeCursor();
186
-
187
-		// query for shared addressbooks
188
-		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
189
-		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
190
-
191
-		$principals = array_map(function ($principal) {
192
-			return urldecode($principal);
193
-		}, $principals);
194
-		$principals[] = $principalUri;
195
-
196
-		$query = $this->db->getQueryBuilder();
197
-		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
198
-			->from('dav_shares', 's')
199
-			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
200
-			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
201
-			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
202
-			->setParameter('type', 'addressbook')
203
-			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
204
-			->execute();
205
-
206
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
207
-		while ($row = $result->fetch()) {
208
-			if ($row['principaluri'] === $principalUri) {
209
-				continue;
210
-			}
211
-
212
-			$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
213
-			if (isset($addressBooks[$row['id']])) {
214
-				if ($readOnly) {
215
-					// New share can not have more permissions then the old one.
216
-					continue;
217
-				}
218
-				if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
219
-					$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
220
-					// Old share is already read-write, no more permissions can be gained
221
-					continue;
222
-				}
223
-			}
224
-
225
-			list(, $name) = \Sabre\Uri\split($row['principaluri']);
226
-			$uri = $row['uri'] . '_shared_by_' . $name;
227
-			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
228
-
229
-			$addressBooks[$row['id']] = [
230
-				'id' => $row['id'],
231
-				'uri' => $uri,
232
-				'principaluri' => $principalUriOriginal,
233
-				'{DAV:}displayname' => $displayName,
234
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
235
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
236
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
237
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
238
-				$readOnlyPropertyName => $readOnly,
239
-			];
240
-
241
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
242
-		}
243
-		$result->closeCursor();
244
-
245
-		return array_values($addressBooks);
246
-	}
247
-
248
-	public function getUsersOwnAddressBooks($principalUri) {
249
-		$principalUri = $this->convertPrincipal($principalUri, true);
250
-		$query = $this->db->getQueryBuilder();
251
-		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
252
-			->from('addressbooks')
253
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
254
-
255
-		$addressBooks = [];
256
-
257
-		$result = $query->execute();
258
-		while ($row = $result->fetch()) {
259
-			$addressBooks[$row['id']] = [
260
-				'id' => $row['id'],
261
-				'uri' => $row['uri'],
262
-				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
263
-				'{DAV:}displayname' => $row['displayname'],
264
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
265
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
266
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
267
-			];
268
-
269
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
270
-		}
271
-		$result->closeCursor();
272
-
273
-		return array_values($addressBooks);
274
-	}
275
-
276
-	private function getUserDisplayName($uid) {
277
-		if (!isset($this->userDisplayNames[$uid])) {
278
-			$user = $this->userManager->get($uid);
279
-
280
-			if ($user instanceof IUser) {
281
-				$this->userDisplayNames[$uid] = $user->getDisplayName();
282
-			} else {
283
-				$this->userDisplayNames[$uid] = $uid;
284
-			}
285
-		}
286
-
287
-		return $this->userDisplayNames[$uid];
288
-	}
289
-
290
-	/**
291
-	 * @param int $addressBookId
292
-	 */
293
-	public function getAddressBookById($addressBookId) {
294
-		$query = $this->db->getQueryBuilder();
295
-		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
296
-			->from('addressbooks')
297
-			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
298
-			->execute();
299
-
300
-		$row = $result->fetch();
301
-		$result->closeCursor();
302
-		if ($row === false) {
303
-			return null;
304
-		}
305
-
306
-		$addressBook = [
307
-			'id' => $row['id'],
308
-			'uri' => $row['uri'],
309
-			'principaluri' => $row['principaluri'],
310
-			'{DAV:}displayname' => $row['displayname'],
311
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
312
-			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
313
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
314
-		];
315
-
316
-		$this->addOwnerPrincipal($addressBook);
317
-
318
-		return $addressBook;
319
-	}
320
-
321
-	/**
322
-	 * @param $addressBookUri
323
-	 * @return array|null
324
-	 */
325
-	public function getAddressBooksByUri($principal, $addressBookUri) {
326
-		$query = $this->db->getQueryBuilder();
327
-		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
328
-			->from('addressbooks')
329
-			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
330
-			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
331
-			->setMaxResults(1)
332
-			->execute();
333
-
334
-		$row = $result->fetch();
335
-		$result->closeCursor();
336
-		if ($row === false) {
337
-			return null;
338
-		}
339
-
340
-		$addressBook = [
341
-			'id' => $row['id'],
342
-			'uri' => $row['uri'],
343
-			'principaluri' => $row['principaluri'],
344
-			'{DAV:}displayname' => $row['displayname'],
345
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
346
-			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
347
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
348
-		];
349
-
350
-		$this->addOwnerPrincipal($addressBook);
351
-
352
-		return $addressBook;
353
-	}
354
-
355
-	/**
356
-	 * Updates properties for an address book.
357
-	 *
358
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
359
-	 * To do the actual updates, you must tell this object which properties
360
-	 * you're going to process with the handle() method.
361
-	 *
362
-	 * Calling the handle method is like telling the PropPatch object "I
363
-	 * promise I can handle updating this property".
364
-	 *
365
-	 * Read the PropPatch documentation for more info and examples.
366
-	 *
367
-	 * @param string $addressBookId
368
-	 * @param \Sabre\DAV\PropPatch $propPatch
369
-	 * @return void
370
-	 */
371
-	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
372
-		$supportedProperties = [
373
-			'{DAV:}displayname',
374
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
375
-		];
376
-
377
-		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
378
-			$updates = [];
379
-			foreach ($mutations as $property => $newValue) {
380
-				switch ($property) {
381
-					case '{DAV:}displayname':
382
-						$updates['displayname'] = $newValue;
383
-						break;
384
-					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
385
-						$updates['description'] = $newValue;
386
-						break;
387
-				}
388
-			}
389
-			$query = $this->db->getQueryBuilder();
390
-			$query->update('addressbooks');
391
-
392
-			foreach ($updates as $key => $value) {
393
-				$query->set($key, $query->createNamedParameter($value));
394
-			}
395
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
396
-				->execute();
397
-
398
-			$this->addChange($addressBookId, "", 2);
399
-
400
-			$addressBookRow = $this->getAddressBookById((int)$addressBookId);
401
-			$shares = $this->getShares($addressBookId);
402
-			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
403
-
404
-			return true;
405
-		});
406
-	}
407
-
408
-	/**
409
-	 * Creates a new address book
410
-	 *
411
-	 * @param string $principalUri
412
-	 * @param string $url Just the 'basename' of the url.
413
-	 * @param array $properties
414
-	 * @return int
415
-	 * @throws BadRequest
416
-	 */
417
-	public function createAddressBook($principalUri, $url, array $properties) {
418
-		$values = [
419
-			'displayname' => null,
420
-			'description' => null,
421
-			'principaluri' => $principalUri,
422
-			'uri' => $url,
423
-			'synctoken' => 1
424
-		];
425
-
426
-		foreach ($properties as $property => $newValue) {
427
-			switch ($property) {
428
-				case '{DAV:}displayname':
429
-					$values['displayname'] = $newValue;
430
-					break;
431
-				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
432
-					$values['description'] = $newValue;
433
-					break;
434
-				default:
435
-					throw new BadRequest('Unknown property: ' . $property);
436
-			}
437
-		}
438
-
439
-		// Fallback to make sure the displayname is set. Some clients may refuse
440
-		// to work with addressbooks not having a displayname.
441
-		if (is_null($values['displayname'])) {
442
-			$values['displayname'] = $url;
443
-		}
444
-
445
-		$query = $this->db->getQueryBuilder();
446
-		$query->insert('addressbooks')
447
-			->values([
448
-				'uri' => $query->createParameter('uri'),
449
-				'displayname' => $query->createParameter('displayname'),
450
-				'description' => $query->createParameter('description'),
451
-				'principaluri' => $query->createParameter('principaluri'),
452
-				'synctoken' => $query->createParameter('synctoken'),
453
-			])
454
-			->setParameters($values)
455
-			->execute();
456
-
457
-		$addressBookId = $query->getLastInsertId();
458
-		$addressBookRow = $this->getAddressBookById($addressBookId);
459
-		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
460
-
461
-		return $addressBookId;
462
-	}
463
-
464
-	/**
465
-	 * Deletes an entire addressbook and all its contents
466
-	 *
467
-	 * @param mixed $addressBookId
468
-	 * @return void
469
-	 */
470
-	public function deleteAddressBook($addressBookId) {
471
-		$addressBookData = $this->getAddressBookById($addressBookId);
472
-		$shares = $this->getShares($addressBookId);
473
-
474
-		$query = $this->db->getQueryBuilder();
475
-		$query->delete($this->dbCardsTable)
476
-			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
477
-			->setParameter('addressbookid', $addressBookId)
478
-			->execute();
479
-
480
-		$query->delete('addressbookchanges')
481
-			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
482
-			->setParameter('addressbookid', $addressBookId)
483
-			->execute();
484
-
485
-		$query->delete('addressbooks')
486
-			->where($query->expr()->eq('id', $query->createParameter('id')))
487
-			->setParameter('id', $addressBookId)
488
-			->execute();
489
-
490
-		$this->sharingBackend->deleteAllShares($addressBookId);
491
-
492
-		$query->delete($this->dbCardsPropertiesTable)
493
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
494
-			->execute();
495
-
496
-		if ($addressBookData) {
497
-			$this->dispatcher->dispatchTyped(new AddressBookDeletedEvent((int) $addressBookId, $addressBookData, $shares));
498
-		}
499
-	}
500
-
501
-	/**
502
-	 * Returns all cards for a specific addressbook id.
503
-	 *
504
-	 * This method should return the following properties for each card:
505
-	 *   * carddata - raw vcard data
506
-	 *   * uri - Some unique url
507
-	 *   * lastmodified - A unix timestamp
508
-	 *
509
-	 * It's recommended to also return the following properties:
510
-	 *   * etag - A unique etag. This must change every time the card changes.
511
-	 *   * size - The size of the card in bytes.
512
-	 *
513
-	 * If these last two properties are provided, less time will be spent
514
-	 * calculating them. If they are specified, you can also ommit carddata.
515
-	 * This may speed up certain requests, especially with large cards.
516
-	 *
517
-	 * @param mixed $addressBookId
518
-	 * @return array
519
-	 */
520
-	public function getCards($addressBookId) {
521
-		$query = $this->db->getQueryBuilder();
522
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
523
-			->from($this->dbCardsTable)
524
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
525
-
526
-		$cards = [];
527
-
528
-		$result = $query->execute();
529
-		while ($row = $result->fetch()) {
530
-			$row['etag'] = '"' . $row['etag'] . '"';
531
-
532
-			$modified = false;
533
-			$row['carddata'] = $this->readBlob($row['carddata'], $modified);
534
-			if ($modified) {
535
-				$row['size'] = strlen($row['carddata']);
536
-			}
537
-
538
-			$cards[] = $row;
539
-		}
540
-		$result->closeCursor();
541
-
542
-		return $cards;
543
-	}
544
-
545
-	/**
546
-	 * Returns a specific card.
547
-	 *
548
-	 * The same set of properties must be returned as with getCards. The only
549
-	 * exception is that 'carddata' is absolutely required.
550
-	 *
551
-	 * If the card does not exist, you must return false.
552
-	 *
553
-	 * @param mixed $addressBookId
554
-	 * @param string $cardUri
555
-	 * @return array
556
-	 */
557
-	public function getCard($addressBookId, $cardUri) {
558
-		$query = $this->db->getQueryBuilder();
559
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
560
-			->from($this->dbCardsTable)
561
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
562
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
563
-			->setMaxResults(1);
564
-
565
-		$result = $query->execute();
566
-		$row = $result->fetch();
567
-		if (!$row) {
568
-			return false;
569
-		}
570
-		$row['etag'] = '"' . $row['etag'] . '"';
571
-
572
-		$modified = false;
573
-		$row['carddata'] = $this->readBlob($row['carddata'], $modified);
574
-		if ($modified) {
575
-			$row['size'] = strlen($row['carddata']);
576
-		}
577
-
578
-		return $row;
579
-	}
580
-
581
-	/**
582
-	 * Returns a list of cards.
583
-	 *
584
-	 * This method should work identical to getCard, but instead return all the
585
-	 * cards in the list as an array.
586
-	 *
587
-	 * If the backend supports this, it may allow for some speed-ups.
588
-	 *
589
-	 * @param mixed $addressBookId
590
-	 * @param string[] $uris
591
-	 * @return array
592
-	 */
593
-	public function getMultipleCards($addressBookId, array $uris) {
594
-		if (empty($uris)) {
595
-			return [];
596
-		}
597
-
598
-		$chunks = array_chunk($uris, 100);
599
-		$cards = [];
600
-
601
-		$query = $this->db->getQueryBuilder();
602
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
603
-			->from($this->dbCardsTable)
604
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
605
-			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
606
-
607
-		foreach ($chunks as $uris) {
608
-			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
609
-			$result = $query->execute();
610
-
611
-			while ($row = $result->fetch()) {
612
-				$row['etag'] = '"' . $row['etag'] . '"';
613
-
614
-				$modified = false;
615
-				$row['carddata'] = $this->readBlob($row['carddata'], $modified);
616
-				if ($modified) {
617
-					$row['size'] = strlen($row['carddata']);
618
-				}
619
-
620
-				$cards[] = $row;
621
-			}
622
-			$result->closeCursor();
623
-		}
624
-		return $cards;
625
-	}
626
-
627
-	/**
628
-	 * Creates a new card.
629
-	 *
630
-	 * The addressbook id will be passed as the first argument. This is the
631
-	 * same id as it is returned from the getAddressBooksForUser method.
632
-	 *
633
-	 * The cardUri is a base uri, and doesn't include the full path. The
634
-	 * cardData argument is the vcard body, and is passed as a string.
635
-	 *
636
-	 * It is possible to return an ETag from this method. This ETag is for the
637
-	 * newly created resource, and must be enclosed with double quotes (that
638
-	 * is, the string itself must contain the double quotes).
639
-	 *
640
-	 * You should only return the ETag if you store the carddata as-is. If a
641
-	 * subsequent GET request on the same card does not have the same body,
642
-	 * byte-by-byte and you did return an ETag here, clients tend to get
643
-	 * confused.
644
-	 *
645
-	 * If you don't return an ETag, you can just return null.
646
-	 *
647
-	 * @param mixed $addressBookId
648
-	 * @param string $cardUri
649
-	 * @param string $cardData
650
-	 * @return string
651
-	 */
652
-	public function createCard($addressBookId, $cardUri, $cardData) {
653
-		$etag = md5($cardData);
654
-		$uid = $this->getUID($cardData);
655
-
656
-		$q = $this->db->getQueryBuilder();
657
-		$q->select('uid')
658
-			->from($this->dbCardsTable)
659
-			->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
660
-			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
661
-			->setMaxResults(1);
662
-		$result = $q->execute();
663
-		$count = (bool)$result->fetchColumn();
664
-		$result->closeCursor();
665
-		if ($count) {
666
-			throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
667
-		}
668
-
669
-		$query = $this->db->getQueryBuilder();
670
-		$query->insert('cards')
671
-			->values([
672
-				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
673
-				'uri' => $query->createNamedParameter($cardUri),
674
-				'lastmodified' => $query->createNamedParameter(time()),
675
-				'addressbookid' => $query->createNamedParameter($addressBookId),
676
-				'size' => $query->createNamedParameter(strlen($cardData)),
677
-				'etag' => $query->createNamedParameter($etag),
678
-				'uid' => $query->createNamedParameter($uid),
679
-			])
680
-			->execute();
681
-
682
-		$etagCacheKey = "$addressBookId#$cardUri";
683
-		$this->etagCache[$etagCacheKey] = $etag;
684
-
685
-		$this->addChange($addressBookId, $cardUri, 1);
686
-		$this->updateProperties($addressBookId, $cardUri, $cardData);
687
-
688
-		$addressBookData = $this->getAddressBookById($addressBookId);
689
-		$shares = $this->getShares($addressBookId);
690
-		$objectRow = $this->getCard($addressBookId, $cardUri);
691
-		$this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
692
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
693
-			new GenericEvent(null, [
694
-				'addressBookId' => $addressBookId,
695
-				'cardUri' => $cardUri,
696
-				'cardData' => $cardData]));
697
-
698
-		return '"' . $etag . '"';
699
-	}
700
-
701
-	/**
702
-	 * Updates a card.
703
-	 *
704
-	 * The addressbook id will be passed as the first argument. This is the
705
-	 * same id as it is returned from the getAddressBooksForUser method.
706
-	 *
707
-	 * The cardUri is a base uri, and doesn't include the full path. The
708
-	 * cardData argument is the vcard body, and is passed as a string.
709
-	 *
710
-	 * It is possible to return an ETag from this method. This ETag should
711
-	 * match that of the updated resource, and must be enclosed with double
712
-	 * quotes (that is: the string itself must contain the actual quotes).
713
-	 *
714
-	 * You should only return the ETag if you store the carddata as-is. If a
715
-	 * subsequent GET request on the same card does not have the same body,
716
-	 * byte-by-byte and you did return an ETag here, clients tend to get
717
-	 * confused.
718
-	 *
719
-	 * If you don't return an ETag, you can just return null.
720
-	 *
721
-	 * @param mixed $addressBookId
722
-	 * @param string $cardUri
723
-	 * @param string $cardData
724
-	 * @return string
725
-	 */
726
-	public function updateCard($addressBookId, $cardUri, $cardData) {
727
-		$uid = $this->getUID($cardData);
728
-		$etag = md5($cardData);
729
-		$query = $this->db->getQueryBuilder();
730
-
731
-		// check for recently stored etag and stop if it is the same
732
-		$etagCacheKey = "$addressBookId#$cardUri";
733
-		if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
734
-			return '"' . $etag . '"';
735
-		}
736
-
737
-		$query->update($this->dbCardsTable)
738
-			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
739
-			->set('lastmodified', $query->createNamedParameter(time()))
740
-			->set('size', $query->createNamedParameter(strlen($cardData)))
741
-			->set('etag', $query->createNamedParameter($etag))
742
-			->set('uid', $query->createNamedParameter($uid))
743
-			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
744
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
745
-			->execute();
746
-
747
-		$this->etagCache[$etagCacheKey] = $etag;
748
-
749
-		$this->addChange($addressBookId, $cardUri, 2);
750
-		$this->updateProperties($addressBookId, $cardUri, $cardData);
751
-
752
-		$addressBookData = $this->getAddressBookById($addressBookId);
753
-		$shares = $this->getShares($addressBookId);
754
-		$objectRow = $this->getCard($addressBookId, $cardUri);
755
-		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
756
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
757
-			new GenericEvent(null, [
758
-				'addressBookId' => $addressBookId,
759
-				'cardUri' => $cardUri,
760
-				'cardData' => $cardData]));
761
-
762
-		return '"' . $etag . '"';
763
-	}
764
-
765
-	/**
766
-	 * Deletes a card
767
-	 *
768
-	 * @param mixed $addressBookId
769
-	 * @param string $cardUri
770
-	 * @return bool
771
-	 */
772
-	public function deleteCard($addressBookId, $cardUri) {
773
-		$addressBookData = $this->getAddressBookById($addressBookId);
774
-		$shares = $this->getShares($addressBookId);
775
-		$objectRow = $this->getCard($addressBookId, $cardUri);
776
-
777
-		try {
778
-			$cardId = $this->getCardId($addressBookId, $cardUri);
779
-		} catch (\InvalidArgumentException $e) {
780
-			$cardId = null;
781
-		}
782
-		$query = $this->db->getQueryBuilder();
783
-		$ret = $query->delete($this->dbCardsTable)
784
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
785
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
786
-			->execute();
787
-
788
-		$this->addChange($addressBookId, $cardUri, 3);
789
-
790
-		if ($ret === 1) {
791
-			if ($cardId !== null) {
792
-				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
793
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
794
-					new GenericEvent(null, [
795
-						'addressBookId' => $addressBookId,
796
-						'cardUri' => $cardUri]));
797
-
798
-				$this->purgeProperties($addressBookId, $cardId);
799
-			}
800
-			return true;
801
-		}
802
-
803
-		return false;
804
-	}
805
-
806
-	/**
807
-	 * The getChanges method returns all the changes that have happened, since
808
-	 * the specified syncToken in the specified address book.
809
-	 *
810
-	 * This function should return an array, such as the following:
811
-	 *
812
-	 * [
813
-	 *   'syncToken' => 'The current synctoken',
814
-	 *   'added'   => [
815
-	 *      'new.txt',
816
-	 *   ],
817
-	 *   'modified'   => [
818
-	 *      'modified.txt',
819
-	 *   ],
820
-	 *   'deleted' => [
821
-	 *      'foo.php.bak',
822
-	 *      'old.txt'
823
-	 *   ]
824
-	 * ];
825
-	 *
826
-	 * The returned syncToken property should reflect the *current* syncToken
827
-	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
828
-	 * property. This is needed here too, to ensure the operation is atomic.
829
-	 *
830
-	 * If the $syncToken argument is specified as null, this is an initial
831
-	 * sync, and all members should be reported.
832
-	 *
833
-	 * The modified property is an array of nodenames that have changed since
834
-	 * the last token.
835
-	 *
836
-	 * The deleted property is an array with nodenames, that have been deleted
837
-	 * from collection.
838
-	 *
839
-	 * The $syncLevel argument is basically the 'depth' of the report. If it's
840
-	 * 1, you only have to report changes that happened only directly in
841
-	 * immediate descendants. If it's 2, it should also include changes from
842
-	 * the nodes below the child collections. (grandchildren)
843
-	 *
844
-	 * The $limit argument allows a client to specify how many results should
845
-	 * be returned at most. If the limit is not specified, it should be treated
846
-	 * as infinite.
847
-	 *
848
-	 * If the limit (infinite or not) is higher than you're willing to return,
849
-	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
850
-	 *
851
-	 * If the syncToken is expired (due to data cleanup) or unknown, you must
852
-	 * return null.
853
-	 *
854
-	 * The limit is 'suggestive'. You are free to ignore it.
855
-	 *
856
-	 * @param string $addressBookId
857
-	 * @param string $syncToken
858
-	 * @param int $syncLevel
859
-	 * @param int $limit
860
-	 * @return array
861
-	 */
862
-	public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
863
-		// Current synctoken
864
-		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
865
-		$stmt->execute([$addressBookId]);
866
-		$currentToken = $stmt->fetchColumn(0);
867
-
868
-		if (is_null($currentToken)) {
869
-			return null;
870
-		}
871
-
872
-		$result = [
873
-			'syncToken' => $currentToken,
874
-			'added' => [],
875
-			'modified' => [],
876
-			'deleted' => [],
877
-		];
878
-
879
-		if ($syncToken) {
880
-			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
881
-			if ($limit > 0) {
882
-				$query .= " LIMIT " . (int)$limit;
883
-			}
884
-
885
-			// Fetching all changes
886
-			$stmt = $this->db->prepare($query);
887
-			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
888
-
889
-			$changes = [];
890
-
891
-			// This loop ensures that any duplicates are overwritten, only the
892
-			// last change on a node is relevant.
893
-			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
894
-				$changes[$row['uri']] = $row['operation'];
895
-			}
896
-
897
-			foreach ($changes as $uri => $operation) {
898
-				switch ($operation) {
899
-					case 1:
900
-						$result['added'][] = $uri;
901
-						break;
902
-					case 2:
903
-						$result['modified'][] = $uri;
904
-						break;
905
-					case 3:
906
-						$result['deleted'][] = $uri;
907
-						break;
908
-				}
909
-			}
910
-		} else {
911
-			// No synctoken supplied, this is the initial sync.
912
-			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
913
-			$stmt = $this->db->prepare($query);
914
-			$stmt->execute([$addressBookId]);
915
-
916
-			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
917
-		}
918
-		return $result;
919
-	}
920
-
921
-	/**
922
-	 * Adds a change record to the addressbookchanges table.
923
-	 *
924
-	 * @param mixed $addressBookId
925
-	 * @param string $objectUri
926
-	 * @param int $operation 1 = add, 2 = modify, 3 = delete
927
-	 * @return void
928
-	 */
929
-	protected function addChange($addressBookId, $objectUri, $operation) {
930
-		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
931
-		$stmt = $this->db->prepare($sql);
932
-		$stmt->execute([
933
-			$objectUri,
934
-			$addressBookId,
935
-			$operation,
936
-			$addressBookId
937
-		]);
938
-		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
939
-		$stmt->execute([
940
-			$addressBookId
941
-		]);
942
-	}
943
-
944
-	/**
945
-	 * @param resource|string $cardData
946
-	 * @param bool $modified
947
-	 * @return string
948
-	 */
949
-	private function readBlob($cardData, &$modified = false) {
950
-		if (is_resource($cardData)) {
951
-			$cardData = stream_get_contents($cardData);
952
-		}
953
-
954
-		$cardDataArray = explode("\r\n", $cardData);
955
-
956
-		$cardDataFiltered = [];
957
-		$removingPhoto = false;
958
-		foreach ($cardDataArray as $line) {
959
-			if (strpos($line, 'PHOTO:data:') === 0
960
-				&& strpos($line, 'PHOTO:data:image/') !== 0) {
961
-				// Filter out PHOTO data of non-images
962
-				$removingPhoto = true;
963
-				$modified = true;
964
-				continue;
965
-			}
966
-
967
-			if ($removingPhoto) {
968
-				if (strpos($line, ' ') === 0) {
969
-					continue;
970
-				}
971
-				// No leading space means this is a new property
972
-				$removingPhoto = false;
973
-			}
974
-
975
-			$cardDataFiltered[] = $line;
976
-		}
977
-
978
-		return implode("\r\n", $cardDataFiltered);
979
-	}
980
-
981
-	/**
982
-	 * @param IShareable $shareable
983
-	 * @param string[] $add
984
-	 * @param string[] $remove
985
-	 */
986
-	public function updateShares(IShareable $shareable, $add, $remove) {
987
-		$addressBookId = $shareable->getResourceId();
988
-		$addressBookData = $this->getAddressBookById($addressBookId);
989
-		$oldShares = $this->getShares($addressBookId);
990
-
991
-		$this->sharingBackend->updateShares($shareable, $add, $remove);
992
-
993
-		$this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
994
-	}
995
-
996
-	/**
997
-	 * Search contacts in a specific address-book
998
-	 *
999
-	 * @param int $addressBookId
1000
-	 * @param string $pattern which should match within the $searchProperties
1001
-	 * @param array $searchProperties defines the properties within the query pattern should match
1002
-	 * @param array $options = array() to define the search behavior
1003
-	 *    - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
1004
-	 *    - 'limit' - Set a numeric limit for the search results
1005
-	 *    - 'offset' - Set the offset for the limited search results
1006
-	 * @return array an array of contacts which are arrays of key-value-pairs
1007
-	 */
1008
-	public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
1009
-		return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
1010
-	}
1011
-
1012
-	/**
1013
-	 * Search contacts in all address-books accessible by a user
1014
-	 *
1015
-	 * @param string $principalUri
1016
-	 * @param string $pattern
1017
-	 * @param array $searchProperties
1018
-	 * @param array $options
1019
-	 * @return array
1020
-	 */
1021
-	public function searchPrincipalUri(string $principalUri,
1022
-									   string $pattern,
1023
-									   array $searchProperties,
1024
-									   array $options = []): array {
1025
-		$addressBookIds = array_map(static function ($row):int {
1026
-			return (int) $row['id'];
1027
-		}, $this->getAddressBooksForUser($principalUri));
1028
-
1029
-		return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
1030
-	}
1031
-
1032
-	/**
1033
-	 * @param array $addressBookIds
1034
-	 * @param string $pattern
1035
-	 * @param array $searchProperties
1036
-	 * @param array $options
1037
-	 * @return array
1038
-	 */
1039
-	private function searchByAddressBookIds(array $addressBookIds,
1040
-											string $pattern,
1041
-											array $searchProperties,
1042
-											array $options = []): array {
1043
-		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1044
-
1045
-		$query2 = $this->db->getQueryBuilder();
1046
-
1047
-		$addressBookOr =  $query2->expr()->orX();
1048
-		foreach ($addressBookIds as $addressBookId) {
1049
-			$addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1050
-		}
1051
-
1052
-		if ($addressBookOr->count() === 0) {
1053
-			return [];
1054
-		}
1055
-
1056
-		$propertyOr = $query2->expr()->orX();
1057
-		foreach ($searchProperties as $property) {
1058
-			if ($escapePattern) {
1059
-				if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
1060
-					// There can be no spaces in emails
1061
-					continue;
1062
-				}
1063
-
1064
-				if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
1065
-					// There can be no chars in cloud ids which are not valid for user ids plus :/
1066
-					// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1067
-					continue;
1068
-				}
1069
-			}
1070
-
1071
-			$propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1072
-		}
1073
-
1074
-		if ($propertyOr->count() === 0) {
1075
-			return [];
1076
-		}
1077
-
1078
-		$query2->selectDistinct('cp.cardid')
1079
-			->from($this->dbCardsPropertiesTable, 'cp')
1080
-			->andWhere($addressBookOr)
1081
-			->andWhere($propertyOr);
1082
-
1083
-		// No need for like when the pattern is empty
1084
-		if ('' !== $pattern) {
1085
-			if (!$escapePattern) {
1086
-				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1087
-			} else {
1088
-				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1089
-			}
1090
-		}
1091
-
1092
-		if (isset($options['limit'])) {
1093
-			$query2->setMaxResults($options['limit']);
1094
-		}
1095
-		if (isset($options['offset'])) {
1096
-			$query2->setFirstResult($options['offset']);
1097
-		}
1098
-
1099
-		$result = $query2->execute();
1100
-		$matches = $result->fetchAll();
1101
-		$result->closeCursor();
1102
-		$matches = array_map(function ($match) {
1103
-			return (int)$match['cardid'];
1104
-		}, $matches);
1105
-
1106
-		$query = $this->db->getQueryBuilder();
1107
-		$query->select('c.addressbookid', 'c.carddata', 'c.uri')
1108
-			->from($this->dbCardsTable, 'c')
1109
-			->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1110
-
1111
-		$result = $query->execute();
1112
-		$cards = $result->fetchAll();
1113
-
1114
-		$result->closeCursor();
1115
-
1116
-		return array_map(function ($array) {
1117
-			$array['addressbookid'] = (int) $array['addressbookid'];
1118
-			$modified = false;
1119
-			$array['carddata'] = $this->readBlob($array['carddata'], $modified);
1120
-			if ($modified) {
1121
-				$array['size'] = strlen($array['carddata']);
1122
-			}
1123
-			return $array;
1124
-		}, $cards);
1125
-	}
1126
-
1127
-	/**
1128
-	 * @param int $bookId
1129
-	 * @param string $name
1130
-	 * @return array
1131
-	 */
1132
-	public function collectCardProperties($bookId, $name) {
1133
-		$query = $this->db->getQueryBuilder();
1134
-		$result = $query->selectDistinct('value')
1135
-			->from($this->dbCardsPropertiesTable)
1136
-			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
1137
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
1138
-			->execute();
1139
-
1140
-		$all = $result->fetchAll(PDO::FETCH_COLUMN);
1141
-		$result->closeCursor();
1142
-
1143
-		return $all;
1144
-	}
1145
-
1146
-	/**
1147
-	 * get URI from a given contact
1148
-	 *
1149
-	 * @param int $id
1150
-	 * @return string
1151
-	 */
1152
-	public function getCardUri($id) {
1153
-		$query = $this->db->getQueryBuilder();
1154
-		$query->select('uri')->from($this->dbCardsTable)
1155
-			->where($query->expr()->eq('id', $query->createParameter('id')))
1156
-			->setParameter('id', $id);
1157
-
1158
-		$result = $query->execute();
1159
-		$uri = $result->fetch();
1160
-		$result->closeCursor();
1161
-
1162
-		if (!isset($uri['uri'])) {
1163
-			throw new \InvalidArgumentException('Card does not exists: ' . $id);
1164
-		}
1165
-
1166
-		return $uri['uri'];
1167
-	}
1168
-
1169
-	/**
1170
-	 * return contact with the given URI
1171
-	 *
1172
-	 * @param int $addressBookId
1173
-	 * @param string $uri
1174
-	 * @returns array
1175
-	 */
1176
-	public function getContact($addressBookId, $uri) {
1177
-		$result = [];
1178
-		$query = $this->db->getQueryBuilder();
1179
-		$query->select('*')->from($this->dbCardsTable)
1180
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1181
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1182
-		$queryResult = $query->execute();
1183
-		$contact = $queryResult->fetch();
1184
-		$queryResult->closeCursor();
1185
-
1186
-		if (is_array($contact)) {
1187
-			$modified = false;
1188
-			$contact['etag'] = '"' . $contact['etag'] . '"';
1189
-			$contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1190
-			if ($modified) {
1191
-				$contact['size'] = strlen($contact['carddata']);
1192
-			}
1193
-
1194
-			$result = $contact;
1195
-		}
1196
-
1197
-		return $result;
1198
-	}
1199
-
1200
-	/**
1201
-	 * Returns the list of people whom this address book is shared with.
1202
-	 *
1203
-	 * Every element in this array should have the following properties:
1204
-	 *   * href - Often a mailto: address
1205
-	 *   * commonName - Optional, for example a first + last name
1206
-	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1207
-	 *   * readOnly - boolean
1208
-	 *   * summary - Optional, a description for the share
1209
-	 *
1210
-	 * @return array
1211
-	 */
1212
-	public function getShares($addressBookId) {
1213
-		return $this->sharingBackend->getShares($addressBookId);
1214
-	}
1215
-
1216
-	/**
1217
-	 * update properties table
1218
-	 *
1219
-	 * @param int $addressBookId
1220
-	 * @param string $cardUri
1221
-	 * @param string $vCardSerialized
1222
-	 */
1223
-	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1224
-		$cardId = $this->getCardId($addressBookId, $cardUri);
1225
-		$vCard = $this->readCard($vCardSerialized);
1226
-
1227
-		$this->purgeProperties($addressBookId, $cardId);
1228
-
1229
-		$query = $this->db->getQueryBuilder();
1230
-		$query->insert($this->dbCardsPropertiesTable)
1231
-			->values(
1232
-				[
1233
-					'addressbookid' => $query->createNamedParameter($addressBookId),
1234
-					'cardid' => $query->createNamedParameter($cardId),
1235
-					'name' => $query->createParameter('name'),
1236
-					'value' => $query->createParameter('value'),
1237
-					'preferred' => $query->createParameter('preferred')
1238
-				]
1239
-			);
1240
-
1241
-		foreach ($vCard->children() as $property) {
1242
-			if (!in_array($property->name, self::$indexProperties)) {
1243
-				continue;
1244
-			}
1245
-			$preferred = 0;
1246
-			foreach ($property->parameters as $parameter) {
1247
-				if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1248
-					$preferred = 1;
1249
-					break;
1250
-				}
1251
-			}
1252
-			$query->setParameter('name', $property->name);
1253
-			$query->setParameter('value', mb_substr($property->getValue(), 0, 254));
1254
-			$query->setParameter('preferred', $preferred);
1255
-			$query->execute();
1256
-		}
1257
-	}
1258
-
1259
-	/**
1260
-	 * read vCard data into a vCard object
1261
-	 *
1262
-	 * @param string $cardData
1263
-	 * @return VCard
1264
-	 */
1265
-	protected function readCard($cardData) {
1266
-		return Reader::read($cardData);
1267
-	}
1268
-
1269
-	/**
1270
-	 * delete all properties from a given card
1271
-	 *
1272
-	 * @param int $addressBookId
1273
-	 * @param int $cardId
1274
-	 */
1275
-	protected function purgeProperties($addressBookId, $cardId) {
1276
-		$query = $this->db->getQueryBuilder();
1277
-		$query->delete($this->dbCardsPropertiesTable)
1278
-			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1279
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1280
-		$query->execute();
1281
-	}
1282
-
1283
-	/**
1284
-	 * get ID from a given contact
1285
-	 *
1286
-	 * @param int $addressBookId
1287
-	 * @param string $uri
1288
-	 * @return int
1289
-	 */
1290
-	protected function getCardId($addressBookId, $uri) {
1291
-		$query = $this->db->getQueryBuilder();
1292
-		$query->select('id')->from($this->dbCardsTable)
1293
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1294
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1295
-
1296
-		$result = $query->execute();
1297
-		$cardIds = $result->fetch();
1298
-		$result->closeCursor();
1299
-
1300
-		if (!isset($cardIds['id'])) {
1301
-			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1302
-		}
1303
-
1304
-		return (int)$cardIds['id'];
1305
-	}
1306
-
1307
-	/**
1308
-	 * For shared address books the sharee is set in the ACL of the address book
1309
-	 *
1310
-	 * @param $addressBookId
1311
-	 * @param $acl
1312
-	 * @return array
1313
-	 */
1314
-	public function applyShareAcl($addressBookId, $acl) {
1315
-		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1316
-	}
1317
-
1318
-	private function convertPrincipal($principalUri, $toV2) {
1319
-		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1320
-			list(, $name) = \Sabre\Uri\split($principalUri);
1321
-			if ($toV2 === true) {
1322
-				return "principals/users/$name";
1323
-			}
1324
-			return "principals/$name";
1325
-		}
1326
-		return $principalUri;
1327
-	}
1328
-
1329
-	private function addOwnerPrincipal(&$addressbookInfo) {
1330
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1331
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1332
-		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1333
-			$uri = $addressbookInfo[$ownerPrincipalKey];
1334
-		} else {
1335
-			$uri = $addressbookInfo['principaluri'];
1336
-		}
1337
-
1338
-		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1339
-		if (isset($principalInformation['{DAV:}displayname'])) {
1340
-			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1341
-		}
1342
-	}
1343
-
1344
-	/**
1345
-	 * Extract UID from vcard
1346
-	 *
1347
-	 * @param string $cardData the vcard raw data
1348
-	 * @return string the uid
1349
-	 * @throws BadRequest if no UID is available
1350
-	 */
1351
-	private function getUID($cardData) {
1352
-		if ($cardData != '') {
1353
-			$vCard = Reader::read($cardData);
1354
-			if ($vCard->UID) {
1355
-				$uid = $vCard->UID->getValue();
1356
-				return $uid;
1357
-			}
1358
-			// should already be handled, but just in case
1359
-			throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1360
-		}
1361
-		// should already be handled, but just in case
1362
-		throw new BadRequest('vCard can not be empty');
1363
-	}
65
+    public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
66
+    public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
67
+
68
+    /** @var Principal */
69
+    private $principalBackend;
70
+
71
+    /** @var string */
72
+    private $dbCardsTable = 'cards';
73
+
74
+    /** @var string */
75
+    private $dbCardsPropertiesTable = 'cards_properties';
76
+
77
+    /** @var IDBConnection */
78
+    private $db;
79
+
80
+    /** @var Backend */
81
+    private $sharingBackend;
82
+
83
+    /** @var array properties to index */
84
+    public static $indexProperties = [
85
+        'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
86
+        'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'];
87
+
88
+    /**
89
+     * @var string[] Map of uid => display name
90
+     */
91
+    protected $userDisplayNames;
92
+
93
+    /** @var IUserManager */
94
+    private $userManager;
95
+
96
+    /** @var IEventDispatcher */
97
+    private $dispatcher;
98
+
99
+    /** @var EventDispatcherInterface */
100
+    private $legacyDispatcher;
101
+
102
+    private $etagCache = [];
103
+
104
+    /**
105
+     * CardDavBackend constructor.
106
+     *
107
+     * @param IDBConnection $db
108
+     * @param Principal $principalBackend
109
+     * @param IUserManager $userManager
110
+     * @param IGroupManager $groupManager
111
+     * @param IEventDispatcher $dispatcher
112
+     * @param EventDispatcherInterface $legacyDispatcher
113
+     */
114
+    public function __construct(IDBConnection $db,
115
+                                Principal $principalBackend,
116
+                                IUserManager $userManager,
117
+                                IGroupManager $groupManager,
118
+                                IEventDispatcher $dispatcher,
119
+                                EventDispatcherInterface $legacyDispatcher) {
120
+        $this->db = $db;
121
+        $this->principalBackend = $principalBackend;
122
+        $this->userManager = $userManager;
123
+        $this->dispatcher = $dispatcher;
124
+        $this->legacyDispatcher = $legacyDispatcher;
125
+        $this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
126
+    }
127
+
128
+    /**
129
+     * Return the number of address books for a principal
130
+     *
131
+     * @param $principalUri
132
+     * @return int
133
+     */
134
+    public function getAddressBooksForUserCount($principalUri) {
135
+        $principalUri = $this->convertPrincipal($principalUri, true);
136
+        $query = $this->db->getQueryBuilder();
137
+        $query->select($query->func()->count('*'))
138
+            ->from('addressbooks')
139
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
140
+
141
+        return (int)$query->execute()->fetchColumn();
142
+    }
143
+
144
+    /**
145
+     * Returns the list of address books for a specific user.
146
+     *
147
+     * Every addressbook should have the following properties:
148
+     *   id - an arbitrary unique id
149
+     *   uri - the 'basename' part of the url
150
+     *   principaluri - Same as the passed parameter
151
+     *
152
+     * Any additional clark-notation property may be passed besides this. Some
153
+     * common ones are :
154
+     *   {DAV:}displayname
155
+     *   {urn:ietf:params:xml:ns:carddav}addressbook-description
156
+     *   {http://calendarserver.org/ns/}getctag
157
+     *
158
+     * @param string $principalUri
159
+     * @return array
160
+     */
161
+    public function getAddressBooksForUser($principalUri) {
162
+        $principalUriOriginal = $principalUri;
163
+        $principalUri = $this->convertPrincipal($principalUri, true);
164
+        $query = $this->db->getQueryBuilder();
165
+        $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
166
+            ->from('addressbooks')
167
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
168
+
169
+        $addressBooks = [];
170
+
171
+        $result = $query->execute();
172
+        while ($row = $result->fetch()) {
173
+            $addressBooks[$row['id']] = [
174
+                'id' => $row['id'],
175
+                'uri' => $row['uri'],
176
+                'principaluri' => $this->convertPrincipal($row['principaluri'], false),
177
+                '{DAV:}displayname' => $row['displayname'],
178
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
179
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
180
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
181
+            ];
182
+
183
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
184
+        }
185
+        $result->closeCursor();
186
+
187
+        // query for shared addressbooks
188
+        $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
189
+        $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
190
+
191
+        $principals = array_map(function ($principal) {
192
+            return urldecode($principal);
193
+        }, $principals);
194
+        $principals[] = $principalUri;
195
+
196
+        $query = $this->db->getQueryBuilder();
197
+        $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
198
+            ->from('dav_shares', 's')
199
+            ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
200
+            ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
201
+            ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
202
+            ->setParameter('type', 'addressbook')
203
+            ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
204
+            ->execute();
205
+
206
+        $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
207
+        while ($row = $result->fetch()) {
208
+            if ($row['principaluri'] === $principalUri) {
209
+                continue;
210
+            }
211
+
212
+            $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
213
+            if (isset($addressBooks[$row['id']])) {
214
+                if ($readOnly) {
215
+                    // New share can not have more permissions then the old one.
216
+                    continue;
217
+                }
218
+                if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
219
+                    $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
220
+                    // Old share is already read-write, no more permissions can be gained
221
+                    continue;
222
+                }
223
+            }
224
+
225
+            list(, $name) = \Sabre\Uri\split($row['principaluri']);
226
+            $uri = $row['uri'] . '_shared_by_' . $name;
227
+            $displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
228
+
229
+            $addressBooks[$row['id']] = [
230
+                'id' => $row['id'],
231
+                'uri' => $uri,
232
+                'principaluri' => $principalUriOriginal,
233
+                '{DAV:}displayname' => $displayName,
234
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
235
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
236
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
237
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
238
+                $readOnlyPropertyName => $readOnly,
239
+            ];
240
+
241
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
242
+        }
243
+        $result->closeCursor();
244
+
245
+        return array_values($addressBooks);
246
+    }
247
+
248
+    public function getUsersOwnAddressBooks($principalUri) {
249
+        $principalUri = $this->convertPrincipal($principalUri, true);
250
+        $query = $this->db->getQueryBuilder();
251
+        $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
252
+            ->from('addressbooks')
253
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
254
+
255
+        $addressBooks = [];
256
+
257
+        $result = $query->execute();
258
+        while ($row = $result->fetch()) {
259
+            $addressBooks[$row['id']] = [
260
+                'id' => $row['id'],
261
+                'uri' => $row['uri'],
262
+                'principaluri' => $this->convertPrincipal($row['principaluri'], false),
263
+                '{DAV:}displayname' => $row['displayname'],
264
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
265
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
266
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
267
+            ];
268
+
269
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
270
+        }
271
+        $result->closeCursor();
272
+
273
+        return array_values($addressBooks);
274
+    }
275
+
276
+    private function getUserDisplayName($uid) {
277
+        if (!isset($this->userDisplayNames[$uid])) {
278
+            $user = $this->userManager->get($uid);
279
+
280
+            if ($user instanceof IUser) {
281
+                $this->userDisplayNames[$uid] = $user->getDisplayName();
282
+            } else {
283
+                $this->userDisplayNames[$uid] = $uid;
284
+            }
285
+        }
286
+
287
+        return $this->userDisplayNames[$uid];
288
+    }
289
+
290
+    /**
291
+     * @param int $addressBookId
292
+     */
293
+    public function getAddressBookById($addressBookId) {
294
+        $query = $this->db->getQueryBuilder();
295
+        $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
296
+            ->from('addressbooks')
297
+            ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
298
+            ->execute();
299
+
300
+        $row = $result->fetch();
301
+        $result->closeCursor();
302
+        if ($row === false) {
303
+            return null;
304
+        }
305
+
306
+        $addressBook = [
307
+            'id' => $row['id'],
308
+            'uri' => $row['uri'],
309
+            'principaluri' => $row['principaluri'],
310
+            '{DAV:}displayname' => $row['displayname'],
311
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
312
+            '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
313
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
314
+        ];
315
+
316
+        $this->addOwnerPrincipal($addressBook);
317
+
318
+        return $addressBook;
319
+    }
320
+
321
+    /**
322
+     * @param $addressBookUri
323
+     * @return array|null
324
+     */
325
+    public function getAddressBooksByUri($principal, $addressBookUri) {
326
+        $query = $this->db->getQueryBuilder();
327
+        $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
328
+            ->from('addressbooks')
329
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
330
+            ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
331
+            ->setMaxResults(1)
332
+            ->execute();
333
+
334
+        $row = $result->fetch();
335
+        $result->closeCursor();
336
+        if ($row === false) {
337
+            return null;
338
+        }
339
+
340
+        $addressBook = [
341
+            'id' => $row['id'],
342
+            'uri' => $row['uri'],
343
+            'principaluri' => $row['principaluri'],
344
+            '{DAV:}displayname' => $row['displayname'],
345
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
346
+            '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
347
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
348
+        ];
349
+
350
+        $this->addOwnerPrincipal($addressBook);
351
+
352
+        return $addressBook;
353
+    }
354
+
355
+    /**
356
+     * Updates properties for an address book.
357
+     *
358
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
359
+     * To do the actual updates, you must tell this object which properties
360
+     * you're going to process with the handle() method.
361
+     *
362
+     * Calling the handle method is like telling the PropPatch object "I
363
+     * promise I can handle updating this property".
364
+     *
365
+     * Read the PropPatch documentation for more info and examples.
366
+     *
367
+     * @param string $addressBookId
368
+     * @param \Sabre\DAV\PropPatch $propPatch
369
+     * @return void
370
+     */
371
+    public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
372
+        $supportedProperties = [
373
+            '{DAV:}displayname',
374
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description',
375
+        ];
376
+
377
+        $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
378
+            $updates = [];
379
+            foreach ($mutations as $property => $newValue) {
380
+                switch ($property) {
381
+                    case '{DAV:}displayname':
382
+                        $updates['displayname'] = $newValue;
383
+                        break;
384
+                    case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
385
+                        $updates['description'] = $newValue;
386
+                        break;
387
+                }
388
+            }
389
+            $query = $this->db->getQueryBuilder();
390
+            $query->update('addressbooks');
391
+
392
+            foreach ($updates as $key => $value) {
393
+                $query->set($key, $query->createNamedParameter($value));
394
+            }
395
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
396
+                ->execute();
397
+
398
+            $this->addChange($addressBookId, "", 2);
399
+
400
+            $addressBookRow = $this->getAddressBookById((int)$addressBookId);
401
+            $shares = $this->getShares($addressBookId);
402
+            $this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
403
+
404
+            return true;
405
+        });
406
+    }
407
+
408
+    /**
409
+     * Creates a new address book
410
+     *
411
+     * @param string $principalUri
412
+     * @param string $url Just the 'basename' of the url.
413
+     * @param array $properties
414
+     * @return int
415
+     * @throws BadRequest
416
+     */
417
+    public function createAddressBook($principalUri, $url, array $properties) {
418
+        $values = [
419
+            'displayname' => null,
420
+            'description' => null,
421
+            'principaluri' => $principalUri,
422
+            'uri' => $url,
423
+            'synctoken' => 1
424
+        ];
425
+
426
+        foreach ($properties as $property => $newValue) {
427
+            switch ($property) {
428
+                case '{DAV:}displayname':
429
+                    $values['displayname'] = $newValue;
430
+                    break;
431
+                case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
432
+                    $values['description'] = $newValue;
433
+                    break;
434
+                default:
435
+                    throw new BadRequest('Unknown property: ' . $property);
436
+            }
437
+        }
438
+
439
+        // Fallback to make sure the displayname is set. Some clients may refuse
440
+        // to work with addressbooks not having a displayname.
441
+        if (is_null($values['displayname'])) {
442
+            $values['displayname'] = $url;
443
+        }
444
+
445
+        $query = $this->db->getQueryBuilder();
446
+        $query->insert('addressbooks')
447
+            ->values([
448
+                'uri' => $query->createParameter('uri'),
449
+                'displayname' => $query->createParameter('displayname'),
450
+                'description' => $query->createParameter('description'),
451
+                'principaluri' => $query->createParameter('principaluri'),
452
+                'synctoken' => $query->createParameter('synctoken'),
453
+            ])
454
+            ->setParameters($values)
455
+            ->execute();
456
+
457
+        $addressBookId = $query->getLastInsertId();
458
+        $addressBookRow = $this->getAddressBookById($addressBookId);
459
+        $this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
460
+
461
+        return $addressBookId;
462
+    }
463
+
464
+    /**
465
+     * Deletes an entire addressbook and all its contents
466
+     *
467
+     * @param mixed $addressBookId
468
+     * @return void
469
+     */
470
+    public function deleteAddressBook($addressBookId) {
471
+        $addressBookData = $this->getAddressBookById($addressBookId);
472
+        $shares = $this->getShares($addressBookId);
473
+
474
+        $query = $this->db->getQueryBuilder();
475
+        $query->delete($this->dbCardsTable)
476
+            ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
477
+            ->setParameter('addressbookid', $addressBookId)
478
+            ->execute();
479
+
480
+        $query->delete('addressbookchanges')
481
+            ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
482
+            ->setParameter('addressbookid', $addressBookId)
483
+            ->execute();
484
+
485
+        $query->delete('addressbooks')
486
+            ->where($query->expr()->eq('id', $query->createParameter('id')))
487
+            ->setParameter('id', $addressBookId)
488
+            ->execute();
489
+
490
+        $this->sharingBackend->deleteAllShares($addressBookId);
491
+
492
+        $query->delete($this->dbCardsPropertiesTable)
493
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
494
+            ->execute();
495
+
496
+        if ($addressBookData) {
497
+            $this->dispatcher->dispatchTyped(new AddressBookDeletedEvent((int) $addressBookId, $addressBookData, $shares));
498
+        }
499
+    }
500
+
501
+    /**
502
+     * Returns all cards for a specific addressbook id.
503
+     *
504
+     * This method should return the following properties for each card:
505
+     *   * carddata - raw vcard data
506
+     *   * uri - Some unique url
507
+     *   * lastmodified - A unix timestamp
508
+     *
509
+     * It's recommended to also return the following properties:
510
+     *   * etag - A unique etag. This must change every time the card changes.
511
+     *   * size - The size of the card in bytes.
512
+     *
513
+     * If these last two properties are provided, less time will be spent
514
+     * calculating them. If they are specified, you can also ommit carddata.
515
+     * This may speed up certain requests, especially with large cards.
516
+     *
517
+     * @param mixed $addressBookId
518
+     * @return array
519
+     */
520
+    public function getCards($addressBookId) {
521
+        $query = $this->db->getQueryBuilder();
522
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
523
+            ->from($this->dbCardsTable)
524
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
525
+
526
+        $cards = [];
527
+
528
+        $result = $query->execute();
529
+        while ($row = $result->fetch()) {
530
+            $row['etag'] = '"' . $row['etag'] . '"';
531
+
532
+            $modified = false;
533
+            $row['carddata'] = $this->readBlob($row['carddata'], $modified);
534
+            if ($modified) {
535
+                $row['size'] = strlen($row['carddata']);
536
+            }
537
+
538
+            $cards[] = $row;
539
+        }
540
+        $result->closeCursor();
541
+
542
+        return $cards;
543
+    }
544
+
545
+    /**
546
+     * Returns a specific card.
547
+     *
548
+     * The same set of properties must be returned as with getCards. The only
549
+     * exception is that 'carddata' is absolutely required.
550
+     *
551
+     * If the card does not exist, you must return false.
552
+     *
553
+     * @param mixed $addressBookId
554
+     * @param string $cardUri
555
+     * @return array
556
+     */
557
+    public function getCard($addressBookId, $cardUri) {
558
+        $query = $this->db->getQueryBuilder();
559
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
560
+            ->from($this->dbCardsTable)
561
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
562
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
563
+            ->setMaxResults(1);
564
+
565
+        $result = $query->execute();
566
+        $row = $result->fetch();
567
+        if (!$row) {
568
+            return false;
569
+        }
570
+        $row['etag'] = '"' . $row['etag'] . '"';
571
+
572
+        $modified = false;
573
+        $row['carddata'] = $this->readBlob($row['carddata'], $modified);
574
+        if ($modified) {
575
+            $row['size'] = strlen($row['carddata']);
576
+        }
577
+
578
+        return $row;
579
+    }
580
+
581
+    /**
582
+     * Returns a list of cards.
583
+     *
584
+     * This method should work identical to getCard, but instead return all the
585
+     * cards in the list as an array.
586
+     *
587
+     * If the backend supports this, it may allow for some speed-ups.
588
+     *
589
+     * @param mixed $addressBookId
590
+     * @param string[] $uris
591
+     * @return array
592
+     */
593
+    public function getMultipleCards($addressBookId, array $uris) {
594
+        if (empty($uris)) {
595
+            return [];
596
+        }
597
+
598
+        $chunks = array_chunk($uris, 100);
599
+        $cards = [];
600
+
601
+        $query = $this->db->getQueryBuilder();
602
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
603
+            ->from($this->dbCardsTable)
604
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
605
+            ->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
606
+
607
+        foreach ($chunks as $uris) {
608
+            $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
609
+            $result = $query->execute();
610
+
611
+            while ($row = $result->fetch()) {
612
+                $row['etag'] = '"' . $row['etag'] . '"';
613
+
614
+                $modified = false;
615
+                $row['carddata'] = $this->readBlob($row['carddata'], $modified);
616
+                if ($modified) {
617
+                    $row['size'] = strlen($row['carddata']);
618
+                }
619
+
620
+                $cards[] = $row;
621
+            }
622
+            $result->closeCursor();
623
+        }
624
+        return $cards;
625
+    }
626
+
627
+    /**
628
+     * Creates a new card.
629
+     *
630
+     * The addressbook id will be passed as the first argument. This is the
631
+     * same id as it is returned from the getAddressBooksForUser method.
632
+     *
633
+     * The cardUri is a base uri, and doesn't include the full path. The
634
+     * cardData argument is the vcard body, and is passed as a string.
635
+     *
636
+     * It is possible to return an ETag from this method. This ETag is for the
637
+     * newly created resource, and must be enclosed with double quotes (that
638
+     * is, the string itself must contain the double quotes).
639
+     *
640
+     * You should only return the ETag if you store the carddata as-is. If a
641
+     * subsequent GET request on the same card does not have the same body,
642
+     * byte-by-byte and you did return an ETag here, clients tend to get
643
+     * confused.
644
+     *
645
+     * If you don't return an ETag, you can just return null.
646
+     *
647
+     * @param mixed $addressBookId
648
+     * @param string $cardUri
649
+     * @param string $cardData
650
+     * @return string
651
+     */
652
+    public function createCard($addressBookId, $cardUri, $cardData) {
653
+        $etag = md5($cardData);
654
+        $uid = $this->getUID($cardData);
655
+
656
+        $q = $this->db->getQueryBuilder();
657
+        $q->select('uid')
658
+            ->from($this->dbCardsTable)
659
+            ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
660
+            ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
661
+            ->setMaxResults(1);
662
+        $result = $q->execute();
663
+        $count = (bool)$result->fetchColumn();
664
+        $result->closeCursor();
665
+        if ($count) {
666
+            throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
667
+        }
668
+
669
+        $query = $this->db->getQueryBuilder();
670
+        $query->insert('cards')
671
+            ->values([
672
+                'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
673
+                'uri' => $query->createNamedParameter($cardUri),
674
+                'lastmodified' => $query->createNamedParameter(time()),
675
+                'addressbookid' => $query->createNamedParameter($addressBookId),
676
+                'size' => $query->createNamedParameter(strlen($cardData)),
677
+                'etag' => $query->createNamedParameter($etag),
678
+                'uid' => $query->createNamedParameter($uid),
679
+            ])
680
+            ->execute();
681
+
682
+        $etagCacheKey = "$addressBookId#$cardUri";
683
+        $this->etagCache[$etagCacheKey] = $etag;
684
+
685
+        $this->addChange($addressBookId, $cardUri, 1);
686
+        $this->updateProperties($addressBookId, $cardUri, $cardData);
687
+
688
+        $addressBookData = $this->getAddressBookById($addressBookId);
689
+        $shares = $this->getShares($addressBookId);
690
+        $objectRow = $this->getCard($addressBookId, $cardUri);
691
+        $this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
692
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
693
+            new GenericEvent(null, [
694
+                'addressBookId' => $addressBookId,
695
+                'cardUri' => $cardUri,
696
+                'cardData' => $cardData]));
697
+
698
+        return '"' . $etag . '"';
699
+    }
700
+
701
+    /**
702
+     * Updates a card.
703
+     *
704
+     * The addressbook id will be passed as the first argument. This is the
705
+     * same id as it is returned from the getAddressBooksForUser method.
706
+     *
707
+     * The cardUri is a base uri, and doesn't include the full path. The
708
+     * cardData argument is the vcard body, and is passed as a string.
709
+     *
710
+     * It is possible to return an ETag from this method. This ETag should
711
+     * match that of the updated resource, and must be enclosed with double
712
+     * quotes (that is: the string itself must contain the actual quotes).
713
+     *
714
+     * You should only return the ETag if you store the carddata as-is. If a
715
+     * subsequent GET request on the same card does not have the same body,
716
+     * byte-by-byte and you did return an ETag here, clients tend to get
717
+     * confused.
718
+     *
719
+     * If you don't return an ETag, you can just return null.
720
+     *
721
+     * @param mixed $addressBookId
722
+     * @param string $cardUri
723
+     * @param string $cardData
724
+     * @return string
725
+     */
726
+    public function updateCard($addressBookId, $cardUri, $cardData) {
727
+        $uid = $this->getUID($cardData);
728
+        $etag = md5($cardData);
729
+        $query = $this->db->getQueryBuilder();
730
+
731
+        // check for recently stored etag and stop if it is the same
732
+        $etagCacheKey = "$addressBookId#$cardUri";
733
+        if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
734
+            return '"' . $etag . '"';
735
+        }
736
+
737
+        $query->update($this->dbCardsTable)
738
+            ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
739
+            ->set('lastmodified', $query->createNamedParameter(time()))
740
+            ->set('size', $query->createNamedParameter(strlen($cardData)))
741
+            ->set('etag', $query->createNamedParameter($etag))
742
+            ->set('uid', $query->createNamedParameter($uid))
743
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
744
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
745
+            ->execute();
746
+
747
+        $this->etagCache[$etagCacheKey] = $etag;
748
+
749
+        $this->addChange($addressBookId, $cardUri, 2);
750
+        $this->updateProperties($addressBookId, $cardUri, $cardData);
751
+
752
+        $addressBookData = $this->getAddressBookById($addressBookId);
753
+        $shares = $this->getShares($addressBookId);
754
+        $objectRow = $this->getCard($addressBookId, $cardUri);
755
+        $this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
756
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
757
+            new GenericEvent(null, [
758
+                'addressBookId' => $addressBookId,
759
+                'cardUri' => $cardUri,
760
+                'cardData' => $cardData]));
761
+
762
+        return '"' . $etag . '"';
763
+    }
764
+
765
+    /**
766
+     * Deletes a card
767
+     *
768
+     * @param mixed $addressBookId
769
+     * @param string $cardUri
770
+     * @return bool
771
+     */
772
+    public function deleteCard($addressBookId, $cardUri) {
773
+        $addressBookData = $this->getAddressBookById($addressBookId);
774
+        $shares = $this->getShares($addressBookId);
775
+        $objectRow = $this->getCard($addressBookId, $cardUri);
776
+
777
+        try {
778
+            $cardId = $this->getCardId($addressBookId, $cardUri);
779
+        } catch (\InvalidArgumentException $e) {
780
+            $cardId = null;
781
+        }
782
+        $query = $this->db->getQueryBuilder();
783
+        $ret = $query->delete($this->dbCardsTable)
784
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
785
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
786
+            ->execute();
787
+
788
+        $this->addChange($addressBookId, $cardUri, 3);
789
+
790
+        if ($ret === 1) {
791
+            if ($cardId !== null) {
792
+                $this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
793
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
794
+                    new GenericEvent(null, [
795
+                        'addressBookId' => $addressBookId,
796
+                        'cardUri' => $cardUri]));
797
+
798
+                $this->purgeProperties($addressBookId, $cardId);
799
+            }
800
+            return true;
801
+        }
802
+
803
+        return false;
804
+    }
805
+
806
+    /**
807
+     * The getChanges method returns all the changes that have happened, since
808
+     * the specified syncToken in the specified address book.
809
+     *
810
+     * This function should return an array, such as the following:
811
+     *
812
+     * [
813
+     *   'syncToken' => 'The current synctoken',
814
+     *   'added'   => [
815
+     *      'new.txt',
816
+     *   ],
817
+     *   'modified'   => [
818
+     *      'modified.txt',
819
+     *   ],
820
+     *   'deleted' => [
821
+     *      'foo.php.bak',
822
+     *      'old.txt'
823
+     *   ]
824
+     * ];
825
+     *
826
+     * The returned syncToken property should reflect the *current* syncToken
827
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
828
+     * property. This is needed here too, to ensure the operation is atomic.
829
+     *
830
+     * If the $syncToken argument is specified as null, this is an initial
831
+     * sync, and all members should be reported.
832
+     *
833
+     * The modified property is an array of nodenames that have changed since
834
+     * the last token.
835
+     *
836
+     * The deleted property is an array with nodenames, that have been deleted
837
+     * from collection.
838
+     *
839
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
840
+     * 1, you only have to report changes that happened only directly in
841
+     * immediate descendants. If it's 2, it should also include changes from
842
+     * the nodes below the child collections. (grandchildren)
843
+     *
844
+     * The $limit argument allows a client to specify how many results should
845
+     * be returned at most. If the limit is not specified, it should be treated
846
+     * as infinite.
847
+     *
848
+     * If the limit (infinite or not) is higher than you're willing to return,
849
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
850
+     *
851
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
852
+     * return null.
853
+     *
854
+     * The limit is 'suggestive'. You are free to ignore it.
855
+     *
856
+     * @param string $addressBookId
857
+     * @param string $syncToken
858
+     * @param int $syncLevel
859
+     * @param int $limit
860
+     * @return array
861
+     */
862
+    public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
863
+        // Current synctoken
864
+        $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
865
+        $stmt->execute([$addressBookId]);
866
+        $currentToken = $stmt->fetchColumn(0);
867
+
868
+        if (is_null($currentToken)) {
869
+            return null;
870
+        }
871
+
872
+        $result = [
873
+            'syncToken' => $currentToken,
874
+            'added' => [],
875
+            'modified' => [],
876
+            'deleted' => [],
877
+        ];
878
+
879
+        if ($syncToken) {
880
+            $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
881
+            if ($limit > 0) {
882
+                $query .= " LIMIT " . (int)$limit;
883
+            }
884
+
885
+            // Fetching all changes
886
+            $stmt = $this->db->prepare($query);
887
+            $stmt->execute([$syncToken, $currentToken, $addressBookId]);
888
+
889
+            $changes = [];
890
+
891
+            // This loop ensures that any duplicates are overwritten, only the
892
+            // last change on a node is relevant.
893
+            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
894
+                $changes[$row['uri']] = $row['operation'];
895
+            }
896
+
897
+            foreach ($changes as $uri => $operation) {
898
+                switch ($operation) {
899
+                    case 1:
900
+                        $result['added'][] = $uri;
901
+                        break;
902
+                    case 2:
903
+                        $result['modified'][] = $uri;
904
+                        break;
905
+                    case 3:
906
+                        $result['deleted'][] = $uri;
907
+                        break;
908
+                }
909
+            }
910
+        } else {
911
+            // No synctoken supplied, this is the initial sync.
912
+            $query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
913
+            $stmt = $this->db->prepare($query);
914
+            $stmt->execute([$addressBookId]);
915
+
916
+            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
917
+        }
918
+        return $result;
919
+    }
920
+
921
+    /**
922
+     * Adds a change record to the addressbookchanges table.
923
+     *
924
+     * @param mixed $addressBookId
925
+     * @param string $objectUri
926
+     * @param int $operation 1 = add, 2 = modify, 3 = delete
927
+     * @return void
928
+     */
929
+    protected function addChange($addressBookId, $objectUri, $operation) {
930
+        $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
931
+        $stmt = $this->db->prepare($sql);
932
+        $stmt->execute([
933
+            $objectUri,
934
+            $addressBookId,
935
+            $operation,
936
+            $addressBookId
937
+        ]);
938
+        $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
939
+        $stmt->execute([
940
+            $addressBookId
941
+        ]);
942
+    }
943
+
944
+    /**
945
+     * @param resource|string $cardData
946
+     * @param bool $modified
947
+     * @return string
948
+     */
949
+    private function readBlob($cardData, &$modified = false) {
950
+        if (is_resource($cardData)) {
951
+            $cardData = stream_get_contents($cardData);
952
+        }
953
+
954
+        $cardDataArray = explode("\r\n", $cardData);
955
+
956
+        $cardDataFiltered = [];
957
+        $removingPhoto = false;
958
+        foreach ($cardDataArray as $line) {
959
+            if (strpos($line, 'PHOTO:data:') === 0
960
+                && strpos($line, 'PHOTO:data:image/') !== 0) {
961
+                // Filter out PHOTO data of non-images
962
+                $removingPhoto = true;
963
+                $modified = true;
964
+                continue;
965
+            }
966
+
967
+            if ($removingPhoto) {
968
+                if (strpos($line, ' ') === 0) {
969
+                    continue;
970
+                }
971
+                // No leading space means this is a new property
972
+                $removingPhoto = false;
973
+            }
974
+
975
+            $cardDataFiltered[] = $line;
976
+        }
977
+
978
+        return implode("\r\n", $cardDataFiltered);
979
+    }
980
+
981
+    /**
982
+     * @param IShareable $shareable
983
+     * @param string[] $add
984
+     * @param string[] $remove
985
+     */
986
+    public function updateShares(IShareable $shareable, $add, $remove) {
987
+        $addressBookId = $shareable->getResourceId();
988
+        $addressBookData = $this->getAddressBookById($addressBookId);
989
+        $oldShares = $this->getShares($addressBookId);
990
+
991
+        $this->sharingBackend->updateShares($shareable, $add, $remove);
992
+
993
+        $this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
994
+    }
995
+
996
+    /**
997
+     * Search contacts in a specific address-book
998
+     *
999
+     * @param int $addressBookId
1000
+     * @param string $pattern which should match within the $searchProperties
1001
+     * @param array $searchProperties defines the properties within the query pattern should match
1002
+     * @param array $options = array() to define the search behavior
1003
+     *    - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
1004
+     *    - 'limit' - Set a numeric limit for the search results
1005
+     *    - 'offset' - Set the offset for the limited search results
1006
+     * @return array an array of contacts which are arrays of key-value-pairs
1007
+     */
1008
+    public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
1009
+        return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
1010
+    }
1011
+
1012
+    /**
1013
+     * Search contacts in all address-books accessible by a user
1014
+     *
1015
+     * @param string $principalUri
1016
+     * @param string $pattern
1017
+     * @param array $searchProperties
1018
+     * @param array $options
1019
+     * @return array
1020
+     */
1021
+    public function searchPrincipalUri(string $principalUri,
1022
+                                        string $pattern,
1023
+                                        array $searchProperties,
1024
+                                        array $options = []): array {
1025
+        $addressBookIds = array_map(static function ($row):int {
1026
+            return (int) $row['id'];
1027
+        }, $this->getAddressBooksForUser($principalUri));
1028
+
1029
+        return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
1030
+    }
1031
+
1032
+    /**
1033
+     * @param array $addressBookIds
1034
+     * @param string $pattern
1035
+     * @param array $searchProperties
1036
+     * @param array $options
1037
+     * @return array
1038
+     */
1039
+    private function searchByAddressBookIds(array $addressBookIds,
1040
+                                            string $pattern,
1041
+                                            array $searchProperties,
1042
+                                            array $options = []): array {
1043
+        $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1044
+
1045
+        $query2 = $this->db->getQueryBuilder();
1046
+
1047
+        $addressBookOr =  $query2->expr()->orX();
1048
+        foreach ($addressBookIds as $addressBookId) {
1049
+            $addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1050
+        }
1051
+
1052
+        if ($addressBookOr->count() === 0) {
1053
+            return [];
1054
+        }
1055
+
1056
+        $propertyOr = $query2->expr()->orX();
1057
+        foreach ($searchProperties as $property) {
1058
+            if ($escapePattern) {
1059
+                if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
1060
+                    // There can be no spaces in emails
1061
+                    continue;
1062
+                }
1063
+
1064
+                if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
1065
+                    // There can be no chars in cloud ids which are not valid for user ids plus :/
1066
+                    // worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1067
+                    continue;
1068
+                }
1069
+            }
1070
+
1071
+            $propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1072
+        }
1073
+
1074
+        if ($propertyOr->count() === 0) {
1075
+            return [];
1076
+        }
1077
+
1078
+        $query2->selectDistinct('cp.cardid')
1079
+            ->from($this->dbCardsPropertiesTable, 'cp')
1080
+            ->andWhere($addressBookOr)
1081
+            ->andWhere($propertyOr);
1082
+
1083
+        // No need for like when the pattern is empty
1084
+        if ('' !== $pattern) {
1085
+            if (!$escapePattern) {
1086
+                $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1087
+            } else {
1088
+                $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1089
+            }
1090
+        }
1091
+
1092
+        if (isset($options['limit'])) {
1093
+            $query2->setMaxResults($options['limit']);
1094
+        }
1095
+        if (isset($options['offset'])) {
1096
+            $query2->setFirstResult($options['offset']);
1097
+        }
1098
+
1099
+        $result = $query2->execute();
1100
+        $matches = $result->fetchAll();
1101
+        $result->closeCursor();
1102
+        $matches = array_map(function ($match) {
1103
+            return (int)$match['cardid'];
1104
+        }, $matches);
1105
+
1106
+        $query = $this->db->getQueryBuilder();
1107
+        $query->select('c.addressbookid', 'c.carddata', 'c.uri')
1108
+            ->from($this->dbCardsTable, 'c')
1109
+            ->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1110
+
1111
+        $result = $query->execute();
1112
+        $cards = $result->fetchAll();
1113
+
1114
+        $result->closeCursor();
1115
+
1116
+        return array_map(function ($array) {
1117
+            $array['addressbookid'] = (int) $array['addressbookid'];
1118
+            $modified = false;
1119
+            $array['carddata'] = $this->readBlob($array['carddata'], $modified);
1120
+            if ($modified) {
1121
+                $array['size'] = strlen($array['carddata']);
1122
+            }
1123
+            return $array;
1124
+        }, $cards);
1125
+    }
1126
+
1127
+    /**
1128
+     * @param int $bookId
1129
+     * @param string $name
1130
+     * @return array
1131
+     */
1132
+    public function collectCardProperties($bookId, $name) {
1133
+        $query = $this->db->getQueryBuilder();
1134
+        $result = $query->selectDistinct('value')
1135
+            ->from($this->dbCardsPropertiesTable)
1136
+            ->where($query->expr()->eq('name', $query->createNamedParameter($name)))
1137
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
1138
+            ->execute();
1139
+
1140
+        $all = $result->fetchAll(PDO::FETCH_COLUMN);
1141
+        $result->closeCursor();
1142
+
1143
+        return $all;
1144
+    }
1145
+
1146
+    /**
1147
+     * get URI from a given contact
1148
+     *
1149
+     * @param int $id
1150
+     * @return string
1151
+     */
1152
+    public function getCardUri($id) {
1153
+        $query = $this->db->getQueryBuilder();
1154
+        $query->select('uri')->from($this->dbCardsTable)
1155
+            ->where($query->expr()->eq('id', $query->createParameter('id')))
1156
+            ->setParameter('id', $id);
1157
+
1158
+        $result = $query->execute();
1159
+        $uri = $result->fetch();
1160
+        $result->closeCursor();
1161
+
1162
+        if (!isset($uri['uri'])) {
1163
+            throw new \InvalidArgumentException('Card does not exists: ' . $id);
1164
+        }
1165
+
1166
+        return $uri['uri'];
1167
+    }
1168
+
1169
+    /**
1170
+     * return contact with the given URI
1171
+     *
1172
+     * @param int $addressBookId
1173
+     * @param string $uri
1174
+     * @returns array
1175
+     */
1176
+    public function getContact($addressBookId, $uri) {
1177
+        $result = [];
1178
+        $query = $this->db->getQueryBuilder();
1179
+        $query->select('*')->from($this->dbCardsTable)
1180
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1181
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1182
+        $queryResult = $query->execute();
1183
+        $contact = $queryResult->fetch();
1184
+        $queryResult->closeCursor();
1185
+
1186
+        if (is_array($contact)) {
1187
+            $modified = false;
1188
+            $contact['etag'] = '"' . $contact['etag'] . '"';
1189
+            $contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1190
+            if ($modified) {
1191
+                $contact['size'] = strlen($contact['carddata']);
1192
+            }
1193
+
1194
+            $result = $contact;
1195
+        }
1196
+
1197
+        return $result;
1198
+    }
1199
+
1200
+    /**
1201
+     * Returns the list of people whom this address book is shared with.
1202
+     *
1203
+     * Every element in this array should have the following properties:
1204
+     *   * href - Often a mailto: address
1205
+     *   * commonName - Optional, for example a first + last name
1206
+     *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1207
+     *   * readOnly - boolean
1208
+     *   * summary - Optional, a description for the share
1209
+     *
1210
+     * @return array
1211
+     */
1212
+    public function getShares($addressBookId) {
1213
+        return $this->sharingBackend->getShares($addressBookId);
1214
+    }
1215
+
1216
+    /**
1217
+     * update properties table
1218
+     *
1219
+     * @param int $addressBookId
1220
+     * @param string $cardUri
1221
+     * @param string $vCardSerialized
1222
+     */
1223
+    protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1224
+        $cardId = $this->getCardId($addressBookId, $cardUri);
1225
+        $vCard = $this->readCard($vCardSerialized);
1226
+
1227
+        $this->purgeProperties($addressBookId, $cardId);
1228
+
1229
+        $query = $this->db->getQueryBuilder();
1230
+        $query->insert($this->dbCardsPropertiesTable)
1231
+            ->values(
1232
+                [
1233
+                    'addressbookid' => $query->createNamedParameter($addressBookId),
1234
+                    'cardid' => $query->createNamedParameter($cardId),
1235
+                    'name' => $query->createParameter('name'),
1236
+                    'value' => $query->createParameter('value'),
1237
+                    'preferred' => $query->createParameter('preferred')
1238
+                ]
1239
+            );
1240
+
1241
+        foreach ($vCard->children() as $property) {
1242
+            if (!in_array($property->name, self::$indexProperties)) {
1243
+                continue;
1244
+            }
1245
+            $preferred = 0;
1246
+            foreach ($property->parameters as $parameter) {
1247
+                if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1248
+                    $preferred = 1;
1249
+                    break;
1250
+                }
1251
+            }
1252
+            $query->setParameter('name', $property->name);
1253
+            $query->setParameter('value', mb_substr($property->getValue(), 0, 254));
1254
+            $query->setParameter('preferred', $preferred);
1255
+            $query->execute();
1256
+        }
1257
+    }
1258
+
1259
+    /**
1260
+     * read vCard data into a vCard object
1261
+     *
1262
+     * @param string $cardData
1263
+     * @return VCard
1264
+     */
1265
+    protected function readCard($cardData) {
1266
+        return Reader::read($cardData);
1267
+    }
1268
+
1269
+    /**
1270
+     * delete all properties from a given card
1271
+     *
1272
+     * @param int $addressBookId
1273
+     * @param int $cardId
1274
+     */
1275
+    protected function purgeProperties($addressBookId, $cardId) {
1276
+        $query = $this->db->getQueryBuilder();
1277
+        $query->delete($this->dbCardsPropertiesTable)
1278
+            ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1279
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1280
+        $query->execute();
1281
+    }
1282
+
1283
+    /**
1284
+     * get ID from a given contact
1285
+     *
1286
+     * @param int $addressBookId
1287
+     * @param string $uri
1288
+     * @return int
1289
+     */
1290
+    protected function getCardId($addressBookId, $uri) {
1291
+        $query = $this->db->getQueryBuilder();
1292
+        $query->select('id')->from($this->dbCardsTable)
1293
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1294
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1295
+
1296
+        $result = $query->execute();
1297
+        $cardIds = $result->fetch();
1298
+        $result->closeCursor();
1299
+
1300
+        if (!isset($cardIds['id'])) {
1301
+            throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1302
+        }
1303
+
1304
+        return (int)$cardIds['id'];
1305
+    }
1306
+
1307
+    /**
1308
+     * For shared address books the sharee is set in the ACL of the address book
1309
+     *
1310
+     * @param $addressBookId
1311
+     * @param $acl
1312
+     * @return array
1313
+     */
1314
+    public function applyShareAcl($addressBookId, $acl) {
1315
+        return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1316
+    }
1317
+
1318
+    private function convertPrincipal($principalUri, $toV2) {
1319
+        if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1320
+            list(, $name) = \Sabre\Uri\split($principalUri);
1321
+            if ($toV2 === true) {
1322
+                return "principals/users/$name";
1323
+            }
1324
+            return "principals/$name";
1325
+        }
1326
+        return $principalUri;
1327
+    }
1328
+
1329
+    private function addOwnerPrincipal(&$addressbookInfo) {
1330
+        $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1331
+        $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1332
+        if (isset($addressbookInfo[$ownerPrincipalKey])) {
1333
+            $uri = $addressbookInfo[$ownerPrincipalKey];
1334
+        } else {
1335
+            $uri = $addressbookInfo['principaluri'];
1336
+        }
1337
+
1338
+        $principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1339
+        if (isset($principalInformation['{DAV:}displayname'])) {
1340
+            $addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1341
+        }
1342
+    }
1343
+
1344
+    /**
1345
+     * Extract UID from vcard
1346
+     *
1347
+     * @param string $cardData the vcard raw data
1348
+     * @return string the uid
1349
+     * @throws BadRequest if no UID is available
1350
+     */
1351
+    private function getUID($cardData) {
1352
+        if ($cardData != '') {
1353
+            $vCard = Reader::read($cardData);
1354
+            if ($vCard->UID) {
1355
+                $uid = $vCard->UID->getValue();
1356
+                return $uid;
1357
+            }
1358
+            // should already be handled, but just in case
1359
+            throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1360
+        }
1361
+        // should already be handled, but just in case
1362
+        throw new BadRequest('vCard can not be empty');
1363
+    }
1364 1364
 }
Please login to merge, or discard this patch.
Spacing   +43 added lines, -43 removed lines patch added patch discarded remove patch
@@ -138,7 +138,7 @@  discard block
 block discarded – undo
138 138
 			->from('addressbooks')
139 139
 			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
140 140
 
141
-		return (int)$query->execute()->fetchColumn();
141
+		return (int) $query->execute()->fetchColumn();
142 142
 	}
143 143
 
144 144
 	/**
@@ -175,7 +175,7 @@  discard block
 block discarded – undo
175 175
 				'uri' => $row['uri'],
176 176
 				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
177 177
 				'{DAV:}displayname' => $row['displayname'],
178
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
178
+				'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
179 179
 				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
180 180
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
181 181
 			];
@@ -188,7 +188,7 @@  discard block
 block discarded – undo
188 188
 		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
189 189
 		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
190 190
 
191
-		$principals = array_map(function ($principal) {
191
+		$principals = array_map(function($principal) {
192 192
 			return urldecode($principal);
193 193
 		}, $principals);
194 194
 		$principals[] = $principalUri;
@@ -203,13 +203,13 @@  discard block
 block discarded – undo
203 203
 			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
204 204
 			->execute();
205 205
 
206
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
206
+		$readOnlyPropertyName = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only';
207 207
 		while ($row = $result->fetch()) {
208 208
 			if ($row['principaluri'] === $principalUri) {
209 209
 				continue;
210 210
 			}
211 211
 
212
-			$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
212
+			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
213 213
 			if (isset($addressBooks[$row['id']])) {
214 214
 				if ($readOnly) {
215 215
 					// New share can not have more permissions then the old one.
@@ -223,18 +223,18 @@  discard block
 block discarded – undo
223 223
 			}
224 224
 
225 225
 			list(, $name) = \Sabre\Uri\split($row['principaluri']);
226
-			$uri = $row['uri'] . '_shared_by_' . $name;
227
-			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
226
+			$uri = $row['uri'].'_shared_by_'.$name;
227
+			$displayName = $row['displayname'].' ('.$this->getUserDisplayName($name).')';
228 228
 
229 229
 			$addressBooks[$row['id']] = [
230 230
 				'id' => $row['id'],
231 231
 				'uri' => $uri,
232 232
 				'principaluri' => $principalUriOriginal,
233 233
 				'{DAV:}displayname' => $displayName,
234
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
234
+				'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
235 235
 				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
236 236
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
237
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
237
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $row['principaluri'],
238 238
 				$readOnlyPropertyName => $readOnly,
239 239
 			];
240 240
 
@@ -261,7 +261,7 @@  discard block
 block discarded – undo
261 261
 				'uri' => $row['uri'],
262 262
 				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
263 263
 				'{DAV:}displayname' => $row['displayname'],
264
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
264
+				'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
265 265
 				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
266 266
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
267 267
 			];
@@ -308,7 +308,7 @@  discard block
 block discarded – undo
308 308
 			'uri' => $row['uri'],
309 309
 			'principaluri' => $row['principaluri'],
310 310
 			'{DAV:}displayname' => $row['displayname'],
311
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
311
+			'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
312 312
 			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
313 313
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
314 314
 		];
@@ -342,7 +342,7 @@  discard block
 block discarded – undo
342 342
 			'uri' => $row['uri'],
343 343
 			'principaluri' => $row['principaluri'],
344 344
 			'{DAV:}displayname' => $row['displayname'],
345
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
345
+			'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
346 346
 			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
347 347
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
348 348
 		];
@@ -371,17 +371,17 @@  discard block
 block discarded – undo
371 371
 	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
372 372
 		$supportedProperties = [
373 373
 			'{DAV:}displayname',
374
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
374
+			'{'.Plugin::NS_CARDDAV.'}addressbook-description',
375 375
 		];
376 376
 
377
-		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
377
+		$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
378 378
 			$updates = [];
379 379
 			foreach ($mutations as $property => $newValue) {
380 380
 				switch ($property) {
381 381
 					case '{DAV:}displayname':
382 382
 						$updates['displayname'] = $newValue;
383 383
 						break;
384
-					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
384
+					case '{'.Plugin::NS_CARDDAV.'}addressbook-description':
385 385
 						$updates['description'] = $newValue;
386 386
 						break;
387 387
 				}
@@ -397,9 +397,9 @@  discard block
 block discarded – undo
397 397
 
398 398
 			$this->addChange($addressBookId, "", 2);
399 399
 
400
-			$addressBookRow = $this->getAddressBookById((int)$addressBookId);
400
+			$addressBookRow = $this->getAddressBookById((int) $addressBookId);
401 401
 			$shares = $this->getShares($addressBookId);
402
-			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
402
+			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int) $addressBookId, $addressBookRow, $shares, $mutations));
403 403
 
404 404
 			return true;
405 405
 		});
@@ -428,11 +428,11 @@  discard block
 block discarded – undo
428 428
 				case '{DAV:}displayname':
429 429
 					$values['displayname'] = $newValue;
430 430
 					break;
431
-				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
431
+				case '{'.Plugin::NS_CARDDAV.'}addressbook-description':
432 432
 					$values['description'] = $newValue;
433 433
 					break;
434 434
 				default:
435
-					throw new BadRequest('Unknown property: ' . $property);
435
+					throw new BadRequest('Unknown property: '.$property);
436 436
 			}
437 437
 		}
438 438
 
@@ -456,7 +456,7 @@  discard block
 block discarded – undo
456 456
 
457 457
 		$addressBookId = $query->getLastInsertId();
458 458
 		$addressBookRow = $this->getAddressBookById($addressBookId);
459
-		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
459
+		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int) $addressBookId, $addressBookRow));
460 460
 
461 461
 		return $addressBookId;
462 462
 	}
@@ -527,7 +527,7 @@  discard block
 block discarded – undo
527 527
 
528 528
 		$result = $query->execute();
529 529
 		while ($row = $result->fetch()) {
530
-			$row['etag'] = '"' . $row['etag'] . '"';
530
+			$row['etag'] = '"'.$row['etag'].'"';
531 531
 
532 532
 			$modified = false;
533 533
 			$row['carddata'] = $this->readBlob($row['carddata'], $modified);
@@ -567,7 +567,7 @@  discard block
 block discarded – undo
567 567
 		if (!$row) {
568 568
 			return false;
569 569
 		}
570
-		$row['etag'] = '"' . $row['etag'] . '"';
570
+		$row['etag'] = '"'.$row['etag'].'"';
571 571
 
572 572
 		$modified = false;
573 573
 		$row['carddata'] = $this->readBlob($row['carddata'], $modified);
@@ -609,7 +609,7 @@  discard block
 block discarded – undo
609 609
 			$result = $query->execute();
610 610
 
611 611
 			while ($row = $result->fetch()) {
612
-				$row['etag'] = '"' . $row['etag'] . '"';
612
+				$row['etag'] = '"'.$row['etag'].'"';
613 613
 
614 614
 				$modified = false;
615 615
 				$row['carddata'] = $this->readBlob($row['carddata'], $modified);
@@ -660,7 +660,7 @@  discard block
 block discarded – undo
660 660
 			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
661 661
 			->setMaxResults(1);
662 662
 		$result = $q->execute();
663
-		$count = (bool)$result->fetchColumn();
663
+		$count = (bool) $result->fetchColumn();
664 664
 		$result->closeCursor();
665 665
 		if ($count) {
666 666
 			throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
@@ -688,14 +688,14 @@  discard block
 block discarded – undo
688 688
 		$addressBookData = $this->getAddressBookById($addressBookId);
689 689
 		$shares = $this->getShares($addressBookId);
690 690
 		$objectRow = $this->getCard($addressBookId, $cardUri);
691
-		$this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
691
+		$this->dispatcher->dispatchTyped(new CardCreatedEvent((int) $addressBookId, $addressBookData, $shares, $objectRow));
692 692
 		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
693 693
 			new GenericEvent(null, [
694 694
 				'addressBookId' => $addressBookId,
695 695
 				'cardUri' => $cardUri,
696 696
 				'cardData' => $cardData]));
697 697
 
698
-		return '"' . $etag . '"';
698
+		return '"'.$etag.'"';
699 699
 	}
700 700
 
701 701
 	/**
@@ -731,7 +731,7 @@  discard block
 block discarded – undo
731 731
 		// check for recently stored etag and stop if it is the same
732 732
 		$etagCacheKey = "$addressBookId#$cardUri";
733 733
 		if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
734
-			return '"' . $etag . '"';
734
+			return '"'.$etag.'"';
735 735
 		}
736 736
 
737 737
 		$query->update($this->dbCardsTable)
@@ -752,14 +752,14 @@  discard block
 block discarded – undo
752 752
 		$addressBookData = $this->getAddressBookById($addressBookId);
753 753
 		$shares = $this->getShares($addressBookId);
754 754
 		$objectRow = $this->getCard($addressBookId, $cardUri);
755
-		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
755
+		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int) $addressBookId, $addressBookData, $shares, $objectRow));
756 756
 		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
757 757
 			new GenericEvent(null, [
758 758
 				'addressBookId' => $addressBookId,
759 759
 				'cardUri' => $cardUri,
760 760
 				'cardData' => $cardData]));
761 761
 
762
-		return '"' . $etag . '"';
762
+		return '"'.$etag.'"';
763 763
 	}
764 764
 
765 765
 	/**
@@ -789,7 +789,7 @@  discard block
 block discarded – undo
789 789
 
790 790
 		if ($ret === 1) {
791 791
 			if ($cardId !== null) {
792
-				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
792
+				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int) $addressBookId, $addressBookData, $shares, $objectRow));
793 793
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
794 794
 					new GenericEvent(null, [
795 795
 						'addressBookId' => $addressBookId,
@@ -879,7 +879,7 @@  discard block
 block discarded – undo
879 879
 		if ($syncToken) {
880 880
 			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
881 881
 			if ($limit > 0) {
882
-				$query .= " LIMIT " . (int)$limit;
882
+				$query .= " LIMIT ".(int) $limit;
883 883
 			}
884 884
 
885 885
 			// Fetching all changes
@@ -1022,7 +1022,7 @@  discard block
 block discarded – undo
1022 1022
 									   string $pattern,
1023 1023
 									   array $searchProperties,
1024 1024
 									   array $options = []): array {
1025
-		$addressBookIds = array_map(static function ($row):int {
1025
+		$addressBookIds = array_map(static function($row):int {
1026 1026
 			return (int) $row['id'];
1027 1027
 		}, $this->getAddressBooksForUser($principalUri));
1028 1028
 
@@ -1044,7 +1044,7 @@  discard block
 block discarded – undo
1044 1044
 
1045 1045
 		$query2 = $this->db->getQueryBuilder();
1046 1046
 
1047
-		$addressBookOr =  $query2->expr()->orX();
1047
+		$addressBookOr = $query2->expr()->orX();
1048 1048
 		foreach ($addressBookIds as $addressBookId) {
1049 1049
 			$addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1050 1050
 		}
@@ -1085,7 +1085,7 @@  discard block
 block discarded – undo
1085 1085
 			if (!$escapePattern) {
1086 1086
 				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1087 1087
 			} else {
1088
-				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1088
+				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%'.$this->db->escapeLikeParameter($pattern).'%')));
1089 1089
 			}
1090 1090
 		}
1091 1091
 
@@ -1099,8 +1099,8 @@  discard block
 block discarded – undo
1099 1099
 		$result = $query2->execute();
1100 1100
 		$matches = $result->fetchAll();
1101 1101
 		$result->closeCursor();
1102
-		$matches = array_map(function ($match) {
1103
-			return (int)$match['cardid'];
1102
+		$matches = array_map(function($match) {
1103
+			return (int) $match['cardid'];
1104 1104
 		}, $matches);
1105 1105
 
1106 1106
 		$query = $this->db->getQueryBuilder();
@@ -1113,7 +1113,7 @@  discard block
 block discarded – undo
1113 1113
 
1114 1114
 		$result->closeCursor();
1115 1115
 
1116
-		return array_map(function ($array) {
1116
+		return array_map(function($array) {
1117 1117
 			$array['addressbookid'] = (int) $array['addressbookid'];
1118 1118
 			$modified = false;
1119 1119
 			$array['carddata'] = $this->readBlob($array['carddata'], $modified);
@@ -1160,7 +1160,7 @@  discard block
 block discarded – undo
1160 1160
 		$result->closeCursor();
1161 1161
 
1162 1162
 		if (!isset($uri['uri'])) {
1163
-			throw new \InvalidArgumentException('Card does not exists: ' . $id);
1163
+			throw new \InvalidArgumentException('Card does not exists: '.$id);
1164 1164
 		}
1165 1165
 
1166 1166
 		return $uri['uri'];
@@ -1185,7 +1185,7 @@  discard block
 block discarded – undo
1185 1185
 
1186 1186
 		if (is_array($contact)) {
1187 1187
 			$modified = false;
1188
-			$contact['etag'] = '"' . $contact['etag'] . '"';
1188
+			$contact['etag'] = '"'.$contact['etag'].'"';
1189 1189
 			$contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1190 1190
 			if ($modified) {
1191 1191
 				$contact['size'] = strlen($contact['carddata']);
@@ -1298,10 +1298,10 @@  discard block
 block discarded – undo
1298 1298
 		$result->closeCursor();
1299 1299
 
1300 1300
 		if (!isset($cardIds['id'])) {
1301
-			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1301
+			throw new \InvalidArgumentException('Card does not exists: '.$uri);
1302 1302
 		}
1303 1303
 
1304
-		return (int)$cardIds['id'];
1304
+		return (int) $cardIds['id'];
1305 1305
 	}
1306 1306
 
1307 1307
 	/**
@@ -1327,8 +1327,8 @@  discard block
 block discarded – undo
1327 1327
 	}
1328 1328
 
1329 1329
 	private function addOwnerPrincipal(&$addressbookInfo) {
1330
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1331
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1330
+		$ownerPrincipalKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal';
1331
+		$displaynameKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}owner-displayname';
1332 1332
 		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1333 1333
 			$uri = $addressbookInfo[$ownerPrincipalKey];
1334 1334
 		} else {
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/CalDavBackend.php 2 patches
Indentation   +2705 added lines, -2705 removed lines patch added patch discarded remove patch
@@ -93,2709 +93,2709 @@
 block discarded – undo
93 93
  * @package OCA\DAV\CalDAV
94 94
  */
95 95
 class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
96
-	public const CALENDAR_TYPE_CALENDAR = 0;
97
-	public const CALENDAR_TYPE_SUBSCRIPTION = 1;
98
-
99
-	public const PERSONAL_CALENDAR_URI = 'personal';
100
-	public const PERSONAL_CALENDAR_NAME = 'Personal';
101
-
102
-	public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
103
-	public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
104
-
105
-	/**
106
-	 * We need to specify a max date, because we need to stop *somewhere*
107
-	 *
108
-	 * On 32 bit system the maximum for a signed integer is 2147483647, so
109
-	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
110
-	 * in 2038-01-19 to avoid problems when the date is converted
111
-	 * to a unix timestamp.
112
-	 */
113
-	public const MAX_DATE = '2038-01-01';
114
-
115
-	public const ACCESS_PUBLIC = 4;
116
-	public const CLASSIFICATION_PUBLIC = 0;
117
-	public const CLASSIFICATION_PRIVATE = 1;
118
-	public const CLASSIFICATION_CONFIDENTIAL = 2;
119
-
120
-	/**
121
-	 * List of CalDAV properties, and how they map to database field names
122
-	 * Add your own properties by simply adding on to this array.
123
-	 *
124
-	 * Note that only string-based properties are supported here.
125
-	 *
126
-	 * @var array
127
-	 */
128
-	public $propertyMap = [
129
-		'{DAV:}displayname'                          => 'displayname',
130
-		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
131
-		'{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
132
-		'{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
133
-		'{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
134
-	];
135
-
136
-	/**
137
-	 * List of subscription properties, and how they map to database field names.
138
-	 *
139
-	 * @var array
140
-	 */
141
-	public $subscriptionPropertyMap = [
142
-		'{DAV:}displayname'                                           => 'displayname',
143
-		'{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
144
-		'{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
145
-		'{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
146
-		'{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
147
-		'{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
148
-		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
149
-	];
150
-
151
-	/** @var array properties to index */
152
-	public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION',
153
-		'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT',
154
-		'ORGANIZER'];
155
-
156
-	/** @var array parameters to index */
157
-	public static $indexParameters = [
158
-		'ATTENDEE' => ['CN'],
159
-		'ORGANIZER' => ['CN'],
160
-	];
161
-
162
-	/**
163
-	 * @var string[] Map of uid => display name
164
-	 */
165
-	protected $userDisplayNames;
166
-
167
-	/** @var IDBConnection */
168
-	private $db;
169
-
170
-	/** @var Backend */
171
-	private $calendarSharingBackend;
172
-
173
-	/** @var Principal */
174
-	private $principalBackend;
175
-
176
-	/** @var IUserManager */
177
-	private $userManager;
178
-
179
-	/** @var ISecureRandom */
180
-	private $random;
181
-
182
-	/** @var ILogger */
183
-	private $logger;
184
-
185
-	/** @var IEventDispatcher */
186
-	private $dispatcher;
187
-
188
-	/** @var EventDispatcherInterface */
189
-	private $legacyDispatcher;
190
-
191
-	/** @var bool */
192
-	private $legacyEndpoint;
193
-
194
-	/** @var string */
195
-	private $dbObjectPropertiesTable = 'calendarobjects_props';
196
-
197
-	/**
198
-	 * CalDavBackend constructor.
199
-	 *
200
-	 * @param IDBConnection $db
201
-	 * @param Principal $principalBackend
202
-	 * @param IUserManager $userManager
203
-	 * @param IGroupManager $groupManager
204
-	 * @param ISecureRandom $random
205
-	 * @param ILogger $logger
206
-	 * @param IEventDispatcher $dispatcher
207
-	 * @param EventDispatcherInterface $legacyDispatcher
208
-	 * @param bool $legacyEndpoint
209
-	 */
210
-	public function __construct(IDBConnection $db,
211
-								Principal $principalBackend,
212
-								IUserManager $userManager,
213
-								IGroupManager $groupManager,
214
-								ISecureRandom $random,
215
-								ILogger $logger,
216
-								IEventDispatcher $dispatcher,
217
-								EventDispatcherInterface $legacyDispatcher,
218
-								bool $legacyEndpoint = false) {
219
-		$this->db = $db;
220
-		$this->principalBackend = $principalBackend;
221
-		$this->userManager = $userManager;
222
-		$this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
223
-		$this->random = $random;
224
-		$this->logger = $logger;
225
-		$this->dispatcher = $dispatcher;
226
-		$this->legacyDispatcher = $legacyDispatcher;
227
-		$this->legacyEndpoint = $legacyEndpoint;
228
-	}
229
-
230
-	/**
231
-	 * Return the number of calendars for a principal
232
-	 *
233
-	 * By default this excludes the automatically generated birthday calendar
234
-	 *
235
-	 * @param $principalUri
236
-	 * @param bool $excludeBirthday
237
-	 * @return int
238
-	 */
239
-	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
240
-		$principalUri = $this->convertPrincipal($principalUri, true);
241
-		$query = $this->db->getQueryBuilder();
242
-		$query->select($query->func()->count('*'))
243
-			->from('calendars')
244
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
245
-
246
-		if ($excludeBirthday) {
247
-			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
248
-		}
249
-
250
-		return (int)$query->execute()->fetchColumn();
251
-	}
252
-
253
-	/**
254
-	 * Returns a list of calendars for a principal.
255
-	 *
256
-	 * Every project is an array with the following keys:
257
-	 *  * id, a unique id that will be used by other functions to modify the
258
-	 *    calendar. This can be the same as the uri or a database key.
259
-	 *  * uri, which the basename of the uri with which the calendar is
260
-	 *    accessed.
261
-	 *  * principaluri. The owner of the calendar. Almost always the same as
262
-	 *    principalUri passed to this method.
263
-	 *
264
-	 * Furthermore it can contain webdav properties in clark notation. A very
265
-	 * common one is '{DAV:}displayname'.
266
-	 *
267
-	 * Many clients also require:
268
-	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
269
-	 * For this property, you can just return an instance of
270
-	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
271
-	 *
272
-	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
273
-	 * ACL will automatically be put in read-only mode.
274
-	 *
275
-	 * @param string $principalUri
276
-	 * @return array
277
-	 */
278
-	public function getCalendarsForUser($principalUri) {
279
-		$principalUriOriginal = $principalUri;
280
-		$principalUri = $this->convertPrincipal($principalUri, true);
281
-		$fields = array_values($this->propertyMap);
282
-		$fields[] = 'id';
283
-		$fields[] = 'uri';
284
-		$fields[] = 'synctoken';
285
-		$fields[] = 'components';
286
-		$fields[] = 'principaluri';
287
-		$fields[] = 'transparent';
288
-
289
-		// Making fields a comma-delimited list
290
-		$query = $this->db->getQueryBuilder();
291
-		$query->select($fields)->from('calendars')
292
-				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
293
-				->orderBy('calendarorder', 'ASC');
294
-		$stmt = $query->execute();
295
-
296
-		$calendars = [];
297
-		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
298
-			$components = [];
299
-			if ($row['components']) {
300
-				$components = explode(',',$row['components']);
301
-			}
302
-
303
-			$calendar = [
304
-				'id' => $row['id'],
305
-				'uri' => $row['uri'],
306
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
307
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
308
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
309
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
310
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
311
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
312
-			];
313
-
314
-			foreach ($this->propertyMap as $xmlName=>$dbName) {
315
-				$calendar[$xmlName] = $row[$dbName];
316
-			}
317
-
318
-			$this->addOwnerPrincipal($calendar);
319
-
320
-			if (!isset($calendars[$calendar['id']])) {
321
-				$calendars[$calendar['id']] = $calendar;
322
-			}
323
-		}
324
-
325
-		$stmt->closeCursor();
326
-
327
-		// query for shared calendars
328
-		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
329
-		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
330
-
331
-		$principals = array_map(function ($principal) {
332
-			return urldecode($principal);
333
-		}, $principals);
334
-		$principals[]= $principalUri;
335
-
336
-		$fields = array_values($this->propertyMap);
337
-		$fields[] = 'a.id';
338
-		$fields[] = 'a.uri';
339
-		$fields[] = 'a.synctoken';
340
-		$fields[] = 'a.components';
341
-		$fields[] = 'a.principaluri';
342
-		$fields[] = 'a.transparent';
343
-		$fields[] = 's.access';
344
-		$query = $this->db->getQueryBuilder();
345
-		$result = $query->select($fields)
346
-			->from('dav_shares', 's')
347
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
348
-			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
349
-			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
350
-			->setParameter('type', 'calendar')
351
-			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
352
-			->execute();
353
-
354
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
355
-		while ($row = $result->fetch()) {
356
-			if ($row['principaluri'] === $principalUri) {
357
-				continue;
358
-			}
359
-
360
-			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
361
-			if (isset($calendars[$row['id']])) {
362
-				if ($readOnly) {
363
-					// New share can not have more permissions then the old one.
364
-					continue;
365
-				}
366
-				if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
367
-					$calendars[$row['id']][$readOnlyPropertyName] === 0) {
368
-					// Old share is already read-write, no more permissions can be gained
369
-					continue;
370
-				}
371
-			}
372
-
373
-			list(, $name) = Uri\split($row['principaluri']);
374
-			$uri = $row['uri'] . '_shared_by_' . $name;
375
-			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
376
-			$components = [];
377
-			if ($row['components']) {
378
-				$components = explode(',',$row['components']);
379
-			}
380
-			$calendar = [
381
-				'id' => $row['id'],
382
-				'uri' => $uri,
383
-				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
384
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
385
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
386
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
387
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
388
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
389
-				$readOnlyPropertyName => $readOnly,
390
-			];
391
-
392
-			foreach ($this->propertyMap as $xmlName=>$dbName) {
393
-				$calendar[$xmlName] = $row[$dbName];
394
-			}
395
-
396
-			$this->addOwnerPrincipal($calendar);
397
-
398
-			$calendars[$calendar['id']] = $calendar;
399
-		}
400
-		$result->closeCursor();
401
-
402
-		return array_values($calendars);
403
-	}
404
-
405
-	/**
406
-	 * @param $principalUri
407
-	 * @return array
408
-	 */
409
-	public function getUsersOwnCalendars($principalUri) {
410
-		$principalUri = $this->convertPrincipal($principalUri, true);
411
-		$fields = array_values($this->propertyMap);
412
-		$fields[] = 'id';
413
-		$fields[] = 'uri';
414
-		$fields[] = 'synctoken';
415
-		$fields[] = 'components';
416
-		$fields[] = 'principaluri';
417
-		$fields[] = 'transparent';
418
-		// Making fields a comma-delimited list
419
-		$query = $this->db->getQueryBuilder();
420
-		$query->select($fields)->from('calendars')
421
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
422
-			->orderBy('calendarorder', 'ASC');
423
-		$stmt = $query->execute();
424
-		$calendars = [];
425
-		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
426
-			$components = [];
427
-			if ($row['components']) {
428
-				$components = explode(',',$row['components']);
429
-			}
430
-			$calendar = [
431
-				'id' => $row['id'],
432
-				'uri' => $row['uri'],
433
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
434
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
435
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
436
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
437
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
438
-			];
439
-			foreach ($this->propertyMap as $xmlName=>$dbName) {
440
-				$calendar[$xmlName] = $row[$dbName];
441
-			}
442
-
443
-			$this->addOwnerPrincipal($calendar);
444
-
445
-			if (!isset($calendars[$calendar['id']])) {
446
-				$calendars[$calendar['id']] = $calendar;
447
-			}
448
-		}
449
-		$stmt->closeCursor();
450
-		return array_values($calendars);
451
-	}
452
-
453
-
454
-	/**
455
-	 * @param $uid
456
-	 * @return string
457
-	 */
458
-	private function getUserDisplayName($uid) {
459
-		if (!isset($this->userDisplayNames[$uid])) {
460
-			$user = $this->userManager->get($uid);
461
-
462
-			if ($user instanceof IUser) {
463
-				$this->userDisplayNames[$uid] = $user->getDisplayName();
464
-			} else {
465
-				$this->userDisplayNames[$uid] = $uid;
466
-			}
467
-		}
468
-
469
-		return $this->userDisplayNames[$uid];
470
-	}
471
-
472
-	/**
473
-	 * @return array
474
-	 */
475
-	public function getPublicCalendars() {
476
-		$fields = array_values($this->propertyMap);
477
-		$fields[] = 'a.id';
478
-		$fields[] = 'a.uri';
479
-		$fields[] = 'a.synctoken';
480
-		$fields[] = 'a.components';
481
-		$fields[] = 'a.principaluri';
482
-		$fields[] = 'a.transparent';
483
-		$fields[] = 's.access';
484
-		$fields[] = 's.publicuri';
485
-		$calendars = [];
486
-		$query = $this->db->getQueryBuilder();
487
-		$result = $query->select($fields)
488
-			->from('dav_shares', 's')
489
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
490
-			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
491
-			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
492
-			->execute();
493
-
494
-		while ($row = $result->fetch()) {
495
-			list(, $name) = Uri\split($row['principaluri']);
496
-			$row['displayname'] = $row['displayname'] . "($name)";
497
-			$components = [];
498
-			if ($row['components']) {
499
-				$components = explode(',',$row['components']);
500
-			}
501
-			$calendar = [
502
-				'id' => $row['id'],
503
-				'uri' => $row['publicuri'],
504
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
505
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
506
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
507
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
508
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
509
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
510
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
511
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
512
-			];
513
-
514
-			foreach ($this->propertyMap as $xmlName=>$dbName) {
515
-				$calendar[$xmlName] = $row[$dbName];
516
-			}
517
-
518
-			$this->addOwnerPrincipal($calendar);
519
-
520
-			if (!isset($calendars[$calendar['id']])) {
521
-				$calendars[$calendar['id']] = $calendar;
522
-			}
523
-		}
524
-		$result->closeCursor();
525
-
526
-		return array_values($calendars);
527
-	}
528
-
529
-	/**
530
-	 * @param string $uri
531
-	 * @return array
532
-	 * @throws NotFound
533
-	 */
534
-	public function getPublicCalendar($uri) {
535
-		$fields = array_values($this->propertyMap);
536
-		$fields[] = 'a.id';
537
-		$fields[] = 'a.uri';
538
-		$fields[] = 'a.synctoken';
539
-		$fields[] = 'a.components';
540
-		$fields[] = 'a.principaluri';
541
-		$fields[] = 'a.transparent';
542
-		$fields[] = 's.access';
543
-		$fields[] = 's.publicuri';
544
-		$query = $this->db->getQueryBuilder();
545
-		$result = $query->select($fields)
546
-			->from('dav_shares', 's')
547
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
548
-			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
549
-			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
550
-			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
551
-			->execute();
552
-
553
-		$row = $result->fetch(\PDO::FETCH_ASSOC);
554
-
555
-		$result->closeCursor();
556
-
557
-		if ($row === false) {
558
-			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
559
-		}
560
-
561
-		list(, $name) = Uri\split($row['principaluri']);
562
-		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
563
-		$components = [];
564
-		if ($row['components']) {
565
-			$components = explode(',',$row['components']);
566
-		}
567
-		$calendar = [
568
-			'id' => $row['id'],
569
-			'uri' => $row['publicuri'],
570
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
571
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
572
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
573
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
574
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
575
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
576
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
577
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
578
-		];
579
-
580
-		foreach ($this->propertyMap as $xmlName=>$dbName) {
581
-			$calendar[$xmlName] = $row[$dbName];
582
-		}
583
-
584
-		$this->addOwnerPrincipal($calendar);
585
-
586
-		return $calendar;
587
-	}
588
-
589
-	/**
590
-	 * @param string $principal
591
-	 * @param string $uri
592
-	 * @return array|null
593
-	 */
594
-	public function getCalendarByUri($principal, $uri) {
595
-		$fields = array_values($this->propertyMap);
596
-		$fields[] = 'id';
597
-		$fields[] = 'uri';
598
-		$fields[] = 'synctoken';
599
-		$fields[] = 'components';
600
-		$fields[] = 'principaluri';
601
-		$fields[] = 'transparent';
602
-
603
-		// Making fields a comma-delimited list
604
-		$query = $this->db->getQueryBuilder();
605
-		$query->select($fields)->from('calendars')
606
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
607
-			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
608
-			->setMaxResults(1);
609
-		$stmt = $query->execute();
610
-
611
-		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
612
-		$stmt->closeCursor();
613
-		if ($row === false) {
614
-			return null;
615
-		}
616
-
617
-		$components = [];
618
-		if ($row['components']) {
619
-			$components = explode(',',$row['components']);
620
-		}
621
-
622
-		$calendar = [
623
-			'id' => $row['id'],
624
-			'uri' => $row['uri'],
625
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
626
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
627
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
628
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
629
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
630
-		];
631
-
632
-		foreach ($this->propertyMap as $xmlName=>$dbName) {
633
-			$calendar[$xmlName] = $row[$dbName];
634
-		}
635
-
636
-		$this->addOwnerPrincipal($calendar);
637
-
638
-		return $calendar;
639
-	}
640
-
641
-	/**
642
-	 * @param $calendarId
643
-	 * @return array|null
644
-	 */
645
-	public function getCalendarById($calendarId) {
646
-		$fields = array_values($this->propertyMap);
647
-		$fields[] = 'id';
648
-		$fields[] = 'uri';
649
-		$fields[] = 'synctoken';
650
-		$fields[] = 'components';
651
-		$fields[] = 'principaluri';
652
-		$fields[] = 'transparent';
653
-
654
-		// Making fields a comma-delimited list
655
-		$query = $this->db->getQueryBuilder();
656
-		$query->select($fields)->from('calendars')
657
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
658
-			->setMaxResults(1);
659
-		$stmt = $query->execute();
660
-
661
-		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
662
-		$stmt->closeCursor();
663
-		if ($row === false) {
664
-			return null;
665
-		}
666
-
667
-		$components = [];
668
-		if ($row['components']) {
669
-			$components = explode(',',$row['components']);
670
-		}
671
-
672
-		$calendar = [
673
-			'id' => $row['id'],
674
-			'uri' => $row['uri'],
675
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
676
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
677
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
678
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
679
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
680
-		];
681
-
682
-		foreach ($this->propertyMap as $xmlName=>$dbName) {
683
-			$calendar[$xmlName] = $row[$dbName];
684
-		}
685
-
686
-		$this->addOwnerPrincipal($calendar);
687
-
688
-		return $calendar;
689
-	}
690
-
691
-	/**
692
-	 * @param $subscriptionId
693
-	 */
694
-	public function getSubscriptionById($subscriptionId) {
695
-		$fields = array_values($this->subscriptionPropertyMap);
696
-		$fields[] = 'id';
697
-		$fields[] = 'uri';
698
-		$fields[] = 'source';
699
-		$fields[] = 'synctoken';
700
-		$fields[] = 'principaluri';
701
-		$fields[] = 'lastmodified';
702
-
703
-		$query = $this->db->getQueryBuilder();
704
-		$query->select($fields)
705
-			->from('calendarsubscriptions')
706
-			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
707
-			->orderBy('calendarorder', 'asc');
708
-		$stmt =$query->execute();
709
-
710
-		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
711
-		$stmt->closeCursor();
712
-		if ($row === false) {
713
-			return null;
714
-		}
715
-
716
-		$subscription = [
717
-			'id'           => $row['id'],
718
-			'uri'          => $row['uri'],
719
-			'principaluri' => $row['principaluri'],
720
-			'source'       => $row['source'],
721
-			'lastmodified' => $row['lastmodified'],
722
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
723
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
724
-		];
725
-
726
-		foreach ($this->subscriptionPropertyMap as $xmlName=>$dbName) {
727
-			if (!is_null($row[$dbName])) {
728
-				$subscription[$xmlName] = $row[$dbName];
729
-			}
730
-		}
731
-
732
-		return $subscription;
733
-	}
734
-
735
-	/**
736
-	 * Creates a new calendar for a principal.
737
-	 *
738
-	 * If the creation was a success, an id must be returned that can be used to reference
739
-	 * this calendar in other methods, such as updateCalendar.
740
-	 *
741
-	 * @param string $principalUri
742
-	 * @param string $calendarUri
743
-	 * @param array $properties
744
-	 * @return int
745
-	 */
746
-	public function createCalendar($principalUri, $calendarUri, array $properties) {
747
-		$values = [
748
-			'principaluri' => $this->convertPrincipal($principalUri, true),
749
-			'uri'          => $calendarUri,
750
-			'synctoken'    => 1,
751
-			'transparent'  => 0,
752
-			'components'   => 'VEVENT,VTODO',
753
-			'displayname'  => $calendarUri
754
-		];
755
-
756
-		// Default value
757
-		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
758
-		if (isset($properties[$sccs])) {
759
-			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
760
-				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
761
-			}
762
-			$values['components'] = implode(',',$properties[$sccs]->getValue());
763
-		} elseif (isset($properties['components'])) {
764
-			// Allow to provide components internally without having
765
-			// to create a SupportedCalendarComponentSet object
766
-			$values['components'] = $properties['components'];
767
-		}
768
-
769
-		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
770
-		if (isset($properties[$transp])) {
771
-			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
772
-		}
773
-
774
-		foreach ($this->propertyMap as $xmlName=>$dbName) {
775
-			if (isset($properties[$xmlName])) {
776
-				$values[$dbName] = $properties[$xmlName];
777
-			}
778
-		}
779
-
780
-		$query = $this->db->getQueryBuilder();
781
-		$query->insert('calendars');
782
-		foreach ($values as $column => $value) {
783
-			$query->setValue($column, $query->createNamedParameter($value));
784
-		}
785
-		$query->execute();
786
-		$calendarId = $query->getLastInsertId();
787
-
788
-		$calendarData = $this->getCalendarById($calendarId);
789
-		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
790
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
791
-			'\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
792
-			[
793
-				'calendarId' => $calendarId,
794
-				'calendarData' => $calendarData,
795
-			]));
796
-
797
-		return $calendarId;
798
-	}
799
-
800
-	/**
801
-	 * Updates properties for a calendar.
802
-	 *
803
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
804
-	 * To do the actual updates, you must tell this object which properties
805
-	 * you're going to process with the handle() method.
806
-	 *
807
-	 * Calling the handle method is like telling the PropPatch object "I
808
-	 * promise I can handle updating this property".
809
-	 *
810
-	 * Read the PropPatch documentation for more info and examples.
811
-	 *
812
-	 * @param mixed $calendarId
813
-	 * @param PropPatch $propPatch
814
-	 * @return void
815
-	 */
816
-	public function updateCalendar($calendarId, PropPatch $propPatch) {
817
-		$supportedProperties = array_keys($this->propertyMap);
818
-		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
819
-
820
-		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
821
-			$newValues = [];
822
-			foreach ($mutations as $propertyName => $propertyValue) {
823
-				switch ($propertyName) {
824
-					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
825
-						$fieldName = 'transparent';
826
-						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
827
-						break;
828
-					default:
829
-						$fieldName = $this->propertyMap[$propertyName];
830
-						$newValues[$fieldName] = $propertyValue;
831
-						break;
832
-				}
833
-			}
834
-			$query = $this->db->getQueryBuilder();
835
-			$query->update('calendars');
836
-			foreach ($newValues as $fieldName => $value) {
837
-				$query->set($fieldName, $query->createNamedParameter($value));
838
-			}
839
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
840
-			$query->execute();
841
-
842
-			$this->addChange($calendarId, "", 2);
843
-
844
-			$calendarData = $this->getCalendarById($calendarId);
845
-			$shares = $this->getShares($calendarId);
846
-			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
847
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
848
-				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
849
-				[
850
-					'calendarId' => $calendarId,
851
-					'calendarData' => $calendarData,
852
-					'shares' => $shares,
853
-					'propertyMutations' => $mutations,
854
-				]));
855
-
856
-			return true;
857
-		});
858
-	}
859
-
860
-	/**
861
-	 * Delete a calendar and all it's objects
862
-	 *
863
-	 * @param mixed $calendarId
864
-	 * @return void
865
-	 */
866
-	public function deleteCalendar($calendarId) {
867
-		$calendarData = $this->getCalendarById($calendarId);
868
-		$shares = $this->getShares($calendarId);
869
-
870
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', new GenericEvent(
871
-			'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar',
872
-			[
873
-				'calendarId' => $calendarId,
874
-				'calendarData' => $calendarData,
875
-				'shares' => $shares,
876
-			]));
877
-
878
-		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?');
879
-		$stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
880
-
881
-		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
882
-		$stmt->execute([$calendarId]);
883
-
884
-		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?');
885
-		$stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
886
-
887
-		$this->calendarSharingBackend->deleteAllShares($calendarId);
888
-
889
-		$query = $this->db->getQueryBuilder();
890
-		$query->delete($this->dbObjectPropertiesTable)
891
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
892
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
893
-			->execute();
894
-
895
-		if ($calendarData) {
896
-			$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
897
-		}
898
-	}
899
-
900
-	/**
901
-	 * Delete all of an user's shares
902
-	 *
903
-	 * @param string $principaluri
904
-	 * @return void
905
-	 */
906
-	public function deleteAllSharesByUser($principaluri) {
907
-		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
908
-	}
909
-
910
-	/**
911
-	 * Returns all calendar objects within a calendar.
912
-	 *
913
-	 * Every item contains an array with the following keys:
914
-	 *   * calendardata - The iCalendar-compatible calendar data
915
-	 *   * uri - a unique key which will be used to construct the uri. This can
916
-	 *     be any arbitrary string, but making sure it ends with '.ics' is a
917
-	 *     good idea. This is only the basename, or filename, not the full
918
-	 *     path.
919
-	 *   * lastmodified - a timestamp of the last modification time
920
-	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
921
-	 *   '"abcdef"')
922
-	 *   * size - The size of the calendar objects, in bytes.
923
-	 *   * component - optional, a string containing the type of object, such
924
-	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
925
-	 *     the Content-Type header.
926
-	 *
927
-	 * Note that the etag is optional, but it's highly encouraged to return for
928
-	 * speed reasons.
929
-	 *
930
-	 * The calendardata is also optional. If it's not returned
931
-	 * 'getCalendarObject' will be called later, which *is* expected to return
932
-	 * calendardata.
933
-	 *
934
-	 * If neither etag or size are specified, the calendardata will be
935
-	 * used/fetched to determine these numbers. If both are specified the
936
-	 * amount of times this is needed is reduced by a great degree.
937
-	 *
938
-	 * @param mixed $calendarId
939
-	 * @param int $calendarType
940
-	 * @return array
941
-	 */
942
-	public function getCalendarObjects($calendarId, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
943
-		$query = $this->db->getQueryBuilder();
944
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
945
-			->from('calendarobjects')
946
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
947
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
948
-		$stmt = $query->execute();
949
-
950
-		$result = [];
951
-		foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
952
-			$result[] = [
953
-				'id'           => $row['id'],
954
-				'uri'          => $row['uri'],
955
-				'lastmodified' => $row['lastmodified'],
956
-				'etag'         => '"' . $row['etag'] . '"',
957
-				'calendarid'   => $row['calendarid'],
958
-				'size'         => (int)$row['size'],
959
-				'component'    => strtolower($row['componenttype']),
960
-				'classification'=> (int)$row['classification']
961
-			];
962
-		}
963
-
964
-		return $result;
965
-	}
966
-
967
-	/**
968
-	 * Returns information from a single calendar object, based on it's object
969
-	 * uri.
970
-	 *
971
-	 * The object uri is only the basename, or filename and not a full path.
972
-	 *
973
-	 * The returned array must have the same keys as getCalendarObjects. The
974
-	 * 'calendardata' object is required here though, while it's not required
975
-	 * for getCalendarObjects.
976
-	 *
977
-	 * This method must return null if the object did not exist.
978
-	 *
979
-	 * @param mixed $calendarId
980
-	 * @param string $objectUri
981
-	 * @param int $calendarType
982
-	 * @return array|null
983
-	 */
984
-	public function getCalendarObject($calendarId, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
985
-		$query = $this->db->getQueryBuilder();
986
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
987
-			->from('calendarobjects')
988
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
989
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
990
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
991
-		$stmt = $query->execute();
992
-		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
993
-
994
-		if (!$row) {
995
-			return null;
996
-		}
997
-
998
-		return [
999
-			'id'            => $row['id'],
1000
-			'uri'           => $row['uri'],
1001
-			'lastmodified'  => $row['lastmodified'],
1002
-			'etag'          => '"' . $row['etag'] . '"',
1003
-			'calendarid'    => $row['calendarid'],
1004
-			'size'          => (int)$row['size'],
1005
-			'calendardata'  => $this->readBlob($row['calendardata']),
1006
-			'component'     => strtolower($row['componenttype']),
1007
-			'classification'=> (int)$row['classification']
1008
-		];
1009
-	}
1010
-
1011
-	/**
1012
-	 * Returns a list of calendar objects.
1013
-	 *
1014
-	 * This method should work identical to getCalendarObject, but instead
1015
-	 * return all the calendar objects in the list as an array.
1016
-	 *
1017
-	 * If the backend supports this, it may allow for some speed-ups.
1018
-	 *
1019
-	 * @param mixed $calendarId
1020
-	 * @param string[] $uris
1021
-	 * @param int $calendarType
1022
-	 * @return array
1023
-	 */
1024
-	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
1025
-		if (empty($uris)) {
1026
-			return [];
1027
-		}
1028
-
1029
-		$chunks = array_chunk($uris, 100);
1030
-		$objects = [];
1031
-
1032
-		$query = $this->db->getQueryBuilder();
1033
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1034
-			->from('calendarobjects')
1035
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1036
-			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1037
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1038
-
1039
-		foreach ($chunks as $uris) {
1040
-			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1041
-			$result = $query->execute();
1042
-
1043
-			while ($row = $result->fetch()) {
1044
-				$objects[] = [
1045
-					'id'           => $row['id'],
1046
-					'uri'          => $row['uri'],
1047
-					'lastmodified' => $row['lastmodified'],
1048
-					'etag'         => '"' . $row['etag'] . '"',
1049
-					'calendarid'   => $row['calendarid'],
1050
-					'size'         => (int)$row['size'],
1051
-					'calendardata' => $this->readBlob($row['calendardata']),
1052
-					'component'    => strtolower($row['componenttype']),
1053
-					'classification' => (int)$row['classification']
1054
-				];
1055
-			}
1056
-			$result->closeCursor();
1057
-		}
1058
-
1059
-		return $objects;
1060
-	}
1061
-
1062
-	/**
1063
-	 * Creates a new calendar object.
1064
-	 *
1065
-	 * The object uri is only the basename, or filename and not a full path.
1066
-	 *
1067
-	 * It is possible return an etag from this function, which will be used in
1068
-	 * the response to this PUT request. Note that the ETag must be surrounded
1069
-	 * by double-quotes.
1070
-	 *
1071
-	 * However, you should only really return this ETag if you don't mangle the
1072
-	 * calendar-data. If the result of a subsequent GET to this object is not
1073
-	 * the exact same as this request body, you should omit the ETag.
1074
-	 *
1075
-	 * @param mixed $calendarId
1076
-	 * @param string $objectUri
1077
-	 * @param string $calendarData
1078
-	 * @param int $calendarType
1079
-	 * @return string
1080
-	 */
1081
-	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1082
-		$extraData = $this->getDenormalizedData($calendarData);
1083
-
1084
-		$q = $this->db->getQueryBuilder();
1085
-		$q->select($q->func()->count('*'))
1086
-			->from('calendarobjects')
1087
-			->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId)))
1088
-			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid'])))
1089
-			->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType)));
1090
-
1091
-		$result = $q->execute();
1092
-		$count = (int) $result->fetchColumn();
1093
-		$result->closeCursor();
1094
-
1095
-		if ($count !== 0) {
1096
-			throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.');
1097
-		}
1098
-
1099
-		$query = $this->db->getQueryBuilder();
1100
-		$query->insert('calendarobjects')
1101
-			->values([
1102
-				'calendarid' => $query->createNamedParameter($calendarId),
1103
-				'uri' => $query->createNamedParameter($objectUri),
1104
-				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1105
-				'lastmodified' => $query->createNamedParameter(time()),
1106
-				'etag' => $query->createNamedParameter($extraData['etag']),
1107
-				'size' => $query->createNamedParameter($extraData['size']),
1108
-				'componenttype' => $query->createNamedParameter($extraData['componentType']),
1109
-				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1110
-				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1111
-				'classification' => $query->createNamedParameter($extraData['classification']),
1112
-				'uid' => $query->createNamedParameter($extraData['uid']),
1113
-				'calendartype' => $query->createNamedParameter($calendarType),
1114
-			])
1115
-			->execute();
1116
-
1117
-		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1118
-		$this->addChange($calendarId, $objectUri, 1, $calendarType);
1119
-
1120
-		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1121
-		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1122
-			$calendarRow = $this->getCalendarById($calendarId);
1123
-			$shares = $this->getShares($calendarId);
1124
-
1125
-			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1126
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1127
-				'\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1128
-				[
1129
-					'calendarId' => $calendarId,
1130
-					'calendarData' => $calendarRow,
1131
-					'shares' => $shares,
1132
-					'objectData' => $objectRow,
1133
-				]
1134
-			));
1135
-		} else {
1136
-			$subscriptionRow = $this->getSubscriptionById($calendarId);
1137
-
1138
-			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1139
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1140
-				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1141
-				[
1142
-					'subscriptionId' => $calendarId,
1143
-					'calendarData' => $subscriptionRow,
1144
-					'shares' => [],
1145
-					'objectData' => $objectRow,
1146
-				]
1147
-			));
1148
-		}
1149
-
1150
-		return '"' . $extraData['etag'] . '"';
1151
-	}
1152
-
1153
-	/**
1154
-	 * Updates an existing calendarobject, based on it's uri.
1155
-	 *
1156
-	 * The object uri is only the basename, or filename and not a full path.
1157
-	 *
1158
-	 * It is possible return an etag from this function, which will be used in
1159
-	 * the response to this PUT request. Note that the ETag must be surrounded
1160
-	 * by double-quotes.
1161
-	 *
1162
-	 * However, you should only really return this ETag if you don't mangle the
1163
-	 * calendar-data. If the result of a subsequent GET to this object is not
1164
-	 * the exact same as this request body, you should omit the ETag.
1165
-	 *
1166
-	 * @param mixed $calendarId
1167
-	 * @param string $objectUri
1168
-	 * @param string $calendarData
1169
-	 * @param int $calendarType
1170
-	 * @return string
1171
-	 */
1172
-	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1173
-		$extraData = $this->getDenormalizedData($calendarData);
1174
-		$query = $this->db->getQueryBuilder();
1175
-		$query->update('calendarobjects')
1176
-				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1177
-				->set('lastmodified', $query->createNamedParameter(time()))
1178
-				->set('etag', $query->createNamedParameter($extraData['etag']))
1179
-				->set('size', $query->createNamedParameter($extraData['size']))
1180
-				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1181
-				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1182
-				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1183
-				->set('classification', $query->createNamedParameter($extraData['classification']))
1184
-				->set('uid', $query->createNamedParameter($extraData['uid']))
1185
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1186
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1187
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1188
-			->execute();
1189
-
1190
-		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1191
-		$this->addChange($calendarId, $objectUri, 2, $calendarType);
1192
-
1193
-		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1194
-		if (is_array($objectRow)) {
1195
-			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1196
-				$calendarRow = $this->getCalendarById($calendarId);
1197
-				$shares = $this->getShares($calendarId);
1198
-
1199
-				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1200
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1201
-					'\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1202
-					[
1203
-						'calendarId' => $calendarId,
1204
-						'calendarData' => $calendarRow,
1205
-						'shares' => $shares,
1206
-						'objectData' => $objectRow,
1207
-					]
1208
-				));
1209
-			} else {
1210
-				$subscriptionRow = $this->getSubscriptionById($calendarId);
1211
-
1212
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1213
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1214
-					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1215
-					[
1216
-						'subscriptionId' => $calendarId,
1217
-						'calendarData' => $subscriptionRow,
1218
-						'shares' => [],
1219
-						'objectData' => $objectRow,
1220
-					]
1221
-				));
1222
-			}
1223
-		}
1224
-
1225
-		return '"' . $extraData['etag'] . '"';
1226
-	}
1227
-
1228
-	/**
1229
-	 * @param int $calendarObjectId
1230
-	 * @param int $classification
1231
-	 */
1232
-	public function setClassification($calendarObjectId, $classification) {
1233
-		if (!in_array($classification, [
1234
-			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1235
-		])) {
1236
-			throw new \InvalidArgumentException();
1237
-		}
1238
-		$query = $this->db->getQueryBuilder();
1239
-		$query->update('calendarobjects')
1240
-			->set('classification', $query->createNamedParameter($classification))
1241
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1242
-			->execute();
1243
-	}
1244
-
1245
-	/**
1246
-	 * Deletes an existing calendar object.
1247
-	 *
1248
-	 * The object uri is only the basename, or filename and not a full path.
1249
-	 *
1250
-	 * @param mixed $calendarId
1251
-	 * @param string $objectUri
1252
-	 * @param int $calendarType
1253
-	 * @return void
1254
-	 */
1255
-	public function deleteCalendarObject($calendarId, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1256
-		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1257
-		if (is_array($data)) {
1258
-			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1259
-				$calendarRow = $this->getCalendarById($calendarId);
1260
-				$shares = $this->getShares($calendarId);
1261
-
1262
-				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1263
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1264
-					'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1265
-					[
1266
-						'calendarId' => $calendarId,
1267
-						'calendarData' => $calendarRow,
1268
-						'shares' => $shares,
1269
-						'objectData' => $data,
1270
-					]
1271
-				));
1272
-			} else {
1273
-				$subscriptionRow = $this->getSubscriptionById($calendarId);
1274
-
1275
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1276
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1277
-					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1278
-					[
1279
-						'subscriptionId' => $calendarId,
1280
-						'calendarData' => $subscriptionRow,
1281
-						'shares' => [],
1282
-						'objectData' => $data,
1283
-					]
1284
-				));
1285
-			}
1286
-		}
1287
-
1288
-		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1289
-		$stmt->execute([$calendarId, $objectUri, $calendarType]);
1290
-
1291
-		if (is_array($data)) {
1292
-			$this->purgeProperties($calendarId, $data['id'], $calendarType);
1293
-		}
1294
-
1295
-		$this->addChange($calendarId, $objectUri, 3, $calendarType);
1296
-	}
1297
-
1298
-	/**
1299
-	 * Performs a calendar-query on the contents of this calendar.
1300
-	 *
1301
-	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1302
-	 * calendar-query it is possible for a client to request a specific set of
1303
-	 * object, based on contents of iCalendar properties, date-ranges and
1304
-	 * iCalendar component types (VTODO, VEVENT).
1305
-	 *
1306
-	 * This method should just return a list of (relative) urls that match this
1307
-	 * query.
1308
-	 *
1309
-	 * The list of filters are specified as an array. The exact array is
1310
-	 * documented by Sabre\CalDAV\CalendarQueryParser.
1311
-	 *
1312
-	 * Note that it is extremely likely that getCalendarObject for every path
1313
-	 * returned from this method will be called almost immediately after. You
1314
-	 * may want to anticipate this to speed up these requests.
1315
-	 *
1316
-	 * This method provides a default implementation, which parses *all* the
1317
-	 * iCalendar objects in the specified calendar.
1318
-	 *
1319
-	 * This default may well be good enough for personal use, and calendars
1320
-	 * that aren't very large. But if you anticipate high usage, big calendars
1321
-	 * or high loads, you are strongly advised to optimize certain paths.
1322
-	 *
1323
-	 * The best way to do so is override this method and to optimize
1324
-	 * specifically for 'common filters'.
1325
-	 *
1326
-	 * Requests that are extremely common are:
1327
-	 *   * requests for just VEVENTS
1328
-	 *   * requests for just VTODO
1329
-	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1330
-	 *
1331
-	 * ..and combinations of these requests. It may not be worth it to try to
1332
-	 * handle every possible situation and just rely on the (relatively
1333
-	 * easy to use) CalendarQueryValidator to handle the rest.
1334
-	 *
1335
-	 * Note that especially time-range-filters may be difficult to parse. A
1336
-	 * time-range filter specified on a VEVENT must for instance also handle
1337
-	 * recurrence rules correctly.
1338
-	 * A good example of how to interprete all these filters can also simply
1339
-	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1340
-	 * as possible, so it gives you a good idea on what type of stuff you need
1341
-	 * to think of.
1342
-	 *
1343
-	 * @param mixed $calendarId
1344
-	 * @param array $filters
1345
-	 * @param int $calendarType
1346
-	 * @return array
1347
-	 */
1348
-	public function calendarQuery($calendarId, array $filters, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
1349
-		$componentType = null;
1350
-		$requirePostFilter = true;
1351
-		$timeRange = null;
1352
-
1353
-		// if no filters were specified, we don't need to filter after a query
1354
-		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1355
-			$requirePostFilter = false;
1356
-		}
1357
-
1358
-		// Figuring out if there's a component filter
1359
-		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1360
-			$componentType = $filters['comp-filters'][0]['name'];
1361
-
1362
-			// Checking if we need post-filters
1363
-			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1364
-				$requirePostFilter = false;
1365
-			}
1366
-			// There was a time-range filter
1367
-			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
1368
-				$timeRange = $filters['comp-filters'][0]['time-range'];
1369
-
1370
-				// If start time OR the end time is not specified, we can do a
1371
-				// 100% accurate mysql query.
1372
-				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1373
-					$requirePostFilter = false;
1374
-				}
1375
-			}
1376
-		}
1377
-		$columns = ['uri'];
1378
-		if ($requirePostFilter) {
1379
-			$columns = ['uri', 'calendardata'];
1380
-		}
1381
-		$query = $this->db->getQueryBuilder();
1382
-		$query->select($columns)
1383
-			->from('calendarobjects')
1384
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1385
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1386
-
1387
-		if ($componentType) {
1388
-			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1389
-		}
1390
-
1391
-		if ($timeRange && $timeRange['start']) {
1392
-			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1393
-		}
1394
-		if ($timeRange && $timeRange['end']) {
1395
-			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1396
-		}
1397
-
1398
-		$stmt = $query->execute();
1399
-
1400
-		$result = [];
1401
-		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1402
-			if ($requirePostFilter) {
1403
-				// validateFilterForObject will parse the calendar data
1404
-				// catch parsing errors
1405
-				try {
1406
-					$matches = $this->validateFilterForObject($row, $filters);
1407
-				} catch (ParseException $ex) {
1408
-					$this->logger->logException($ex, [
1409
-						'app' => 'dav',
1410
-						'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1411
-					]);
1412
-					continue;
1413
-				} catch (InvalidDataException $ex) {
1414
-					$this->logger->logException($ex, [
1415
-						'app' => 'dav',
1416
-						'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1417
-					]);
1418
-					continue;
1419
-				}
1420
-
1421
-				if (!$matches) {
1422
-					continue;
1423
-				}
1424
-			}
1425
-			$result[] = $row['uri'];
1426
-		}
1427
-
1428
-		return $result;
1429
-	}
1430
-
1431
-	/**
1432
-	 * custom Nextcloud search extension for CalDAV
1433
-	 *
1434
-	 * TODO - this should optionally cover cached calendar objects as well
1435
-	 *
1436
-	 * @param string $principalUri
1437
-	 * @param array $filters
1438
-	 * @param integer|null $limit
1439
-	 * @param integer|null $offset
1440
-	 * @return array
1441
-	 */
1442
-	public function calendarSearch($principalUri, array $filters, $limit=null, $offset=null) {
1443
-		$calendars = $this->getCalendarsForUser($principalUri);
1444
-		$ownCalendars = [];
1445
-		$sharedCalendars = [];
1446
-
1447
-		$uriMapper = [];
1448
-
1449
-		foreach ($calendars as $calendar) {
1450
-			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1451
-				$ownCalendars[] = $calendar['id'];
1452
-			} else {
1453
-				$sharedCalendars[] = $calendar['id'];
1454
-			}
1455
-			$uriMapper[$calendar['id']] = $calendar['uri'];
1456
-		}
1457
-		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1458
-			return [];
1459
-		}
1460
-
1461
-		$query = $this->db->getQueryBuilder();
1462
-		// Calendar id expressions
1463
-		$calendarExpressions = [];
1464
-		foreach ($ownCalendars as $id) {
1465
-			$calendarExpressions[] = $query->expr()->andX(
1466
-				$query->expr()->eq('c.calendarid',
1467
-					$query->createNamedParameter($id)),
1468
-				$query->expr()->eq('c.calendartype',
1469
-						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1470
-		}
1471
-		foreach ($sharedCalendars as $id) {
1472
-			$calendarExpressions[] = $query->expr()->andX(
1473
-				$query->expr()->eq('c.calendarid',
1474
-					$query->createNamedParameter($id)),
1475
-				$query->expr()->eq('c.classification',
1476
-					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1477
-				$query->expr()->eq('c.calendartype',
1478
-					$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1479
-		}
1480
-
1481
-		if (count($calendarExpressions) === 1) {
1482
-			$calExpr = $calendarExpressions[0];
1483
-		} else {
1484
-			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1485
-		}
1486
-
1487
-		// Component expressions
1488
-		$compExpressions = [];
1489
-		foreach ($filters['comps'] as $comp) {
1490
-			$compExpressions[] = $query->expr()
1491
-				->eq('c.componenttype', $query->createNamedParameter($comp));
1492
-		}
1493
-
1494
-		if (count($compExpressions) === 1) {
1495
-			$compExpr = $compExpressions[0];
1496
-		} else {
1497
-			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1498
-		}
1499
-
1500
-		if (!isset($filters['props'])) {
1501
-			$filters['props'] = [];
1502
-		}
1503
-		if (!isset($filters['params'])) {
1504
-			$filters['params'] = [];
1505
-		}
1506
-
1507
-		$propParamExpressions = [];
1508
-		foreach ($filters['props'] as $prop) {
1509
-			$propParamExpressions[] = $query->expr()->andX(
1510
-				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1511
-				$query->expr()->isNull('i.parameter')
1512
-			);
1513
-		}
1514
-		foreach ($filters['params'] as $param) {
1515
-			$propParamExpressions[] = $query->expr()->andX(
1516
-				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1517
-				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1518
-			);
1519
-		}
1520
-
1521
-		if (count($propParamExpressions) === 1) {
1522
-			$propParamExpr = $propParamExpressions[0];
1523
-		} else {
1524
-			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1525
-		}
1526
-
1527
-		$query->select(['c.calendarid', 'c.uri'])
1528
-			->from($this->dbObjectPropertiesTable, 'i')
1529
-			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1530
-			->where($calExpr)
1531
-			->andWhere($compExpr)
1532
-			->andWhere($propParamExpr)
1533
-			->andWhere($query->expr()->iLike('i.value',
1534
-				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
1535
-
1536
-		if ($offset) {
1537
-			$query->setFirstResult($offset);
1538
-		}
1539
-		if ($limit) {
1540
-			$query->setMaxResults($limit);
1541
-		}
1542
-
1543
-		$stmt = $query->execute();
1544
-
1545
-		$result = [];
1546
-		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1547
-			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1548
-			if (!in_array($path, $result)) {
1549
-				$result[] = $path;
1550
-			}
1551
-		}
1552
-
1553
-		return $result;
1554
-	}
1555
-
1556
-	/**
1557
-	 * used for Nextcloud's calendar API
1558
-	 *
1559
-	 * @param array $calendarInfo
1560
-	 * @param string $pattern
1561
-	 * @param array $searchProperties
1562
-	 * @param array $options
1563
-	 * @param integer|null $limit
1564
-	 * @param integer|null $offset
1565
-	 *
1566
-	 * @return array
1567
-	 */
1568
-	public function search(array $calendarInfo, $pattern, array $searchProperties,
1569
-						   array $options, $limit, $offset) {
1570
-		$outerQuery = $this->db->getQueryBuilder();
1571
-		$innerQuery = $this->db->getQueryBuilder();
1572
-
1573
-		$innerQuery->selectDistinct('op.objectid')
1574
-			->from($this->dbObjectPropertiesTable, 'op')
1575
-			->andWhere($innerQuery->expr()->eq('op.calendarid',
1576
-				$outerQuery->createNamedParameter($calendarInfo['id'])))
1577
-			->andWhere($innerQuery->expr()->eq('op.calendartype',
1578
-				$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1579
-
1580
-		// only return public items for shared calendars for now
1581
-		if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1582
-			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1583
-				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1584
-		}
1585
-
1586
-		$or = $innerQuery->expr()->orX();
1587
-		foreach ($searchProperties as $searchProperty) {
1588
-			$or->add($innerQuery->expr()->eq('op.name',
1589
-				$outerQuery->createNamedParameter($searchProperty)));
1590
-		}
1591
-		$innerQuery->andWhere($or);
1592
-
1593
-		if ($pattern !== '') {
1594
-			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1595
-				$outerQuery->createNamedParameter('%' .
1596
-					$this->db->escapeLikeParameter($pattern) . '%')));
1597
-		}
1598
-
1599
-		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1600
-			->from('calendarobjects', 'c');
1601
-
1602
-		if (isset($options['timerange'])) {
1603
-			if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) {
1604
-				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1605
-					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1606
-			}
1607
-			if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTime) {
1608
-				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1609
-					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1610
-			}
1611
-		}
1612
-
1613
-		if (isset($options['types'])) {
1614
-			$or = $outerQuery->expr()->orX();
1615
-			foreach ($options['types'] as $type) {
1616
-				$or->add($outerQuery->expr()->eq('componenttype',
1617
-					$outerQuery->createNamedParameter($type)));
1618
-			}
1619
-			$outerQuery->andWhere($or);
1620
-		}
1621
-
1622
-		$outerQuery->andWhere($outerQuery->expr()->in('c.id',
1623
-			$outerQuery->createFunction($innerQuery->getSQL())));
1624
-
1625
-		if ($offset) {
1626
-			$outerQuery->setFirstResult($offset);
1627
-		}
1628
-		if ($limit) {
1629
-			$outerQuery->setMaxResults($limit);
1630
-		}
1631
-
1632
-		$result = $outerQuery->execute();
1633
-		$calendarObjects = $result->fetchAll();
1634
-
1635
-		return array_map(function ($o) {
1636
-			$calendarData = Reader::read($o['calendardata']);
1637
-			$comps = $calendarData->getComponents();
1638
-			$objects = [];
1639
-			$timezones = [];
1640
-			foreach ($comps as $comp) {
1641
-				if ($comp instanceof VTimeZone) {
1642
-					$timezones[] = $comp;
1643
-				} else {
1644
-					$objects[] = $comp;
1645
-				}
1646
-			}
1647
-
1648
-			return [
1649
-				'id' => $o['id'],
1650
-				'type' => $o['componenttype'],
1651
-				'uid' => $o['uid'],
1652
-				'uri' => $o['uri'],
1653
-				'objects' => array_map(function ($c) {
1654
-					return $this->transformSearchData($c);
1655
-				}, $objects),
1656
-				'timezones' => array_map(function ($c) {
1657
-					return $this->transformSearchData($c);
1658
-				}, $timezones),
1659
-			];
1660
-		}, $calendarObjects);
1661
-	}
1662
-
1663
-	/**
1664
-	 * @param Component $comp
1665
-	 * @return array
1666
-	 */
1667
-	private function transformSearchData(Component $comp) {
1668
-		$data = [];
1669
-		/** @var Component[] $subComponents */
1670
-		$subComponents = $comp->getComponents();
1671
-		/** @var Property[] $properties */
1672
-		$properties = array_filter($comp->children(), function ($c) {
1673
-			return $c instanceof Property;
1674
-		});
1675
-		$validationRules = $comp->getValidationRules();
1676
-
1677
-		foreach ($subComponents as $subComponent) {
1678
-			$name = $subComponent->name;
1679
-			if (!isset($data[$name])) {
1680
-				$data[$name] = [];
1681
-			}
1682
-			$data[$name][] = $this->transformSearchData($subComponent);
1683
-		}
1684
-
1685
-		foreach ($properties as $property) {
1686
-			$name = $property->name;
1687
-			if (!isset($validationRules[$name])) {
1688
-				$validationRules[$name] = '*';
1689
-			}
1690
-
1691
-			$rule = $validationRules[$property->name];
1692
-			if ($rule === '+' || $rule === '*') { // multiple
1693
-				if (!isset($data[$name])) {
1694
-					$data[$name] = [];
1695
-				}
1696
-
1697
-				$data[$name][] = $this->transformSearchProperty($property);
1698
-			} else { // once
1699
-				$data[$name] = $this->transformSearchProperty($property);
1700
-			}
1701
-		}
1702
-
1703
-		return $data;
1704
-	}
1705
-
1706
-	/**
1707
-	 * @param Property $prop
1708
-	 * @return array
1709
-	 */
1710
-	private function transformSearchProperty(Property $prop) {
1711
-		// No need to check Date, as it extends DateTime
1712
-		if ($prop instanceof Property\ICalendar\DateTime) {
1713
-			$value = $prop->getDateTime();
1714
-		} else {
1715
-			$value = $prop->getValue();
1716
-		}
1717
-
1718
-		return [
1719
-			$value,
1720
-			$prop->parameters()
1721
-		];
1722
-	}
1723
-
1724
-	/**
1725
-	 * @param string $principalUri
1726
-	 * @param string $pattern
1727
-	 * @param array $componentTypes
1728
-	 * @param array $searchProperties
1729
-	 * @param array $searchParameters
1730
-	 * @param array $options
1731
-	 * @return array
1732
-	 */
1733
-	public function searchPrincipalUri(string $principalUri,
1734
-									   string $pattern,
1735
-									   array $componentTypes,
1736
-									   array $searchProperties,
1737
-									   array $searchParameters,
1738
-									   array $options = []): array {
1739
-		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1740
-
1741
-		$calendarObjectIdQuery = $this->db->getQueryBuilder();
1742
-		$calendarOr = $calendarObjectIdQuery->expr()->orX();
1743
-		$searchOr = $calendarObjectIdQuery->expr()->orX();
1744
-
1745
-		// Fetch calendars and subscription
1746
-		$calendars = $this->getCalendarsForUser($principalUri);
1747
-		$subscriptions = $this->getSubscriptionsForUser($principalUri);
1748
-		foreach ($calendars as $calendar) {
1749
-			$calendarAnd = $calendarObjectIdQuery->expr()->andX();
1750
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
1751
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1752
-
1753
-			// If it's shared, limit search to public events
1754
-			if ($calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
1755
-				$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1756
-			}
1757
-
1758
-			$calendarOr->add($calendarAnd);
1759
-		}
1760
-		foreach ($subscriptions as $subscription) {
1761
-			$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
1762
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
1763
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
1764
-
1765
-			// If it's shared, limit search to public events
1766
-			if ($subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
1767
-				$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1768
-			}
1769
-
1770
-			$calendarOr->add($subscriptionAnd);
1771
-		}
1772
-
1773
-		foreach ($searchProperties as $property) {
1774
-			$propertyAnd = $calendarObjectIdQuery->expr()->andX();
1775
-			$propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
1776
-			$propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
1777
-
1778
-			$searchOr->add($propertyAnd);
1779
-		}
1780
-		foreach ($searchParameters as $property => $parameter) {
1781
-			$parameterAnd = $calendarObjectIdQuery->expr()->andX();
1782
-			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
1783
-			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR)));
1784
-
1785
-			$searchOr->add($parameterAnd);
1786
-		}
1787
-
1788
-		if ($calendarOr->count() === 0) {
1789
-			return [];
1790
-		}
1791
-		if ($searchOr->count() === 0) {
1792
-			return [];
1793
-		}
1794
-
1795
-		$calendarObjectIdQuery->selectDistinct('cob.objectid')
1796
-			->from($this->dbObjectPropertiesTable, 'cob')
1797
-			->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
1798
-			->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
1799
-			->andWhere($calendarOr)
1800
-			->andWhere($searchOr);
1801
-
1802
-		if ('' !== $pattern) {
1803
-			if (!$escapePattern) {
1804
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
1805
-			} else {
1806
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1807
-			}
1808
-		}
1809
-
1810
-		if (isset($options['limit'])) {
1811
-			$calendarObjectIdQuery->setMaxResults($options['limit']);
1812
-		}
1813
-		if (isset($options['offset'])) {
1814
-			$calendarObjectIdQuery->setFirstResult($options['offset']);
1815
-		}
1816
-
1817
-		$result = $calendarObjectIdQuery->execute();
1818
-		$matches = $result->fetchAll();
1819
-		$result->closeCursor();
1820
-		$matches = array_map(static function (array $match):int {
1821
-			return (int) $match['objectid'];
1822
-		}, $matches);
1823
-
1824
-		$query = $this->db->getQueryBuilder();
1825
-		$query->select('calendardata', 'uri', 'calendarid', 'calendartype')
1826
-			->from('calendarobjects')
1827
-			->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1828
-
1829
-		$result = $query->execute();
1830
-		$calendarObjects = $result->fetchAll();
1831
-		$result->closeCursor();
1832
-
1833
-		return array_map(function (array $array): array {
1834
-			$array['calendarid'] = (int)$array['calendarid'];
1835
-			$array['calendartype'] = (int)$array['calendartype'];
1836
-			$array['calendardata'] = $this->readBlob($array['calendardata']);
1837
-
1838
-			return $array;
1839
-		}, $calendarObjects);
1840
-	}
1841
-
1842
-	/**
1843
-	 * Searches through all of a users calendars and calendar objects to find
1844
-	 * an object with a specific UID.
1845
-	 *
1846
-	 * This method should return the path to this object, relative to the
1847
-	 * calendar home, so this path usually only contains two parts:
1848
-	 *
1849
-	 * calendarpath/objectpath.ics
1850
-	 *
1851
-	 * If the uid is not found, return null.
1852
-	 *
1853
-	 * This method should only consider * objects that the principal owns, so
1854
-	 * any calendars owned by other principals that also appear in this
1855
-	 * collection should be ignored.
1856
-	 *
1857
-	 * @param string $principalUri
1858
-	 * @param string $uid
1859
-	 * @return string|null
1860
-	 */
1861
-	public function getCalendarObjectByUID($principalUri, $uid) {
1862
-		$query = $this->db->getQueryBuilder();
1863
-		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1864
-			->from('calendarobjects', 'co')
1865
-			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1866
-			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1867
-			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1868
-
1869
-		$stmt = $query->execute();
1870
-
1871
-		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1872
-			return $row['calendaruri'] . '/' . $row['objecturi'];
1873
-		}
1874
-
1875
-		return null;
1876
-	}
1877
-
1878
-	/**
1879
-	 * The getChanges method returns all the changes that have happened, since
1880
-	 * the specified syncToken in the specified calendar.
1881
-	 *
1882
-	 * This function should return an array, such as the following:
1883
-	 *
1884
-	 * [
1885
-	 *   'syncToken' => 'The current synctoken',
1886
-	 *   'added'   => [
1887
-	 *      'new.txt',
1888
-	 *   ],
1889
-	 *   'modified'   => [
1890
-	 *      'modified.txt',
1891
-	 *   ],
1892
-	 *   'deleted' => [
1893
-	 *      'foo.php.bak',
1894
-	 *      'old.txt'
1895
-	 *   ]
1896
-	 * );
1897
-	 *
1898
-	 * The returned syncToken property should reflect the *current* syncToken
1899
-	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1900
-	 * property This is * needed here too, to ensure the operation is atomic.
1901
-	 *
1902
-	 * If the $syncToken argument is specified as null, this is an initial
1903
-	 * sync, and all members should be reported.
1904
-	 *
1905
-	 * The modified property is an array of nodenames that have changed since
1906
-	 * the last token.
1907
-	 *
1908
-	 * The deleted property is an array with nodenames, that have been deleted
1909
-	 * from collection.
1910
-	 *
1911
-	 * The $syncLevel argument is basically the 'depth' of the report. If it's
1912
-	 * 1, you only have to report changes that happened only directly in
1913
-	 * immediate descendants. If it's 2, it should also include changes from
1914
-	 * the nodes below the child collections. (grandchildren)
1915
-	 *
1916
-	 * The $limit argument allows a client to specify how many results should
1917
-	 * be returned at most. If the limit is not specified, it should be treated
1918
-	 * as infinite.
1919
-	 *
1920
-	 * If the limit (infinite or not) is higher than you're willing to return,
1921
-	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1922
-	 *
1923
-	 * If the syncToken is expired (due to data cleanup) or unknown, you must
1924
-	 * return null.
1925
-	 *
1926
-	 * The limit is 'suggestive'. You are free to ignore it.
1927
-	 *
1928
-	 * @param string $calendarId
1929
-	 * @param string $syncToken
1930
-	 * @param int $syncLevel
1931
-	 * @param int $limit
1932
-	 * @param int $calendarType
1933
-	 * @return array
1934
-	 */
1935
-	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1936
-		// Current synctoken
1937
-		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1938
-		$stmt->execute([ $calendarId ]);
1939
-		$currentToken = $stmt->fetchColumn(0);
1940
-
1941
-		if (is_null($currentToken)) {
1942
-			return null;
1943
-		}
1944
-
1945
-		$result = [
1946
-			'syncToken' => $currentToken,
1947
-			'added'     => [],
1948
-			'modified'  => [],
1949
-			'deleted'   => [],
1950
-		];
1951
-
1952
-		if ($syncToken) {
1953
-			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? AND `calendartype` = ? ORDER BY `synctoken`";
1954
-			if ($limit>0) {
1955
-				$query.= " LIMIT " . (int)$limit;
1956
-			}
1957
-
1958
-			// Fetching all changes
1959
-			$stmt = $this->db->prepare($query);
1960
-			$stmt->execute([$syncToken, $currentToken, $calendarId, $calendarType]);
1961
-
1962
-			$changes = [];
1963
-
1964
-			// This loop ensures that any duplicates are overwritten, only the
1965
-			// last change on a node is relevant.
1966
-			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1967
-				$changes[$row['uri']] = $row['operation'];
1968
-			}
1969
-
1970
-			foreach ($changes as $uri => $operation) {
1971
-				switch ($operation) {
1972
-					case 1:
1973
-						$result['added'][] = $uri;
1974
-						break;
1975
-					case 2:
1976
-						$result['modified'][] = $uri;
1977
-						break;
1978
-					case 3:
1979
-						$result['deleted'][] = $uri;
1980
-						break;
1981
-				}
1982
-			}
1983
-		} else {
1984
-			// No synctoken supplied, this is the initial sync.
1985
-			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?";
1986
-			$stmt = $this->db->prepare($query);
1987
-			$stmt->execute([$calendarId, $calendarType]);
1988
-
1989
-			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1990
-		}
1991
-		return $result;
1992
-	}
1993
-
1994
-	/**
1995
-	 * Returns a list of subscriptions for a principal.
1996
-	 *
1997
-	 * Every subscription is an array with the following keys:
1998
-	 *  * id, a unique id that will be used by other functions to modify the
1999
-	 *    subscription. This can be the same as the uri or a database key.
2000
-	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2001
-	 *  * principaluri. The owner of the subscription. Almost always the same as
2002
-	 *    principalUri passed to this method.
2003
-	 *
2004
-	 * Furthermore, all the subscription info must be returned too:
2005
-	 *
2006
-	 * 1. {DAV:}displayname
2007
-	 * 2. {http://apple.com/ns/ical/}refreshrate
2008
-	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2009
-	 *    should not be stripped).
2010
-	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2011
-	 *    should not be stripped).
2012
-	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2013
-	 *    attachments should not be stripped).
2014
-	 * 6. {http://calendarserver.org/ns/}source (Must be a
2015
-	 *     Sabre\DAV\Property\Href).
2016
-	 * 7. {http://apple.com/ns/ical/}calendar-color
2017
-	 * 8. {http://apple.com/ns/ical/}calendar-order
2018
-	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2019
-	 *    (should just be an instance of
2020
-	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2021
-	 *    default components).
2022
-	 *
2023
-	 * @param string $principalUri
2024
-	 * @return array
2025
-	 */
2026
-	public function getSubscriptionsForUser($principalUri) {
2027
-		$fields = array_values($this->subscriptionPropertyMap);
2028
-		$fields[] = 'id';
2029
-		$fields[] = 'uri';
2030
-		$fields[] = 'source';
2031
-		$fields[] = 'principaluri';
2032
-		$fields[] = 'lastmodified';
2033
-		$fields[] = 'synctoken';
2034
-
2035
-		$query = $this->db->getQueryBuilder();
2036
-		$query->select($fields)
2037
-			->from('calendarsubscriptions')
2038
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2039
-			->orderBy('calendarorder', 'asc');
2040
-		$stmt =$query->execute();
2041
-
2042
-		$subscriptions = [];
2043
-		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
2044
-			$subscription = [
2045
-				'id'           => $row['id'],
2046
-				'uri'          => $row['uri'],
2047
-				'principaluri' => $row['principaluri'],
2048
-				'source'       => $row['source'],
2049
-				'lastmodified' => $row['lastmodified'],
2050
-
2051
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2052
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2053
-			];
2054
-
2055
-			foreach ($this->subscriptionPropertyMap as $xmlName=>$dbName) {
2056
-				if (!is_null($row[$dbName])) {
2057
-					$subscription[$xmlName] = $row[$dbName];
2058
-				}
2059
-			}
2060
-
2061
-			$subscriptions[] = $subscription;
2062
-		}
2063
-
2064
-		return $subscriptions;
2065
-	}
2066
-
2067
-	/**
2068
-	 * Creates a new subscription for a principal.
2069
-	 *
2070
-	 * If the creation was a success, an id must be returned that can be used to reference
2071
-	 * this subscription in other methods, such as updateSubscription.
2072
-	 *
2073
-	 * @param string $principalUri
2074
-	 * @param string $uri
2075
-	 * @param array $properties
2076
-	 * @return mixed
2077
-	 */
2078
-	public function createSubscription($principalUri, $uri, array $properties) {
2079
-		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2080
-			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2081
-		}
2082
-
2083
-		$values = [
2084
-			'principaluri' => $principalUri,
2085
-			'uri'          => $uri,
2086
-			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2087
-			'lastmodified' => time(),
2088
-		];
2089
-
2090
-		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2091
-
2092
-		foreach ($this->subscriptionPropertyMap as $xmlName=>$dbName) {
2093
-			if (array_key_exists($xmlName, $properties)) {
2094
-				$values[$dbName] = $properties[$xmlName];
2095
-				if (in_array($dbName, $propertiesBoolean)) {
2096
-					$values[$dbName] = true;
2097
-				}
2098
-			}
2099
-		}
2100
-
2101
-		$valuesToInsert = [];
2102
-
2103
-		$query = $this->db->getQueryBuilder();
2104
-
2105
-		foreach (array_keys($values) as $name) {
2106
-			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2107
-		}
2108
-
2109
-		$query->insert('calendarsubscriptions')
2110
-			->values($valuesToInsert)
2111
-			->execute();
2112
-
2113
-		$subscriptionId = $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
2114
-
2115
-		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2116
-		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent((int)$subscriptionId, $subscriptionRow));
2117
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
2118
-			'\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2119
-			[
2120
-				'subscriptionId' => $subscriptionId,
2121
-				'subscriptionData' => $subscriptionRow,
2122
-			]));
2123
-
2124
-		return $subscriptionId;
2125
-	}
2126
-
2127
-	/**
2128
-	 * Updates a subscription
2129
-	 *
2130
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2131
-	 * To do the actual updates, you must tell this object which properties
2132
-	 * you're going to process with the handle() method.
2133
-	 *
2134
-	 * Calling the handle method is like telling the PropPatch object "I
2135
-	 * promise I can handle updating this property".
2136
-	 *
2137
-	 * Read the PropPatch documentation for more info and examples.
2138
-	 *
2139
-	 * @param mixed $subscriptionId
2140
-	 * @param PropPatch $propPatch
2141
-	 * @return void
2142
-	 */
2143
-	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2144
-		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2145
-		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2146
-
2147
-		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2148
-			$newValues = [];
2149
-
2150
-			foreach ($mutations as $propertyName=>$propertyValue) {
2151
-				if ($propertyName === '{http://calendarserver.org/ns/}source') {
2152
-					$newValues['source'] = $propertyValue->getHref();
2153
-				} else {
2154
-					$fieldName = $this->subscriptionPropertyMap[$propertyName];
2155
-					$newValues[$fieldName] = $propertyValue;
2156
-				}
2157
-			}
2158
-
2159
-			$query = $this->db->getQueryBuilder();
2160
-			$query->update('calendarsubscriptions')
2161
-				->set('lastmodified', $query->createNamedParameter(time()));
2162
-			foreach ($newValues as $fieldName=>$value) {
2163
-				$query->set($fieldName, $query->createNamedParameter($value));
2164
-			}
2165
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2166
-				->execute();
2167
-
2168
-			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2169
-			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2170
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2171
-				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2172
-				[
2173
-					'subscriptionId' => $subscriptionId,
2174
-					'subscriptionData' => $subscriptionRow,
2175
-					'propertyMutations' => $mutations,
2176
-				]));
2177
-
2178
-			return true;
2179
-		});
2180
-	}
2181
-
2182
-	/**
2183
-	 * Deletes a subscription.
2184
-	 *
2185
-	 * @param mixed $subscriptionId
2186
-	 * @return void
2187
-	 */
2188
-	public function deleteSubscription($subscriptionId) {
2189
-		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2190
-
2191
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
2192
-			'\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2193
-			[
2194
-				'subscriptionId' => $subscriptionId,
2195
-				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2196
-			]));
2197
-
2198
-		$query = $this->db->getQueryBuilder();
2199
-		$query->delete('calendarsubscriptions')
2200
-			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2201
-			->execute();
2202
-
2203
-		$query = $this->db->getQueryBuilder();
2204
-		$query->delete('calendarobjects')
2205
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2206
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2207
-			->execute();
2208
-
2209
-		$query->delete('calendarchanges')
2210
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2211
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2212
-			->execute();
2213
-
2214
-		$query->delete($this->dbObjectPropertiesTable)
2215
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2216
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2217
-			->execute();
2218
-
2219
-		if ($subscriptionRow) {
2220
-			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2221
-		}
2222
-	}
2223
-
2224
-	/**
2225
-	 * Returns a single scheduling object for the inbox collection.
2226
-	 *
2227
-	 * The returned array should contain the following elements:
2228
-	 *   * uri - A unique basename for the object. This will be used to
2229
-	 *           construct a full uri.
2230
-	 *   * calendardata - The iCalendar object
2231
-	 *   * lastmodified - The last modification date. Can be an int for a unix
2232
-	 *                    timestamp, or a PHP DateTime object.
2233
-	 *   * etag - A unique token that must change if the object changed.
2234
-	 *   * size - The size of the object, in bytes.
2235
-	 *
2236
-	 * @param string $principalUri
2237
-	 * @param string $objectUri
2238
-	 * @return array
2239
-	 */
2240
-	public function getSchedulingObject($principalUri, $objectUri) {
2241
-		$query = $this->db->getQueryBuilder();
2242
-		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2243
-			->from('schedulingobjects')
2244
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2245
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2246
-			->execute();
2247
-
2248
-		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
2249
-
2250
-		if (!$row) {
2251
-			return null;
2252
-		}
2253
-
2254
-		return [
2255
-			'uri'          => $row['uri'],
2256
-			'calendardata' => $row['calendardata'],
2257
-			'lastmodified' => $row['lastmodified'],
2258
-			'etag'         => '"' . $row['etag'] . '"',
2259
-			'size'         => (int)$row['size'],
2260
-		];
2261
-	}
2262
-
2263
-	/**
2264
-	 * Returns all scheduling objects for the inbox collection.
2265
-	 *
2266
-	 * These objects should be returned as an array. Every item in the array
2267
-	 * should follow the same structure as returned from getSchedulingObject.
2268
-	 *
2269
-	 * The main difference is that 'calendardata' is optional.
2270
-	 *
2271
-	 * @param string $principalUri
2272
-	 * @return array
2273
-	 */
2274
-	public function getSchedulingObjects($principalUri) {
2275
-		$query = $this->db->getQueryBuilder();
2276
-		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2277
-				->from('schedulingobjects')
2278
-				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2279
-				->execute();
2280
-
2281
-		$result = [];
2282
-		foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
2283
-			$result[] = [
2284
-				'calendardata' => $row['calendardata'],
2285
-				'uri'          => $row['uri'],
2286
-				'lastmodified' => $row['lastmodified'],
2287
-				'etag'         => '"' . $row['etag'] . '"',
2288
-				'size'         => (int)$row['size'],
2289
-			];
2290
-		}
2291
-
2292
-		return $result;
2293
-	}
2294
-
2295
-	/**
2296
-	 * Deletes a scheduling object from the inbox collection.
2297
-	 *
2298
-	 * @param string $principalUri
2299
-	 * @param string $objectUri
2300
-	 * @return void
2301
-	 */
2302
-	public function deleteSchedulingObject($principalUri, $objectUri) {
2303
-		$query = $this->db->getQueryBuilder();
2304
-		$query->delete('schedulingobjects')
2305
-				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2306
-				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2307
-				->execute();
2308
-	}
2309
-
2310
-	/**
2311
-	 * Creates a new scheduling object. This should land in a users' inbox.
2312
-	 *
2313
-	 * @param string $principalUri
2314
-	 * @param string $objectUri
2315
-	 * @param string $objectData
2316
-	 * @return void
2317
-	 */
2318
-	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2319
-		$query = $this->db->getQueryBuilder();
2320
-		$query->insert('schedulingobjects')
2321
-			->values([
2322
-				'principaluri' => $query->createNamedParameter($principalUri),
2323
-				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2324
-				'uri' => $query->createNamedParameter($objectUri),
2325
-				'lastmodified' => $query->createNamedParameter(time()),
2326
-				'etag' => $query->createNamedParameter(md5($objectData)),
2327
-				'size' => $query->createNamedParameter(strlen($objectData))
2328
-			])
2329
-			->execute();
2330
-	}
2331
-
2332
-	/**
2333
-	 * Adds a change record to the calendarchanges table.
2334
-	 *
2335
-	 * @param mixed $calendarId
2336
-	 * @param string $objectUri
2337
-	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2338
-	 * @param int $calendarType
2339
-	 * @return void
2340
-	 */
2341
-	protected function addChange($calendarId, $objectUri, $operation, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2342
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2343
-
2344
-		$query = $this->db->getQueryBuilder();
2345
-		$query->select('synctoken')
2346
-			->from($table)
2347
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2348
-		$syncToken = (int)$query->execute()->fetchColumn();
2349
-
2350
-		$query = $this->db->getQueryBuilder();
2351
-		$query->insert('calendarchanges')
2352
-			->values([
2353
-				'uri' => $query->createNamedParameter($objectUri),
2354
-				'synctoken' => $query->createNamedParameter($syncToken),
2355
-				'calendarid' => $query->createNamedParameter($calendarId),
2356
-				'operation' => $query->createNamedParameter($operation),
2357
-				'calendartype' => $query->createNamedParameter($calendarType),
2358
-			])
2359
-			->execute();
2360
-
2361
-		$stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2362
-		$stmt->execute([
2363
-			$calendarId
2364
-		]);
2365
-	}
2366
-
2367
-	/**
2368
-	 * Parses some information from calendar objects, used for optimized
2369
-	 * calendar-queries.
2370
-	 *
2371
-	 * Returns an array with the following keys:
2372
-	 *   * etag - An md5 checksum of the object without the quotes.
2373
-	 *   * size - Size of the object in bytes
2374
-	 *   * componentType - VEVENT, VTODO or VJOURNAL
2375
-	 *   * firstOccurence
2376
-	 *   * lastOccurence
2377
-	 *   * uid - value of the UID property
2378
-	 *
2379
-	 * @param string $calendarData
2380
-	 * @return array
2381
-	 */
2382
-	public function getDenormalizedData($calendarData) {
2383
-		$vObject = Reader::read($calendarData);
2384
-		$componentType = null;
2385
-		$component = null;
2386
-		$firstOccurrence = null;
2387
-		$lastOccurrence = null;
2388
-		$uid = null;
2389
-		$classification = self::CLASSIFICATION_PUBLIC;
2390
-		foreach ($vObject->getComponents() as $component) {
2391
-			if ($component->name!=='VTIMEZONE') {
2392
-				$componentType = $component->name;
2393
-				$uid = (string)$component->UID;
2394
-				break;
2395
-			}
2396
-		}
2397
-		if (!$componentType) {
2398
-			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2399
-		}
2400
-		if ($componentType === 'VEVENT' && $component->DTSTART) {
2401
-			$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2402
-			// Finding the last occurrence is a bit harder
2403
-			if (!isset($component->RRULE)) {
2404
-				if (isset($component->DTEND)) {
2405
-					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2406
-				} elseif (isset($component->DURATION)) {
2407
-					$endDate = clone $component->DTSTART->getDateTime();
2408
-					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2409
-					$lastOccurrence = $endDate->getTimeStamp();
2410
-				} elseif (!$component->DTSTART->hasTime()) {
2411
-					$endDate = clone $component->DTSTART->getDateTime();
2412
-					$endDate->modify('+1 day');
2413
-					$lastOccurrence = $endDate->getTimeStamp();
2414
-				} else {
2415
-					$lastOccurrence = $firstOccurrence;
2416
-				}
2417
-			} else {
2418
-				$it = new EventIterator($vObject, (string)$component->UID);
2419
-				$maxDate = new DateTime(self::MAX_DATE);
2420
-				if ($it->isInfinite()) {
2421
-					$lastOccurrence = $maxDate->getTimestamp();
2422
-				} else {
2423
-					$end = $it->getDtEnd();
2424
-					while ($it->valid() && $end < $maxDate) {
2425
-						$end = $it->getDtEnd();
2426
-						$it->next();
2427
-					}
2428
-					$lastOccurrence = $end->getTimestamp();
2429
-				}
2430
-			}
2431
-		}
2432
-
2433
-		if ($component->CLASS) {
2434
-			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2435
-			switch ($component->CLASS->getValue()) {
2436
-				case 'PUBLIC':
2437
-					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2438
-					break;
2439
-				case 'CONFIDENTIAL':
2440
-					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2441
-					break;
2442
-			}
2443
-		}
2444
-		return [
2445
-			'etag' => md5($calendarData),
2446
-			'size' => strlen($calendarData),
2447
-			'componentType' => $componentType,
2448
-			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2449
-			'lastOccurence'  => $lastOccurrence,
2450
-			'uid' => $uid,
2451
-			'classification' => $classification
2452
-		];
2453
-	}
2454
-
2455
-	/**
2456
-	 * @param $cardData
2457
-	 * @return bool|string
2458
-	 */
2459
-	private function readBlob($cardData) {
2460
-		if (is_resource($cardData)) {
2461
-			return stream_get_contents($cardData);
2462
-		}
2463
-
2464
-		return $cardData;
2465
-	}
2466
-
2467
-	/**
2468
-	 * @param IShareable $shareable
2469
-	 * @param array $add
2470
-	 * @param array $remove
2471
-	 */
2472
-	public function updateShares($shareable, $add, $remove) {
2473
-		$calendarId = $shareable->getResourceId();
2474
-		$calendarRow = $this->getCalendarById($calendarId);
2475
-		$oldShares = $this->getShares($calendarId);
2476
-
2477
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2478
-			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2479
-			[
2480
-				'calendarId' => $calendarId,
2481
-				'calendarData' => $calendarRow,
2482
-				'shares' => $oldShares,
2483
-				'add' => $add,
2484
-				'remove' => $remove,
2485
-			]));
2486
-		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2487
-
2488
-		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2489
-	}
2490
-
2491
-	/**
2492
-	 * @param int $resourceId
2493
-	 * @param int $calendarType
2494
-	 * @return array
2495
-	 */
2496
-	public function getShares($resourceId, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2497
-		return $this->calendarSharingBackend->getShares($resourceId);
2498
-	}
2499
-
2500
-	/**
2501
-	 * @param boolean $value
2502
-	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2503
-	 * @return string|null
2504
-	 */
2505
-	public function setPublishStatus($value, $calendar) {
2506
-		$calendarId = $calendar->getResourceId();
2507
-		$calendarData = $this->getCalendarById($calendarId);
2508
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
2509
-			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2510
-			[
2511
-				'calendarId' => $calendarId,
2512
-				'calendarData' => $calendarData,
2513
-				'public' => $value,
2514
-			]));
2515
-
2516
-		$query = $this->db->getQueryBuilder();
2517
-		if ($value) {
2518
-			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2519
-			$query->insert('dav_shares')
2520
-				->values([
2521
-					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2522
-					'type' => $query->createNamedParameter('calendar'),
2523
-					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2524
-					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2525
-					'publicuri' => $query->createNamedParameter($publicUri)
2526
-				]);
2527
-			$query->execute();
2528
-
2529
-			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2530
-			return $publicUri;
2531
-		}
2532
-		$query->delete('dav_shares')
2533
-			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2534
-			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2535
-		$query->execute();
2536
-
2537
-		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2538
-		return null;
2539
-	}
2540
-
2541
-	/**
2542
-	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2543
-	 * @return mixed
2544
-	 */
2545
-	public function getPublishStatus($calendar) {
2546
-		$query = $this->db->getQueryBuilder();
2547
-		$result = $query->select('publicuri')
2548
-			->from('dav_shares')
2549
-			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2550
-			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2551
-			->execute();
2552
-
2553
-		$row = $result->fetch();
2554
-		$result->closeCursor();
2555
-		return $row ? reset($row) : false;
2556
-	}
2557
-
2558
-	/**
2559
-	 * @param int $resourceId
2560
-	 * @param array $acl
2561
-	 * @return array
2562
-	 */
2563
-	public function applyShareAcl($resourceId, $acl) {
2564
-		return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2565
-	}
2566
-
2567
-
2568
-
2569
-	/**
2570
-	 * update properties table
2571
-	 *
2572
-	 * @param int $calendarId
2573
-	 * @param string $objectUri
2574
-	 * @param string $calendarData
2575
-	 * @param int $calendarType
2576
-	 */
2577
-	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2578
-		$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2579
-
2580
-		try {
2581
-			$vCalendar = $this->readCalendarData($calendarData);
2582
-		} catch (\Exception $ex) {
2583
-			return;
2584
-		}
2585
-
2586
-		$this->purgeProperties($calendarId, $objectId);
2587
-
2588
-		$query = $this->db->getQueryBuilder();
2589
-		$query->insert($this->dbObjectPropertiesTable)
2590
-			->values(
2591
-				[
2592
-					'calendarid' => $query->createNamedParameter($calendarId),
2593
-					'calendartype' => $query->createNamedParameter($calendarType),
2594
-					'objectid' => $query->createNamedParameter($objectId),
2595
-					'name' => $query->createParameter('name'),
2596
-					'parameter' => $query->createParameter('parameter'),
2597
-					'value' => $query->createParameter('value'),
2598
-				]
2599
-			);
2600
-
2601
-		$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2602
-		foreach ($vCalendar->getComponents() as $component) {
2603
-			if (!in_array($component->name, $indexComponents)) {
2604
-				continue;
2605
-			}
2606
-
2607
-			foreach ($component->children() as $property) {
2608
-				if (in_array($property->name, self::$indexProperties)) {
2609
-					$value = $property->getValue();
2610
-					// is this a shitty db?
2611
-					if (!$this->db->supports4ByteText()) {
2612
-						$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2613
-					}
2614
-					$value = mb_substr($value, 0, 254);
2615
-
2616
-					$query->setParameter('name', $property->name);
2617
-					$query->setParameter('parameter', null);
2618
-					$query->setParameter('value', $value);
2619
-					$query->execute();
2620
-				}
2621
-
2622
-				if (array_key_exists($property->name, self::$indexParameters)) {
2623
-					$parameters = $property->parameters();
2624
-					$indexedParametersForProperty = self::$indexParameters[$property->name];
2625
-
2626
-					foreach ($parameters as $key => $value) {
2627
-						if (in_array($key, $indexedParametersForProperty)) {
2628
-							// is this a shitty db?
2629
-							if ($this->db->supports4ByteText()) {
2630
-								$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2631
-							}
2632
-
2633
-							$query->setParameter('name', $property->name);
2634
-							$query->setParameter('parameter', mb_substr($key, 0, 254));
2635
-							$query->setParameter('value', mb_substr($value, 0, 254));
2636
-							$query->execute();
2637
-						}
2638
-					}
2639
-				}
2640
-			}
2641
-		}
2642
-	}
2643
-
2644
-	/**
2645
-	 * deletes all birthday calendars
2646
-	 */
2647
-	public function deleteAllBirthdayCalendars() {
2648
-		$query = $this->db->getQueryBuilder();
2649
-		$result = $query->select(['id'])->from('calendars')
2650
-			->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
2651
-			->execute();
2652
-
2653
-		$ids = $result->fetchAll();
2654
-		foreach ($ids as $id) {
2655
-			$this->deleteCalendar($id['id']);
2656
-		}
2657
-	}
2658
-
2659
-	/**
2660
-	 * @param $subscriptionId
2661
-	 */
2662
-	public function purgeAllCachedEventsForSubscription($subscriptionId) {
2663
-		$query = $this->db->getQueryBuilder();
2664
-		$query->select('uri')
2665
-			->from('calendarobjects')
2666
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2667
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2668
-		$stmt = $query->execute();
2669
-
2670
-		$uris = [];
2671
-		foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
2672
-			$uris[] = $row['uri'];
2673
-		}
2674
-		$stmt->closeCursor();
2675
-
2676
-		$query = $this->db->getQueryBuilder();
2677
-		$query->delete('calendarobjects')
2678
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2679
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2680
-			->execute();
2681
-
2682
-		$query->delete('calendarchanges')
2683
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2684
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2685
-			->execute();
2686
-
2687
-		$query->delete($this->dbObjectPropertiesTable)
2688
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2689
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2690
-			->execute();
2691
-
2692
-		foreach ($uris as $uri) {
2693
-			$this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
2694
-		}
2695
-	}
2696
-
2697
-	/**
2698
-	 * Move a calendar from one user to another
2699
-	 *
2700
-	 * @param string $uriName
2701
-	 * @param string $uriOrigin
2702
-	 * @param string $uriDestination
2703
-	 */
2704
-	public function moveCalendar($uriName, $uriOrigin, $uriDestination) {
2705
-		$query = $this->db->getQueryBuilder();
2706
-		$query->update('calendars')
2707
-			->set('principaluri', $query->createNamedParameter($uriDestination))
2708
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
2709
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
2710
-			->execute();
2711
-	}
2712
-
2713
-	/**
2714
-	 * read VCalendar data into a VCalendar object
2715
-	 *
2716
-	 * @param string $objectData
2717
-	 * @return VCalendar
2718
-	 */
2719
-	protected function readCalendarData($objectData) {
2720
-		return Reader::read($objectData);
2721
-	}
2722
-
2723
-	/**
2724
-	 * delete all properties from a given calendar object
2725
-	 *
2726
-	 * @param int $calendarId
2727
-	 * @param int $objectId
2728
-	 */
2729
-	protected function purgeProperties($calendarId, $objectId) {
2730
-		$query = $this->db->getQueryBuilder();
2731
-		$query->delete($this->dbObjectPropertiesTable)
2732
-			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
2733
-			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2734
-		$query->execute();
2735
-	}
2736
-
2737
-	/**
2738
-	 * get ID from a given calendar object
2739
-	 *
2740
-	 * @param int $calendarId
2741
-	 * @param string $uri
2742
-	 * @param int $calendarType
2743
-	 * @return int
2744
-	 */
2745
-	protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
2746
-		$query = $this->db->getQueryBuilder();
2747
-		$query->select('id')
2748
-			->from('calendarobjects')
2749
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
2750
-			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
2751
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
2752
-
2753
-		$result = $query->execute();
2754
-		$objectIds = $result->fetch();
2755
-		$result->closeCursor();
2756
-
2757
-		if (!isset($objectIds['id'])) {
2758
-			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2759
-		}
2760
-
2761
-		return (int)$objectIds['id'];
2762
-	}
2763
-
2764
-	/**
2765
-	 * return legacy endpoint principal name to new principal name
2766
-	 *
2767
-	 * @param $principalUri
2768
-	 * @param $toV2
2769
-	 * @return string
2770
-	 */
2771
-	private function convertPrincipal($principalUri, $toV2) {
2772
-		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
2773
-			list(, $name) = Uri\split($principalUri);
2774
-			if ($toV2 === true) {
2775
-				return "principals/users/$name";
2776
-			}
2777
-			return "principals/$name";
2778
-		}
2779
-		return $principalUri;
2780
-	}
2781
-
2782
-	/**
2783
-	 * adds information about an owner to the calendar data
2784
-	 *
2785
-	 * @param $calendarInfo
2786
-	 */
2787
-	private function addOwnerPrincipal(&$calendarInfo) {
2788
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2789
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2790
-		if (isset($calendarInfo[$ownerPrincipalKey])) {
2791
-			$uri = $calendarInfo[$ownerPrincipalKey];
2792
-		} else {
2793
-			$uri = $calendarInfo['principaluri'];
2794
-		}
2795
-
2796
-		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
2797
-		if (isset($principalInformation['{DAV:}displayname'])) {
2798
-			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
2799
-		}
2800
-	}
96
+    public const CALENDAR_TYPE_CALENDAR = 0;
97
+    public const CALENDAR_TYPE_SUBSCRIPTION = 1;
98
+
99
+    public const PERSONAL_CALENDAR_URI = 'personal';
100
+    public const PERSONAL_CALENDAR_NAME = 'Personal';
101
+
102
+    public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
103
+    public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
104
+
105
+    /**
106
+     * We need to specify a max date, because we need to stop *somewhere*
107
+     *
108
+     * On 32 bit system the maximum for a signed integer is 2147483647, so
109
+     * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
110
+     * in 2038-01-19 to avoid problems when the date is converted
111
+     * to a unix timestamp.
112
+     */
113
+    public const MAX_DATE = '2038-01-01';
114
+
115
+    public const ACCESS_PUBLIC = 4;
116
+    public const CLASSIFICATION_PUBLIC = 0;
117
+    public const CLASSIFICATION_PRIVATE = 1;
118
+    public const CLASSIFICATION_CONFIDENTIAL = 2;
119
+
120
+    /**
121
+     * List of CalDAV properties, and how they map to database field names
122
+     * Add your own properties by simply adding on to this array.
123
+     *
124
+     * Note that only string-based properties are supported here.
125
+     *
126
+     * @var array
127
+     */
128
+    public $propertyMap = [
129
+        '{DAV:}displayname'                          => 'displayname',
130
+        '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
131
+        '{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
132
+        '{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
133
+        '{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
134
+    ];
135
+
136
+    /**
137
+     * List of subscription properties, and how they map to database field names.
138
+     *
139
+     * @var array
140
+     */
141
+    public $subscriptionPropertyMap = [
142
+        '{DAV:}displayname'                                           => 'displayname',
143
+        '{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
144
+        '{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
145
+        '{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
146
+        '{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
147
+        '{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
148
+        '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
149
+    ];
150
+
151
+    /** @var array properties to index */
152
+    public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION',
153
+        'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT',
154
+        'ORGANIZER'];
155
+
156
+    /** @var array parameters to index */
157
+    public static $indexParameters = [
158
+        'ATTENDEE' => ['CN'],
159
+        'ORGANIZER' => ['CN'],
160
+    ];
161
+
162
+    /**
163
+     * @var string[] Map of uid => display name
164
+     */
165
+    protected $userDisplayNames;
166
+
167
+    /** @var IDBConnection */
168
+    private $db;
169
+
170
+    /** @var Backend */
171
+    private $calendarSharingBackend;
172
+
173
+    /** @var Principal */
174
+    private $principalBackend;
175
+
176
+    /** @var IUserManager */
177
+    private $userManager;
178
+
179
+    /** @var ISecureRandom */
180
+    private $random;
181
+
182
+    /** @var ILogger */
183
+    private $logger;
184
+
185
+    /** @var IEventDispatcher */
186
+    private $dispatcher;
187
+
188
+    /** @var EventDispatcherInterface */
189
+    private $legacyDispatcher;
190
+
191
+    /** @var bool */
192
+    private $legacyEndpoint;
193
+
194
+    /** @var string */
195
+    private $dbObjectPropertiesTable = 'calendarobjects_props';
196
+
197
+    /**
198
+     * CalDavBackend constructor.
199
+     *
200
+     * @param IDBConnection $db
201
+     * @param Principal $principalBackend
202
+     * @param IUserManager $userManager
203
+     * @param IGroupManager $groupManager
204
+     * @param ISecureRandom $random
205
+     * @param ILogger $logger
206
+     * @param IEventDispatcher $dispatcher
207
+     * @param EventDispatcherInterface $legacyDispatcher
208
+     * @param bool $legacyEndpoint
209
+     */
210
+    public function __construct(IDBConnection $db,
211
+                                Principal $principalBackend,
212
+                                IUserManager $userManager,
213
+                                IGroupManager $groupManager,
214
+                                ISecureRandom $random,
215
+                                ILogger $logger,
216
+                                IEventDispatcher $dispatcher,
217
+                                EventDispatcherInterface $legacyDispatcher,
218
+                                bool $legacyEndpoint = false) {
219
+        $this->db = $db;
220
+        $this->principalBackend = $principalBackend;
221
+        $this->userManager = $userManager;
222
+        $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
223
+        $this->random = $random;
224
+        $this->logger = $logger;
225
+        $this->dispatcher = $dispatcher;
226
+        $this->legacyDispatcher = $legacyDispatcher;
227
+        $this->legacyEndpoint = $legacyEndpoint;
228
+    }
229
+
230
+    /**
231
+     * Return the number of calendars for a principal
232
+     *
233
+     * By default this excludes the automatically generated birthday calendar
234
+     *
235
+     * @param $principalUri
236
+     * @param bool $excludeBirthday
237
+     * @return int
238
+     */
239
+    public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
240
+        $principalUri = $this->convertPrincipal($principalUri, true);
241
+        $query = $this->db->getQueryBuilder();
242
+        $query->select($query->func()->count('*'))
243
+            ->from('calendars')
244
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
245
+
246
+        if ($excludeBirthday) {
247
+            $query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
248
+        }
249
+
250
+        return (int)$query->execute()->fetchColumn();
251
+    }
252
+
253
+    /**
254
+     * Returns a list of calendars for a principal.
255
+     *
256
+     * Every project is an array with the following keys:
257
+     *  * id, a unique id that will be used by other functions to modify the
258
+     *    calendar. This can be the same as the uri or a database key.
259
+     *  * uri, which the basename of the uri with which the calendar is
260
+     *    accessed.
261
+     *  * principaluri. The owner of the calendar. Almost always the same as
262
+     *    principalUri passed to this method.
263
+     *
264
+     * Furthermore it can contain webdav properties in clark notation. A very
265
+     * common one is '{DAV:}displayname'.
266
+     *
267
+     * Many clients also require:
268
+     * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
269
+     * For this property, you can just return an instance of
270
+     * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
271
+     *
272
+     * If you return {http://sabredav.org/ns}read-only and set the value to 1,
273
+     * ACL will automatically be put in read-only mode.
274
+     *
275
+     * @param string $principalUri
276
+     * @return array
277
+     */
278
+    public function getCalendarsForUser($principalUri) {
279
+        $principalUriOriginal = $principalUri;
280
+        $principalUri = $this->convertPrincipal($principalUri, true);
281
+        $fields = array_values($this->propertyMap);
282
+        $fields[] = 'id';
283
+        $fields[] = 'uri';
284
+        $fields[] = 'synctoken';
285
+        $fields[] = 'components';
286
+        $fields[] = 'principaluri';
287
+        $fields[] = 'transparent';
288
+
289
+        // Making fields a comma-delimited list
290
+        $query = $this->db->getQueryBuilder();
291
+        $query->select($fields)->from('calendars')
292
+                ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
293
+                ->orderBy('calendarorder', 'ASC');
294
+        $stmt = $query->execute();
295
+
296
+        $calendars = [];
297
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
298
+            $components = [];
299
+            if ($row['components']) {
300
+                $components = explode(',',$row['components']);
301
+            }
302
+
303
+            $calendar = [
304
+                'id' => $row['id'],
305
+                'uri' => $row['uri'],
306
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
307
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
308
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
309
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
310
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
311
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
312
+            ];
313
+
314
+            foreach ($this->propertyMap as $xmlName=>$dbName) {
315
+                $calendar[$xmlName] = $row[$dbName];
316
+            }
317
+
318
+            $this->addOwnerPrincipal($calendar);
319
+
320
+            if (!isset($calendars[$calendar['id']])) {
321
+                $calendars[$calendar['id']] = $calendar;
322
+            }
323
+        }
324
+
325
+        $stmt->closeCursor();
326
+
327
+        // query for shared calendars
328
+        $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
329
+        $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
330
+
331
+        $principals = array_map(function ($principal) {
332
+            return urldecode($principal);
333
+        }, $principals);
334
+        $principals[]= $principalUri;
335
+
336
+        $fields = array_values($this->propertyMap);
337
+        $fields[] = 'a.id';
338
+        $fields[] = 'a.uri';
339
+        $fields[] = 'a.synctoken';
340
+        $fields[] = 'a.components';
341
+        $fields[] = 'a.principaluri';
342
+        $fields[] = 'a.transparent';
343
+        $fields[] = 's.access';
344
+        $query = $this->db->getQueryBuilder();
345
+        $result = $query->select($fields)
346
+            ->from('dav_shares', 's')
347
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
348
+            ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
349
+            ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
350
+            ->setParameter('type', 'calendar')
351
+            ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
352
+            ->execute();
353
+
354
+        $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
355
+        while ($row = $result->fetch()) {
356
+            if ($row['principaluri'] === $principalUri) {
357
+                continue;
358
+            }
359
+
360
+            $readOnly = (int) $row['access'] === Backend::ACCESS_READ;
361
+            if (isset($calendars[$row['id']])) {
362
+                if ($readOnly) {
363
+                    // New share can not have more permissions then the old one.
364
+                    continue;
365
+                }
366
+                if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
367
+                    $calendars[$row['id']][$readOnlyPropertyName] === 0) {
368
+                    // Old share is already read-write, no more permissions can be gained
369
+                    continue;
370
+                }
371
+            }
372
+
373
+            list(, $name) = Uri\split($row['principaluri']);
374
+            $uri = $row['uri'] . '_shared_by_' . $name;
375
+            $row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
376
+            $components = [];
377
+            if ($row['components']) {
378
+                $components = explode(',',$row['components']);
379
+            }
380
+            $calendar = [
381
+                'id' => $row['id'],
382
+                'uri' => $uri,
383
+                'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
384
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
385
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
386
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
387
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
388
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
389
+                $readOnlyPropertyName => $readOnly,
390
+            ];
391
+
392
+            foreach ($this->propertyMap as $xmlName=>$dbName) {
393
+                $calendar[$xmlName] = $row[$dbName];
394
+            }
395
+
396
+            $this->addOwnerPrincipal($calendar);
397
+
398
+            $calendars[$calendar['id']] = $calendar;
399
+        }
400
+        $result->closeCursor();
401
+
402
+        return array_values($calendars);
403
+    }
404
+
405
+    /**
406
+     * @param $principalUri
407
+     * @return array
408
+     */
409
+    public function getUsersOwnCalendars($principalUri) {
410
+        $principalUri = $this->convertPrincipal($principalUri, true);
411
+        $fields = array_values($this->propertyMap);
412
+        $fields[] = 'id';
413
+        $fields[] = 'uri';
414
+        $fields[] = 'synctoken';
415
+        $fields[] = 'components';
416
+        $fields[] = 'principaluri';
417
+        $fields[] = 'transparent';
418
+        // Making fields a comma-delimited list
419
+        $query = $this->db->getQueryBuilder();
420
+        $query->select($fields)->from('calendars')
421
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
422
+            ->orderBy('calendarorder', 'ASC');
423
+        $stmt = $query->execute();
424
+        $calendars = [];
425
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
426
+            $components = [];
427
+            if ($row['components']) {
428
+                $components = explode(',',$row['components']);
429
+            }
430
+            $calendar = [
431
+                'id' => $row['id'],
432
+                'uri' => $row['uri'],
433
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
434
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
435
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
436
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
437
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
438
+            ];
439
+            foreach ($this->propertyMap as $xmlName=>$dbName) {
440
+                $calendar[$xmlName] = $row[$dbName];
441
+            }
442
+
443
+            $this->addOwnerPrincipal($calendar);
444
+
445
+            if (!isset($calendars[$calendar['id']])) {
446
+                $calendars[$calendar['id']] = $calendar;
447
+            }
448
+        }
449
+        $stmt->closeCursor();
450
+        return array_values($calendars);
451
+    }
452
+
453
+
454
+    /**
455
+     * @param $uid
456
+     * @return string
457
+     */
458
+    private function getUserDisplayName($uid) {
459
+        if (!isset($this->userDisplayNames[$uid])) {
460
+            $user = $this->userManager->get($uid);
461
+
462
+            if ($user instanceof IUser) {
463
+                $this->userDisplayNames[$uid] = $user->getDisplayName();
464
+            } else {
465
+                $this->userDisplayNames[$uid] = $uid;
466
+            }
467
+        }
468
+
469
+        return $this->userDisplayNames[$uid];
470
+    }
471
+
472
+    /**
473
+     * @return array
474
+     */
475
+    public function getPublicCalendars() {
476
+        $fields = array_values($this->propertyMap);
477
+        $fields[] = 'a.id';
478
+        $fields[] = 'a.uri';
479
+        $fields[] = 'a.synctoken';
480
+        $fields[] = 'a.components';
481
+        $fields[] = 'a.principaluri';
482
+        $fields[] = 'a.transparent';
483
+        $fields[] = 's.access';
484
+        $fields[] = 's.publicuri';
485
+        $calendars = [];
486
+        $query = $this->db->getQueryBuilder();
487
+        $result = $query->select($fields)
488
+            ->from('dav_shares', 's')
489
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
490
+            ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
491
+            ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
492
+            ->execute();
493
+
494
+        while ($row = $result->fetch()) {
495
+            list(, $name) = Uri\split($row['principaluri']);
496
+            $row['displayname'] = $row['displayname'] . "($name)";
497
+            $components = [];
498
+            if ($row['components']) {
499
+                $components = explode(',',$row['components']);
500
+            }
501
+            $calendar = [
502
+                'id' => $row['id'],
503
+                'uri' => $row['publicuri'],
504
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
505
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
506
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
507
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
508
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
509
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
510
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
511
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
512
+            ];
513
+
514
+            foreach ($this->propertyMap as $xmlName=>$dbName) {
515
+                $calendar[$xmlName] = $row[$dbName];
516
+            }
517
+
518
+            $this->addOwnerPrincipal($calendar);
519
+
520
+            if (!isset($calendars[$calendar['id']])) {
521
+                $calendars[$calendar['id']] = $calendar;
522
+            }
523
+        }
524
+        $result->closeCursor();
525
+
526
+        return array_values($calendars);
527
+    }
528
+
529
+    /**
530
+     * @param string $uri
531
+     * @return array
532
+     * @throws NotFound
533
+     */
534
+    public function getPublicCalendar($uri) {
535
+        $fields = array_values($this->propertyMap);
536
+        $fields[] = 'a.id';
537
+        $fields[] = 'a.uri';
538
+        $fields[] = 'a.synctoken';
539
+        $fields[] = 'a.components';
540
+        $fields[] = 'a.principaluri';
541
+        $fields[] = 'a.transparent';
542
+        $fields[] = 's.access';
543
+        $fields[] = 's.publicuri';
544
+        $query = $this->db->getQueryBuilder();
545
+        $result = $query->select($fields)
546
+            ->from('dav_shares', 's')
547
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
548
+            ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
549
+            ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
550
+            ->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
551
+            ->execute();
552
+
553
+        $row = $result->fetch(\PDO::FETCH_ASSOC);
554
+
555
+        $result->closeCursor();
556
+
557
+        if ($row === false) {
558
+            throw new NotFound('Node with name \'' . $uri . '\' could not be found');
559
+        }
560
+
561
+        list(, $name) = Uri\split($row['principaluri']);
562
+        $row['displayname'] = $row['displayname'] . ' ' . "($name)";
563
+        $components = [];
564
+        if ($row['components']) {
565
+            $components = explode(',',$row['components']);
566
+        }
567
+        $calendar = [
568
+            'id' => $row['id'],
569
+            'uri' => $row['publicuri'],
570
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
571
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
572
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
573
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
574
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
575
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
576
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
577
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
578
+        ];
579
+
580
+        foreach ($this->propertyMap as $xmlName=>$dbName) {
581
+            $calendar[$xmlName] = $row[$dbName];
582
+        }
583
+
584
+        $this->addOwnerPrincipal($calendar);
585
+
586
+        return $calendar;
587
+    }
588
+
589
+    /**
590
+     * @param string $principal
591
+     * @param string $uri
592
+     * @return array|null
593
+     */
594
+    public function getCalendarByUri($principal, $uri) {
595
+        $fields = array_values($this->propertyMap);
596
+        $fields[] = 'id';
597
+        $fields[] = 'uri';
598
+        $fields[] = 'synctoken';
599
+        $fields[] = 'components';
600
+        $fields[] = 'principaluri';
601
+        $fields[] = 'transparent';
602
+
603
+        // Making fields a comma-delimited list
604
+        $query = $this->db->getQueryBuilder();
605
+        $query->select($fields)->from('calendars')
606
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
607
+            ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
608
+            ->setMaxResults(1);
609
+        $stmt = $query->execute();
610
+
611
+        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
612
+        $stmt->closeCursor();
613
+        if ($row === false) {
614
+            return null;
615
+        }
616
+
617
+        $components = [];
618
+        if ($row['components']) {
619
+            $components = explode(',',$row['components']);
620
+        }
621
+
622
+        $calendar = [
623
+            'id' => $row['id'],
624
+            'uri' => $row['uri'],
625
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
626
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
627
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
628
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
629
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
630
+        ];
631
+
632
+        foreach ($this->propertyMap as $xmlName=>$dbName) {
633
+            $calendar[$xmlName] = $row[$dbName];
634
+        }
635
+
636
+        $this->addOwnerPrincipal($calendar);
637
+
638
+        return $calendar;
639
+    }
640
+
641
+    /**
642
+     * @param $calendarId
643
+     * @return array|null
644
+     */
645
+    public function getCalendarById($calendarId) {
646
+        $fields = array_values($this->propertyMap);
647
+        $fields[] = 'id';
648
+        $fields[] = 'uri';
649
+        $fields[] = 'synctoken';
650
+        $fields[] = 'components';
651
+        $fields[] = 'principaluri';
652
+        $fields[] = 'transparent';
653
+
654
+        // Making fields a comma-delimited list
655
+        $query = $this->db->getQueryBuilder();
656
+        $query->select($fields)->from('calendars')
657
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
658
+            ->setMaxResults(1);
659
+        $stmt = $query->execute();
660
+
661
+        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
662
+        $stmt->closeCursor();
663
+        if ($row === false) {
664
+            return null;
665
+        }
666
+
667
+        $components = [];
668
+        if ($row['components']) {
669
+            $components = explode(',',$row['components']);
670
+        }
671
+
672
+        $calendar = [
673
+            'id' => $row['id'],
674
+            'uri' => $row['uri'],
675
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
676
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
677
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
678
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
679
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
680
+        ];
681
+
682
+        foreach ($this->propertyMap as $xmlName=>$dbName) {
683
+            $calendar[$xmlName] = $row[$dbName];
684
+        }
685
+
686
+        $this->addOwnerPrincipal($calendar);
687
+
688
+        return $calendar;
689
+    }
690
+
691
+    /**
692
+     * @param $subscriptionId
693
+     */
694
+    public function getSubscriptionById($subscriptionId) {
695
+        $fields = array_values($this->subscriptionPropertyMap);
696
+        $fields[] = 'id';
697
+        $fields[] = 'uri';
698
+        $fields[] = 'source';
699
+        $fields[] = 'synctoken';
700
+        $fields[] = 'principaluri';
701
+        $fields[] = 'lastmodified';
702
+
703
+        $query = $this->db->getQueryBuilder();
704
+        $query->select($fields)
705
+            ->from('calendarsubscriptions')
706
+            ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
707
+            ->orderBy('calendarorder', 'asc');
708
+        $stmt =$query->execute();
709
+
710
+        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
711
+        $stmt->closeCursor();
712
+        if ($row === false) {
713
+            return null;
714
+        }
715
+
716
+        $subscription = [
717
+            'id'           => $row['id'],
718
+            'uri'          => $row['uri'],
719
+            'principaluri' => $row['principaluri'],
720
+            'source'       => $row['source'],
721
+            'lastmodified' => $row['lastmodified'],
722
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
723
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
724
+        ];
725
+
726
+        foreach ($this->subscriptionPropertyMap as $xmlName=>$dbName) {
727
+            if (!is_null($row[$dbName])) {
728
+                $subscription[$xmlName] = $row[$dbName];
729
+            }
730
+        }
731
+
732
+        return $subscription;
733
+    }
734
+
735
+    /**
736
+     * Creates a new calendar for a principal.
737
+     *
738
+     * If the creation was a success, an id must be returned that can be used to reference
739
+     * this calendar in other methods, such as updateCalendar.
740
+     *
741
+     * @param string $principalUri
742
+     * @param string $calendarUri
743
+     * @param array $properties
744
+     * @return int
745
+     */
746
+    public function createCalendar($principalUri, $calendarUri, array $properties) {
747
+        $values = [
748
+            'principaluri' => $this->convertPrincipal($principalUri, true),
749
+            'uri'          => $calendarUri,
750
+            'synctoken'    => 1,
751
+            'transparent'  => 0,
752
+            'components'   => 'VEVENT,VTODO',
753
+            'displayname'  => $calendarUri
754
+        ];
755
+
756
+        // Default value
757
+        $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
758
+        if (isset($properties[$sccs])) {
759
+            if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
760
+                throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
761
+            }
762
+            $values['components'] = implode(',',$properties[$sccs]->getValue());
763
+        } elseif (isset($properties['components'])) {
764
+            // Allow to provide components internally without having
765
+            // to create a SupportedCalendarComponentSet object
766
+            $values['components'] = $properties['components'];
767
+        }
768
+
769
+        $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
770
+        if (isset($properties[$transp])) {
771
+            $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
772
+        }
773
+
774
+        foreach ($this->propertyMap as $xmlName=>$dbName) {
775
+            if (isset($properties[$xmlName])) {
776
+                $values[$dbName] = $properties[$xmlName];
777
+            }
778
+        }
779
+
780
+        $query = $this->db->getQueryBuilder();
781
+        $query->insert('calendars');
782
+        foreach ($values as $column => $value) {
783
+            $query->setValue($column, $query->createNamedParameter($value));
784
+        }
785
+        $query->execute();
786
+        $calendarId = $query->getLastInsertId();
787
+
788
+        $calendarData = $this->getCalendarById($calendarId);
789
+        $this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
790
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
791
+            '\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
792
+            [
793
+                'calendarId' => $calendarId,
794
+                'calendarData' => $calendarData,
795
+            ]));
796
+
797
+        return $calendarId;
798
+    }
799
+
800
+    /**
801
+     * Updates properties for a calendar.
802
+     *
803
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
804
+     * To do the actual updates, you must tell this object which properties
805
+     * you're going to process with the handle() method.
806
+     *
807
+     * Calling the handle method is like telling the PropPatch object "I
808
+     * promise I can handle updating this property".
809
+     *
810
+     * Read the PropPatch documentation for more info and examples.
811
+     *
812
+     * @param mixed $calendarId
813
+     * @param PropPatch $propPatch
814
+     * @return void
815
+     */
816
+    public function updateCalendar($calendarId, PropPatch $propPatch) {
817
+        $supportedProperties = array_keys($this->propertyMap);
818
+        $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
819
+
820
+        $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
821
+            $newValues = [];
822
+            foreach ($mutations as $propertyName => $propertyValue) {
823
+                switch ($propertyName) {
824
+                    case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
825
+                        $fieldName = 'transparent';
826
+                        $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
827
+                        break;
828
+                    default:
829
+                        $fieldName = $this->propertyMap[$propertyName];
830
+                        $newValues[$fieldName] = $propertyValue;
831
+                        break;
832
+                }
833
+            }
834
+            $query = $this->db->getQueryBuilder();
835
+            $query->update('calendars');
836
+            foreach ($newValues as $fieldName => $value) {
837
+                $query->set($fieldName, $query->createNamedParameter($value));
838
+            }
839
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
840
+            $query->execute();
841
+
842
+            $this->addChange($calendarId, "", 2);
843
+
844
+            $calendarData = $this->getCalendarById($calendarId);
845
+            $shares = $this->getShares($calendarId);
846
+            $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
847
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
848
+                '\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
849
+                [
850
+                    'calendarId' => $calendarId,
851
+                    'calendarData' => $calendarData,
852
+                    'shares' => $shares,
853
+                    'propertyMutations' => $mutations,
854
+                ]));
855
+
856
+            return true;
857
+        });
858
+    }
859
+
860
+    /**
861
+     * Delete a calendar and all it's objects
862
+     *
863
+     * @param mixed $calendarId
864
+     * @return void
865
+     */
866
+    public function deleteCalendar($calendarId) {
867
+        $calendarData = $this->getCalendarById($calendarId);
868
+        $shares = $this->getShares($calendarId);
869
+
870
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', new GenericEvent(
871
+            '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar',
872
+            [
873
+                'calendarId' => $calendarId,
874
+                'calendarData' => $calendarData,
875
+                'shares' => $shares,
876
+            ]));
877
+
878
+        $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?');
879
+        $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
880
+
881
+        $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
882
+        $stmt->execute([$calendarId]);
883
+
884
+        $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?');
885
+        $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
886
+
887
+        $this->calendarSharingBackend->deleteAllShares($calendarId);
888
+
889
+        $query = $this->db->getQueryBuilder();
890
+        $query->delete($this->dbObjectPropertiesTable)
891
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
892
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
893
+            ->execute();
894
+
895
+        if ($calendarData) {
896
+            $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
897
+        }
898
+    }
899
+
900
+    /**
901
+     * Delete all of an user's shares
902
+     *
903
+     * @param string $principaluri
904
+     * @return void
905
+     */
906
+    public function deleteAllSharesByUser($principaluri) {
907
+        $this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
908
+    }
909
+
910
+    /**
911
+     * Returns all calendar objects within a calendar.
912
+     *
913
+     * Every item contains an array with the following keys:
914
+     *   * calendardata - The iCalendar-compatible calendar data
915
+     *   * uri - a unique key which will be used to construct the uri. This can
916
+     *     be any arbitrary string, but making sure it ends with '.ics' is a
917
+     *     good idea. This is only the basename, or filename, not the full
918
+     *     path.
919
+     *   * lastmodified - a timestamp of the last modification time
920
+     *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
921
+     *   '"abcdef"')
922
+     *   * size - The size of the calendar objects, in bytes.
923
+     *   * component - optional, a string containing the type of object, such
924
+     *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
925
+     *     the Content-Type header.
926
+     *
927
+     * Note that the etag is optional, but it's highly encouraged to return for
928
+     * speed reasons.
929
+     *
930
+     * The calendardata is also optional. If it's not returned
931
+     * 'getCalendarObject' will be called later, which *is* expected to return
932
+     * calendardata.
933
+     *
934
+     * If neither etag or size are specified, the calendardata will be
935
+     * used/fetched to determine these numbers. If both are specified the
936
+     * amount of times this is needed is reduced by a great degree.
937
+     *
938
+     * @param mixed $calendarId
939
+     * @param int $calendarType
940
+     * @return array
941
+     */
942
+    public function getCalendarObjects($calendarId, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
943
+        $query = $this->db->getQueryBuilder();
944
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
945
+            ->from('calendarobjects')
946
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
947
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
948
+        $stmt = $query->execute();
949
+
950
+        $result = [];
951
+        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
952
+            $result[] = [
953
+                'id'           => $row['id'],
954
+                'uri'          => $row['uri'],
955
+                'lastmodified' => $row['lastmodified'],
956
+                'etag'         => '"' . $row['etag'] . '"',
957
+                'calendarid'   => $row['calendarid'],
958
+                'size'         => (int)$row['size'],
959
+                'component'    => strtolower($row['componenttype']),
960
+                'classification'=> (int)$row['classification']
961
+            ];
962
+        }
963
+
964
+        return $result;
965
+    }
966
+
967
+    /**
968
+     * Returns information from a single calendar object, based on it's object
969
+     * uri.
970
+     *
971
+     * The object uri is only the basename, or filename and not a full path.
972
+     *
973
+     * The returned array must have the same keys as getCalendarObjects. The
974
+     * 'calendardata' object is required here though, while it's not required
975
+     * for getCalendarObjects.
976
+     *
977
+     * This method must return null if the object did not exist.
978
+     *
979
+     * @param mixed $calendarId
980
+     * @param string $objectUri
981
+     * @param int $calendarType
982
+     * @return array|null
983
+     */
984
+    public function getCalendarObject($calendarId, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
985
+        $query = $this->db->getQueryBuilder();
986
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
987
+            ->from('calendarobjects')
988
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
989
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
990
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
991
+        $stmt = $query->execute();
992
+        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
993
+
994
+        if (!$row) {
995
+            return null;
996
+        }
997
+
998
+        return [
999
+            'id'            => $row['id'],
1000
+            'uri'           => $row['uri'],
1001
+            'lastmodified'  => $row['lastmodified'],
1002
+            'etag'          => '"' . $row['etag'] . '"',
1003
+            'calendarid'    => $row['calendarid'],
1004
+            'size'          => (int)$row['size'],
1005
+            'calendardata'  => $this->readBlob($row['calendardata']),
1006
+            'component'     => strtolower($row['componenttype']),
1007
+            'classification'=> (int)$row['classification']
1008
+        ];
1009
+    }
1010
+
1011
+    /**
1012
+     * Returns a list of calendar objects.
1013
+     *
1014
+     * This method should work identical to getCalendarObject, but instead
1015
+     * return all the calendar objects in the list as an array.
1016
+     *
1017
+     * If the backend supports this, it may allow for some speed-ups.
1018
+     *
1019
+     * @param mixed $calendarId
1020
+     * @param string[] $uris
1021
+     * @param int $calendarType
1022
+     * @return array
1023
+     */
1024
+    public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
1025
+        if (empty($uris)) {
1026
+            return [];
1027
+        }
1028
+
1029
+        $chunks = array_chunk($uris, 100);
1030
+        $objects = [];
1031
+
1032
+        $query = $this->db->getQueryBuilder();
1033
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1034
+            ->from('calendarobjects')
1035
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1036
+            ->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1037
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1038
+
1039
+        foreach ($chunks as $uris) {
1040
+            $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1041
+            $result = $query->execute();
1042
+
1043
+            while ($row = $result->fetch()) {
1044
+                $objects[] = [
1045
+                    'id'           => $row['id'],
1046
+                    'uri'          => $row['uri'],
1047
+                    'lastmodified' => $row['lastmodified'],
1048
+                    'etag'         => '"' . $row['etag'] . '"',
1049
+                    'calendarid'   => $row['calendarid'],
1050
+                    'size'         => (int)$row['size'],
1051
+                    'calendardata' => $this->readBlob($row['calendardata']),
1052
+                    'component'    => strtolower($row['componenttype']),
1053
+                    'classification' => (int)$row['classification']
1054
+                ];
1055
+            }
1056
+            $result->closeCursor();
1057
+        }
1058
+
1059
+        return $objects;
1060
+    }
1061
+
1062
+    /**
1063
+     * Creates a new calendar object.
1064
+     *
1065
+     * The object uri is only the basename, or filename and not a full path.
1066
+     *
1067
+     * It is possible return an etag from this function, which will be used in
1068
+     * the response to this PUT request. Note that the ETag must be surrounded
1069
+     * by double-quotes.
1070
+     *
1071
+     * However, you should only really return this ETag if you don't mangle the
1072
+     * calendar-data. If the result of a subsequent GET to this object is not
1073
+     * the exact same as this request body, you should omit the ETag.
1074
+     *
1075
+     * @param mixed $calendarId
1076
+     * @param string $objectUri
1077
+     * @param string $calendarData
1078
+     * @param int $calendarType
1079
+     * @return string
1080
+     */
1081
+    public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1082
+        $extraData = $this->getDenormalizedData($calendarData);
1083
+
1084
+        $q = $this->db->getQueryBuilder();
1085
+        $q->select($q->func()->count('*'))
1086
+            ->from('calendarobjects')
1087
+            ->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId)))
1088
+            ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid'])))
1089
+            ->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType)));
1090
+
1091
+        $result = $q->execute();
1092
+        $count = (int) $result->fetchColumn();
1093
+        $result->closeCursor();
1094
+
1095
+        if ($count !== 0) {
1096
+            throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.');
1097
+        }
1098
+
1099
+        $query = $this->db->getQueryBuilder();
1100
+        $query->insert('calendarobjects')
1101
+            ->values([
1102
+                'calendarid' => $query->createNamedParameter($calendarId),
1103
+                'uri' => $query->createNamedParameter($objectUri),
1104
+                'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1105
+                'lastmodified' => $query->createNamedParameter(time()),
1106
+                'etag' => $query->createNamedParameter($extraData['etag']),
1107
+                'size' => $query->createNamedParameter($extraData['size']),
1108
+                'componenttype' => $query->createNamedParameter($extraData['componentType']),
1109
+                'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1110
+                'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1111
+                'classification' => $query->createNamedParameter($extraData['classification']),
1112
+                'uid' => $query->createNamedParameter($extraData['uid']),
1113
+                'calendartype' => $query->createNamedParameter($calendarType),
1114
+            ])
1115
+            ->execute();
1116
+
1117
+        $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1118
+        $this->addChange($calendarId, $objectUri, 1, $calendarType);
1119
+
1120
+        $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1121
+        if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1122
+            $calendarRow = $this->getCalendarById($calendarId);
1123
+            $shares = $this->getShares($calendarId);
1124
+
1125
+            $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1126
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1127
+                '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1128
+                [
1129
+                    'calendarId' => $calendarId,
1130
+                    'calendarData' => $calendarRow,
1131
+                    'shares' => $shares,
1132
+                    'objectData' => $objectRow,
1133
+                ]
1134
+            ));
1135
+        } else {
1136
+            $subscriptionRow = $this->getSubscriptionById($calendarId);
1137
+
1138
+            $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1139
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1140
+                '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1141
+                [
1142
+                    'subscriptionId' => $calendarId,
1143
+                    'calendarData' => $subscriptionRow,
1144
+                    'shares' => [],
1145
+                    'objectData' => $objectRow,
1146
+                ]
1147
+            ));
1148
+        }
1149
+
1150
+        return '"' . $extraData['etag'] . '"';
1151
+    }
1152
+
1153
+    /**
1154
+     * Updates an existing calendarobject, based on it's uri.
1155
+     *
1156
+     * The object uri is only the basename, or filename and not a full path.
1157
+     *
1158
+     * It is possible return an etag from this function, which will be used in
1159
+     * the response to this PUT request. Note that the ETag must be surrounded
1160
+     * by double-quotes.
1161
+     *
1162
+     * However, you should only really return this ETag if you don't mangle the
1163
+     * calendar-data. If the result of a subsequent GET to this object is not
1164
+     * the exact same as this request body, you should omit the ETag.
1165
+     *
1166
+     * @param mixed $calendarId
1167
+     * @param string $objectUri
1168
+     * @param string $calendarData
1169
+     * @param int $calendarType
1170
+     * @return string
1171
+     */
1172
+    public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1173
+        $extraData = $this->getDenormalizedData($calendarData);
1174
+        $query = $this->db->getQueryBuilder();
1175
+        $query->update('calendarobjects')
1176
+                ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1177
+                ->set('lastmodified', $query->createNamedParameter(time()))
1178
+                ->set('etag', $query->createNamedParameter($extraData['etag']))
1179
+                ->set('size', $query->createNamedParameter($extraData['size']))
1180
+                ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1181
+                ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1182
+                ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1183
+                ->set('classification', $query->createNamedParameter($extraData['classification']))
1184
+                ->set('uid', $query->createNamedParameter($extraData['uid']))
1185
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1186
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1187
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1188
+            ->execute();
1189
+
1190
+        $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1191
+        $this->addChange($calendarId, $objectUri, 2, $calendarType);
1192
+
1193
+        $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1194
+        if (is_array($objectRow)) {
1195
+            if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1196
+                $calendarRow = $this->getCalendarById($calendarId);
1197
+                $shares = $this->getShares($calendarId);
1198
+
1199
+                $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1200
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1201
+                    '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1202
+                    [
1203
+                        'calendarId' => $calendarId,
1204
+                        'calendarData' => $calendarRow,
1205
+                        'shares' => $shares,
1206
+                        'objectData' => $objectRow,
1207
+                    ]
1208
+                ));
1209
+            } else {
1210
+                $subscriptionRow = $this->getSubscriptionById($calendarId);
1211
+
1212
+                $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1213
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1214
+                    '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1215
+                    [
1216
+                        'subscriptionId' => $calendarId,
1217
+                        'calendarData' => $subscriptionRow,
1218
+                        'shares' => [],
1219
+                        'objectData' => $objectRow,
1220
+                    ]
1221
+                ));
1222
+            }
1223
+        }
1224
+
1225
+        return '"' . $extraData['etag'] . '"';
1226
+    }
1227
+
1228
+    /**
1229
+     * @param int $calendarObjectId
1230
+     * @param int $classification
1231
+     */
1232
+    public function setClassification($calendarObjectId, $classification) {
1233
+        if (!in_array($classification, [
1234
+            self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1235
+        ])) {
1236
+            throw new \InvalidArgumentException();
1237
+        }
1238
+        $query = $this->db->getQueryBuilder();
1239
+        $query->update('calendarobjects')
1240
+            ->set('classification', $query->createNamedParameter($classification))
1241
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1242
+            ->execute();
1243
+    }
1244
+
1245
+    /**
1246
+     * Deletes an existing calendar object.
1247
+     *
1248
+     * The object uri is only the basename, or filename and not a full path.
1249
+     *
1250
+     * @param mixed $calendarId
1251
+     * @param string $objectUri
1252
+     * @param int $calendarType
1253
+     * @return void
1254
+     */
1255
+    public function deleteCalendarObject($calendarId, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1256
+        $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1257
+        if (is_array($data)) {
1258
+            if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1259
+                $calendarRow = $this->getCalendarById($calendarId);
1260
+                $shares = $this->getShares($calendarId);
1261
+
1262
+                $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1263
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1264
+                    '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1265
+                    [
1266
+                        'calendarId' => $calendarId,
1267
+                        'calendarData' => $calendarRow,
1268
+                        'shares' => $shares,
1269
+                        'objectData' => $data,
1270
+                    ]
1271
+                ));
1272
+            } else {
1273
+                $subscriptionRow = $this->getSubscriptionById($calendarId);
1274
+
1275
+                $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1276
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1277
+                    '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1278
+                    [
1279
+                        'subscriptionId' => $calendarId,
1280
+                        'calendarData' => $subscriptionRow,
1281
+                        'shares' => [],
1282
+                        'objectData' => $data,
1283
+                    ]
1284
+                ));
1285
+            }
1286
+        }
1287
+
1288
+        $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1289
+        $stmt->execute([$calendarId, $objectUri, $calendarType]);
1290
+
1291
+        if (is_array($data)) {
1292
+            $this->purgeProperties($calendarId, $data['id'], $calendarType);
1293
+        }
1294
+
1295
+        $this->addChange($calendarId, $objectUri, 3, $calendarType);
1296
+    }
1297
+
1298
+    /**
1299
+     * Performs a calendar-query on the contents of this calendar.
1300
+     *
1301
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
1302
+     * calendar-query it is possible for a client to request a specific set of
1303
+     * object, based on contents of iCalendar properties, date-ranges and
1304
+     * iCalendar component types (VTODO, VEVENT).
1305
+     *
1306
+     * This method should just return a list of (relative) urls that match this
1307
+     * query.
1308
+     *
1309
+     * The list of filters are specified as an array. The exact array is
1310
+     * documented by Sabre\CalDAV\CalendarQueryParser.
1311
+     *
1312
+     * Note that it is extremely likely that getCalendarObject for every path
1313
+     * returned from this method will be called almost immediately after. You
1314
+     * may want to anticipate this to speed up these requests.
1315
+     *
1316
+     * This method provides a default implementation, which parses *all* the
1317
+     * iCalendar objects in the specified calendar.
1318
+     *
1319
+     * This default may well be good enough for personal use, and calendars
1320
+     * that aren't very large. But if you anticipate high usage, big calendars
1321
+     * or high loads, you are strongly advised to optimize certain paths.
1322
+     *
1323
+     * The best way to do so is override this method and to optimize
1324
+     * specifically for 'common filters'.
1325
+     *
1326
+     * Requests that are extremely common are:
1327
+     *   * requests for just VEVENTS
1328
+     *   * requests for just VTODO
1329
+     *   * requests with a time-range-filter on either VEVENT or VTODO.
1330
+     *
1331
+     * ..and combinations of these requests. It may not be worth it to try to
1332
+     * handle every possible situation and just rely on the (relatively
1333
+     * easy to use) CalendarQueryValidator to handle the rest.
1334
+     *
1335
+     * Note that especially time-range-filters may be difficult to parse. A
1336
+     * time-range filter specified on a VEVENT must for instance also handle
1337
+     * recurrence rules correctly.
1338
+     * A good example of how to interprete all these filters can also simply
1339
+     * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1340
+     * as possible, so it gives you a good idea on what type of stuff you need
1341
+     * to think of.
1342
+     *
1343
+     * @param mixed $calendarId
1344
+     * @param array $filters
1345
+     * @param int $calendarType
1346
+     * @return array
1347
+     */
1348
+    public function calendarQuery($calendarId, array $filters, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
1349
+        $componentType = null;
1350
+        $requirePostFilter = true;
1351
+        $timeRange = null;
1352
+
1353
+        // if no filters were specified, we don't need to filter after a query
1354
+        if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1355
+            $requirePostFilter = false;
1356
+        }
1357
+
1358
+        // Figuring out if there's a component filter
1359
+        if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1360
+            $componentType = $filters['comp-filters'][0]['name'];
1361
+
1362
+            // Checking if we need post-filters
1363
+            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1364
+                $requirePostFilter = false;
1365
+            }
1366
+            // There was a time-range filter
1367
+            if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
1368
+                $timeRange = $filters['comp-filters'][0]['time-range'];
1369
+
1370
+                // If start time OR the end time is not specified, we can do a
1371
+                // 100% accurate mysql query.
1372
+                if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1373
+                    $requirePostFilter = false;
1374
+                }
1375
+            }
1376
+        }
1377
+        $columns = ['uri'];
1378
+        if ($requirePostFilter) {
1379
+            $columns = ['uri', 'calendardata'];
1380
+        }
1381
+        $query = $this->db->getQueryBuilder();
1382
+        $query->select($columns)
1383
+            ->from('calendarobjects')
1384
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1385
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1386
+
1387
+        if ($componentType) {
1388
+            $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1389
+        }
1390
+
1391
+        if ($timeRange && $timeRange['start']) {
1392
+            $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1393
+        }
1394
+        if ($timeRange && $timeRange['end']) {
1395
+            $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1396
+        }
1397
+
1398
+        $stmt = $query->execute();
1399
+
1400
+        $result = [];
1401
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1402
+            if ($requirePostFilter) {
1403
+                // validateFilterForObject will parse the calendar data
1404
+                // catch parsing errors
1405
+                try {
1406
+                    $matches = $this->validateFilterForObject($row, $filters);
1407
+                } catch (ParseException $ex) {
1408
+                    $this->logger->logException($ex, [
1409
+                        'app' => 'dav',
1410
+                        'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1411
+                    ]);
1412
+                    continue;
1413
+                } catch (InvalidDataException $ex) {
1414
+                    $this->logger->logException($ex, [
1415
+                        'app' => 'dav',
1416
+                        'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1417
+                    ]);
1418
+                    continue;
1419
+                }
1420
+
1421
+                if (!$matches) {
1422
+                    continue;
1423
+                }
1424
+            }
1425
+            $result[] = $row['uri'];
1426
+        }
1427
+
1428
+        return $result;
1429
+    }
1430
+
1431
+    /**
1432
+     * custom Nextcloud search extension for CalDAV
1433
+     *
1434
+     * TODO - this should optionally cover cached calendar objects as well
1435
+     *
1436
+     * @param string $principalUri
1437
+     * @param array $filters
1438
+     * @param integer|null $limit
1439
+     * @param integer|null $offset
1440
+     * @return array
1441
+     */
1442
+    public function calendarSearch($principalUri, array $filters, $limit=null, $offset=null) {
1443
+        $calendars = $this->getCalendarsForUser($principalUri);
1444
+        $ownCalendars = [];
1445
+        $sharedCalendars = [];
1446
+
1447
+        $uriMapper = [];
1448
+
1449
+        foreach ($calendars as $calendar) {
1450
+            if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1451
+                $ownCalendars[] = $calendar['id'];
1452
+            } else {
1453
+                $sharedCalendars[] = $calendar['id'];
1454
+            }
1455
+            $uriMapper[$calendar['id']] = $calendar['uri'];
1456
+        }
1457
+        if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1458
+            return [];
1459
+        }
1460
+
1461
+        $query = $this->db->getQueryBuilder();
1462
+        // Calendar id expressions
1463
+        $calendarExpressions = [];
1464
+        foreach ($ownCalendars as $id) {
1465
+            $calendarExpressions[] = $query->expr()->andX(
1466
+                $query->expr()->eq('c.calendarid',
1467
+                    $query->createNamedParameter($id)),
1468
+                $query->expr()->eq('c.calendartype',
1469
+                        $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1470
+        }
1471
+        foreach ($sharedCalendars as $id) {
1472
+            $calendarExpressions[] = $query->expr()->andX(
1473
+                $query->expr()->eq('c.calendarid',
1474
+                    $query->createNamedParameter($id)),
1475
+                $query->expr()->eq('c.classification',
1476
+                    $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1477
+                $query->expr()->eq('c.calendartype',
1478
+                    $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1479
+        }
1480
+
1481
+        if (count($calendarExpressions) === 1) {
1482
+            $calExpr = $calendarExpressions[0];
1483
+        } else {
1484
+            $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1485
+        }
1486
+
1487
+        // Component expressions
1488
+        $compExpressions = [];
1489
+        foreach ($filters['comps'] as $comp) {
1490
+            $compExpressions[] = $query->expr()
1491
+                ->eq('c.componenttype', $query->createNamedParameter($comp));
1492
+        }
1493
+
1494
+        if (count($compExpressions) === 1) {
1495
+            $compExpr = $compExpressions[0];
1496
+        } else {
1497
+            $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1498
+        }
1499
+
1500
+        if (!isset($filters['props'])) {
1501
+            $filters['props'] = [];
1502
+        }
1503
+        if (!isset($filters['params'])) {
1504
+            $filters['params'] = [];
1505
+        }
1506
+
1507
+        $propParamExpressions = [];
1508
+        foreach ($filters['props'] as $prop) {
1509
+            $propParamExpressions[] = $query->expr()->andX(
1510
+                $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1511
+                $query->expr()->isNull('i.parameter')
1512
+            );
1513
+        }
1514
+        foreach ($filters['params'] as $param) {
1515
+            $propParamExpressions[] = $query->expr()->andX(
1516
+                $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1517
+                $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1518
+            );
1519
+        }
1520
+
1521
+        if (count($propParamExpressions) === 1) {
1522
+            $propParamExpr = $propParamExpressions[0];
1523
+        } else {
1524
+            $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1525
+        }
1526
+
1527
+        $query->select(['c.calendarid', 'c.uri'])
1528
+            ->from($this->dbObjectPropertiesTable, 'i')
1529
+            ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1530
+            ->where($calExpr)
1531
+            ->andWhere($compExpr)
1532
+            ->andWhere($propParamExpr)
1533
+            ->andWhere($query->expr()->iLike('i.value',
1534
+                $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
1535
+
1536
+        if ($offset) {
1537
+            $query->setFirstResult($offset);
1538
+        }
1539
+        if ($limit) {
1540
+            $query->setMaxResults($limit);
1541
+        }
1542
+
1543
+        $stmt = $query->execute();
1544
+
1545
+        $result = [];
1546
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1547
+            $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1548
+            if (!in_array($path, $result)) {
1549
+                $result[] = $path;
1550
+            }
1551
+        }
1552
+
1553
+        return $result;
1554
+    }
1555
+
1556
+    /**
1557
+     * used for Nextcloud's calendar API
1558
+     *
1559
+     * @param array $calendarInfo
1560
+     * @param string $pattern
1561
+     * @param array $searchProperties
1562
+     * @param array $options
1563
+     * @param integer|null $limit
1564
+     * @param integer|null $offset
1565
+     *
1566
+     * @return array
1567
+     */
1568
+    public function search(array $calendarInfo, $pattern, array $searchProperties,
1569
+                            array $options, $limit, $offset) {
1570
+        $outerQuery = $this->db->getQueryBuilder();
1571
+        $innerQuery = $this->db->getQueryBuilder();
1572
+
1573
+        $innerQuery->selectDistinct('op.objectid')
1574
+            ->from($this->dbObjectPropertiesTable, 'op')
1575
+            ->andWhere($innerQuery->expr()->eq('op.calendarid',
1576
+                $outerQuery->createNamedParameter($calendarInfo['id'])))
1577
+            ->andWhere($innerQuery->expr()->eq('op.calendartype',
1578
+                $outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1579
+
1580
+        // only return public items for shared calendars for now
1581
+        if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1582
+            $innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1583
+                $outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1584
+        }
1585
+
1586
+        $or = $innerQuery->expr()->orX();
1587
+        foreach ($searchProperties as $searchProperty) {
1588
+            $or->add($innerQuery->expr()->eq('op.name',
1589
+                $outerQuery->createNamedParameter($searchProperty)));
1590
+        }
1591
+        $innerQuery->andWhere($or);
1592
+
1593
+        if ($pattern !== '') {
1594
+            $innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1595
+                $outerQuery->createNamedParameter('%' .
1596
+                    $this->db->escapeLikeParameter($pattern) . '%')));
1597
+        }
1598
+
1599
+        $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1600
+            ->from('calendarobjects', 'c');
1601
+
1602
+        if (isset($options['timerange'])) {
1603
+            if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) {
1604
+                $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1605
+                    $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1606
+            }
1607
+            if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTime) {
1608
+                $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1609
+                    $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1610
+            }
1611
+        }
1612
+
1613
+        if (isset($options['types'])) {
1614
+            $or = $outerQuery->expr()->orX();
1615
+            foreach ($options['types'] as $type) {
1616
+                $or->add($outerQuery->expr()->eq('componenttype',
1617
+                    $outerQuery->createNamedParameter($type)));
1618
+            }
1619
+            $outerQuery->andWhere($or);
1620
+        }
1621
+
1622
+        $outerQuery->andWhere($outerQuery->expr()->in('c.id',
1623
+            $outerQuery->createFunction($innerQuery->getSQL())));
1624
+
1625
+        if ($offset) {
1626
+            $outerQuery->setFirstResult($offset);
1627
+        }
1628
+        if ($limit) {
1629
+            $outerQuery->setMaxResults($limit);
1630
+        }
1631
+
1632
+        $result = $outerQuery->execute();
1633
+        $calendarObjects = $result->fetchAll();
1634
+
1635
+        return array_map(function ($o) {
1636
+            $calendarData = Reader::read($o['calendardata']);
1637
+            $comps = $calendarData->getComponents();
1638
+            $objects = [];
1639
+            $timezones = [];
1640
+            foreach ($comps as $comp) {
1641
+                if ($comp instanceof VTimeZone) {
1642
+                    $timezones[] = $comp;
1643
+                } else {
1644
+                    $objects[] = $comp;
1645
+                }
1646
+            }
1647
+
1648
+            return [
1649
+                'id' => $o['id'],
1650
+                'type' => $o['componenttype'],
1651
+                'uid' => $o['uid'],
1652
+                'uri' => $o['uri'],
1653
+                'objects' => array_map(function ($c) {
1654
+                    return $this->transformSearchData($c);
1655
+                }, $objects),
1656
+                'timezones' => array_map(function ($c) {
1657
+                    return $this->transformSearchData($c);
1658
+                }, $timezones),
1659
+            ];
1660
+        }, $calendarObjects);
1661
+    }
1662
+
1663
+    /**
1664
+     * @param Component $comp
1665
+     * @return array
1666
+     */
1667
+    private function transformSearchData(Component $comp) {
1668
+        $data = [];
1669
+        /** @var Component[] $subComponents */
1670
+        $subComponents = $comp->getComponents();
1671
+        /** @var Property[] $properties */
1672
+        $properties = array_filter($comp->children(), function ($c) {
1673
+            return $c instanceof Property;
1674
+        });
1675
+        $validationRules = $comp->getValidationRules();
1676
+
1677
+        foreach ($subComponents as $subComponent) {
1678
+            $name = $subComponent->name;
1679
+            if (!isset($data[$name])) {
1680
+                $data[$name] = [];
1681
+            }
1682
+            $data[$name][] = $this->transformSearchData($subComponent);
1683
+        }
1684
+
1685
+        foreach ($properties as $property) {
1686
+            $name = $property->name;
1687
+            if (!isset($validationRules[$name])) {
1688
+                $validationRules[$name] = '*';
1689
+            }
1690
+
1691
+            $rule = $validationRules[$property->name];
1692
+            if ($rule === '+' || $rule === '*') { // multiple
1693
+                if (!isset($data[$name])) {
1694
+                    $data[$name] = [];
1695
+                }
1696
+
1697
+                $data[$name][] = $this->transformSearchProperty($property);
1698
+            } else { // once
1699
+                $data[$name] = $this->transformSearchProperty($property);
1700
+            }
1701
+        }
1702
+
1703
+        return $data;
1704
+    }
1705
+
1706
+    /**
1707
+     * @param Property $prop
1708
+     * @return array
1709
+     */
1710
+    private function transformSearchProperty(Property $prop) {
1711
+        // No need to check Date, as it extends DateTime
1712
+        if ($prop instanceof Property\ICalendar\DateTime) {
1713
+            $value = $prop->getDateTime();
1714
+        } else {
1715
+            $value = $prop->getValue();
1716
+        }
1717
+
1718
+        return [
1719
+            $value,
1720
+            $prop->parameters()
1721
+        ];
1722
+    }
1723
+
1724
+    /**
1725
+     * @param string $principalUri
1726
+     * @param string $pattern
1727
+     * @param array $componentTypes
1728
+     * @param array $searchProperties
1729
+     * @param array $searchParameters
1730
+     * @param array $options
1731
+     * @return array
1732
+     */
1733
+    public function searchPrincipalUri(string $principalUri,
1734
+                                        string $pattern,
1735
+                                        array $componentTypes,
1736
+                                        array $searchProperties,
1737
+                                        array $searchParameters,
1738
+                                        array $options = []): array {
1739
+        $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1740
+
1741
+        $calendarObjectIdQuery = $this->db->getQueryBuilder();
1742
+        $calendarOr = $calendarObjectIdQuery->expr()->orX();
1743
+        $searchOr = $calendarObjectIdQuery->expr()->orX();
1744
+
1745
+        // Fetch calendars and subscription
1746
+        $calendars = $this->getCalendarsForUser($principalUri);
1747
+        $subscriptions = $this->getSubscriptionsForUser($principalUri);
1748
+        foreach ($calendars as $calendar) {
1749
+            $calendarAnd = $calendarObjectIdQuery->expr()->andX();
1750
+            $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
1751
+            $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1752
+
1753
+            // If it's shared, limit search to public events
1754
+            if ($calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
1755
+                $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1756
+            }
1757
+
1758
+            $calendarOr->add($calendarAnd);
1759
+        }
1760
+        foreach ($subscriptions as $subscription) {
1761
+            $subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
1762
+            $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
1763
+            $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
1764
+
1765
+            // If it's shared, limit search to public events
1766
+            if ($subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
1767
+                $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1768
+            }
1769
+
1770
+            $calendarOr->add($subscriptionAnd);
1771
+        }
1772
+
1773
+        foreach ($searchProperties as $property) {
1774
+            $propertyAnd = $calendarObjectIdQuery->expr()->andX();
1775
+            $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
1776
+            $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
1777
+
1778
+            $searchOr->add($propertyAnd);
1779
+        }
1780
+        foreach ($searchParameters as $property => $parameter) {
1781
+            $parameterAnd = $calendarObjectIdQuery->expr()->andX();
1782
+            $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
1783
+            $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR)));
1784
+
1785
+            $searchOr->add($parameterAnd);
1786
+        }
1787
+
1788
+        if ($calendarOr->count() === 0) {
1789
+            return [];
1790
+        }
1791
+        if ($searchOr->count() === 0) {
1792
+            return [];
1793
+        }
1794
+
1795
+        $calendarObjectIdQuery->selectDistinct('cob.objectid')
1796
+            ->from($this->dbObjectPropertiesTable, 'cob')
1797
+            ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
1798
+            ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
1799
+            ->andWhere($calendarOr)
1800
+            ->andWhere($searchOr);
1801
+
1802
+        if ('' !== $pattern) {
1803
+            if (!$escapePattern) {
1804
+                $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
1805
+            } else {
1806
+                $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1807
+            }
1808
+        }
1809
+
1810
+        if (isset($options['limit'])) {
1811
+            $calendarObjectIdQuery->setMaxResults($options['limit']);
1812
+        }
1813
+        if (isset($options['offset'])) {
1814
+            $calendarObjectIdQuery->setFirstResult($options['offset']);
1815
+        }
1816
+
1817
+        $result = $calendarObjectIdQuery->execute();
1818
+        $matches = $result->fetchAll();
1819
+        $result->closeCursor();
1820
+        $matches = array_map(static function (array $match):int {
1821
+            return (int) $match['objectid'];
1822
+        }, $matches);
1823
+
1824
+        $query = $this->db->getQueryBuilder();
1825
+        $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
1826
+            ->from('calendarobjects')
1827
+            ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1828
+
1829
+        $result = $query->execute();
1830
+        $calendarObjects = $result->fetchAll();
1831
+        $result->closeCursor();
1832
+
1833
+        return array_map(function (array $array): array {
1834
+            $array['calendarid'] = (int)$array['calendarid'];
1835
+            $array['calendartype'] = (int)$array['calendartype'];
1836
+            $array['calendardata'] = $this->readBlob($array['calendardata']);
1837
+
1838
+            return $array;
1839
+        }, $calendarObjects);
1840
+    }
1841
+
1842
+    /**
1843
+     * Searches through all of a users calendars and calendar objects to find
1844
+     * an object with a specific UID.
1845
+     *
1846
+     * This method should return the path to this object, relative to the
1847
+     * calendar home, so this path usually only contains two parts:
1848
+     *
1849
+     * calendarpath/objectpath.ics
1850
+     *
1851
+     * If the uid is not found, return null.
1852
+     *
1853
+     * This method should only consider * objects that the principal owns, so
1854
+     * any calendars owned by other principals that also appear in this
1855
+     * collection should be ignored.
1856
+     *
1857
+     * @param string $principalUri
1858
+     * @param string $uid
1859
+     * @return string|null
1860
+     */
1861
+    public function getCalendarObjectByUID($principalUri, $uid) {
1862
+        $query = $this->db->getQueryBuilder();
1863
+        $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1864
+            ->from('calendarobjects', 'co')
1865
+            ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1866
+            ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1867
+            ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1868
+
1869
+        $stmt = $query->execute();
1870
+
1871
+        if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1872
+            return $row['calendaruri'] . '/' . $row['objecturi'];
1873
+        }
1874
+
1875
+        return null;
1876
+    }
1877
+
1878
+    /**
1879
+     * The getChanges method returns all the changes that have happened, since
1880
+     * the specified syncToken in the specified calendar.
1881
+     *
1882
+     * This function should return an array, such as the following:
1883
+     *
1884
+     * [
1885
+     *   'syncToken' => 'The current synctoken',
1886
+     *   'added'   => [
1887
+     *      'new.txt',
1888
+     *   ],
1889
+     *   'modified'   => [
1890
+     *      'modified.txt',
1891
+     *   ],
1892
+     *   'deleted' => [
1893
+     *      'foo.php.bak',
1894
+     *      'old.txt'
1895
+     *   ]
1896
+     * );
1897
+     *
1898
+     * The returned syncToken property should reflect the *current* syncToken
1899
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1900
+     * property This is * needed here too, to ensure the operation is atomic.
1901
+     *
1902
+     * If the $syncToken argument is specified as null, this is an initial
1903
+     * sync, and all members should be reported.
1904
+     *
1905
+     * The modified property is an array of nodenames that have changed since
1906
+     * the last token.
1907
+     *
1908
+     * The deleted property is an array with nodenames, that have been deleted
1909
+     * from collection.
1910
+     *
1911
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
1912
+     * 1, you only have to report changes that happened only directly in
1913
+     * immediate descendants. If it's 2, it should also include changes from
1914
+     * the nodes below the child collections. (grandchildren)
1915
+     *
1916
+     * The $limit argument allows a client to specify how many results should
1917
+     * be returned at most. If the limit is not specified, it should be treated
1918
+     * as infinite.
1919
+     *
1920
+     * If the limit (infinite or not) is higher than you're willing to return,
1921
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1922
+     *
1923
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
1924
+     * return null.
1925
+     *
1926
+     * The limit is 'suggestive'. You are free to ignore it.
1927
+     *
1928
+     * @param string $calendarId
1929
+     * @param string $syncToken
1930
+     * @param int $syncLevel
1931
+     * @param int $limit
1932
+     * @param int $calendarType
1933
+     * @return array
1934
+     */
1935
+    public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1936
+        // Current synctoken
1937
+        $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1938
+        $stmt->execute([ $calendarId ]);
1939
+        $currentToken = $stmt->fetchColumn(0);
1940
+
1941
+        if (is_null($currentToken)) {
1942
+            return null;
1943
+        }
1944
+
1945
+        $result = [
1946
+            'syncToken' => $currentToken,
1947
+            'added'     => [],
1948
+            'modified'  => [],
1949
+            'deleted'   => [],
1950
+        ];
1951
+
1952
+        if ($syncToken) {
1953
+            $query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? AND `calendartype` = ? ORDER BY `synctoken`";
1954
+            if ($limit>0) {
1955
+                $query.= " LIMIT " . (int)$limit;
1956
+            }
1957
+
1958
+            // Fetching all changes
1959
+            $stmt = $this->db->prepare($query);
1960
+            $stmt->execute([$syncToken, $currentToken, $calendarId, $calendarType]);
1961
+
1962
+            $changes = [];
1963
+
1964
+            // This loop ensures that any duplicates are overwritten, only the
1965
+            // last change on a node is relevant.
1966
+            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1967
+                $changes[$row['uri']] = $row['operation'];
1968
+            }
1969
+
1970
+            foreach ($changes as $uri => $operation) {
1971
+                switch ($operation) {
1972
+                    case 1:
1973
+                        $result['added'][] = $uri;
1974
+                        break;
1975
+                    case 2:
1976
+                        $result['modified'][] = $uri;
1977
+                        break;
1978
+                    case 3:
1979
+                        $result['deleted'][] = $uri;
1980
+                        break;
1981
+                }
1982
+            }
1983
+        } else {
1984
+            // No synctoken supplied, this is the initial sync.
1985
+            $query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?";
1986
+            $stmt = $this->db->prepare($query);
1987
+            $stmt->execute([$calendarId, $calendarType]);
1988
+
1989
+            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1990
+        }
1991
+        return $result;
1992
+    }
1993
+
1994
+    /**
1995
+     * Returns a list of subscriptions for a principal.
1996
+     *
1997
+     * Every subscription is an array with the following keys:
1998
+     *  * id, a unique id that will be used by other functions to modify the
1999
+     *    subscription. This can be the same as the uri or a database key.
2000
+     *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2001
+     *  * principaluri. The owner of the subscription. Almost always the same as
2002
+     *    principalUri passed to this method.
2003
+     *
2004
+     * Furthermore, all the subscription info must be returned too:
2005
+     *
2006
+     * 1. {DAV:}displayname
2007
+     * 2. {http://apple.com/ns/ical/}refreshrate
2008
+     * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2009
+     *    should not be stripped).
2010
+     * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2011
+     *    should not be stripped).
2012
+     * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2013
+     *    attachments should not be stripped).
2014
+     * 6. {http://calendarserver.org/ns/}source (Must be a
2015
+     *     Sabre\DAV\Property\Href).
2016
+     * 7. {http://apple.com/ns/ical/}calendar-color
2017
+     * 8. {http://apple.com/ns/ical/}calendar-order
2018
+     * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2019
+     *    (should just be an instance of
2020
+     *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2021
+     *    default components).
2022
+     *
2023
+     * @param string $principalUri
2024
+     * @return array
2025
+     */
2026
+    public function getSubscriptionsForUser($principalUri) {
2027
+        $fields = array_values($this->subscriptionPropertyMap);
2028
+        $fields[] = 'id';
2029
+        $fields[] = 'uri';
2030
+        $fields[] = 'source';
2031
+        $fields[] = 'principaluri';
2032
+        $fields[] = 'lastmodified';
2033
+        $fields[] = 'synctoken';
2034
+
2035
+        $query = $this->db->getQueryBuilder();
2036
+        $query->select($fields)
2037
+            ->from('calendarsubscriptions')
2038
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2039
+            ->orderBy('calendarorder', 'asc');
2040
+        $stmt =$query->execute();
2041
+
2042
+        $subscriptions = [];
2043
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
2044
+            $subscription = [
2045
+                'id'           => $row['id'],
2046
+                'uri'          => $row['uri'],
2047
+                'principaluri' => $row['principaluri'],
2048
+                'source'       => $row['source'],
2049
+                'lastmodified' => $row['lastmodified'],
2050
+
2051
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2052
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2053
+            ];
2054
+
2055
+            foreach ($this->subscriptionPropertyMap as $xmlName=>$dbName) {
2056
+                if (!is_null($row[$dbName])) {
2057
+                    $subscription[$xmlName] = $row[$dbName];
2058
+                }
2059
+            }
2060
+
2061
+            $subscriptions[] = $subscription;
2062
+        }
2063
+
2064
+        return $subscriptions;
2065
+    }
2066
+
2067
+    /**
2068
+     * Creates a new subscription for a principal.
2069
+     *
2070
+     * If the creation was a success, an id must be returned that can be used to reference
2071
+     * this subscription in other methods, such as updateSubscription.
2072
+     *
2073
+     * @param string $principalUri
2074
+     * @param string $uri
2075
+     * @param array $properties
2076
+     * @return mixed
2077
+     */
2078
+    public function createSubscription($principalUri, $uri, array $properties) {
2079
+        if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2080
+            throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2081
+        }
2082
+
2083
+        $values = [
2084
+            'principaluri' => $principalUri,
2085
+            'uri'          => $uri,
2086
+            'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2087
+            'lastmodified' => time(),
2088
+        ];
2089
+
2090
+        $propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2091
+
2092
+        foreach ($this->subscriptionPropertyMap as $xmlName=>$dbName) {
2093
+            if (array_key_exists($xmlName, $properties)) {
2094
+                $values[$dbName] = $properties[$xmlName];
2095
+                if (in_array($dbName, $propertiesBoolean)) {
2096
+                    $values[$dbName] = true;
2097
+                }
2098
+            }
2099
+        }
2100
+
2101
+        $valuesToInsert = [];
2102
+
2103
+        $query = $this->db->getQueryBuilder();
2104
+
2105
+        foreach (array_keys($values) as $name) {
2106
+            $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2107
+        }
2108
+
2109
+        $query->insert('calendarsubscriptions')
2110
+            ->values($valuesToInsert)
2111
+            ->execute();
2112
+
2113
+        $subscriptionId = $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
2114
+
2115
+        $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2116
+        $this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent((int)$subscriptionId, $subscriptionRow));
2117
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
2118
+            '\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2119
+            [
2120
+                'subscriptionId' => $subscriptionId,
2121
+                'subscriptionData' => $subscriptionRow,
2122
+            ]));
2123
+
2124
+        return $subscriptionId;
2125
+    }
2126
+
2127
+    /**
2128
+     * Updates a subscription
2129
+     *
2130
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2131
+     * To do the actual updates, you must tell this object which properties
2132
+     * you're going to process with the handle() method.
2133
+     *
2134
+     * Calling the handle method is like telling the PropPatch object "I
2135
+     * promise I can handle updating this property".
2136
+     *
2137
+     * Read the PropPatch documentation for more info and examples.
2138
+     *
2139
+     * @param mixed $subscriptionId
2140
+     * @param PropPatch $propPatch
2141
+     * @return void
2142
+     */
2143
+    public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2144
+        $supportedProperties = array_keys($this->subscriptionPropertyMap);
2145
+        $supportedProperties[] = '{http://calendarserver.org/ns/}source';
2146
+
2147
+        $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2148
+            $newValues = [];
2149
+
2150
+            foreach ($mutations as $propertyName=>$propertyValue) {
2151
+                if ($propertyName === '{http://calendarserver.org/ns/}source') {
2152
+                    $newValues['source'] = $propertyValue->getHref();
2153
+                } else {
2154
+                    $fieldName = $this->subscriptionPropertyMap[$propertyName];
2155
+                    $newValues[$fieldName] = $propertyValue;
2156
+                }
2157
+            }
2158
+
2159
+            $query = $this->db->getQueryBuilder();
2160
+            $query->update('calendarsubscriptions')
2161
+                ->set('lastmodified', $query->createNamedParameter(time()));
2162
+            foreach ($newValues as $fieldName=>$value) {
2163
+                $query->set($fieldName, $query->createNamedParameter($value));
2164
+            }
2165
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2166
+                ->execute();
2167
+
2168
+            $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2169
+            $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2170
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2171
+                '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2172
+                [
2173
+                    'subscriptionId' => $subscriptionId,
2174
+                    'subscriptionData' => $subscriptionRow,
2175
+                    'propertyMutations' => $mutations,
2176
+                ]));
2177
+
2178
+            return true;
2179
+        });
2180
+    }
2181
+
2182
+    /**
2183
+     * Deletes a subscription.
2184
+     *
2185
+     * @param mixed $subscriptionId
2186
+     * @return void
2187
+     */
2188
+    public function deleteSubscription($subscriptionId) {
2189
+        $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2190
+
2191
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
2192
+            '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2193
+            [
2194
+                'subscriptionId' => $subscriptionId,
2195
+                'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2196
+            ]));
2197
+
2198
+        $query = $this->db->getQueryBuilder();
2199
+        $query->delete('calendarsubscriptions')
2200
+            ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2201
+            ->execute();
2202
+
2203
+        $query = $this->db->getQueryBuilder();
2204
+        $query->delete('calendarobjects')
2205
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2206
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2207
+            ->execute();
2208
+
2209
+        $query->delete('calendarchanges')
2210
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2211
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2212
+            ->execute();
2213
+
2214
+        $query->delete($this->dbObjectPropertiesTable)
2215
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2216
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2217
+            ->execute();
2218
+
2219
+        if ($subscriptionRow) {
2220
+            $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2221
+        }
2222
+    }
2223
+
2224
+    /**
2225
+     * Returns a single scheduling object for the inbox collection.
2226
+     *
2227
+     * The returned array should contain the following elements:
2228
+     *   * uri - A unique basename for the object. This will be used to
2229
+     *           construct a full uri.
2230
+     *   * calendardata - The iCalendar object
2231
+     *   * lastmodified - The last modification date. Can be an int for a unix
2232
+     *                    timestamp, or a PHP DateTime object.
2233
+     *   * etag - A unique token that must change if the object changed.
2234
+     *   * size - The size of the object, in bytes.
2235
+     *
2236
+     * @param string $principalUri
2237
+     * @param string $objectUri
2238
+     * @return array
2239
+     */
2240
+    public function getSchedulingObject($principalUri, $objectUri) {
2241
+        $query = $this->db->getQueryBuilder();
2242
+        $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2243
+            ->from('schedulingobjects')
2244
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2245
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2246
+            ->execute();
2247
+
2248
+        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
2249
+
2250
+        if (!$row) {
2251
+            return null;
2252
+        }
2253
+
2254
+        return [
2255
+            'uri'          => $row['uri'],
2256
+            'calendardata' => $row['calendardata'],
2257
+            'lastmodified' => $row['lastmodified'],
2258
+            'etag'         => '"' . $row['etag'] . '"',
2259
+            'size'         => (int)$row['size'],
2260
+        ];
2261
+    }
2262
+
2263
+    /**
2264
+     * Returns all scheduling objects for the inbox collection.
2265
+     *
2266
+     * These objects should be returned as an array. Every item in the array
2267
+     * should follow the same structure as returned from getSchedulingObject.
2268
+     *
2269
+     * The main difference is that 'calendardata' is optional.
2270
+     *
2271
+     * @param string $principalUri
2272
+     * @return array
2273
+     */
2274
+    public function getSchedulingObjects($principalUri) {
2275
+        $query = $this->db->getQueryBuilder();
2276
+        $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2277
+                ->from('schedulingobjects')
2278
+                ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2279
+                ->execute();
2280
+
2281
+        $result = [];
2282
+        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
2283
+            $result[] = [
2284
+                'calendardata' => $row['calendardata'],
2285
+                'uri'          => $row['uri'],
2286
+                'lastmodified' => $row['lastmodified'],
2287
+                'etag'         => '"' . $row['etag'] . '"',
2288
+                'size'         => (int)$row['size'],
2289
+            ];
2290
+        }
2291
+
2292
+        return $result;
2293
+    }
2294
+
2295
+    /**
2296
+     * Deletes a scheduling object from the inbox collection.
2297
+     *
2298
+     * @param string $principalUri
2299
+     * @param string $objectUri
2300
+     * @return void
2301
+     */
2302
+    public function deleteSchedulingObject($principalUri, $objectUri) {
2303
+        $query = $this->db->getQueryBuilder();
2304
+        $query->delete('schedulingobjects')
2305
+                ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2306
+                ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2307
+                ->execute();
2308
+    }
2309
+
2310
+    /**
2311
+     * Creates a new scheduling object. This should land in a users' inbox.
2312
+     *
2313
+     * @param string $principalUri
2314
+     * @param string $objectUri
2315
+     * @param string $objectData
2316
+     * @return void
2317
+     */
2318
+    public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2319
+        $query = $this->db->getQueryBuilder();
2320
+        $query->insert('schedulingobjects')
2321
+            ->values([
2322
+                'principaluri' => $query->createNamedParameter($principalUri),
2323
+                'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2324
+                'uri' => $query->createNamedParameter($objectUri),
2325
+                'lastmodified' => $query->createNamedParameter(time()),
2326
+                'etag' => $query->createNamedParameter(md5($objectData)),
2327
+                'size' => $query->createNamedParameter(strlen($objectData))
2328
+            ])
2329
+            ->execute();
2330
+    }
2331
+
2332
+    /**
2333
+     * Adds a change record to the calendarchanges table.
2334
+     *
2335
+     * @param mixed $calendarId
2336
+     * @param string $objectUri
2337
+     * @param int $operation 1 = add, 2 = modify, 3 = delete.
2338
+     * @param int $calendarType
2339
+     * @return void
2340
+     */
2341
+    protected function addChange($calendarId, $objectUri, $operation, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2342
+        $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2343
+
2344
+        $query = $this->db->getQueryBuilder();
2345
+        $query->select('synctoken')
2346
+            ->from($table)
2347
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2348
+        $syncToken = (int)$query->execute()->fetchColumn();
2349
+
2350
+        $query = $this->db->getQueryBuilder();
2351
+        $query->insert('calendarchanges')
2352
+            ->values([
2353
+                'uri' => $query->createNamedParameter($objectUri),
2354
+                'synctoken' => $query->createNamedParameter($syncToken),
2355
+                'calendarid' => $query->createNamedParameter($calendarId),
2356
+                'operation' => $query->createNamedParameter($operation),
2357
+                'calendartype' => $query->createNamedParameter($calendarType),
2358
+            ])
2359
+            ->execute();
2360
+
2361
+        $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2362
+        $stmt->execute([
2363
+            $calendarId
2364
+        ]);
2365
+    }
2366
+
2367
+    /**
2368
+     * Parses some information from calendar objects, used for optimized
2369
+     * calendar-queries.
2370
+     *
2371
+     * Returns an array with the following keys:
2372
+     *   * etag - An md5 checksum of the object without the quotes.
2373
+     *   * size - Size of the object in bytes
2374
+     *   * componentType - VEVENT, VTODO or VJOURNAL
2375
+     *   * firstOccurence
2376
+     *   * lastOccurence
2377
+     *   * uid - value of the UID property
2378
+     *
2379
+     * @param string $calendarData
2380
+     * @return array
2381
+     */
2382
+    public function getDenormalizedData($calendarData) {
2383
+        $vObject = Reader::read($calendarData);
2384
+        $componentType = null;
2385
+        $component = null;
2386
+        $firstOccurrence = null;
2387
+        $lastOccurrence = null;
2388
+        $uid = null;
2389
+        $classification = self::CLASSIFICATION_PUBLIC;
2390
+        foreach ($vObject->getComponents() as $component) {
2391
+            if ($component->name!=='VTIMEZONE') {
2392
+                $componentType = $component->name;
2393
+                $uid = (string)$component->UID;
2394
+                break;
2395
+            }
2396
+        }
2397
+        if (!$componentType) {
2398
+            throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2399
+        }
2400
+        if ($componentType === 'VEVENT' && $component->DTSTART) {
2401
+            $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2402
+            // Finding the last occurrence is a bit harder
2403
+            if (!isset($component->RRULE)) {
2404
+                if (isset($component->DTEND)) {
2405
+                    $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2406
+                } elseif (isset($component->DURATION)) {
2407
+                    $endDate = clone $component->DTSTART->getDateTime();
2408
+                    $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2409
+                    $lastOccurrence = $endDate->getTimeStamp();
2410
+                } elseif (!$component->DTSTART->hasTime()) {
2411
+                    $endDate = clone $component->DTSTART->getDateTime();
2412
+                    $endDate->modify('+1 day');
2413
+                    $lastOccurrence = $endDate->getTimeStamp();
2414
+                } else {
2415
+                    $lastOccurrence = $firstOccurrence;
2416
+                }
2417
+            } else {
2418
+                $it = new EventIterator($vObject, (string)$component->UID);
2419
+                $maxDate = new DateTime(self::MAX_DATE);
2420
+                if ($it->isInfinite()) {
2421
+                    $lastOccurrence = $maxDate->getTimestamp();
2422
+                } else {
2423
+                    $end = $it->getDtEnd();
2424
+                    while ($it->valid() && $end < $maxDate) {
2425
+                        $end = $it->getDtEnd();
2426
+                        $it->next();
2427
+                    }
2428
+                    $lastOccurrence = $end->getTimestamp();
2429
+                }
2430
+            }
2431
+        }
2432
+
2433
+        if ($component->CLASS) {
2434
+            $classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2435
+            switch ($component->CLASS->getValue()) {
2436
+                case 'PUBLIC':
2437
+                    $classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2438
+                    break;
2439
+                case 'CONFIDENTIAL':
2440
+                    $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2441
+                    break;
2442
+            }
2443
+        }
2444
+        return [
2445
+            'etag' => md5($calendarData),
2446
+            'size' => strlen($calendarData),
2447
+            'componentType' => $componentType,
2448
+            'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2449
+            'lastOccurence'  => $lastOccurrence,
2450
+            'uid' => $uid,
2451
+            'classification' => $classification
2452
+        ];
2453
+    }
2454
+
2455
+    /**
2456
+     * @param $cardData
2457
+     * @return bool|string
2458
+     */
2459
+    private function readBlob($cardData) {
2460
+        if (is_resource($cardData)) {
2461
+            return stream_get_contents($cardData);
2462
+        }
2463
+
2464
+        return $cardData;
2465
+    }
2466
+
2467
+    /**
2468
+     * @param IShareable $shareable
2469
+     * @param array $add
2470
+     * @param array $remove
2471
+     */
2472
+    public function updateShares($shareable, $add, $remove) {
2473
+        $calendarId = $shareable->getResourceId();
2474
+        $calendarRow = $this->getCalendarById($calendarId);
2475
+        $oldShares = $this->getShares($calendarId);
2476
+
2477
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2478
+            '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2479
+            [
2480
+                'calendarId' => $calendarId,
2481
+                'calendarData' => $calendarRow,
2482
+                'shares' => $oldShares,
2483
+                'add' => $add,
2484
+                'remove' => $remove,
2485
+            ]));
2486
+        $this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2487
+
2488
+        $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2489
+    }
2490
+
2491
+    /**
2492
+     * @param int $resourceId
2493
+     * @param int $calendarType
2494
+     * @return array
2495
+     */
2496
+    public function getShares($resourceId, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2497
+        return $this->calendarSharingBackend->getShares($resourceId);
2498
+    }
2499
+
2500
+    /**
2501
+     * @param boolean $value
2502
+     * @param \OCA\DAV\CalDAV\Calendar $calendar
2503
+     * @return string|null
2504
+     */
2505
+    public function setPublishStatus($value, $calendar) {
2506
+        $calendarId = $calendar->getResourceId();
2507
+        $calendarData = $this->getCalendarById($calendarId);
2508
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
2509
+            '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2510
+            [
2511
+                'calendarId' => $calendarId,
2512
+                'calendarData' => $calendarData,
2513
+                'public' => $value,
2514
+            ]));
2515
+
2516
+        $query = $this->db->getQueryBuilder();
2517
+        if ($value) {
2518
+            $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2519
+            $query->insert('dav_shares')
2520
+                ->values([
2521
+                    'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2522
+                    'type' => $query->createNamedParameter('calendar'),
2523
+                    'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2524
+                    'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2525
+                    'publicuri' => $query->createNamedParameter($publicUri)
2526
+                ]);
2527
+            $query->execute();
2528
+
2529
+            $this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2530
+            return $publicUri;
2531
+        }
2532
+        $query->delete('dav_shares')
2533
+            ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2534
+            ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2535
+        $query->execute();
2536
+
2537
+        $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2538
+        return null;
2539
+    }
2540
+
2541
+    /**
2542
+     * @param \OCA\DAV\CalDAV\Calendar $calendar
2543
+     * @return mixed
2544
+     */
2545
+    public function getPublishStatus($calendar) {
2546
+        $query = $this->db->getQueryBuilder();
2547
+        $result = $query->select('publicuri')
2548
+            ->from('dav_shares')
2549
+            ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2550
+            ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2551
+            ->execute();
2552
+
2553
+        $row = $result->fetch();
2554
+        $result->closeCursor();
2555
+        return $row ? reset($row) : false;
2556
+    }
2557
+
2558
+    /**
2559
+     * @param int $resourceId
2560
+     * @param array $acl
2561
+     * @return array
2562
+     */
2563
+    public function applyShareAcl($resourceId, $acl) {
2564
+        return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2565
+    }
2566
+
2567
+
2568
+
2569
+    /**
2570
+     * update properties table
2571
+     *
2572
+     * @param int $calendarId
2573
+     * @param string $objectUri
2574
+     * @param string $calendarData
2575
+     * @param int $calendarType
2576
+     */
2577
+    public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2578
+        $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2579
+
2580
+        try {
2581
+            $vCalendar = $this->readCalendarData($calendarData);
2582
+        } catch (\Exception $ex) {
2583
+            return;
2584
+        }
2585
+
2586
+        $this->purgeProperties($calendarId, $objectId);
2587
+
2588
+        $query = $this->db->getQueryBuilder();
2589
+        $query->insert($this->dbObjectPropertiesTable)
2590
+            ->values(
2591
+                [
2592
+                    'calendarid' => $query->createNamedParameter($calendarId),
2593
+                    'calendartype' => $query->createNamedParameter($calendarType),
2594
+                    'objectid' => $query->createNamedParameter($objectId),
2595
+                    'name' => $query->createParameter('name'),
2596
+                    'parameter' => $query->createParameter('parameter'),
2597
+                    'value' => $query->createParameter('value'),
2598
+                ]
2599
+            );
2600
+
2601
+        $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2602
+        foreach ($vCalendar->getComponents() as $component) {
2603
+            if (!in_array($component->name, $indexComponents)) {
2604
+                continue;
2605
+            }
2606
+
2607
+            foreach ($component->children() as $property) {
2608
+                if (in_array($property->name, self::$indexProperties)) {
2609
+                    $value = $property->getValue();
2610
+                    // is this a shitty db?
2611
+                    if (!$this->db->supports4ByteText()) {
2612
+                        $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2613
+                    }
2614
+                    $value = mb_substr($value, 0, 254);
2615
+
2616
+                    $query->setParameter('name', $property->name);
2617
+                    $query->setParameter('parameter', null);
2618
+                    $query->setParameter('value', $value);
2619
+                    $query->execute();
2620
+                }
2621
+
2622
+                if (array_key_exists($property->name, self::$indexParameters)) {
2623
+                    $parameters = $property->parameters();
2624
+                    $indexedParametersForProperty = self::$indexParameters[$property->name];
2625
+
2626
+                    foreach ($parameters as $key => $value) {
2627
+                        if (in_array($key, $indexedParametersForProperty)) {
2628
+                            // is this a shitty db?
2629
+                            if ($this->db->supports4ByteText()) {
2630
+                                $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2631
+                            }
2632
+
2633
+                            $query->setParameter('name', $property->name);
2634
+                            $query->setParameter('parameter', mb_substr($key, 0, 254));
2635
+                            $query->setParameter('value', mb_substr($value, 0, 254));
2636
+                            $query->execute();
2637
+                        }
2638
+                    }
2639
+                }
2640
+            }
2641
+        }
2642
+    }
2643
+
2644
+    /**
2645
+     * deletes all birthday calendars
2646
+     */
2647
+    public function deleteAllBirthdayCalendars() {
2648
+        $query = $this->db->getQueryBuilder();
2649
+        $result = $query->select(['id'])->from('calendars')
2650
+            ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
2651
+            ->execute();
2652
+
2653
+        $ids = $result->fetchAll();
2654
+        foreach ($ids as $id) {
2655
+            $this->deleteCalendar($id['id']);
2656
+        }
2657
+    }
2658
+
2659
+    /**
2660
+     * @param $subscriptionId
2661
+     */
2662
+    public function purgeAllCachedEventsForSubscription($subscriptionId) {
2663
+        $query = $this->db->getQueryBuilder();
2664
+        $query->select('uri')
2665
+            ->from('calendarobjects')
2666
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2667
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2668
+        $stmt = $query->execute();
2669
+
2670
+        $uris = [];
2671
+        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
2672
+            $uris[] = $row['uri'];
2673
+        }
2674
+        $stmt->closeCursor();
2675
+
2676
+        $query = $this->db->getQueryBuilder();
2677
+        $query->delete('calendarobjects')
2678
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2679
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2680
+            ->execute();
2681
+
2682
+        $query->delete('calendarchanges')
2683
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2684
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2685
+            ->execute();
2686
+
2687
+        $query->delete($this->dbObjectPropertiesTable)
2688
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2689
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2690
+            ->execute();
2691
+
2692
+        foreach ($uris as $uri) {
2693
+            $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
2694
+        }
2695
+    }
2696
+
2697
+    /**
2698
+     * Move a calendar from one user to another
2699
+     *
2700
+     * @param string $uriName
2701
+     * @param string $uriOrigin
2702
+     * @param string $uriDestination
2703
+     */
2704
+    public function moveCalendar($uriName, $uriOrigin, $uriDestination) {
2705
+        $query = $this->db->getQueryBuilder();
2706
+        $query->update('calendars')
2707
+            ->set('principaluri', $query->createNamedParameter($uriDestination))
2708
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
2709
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
2710
+            ->execute();
2711
+    }
2712
+
2713
+    /**
2714
+     * read VCalendar data into a VCalendar object
2715
+     *
2716
+     * @param string $objectData
2717
+     * @return VCalendar
2718
+     */
2719
+    protected function readCalendarData($objectData) {
2720
+        return Reader::read($objectData);
2721
+    }
2722
+
2723
+    /**
2724
+     * delete all properties from a given calendar object
2725
+     *
2726
+     * @param int $calendarId
2727
+     * @param int $objectId
2728
+     */
2729
+    protected function purgeProperties($calendarId, $objectId) {
2730
+        $query = $this->db->getQueryBuilder();
2731
+        $query->delete($this->dbObjectPropertiesTable)
2732
+            ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
2733
+            ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2734
+        $query->execute();
2735
+    }
2736
+
2737
+    /**
2738
+     * get ID from a given calendar object
2739
+     *
2740
+     * @param int $calendarId
2741
+     * @param string $uri
2742
+     * @param int $calendarType
2743
+     * @return int
2744
+     */
2745
+    protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
2746
+        $query = $this->db->getQueryBuilder();
2747
+        $query->select('id')
2748
+            ->from('calendarobjects')
2749
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
2750
+            ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
2751
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
2752
+
2753
+        $result = $query->execute();
2754
+        $objectIds = $result->fetch();
2755
+        $result->closeCursor();
2756
+
2757
+        if (!isset($objectIds['id'])) {
2758
+            throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2759
+        }
2760
+
2761
+        return (int)$objectIds['id'];
2762
+    }
2763
+
2764
+    /**
2765
+     * return legacy endpoint principal name to new principal name
2766
+     *
2767
+     * @param $principalUri
2768
+     * @param $toV2
2769
+     * @return string
2770
+     */
2771
+    private function convertPrincipal($principalUri, $toV2) {
2772
+        if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
2773
+            list(, $name) = Uri\split($principalUri);
2774
+            if ($toV2 === true) {
2775
+                return "principals/users/$name";
2776
+            }
2777
+            return "principals/$name";
2778
+        }
2779
+        return $principalUri;
2780
+    }
2781
+
2782
+    /**
2783
+     * adds information about an owner to the calendar data
2784
+     *
2785
+     * @param $calendarInfo
2786
+     */
2787
+    private function addOwnerPrincipal(&$calendarInfo) {
2788
+        $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2789
+        $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2790
+        if (isset($calendarInfo[$ownerPrincipalKey])) {
2791
+            $uri = $calendarInfo[$ownerPrincipalKey];
2792
+        } else {
2793
+            $uri = $calendarInfo['principaluri'];
2794
+        }
2795
+
2796
+        $principalInformation = $this->principalBackend->getPrincipalByPath($uri);
2797
+        if (isset($principalInformation['{DAV:}displayname'])) {
2798
+            $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
2799
+        }
2800
+    }
2801 2801
 }
Please login to merge, or discard this patch.
Spacing   +134 added lines, -134 removed lines patch added patch discarded remove patch
@@ -247,7 +247,7 @@  discard block
 block discarded – undo
247 247
 			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
248 248
 		}
249 249
 
250
-		return (int)$query->execute()->fetchColumn();
250
+		return (int) $query->execute()->fetchColumn();
251 251
 	}
252 252
 
253 253
 	/**
@@ -297,18 +297,18 @@  discard block
 block discarded – undo
297 297
 		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
298 298
 			$components = [];
299 299
 			if ($row['components']) {
300
-				$components = explode(',',$row['components']);
300
+				$components = explode(',', $row['components']);
301 301
 			}
302 302
 
303 303
 			$calendar = [
304 304
 				'id' => $row['id'],
305 305
 				'uri' => $row['uri'],
306 306
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
307
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
308
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
309
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
310
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
311
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
307
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
308
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
309
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
310
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
311
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
312 312
 			];
313 313
 
314 314
 			foreach ($this->propertyMap as $xmlName=>$dbName) {
@@ -328,10 +328,10 @@  discard block
 block discarded – undo
328 328
 		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
329 329
 		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
330 330
 
331
-		$principals = array_map(function ($principal) {
331
+		$principals = array_map(function($principal) {
332 332
 			return urldecode($principal);
333 333
 		}, $principals);
334
-		$principals[]= $principalUri;
334
+		$principals[] = $principalUri;
335 335
 
336 336
 		$fields = array_values($this->propertyMap);
337 337
 		$fields[] = 'a.id';
@@ -351,7 +351,7 @@  discard block
 block discarded – undo
351 351
 			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
352 352
 			->execute();
353 353
 
354
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
354
+		$readOnlyPropertyName = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only';
355 355
 		while ($row = $result->fetch()) {
356 356
 			if ($row['principaluri'] === $principalUri) {
357 357
 				continue;
@@ -371,21 +371,21 @@  discard block
 block discarded – undo
371 371
 			}
372 372
 
373 373
 			list(, $name) = Uri\split($row['principaluri']);
374
-			$uri = $row['uri'] . '_shared_by_' . $name;
375
-			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
374
+			$uri = $row['uri'].'_shared_by_'.$name;
375
+			$row['displayname'] = $row['displayname'].' ('.$this->getUserDisplayName($name).')';
376 376
 			$components = [];
377 377
 			if ($row['components']) {
378
-				$components = explode(',',$row['components']);
378
+				$components = explode(',', $row['components']);
379 379
 			}
380 380
 			$calendar = [
381 381
 				'id' => $row['id'],
382 382
 				'uri' => $uri,
383 383
 				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
384
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
385
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
386
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
387
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
388
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
384
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
385
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
386
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
387
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
388
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
389 389
 				$readOnlyPropertyName => $readOnly,
390 390
 			];
391 391
 
@@ -425,16 +425,16 @@  discard block
 block discarded – undo
425 425
 		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
426 426
 			$components = [];
427 427
 			if ($row['components']) {
428
-				$components = explode(',',$row['components']);
428
+				$components = explode(',', $row['components']);
429 429
 			}
430 430
 			$calendar = [
431 431
 				'id' => $row['id'],
432 432
 				'uri' => $row['uri'],
433 433
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
434
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
435
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
436
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
437
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
434
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
435
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
436
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
437
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
438 438
 			];
439 439
 			foreach ($this->propertyMap as $xmlName=>$dbName) {
440 440
 				$calendar[$xmlName] = $row[$dbName];
@@ -493,22 +493,22 @@  discard block
 block discarded – undo
493 493
 
494 494
 		while ($row = $result->fetch()) {
495 495
 			list(, $name) = Uri\split($row['principaluri']);
496
-			$row['displayname'] = $row['displayname'] . "($name)";
496
+			$row['displayname'] = $row['displayname']."($name)";
497 497
 			$components = [];
498 498
 			if ($row['components']) {
499
-				$components = explode(',',$row['components']);
499
+				$components = explode(',', $row['components']);
500 500
 			}
501 501
 			$calendar = [
502 502
 				'id' => $row['id'],
503 503
 				'uri' => $row['publicuri'],
504 504
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
505
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
506
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
507
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
508
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
509
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
510
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
511
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
505
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
506
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
507
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
508
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
509
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
510
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only' => (int) $row['access'] === Backend::ACCESS_READ,
511
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}public' => (int) $row['access'] === self::ACCESS_PUBLIC,
512 512
 			];
513 513
 
514 514
 			foreach ($this->propertyMap as $xmlName=>$dbName) {
@@ -555,26 +555,26 @@  discard block
 block discarded – undo
555 555
 		$result->closeCursor();
556 556
 
557 557
 		if ($row === false) {
558
-			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
558
+			throw new NotFound('Node with name \''.$uri.'\' could not be found');
559 559
 		}
560 560
 
561 561
 		list(, $name) = Uri\split($row['principaluri']);
562
-		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
562
+		$row['displayname'] = $row['displayname'].' '."($name)";
563 563
 		$components = [];
564 564
 		if ($row['components']) {
565
-			$components = explode(',',$row['components']);
565
+			$components = explode(',', $row['components']);
566 566
 		}
567 567
 		$calendar = [
568 568
 			'id' => $row['id'],
569 569
 			'uri' => $row['publicuri'],
570 570
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
571
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
572
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
573
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
574
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
575
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
576
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
577
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
571
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
572
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
573
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
574
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
575
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
576
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only' => (int) $row['access'] === Backend::ACCESS_READ,
577
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}public' => (int) $row['access'] === self::ACCESS_PUBLIC,
578 578
 		];
579 579
 
580 580
 		foreach ($this->propertyMap as $xmlName=>$dbName) {
@@ -616,17 +616,17 @@  discard block
 block discarded – undo
616 616
 
617 617
 		$components = [];
618 618
 		if ($row['components']) {
619
-			$components = explode(',',$row['components']);
619
+			$components = explode(',', $row['components']);
620 620
 		}
621 621
 
622 622
 		$calendar = [
623 623
 			'id' => $row['id'],
624 624
 			'uri' => $row['uri'],
625 625
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
626
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
627
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
628
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
629
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
626
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
627
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
628
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
629
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
630 630
 		];
631 631
 
632 632
 		foreach ($this->propertyMap as $xmlName=>$dbName) {
@@ -666,17 +666,17 @@  discard block
 block discarded – undo
666 666
 
667 667
 		$components = [];
668 668
 		if ($row['components']) {
669
-			$components = explode(',',$row['components']);
669
+			$components = explode(',', $row['components']);
670 670
 		}
671 671
 
672 672
 		$calendar = [
673 673
 			'id' => $row['id'],
674 674
 			'uri' => $row['uri'],
675 675
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
676
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
677
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
678
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
679
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
676
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
677
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
678
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
679
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
680 680
 		];
681 681
 
682 682
 		foreach ($this->propertyMap as $xmlName=>$dbName) {
@@ -705,7 +705,7 @@  discard block
 block discarded – undo
705 705
 			->from('calendarsubscriptions')
706 706
 			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
707 707
 			->orderBy('calendarorder', 'asc');
708
-		$stmt =$query->execute();
708
+		$stmt = $query->execute();
709 709
 
710 710
 		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
711 711
 		$stmt->closeCursor();
@@ -719,8 +719,8 @@  discard block
 block discarded – undo
719 719
 			'principaluri' => $row['principaluri'],
720 720
 			'source'       => $row['source'],
721 721
 			'lastmodified' => $row['lastmodified'],
722
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
723
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
722
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
723
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
724 724
 		];
725 725
 
726 726
 		foreach ($this->subscriptionPropertyMap as $xmlName=>$dbName) {
@@ -757,16 +757,16 @@  discard block
 block discarded – undo
757 757
 		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
758 758
 		if (isset($properties[$sccs])) {
759 759
 			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
760
-				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
760
+				throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
761 761
 			}
762
-			$values['components'] = implode(',',$properties[$sccs]->getValue());
762
+			$values['components'] = implode(',', $properties[$sccs]->getValue());
763 763
 		} elseif (isset($properties['components'])) {
764 764
 			// Allow to provide components internally without having
765 765
 			// to create a SupportedCalendarComponentSet object
766 766
 			$values['components'] = $properties['components'];
767 767
 		}
768 768
 
769
-		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
769
+		$transp = '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp';
770 770
 		if (isset($properties[$transp])) {
771 771
 			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
772 772
 		}
@@ -786,7 +786,7 @@  discard block
 block discarded – undo
786 786
 		$calendarId = $query->getLastInsertId();
787 787
 
788 788
 		$calendarData = $this->getCalendarById($calendarId);
789
-		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
789
+		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int) $calendarId, $calendarData));
790 790
 		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
791 791
 			'\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
792 792
 			[
@@ -815,13 +815,13 @@  discard block
 block discarded – undo
815 815
 	 */
816 816
 	public function updateCalendar($calendarId, PropPatch $propPatch) {
817 817
 		$supportedProperties = array_keys($this->propertyMap);
818
-		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
818
+		$supportedProperties[] = '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp';
819 819
 
820
-		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
820
+		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
821 821
 			$newValues = [];
822 822
 			foreach ($mutations as $propertyName => $propertyValue) {
823 823
 				switch ($propertyName) {
824
-					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
824
+					case '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp':
825 825
 						$fieldName = 'transparent';
826 826
 						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
827 827
 						break;
@@ -843,7 +843,7 @@  discard block
 block discarded – undo
843 843
 
844 844
 			$calendarData = $this->getCalendarById($calendarId);
845 845
 			$shares = $this->getShares($calendarId);
846
-			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
846
+			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int) $calendarId, $calendarData, $shares, $mutations));
847 847
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
848 848
 				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
849 849
 				[
@@ -893,7 +893,7 @@  discard block
 block discarded – undo
893 893
 			->execute();
894 894
 
895 895
 		if ($calendarData) {
896
-			$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
896
+			$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int) $calendarId, $calendarData, $shares));
897 897
 		}
898 898
 	}
899 899
 
@@ -939,7 +939,7 @@  discard block
 block discarded – undo
939 939
 	 * @param int $calendarType
940 940
 	 * @return array
941 941
 	 */
942
-	public function getCalendarObjects($calendarId, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
942
+	public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
943 943
 		$query = $this->db->getQueryBuilder();
944 944
 		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
945 945
 			->from('calendarobjects')
@@ -953,11 +953,11 @@  discard block
 block discarded – undo
953 953
 				'id'           => $row['id'],
954 954
 				'uri'          => $row['uri'],
955 955
 				'lastmodified' => $row['lastmodified'],
956
-				'etag'         => '"' . $row['etag'] . '"',
956
+				'etag'         => '"'.$row['etag'].'"',
957 957
 				'calendarid'   => $row['calendarid'],
958
-				'size'         => (int)$row['size'],
958
+				'size'         => (int) $row['size'],
959 959
 				'component'    => strtolower($row['componenttype']),
960
-				'classification'=> (int)$row['classification']
960
+				'classification'=> (int) $row['classification']
961 961
 			];
962 962
 		}
963 963
 
@@ -981,7 +981,7 @@  discard block
 block discarded – undo
981 981
 	 * @param int $calendarType
982 982
 	 * @return array|null
983 983
 	 */
984
-	public function getCalendarObject($calendarId, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
984
+	public function getCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
985 985
 		$query = $this->db->getQueryBuilder();
986 986
 		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
987 987
 			->from('calendarobjects')
@@ -999,12 +999,12 @@  discard block
 block discarded – undo
999 999
 			'id'            => $row['id'],
1000 1000
 			'uri'           => $row['uri'],
1001 1001
 			'lastmodified'  => $row['lastmodified'],
1002
-			'etag'          => '"' . $row['etag'] . '"',
1002
+			'etag'          => '"'.$row['etag'].'"',
1003 1003
 			'calendarid'    => $row['calendarid'],
1004
-			'size'          => (int)$row['size'],
1004
+			'size'          => (int) $row['size'],
1005 1005
 			'calendardata'  => $this->readBlob($row['calendardata']),
1006 1006
 			'component'     => strtolower($row['componenttype']),
1007
-			'classification'=> (int)$row['classification']
1007
+			'classification'=> (int) $row['classification']
1008 1008
 		];
1009 1009
 	}
1010 1010
 
@@ -1021,7 +1021,7 @@  discard block
 block discarded – undo
1021 1021
 	 * @param int $calendarType
1022 1022
 	 * @return array
1023 1023
 	 */
1024
-	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
1024
+	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1025 1025
 		if (empty($uris)) {
1026 1026
 			return [];
1027 1027
 		}
@@ -1045,12 +1045,12 @@  discard block
 block discarded – undo
1045 1045
 					'id'           => $row['id'],
1046 1046
 					'uri'          => $row['uri'],
1047 1047
 					'lastmodified' => $row['lastmodified'],
1048
-					'etag'         => '"' . $row['etag'] . '"',
1048
+					'etag'         => '"'.$row['etag'].'"',
1049 1049
 					'calendarid'   => $row['calendarid'],
1050
-					'size'         => (int)$row['size'],
1050
+					'size'         => (int) $row['size'],
1051 1051
 					'calendardata' => $this->readBlob($row['calendardata']),
1052 1052
 					'component'    => strtolower($row['componenttype']),
1053
-					'classification' => (int)$row['classification']
1053
+					'classification' => (int) $row['classification']
1054 1054
 				];
1055 1055
 			}
1056 1056
 			$result->closeCursor();
@@ -1078,7 +1078,7 @@  discard block
 block discarded – undo
1078 1078
 	 * @param int $calendarType
1079 1079
 	 * @return string
1080 1080
 	 */
1081
-	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1081
+	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1082 1082
 		$extraData = $this->getDenormalizedData($calendarData);
1083 1083
 
1084 1084
 		$q = $this->db->getQueryBuilder();
@@ -1122,7 +1122,7 @@  discard block
 block discarded – undo
1122 1122
 			$calendarRow = $this->getCalendarById($calendarId);
1123 1123
 			$shares = $this->getShares($calendarId);
1124 1124
 
1125
-			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1125
+			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int) $calendarId, $calendarRow, $shares, $objectRow));
1126 1126
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1127 1127
 				'\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1128 1128
 				[
@@ -1135,7 +1135,7 @@  discard block
 block discarded – undo
1135 1135
 		} else {
1136 1136
 			$subscriptionRow = $this->getSubscriptionById($calendarId);
1137 1137
 
1138
-			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1138
+			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int) $calendarId, $subscriptionRow, [], $objectRow));
1139 1139
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1140 1140
 				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1141 1141
 				[
@@ -1147,7 +1147,7 @@  discard block
 block discarded – undo
1147 1147
 			));
1148 1148
 		}
1149 1149
 
1150
-		return '"' . $extraData['etag'] . '"';
1150
+		return '"'.$extraData['etag'].'"';
1151 1151
 	}
1152 1152
 
1153 1153
 	/**
@@ -1169,7 +1169,7 @@  discard block
 block discarded – undo
1169 1169
 	 * @param int $calendarType
1170 1170
 	 * @return string
1171 1171
 	 */
1172
-	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1172
+	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1173 1173
 		$extraData = $this->getDenormalizedData($calendarData);
1174 1174
 		$query = $this->db->getQueryBuilder();
1175 1175
 		$query->update('calendarobjects')
@@ -1196,7 +1196,7 @@  discard block
 block discarded – undo
1196 1196
 				$calendarRow = $this->getCalendarById($calendarId);
1197 1197
 				$shares = $this->getShares($calendarId);
1198 1198
 
1199
-				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1199
+				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int) $calendarId, $calendarRow, $shares, $objectRow));
1200 1200
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1201 1201
 					'\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1202 1202
 					[
@@ -1209,7 +1209,7 @@  discard block
 block discarded – undo
1209 1209
 			} else {
1210 1210
 				$subscriptionRow = $this->getSubscriptionById($calendarId);
1211 1211
 
1212
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1212
+				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int) $calendarId, $subscriptionRow, [], $objectRow));
1213 1213
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1214 1214
 					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1215 1215
 					[
@@ -1222,7 +1222,7 @@  discard block
 block discarded – undo
1222 1222
 			}
1223 1223
 		}
1224 1224
 
1225
-		return '"' . $extraData['etag'] . '"';
1225
+		return '"'.$extraData['etag'].'"';
1226 1226
 	}
1227 1227
 
1228 1228
 	/**
@@ -1252,14 +1252,14 @@  discard block
 block discarded – undo
1252 1252
 	 * @param int $calendarType
1253 1253
 	 * @return void
1254 1254
 	 */
1255
-	public function deleteCalendarObject($calendarId, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1255
+	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1256 1256
 		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1257 1257
 		if (is_array($data)) {
1258 1258
 			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1259 1259
 				$calendarRow = $this->getCalendarById($calendarId);
1260 1260
 				$shares = $this->getShares($calendarId);
1261 1261
 
1262
-				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1262
+				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int) $calendarId, $calendarRow, $shares, $data));
1263 1263
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1264 1264
 					'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1265 1265
 					[
@@ -1272,7 +1272,7 @@  discard block
 block discarded – undo
1272 1272
 			} else {
1273 1273
 				$subscriptionRow = $this->getSubscriptionById($calendarId);
1274 1274
 
1275
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1275
+				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int) $calendarId, $subscriptionRow, [], $data));
1276 1276
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1277 1277
 					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1278 1278
 					[
@@ -1345,7 +1345,7 @@  discard block
 block discarded – undo
1345 1345
 	 * @param int $calendarType
1346 1346
 	 * @return array
1347 1347
 	 */
1348
-	public function calendarQuery($calendarId, array $filters, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
1348
+	public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1349 1349
 		$componentType = null;
1350 1350
 		$requirePostFilter = true;
1351 1351
 		$timeRange = null;
@@ -1439,7 +1439,7 @@  discard block
 block discarded – undo
1439 1439
 	 * @param integer|null $offset
1440 1440
 	 * @return array
1441 1441
 	 */
1442
-	public function calendarSearch($principalUri, array $filters, $limit=null, $offset=null) {
1442
+	public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1443 1443
 		$calendars = $this->getCalendarsForUser($principalUri);
1444 1444
 		$ownCalendars = [];
1445 1445
 		$sharedCalendars = [];
@@ -1544,7 +1544,7 @@  discard block
 block discarded – undo
1544 1544
 
1545 1545
 		$result = [];
1546 1546
 		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1547
-			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1547
+			$path = $uriMapper[$row['calendarid']].'/'.$row['uri'];
1548 1548
 			if (!in_array($path, $result)) {
1549 1549
 				$result[] = $path;
1550 1550
 			}
@@ -1592,8 +1592,8 @@  discard block
 block discarded – undo
1592 1592
 
1593 1593
 		if ($pattern !== '') {
1594 1594
 			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1595
-				$outerQuery->createNamedParameter('%' .
1596
-					$this->db->escapeLikeParameter($pattern) . '%')));
1595
+				$outerQuery->createNamedParameter('%'.
1596
+					$this->db->escapeLikeParameter($pattern).'%')));
1597 1597
 		}
1598 1598
 
1599 1599
 		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
@@ -1632,7 +1632,7 @@  discard block
 block discarded – undo
1632 1632
 		$result = $outerQuery->execute();
1633 1633
 		$calendarObjects = $result->fetchAll();
1634 1634
 
1635
-		return array_map(function ($o) {
1635
+		return array_map(function($o) {
1636 1636
 			$calendarData = Reader::read($o['calendardata']);
1637 1637
 			$comps = $calendarData->getComponents();
1638 1638
 			$objects = [];
@@ -1650,10 +1650,10 @@  discard block
 block discarded – undo
1650 1650
 				'type' => $o['componenttype'],
1651 1651
 				'uid' => $o['uid'],
1652 1652
 				'uri' => $o['uri'],
1653
-				'objects' => array_map(function ($c) {
1653
+				'objects' => array_map(function($c) {
1654 1654
 					return $this->transformSearchData($c);
1655 1655
 				}, $objects),
1656
-				'timezones' => array_map(function ($c) {
1656
+				'timezones' => array_map(function($c) {
1657 1657
 					return $this->transformSearchData($c);
1658 1658
 				}, $timezones),
1659 1659
 			];
@@ -1669,7 +1669,7 @@  discard block
 block discarded – undo
1669 1669
 		/** @var Component[] $subComponents */
1670 1670
 		$subComponents = $comp->getComponents();
1671 1671
 		/** @var Property[] $properties */
1672
-		$properties = array_filter($comp->children(), function ($c) {
1672
+		$properties = array_filter($comp->children(), function($c) {
1673 1673
 			return $c instanceof Property;
1674 1674
 		});
1675 1675
 		$validationRules = $comp->getValidationRules();
@@ -1747,7 +1747,7 @@  discard block
 block discarded – undo
1747 1747
 		$subscriptions = $this->getSubscriptionsForUser($principalUri);
1748 1748
 		foreach ($calendars as $calendar) {
1749 1749
 			$calendarAnd = $calendarObjectIdQuery->expr()->andX();
1750
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
1750
+			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int) $calendar['id'])));
1751 1751
 			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1752 1752
 
1753 1753
 			// If it's shared, limit search to public events
@@ -1759,7 +1759,7 @@  discard block
 block discarded – undo
1759 1759
 		}
1760 1760
 		foreach ($subscriptions as $subscription) {
1761 1761
 			$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
1762
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
1762
+			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int) $subscription['id'])));
1763 1763
 			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
1764 1764
 
1765 1765
 			// If it's shared, limit search to public events
@@ -1803,7 +1803,7 @@  discard block
 block discarded – undo
1803 1803
 			if (!$escapePattern) {
1804 1804
 				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
1805 1805
 			} else {
1806
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1806
+				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%'.$this->db->escapeLikeParameter($pattern).'%')));
1807 1807
 			}
1808 1808
 		}
1809 1809
 
@@ -1817,7 +1817,7 @@  discard block
 block discarded – undo
1817 1817
 		$result = $calendarObjectIdQuery->execute();
1818 1818
 		$matches = $result->fetchAll();
1819 1819
 		$result->closeCursor();
1820
-		$matches = array_map(static function (array $match):int {
1820
+		$matches = array_map(static function(array $match):int {
1821 1821
 			return (int) $match['objectid'];
1822 1822
 		}, $matches);
1823 1823
 
@@ -1830,9 +1830,9 @@  discard block
 block discarded – undo
1830 1830
 		$calendarObjects = $result->fetchAll();
1831 1831
 		$result->closeCursor();
1832 1832
 
1833
-		return array_map(function (array $array): array {
1834
-			$array['calendarid'] = (int)$array['calendarid'];
1835
-			$array['calendartype'] = (int)$array['calendartype'];
1833
+		return array_map(function(array $array): array {
1834
+			$array['calendarid'] = (int) $array['calendarid'];
1835
+			$array['calendartype'] = (int) $array['calendartype'];
1836 1836
 			$array['calendardata'] = $this->readBlob($array['calendardata']);
1837 1837
 
1838 1838
 			return $array;
@@ -1869,7 +1869,7 @@  discard block
 block discarded – undo
1869 1869
 		$stmt = $query->execute();
1870 1870
 
1871 1871
 		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1872
-			return $row['calendaruri'] . '/' . $row['objecturi'];
1872
+			return $row['calendaruri'].'/'.$row['objecturi'];
1873 1873
 		}
1874 1874
 
1875 1875
 		return null;
@@ -1932,10 +1932,10 @@  discard block
 block discarded – undo
1932 1932
 	 * @param int $calendarType
1933 1933
 	 * @return array
1934 1934
 	 */
1935
-	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
1935
+	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1936 1936
 		// Current synctoken
1937 1937
 		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1938
-		$stmt->execute([ $calendarId ]);
1938
+		$stmt->execute([$calendarId]);
1939 1939
 		$currentToken = $stmt->fetchColumn(0);
1940 1940
 
1941 1941
 		if (is_null($currentToken)) {
@@ -1951,8 +1951,8 @@  discard block
 block discarded – undo
1951 1951
 
1952 1952
 		if ($syncToken) {
1953 1953
 			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? AND `calendartype` = ? ORDER BY `synctoken`";
1954
-			if ($limit>0) {
1955
-				$query.= " LIMIT " . (int)$limit;
1954
+			if ($limit > 0) {
1955
+				$query .= " LIMIT ".(int) $limit;
1956 1956
 			}
1957 1957
 
1958 1958
 			// Fetching all changes
@@ -2037,7 +2037,7 @@  discard block
 block discarded – undo
2037 2037
 			->from('calendarsubscriptions')
2038 2038
 			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2039 2039
 			->orderBy('calendarorder', 'asc');
2040
-		$stmt =$query->execute();
2040
+		$stmt = $query->execute();
2041 2041
 
2042 2042
 		$subscriptions = [];
2043 2043
 		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
@@ -2048,8 +2048,8 @@  discard block
 block discarded – undo
2048 2048
 				'source'       => $row['source'],
2049 2049
 				'lastmodified' => $row['lastmodified'],
2050 2050
 
2051
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2052
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2051
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2052
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
2053 2053
 			];
2054 2054
 
2055 2055
 			foreach ($this->subscriptionPropertyMap as $xmlName=>$dbName) {
@@ -2113,7 +2113,7 @@  discard block
 block discarded – undo
2113 2113
 		$subscriptionId = $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
2114 2114
 
2115 2115
 		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2116
-		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent((int)$subscriptionId, $subscriptionRow));
2116
+		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent((int) $subscriptionId, $subscriptionRow));
2117 2117
 		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
2118 2118
 			'\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2119 2119
 			[
@@ -2144,7 +2144,7 @@  discard block
 block discarded – undo
2144 2144
 		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2145 2145
 		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2146 2146
 
2147
-		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2147
+		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
2148 2148
 			$newValues = [];
2149 2149
 
2150 2150
 			foreach ($mutations as $propertyName=>$propertyValue) {
@@ -2166,7 +2166,7 @@  discard block
 block discarded – undo
2166 2166
 				->execute();
2167 2167
 
2168 2168
 			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2169
-			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2169
+			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int) $subscriptionId, $subscriptionRow, [], $mutations));
2170 2170
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2171 2171
 				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2172 2172
 				[
@@ -2217,7 +2217,7 @@  discard block
 block discarded – undo
2217 2217
 			->execute();
2218 2218
 
2219 2219
 		if ($subscriptionRow) {
2220
-			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2220
+			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int) $subscriptionId, $subscriptionRow, []));
2221 2221
 		}
2222 2222
 	}
2223 2223
 
@@ -2255,8 +2255,8 @@  discard block
 block discarded – undo
2255 2255
 			'uri'          => $row['uri'],
2256 2256
 			'calendardata' => $row['calendardata'],
2257 2257
 			'lastmodified' => $row['lastmodified'],
2258
-			'etag'         => '"' . $row['etag'] . '"',
2259
-			'size'         => (int)$row['size'],
2258
+			'etag'         => '"'.$row['etag'].'"',
2259
+			'size'         => (int) $row['size'],
2260 2260
 		];
2261 2261
 	}
2262 2262
 
@@ -2284,8 +2284,8 @@  discard block
 block discarded – undo
2284 2284
 				'calendardata' => $row['calendardata'],
2285 2285
 				'uri'          => $row['uri'],
2286 2286
 				'lastmodified' => $row['lastmodified'],
2287
-				'etag'         => '"' . $row['etag'] . '"',
2288
-				'size'         => (int)$row['size'],
2287
+				'etag'         => '"'.$row['etag'].'"',
2288
+				'size'         => (int) $row['size'],
2289 2289
 			];
2290 2290
 		}
2291 2291
 
@@ -2338,14 +2338,14 @@  discard block
 block discarded – undo
2338 2338
 	 * @param int $calendarType
2339 2339
 	 * @return void
2340 2340
 	 */
2341
-	protected function addChange($calendarId, $objectUri, $operation, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2342
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2341
+	protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2342
+		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars' : 'calendarsubscriptions';
2343 2343
 
2344 2344
 		$query = $this->db->getQueryBuilder();
2345 2345
 		$query->select('synctoken')
2346 2346
 			->from($table)
2347 2347
 			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2348
-		$syncToken = (int)$query->execute()->fetchColumn();
2348
+		$syncToken = (int) $query->execute()->fetchColumn();
2349 2349
 
2350 2350
 		$query = $this->db->getQueryBuilder();
2351 2351
 		$query->insert('calendarchanges')
@@ -2388,9 +2388,9 @@  discard block
 block discarded – undo
2388 2388
 		$uid = null;
2389 2389
 		$classification = self::CLASSIFICATION_PUBLIC;
2390 2390
 		foreach ($vObject->getComponents() as $component) {
2391
-			if ($component->name!=='VTIMEZONE') {
2391
+			if ($component->name !== 'VTIMEZONE') {
2392 2392
 				$componentType = $component->name;
2393
-				$uid = (string)$component->UID;
2393
+				$uid = (string) $component->UID;
2394 2394
 				break;
2395 2395
 			}
2396 2396
 		}
@@ -2415,7 +2415,7 @@  discard block
 block discarded – undo
2415 2415
 					$lastOccurrence = $firstOccurrence;
2416 2416
 				}
2417 2417
 			} else {
2418
-				$it = new EventIterator($vObject, (string)$component->UID);
2418
+				$it = new EventIterator($vObject, (string) $component->UID);
2419 2419
 				$maxDate = new DateTime(self::MAX_DATE);
2420 2420
 				if ($it->isInfinite()) {
2421 2421
 					$lastOccurrence = $maxDate->getTimestamp();
@@ -2485,7 +2485,7 @@  discard block
 block discarded – undo
2485 2485
 			]));
2486 2486
 		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2487 2487
 
2488
-		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2488
+		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int) $calendarId, $calendarRow, $oldShares, $add, $remove));
2489 2489
 	}
2490 2490
 
2491 2491
 	/**
@@ -2493,7 +2493,7 @@  discard block
 block discarded – undo
2493 2493
 	 * @param int $calendarType
2494 2494
 	 * @return array
2495 2495
 	 */
2496
-	public function getShares($resourceId, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2496
+	public function getShares($resourceId, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2497 2497
 		return $this->calendarSharingBackend->getShares($resourceId);
2498 2498
 	}
2499 2499
 
@@ -2526,7 +2526,7 @@  discard block
 block discarded – undo
2526 2526
 				]);
2527 2527
 			$query->execute();
2528 2528
 
2529
-			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2529
+			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int) $calendarId, $calendarData, $publicUri));
2530 2530
 			return $publicUri;
2531 2531
 		}
2532 2532
 		$query->delete('dav_shares')
@@ -2534,7 +2534,7 @@  discard block
 block discarded – undo
2534 2534
 			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2535 2535
 		$query->execute();
2536 2536
 
2537
-		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2537
+		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int) $calendarId, $calendarData));
2538 2538
 		return null;
2539 2539
 	}
2540 2540
 
@@ -2574,7 +2574,7 @@  discard block
 block discarded – undo
2574 2574
 	 * @param string $calendarData
2575 2575
 	 * @param int $calendarType
2576 2576
 	 */
2577
-	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2577
+	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2578 2578
 		$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2579 2579
 
2580 2580
 		try {
@@ -2755,10 +2755,10 @@  discard block
 block discarded – undo
2755 2755
 		$result->closeCursor();
2756 2756
 
2757 2757
 		if (!isset($objectIds['id'])) {
2758
-			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2758
+			throw new \InvalidArgumentException('Calendarobject does not exists: '.$uri);
2759 2759
 		}
2760 2760
 
2761
-		return (int)$objectIds['id'];
2761
+		return (int) $objectIds['id'];
2762 2762
 	}
2763 2763
 
2764 2764
 	/**
@@ -2785,8 +2785,8 @@  discard block
 block discarded – undo
2785 2785
 	 * @param $calendarInfo
2786 2786
 	 */
2787 2787
 	private function addOwnerPrincipal(&$calendarInfo) {
2788
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2789
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2788
+		$ownerPrincipalKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal';
2789
+		$displaynameKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}owner-displayname';
2790 2790
 		if (isset($calendarInfo[$ownerPrincipalKey])) {
2791 2791
 			$uri = $calendarInfo[$ownerPrincipalKey];
2792 2792
 		} else {
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php 1 patch
Indentation   +88 added lines, -88 removed lines patch added patch discarded remove patch
@@ -49,101 +49,101 @@
 block discarded – undo
49 49
  */
50 50
 class PushProvider extends AbstractProvider {
51 51
 
52
-	/** @var string */
53
-	public const NOTIFICATION_TYPE = 'DISPLAY';
52
+    /** @var string */
53
+    public const NOTIFICATION_TYPE = 'DISPLAY';
54 54
 
55
-	/** @var IManager */
56
-	private $manager;
55
+    /** @var IManager */
56
+    private $manager;
57 57
 
58
-	/** @var ITimeFactory */
59
-	private $timeFactory;
58
+    /** @var ITimeFactory */
59
+    private $timeFactory;
60 60
 
61
-	/**
62
-	 * @param IConfig $config
63
-	 * @param IManager $manager
64
-	 * @param ILogger $logger
65
-	 * @param L10NFactory $l10nFactory
66
-	 * @param IUrlGenerator $urlGenerator
67
-	 * @param ITimeFactory $timeFactory
68
-	 */
69
-	public function __construct(IConfig $config,
70
-								IManager $manager,
71
-								ILogger $logger,
72
-								L10NFactory $l10nFactory,
73
-								IURLGenerator $urlGenerator,
74
-								ITimeFactory $timeFactory) {
75
-		parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
76
-		$this->manager = $manager;
77
-		$this->timeFactory = $timeFactory;
78
-	}
61
+    /**
62
+     * @param IConfig $config
63
+     * @param IManager $manager
64
+     * @param ILogger $logger
65
+     * @param L10NFactory $l10nFactory
66
+     * @param IUrlGenerator $urlGenerator
67
+     * @param ITimeFactory $timeFactory
68
+     */
69
+    public function __construct(IConfig $config,
70
+                                IManager $manager,
71
+                                ILogger $logger,
72
+                                L10NFactory $l10nFactory,
73
+                                IURLGenerator $urlGenerator,
74
+                                ITimeFactory $timeFactory) {
75
+        parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
76
+        $this->manager = $manager;
77
+        $this->timeFactory = $timeFactory;
78
+    }
79 79
 
80
-	/**
81
-	 * Send push notification to all users.
82
-	 *
83
-	 * @param VEvent $vevent
84
-	 * @param string $calendarDisplayName
85
-	 * @param IUser[] $users
86
-	 * @throws \Exception
87
-	 */
88
-	public function send(VEvent $vevent,
89
-						 string $calendarDisplayName=null,
90
-						 array $users=[]):void {
91
-		if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'no') !== 'yes') {
92
-			return;
93
-		}
80
+    /**
81
+     * Send push notification to all users.
82
+     *
83
+     * @param VEvent $vevent
84
+     * @param string $calendarDisplayName
85
+     * @param IUser[] $users
86
+     * @throws \Exception
87
+     */
88
+    public function send(VEvent $vevent,
89
+                            string $calendarDisplayName=null,
90
+                            array $users=[]):void {
91
+        if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'no') !== 'yes') {
92
+            return;
93
+        }
94 94
 
95
-		$eventDetails = $this->extractEventDetails($vevent);
96
-		$eventDetails['calendar_displayname'] = $calendarDisplayName;
97
-		$eventUUID = (string) $vevent->UID;
98
-		if (!$eventUUID) {
99
-			return;
100
-		};
101
-		$eventUUIDHash = hash('sha256', $eventUUID, false);
95
+        $eventDetails = $this->extractEventDetails($vevent);
96
+        $eventDetails['calendar_displayname'] = $calendarDisplayName;
97
+        $eventUUID = (string) $vevent->UID;
98
+        if (!$eventUUID) {
99
+            return;
100
+        };
101
+        $eventUUIDHash = hash('sha256', $eventUUID, false);
102 102
 
103
-		foreach ($users as $user) {
104
-			/** @var INotification $notification */
105
-			$notification = $this->manager->createNotification();
106
-			$notification->setApp(Application::APP_ID)
107
-				->setUser($user->getUID())
108
-				->setDateTime($this->timeFactory->getDateTime())
109
-				->setObject(Application::APP_ID, $eventUUIDHash)
110
-				->setSubject('calendar_reminder', [
111
-					'title' => $eventDetails['title'],
112
-					'start_atom' => $eventDetails['start_atom']
113
-				])
114
-				->setMessage('calendar_reminder', $eventDetails);
103
+        foreach ($users as $user) {
104
+            /** @var INotification $notification */
105
+            $notification = $this->manager->createNotification();
106
+            $notification->setApp(Application::APP_ID)
107
+                ->setUser($user->getUID())
108
+                ->setDateTime($this->timeFactory->getDateTime())
109
+                ->setObject(Application::APP_ID, $eventUUIDHash)
110
+                ->setSubject('calendar_reminder', [
111
+                    'title' => $eventDetails['title'],
112
+                    'start_atom' => $eventDetails['start_atom']
113
+                ])
114
+                ->setMessage('calendar_reminder', $eventDetails);
115 115
 
116
-			$this->manager->notify($notification);
117
-		}
118
-	}
116
+            $this->manager->notify($notification);
117
+        }
118
+    }
119 119
 
120
-	/**
121
-	 * @var VEvent $vevent
122
-	 * @return array
123
-	 * @throws \Exception
124
-	 */
125
-	protected function extractEventDetails(VEvent $vevent):array {
126
-		/** @var Property\ICalendar\DateTime $start */
127
-		$start = $vevent->DTSTART;
128
-		$end = $this->getDTEndFromEvent($vevent);
120
+    /**
121
+     * @var VEvent $vevent
122
+     * @return array
123
+     * @throws \Exception
124
+     */
125
+    protected function extractEventDetails(VEvent $vevent):array {
126
+        /** @var Property\ICalendar\DateTime $start */
127
+        $start = $vevent->DTSTART;
128
+        $end = $this->getDTEndFromEvent($vevent);
129 129
 
130
-		return [
131
-			'title' => isset($vevent->SUMMARY)
132
-				? ((string) $vevent->SUMMARY)
133
-				: null,
134
-			'description' => isset($vevent->DESCRIPTION)
135
-				? ((string) $vevent->DESCRIPTION)
136
-				: null,
137
-			'location' => isset($vevent->LOCATION)
138
-				? ((string) $vevent->LOCATION)
139
-				: null,
140
-			'all_day' => $start instanceof Property\ICalendar\Date,
141
-			'start_atom' => $start->getDateTime()->format(\DateTime::ATOM),
142
-			'start_is_floating' => $start->isFloating(),
143
-			'start_timezone' => $start->getDateTime()->getTimezone()->getName(),
144
-			'end_atom' => $end->getDateTime()->format(\DateTime::ATOM),
145
-			'end_is_floating' => $end->isFloating(),
146
-			'end_timezone' => $end->getDateTime()->getTimezone()->getName(),
147
-		];
148
-	}
130
+        return [
131
+            'title' => isset($vevent->SUMMARY)
132
+                ? ((string) $vevent->SUMMARY)
133
+                : null,
134
+            'description' => isset($vevent->DESCRIPTION)
135
+                ? ((string) $vevent->DESCRIPTION)
136
+                : null,
137
+            'location' => isset($vevent->LOCATION)
138
+                ? ((string) $vevent->LOCATION)
139
+                : null,
140
+            'all_day' => $start instanceof Property\ICalendar\Date,
141
+            'start_atom' => $start->getDateTime()->format(\DateTime::ATOM),
142
+            'start_is_floating' => $start->isFloating(),
143
+            'start_timezone' => $start->getDateTime()->getTimezone()->getName(),
144
+            'end_atom' => $end->getDateTime()->format(\DateTime::ATOM),
145
+            'end_is_floating' => $end->isFloating(),
146
+            'end_timezone' => $end->getDateTime()->getTimezone()->getName(),
147
+        ];
148
+    }
149 149
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php 1 patch
Indentation   +442 added lines, -442 removed lines patch added patch discarded remove patch
@@ -51,446 +51,446 @@
 block discarded – undo
51 51
  */
52 52
 class EmailProvider extends AbstractProvider {
53 53
 
54
-	/** @var string */
55
-	public const NOTIFICATION_TYPE = 'EMAIL';
56
-
57
-	/** @var IMailer */
58
-	private $mailer;
59
-
60
-	/**
61
-	 * @param IConfig $config
62
-	 * @param IMailer $mailer
63
-	 * @param ILogger $logger
64
-	 * @param L10NFactory $l10nFactory
65
-	 * @param IUrlGenerator $urlGenerator
66
-	 */
67
-	public function __construct(IConfig $config,
68
-								IMailer $mailer,
69
-								ILogger $logger,
70
-								L10NFactory $l10nFactory,
71
-								IURLGenerator $urlGenerator) {
72
-		parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
73
-		$this->mailer = $mailer;
74
-	}
75
-
76
-	/**
77
-	 * Send out notification via email
78
-	 *
79
-	 * @param VEvent $vevent
80
-	 * @param string $calendarDisplayName
81
-	 * @param array $users
82
-	 * @throws \Exception
83
-	 */
84
-	public function send(VEvent $vevent,
85
-						 string $calendarDisplayName,
86
-						 array $users=[]):void {
87
-		$fallbackLanguage = $this->getFallbackLanguage();
88
-
89
-		$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
90
-		$emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
91
-
92
-		// Quote from php.net:
93
-		// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
94
-		// => if there are duplicate email addresses, it will always take the system value
95
-		$emailAddresses = array_merge(
96
-			$emailAddressesOfAttendees,
97
-			$emailAddressesOfSharees
98
-		);
99
-
100
-		$sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
101
-		$organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
102
-
103
-		foreach ($sortedByLanguage as $lang => $emailAddresses) {
104
-			if (!$this->hasL10NForLang($lang)) {
105
-				$lang = $fallbackLanguage;
106
-			}
107
-			$l10n = $this->getL10NForLang($lang);
108
-			$fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
109
-
110
-			$template = $this->mailer->createEMailTemplate('dav.calendarReminder');
111
-			$template->addHeader();
112
-			$this->addSubjectAndHeading($template, $l10n, $vevent);
113
-			$this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
114
-			$template->addFooter();
115
-
116
-			foreach ($emailAddresses as $emailAddress) {
117
-				$message = $this->mailer->createMessage();
118
-				$message->setFrom([$fromEMail]);
119
-				if ($organizer) {
120
-					$message->setReplyTo($organizer);
121
-				}
122
-				$message->setTo([$emailAddress]);
123
-				$message->useTemplate($template);
124
-
125
-				try {
126
-					$failed = $this->mailer->send($message);
127
-					if ($failed) {
128
-						$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
129
-					}
130
-				} catch (\Exception $ex) {
131
-					$this->logger->logException($ex, ['app' => 'dav']);
132
-				}
133
-			}
134
-		}
135
-	}
136
-
137
-	/**
138
-	 * @param IEMailTemplate $template
139
-	 * @param IL10N $l10n
140
-	 * @param VEvent $vevent
141
-	 */
142
-	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
143
-		$template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
144
-		$template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
145
-	}
146
-
147
-	/**
148
-	 * @param IEMailTemplate $template
149
-	 * @param IL10N $l10n
150
-	 * @param string $calendarDisplayName
151
-	 * @param array $eventData
152
-	 */
153
-	private function addBulletList(IEMailTemplate $template,
154
-								   IL10N $l10n,
155
-								   string $calendarDisplayName,
156
-								   VEvent $vevent):void {
157
-		$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
158
-			$this->getAbsoluteImagePath('actions/info.svg'));
159
-
160
-		$template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
161
-			$this->getAbsoluteImagePath('places/calendar.svg'));
162
-
163
-		if (isset($vevent->LOCATION)) {
164
-			$template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
165
-				$this->getAbsoluteImagePath('actions/address.svg'));
166
-		}
167
-		if (isset($vevent->DESCRIPTION)) {
168
-			$template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
169
-				$this->getAbsoluteImagePath('actions/more.svg'));
170
-		}
171
-	}
172
-
173
-	/**
174
-	 * @param string $path
175
-	 * @return string
176
-	 */
177
-	private function getAbsoluteImagePath(string $path):string {
178
-		return $this->urlGenerator->getAbsoluteURL(
179
-			$this->urlGenerator->imagePath('core', $path)
180
-		);
181
-	}
182
-
183
-	/**
184
-	 * @param VEvent $vevent
185
-	 * @return array|null
186
-	 */
187
-	private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
188
-		if (!$vevent->ORGANIZER) {
189
-			return null;
190
-		}
191
-
192
-		$organizer = $vevent->ORGANIZER;
193
-		if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
194
-			return null;
195
-		}
196
-
197
-		$organizerEMail = substr($organizer->getValue(), 7);
198
-
199
-		$name = $organizer->offsetGet('CN');
200
-		if ($name instanceof Parameter) {
201
-			return [$organizerEMail => $name];
202
-		}
203
-
204
-		return [$organizerEMail];
205
-	}
206
-
207
-	/**
208
-	 * @param array $emails
209
-	 * @param string $defaultLanguage
210
-	 * @return array
211
-	 */
212
-	private function sortEMailAddressesByLanguage(array $emails,
213
-												  string $defaultLanguage):array {
214
-		$sortedByLanguage = [];
215
-
216
-		foreach ($emails as $emailAddress => $parameters) {
217
-			if (isset($parameters['LANG'])) {
218
-				$lang = $parameters['LANG'];
219
-			} else {
220
-				$lang = $defaultLanguage;
221
-			}
222
-
223
-			if (!isset($sortedByLanguage[$lang])) {
224
-				$sortedByLanguage[$lang] = [];
225
-			}
226
-
227
-			$sortedByLanguage[$lang][] = $emailAddress;
228
-		}
229
-
230
-		return $sortedByLanguage;
231
-	}
232
-
233
-	/**
234
-	 * @param VEvent $vevent
235
-	 * @return array
236
-	 */
237
-	private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
238
-		$emailAddresses = [];
239
-
240
-		if (isset($vevent->ATTENDEE)) {
241
-			foreach ($vevent->ATTENDEE as $attendee) {
242
-				if (!($attendee instanceof VObject\Property)) {
243
-					continue;
244
-				}
245
-
246
-				$cuType = $this->getCUTypeOfAttendee($attendee);
247
-				if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
248
-					// Don't send emails to things
249
-					continue;
250
-				}
251
-
252
-				$partstat = $this->getPartstatOfAttendee($attendee);
253
-				if ($partstat === 'DECLINED') {
254
-					// Don't send out emails to people who declined
255
-					continue;
256
-				}
257
-				if ($partstat === 'DELEGATED') {
258
-					$delegates = $attendee->offsetGet('DELEGATED-TO');
259
-					if (!($delegates instanceof VObject\Parameter)) {
260
-						continue;
261
-					}
262
-
263
-					$emailAddressesOfDelegates = $delegates->getParts();
264
-					foreach ($emailAddressesOfDelegates as $addressesOfDelegate) {
265
-						if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
266
-							$emailAddresses[substr($addressesOfDelegate, 7)] = [];
267
-						}
268
-					}
269
-
270
-					continue;
271
-				}
272
-
273
-				$emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
274
-				if ($emailAddressOfAttendee !== null) {
275
-					$properties = [];
276
-
277
-					$langProp = $attendee->offsetGet('LANG');
278
-					if ($langProp instanceof VObject\Parameter) {
279
-						$properties['LANG'] = $langProp->getValue();
280
-					}
281
-
282
-					$emailAddresses[$emailAddressOfAttendee] = $properties;
283
-				}
284
-			}
285
-		}
286
-
287
-		if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
288
-			$emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
289
-		}
290
-
291
-		return $emailAddresses;
292
-	}
293
-
294
-
295
-
296
-	/**
297
-	 * @param VObject\Property $attendee
298
-	 * @return string
299
-	 */
300
-	private function getCUTypeOfAttendee(VObject\Property $attendee):string {
301
-		$cuType = $attendee->offsetGet('CUTYPE');
302
-		if ($cuType instanceof VObject\Parameter) {
303
-			return strtoupper($cuType->getValue());
304
-		}
305
-
306
-		return 'INDIVIDUAL';
307
-	}
308
-
309
-	/**
310
-	 * @param VObject\Property $attendee
311
-	 * @return string
312
-	 */
313
-	private function getPartstatOfAttendee(VObject\Property $attendee):string {
314
-		$partstat = $attendee->offsetGet('PARTSTAT');
315
-		if ($partstat instanceof VObject\Parameter) {
316
-			return strtoupper($partstat->getValue());
317
-		}
318
-
319
-		return 'NEEDS-ACTION';
320
-	}
321
-
322
-	/**
323
-	 * @param VObject\Property $attendee
324
-	 * @return bool
325
-	 */
326
-	private function hasAttendeeMailURI(VObject\Property $attendee):bool {
327
-		return stripos($attendee->getValue(), 'mailto:') === 0;
328
-	}
329
-
330
-	/**
331
-	 * @param VObject\Property $attendee
332
-	 * @return string|null
333
-	 */
334
-	private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
335
-		if (!$this->hasAttendeeMailURI($attendee)) {
336
-			return null;
337
-		}
338
-
339
-		return substr($attendee->getValue(), 7);
340
-	}
341
-
342
-	/**
343
-	 * @param array $users
344
-	 * @return array
345
-	 */
346
-	private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
347
-		$emailAddresses = [];
348
-
349
-		foreach ($users as $user) {
350
-			$emailAddress = $user->getEMailAddress();
351
-			if ($emailAddress) {
352
-				$lang = $this->l10nFactory->getUserLanguage($user);
353
-				if ($lang) {
354
-					$emailAddresses[$emailAddress] = [
355
-						'LANG' => $lang,
356
-					];
357
-				} else {
358
-					$emailAddresses[$emailAddress] = [];
359
-				}
360
-			}
361
-		}
362
-
363
-		return $emailAddresses;
364
-	}
365
-
366
-	/**
367
-	 * @param IL10N $l10n
368
-	 * @param VEvent $vevent
369
-	 * @return string
370
-	 * @throws \Exception
371
-	 */
372
-	private function generateDateString(IL10N $l10n, VEvent $vevent):string {
373
-		$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
374
-
375
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
376
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
377
-		/** @var \DateTimeImmutable $dtstartDt */
378
-		$dtstartDt = $vevent->DTSTART->getDateTime();
379
-		/** @var \DateTimeImmutable $dtendDt */
380
-		$dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
381
-
382
-		$diff = $dtstartDt->diff($dtendDt);
383
-
384
-		$dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
385
-		$dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
386
-
387
-		if ($isAllDay) {
388
-			// One day event
389
-			if ($diff->days === 1) {
390
-				return $this->getDateString($l10n, $dtstartDt);
391
-			}
392
-
393
-			return implode(' - ', [
394
-				$this->getDateString($l10n, $dtstartDt),
395
-				$this->getDateString($l10n, $dtendDt),
396
-			]);
397
-		}
398
-
399
-		$startTimezone = $endTimezone = null;
400
-		if (!$vevent->DTSTART->isFloating()) {
401
-			$startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
402
-			$endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
403
-		}
404
-
405
-		$localeStart = implode(', ', [
406
-			$this->getWeekDayName($l10n, $dtstartDt),
407
-			$this->getDateTimeString($l10n, $dtstartDt)
408
-		]);
409
-
410
-		// always show full date with timezone if timezones are different
411
-		if ($startTimezone !== $endTimezone) {
412
-			$localeEnd = implode(', ', [
413
-				$this->getWeekDayName($l10n, $dtendDt),
414
-				$this->getDateTimeString($l10n, $dtendDt)
415
-			]);
416
-
417
-			return $localeStart
418
-				. ' (' . $startTimezone . ') '
419
-				. ' - '
420
-				. $localeEnd
421
-				. ' (' . $endTimezone . ')';
422
-		}
423
-
424
-		// Show only the time if the day is the same
425
-		$localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
426
-			? $this->getTimeString($l10n, $dtendDt)
427
-			: implode(', ', [
428
-				$this->getWeekDayName($l10n, $dtendDt),
429
-				$this->getDateTimeString($l10n, $dtendDt)
430
-			]);
431
-
432
-		return $localeStart
433
-			. ' - '
434
-			. $localeEnd
435
-			. ' (' . $startTimezone . ')';
436
-	}
437
-
438
-	/**
439
-	 * @param DateTime $dtStart
440
-	 * @param DateTime $dtEnd
441
-	 * @return bool
442
-	 */
443
-	private function isDayEqual(DateTime $dtStart,
444
-								DateTime $dtEnd):bool {
445
-		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
446
-	}
447
-
448
-	/**
449
-	 * @param IL10N $l10n
450
-	 * @param DateTime $dt
451
-	 * @return string
452
-	 */
453
-	private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
454
-		return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
455
-	}
456
-
457
-	/**
458
-	 * @param IL10N $l10n
459
-	 * @param DateTime $dt
460
-	 * @return string
461
-	 */
462
-	private function getDateString(IL10N $l10n, DateTime $dt):string {
463
-		return $l10n->l('date', $dt, ['width' => 'medium']);
464
-	}
465
-
466
-	/**
467
-	 * @param IL10N $l10n
468
-	 * @param DateTime $dt
469
-	 * @return string
470
-	 */
471
-	private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
472
-		return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
473
-	}
474
-
475
-	/**
476
-	 * @param IL10N $l10n
477
-	 * @param DateTime $dt
478
-	 * @return string
479
-	 */
480
-	private function getTimeString(IL10N $l10n, DateTime $dt):string {
481
-		return $l10n->l('time', $dt, ['width' => 'short']);
482
-	}
483
-
484
-	/**
485
-	 * @param VEvent $vevent
486
-	 * @param IL10N $l10n
487
-	 * @return string
488
-	 */
489
-	private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
490
-		if (isset($vevent->SUMMARY)) {
491
-			return (string)$vevent->SUMMARY;
492
-		}
493
-
494
-		return $l10n->t('Untitled event');
495
-	}
54
+    /** @var string */
55
+    public const NOTIFICATION_TYPE = 'EMAIL';
56
+
57
+    /** @var IMailer */
58
+    private $mailer;
59
+
60
+    /**
61
+     * @param IConfig $config
62
+     * @param IMailer $mailer
63
+     * @param ILogger $logger
64
+     * @param L10NFactory $l10nFactory
65
+     * @param IUrlGenerator $urlGenerator
66
+     */
67
+    public function __construct(IConfig $config,
68
+                                IMailer $mailer,
69
+                                ILogger $logger,
70
+                                L10NFactory $l10nFactory,
71
+                                IURLGenerator $urlGenerator) {
72
+        parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
73
+        $this->mailer = $mailer;
74
+    }
75
+
76
+    /**
77
+     * Send out notification via email
78
+     *
79
+     * @param VEvent $vevent
80
+     * @param string $calendarDisplayName
81
+     * @param array $users
82
+     * @throws \Exception
83
+     */
84
+    public function send(VEvent $vevent,
85
+                            string $calendarDisplayName,
86
+                            array $users=[]):void {
87
+        $fallbackLanguage = $this->getFallbackLanguage();
88
+
89
+        $emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
90
+        $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
91
+
92
+        // Quote from php.net:
93
+        // If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
94
+        // => if there are duplicate email addresses, it will always take the system value
95
+        $emailAddresses = array_merge(
96
+            $emailAddressesOfAttendees,
97
+            $emailAddressesOfSharees
98
+        );
99
+
100
+        $sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
101
+        $organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
102
+
103
+        foreach ($sortedByLanguage as $lang => $emailAddresses) {
104
+            if (!$this->hasL10NForLang($lang)) {
105
+                $lang = $fallbackLanguage;
106
+            }
107
+            $l10n = $this->getL10NForLang($lang);
108
+            $fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
109
+
110
+            $template = $this->mailer->createEMailTemplate('dav.calendarReminder');
111
+            $template->addHeader();
112
+            $this->addSubjectAndHeading($template, $l10n, $vevent);
113
+            $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
114
+            $template->addFooter();
115
+
116
+            foreach ($emailAddresses as $emailAddress) {
117
+                $message = $this->mailer->createMessage();
118
+                $message->setFrom([$fromEMail]);
119
+                if ($organizer) {
120
+                    $message->setReplyTo($organizer);
121
+                }
122
+                $message->setTo([$emailAddress]);
123
+                $message->useTemplate($template);
124
+
125
+                try {
126
+                    $failed = $this->mailer->send($message);
127
+                    if ($failed) {
128
+                        $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
129
+                    }
130
+                } catch (\Exception $ex) {
131
+                    $this->logger->logException($ex, ['app' => 'dav']);
132
+                }
133
+            }
134
+        }
135
+    }
136
+
137
+    /**
138
+     * @param IEMailTemplate $template
139
+     * @param IL10N $l10n
140
+     * @param VEvent $vevent
141
+     */
142
+    private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
143
+        $template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
144
+        $template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
145
+    }
146
+
147
+    /**
148
+     * @param IEMailTemplate $template
149
+     * @param IL10N $l10n
150
+     * @param string $calendarDisplayName
151
+     * @param array $eventData
152
+     */
153
+    private function addBulletList(IEMailTemplate $template,
154
+                                    IL10N $l10n,
155
+                                    string $calendarDisplayName,
156
+                                    VEvent $vevent):void {
157
+        $template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
158
+            $this->getAbsoluteImagePath('actions/info.svg'));
159
+
160
+        $template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
161
+            $this->getAbsoluteImagePath('places/calendar.svg'));
162
+
163
+        if (isset($vevent->LOCATION)) {
164
+            $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
165
+                $this->getAbsoluteImagePath('actions/address.svg'));
166
+        }
167
+        if (isset($vevent->DESCRIPTION)) {
168
+            $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
169
+                $this->getAbsoluteImagePath('actions/more.svg'));
170
+        }
171
+    }
172
+
173
+    /**
174
+     * @param string $path
175
+     * @return string
176
+     */
177
+    private function getAbsoluteImagePath(string $path):string {
178
+        return $this->urlGenerator->getAbsoluteURL(
179
+            $this->urlGenerator->imagePath('core', $path)
180
+        );
181
+    }
182
+
183
+    /**
184
+     * @param VEvent $vevent
185
+     * @return array|null
186
+     */
187
+    private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
188
+        if (!$vevent->ORGANIZER) {
189
+            return null;
190
+        }
191
+
192
+        $organizer = $vevent->ORGANIZER;
193
+        if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
194
+            return null;
195
+        }
196
+
197
+        $organizerEMail = substr($organizer->getValue(), 7);
198
+
199
+        $name = $organizer->offsetGet('CN');
200
+        if ($name instanceof Parameter) {
201
+            return [$organizerEMail => $name];
202
+        }
203
+
204
+        return [$organizerEMail];
205
+    }
206
+
207
+    /**
208
+     * @param array $emails
209
+     * @param string $defaultLanguage
210
+     * @return array
211
+     */
212
+    private function sortEMailAddressesByLanguage(array $emails,
213
+                                                    string $defaultLanguage):array {
214
+        $sortedByLanguage = [];
215
+
216
+        foreach ($emails as $emailAddress => $parameters) {
217
+            if (isset($parameters['LANG'])) {
218
+                $lang = $parameters['LANG'];
219
+            } else {
220
+                $lang = $defaultLanguage;
221
+            }
222
+
223
+            if (!isset($sortedByLanguage[$lang])) {
224
+                $sortedByLanguage[$lang] = [];
225
+            }
226
+
227
+            $sortedByLanguage[$lang][] = $emailAddress;
228
+        }
229
+
230
+        return $sortedByLanguage;
231
+    }
232
+
233
+    /**
234
+     * @param VEvent $vevent
235
+     * @return array
236
+     */
237
+    private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
238
+        $emailAddresses = [];
239
+
240
+        if (isset($vevent->ATTENDEE)) {
241
+            foreach ($vevent->ATTENDEE as $attendee) {
242
+                if (!($attendee instanceof VObject\Property)) {
243
+                    continue;
244
+                }
245
+
246
+                $cuType = $this->getCUTypeOfAttendee($attendee);
247
+                if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
248
+                    // Don't send emails to things
249
+                    continue;
250
+                }
251
+
252
+                $partstat = $this->getPartstatOfAttendee($attendee);
253
+                if ($partstat === 'DECLINED') {
254
+                    // Don't send out emails to people who declined
255
+                    continue;
256
+                }
257
+                if ($partstat === 'DELEGATED') {
258
+                    $delegates = $attendee->offsetGet('DELEGATED-TO');
259
+                    if (!($delegates instanceof VObject\Parameter)) {
260
+                        continue;
261
+                    }
262
+
263
+                    $emailAddressesOfDelegates = $delegates->getParts();
264
+                    foreach ($emailAddressesOfDelegates as $addressesOfDelegate) {
265
+                        if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
266
+                            $emailAddresses[substr($addressesOfDelegate, 7)] = [];
267
+                        }
268
+                    }
269
+
270
+                    continue;
271
+                }
272
+
273
+                $emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
274
+                if ($emailAddressOfAttendee !== null) {
275
+                    $properties = [];
276
+
277
+                    $langProp = $attendee->offsetGet('LANG');
278
+                    if ($langProp instanceof VObject\Parameter) {
279
+                        $properties['LANG'] = $langProp->getValue();
280
+                    }
281
+
282
+                    $emailAddresses[$emailAddressOfAttendee] = $properties;
283
+                }
284
+            }
285
+        }
286
+
287
+        if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
288
+            $emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
289
+        }
290
+
291
+        return $emailAddresses;
292
+    }
293
+
294
+
295
+
296
+    /**
297
+     * @param VObject\Property $attendee
298
+     * @return string
299
+     */
300
+    private function getCUTypeOfAttendee(VObject\Property $attendee):string {
301
+        $cuType = $attendee->offsetGet('CUTYPE');
302
+        if ($cuType instanceof VObject\Parameter) {
303
+            return strtoupper($cuType->getValue());
304
+        }
305
+
306
+        return 'INDIVIDUAL';
307
+    }
308
+
309
+    /**
310
+     * @param VObject\Property $attendee
311
+     * @return string
312
+     */
313
+    private function getPartstatOfAttendee(VObject\Property $attendee):string {
314
+        $partstat = $attendee->offsetGet('PARTSTAT');
315
+        if ($partstat instanceof VObject\Parameter) {
316
+            return strtoupper($partstat->getValue());
317
+        }
318
+
319
+        return 'NEEDS-ACTION';
320
+    }
321
+
322
+    /**
323
+     * @param VObject\Property $attendee
324
+     * @return bool
325
+     */
326
+    private function hasAttendeeMailURI(VObject\Property $attendee):bool {
327
+        return stripos($attendee->getValue(), 'mailto:') === 0;
328
+    }
329
+
330
+    /**
331
+     * @param VObject\Property $attendee
332
+     * @return string|null
333
+     */
334
+    private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
335
+        if (!$this->hasAttendeeMailURI($attendee)) {
336
+            return null;
337
+        }
338
+
339
+        return substr($attendee->getValue(), 7);
340
+    }
341
+
342
+    /**
343
+     * @param array $users
344
+     * @return array
345
+     */
346
+    private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
347
+        $emailAddresses = [];
348
+
349
+        foreach ($users as $user) {
350
+            $emailAddress = $user->getEMailAddress();
351
+            if ($emailAddress) {
352
+                $lang = $this->l10nFactory->getUserLanguage($user);
353
+                if ($lang) {
354
+                    $emailAddresses[$emailAddress] = [
355
+                        'LANG' => $lang,
356
+                    ];
357
+                } else {
358
+                    $emailAddresses[$emailAddress] = [];
359
+                }
360
+            }
361
+        }
362
+
363
+        return $emailAddresses;
364
+    }
365
+
366
+    /**
367
+     * @param IL10N $l10n
368
+     * @param VEvent $vevent
369
+     * @return string
370
+     * @throws \Exception
371
+     */
372
+    private function generateDateString(IL10N $l10n, VEvent $vevent):string {
373
+        $isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
374
+
375
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
376
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
377
+        /** @var \DateTimeImmutable $dtstartDt */
378
+        $dtstartDt = $vevent->DTSTART->getDateTime();
379
+        /** @var \DateTimeImmutable $dtendDt */
380
+        $dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
381
+
382
+        $diff = $dtstartDt->diff($dtendDt);
383
+
384
+        $dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
385
+        $dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
386
+
387
+        if ($isAllDay) {
388
+            // One day event
389
+            if ($diff->days === 1) {
390
+                return $this->getDateString($l10n, $dtstartDt);
391
+            }
392
+
393
+            return implode(' - ', [
394
+                $this->getDateString($l10n, $dtstartDt),
395
+                $this->getDateString($l10n, $dtendDt),
396
+            ]);
397
+        }
398
+
399
+        $startTimezone = $endTimezone = null;
400
+        if (!$vevent->DTSTART->isFloating()) {
401
+            $startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
402
+            $endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
403
+        }
404
+
405
+        $localeStart = implode(', ', [
406
+            $this->getWeekDayName($l10n, $dtstartDt),
407
+            $this->getDateTimeString($l10n, $dtstartDt)
408
+        ]);
409
+
410
+        // always show full date with timezone if timezones are different
411
+        if ($startTimezone !== $endTimezone) {
412
+            $localeEnd = implode(', ', [
413
+                $this->getWeekDayName($l10n, $dtendDt),
414
+                $this->getDateTimeString($l10n, $dtendDt)
415
+            ]);
416
+
417
+            return $localeStart
418
+                . ' (' . $startTimezone . ') '
419
+                . ' - '
420
+                . $localeEnd
421
+                . ' (' . $endTimezone . ')';
422
+        }
423
+
424
+        // Show only the time if the day is the same
425
+        $localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
426
+            ? $this->getTimeString($l10n, $dtendDt)
427
+            : implode(', ', [
428
+                $this->getWeekDayName($l10n, $dtendDt),
429
+                $this->getDateTimeString($l10n, $dtendDt)
430
+            ]);
431
+
432
+        return $localeStart
433
+            . ' - '
434
+            . $localeEnd
435
+            . ' (' . $startTimezone . ')';
436
+    }
437
+
438
+    /**
439
+     * @param DateTime $dtStart
440
+     * @param DateTime $dtEnd
441
+     * @return bool
442
+     */
443
+    private function isDayEqual(DateTime $dtStart,
444
+                                DateTime $dtEnd):bool {
445
+        return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
446
+    }
447
+
448
+    /**
449
+     * @param IL10N $l10n
450
+     * @param DateTime $dt
451
+     * @return string
452
+     */
453
+    private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
454
+        return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
455
+    }
456
+
457
+    /**
458
+     * @param IL10N $l10n
459
+     * @param DateTime $dt
460
+     * @return string
461
+     */
462
+    private function getDateString(IL10N $l10n, DateTime $dt):string {
463
+        return $l10n->l('date', $dt, ['width' => 'medium']);
464
+    }
465
+
466
+    /**
467
+     * @param IL10N $l10n
468
+     * @param DateTime $dt
469
+     * @return string
470
+     */
471
+    private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
472
+        return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
473
+    }
474
+
475
+    /**
476
+     * @param IL10N $l10n
477
+     * @param DateTime $dt
478
+     * @return string
479
+     */
480
+    private function getTimeString(IL10N $l10n, DateTime $dt):string {
481
+        return $l10n->l('time', $dt, ['width' => 'short']);
482
+    }
483
+
484
+    /**
485
+     * @param VEvent $vevent
486
+     * @param IL10N $l10n
487
+     * @return string
488
+     */
489
+    private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
490
+        if (isset($vevent->SUMMARY)) {
491
+            return (string)$vevent->SUMMARY;
492
+        }
493
+
494
+        return $l10n->t('Untitled event');
495
+    }
496 496
 }
Please login to merge, or discard this patch.
lib/private/Security/Bruteforce/Throttler.php 1 patch
Indentation   +298 added lines, -298 removed lines patch added patch discarded remove patch
@@ -54,302 +54,302 @@
 block discarded – undo
54 54
  * @package OC\Security\Bruteforce
55 55
  */
56 56
 class Throttler {
57
-	public const LOGIN_ACTION = 'login';
58
-	public const MAX_DELAY = 25;
59
-	public const MAX_DELAY_MS = 25000; // in milliseconds
60
-	public const MAX_ATTEMPTS = 10;
61
-
62
-	/** @var IDBConnection */
63
-	private $db;
64
-	/** @var ITimeFactory */
65
-	private $timeFactory;
66
-	/** @var ILogger */
67
-	private $logger;
68
-	/** @var IConfig */
69
-	private $config;
70
-
71
-	/**
72
-	 * @param IDBConnection $db
73
-	 * @param ITimeFactory $timeFactory
74
-	 * @param ILogger $logger
75
-	 * @param IConfig $config
76
-	 */
77
-	public function __construct(IDBConnection $db,
78
-								ITimeFactory $timeFactory,
79
-								ILogger $logger,
80
-								IConfig $config) {
81
-		$this->db = $db;
82
-		$this->timeFactory = $timeFactory;
83
-		$this->logger = $logger;
84
-		$this->config = $config;
85
-	}
86
-
87
-	/**
88
-	 * Convert a number of seconds into the appropriate DateInterval
89
-	 *
90
-	 * @param int $expire
91
-	 * @return \DateInterval
92
-	 */
93
-	private function getCutoff(int $expire): \DateInterval {
94
-		$d1 = new \DateTime();
95
-		$d2 = clone $d1;
96
-		$d2->sub(new \DateInterval('PT' . $expire . 'S'));
97
-		return $d2->diff($d1);
98
-	}
99
-
100
-	/**
101
-	 *  Calculate the cut off timestamp
102
-	 *
103
-	 * @param float $maxAgeHours
104
-	 * @return int
105
-	 */
106
-	private function getCutoffTimestamp(float $maxAgeHours = 12.0): int {
107
-		return (new \DateTime())
108
-			->sub($this->getCutoff((int) ($maxAgeHours * 3600)))
109
-			->getTimestamp();
110
-	}
111
-
112
-	/**
113
-	 * Register a failed attempt to bruteforce a security control
114
-	 *
115
-	 * @param string $action
116
-	 * @param string $ip
117
-	 * @param array $metadata Optional metadata logged to the database
118
-	 */
119
-	public function registerAttempt(string $action,
120
-									string $ip,
121
-									array $metadata = []): void {
122
-		// No need to log if the bruteforce protection is disabled
123
-		if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
124
-			return;
125
-		}
126
-
127
-		$ipAddress = new IpAddress($ip);
128
-		$values = [
129
-			'action' => $action,
130
-			'occurred' => $this->timeFactory->getTime(),
131
-			'ip' => (string)$ipAddress,
132
-			'subnet' => $ipAddress->getSubnet(),
133
-			'metadata' => json_encode($metadata),
134
-		];
135
-
136
-		$this->logger->notice(
137
-			sprintf(
138
-				'Bruteforce attempt from "%s" detected for action "%s".',
139
-				$ip,
140
-				$action
141
-			),
142
-			[
143
-				'app' => 'core',
144
-			]
145
-		);
146
-
147
-		$qb = $this->db->getQueryBuilder();
148
-		$qb->insert('bruteforce_attempts');
149
-		foreach ($values as $column => $value) {
150
-			$qb->setValue($column, $qb->createNamedParameter($value));
151
-		}
152
-		$qb->execute();
153
-	}
154
-
155
-	/**
156
-	 * Check if the IP is whitelisted
157
-	 *
158
-	 * @param string $ip
159
-	 * @return bool
160
-	 */
161
-	private function isIPWhitelisted(string $ip): bool {
162
-		if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
163
-			return true;
164
-		}
165
-
166
-		$keys = $this->config->getAppKeys('bruteForce');
167
-		$keys = array_filter($keys, function ($key) {
168
-			return 0 === strpos($key, 'whitelist_');
169
-		});
170
-
171
-		if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
172
-			$type = 4;
173
-		} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
174
-			$type = 6;
175
-		} else {
176
-			return false;
177
-		}
178
-
179
-		$ip = inet_pton($ip);
180
-
181
-		foreach ($keys as $key) {
182
-			$cidr = $this->config->getAppValue('bruteForce', $key, null);
183
-
184
-			$cx = explode('/', $cidr);
185
-			$addr = $cx[0];
186
-			$mask = (int)$cx[1];
187
-
188
-			// Do not compare ipv4 to ipv6
189
-			if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ||
190
-				($type === 6 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
191
-				continue;
192
-			}
193
-
194
-			$addr = inet_pton($addr);
195
-
196
-			$valid = true;
197
-			for ($i = 0; $i < $mask; $i++) {
198
-				$part = ord($addr[(int)($i/8)]);
199
-				$orig = ord($ip[(int)($i/8)]);
200
-
201
-				$bitmask = 1 << (7 - ($i % 8));
202
-
203
-				$part = $part & $bitmask;
204
-				$orig = $orig & $bitmask;
205
-
206
-				if ($part !== $orig) {
207
-					$valid = false;
208
-					break;
209
-				}
210
-			}
211
-
212
-			if ($valid === true) {
213
-				return true;
214
-			}
215
-		}
216
-
217
-		return false;
218
-	}
219
-
220
-	/**
221
-	 * Get the throttling delay (in milliseconds)
222
-	 *
223
-	 * @param string $ip
224
-	 * @param string $action optionally filter by action
225
-	 * @param float $maxAgeHours
226
-	 * @return int
227
-	 */
228
-	public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
229
-		if ($ip === '') {
230
-			return 0;
231
-		}
232
-
233
-		$ipAddress = new IpAddress($ip);
234
-		if ($this->isIPWhitelisted((string)$ipAddress)) {
235
-			return 0;
236
-		}
237
-
238
-		$cutoffTime = $this->getCutoffTimestamp($maxAgeHours);
239
-
240
-		$qb = $this->db->getQueryBuilder();
241
-		$qb->select($qb->func()->count('*', 'attempts'))
242
-			->from('bruteforce_attempts')
243
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
244
-			->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())));
245
-
246
-		if ($action !== '') {
247
-			$qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)));
248
-		}
249
-
250
-		$result = $qb->execute();
251
-		$row = $result->fetch();
252
-		$result->closeCursor();
253
-
254
-		return (int) $row['attempts'];
255
-	}
256
-
257
-	/**
258
-	 * Get the throttling delay (in milliseconds)
259
-	 *
260
-	 * @param string $ip
261
-	 * @param string $action optionally filter by action
262
-	 * @return int
263
-	 */
264
-	public function getDelay(string $ip, string $action = ''): int {
265
-		$attempts = $this->getAttempts($ip, $action);
266
-		if ($attempts === 0) {
267
-			return 0;
268
-		}
269
-
270
-		$firstDelay = 0.1;
271
-		if ($attempts > self::MAX_ATTEMPTS) {
272
-			// Don't ever overflow. Just assume the maxDelay time:s
273
-			return self::MAX_DELAY_MS;
274
-		}
275
-
276
-		$delay = $firstDelay * 2**$attempts;
277
-		if ($delay > self::MAX_DELAY) {
278
-			return self::MAX_DELAY_MS;
279
-		}
280
-		return (int) \ceil($delay * 1000);
281
-	}
282
-
283
-	/**
284
-	 * Reset the throttling delay for an IP address, action and metadata
285
-	 *
286
-	 * @param string $ip
287
-	 * @param string $action
288
-	 * @param array $metadata
289
-	 */
290
-	public function resetDelay(string $ip, string $action, array $metadata): void {
291
-		$ipAddress = new IpAddress($ip);
292
-		if ($this->isIPWhitelisted((string)$ipAddress)) {
293
-			return;
294
-		}
295
-
296
-		$cutoffTime = $this->getCutoffTimestamp();
297
-
298
-		$qb = $this->db->getQueryBuilder();
299
-		$qb->delete('bruteforce_attempts')
300
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
301
-			->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())))
302
-			->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)))
303
-			->andWhere($qb->expr()->eq('metadata', $qb->createNamedParameter(json_encode($metadata))));
304
-
305
-		$qb->execute();
306
-	}
307
-
308
-	/**
309
-	 * Reset the throttling delay for an IP address
310
-	 *
311
-	 * @param string $ip
312
-	 */
313
-	public function resetDelayForIP($ip) {
314
-		$cutoffTime = $this->getCutoffTimestamp();
315
-
316
-		$qb = $this->db->getQueryBuilder();
317
-		$qb->delete('bruteforce_attempts')
318
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
319
-			->andWhere($qb->expr()->eq('ip', $qb->createNamedParameter($ip)));
320
-
321
-		$qb->execute();
322
-	}
323
-
324
-	/**
325
-	 * Will sleep for the defined amount of time
326
-	 *
327
-	 * @param string $ip
328
-	 * @param string $action optionally filter by action
329
-	 * @return int the time spent sleeping
330
-	 */
331
-	public function sleepDelay(string $ip, string $action = ''): int {
332
-		$delay = $this->getDelay($ip, $action);
333
-		usleep($delay * 1000);
334
-		return $delay;
335
-	}
336
-
337
-	/**
338
-	 * Will sleep for the defined amount of time unless maximum was reached in the last 30 minutes
339
-	 * In this case a "429 Too Many Request" exception is thrown
340
-	 *
341
-	 * @param string $ip
342
-	 * @param string $action optionally filter by action
343
-	 * @return int the time spent sleeping
344
-	 * @throws MaxDelayReached when reached the maximum
345
-	 */
346
-	public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
347
-		$delay = $this->getDelay($ip, $action);
348
-		if (($delay === self::MAX_DELAY_MS) && $this->getAttempts($ip, $action, 0.5) > self::MAX_ATTEMPTS) {
349
-			// If the ip made too many attempts within the last 30 mins we don't execute anymore
350
-			throw new MaxDelayReached('Reached maximum delay');
351
-		}
352
-		usleep($delay * 1000);
353
-		return $delay;
354
-	}
57
+    public const LOGIN_ACTION = 'login';
58
+    public const MAX_DELAY = 25;
59
+    public const MAX_DELAY_MS = 25000; // in milliseconds
60
+    public const MAX_ATTEMPTS = 10;
61
+
62
+    /** @var IDBConnection */
63
+    private $db;
64
+    /** @var ITimeFactory */
65
+    private $timeFactory;
66
+    /** @var ILogger */
67
+    private $logger;
68
+    /** @var IConfig */
69
+    private $config;
70
+
71
+    /**
72
+     * @param IDBConnection $db
73
+     * @param ITimeFactory $timeFactory
74
+     * @param ILogger $logger
75
+     * @param IConfig $config
76
+     */
77
+    public function __construct(IDBConnection $db,
78
+                                ITimeFactory $timeFactory,
79
+                                ILogger $logger,
80
+                                IConfig $config) {
81
+        $this->db = $db;
82
+        $this->timeFactory = $timeFactory;
83
+        $this->logger = $logger;
84
+        $this->config = $config;
85
+    }
86
+
87
+    /**
88
+     * Convert a number of seconds into the appropriate DateInterval
89
+     *
90
+     * @param int $expire
91
+     * @return \DateInterval
92
+     */
93
+    private function getCutoff(int $expire): \DateInterval {
94
+        $d1 = new \DateTime();
95
+        $d2 = clone $d1;
96
+        $d2->sub(new \DateInterval('PT' . $expire . 'S'));
97
+        return $d2->diff($d1);
98
+    }
99
+
100
+    /**
101
+     *  Calculate the cut off timestamp
102
+     *
103
+     * @param float $maxAgeHours
104
+     * @return int
105
+     */
106
+    private function getCutoffTimestamp(float $maxAgeHours = 12.0): int {
107
+        return (new \DateTime())
108
+            ->sub($this->getCutoff((int) ($maxAgeHours * 3600)))
109
+            ->getTimestamp();
110
+    }
111
+
112
+    /**
113
+     * Register a failed attempt to bruteforce a security control
114
+     *
115
+     * @param string $action
116
+     * @param string $ip
117
+     * @param array $metadata Optional metadata logged to the database
118
+     */
119
+    public function registerAttempt(string $action,
120
+                                    string $ip,
121
+                                    array $metadata = []): void {
122
+        // No need to log if the bruteforce protection is disabled
123
+        if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
124
+            return;
125
+        }
126
+
127
+        $ipAddress = new IpAddress($ip);
128
+        $values = [
129
+            'action' => $action,
130
+            'occurred' => $this->timeFactory->getTime(),
131
+            'ip' => (string)$ipAddress,
132
+            'subnet' => $ipAddress->getSubnet(),
133
+            'metadata' => json_encode($metadata),
134
+        ];
135
+
136
+        $this->logger->notice(
137
+            sprintf(
138
+                'Bruteforce attempt from "%s" detected for action "%s".',
139
+                $ip,
140
+                $action
141
+            ),
142
+            [
143
+                'app' => 'core',
144
+            ]
145
+        );
146
+
147
+        $qb = $this->db->getQueryBuilder();
148
+        $qb->insert('bruteforce_attempts');
149
+        foreach ($values as $column => $value) {
150
+            $qb->setValue($column, $qb->createNamedParameter($value));
151
+        }
152
+        $qb->execute();
153
+    }
154
+
155
+    /**
156
+     * Check if the IP is whitelisted
157
+     *
158
+     * @param string $ip
159
+     * @return bool
160
+     */
161
+    private function isIPWhitelisted(string $ip): bool {
162
+        if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
163
+            return true;
164
+        }
165
+
166
+        $keys = $this->config->getAppKeys('bruteForce');
167
+        $keys = array_filter($keys, function ($key) {
168
+            return 0 === strpos($key, 'whitelist_');
169
+        });
170
+
171
+        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
172
+            $type = 4;
173
+        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
174
+            $type = 6;
175
+        } else {
176
+            return false;
177
+        }
178
+
179
+        $ip = inet_pton($ip);
180
+
181
+        foreach ($keys as $key) {
182
+            $cidr = $this->config->getAppValue('bruteForce', $key, null);
183
+
184
+            $cx = explode('/', $cidr);
185
+            $addr = $cx[0];
186
+            $mask = (int)$cx[1];
187
+
188
+            // Do not compare ipv4 to ipv6
189
+            if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ||
190
+                ($type === 6 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
191
+                continue;
192
+            }
193
+
194
+            $addr = inet_pton($addr);
195
+
196
+            $valid = true;
197
+            for ($i = 0; $i < $mask; $i++) {
198
+                $part = ord($addr[(int)($i/8)]);
199
+                $orig = ord($ip[(int)($i/8)]);
200
+
201
+                $bitmask = 1 << (7 - ($i % 8));
202
+
203
+                $part = $part & $bitmask;
204
+                $orig = $orig & $bitmask;
205
+
206
+                if ($part !== $orig) {
207
+                    $valid = false;
208
+                    break;
209
+                }
210
+            }
211
+
212
+            if ($valid === true) {
213
+                return true;
214
+            }
215
+        }
216
+
217
+        return false;
218
+    }
219
+
220
+    /**
221
+     * Get the throttling delay (in milliseconds)
222
+     *
223
+     * @param string $ip
224
+     * @param string $action optionally filter by action
225
+     * @param float $maxAgeHours
226
+     * @return int
227
+     */
228
+    public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
229
+        if ($ip === '') {
230
+            return 0;
231
+        }
232
+
233
+        $ipAddress = new IpAddress($ip);
234
+        if ($this->isIPWhitelisted((string)$ipAddress)) {
235
+            return 0;
236
+        }
237
+
238
+        $cutoffTime = $this->getCutoffTimestamp($maxAgeHours);
239
+
240
+        $qb = $this->db->getQueryBuilder();
241
+        $qb->select($qb->func()->count('*', 'attempts'))
242
+            ->from('bruteforce_attempts')
243
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
244
+            ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())));
245
+
246
+        if ($action !== '') {
247
+            $qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)));
248
+        }
249
+
250
+        $result = $qb->execute();
251
+        $row = $result->fetch();
252
+        $result->closeCursor();
253
+
254
+        return (int) $row['attempts'];
255
+    }
256
+
257
+    /**
258
+     * Get the throttling delay (in milliseconds)
259
+     *
260
+     * @param string $ip
261
+     * @param string $action optionally filter by action
262
+     * @return int
263
+     */
264
+    public function getDelay(string $ip, string $action = ''): int {
265
+        $attempts = $this->getAttempts($ip, $action);
266
+        if ($attempts === 0) {
267
+            return 0;
268
+        }
269
+
270
+        $firstDelay = 0.1;
271
+        if ($attempts > self::MAX_ATTEMPTS) {
272
+            // Don't ever overflow. Just assume the maxDelay time:s
273
+            return self::MAX_DELAY_MS;
274
+        }
275
+
276
+        $delay = $firstDelay * 2**$attempts;
277
+        if ($delay > self::MAX_DELAY) {
278
+            return self::MAX_DELAY_MS;
279
+        }
280
+        return (int) \ceil($delay * 1000);
281
+    }
282
+
283
+    /**
284
+     * Reset the throttling delay for an IP address, action and metadata
285
+     *
286
+     * @param string $ip
287
+     * @param string $action
288
+     * @param array $metadata
289
+     */
290
+    public function resetDelay(string $ip, string $action, array $metadata): void {
291
+        $ipAddress = new IpAddress($ip);
292
+        if ($this->isIPWhitelisted((string)$ipAddress)) {
293
+            return;
294
+        }
295
+
296
+        $cutoffTime = $this->getCutoffTimestamp();
297
+
298
+        $qb = $this->db->getQueryBuilder();
299
+        $qb->delete('bruteforce_attempts')
300
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
301
+            ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())))
302
+            ->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)))
303
+            ->andWhere($qb->expr()->eq('metadata', $qb->createNamedParameter(json_encode($metadata))));
304
+
305
+        $qb->execute();
306
+    }
307
+
308
+    /**
309
+     * Reset the throttling delay for an IP address
310
+     *
311
+     * @param string $ip
312
+     */
313
+    public function resetDelayForIP($ip) {
314
+        $cutoffTime = $this->getCutoffTimestamp();
315
+
316
+        $qb = $this->db->getQueryBuilder();
317
+        $qb->delete('bruteforce_attempts')
318
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
319
+            ->andWhere($qb->expr()->eq('ip', $qb->createNamedParameter($ip)));
320
+
321
+        $qb->execute();
322
+    }
323
+
324
+    /**
325
+     * Will sleep for the defined amount of time
326
+     *
327
+     * @param string $ip
328
+     * @param string $action optionally filter by action
329
+     * @return int the time spent sleeping
330
+     */
331
+    public function sleepDelay(string $ip, string $action = ''): int {
332
+        $delay = $this->getDelay($ip, $action);
333
+        usleep($delay * 1000);
334
+        return $delay;
335
+    }
336
+
337
+    /**
338
+     * Will sleep for the defined amount of time unless maximum was reached in the last 30 minutes
339
+     * In this case a "429 Too Many Request" exception is thrown
340
+     *
341
+     * @param string $ip
342
+     * @param string $action optionally filter by action
343
+     * @return int the time spent sleeping
344
+     * @throws MaxDelayReached when reached the maximum
345
+     */
346
+    public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
347
+        $delay = $this->getDelay($ip, $action);
348
+        if (($delay === self::MAX_DELAY_MS) && $this->getAttempts($ip, $action, 0.5) > self::MAX_ATTEMPTS) {
349
+            // If the ip made too many attempts within the last 30 mins we don't execute anymore
350
+            throw new MaxDelayReached('Reached maximum delay');
351
+        }
352
+        usleep($delay * 1000);
353
+        return $delay;
354
+    }
355 355
 }
Please login to merge, or discard this patch.
lib/private/BackgroundJob/JobList.php 1 patch
Indentation   +290 added lines, -290 removed lines patch added patch discarded remove patch
@@ -42,294 +42,294 @@
 block discarded – undo
42 42
 
43 43
 class JobList implements IJobList {
44 44
 
45
-	/** @var IDBConnection */
46
-	protected $connection;
47
-
48
-	/**@var IConfig */
49
-	protected $config;
50
-
51
-	/**@var ITimeFactory */
52
-	protected $timeFactory;
53
-
54
-	/**
55
-	 * @param IDBConnection $connection
56
-	 * @param IConfig $config
57
-	 * @param ITimeFactory $timeFactory
58
-	 */
59
-	public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory) {
60
-		$this->connection = $connection;
61
-		$this->config = $config;
62
-		$this->timeFactory = $timeFactory;
63
-	}
64
-
65
-	/**
66
-	 * @param IJob|string $job
67
-	 * @param mixed $argument
68
-	 */
69
-	public function add($job, $argument = null) {
70
-		if (!$this->has($job, $argument)) {
71
-			if ($job instanceof IJob) {
72
-				$class = get_class($job);
73
-			} else {
74
-				$class = $job;
75
-			}
76
-
77
-			$argument = json_encode($argument);
78
-			if (strlen($argument) > 4000) {
79
-				throw new \InvalidArgumentException('Background job arguments can\'t exceed 4000 characters (json encoded)');
80
-			}
81
-
82
-			$query = $this->connection->getQueryBuilder();
83
-			$query->insert('jobs')
84
-				->values([
85
-					'class' => $query->createNamedParameter($class),
86
-					'argument' => $query->createNamedParameter($argument),
87
-					'last_run' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT),
88
-					'last_checked' => $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT),
89
-				]);
90
-			$query->execute();
91
-		}
92
-	}
93
-
94
-	/**
95
-	 * @param IJob|string $job
96
-	 * @param mixed $argument
97
-	 */
98
-	public function remove($job, $argument = null) {
99
-		if ($job instanceof IJob) {
100
-			$class = get_class($job);
101
-		} else {
102
-			$class = $job;
103
-		}
104
-
105
-		$query = $this->connection->getQueryBuilder();
106
-		$query->delete('jobs')
107
-			->where($query->expr()->eq('class', $query->createNamedParameter($class)));
108
-		if (!is_null($argument)) {
109
-			$argument = json_encode($argument);
110
-			$query->andWhere($query->expr()->eq('argument', $query->createNamedParameter($argument)));
111
-		}
112
-		$query->execute();
113
-	}
114
-
115
-	/**
116
-	 * @param int $id
117
-	 */
118
-	protected function removeById($id) {
119
-		$query = $this->connection->getQueryBuilder();
120
-		$query->delete('jobs')
121
-			->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
122
-		$query->execute();
123
-	}
124
-
125
-	/**
126
-	 * check if a job is in the list
127
-	 *
128
-	 * @param IJob|string $job
129
-	 * @param mixed $argument
130
-	 * @return bool
131
-	 */
132
-	public function has($job, $argument) {
133
-		if ($job instanceof IJob) {
134
-			$class = get_class($job);
135
-		} else {
136
-			$class = $job;
137
-		}
138
-		$argument = json_encode($argument);
139
-
140
-		$query = $this->connection->getQueryBuilder();
141
-		$query->select('id')
142
-			->from('jobs')
143
-			->where($query->expr()->eq('class', $query->createNamedParameter($class)))
144
-			->andWhere($query->expr()->eq('argument', $query->createNamedParameter($argument)))
145
-			->setMaxResults(1);
146
-
147
-		$result = $query->execute();
148
-		$row = $result->fetch();
149
-		$result->closeCursor();
150
-
151
-		return (bool) $row;
152
-	}
153
-
154
-	/**
155
-	 * get all jobs in the list
156
-	 *
157
-	 * @return IJob[]
158
-	 * @deprecated 9.0.0 - This method is dangerous since it can cause load and
159
-	 * memory problems when creating too many instances.
160
-	 */
161
-	public function getAll() {
162
-		$query = $this->connection->getQueryBuilder();
163
-		$query->select('*')
164
-			->from('jobs');
165
-		$result = $query->execute();
166
-
167
-		$jobs = [];
168
-		while ($row = $result->fetch()) {
169
-			$job = $this->buildJob($row);
170
-			if ($job) {
171
-				$jobs[] = $job;
172
-			}
173
-		}
174
-		$result->closeCursor();
175
-
176
-		return $jobs;
177
-	}
178
-
179
-	/**
180
-	 * get the next job in the list
181
-	 *
182
-	 * @return IJob|null
183
-	 */
184
-	public function getNext() {
185
-		$query = $this->connection->getQueryBuilder();
186
-		$query->select('*')
187
-			->from('jobs')
188
-			->where($query->expr()->lte('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - 12 * 3600, IQueryBuilder::PARAM_INT)))
189
-			->andWhere($query->expr()->lte('last_checked', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT)))
190
-			->orderBy('last_checked', 'ASC')
191
-			->setMaxResults(1);
192
-
193
-		$update = $this->connection->getQueryBuilder();
194
-		$update->update('jobs')
195
-			->set('reserved_at', $update->createNamedParameter($this->timeFactory->getTime()))
196
-			->set('last_checked', $update->createNamedParameter($this->timeFactory->getTime()))
197
-			->where($update->expr()->eq('id', $update->createParameter('jobid')))
198
-			->andWhere($update->expr()->eq('reserved_at', $update->createParameter('reserved_at')))
199
-			->andWhere($update->expr()->eq('last_checked', $update->createParameter('last_checked')));
200
-
201
-		$result = $query->execute();
202
-		$row = $result->fetch();
203
-		$result->closeCursor();
204
-
205
-		if ($row) {
206
-			$update->setParameter('jobid', $row['id']);
207
-			$update->setParameter('reserved_at', $row['reserved_at']);
208
-			$update->setParameter('last_checked', $row['last_checked']);
209
-			$count = $update->execute();
210
-
211
-			if ($count === 0) {
212
-				// Background job already executed elsewhere, try again.
213
-				return $this->getNext();
214
-			}
215
-			$job = $this->buildJob($row);
216
-
217
-			if ($job === null) {
218
-				// set the last_checked to 12h in the future to not check failing jobs all over again
219
-				$reset = $this->connection->getQueryBuilder();
220
-				$reset->update('jobs')
221
-					->set('reserved_at', $reset->expr()->literal(0, IQueryBuilder::PARAM_INT))
222
-					->set('last_checked', $reset->createNamedParameter($this->timeFactory->getTime() + 12 * 3600, IQueryBuilder::PARAM_INT))
223
-					->where($reset->expr()->eq('id', $reset->createNamedParameter($row['id'], IQueryBuilder::PARAM_INT)));
224
-				$reset->execute();
225
-
226
-				// Background job from disabled app, try again.
227
-				return $this->getNext();
228
-			}
229
-
230
-			return $job;
231
-		} else {
232
-			return null;
233
-		}
234
-	}
235
-
236
-	/**
237
-	 * @param int $id
238
-	 * @return IJob|null
239
-	 */
240
-	public function getById($id) {
241
-		$query = $this->connection->getQueryBuilder();
242
-		$query->select('*')
243
-			->from('jobs')
244
-			->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
245
-		$result = $query->execute();
246
-		$row = $result->fetch();
247
-		$result->closeCursor();
248
-
249
-		if ($row) {
250
-			return $this->buildJob($row);
251
-		} else {
252
-			return null;
253
-		}
254
-	}
255
-
256
-	/**
257
-	 * get the job object from a row in the db
258
-	 *
259
-	 * @param array $row
260
-	 * @return IJob|null
261
-	 */
262
-	private function buildJob($row) {
263
-		try {
264
-			try {
265
-				// Try to load the job as a service
266
-				/** @var IJob $job */
267
-				$job = \OC::$server->query($row['class']);
268
-			} catch (QueryException $e) {
269
-				if (class_exists($row['class'])) {
270
-					$class = $row['class'];
271
-					$job = new $class();
272
-				} else {
273
-					// job from disabled app or old version of an app, no need to do anything
274
-					return null;
275
-				}
276
-			}
277
-
278
-			$job->setId((int) $row['id']);
279
-			$job->setLastRun((int) $row['last_run']);
280
-			$job->setArgument(json_decode($row['argument'], true));
281
-			return $job;
282
-		} catch (AutoloadNotAllowedException $e) {
283
-			// job is from a disabled app, ignore
284
-			return null;
285
-		}
286
-	}
287
-
288
-	/**
289
-	 * set the job that was last ran
290
-	 *
291
-	 * @param IJob $job
292
-	 */
293
-	public function setLastJob(IJob $job) {
294
-		$this->unlockJob($job);
295
-		$this->config->setAppValue('backgroundjob', 'lastjob', $job->getId());
296
-	}
297
-
298
-	/**
299
-	 * Remove the reservation for a job
300
-	 *
301
-	 * @param IJob $job
302
-	 */
303
-	public function unlockJob(IJob $job) {
304
-		$query = $this->connection->getQueryBuilder();
305
-		$query->update('jobs')
306
-			->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT))
307
-			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
308
-		$query->execute();
309
-	}
310
-
311
-	/**
312
-	 * set the lastRun of $job to now
313
-	 *
314
-	 * @param IJob $job
315
-	 */
316
-	public function setLastRun(IJob $job) {
317
-		$query = $this->connection->getQueryBuilder();
318
-		$query->update('jobs')
319
-			->set('last_run', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
320
-			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
321
-		$query->execute();
322
-	}
323
-
324
-	/**
325
-	 * @param IJob $job
326
-	 * @param $timeTaken
327
-	 */
328
-	public function setExecutionTime(IJob $job, $timeTaken) {
329
-		$query = $this->connection->getQueryBuilder();
330
-		$query->update('jobs')
331
-			->set('execution_duration', $query->createNamedParameter($timeTaken, IQueryBuilder::PARAM_INT))
332
-			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
333
-		$query->execute();
334
-	}
45
+    /** @var IDBConnection */
46
+    protected $connection;
47
+
48
+    /**@var IConfig */
49
+    protected $config;
50
+
51
+    /**@var ITimeFactory */
52
+    protected $timeFactory;
53
+
54
+    /**
55
+     * @param IDBConnection $connection
56
+     * @param IConfig $config
57
+     * @param ITimeFactory $timeFactory
58
+     */
59
+    public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory) {
60
+        $this->connection = $connection;
61
+        $this->config = $config;
62
+        $this->timeFactory = $timeFactory;
63
+    }
64
+
65
+    /**
66
+     * @param IJob|string $job
67
+     * @param mixed $argument
68
+     */
69
+    public function add($job, $argument = null) {
70
+        if (!$this->has($job, $argument)) {
71
+            if ($job instanceof IJob) {
72
+                $class = get_class($job);
73
+            } else {
74
+                $class = $job;
75
+            }
76
+
77
+            $argument = json_encode($argument);
78
+            if (strlen($argument) > 4000) {
79
+                throw new \InvalidArgumentException('Background job arguments can\'t exceed 4000 characters (json encoded)');
80
+            }
81
+
82
+            $query = $this->connection->getQueryBuilder();
83
+            $query->insert('jobs')
84
+                ->values([
85
+                    'class' => $query->createNamedParameter($class),
86
+                    'argument' => $query->createNamedParameter($argument),
87
+                    'last_run' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT),
88
+                    'last_checked' => $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT),
89
+                ]);
90
+            $query->execute();
91
+        }
92
+    }
93
+
94
+    /**
95
+     * @param IJob|string $job
96
+     * @param mixed $argument
97
+     */
98
+    public function remove($job, $argument = null) {
99
+        if ($job instanceof IJob) {
100
+            $class = get_class($job);
101
+        } else {
102
+            $class = $job;
103
+        }
104
+
105
+        $query = $this->connection->getQueryBuilder();
106
+        $query->delete('jobs')
107
+            ->where($query->expr()->eq('class', $query->createNamedParameter($class)));
108
+        if (!is_null($argument)) {
109
+            $argument = json_encode($argument);
110
+            $query->andWhere($query->expr()->eq('argument', $query->createNamedParameter($argument)));
111
+        }
112
+        $query->execute();
113
+    }
114
+
115
+    /**
116
+     * @param int $id
117
+     */
118
+    protected function removeById($id) {
119
+        $query = $this->connection->getQueryBuilder();
120
+        $query->delete('jobs')
121
+            ->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
122
+        $query->execute();
123
+    }
124
+
125
+    /**
126
+     * check if a job is in the list
127
+     *
128
+     * @param IJob|string $job
129
+     * @param mixed $argument
130
+     * @return bool
131
+     */
132
+    public function has($job, $argument) {
133
+        if ($job instanceof IJob) {
134
+            $class = get_class($job);
135
+        } else {
136
+            $class = $job;
137
+        }
138
+        $argument = json_encode($argument);
139
+
140
+        $query = $this->connection->getQueryBuilder();
141
+        $query->select('id')
142
+            ->from('jobs')
143
+            ->where($query->expr()->eq('class', $query->createNamedParameter($class)))
144
+            ->andWhere($query->expr()->eq('argument', $query->createNamedParameter($argument)))
145
+            ->setMaxResults(1);
146
+
147
+        $result = $query->execute();
148
+        $row = $result->fetch();
149
+        $result->closeCursor();
150
+
151
+        return (bool) $row;
152
+    }
153
+
154
+    /**
155
+     * get all jobs in the list
156
+     *
157
+     * @return IJob[]
158
+     * @deprecated 9.0.0 - This method is dangerous since it can cause load and
159
+     * memory problems when creating too many instances.
160
+     */
161
+    public function getAll() {
162
+        $query = $this->connection->getQueryBuilder();
163
+        $query->select('*')
164
+            ->from('jobs');
165
+        $result = $query->execute();
166
+
167
+        $jobs = [];
168
+        while ($row = $result->fetch()) {
169
+            $job = $this->buildJob($row);
170
+            if ($job) {
171
+                $jobs[] = $job;
172
+            }
173
+        }
174
+        $result->closeCursor();
175
+
176
+        return $jobs;
177
+    }
178
+
179
+    /**
180
+     * get the next job in the list
181
+     *
182
+     * @return IJob|null
183
+     */
184
+    public function getNext() {
185
+        $query = $this->connection->getQueryBuilder();
186
+        $query->select('*')
187
+            ->from('jobs')
188
+            ->where($query->expr()->lte('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - 12 * 3600, IQueryBuilder::PARAM_INT)))
189
+            ->andWhere($query->expr()->lte('last_checked', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT)))
190
+            ->orderBy('last_checked', 'ASC')
191
+            ->setMaxResults(1);
192
+
193
+        $update = $this->connection->getQueryBuilder();
194
+        $update->update('jobs')
195
+            ->set('reserved_at', $update->createNamedParameter($this->timeFactory->getTime()))
196
+            ->set('last_checked', $update->createNamedParameter($this->timeFactory->getTime()))
197
+            ->where($update->expr()->eq('id', $update->createParameter('jobid')))
198
+            ->andWhere($update->expr()->eq('reserved_at', $update->createParameter('reserved_at')))
199
+            ->andWhere($update->expr()->eq('last_checked', $update->createParameter('last_checked')));
200
+
201
+        $result = $query->execute();
202
+        $row = $result->fetch();
203
+        $result->closeCursor();
204
+
205
+        if ($row) {
206
+            $update->setParameter('jobid', $row['id']);
207
+            $update->setParameter('reserved_at', $row['reserved_at']);
208
+            $update->setParameter('last_checked', $row['last_checked']);
209
+            $count = $update->execute();
210
+
211
+            if ($count === 0) {
212
+                // Background job already executed elsewhere, try again.
213
+                return $this->getNext();
214
+            }
215
+            $job = $this->buildJob($row);
216
+
217
+            if ($job === null) {
218
+                // set the last_checked to 12h in the future to not check failing jobs all over again
219
+                $reset = $this->connection->getQueryBuilder();
220
+                $reset->update('jobs')
221
+                    ->set('reserved_at', $reset->expr()->literal(0, IQueryBuilder::PARAM_INT))
222
+                    ->set('last_checked', $reset->createNamedParameter($this->timeFactory->getTime() + 12 * 3600, IQueryBuilder::PARAM_INT))
223
+                    ->where($reset->expr()->eq('id', $reset->createNamedParameter($row['id'], IQueryBuilder::PARAM_INT)));
224
+                $reset->execute();
225
+
226
+                // Background job from disabled app, try again.
227
+                return $this->getNext();
228
+            }
229
+
230
+            return $job;
231
+        } else {
232
+            return null;
233
+        }
234
+    }
235
+
236
+    /**
237
+     * @param int $id
238
+     * @return IJob|null
239
+     */
240
+    public function getById($id) {
241
+        $query = $this->connection->getQueryBuilder();
242
+        $query->select('*')
243
+            ->from('jobs')
244
+            ->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
245
+        $result = $query->execute();
246
+        $row = $result->fetch();
247
+        $result->closeCursor();
248
+
249
+        if ($row) {
250
+            return $this->buildJob($row);
251
+        } else {
252
+            return null;
253
+        }
254
+    }
255
+
256
+    /**
257
+     * get the job object from a row in the db
258
+     *
259
+     * @param array $row
260
+     * @return IJob|null
261
+     */
262
+    private function buildJob($row) {
263
+        try {
264
+            try {
265
+                // Try to load the job as a service
266
+                /** @var IJob $job */
267
+                $job = \OC::$server->query($row['class']);
268
+            } catch (QueryException $e) {
269
+                if (class_exists($row['class'])) {
270
+                    $class = $row['class'];
271
+                    $job = new $class();
272
+                } else {
273
+                    // job from disabled app or old version of an app, no need to do anything
274
+                    return null;
275
+                }
276
+            }
277
+
278
+            $job->setId((int) $row['id']);
279
+            $job->setLastRun((int) $row['last_run']);
280
+            $job->setArgument(json_decode($row['argument'], true));
281
+            return $job;
282
+        } catch (AutoloadNotAllowedException $e) {
283
+            // job is from a disabled app, ignore
284
+            return null;
285
+        }
286
+    }
287
+
288
+    /**
289
+     * set the job that was last ran
290
+     *
291
+     * @param IJob $job
292
+     */
293
+    public function setLastJob(IJob $job) {
294
+        $this->unlockJob($job);
295
+        $this->config->setAppValue('backgroundjob', 'lastjob', $job->getId());
296
+    }
297
+
298
+    /**
299
+     * Remove the reservation for a job
300
+     *
301
+     * @param IJob $job
302
+     */
303
+    public function unlockJob(IJob $job) {
304
+        $query = $this->connection->getQueryBuilder();
305
+        $query->update('jobs')
306
+            ->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT))
307
+            ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
308
+        $query->execute();
309
+    }
310
+
311
+    /**
312
+     * set the lastRun of $job to now
313
+     *
314
+     * @param IJob $job
315
+     */
316
+    public function setLastRun(IJob $job) {
317
+        $query = $this->connection->getQueryBuilder();
318
+        $query->update('jobs')
319
+            ->set('last_run', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
320
+            ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
321
+        $query->execute();
322
+    }
323
+
324
+    /**
325
+     * @param IJob $job
326
+     * @param $timeTaken
327
+     */
328
+    public function setExecutionTime(IJob $job, $timeTaken) {
329
+        $query = $this->connection->getQueryBuilder();
330
+        $query->update('jobs')
331
+            ->set('execution_duration', $query->createNamedParameter($timeTaken, IQueryBuilder::PARAM_INT))
332
+            ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
333
+        $query->execute();
334
+    }
335 335
 }
Please login to merge, or discard this patch.