Passed
Push — master ( 4d82a9...1089ad )
by Morris
15:08 queued 12s
created
apps/oauth2/lib/Db/AccessTokenMapper.php 1 patch
Indentation   +36 added lines, -36 removed lines patch added patch discarded remove patch
@@ -39,44 +39,44 @@
 block discarded – undo
39 39
  */
40 40
 class AccessTokenMapper extends QBMapper {
41 41
 
42
-	/**
43
-	 * @param IDBConnection $db
44
-	 */
45
-	public function __construct(IDBConnection $db) {
46
-		parent::__construct($db, 'oauth2_access_tokens');
47
-	}
42
+    /**
43
+     * @param IDBConnection $db
44
+     */
45
+    public function __construct(IDBConnection $db) {
46
+        parent::__construct($db, 'oauth2_access_tokens');
47
+    }
48 48
 
49
-	/**
50
-	 * @param string $code
51
-	 * @return AccessToken
52
-	 * @throws AccessTokenNotFoundException
53
-	 */
54
-	public function getByCode(string $code): AccessToken {
55
-		$qb = $this->db->getQueryBuilder();
56
-		$qb
57
-			->select('*')
58
-			->from($this->tableName)
59
-			->where($qb->expr()->eq('hashed_code', $qb->createNamedParameter(hash('sha512', $code))));
49
+    /**
50
+     * @param string $code
51
+     * @return AccessToken
52
+     * @throws AccessTokenNotFoundException
53
+     */
54
+    public function getByCode(string $code): AccessToken {
55
+        $qb = $this->db->getQueryBuilder();
56
+        $qb
57
+            ->select('*')
58
+            ->from($this->tableName)
59
+            ->where($qb->expr()->eq('hashed_code', $qb->createNamedParameter(hash('sha512', $code))));
60 60
 
61
-		try {
62
-			$token = $this->findEntity($qb);
63
-		} catch (IMapperException $e) {
64
-			throw new AccessTokenNotFoundException('Could not find access token', 0, $e);
65
-		}
61
+        try {
62
+            $token = $this->findEntity($qb);
63
+        } catch (IMapperException $e) {
64
+            throw new AccessTokenNotFoundException('Could not find access token', 0, $e);
65
+        }
66 66
 
67
-		return $token;
68
-	}
67
+        return $token;
68
+    }
69 69
 
70
-	/**
71
-	 * delete all access token from a given client
72
-	 *
73
-	 * @param int $id
74
-	 */
75
-	public function deleteByClientId(int $id) {
76
-		$qb = $this->db->getQueryBuilder();
77
-		$qb
78
-			->delete($this->tableName)
79
-			->where($qb->expr()->eq('client_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
80
-		$qb->executeStatement();
81
-	}
70
+    /**
71
+     * delete all access token from a given client
72
+     *
73
+     * @param int $id
74
+     */
75
+    public function deleteByClientId(int $id) {
76
+        $qb = $this->db->getQueryBuilder();
77
+        $qb
78
+            ->delete($this->tableName)
79
+            ->where($qb->expr()->eq('client_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
80
+        $qb->executeStatement();
81
+    }
82 82
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/CalDavBackend.php 2 patches
Indentation   +2764 added lines, -2764 removed lines patch added patch discarded remove patch
@@ -96,2768 +96,2768 @@
 block discarded – undo
96 96
  * @package OCA\DAV\CalDAV
97 97
  */
98 98
 class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
99
-	public const CALENDAR_TYPE_CALENDAR = 0;
100
-	public const CALENDAR_TYPE_SUBSCRIPTION = 1;
101
-
102
-	public const PERSONAL_CALENDAR_URI = 'personal';
103
-	public const PERSONAL_CALENDAR_NAME = 'Personal';
104
-
105
-	public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
106
-	public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
107
-
108
-	/**
109
-	 * We need to specify a max date, because we need to stop *somewhere*
110
-	 *
111
-	 * On 32 bit system the maximum for a signed integer is 2147483647, so
112
-	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
113
-	 * in 2038-01-19 to avoid problems when the date is converted
114
-	 * to a unix timestamp.
115
-	 */
116
-	public const MAX_DATE = '2038-01-01';
117
-
118
-	public const ACCESS_PUBLIC = 4;
119
-	public const CLASSIFICATION_PUBLIC = 0;
120
-	public const CLASSIFICATION_PRIVATE = 1;
121
-	public const CLASSIFICATION_CONFIDENTIAL = 2;
122
-
123
-	/**
124
-	 * List of CalDAV properties, and how they map to database field names
125
-	 * Add your own properties by simply adding on to this array.
126
-	 *
127
-	 * Note that only string-based properties are supported here.
128
-	 *
129
-	 * @var array
130
-	 */
131
-	public $propertyMap = [
132
-		'{DAV:}displayname' => 'displayname',
133
-		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
134
-		'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
135
-		'{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
136
-		'{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
137
-	];
138
-
139
-	/**
140
-	 * List of subscription properties, and how they map to database field names.
141
-	 *
142
-	 * @var array
143
-	 */
144
-	public $subscriptionPropertyMap = [
145
-		'{DAV:}displayname' => 'displayname',
146
-		'{http://apple.com/ns/ical/}refreshrate' => 'refreshrate',
147
-		'{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
148
-		'{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
149
-		'{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos',
150
-		'{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms',
151
-		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
152
-	];
153
-
154
-	/** @var array properties to index */
155
-	public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION',
156
-		'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT',
157
-		'ORGANIZER'];
158
-
159
-	/** @var array parameters to index */
160
-	public static $indexParameters = [
161
-		'ATTENDEE' => ['CN'],
162
-		'ORGANIZER' => ['CN'],
163
-	];
164
-
165
-	/**
166
-	 * @var string[] Map of uid => display name
167
-	 */
168
-	protected $userDisplayNames;
169
-
170
-	/** @var IDBConnection */
171
-	private $db;
172
-
173
-	/** @var Backend */
174
-	private $calendarSharingBackend;
175
-
176
-	/** @var Principal */
177
-	private $principalBackend;
178
-
179
-	/** @var IUserManager */
180
-	private $userManager;
181
-
182
-	/** @var ISecureRandom */
183
-	private $random;
184
-
185
-	/** @var ILogger */
186
-	private $logger;
187
-
188
-	/** @var IEventDispatcher */
189
-	private $dispatcher;
190
-
191
-	/** @var EventDispatcherInterface */
192
-	private $legacyDispatcher;
193
-
194
-	/** @var bool */
195
-	private $legacyEndpoint;
196
-
197
-	/** @var string */
198
-	private $dbObjectPropertiesTable = 'calendarobjects_props';
199
-
200
-	/**
201
-	 * CalDavBackend constructor.
202
-	 *
203
-	 * @param IDBConnection $db
204
-	 * @param Principal $principalBackend
205
-	 * @param IUserManager $userManager
206
-	 * @param IGroupManager $groupManager
207
-	 * @param ISecureRandom $random
208
-	 * @param ILogger $logger
209
-	 * @param IEventDispatcher $dispatcher
210
-	 * @param EventDispatcherInterface $legacyDispatcher
211
-	 * @param bool $legacyEndpoint
212
-	 */
213
-	public function __construct(IDBConnection $db,
214
-								Principal $principalBackend,
215
-								IUserManager $userManager,
216
-								IGroupManager $groupManager,
217
-								ISecureRandom $random,
218
-								ILogger $logger,
219
-								IEventDispatcher $dispatcher,
220
-								EventDispatcherInterface $legacyDispatcher,
221
-								bool $legacyEndpoint = false) {
222
-		$this->db = $db;
223
-		$this->principalBackend = $principalBackend;
224
-		$this->userManager = $userManager;
225
-		$this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
226
-		$this->random = $random;
227
-		$this->logger = $logger;
228
-		$this->dispatcher = $dispatcher;
229
-		$this->legacyDispatcher = $legacyDispatcher;
230
-		$this->legacyEndpoint = $legacyEndpoint;
231
-	}
232
-
233
-	/**
234
-	 * Return the number of calendars for a principal
235
-	 *
236
-	 * By default this excludes the automatically generated birthday calendar
237
-	 *
238
-	 * @param $principalUri
239
-	 * @param bool $excludeBirthday
240
-	 * @return int
241
-	 */
242
-	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
243
-		$principalUri = $this->convertPrincipal($principalUri, true);
244
-		$query = $this->db->getQueryBuilder();
245
-		$query->select($query->func()->count('*'))
246
-			->from('calendars');
247
-
248
-		if ($principalUri === '') {
249
-			$query->where($query->expr()->emptyString('principaluri'));
250
-		} else {
251
-			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
252
-		}
253
-
254
-		if ($excludeBirthday) {
255
-			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
256
-		}
257
-
258
-		$result = $query->executeQuery();
259
-		$column = (int)$result->fetchOne();
260
-		$result->closeCursor();
261
-		return $column;
262
-	}
263
-
264
-	/**
265
-	 * Returns a list of calendars for a principal.
266
-	 *
267
-	 * Every project is an array with the following keys:
268
-	 *  * id, a unique id that will be used by other functions to modify the
269
-	 *    calendar. This can be the same as the uri or a database key.
270
-	 *  * uri, which the basename of the uri with which the calendar is
271
-	 *    accessed.
272
-	 *  * principaluri. The owner of the calendar. Almost always the same as
273
-	 *    principalUri passed to this method.
274
-	 *
275
-	 * Furthermore it can contain webdav properties in clark notation. A very
276
-	 * common one is '{DAV:}displayname'.
277
-	 *
278
-	 * Many clients also require:
279
-	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
280
-	 * For this property, you can just return an instance of
281
-	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
282
-	 *
283
-	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
284
-	 * ACL will automatically be put in read-only mode.
285
-	 *
286
-	 * @param string $principalUri
287
-	 * @return array
288
-	 */
289
-	public function getCalendarsForUser($principalUri) {
290
-		$principalUriOriginal = $principalUri;
291
-		$principalUri = $this->convertPrincipal($principalUri, true);
292
-		$fields = array_values($this->propertyMap);
293
-		$fields[] = 'id';
294
-		$fields[] = 'uri';
295
-		$fields[] = 'synctoken';
296
-		$fields[] = 'components';
297
-		$fields[] = 'principaluri';
298
-		$fields[] = 'transparent';
299
-
300
-		// Making fields a comma-delimited list
301
-		$query = $this->db->getQueryBuilder();
302
-		$query->select($fields)
303
-			->from('calendars')
304
-			->orderBy('calendarorder', 'ASC');
305
-
306
-		if ($principalUri === '') {
307
-			$query->where($query->expr()->emptyString('principaluri'));
308
-		} else {
309
-			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
310
-		}
311
-
312
-		$result = $query->executeQuery();
313
-
314
-		$calendars = [];
315
-		while ($row = $result->fetch()) {
316
-			$row['principaluri'] = (string) $row['principaluri'];
317
-			$components = [];
318
-			if ($row['components']) {
319
-				$components = explode(',',$row['components']);
320
-			}
321
-
322
-			$calendar = [
323
-				'id' => $row['id'],
324
-				'uri' => $row['uri'],
325
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
326
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
327
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
328
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
329
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
330
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
331
-			];
332
-
333
-			foreach ($this->propertyMap as $xmlName => $dbName) {
334
-				$calendar[$xmlName] = $row[$dbName];
335
-			}
336
-
337
-			$this->addOwnerPrincipal($calendar);
338
-
339
-			if (!isset($calendars[$calendar['id']])) {
340
-				$calendars[$calendar['id']] = $calendar;
341
-			}
342
-		}
343
-		$result->closeCursor();
344
-
345
-		// query for shared calendars
346
-		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
347
-		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
348
-
349
-		$principals[] = $principalUri;
350
-
351
-		$fields = array_values($this->propertyMap);
352
-		$fields[] = 'a.id';
353
-		$fields[] = 'a.uri';
354
-		$fields[] = 'a.synctoken';
355
-		$fields[] = 'a.components';
356
-		$fields[] = 'a.principaluri';
357
-		$fields[] = 'a.transparent';
358
-		$fields[] = 's.access';
359
-		$query = $this->db->getQueryBuilder();
360
-		$query->select($fields)
361
-			->from('dav_shares', 's')
362
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
363
-			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
364
-			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
365
-			->setParameter('type', 'calendar')
366
-			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
367
-
368
-		$result = $query->executeQuery();
369
-
370
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
371
-		while ($row = $result->fetch()) {
372
-			$row['principaluri'] = (string) $row['principaluri'];
373
-			if ($row['principaluri'] === $principalUri) {
374
-				continue;
375
-			}
376
-
377
-			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
378
-			if (isset($calendars[$row['id']])) {
379
-				if ($readOnly) {
380
-					// New share can not have more permissions then the old one.
381
-					continue;
382
-				}
383
-				if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
384
-					$calendars[$row['id']][$readOnlyPropertyName] === 0) {
385
-					// Old share is already read-write, no more permissions can be gained
386
-					continue;
387
-				}
388
-			}
389
-
390
-			[, $name] = Uri\split($row['principaluri']);
391
-			$uri = $row['uri'] . '_shared_by_' . $name;
392
-			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
393
-			$components = [];
394
-			if ($row['components']) {
395
-				$components = explode(',',$row['components']);
396
-			}
397
-			$calendar = [
398
-				'id' => $row['id'],
399
-				'uri' => $uri,
400
-				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
401
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
402
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
403
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
404
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
405
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
406
-				$readOnlyPropertyName => $readOnly,
407
-			];
408
-
409
-			foreach ($this->propertyMap as $xmlName => $dbName) {
410
-				$calendar[$xmlName] = $row[$dbName];
411
-			}
412
-
413
-			$this->addOwnerPrincipal($calendar);
414
-
415
-			$calendars[$calendar['id']] = $calendar;
416
-		}
417
-		$result->closeCursor();
418
-
419
-		return array_values($calendars);
420
-	}
421
-
422
-	/**
423
-	 * @param $principalUri
424
-	 * @return array
425
-	 */
426
-	public function getUsersOwnCalendars($principalUri) {
427
-		$principalUri = $this->convertPrincipal($principalUri, true);
428
-		$fields = array_values($this->propertyMap);
429
-		$fields[] = 'id';
430
-		$fields[] = 'uri';
431
-		$fields[] = 'synctoken';
432
-		$fields[] = 'components';
433
-		$fields[] = 'principaluri';
434
-		$fields[] = 'transparent';
435
-		// Making fields a comma-delimited list
436
-		$query = $this->db->getQueryBuilder();
437
-		$query->select($fields)->from('calendars')
438
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
439
-			->orderBy('calendarorder', 'ASC');
440
-		$stmt = $query->executeQuery();
441
-		$calendars = [];
442
-		while ($row = $stmt->fetch()) {
443
-			$row['principaluri'] = (string) $row['principaluri'];
444
-			$components = [];
445
-			if ($row['components']) {
446
-				$components = explode(',',$row['components']);
447
-			}
448
-			$calendar = [
449
-				'id' => $row['id'],
450
-				'uri' => $row['uri'],
451
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
452
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
453
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
454
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
455
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
456
-			];
457
-			foreach ($this->propertyMap as $xmlName => $dbName) {
458
-				$calendar[$xmlName] = $row[$dbName];
459
-			}
460
-
461
-			$this->addOwnerPrincipal($calendar);
462
-
463
-			if (!isset($calendars[$calendar['id']])) {
464
-				$calendars[$calendar['id']] = $calendar;
465
-			}
466
-		}
467
-		$stmt->closeCursor();
468
-		return array_values($calendars);
469
-	}
470
-
471
-
472
-	/**
473
-	 * @param $uid
474
-	 * @return string
475
-	 */
476
-	private function getUserDisplayName($uid) {
477
-		if (!isset($this->userDisplayNames[$uid])) {
478
-			$user = $this->userManager->get($uid);
479
-
480
-			if ($user instanceof IUser) {
481
-				$this->userDisplayNames[$uid] = $user->getDisplayName();
482
-			} else {
483
-				$this->userDisplayNames[$uid] = $uid;
484
-			}
485
-		}
486
-
487
-		return $this->userDisplayNames[$uid];
488
-	}
489
-
490
-	/**
491
-	 * @return array
492
-	 */
493
-	public function getPublicCalendars() {
494
-		$fields = array_values($this->propertyMap);
495
-		$fields[] = 'a.id';
496
-		$fields[] = 'a.uri';
497
-		$fields[] = 'a.synctoken';
498
-		$fields[] = 'a.components';
499
-		$fields[] = 'a.principaluri';
500
-		$fields[] = 'a.transparent';
501
-		$fields[] = 's.access';
502
-		$fields[] = 's.publicuri';
503
-		$calendars = [];
504
-		$query = $this->db->getQueryBuilder();
505
-		$result = $query->select($fields)
506
-			->from('dav_shares', 's')
507
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
508
-			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
509
-			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
510
-			->executeQuery();
511
-
512
-		while ($row = $result->fetch()) {
513
-			$row['principaluri'] = (string) $row['principaluri'];
514
-			[, $name] = Uri\split($row['principaluri']);
515
-			$row['displayname'] = $row['displayname'] . "($name)";
516
-			$components = [];
517
-			if ($row['components']) {
518
-				$components = explode(',',$row['components']);
519
-			}
520
-			$calendar = [
521
-				'id' => $row['id'],
522
-				'uri' => $row['publicuri'],
523
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
524
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
525
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
526
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
527
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
528
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
529
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
530
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
531
-			];
532
-
533
-			foreach ($this->propertyMap as $xmlName => $dbName) {
534
-				$calendar[$xmlName] = $row[$dbName];
535
-			}
536
-
537
-			$this->addOwnerPrincipal($calendar);
538
-
539
-			if (!isset($calendars[$calendar['id']])) {
540
-				$calendars[$calendar['id']] = $calendar;
541
-			}
542
-		}
543
-		$result->closeCursor();
544
-
545
-		return array_values($calendars);
546
-	}
547
-
548
-	/**
549
-	 * @param string $uri
550
-	 * @return array
551
-	 * @throws NotFound
552
-	 */
553
-	public function getPublicCalendar($uri) {
554
-		$fields = array_values($this->propertyMap);
555
-		$fields[] = 'a.id';
556
-		$fields[] = 'a.uri';
557
-		$fields[] = 'a.synctoken';
558
-		$fields[] = 'a.components';
559
-		$fields[] = 'a.principaluri';
560
-		$fields[] = 'a.transparent';
561
-		$fields[] = 's.access';
562
-		$fields[] = 's.publicuri';
563
-		$query = $this->db->getQueryBuilder();
564
-		$result = $query->select($fields)
565
-			->from('dav_shares', 's')
566
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
567
-			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
568
-			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
569
-			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
570
-			->executeQuery();
571
-
572
-		$row = $result->fetch();
573
-
574
-		$result->closeCursor();
575
-
576
-		if ($row === false) {
577
-			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
578
-		}
579
-
580
-		$row['principaluri'] = (string) $row['principaluri'];
581
-		[, $name] = Uri\split($row['principaluri']);
582
-		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
583
-		$components = [];
584
-		if ($row['components']) {
585
-			$components = explode(',',$row['components']);
586
-		}
587
-		$calendar = [
588
-			'id' => $row['id'],
589
-			'uri' => $row['publicuri'],
590
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
591
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
592
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
593
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
594
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
595
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
596
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
597
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
598
-		];
599
-
600
-		foreach ($this->propertyMap as $xmlName => $dbName) {
601
-			$calendar[$xmlName] = $row[$dbName];
602
-		}
603
-
604
-		$this->addOwnerPrincipal($calendar);
605
-
606
-		return $calendar;
607
-	}
608
-
609
-	/**
610
-	 * @param string $principal
611
-	 * @param string $uri
612
-	 * @return array|null
613
-	 */
614
-	public function getCalendarByUri($principal, $uri) {
615
-		$fields = array_values($this->propertyMap);
616
-		$fields[] = 'id';
617
-		$fields[] = 'uri';
618
-		$fields[] = 'synctoken';
619
-		$fields[] = 'components';
620
-		$fields[] = 'principaluri';
621
-		$fields[] = 'transparent';
622
-
623
-		// Making fields a comma-delimited list
624
-		$query = $this->db->getQueryBuilder();
625
-		$query->select($fields)->from('calendars')
626
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
627
-			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
628
-			->setMaxResults(1);
629
-		$stmt = $query->executeQuery();
630
-
631
-		$row = $stmt->fetch();
632
-		$stmt->closeCursor();
633
-		if ($row === false) {
634
-			return null;
635
-		}
636
-
637
-		$row['principaluri'] = (string) $row['principaluri'];
638
-		$components = [];
639
-		if ($row['components']) {
640
-			$components = explode(',',$row['components']);
641
-		}
642
-
643
-		$calendar = [
644
-			'id' => $row['id'],
645
-			'uri' => $row['uri'],
646
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
647
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
648
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
649
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
651
-		];
652
-
653
-		foreach ($this->propertyMap as $xmlName => $dbName) {
654
-			$calendar[$xmlName] = $row[$dbName];
655
-		}
656
-
657
-		$this->addOwnerPrincipal($calendar);
658
-
659
-		return $calendar;
660
-	}
661
-
662
-	/**
663
-	 * @param $calendarId
664
-	 * @return array|null
665
-	 */
666
-	public function getCalendarById($calendarId) {
667
-		$fields = array_values($this->propertyMap);
668
-		$fields[] = 'id';
669
-		$fields[] = 'uri';
670
-		$fields[] = 'synctoken';
671
-		$fields[] = 'components';
672
-		$fields[] = 'principaluri';
673
-		$fields[] = 'transparent';
674
-
675
-		// Making fields a comma-delimited list
676
-		$query = $this->db->getQueryBuilder();
677
-		$query->select($fields)->from('calendars')
678
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
679
-			->setMaxResults(1);
680
-		$stmt = $query->executeQuery();
681
-
682
-		$row = $stmt->fetch();
683
-		$stmt->closeCursor();
684
-		if ($row === false) {
685
-			return null;
686
-		}
687
-
688
-		$row['principaluri'] = (string) $row['principaluri'];
689
-		$components = [];
690
-		if ($row['components']) {
691
-			$components = explode(',',$row['components']);
692
-		}
693
-
694
-		$calendar = [
695
-			'id' => $row['id'],
696
-			'uri' => $row['uri'],
697
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
698
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
699
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
700
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
701
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
702
-		];
703
-
704
-		foreach ($this->propertyMap as $xmlName => $dbName) {
705
-			$calendar[$xmlName] = $row[$dbName];
706
-		}
707
-
708
-		$this->addOwnerPrincipal($calendar);
709
-
710
-		return $calendar;
711
-	}
712
-
713
-	/**
714
-	 * @param $subscriptionId
715
-	 */
716
-	public function getSubscriptionById($subscriptionId) {
717
-		$fields = array_values($this->subscriptionPropertyMap);
718
-		$fields[] = 'id';
719
-		$fields[] = 'uri';
720
-		$fields[] = 'source';
721
-		$fields[] = 'synctoken';
722
-		$fields[] = 'principaluri';
723
-		$fields[] = 'lastmodified';
724
-
725
-		$query = $this->db->getQueryBuilder();
726
-		$query->select($fields)
727
-			->from('calendarsubscriptions')
728
-			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
729
-			->orderBy('calendarorder', 'asc');
730
-		$stmt = $query->executeQuery();
731
-
732
-		$row = $stmt->fetch();
733
-		$stmt->closeCursor();
734
-		if ($row === false) {
735
-			return null;
736
-		}
737
-
738
-		$row['principaluri'] = (string) $row['principaluri'];
739
-		$subscription = [
740
-			'id' => $row['id'],
741
-			'uri' => $row['uri'],
742
-			'principaluri' => $row['principaluri'],
743
-			'source' => $row['source'],
744
-			'lastmodified' => $row['lastmodified'],
745
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
746
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
747
-		];
748
-
749
-		foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
750
-			if (!is_null($row[$dbName])) {
751
-				$subscription[$xmlName] = $row[$dbName];
752
-			}
753
-		}
754
-
755
-		return $subscription;
756
-	}
757
-
758
-	/**
759
-	 * Creates a new calendar for a principal.
760
-	 *
761
-	 * If the creation was a success, an id must be returned that can be used to reference
762
-	 * this calendar in other methods, such as updateCalendar.
763
-	 *
764
-	 * @param string $principalUri
765
-	 * @param string $calendarUri
766
-	 * @param array $properties
767
-	 * @return int
768
-	 */
769
-	public function createCalendar($principalUri, $calendarUri, array $properties) {
770
-		$values = [
771
-			'principaluri' => $this->convertPrincipal($principalUri, true),
772
-			'uri' => $calendarUri,
773
-			'synctoken' => 1,
774
-			'transparent' => 0,
775
-			'components' => 'VEVENT,VTODO',
776
-			'displayname' => $calendarUri
777
-		];
778
-
779
-		// Default value
780
-		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
781
-		if (isset($properties[$sccs])) {
782
-			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
783
-				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
784
-			}
785
-			$values['components'] = implode(',',$properties[$sccs]->getValue());
786
-		} elseif (isset($properties['components'])) {
787
-			// Allow to provide components internally without having
788
-			// to create a SupportedCalendarComponentSet object
789
-			$values['components'] = $properties['components'];
790
-		}
791
-
792
-		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
793
-		if (isset($properties[$transp])) {
794
-			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
795
-		}
796
-
797
-		foreach ($this->propertyMap as $xmlName => $dbName) {
798
-			if (isset($properties[$xmlName])) {
799
-				$values[$dbName] = $properties[$xmlName];
800
-			}
801
-		}
802
-
803
-		$query = $this->db->getQueryBuilder();
804
-		$query->insert('calendars');
805
-		foreach ($values as $column => $value) {
806
-			$query->setValue($column, $query->createNamedParameter($value));
807
-		}
808
-		$query->executeStatement();
809
-		$calendarId = $query->getLastInsertId();
810
-
811
-		$calendarData = $this->getCalendarById($calendarId);
812
-		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
813
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
814
-			'\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
815
-			[
816
-				'calendarId' => $calendarId,
817
-				'calendarData' => $calendarData,
818
-			]));
819
-
820
-		return $calendarId;
821
-	}
822
-
823
-	/**
824
-	 * Updates properties for a calendar.
825
-	 *
826
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
827
-	 * To do the actual updates, you must tell this object which properties
828
-	 * you're going to process with the handle() method.
829
-	 *
830
-	 * Calling the handle method is like telling the PropPatch object "I
831
-	 * promise I can handle updating this property".
832
-	 *
833
-	 * Read the PropPatch documentation for more info and examples.
834
-	 *
835
-	 * @param mixed $calendarId
836
-	 * @param PropPatch $propPatch
837
-	 * @return void
838
-	 */
839
-	public function updateCalendar($calendarId, PropPatch $propPatch) {
840
-		$supportedProperties = array_keys($this->propertyMap);
841
-		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
842
-
843
-		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
844
-			$newValues = [];
845
-			foreach ($mutations as $propertyName => $propertyValue) {
846
-				switch ($propertyName) {
847
-					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
848
-						$fieldName = 'transparent';
849
-						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
850
-						break;
851
-					default:
852
-						$fieldName = $this->propertyMap[$propertyName];
853
-						$newValues[$fieldName] = $propertyValue;
854
-						break;
855
-				}
856
-			}
857
-			$query = $this->db->getQueryBuilder();
858
-			$query->update('calendars');
859
-			foreach ($newValues as $fieldName => $value) {
860
-				$query->set($fieldName, $query->createNamedParameter($value));
861
-			}
862
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
863
-			$query->executeStatement();
864
-
865
-			$this->addChange($calendarId, "", 2);
866
-
867
-			$calendarData = $this->getCalendarById($calendarId);
868
-			$shares = $this->getShares($calendarId);
869
-			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
870
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
871
-				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
872
-				[
873
-					'calendarId' => $calendarId,
874
-					'calendarData' => $calendarData,
875
-					'shares' => $shares,
876
-					'propertyMutations' => $mutations,
877
-				]));
878
-
879
-			return true;
880
-		});
881
-	}
882
-
883
-	/**
884
-	 * Delete a calendar and all it's objects
885
-	 *
886
-	 * @param mixed $calendarId
887
-	 * @return void
888
-	 */
889
-	public function deleteCalendar($calendarId) {
890
-		$calendarData = $this->getCalendarById($calendarId);
891
-		$shares = $this->getShares($calendarId);
892
-
893
-		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?');
894
-		$stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
895
-
896
-		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
897
-		$stmt->execute([$calendarId]);
898
-
899
-		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?');
900
-		$stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
901
-
902
-		$this->calendarSharingBackend->deleteAllShares($calendarId);
903
-
904
-		$query = $this->db->getQueryBuilder();
905
-		$query->delete($this->dbObjectPropertiesTable)
906
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
907
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
908
-			->executeStatement();
909
-
910
-		// Only dispatch if we actually deleted anything
911
-		if ($calendarData) {
912
-			$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
913
-		}
914
-	}
915
-
916
-	/**
917
-	 * Delete all of an user's shares
918
-	 *
919
-	 * @param string $principaluri
920
-	 * @return void
921
-	 */
922
-	public function deleteAllSharesByUser($principaluri) {
923
-		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
924
-	}
925
-
926
-	/**
927
-	 * Returns all calendar objects within a calendar.
928
-	 *
929
-	 * Every item contains an array with the following keys:
930
-	 *   * calendardata - The iCalendar-compatible calendar data
931
-	 *   * uri - a unique key which will be used to construct the uri. This can
932
-	 *     be any arbitrary string, but making sure it ends with '.ics' is a
933
-	 *     good idea. This is only the basename, or filename, not the full
934
-	 *     path.
935
-	 *   * lastmodified - a timestamp of the last modification time
936
-	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
937
-	 *   '"abcdef"')
938
-	 *   * size - The size of the calendar objects, in bytes.
939
-	 *   * component - optional, a string containing the type of object, such
940
-	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
941
-	 *     the Content-Type header.
942
-	 *
943
-	 * Note that the etag is optional, but it's highly encouraged to return for
944
-	 * speed reasons.
945
-	 *
946
-	 * The calendardata is also optional. If it's not returned
947
-	 * 'getCalendarObject' will be called later, which *is* expected to return
948
-	 * calendardata.
949
-	 *
950
-	 * If neither etag or size are specified, the calendardata will be
951
-	 * used/fetched to determine these numbers. If both are specified the
952
-	 * amount of times this is needed is reduced by a great degree.
953
-	 *
954
-	 * @param mixed $calendarId
955
-	 * @param int $calendarType
956
-	 * @return array
957
-	 */
958
-	public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
959
-		$query = $this->db->getQueryBuilder();
960
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
961
-			->from('calendarobjects')
962
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
963
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
964
-		$stmt = $query->executeQuery();
965
-
966
-		$result = [];
967
-		foreach ($stmt->fetchAll() as $row) {
968
-			$result[] = [
969
-				'id' => $row['id'],
970
-				'uri' => $row['uri'],
971
-				'lastmodified' => $row['lastmodified'],
972
-				'etag' => '"' . $row['etag'] . '"',
973
-				'calendarid' => $row['calendarid'],
974
-				'size' => (int)$row['size'],
975
-				'component' => strtolower($row['componenttype']),
976
-				'classification' => (int)$row['classification']
977
-			];
978
-		}
979
-		$stmt->closeCursor();
980
-
981
-		return $result;
982
-	}
983
-
984
-	/**
985
-	 * Returns information from a single calendar object, based on it's object
986
-	 * uri.
987
-	 *
988
-	 * The object uri is only the basename, or filename and not a full path.
989
-	 *
990
-	 * The returned array must have the same keys as getCalendarObjects. The
991
-	 * 'calendardata' object is required here though, while it's not required
992
-	 * for getCalendarObjects.
993
-	 *
994
-	 * This method must return null if the object did not exist.
995
-	 *
996
-	 * @param mixed $calendarId
997
-	 * @param string $objectUri
998
-	 * @param int $calendarType
999
-	 * @return array|null
1000
-	 */
1001
-	public function getCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1002
-		$query = $this->db->getQueryBuilder();
1003
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1004
-			->from('calendarobjects')
1005
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1006
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1007
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1008
-		$stmt = $query->executeQuery();
1009
-		$row = $stmt->fetch();
1010
-		$stmt->closeCursor();
1011
-
1012
-		if (!$row) {
1013
-			return null;
1014
-		}
1015
-
1016
-		return [
1017
-			'id' => $row['id'],
1018
-			'uri' => $row['uri'],
1019
-			'lastmodified' => $row['lastmodified'],
1020
-			'etag' => '"' . $row['etag'] . '"',
1021
-			'calendarid' => $row['calendarid'],
1022
-			'size' => (int)$row['size'],
1023
-			'calendardata' => $this->readBlob($row['calendardata']),
1024
-			'component' => strtolower($row['componenttype']),
1025
-			'classification' => (int)$row['classification']
1026
-		];
1027
-	}
1028
-
1029
-	/**
1030
-	 * Returns a list of calendar objects.
1031
-	 *
1032
-	 * This method should work identical to getCalendarObject, but instead
1033
-	 * return all the calendar objects in the list as an array.
1034
-	 *
1035
-	 * If the backend supports this, it may allow for some speed-ups.
1036
-	 *
1037
-	 * @param mixed $calendarId
1038
-	 * @param string[] $uris
1039
-	 * @param int $calendarType
1040
-	 * @return array
1041
-	 */
1042
-	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1043
-		if (empty($uris)) {
1044
-			return [];
1045
-		}
1046
-
1047
-		$chunks = array_chunk($uris, 100);
1048
-		$objects = [];
1049
-
1050
-		$query = $this->db->getQueryBuilder();
1051
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1052
-			->from('calendarobjects')
1053
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1054
-			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1055
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1056
-
1057
-		foreach ($chunks as $uris) {
1058
-			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1059
-			$result = $query->executeQuery();
1060
-
1061
-			while ($row = $result->fetch()) {
1062
-				$objects[] = [
1063
-					'id' => $row['id'],
1064
-					'uri' => $row['uri'],
1065
-					'lastmodified' => $row['lastmodified'],
1066
-					'etag' => '"' . $row['etag'] . '"',
1067
-					'calendarid' => $row['calendarid'],
1068
-					'size' => (int)$row['size'],
1069
-					'calendardata' => $this->readBlob($row['calendardata']),
1070
-					'component' => strtolower($row['componenttype']),
1071
-					'classification' => (int)$row['classification']
1072
-				];
1073
-			}
1074
-			$result->closeCursor();
1075
-		}
1076
-
1077
-		return $objects;
1078
-	}
1079
-
1080
-	/**
1081
-	 * Creates a new calendar object.
1082
-	 *
1083
-	 * The object uri is only the basename, or filename and not a full path.
1084
-	 *
1085
-	 * It is possible return an etag from this function, which will be used in
1086
-	 * the response to this PUT request. Note that the ETag must be surrounded
1087
-	 * by double-quotes.
1088
-	 *
1089
-	 * However, you should only really return this ETag if you don't mangle the
1090
-	 * calendar-data. If the result of a subsequent GET to this object is not
1091
-	 * the exact same as this request body, you should omit the ETag.
1092
-	 *
1093
-	 * @param mixed $calendarId
1094
-	 * @param string $objectUri
1095
-	 * @param string $calendarData
1096
-	 * @param int $calendarType
1097
-	 * @return string
1098
-	 */
1099
-	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1100
-		$extraData = $this->getDenormalizedData($calendarData);
1101
-
1102
-		$q = $this->db->getQueryBuilder();
1103
-		$q->select($q->func()->count('*'))
1104
-			->from('calendarobjects')
1105
-			->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId)))
1106
-			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid'])))
1107
-			->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType)));
1108
-
1109
-		$result = $q->executeQuery();
1110
-		$count = (int) $result->fetchOne();
1111
-		$result->closeCursor();
1112
-
1113
-		if ($count !== 0) {
1114
-			throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.');
1115
-		}
1116
-
1117
-		$query = $this->db->getQueryBuilder();
1118
-		$query->insert('calendarobjects')
1119
-			->values([
1120
-				'calendarid' => $query->createNamedParameter($calendarId),
1121
-				'uri' => $query->createNamedParameter($objectUri),
1122
-				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1123
-				'lastmodified' => $query->createNamedParameter(time()),
1124
-				'etag' => $query->createNamedParameter($extraData['etag']),
1125
-				'size' => $query->createNamedParameter($extraData['size']),
1126
-				'componenttype' => $query->createNamedParameter($extraData['componentType']),
1127
-				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1128
-				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1129
-				'classification' => $query->createNamedParameter($extraData['classification']),
1130
-				'uid' => $query->createNamedParameter($extraData['uid']),
1131
-				'calendartype' => $query->createNamedParameter($calendarType),
1132
-			])
1133
-			->executeStatement();
1134
-
1135
-		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1136
-		$this->addChange($calendarId, $objectUri, 1, $calendarType);
1137
-
1138
-		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1139
-		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1140
-			$calendarRow = $this->getCalendarById($calendarId);
1141
-			$shares = $this->getShares($calendarId);
1142
-
1143
-			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1144
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1145
-				'\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1146
-				[
1147
-					'calendarId' => $calendarId,
1148
-					'calendarData' => $calendarRow,
1149
-					'shares' => $shares,
1150
-					'objectData' => $objectRow,
1151
-				]
1152
-			));
1153
-		} else {
1154
-			$subscriptionRow = $this->getSubscriptionById($calendarId);
1155
-
1156
-			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1157
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1158
-				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1159
-				[
1160
-					'subscriptionId' => $calendarId,
1161
-					'calendarData' => $subscriptionRow,
1162
-					'shares' => [],
1163
-					'objectData' => $objectRow,
1164
-				]
1165
-			));
1166
-		}
1167
-
1168
-		return '"' . $extraData['etag'] . '"';
1169
-	}
1170
-
1171
-	/**
1172
-	 * Updates an existing calendarobject, based on it's uri.
1173
-	 *
1174
-	 * The object uri is only the basename, or filename and not a full path.
1175
-	 *
1176
-	 * It is possible return an etag from this function, which will be used in
1177
-	 * the response to this PUT request. Note that the ETag must be surrounded
1178
-	 * by double-quotes.
1179
-	 *
1180
-	 * However, you should only really return this ETag if you don't mangle the
1181
-	 * calendar-data. If the result of a subsequent GET to this object is not
1182
-	 * the exact same as this request body, you should omit the ETag.
1183
-	 *
1184
-	 * @param mixed $calendarId
1185
-	 * @param string $objectUri
1186
-	 * @param string $calendarData
1187
-	 * @param int $calendarType
1188
-	 * @return string
1189
-	 */
1190
-	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1191
-		$extraData = $this->getDenormalizedData($calendarData);
1192
-		$query = $this->db->getQueryBuilder();
1193
-		$query->update('calendarobjects')
1194
-				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1195
-				->set('lastmodified', $query->createNamedParameter(time()))
1196
-				->set('etag', $query->createNamedParameter($extraData['etag']))
1197
-				->set('size', $query->createNamedParameter($extraData['size']))
1198
-				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1199
-				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1200
-				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1201
-				->set('classification', $query->createNamedParameter($extraData['classification']))
1202
-				->set('uid', $query->createNamedParameter($extraData['uid']))
1203
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1204
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1205
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1206
-			->executeStatement();
1207
-
1208
-		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1209
-		$this->addChange($calendarId, $objectUri, 2, $calendarType);
1210
-
1211
-		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1212
-		if (is_array($objectRow)) {
1213
-			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1214
-				$calendarRow = $this->getCalendarById($calendarId);
1215
-				$shares = $this->getShares($calendarId);
1216
-
1217
-				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1218
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1219
-					'\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1220
-					[
1221
-						'calendarId' => $calendarId,
1222
-						'calendarData' => $calendarRow,
1223
-						'shares' => $shares,
1224
-						'objectData' => $objectRow,
1225
-					]
1226
-				));
1227
-			} else {
1228
-				$subscriptionRow = $this->getSubscriptionById($calendarId);
1229
-
1230
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1231
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1232
-					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1233
-					[
1234
-						'subscriptionId' => $calendarId,
1235
-						'calendarData' => $subscriptionRow,
1236
-						'shares' => [],
1237
-						'objectData' => $objectRow,
1238
-					]
1239
-				));
1240
-			}
1241
-		}
1242
-
1243
-		return '"' . $extraData['etag'] . '"';
1244
-	}
1245
-
1246
-	/**
1247
-	 * @param int $calendarObjectId
1248
-	 * @param int $classification
1249
-	 */
1250
-	public function setClassification($calendarObjectId, $classification) {
1251
-		if (!in_array($classification, [
1252
-			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1253
-		])) {
1254
-			throw new \InvalidArgumentException();
1255
-		}
1256
-		$query = $this->db->getQueryBuilder();
1257
-		$query->update('calendarobjects')
1258
-			->set('classification', $query->createNamedParameter($classification))
1259
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1260
-			->executeStatement();
1261
-	}
1262
-
1263
-	/**
1264
-	 * Deletes an existing calendar object.
1265
-	 *
1266
-	 * The object uri is only the basename, or filename and not a full path.
1267
-	 *
1268
-	 * @param mixed $calendarId
1269
-	 * @param string $objectUri
1270
-	 * @param int $calendarType
1271
-	 * @return void
1272
-	 */
1273
-	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1274
-		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1275
-		if (is_array($data)) {
1276
-			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1277
-				$calendarRow = $this->getCalendarById($calendarId);
1278
-				$shares = $this->getShares($calendarId);
1279
-
1280
-				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1281
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1282
-					'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1283
-					[
1284
-						'calendarId' => $calendarId,
1285
-						'calendarData' => $calendarRow,
1286
-						'shares' => $shares,
1287
-						'objectData' => $data,
1288
-					]
1289
-				));
1290
-			} else {
1291
-				$subscriptionRow = $this->getSubscriptionById($calendarId);
1292
-
1293
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1294
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1295
-					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1296
-					[
1297
-						'subscriptionId' => $calendarId,
1298
-						'calendarData' => $subscriptionRow,
1299
-						'shares' => [],
1300
-						'objectData' => $data,
1301
-					]
1302
-				));
1303
-			}
1304
-		}
1305
-
1306
-		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1307
-		$stmt->execute([$calendarId, $objectUri, $calendarType]);
1308
-
1309
-		if (is_array($data)) {
1310
-			$this->purgeProperties($calendarId, $data['id'], $calendarType);
1311
-		}
1312
-
1313
-		$this->addChange($calendarId, $objectUri, 3, $calendarType);
1314
-	}
1315
-
1316
-	/**
1317
-	 * Performs a calendar-query on the contents of this calendar.
1318
-	 *
1319
-	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1320
-	 * calendar-query it is possible for a client to request a specific set of
1321
-	 * object, based on contents of iCalendar properties, date-ranges and
1322
-	 * iCalendar component types (VTODO, VEVENT).
1323
-	 *
1324
-	 * This method should just return a list of (relative) urls that match this
1325
-	 * query.
1326
-	 *
1327
-	 * The list of filters are specified as an array. The exact array is
1328
-	 * documented by Sabre\CalDAV\CalendarQueryParser.
1329
-	 *
1330
-	 * Note that it is extremely likely that getCalendarObject for every path
1331
-	 * returned from this method will be called almost immediately after. You
1332
-	 * may want to anticipate this to speed up these requests.
1333
-	 *
1334
-	 * This method provides a default implementation, which parses *all* the
1335
-	 * iCalendar objects in the specified calendar.
1336
-	 *
1337
-	 * This default may well be good enough for personal use, and calendars
1338
-	 * that aren't very large. But if you anticipate high usage, big calendars
1339
-	 * or high loads, you are strongly advised to optimize certain paths.
1340
-	 *
1341
-	 * The best way to do so is override this method and to optimize
1342
-	 * specifically for 'common filters'.
1343
-	 *
1344
-	 * Requests that are extremely common are:
1345
-	 *   * requests for just VEVENTS
1346
-	 *   * requests for just VTODO
1347
-	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1348
-	 *
1349
-	 * ..and combinations of these requests. It may not be worth it to try to
1350
-	 * handle every possible situation and just rely on the (relatively
1351
-	 * easy to use) CalendarQueryValidator to handle the rest.
1352
-	 *
1353
-	 * Note that especially time-range-filters may be difficult to parse. A
1354
-	 * time-range filter specified on a VEVENT must for instance also handle
1355
-	 * recurrence rules correctly.
1356
-	 * A good example of how to interprete all these filters can also simply
1357
-	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1358
-	 * as possible, so it gives you a good idea on what type of stuff you need
1359
-	 * to think of.
1360
-	 *
1361
-	 * @param mixed $calendarId
1362
-	 * @param array $filters
1363
-	 * @param int $calendarType
1364
-	 * @return array
1365
-	 */
1366
-	public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1367
-		$componentType = null;
1368
-		$requirePostFilter = true;
1369
-		$timeRange = null;
1370
-
1371
-		// if no filters were specified, we don't need to filter after a query
1372
-		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1373
-			$requirePostFilter = false;
1374
-		}
1375
-
1376
-		// Figuring out if there's a component filter
1377
-		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1378
-			$componentType = $filters['comp-filters'][0]['name'];
1379
-
1380
-			// Checking if we need post-filters
1381
-			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1382
-				$requirePostFilter = false;
1383
-			}
1384
-			// There was a time-range filter
1385
-			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
1386
-				$timeRange = $filters['comp-filters'][0]['time-range'];
1387
-
1388
-				// If start time OR the end time is not specified, we can do a
1389
-				// 100% accurate mysql query.
1390
-				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1391
-					$requirePostFilter = false;
1392
-				}
1393
-			}
1394
-		}
1395
-		$columns = ['uri'];
1396
-		if ($requirePostFilter) {
1397
-			$columns = ['uri', 'calendardata'];
1398
-		}
1399
-		$query = $this->db->getQueryBuilder();
1400
-		$query->select($columns)
1401
-			->from('calendarobjects')
1402
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1403
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1404
-
1405
-		if ($componentType) {
1406
-			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1407
-		}
1408
-
1409
-		if ($timeRange && $timeRange['start']) {
1410
-			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1411
-		}
1412
-		if ($timeRange && $timeRange['end']) {
1413
-			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1414
-		}
1415
-
1416
-		$stmt = $query->executeQuery();
1417
-
1418
-		$result = [];
1419
-		while ($row = $stmt->fetch()) {
1420
-			if ($requirePostFilter) {
1421
-				// validateFilterForObject will parse the calendar data
1422
-				// catch parsing errors
1423
-				try {
1424
-					$matches = $this->validateFilterForObject($row, $filters);
1425
-				} catch (ParseException $ex) {
1426
-					$this->logger->logException($ex, [
1427
-						'app' => 'dav',
1428
-						'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1429
-					]);
1430
-					continue;
1431
-				} catch (InvalidDataException $ex) {
1432
-					$this->logger->logException($ex, [
1433
-						'app' => 'dav',
1434
-						'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1435
-					]);
1436
-					continue;
1437
-				}
1438
-
1439
-				if (!$matches) {
1440
-					continue;
1441
-				}
1442
-			}
1443
-			$result[] = $row['uri'];
1444
-		}
1445
-
1446
-		return $result;
1447
-	}
1448
-
1449
-	/**
1450
-	 * custom Nextcloud search extension for CalDAV
1451
-	 *
1452
-	 * TODO - this should optionally cover cached calendar objects as well
1453
-	 *
1454
-	 * @param string $principalUri
1455
-	 * @param array $filters
1456
-	 * @param integer|null $limit
1457
-	 * @param integer|null $offset
1458
-	 * @return array
1459
-	 */
1460
-	public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1461
-		$calendars = $this->getCalendarsForUser($principalUri);
1462
-		$ownCalendars = [];
1463
-		$sharedCalendars = [];
1464
-
1465
-		$uriMapper = [];
1466
-
1467
-		foreach ($calendars as $calendar) {
1468
-			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1469
-				$ownCalendars[] = $calendar['id'];
1470
-			} else {
1471
-				$sharedCalendars[] = $calendar['id'];
1472
-			}
1473
-			$uriMapper[$calendar['id']] = $calendar['uri'];
1474
-		}
1475
-		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1476
-			return [];
1477
-		}
1478
-
1479
-		$query = $this->db->getQueryBuilder();
1480
-		// Calendar id expressions
1481
-		$calendarExpressions = [];
1482
-		foreach ($ownCalendars as $id) {
1483
-			$calendarExpressions[] = $query->expr()->andX(
1484
-				$query->expr()->eq('c.calendarid',
1485
-					$query->createNamedParameter($id)),
1486
-				$query->expr()->eq('c.calendartype',
1487
-						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1488
-		}
1489
-		foreach ($sharedCalendars as $id) {
1490
-			$calendarExpressions[] = $query->expr()->andX(
1491
-				$query->expr()->eq('c.calendarid',
1492
-					$query->createNamedParameter($id)),
1493
-				$query->expr()->eq('c.classification',
1494
-					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1495
-				$query->expr()->eq('c.calendartype',
1496
-					$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1497
-		}
1498
-
1499
-		if (count($calendarExpressions) === 1) {
1500
-			$calExpr = $calendarExpressions[0];
1501
-		} else {
1502
-			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1503
-		}
1504
-
1505
-		// Component expressions
1506
-		$compExpressions = [];
1507
-		foreach ($filters['comps'] as $comp) {
1508
-			$compExpressions[] = $query->expr()
1509
-				->eq('c.componenttype', $query->createNamedParameter($comp));
1510
-		}
1511
-
1512
-		if (count($compExpressions) === 1) {
1513
-			$compExpr = $compExpressions[0];
1514
-		} else {
1515
-			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1516
-		}
1517
-
1518
-		if (!isset($filters['props'])) {
1519
-			$filters['props'] = [];
1520
-		}
1521
-		if (!isset($filters['params'])) {
1522
-			$filters['params'] = [];
1523
-		}
1524
-
1525
-		$propParamExpressions = [];
1526
-		foreach ($filters['props'] as $prop) {
1527
-			$propParamExpressions[] = $query->expr()->andX(
1528
-				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1529
-				$query->expr()->isNull('i.parameter')
1530
-			);
1531
-		}
1532
-		foreach ($filters['params'] as $param) {
1533
-			$propParamExpressions[] = $query->expr()->andX(
1534
-				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1535
-				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1536
-			);
1537
-		}
1538
-
1539
-		if (count($propParamExpressions) === 1) {
1540
-			$propParamExpr = $propParamExpressions[0];
1541
-		} else {
1542
-			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1543
-		}
1544
-
1545
-		$query->select(['c.calendarid', 'c.uri'])
1546
-			->from($this->dbObjectPropertiesTable, 'i')
1547
-			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1548
-			->where($calExpr)
1549
-			->andWhere($compExpr)
1550
-			->andWhere($propParamExpr)
1551
-			->andWhere($query->expr()->iLike('i.value',
1552
-				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
1553
-
1554
-		if ($offset) {
1555
-			$query->setFirstResult($offset);
1556
-		}
1557
-		if ($limit) {
1558
-			$query->setMaxResults($limit);
1559
-		}
1560
-
1561
-		$stmt = $query->executeQuery();
1562
-
1563
-		$result = [];
1564
-		while ($row = $stmt->fetch()) {
1565
-			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1566
-			if (!in_array($path, $result)) {
1567
-				$result[] = $path;
1568
-			}
1569
-		}
1570
-
1571
-		return $result;
1572
-	}
1573
-
1574
-	/**
1575
-	 * used for Nextcloud's calendar API
1576
-	 *
1577
-	 * @param array $calendarInfo
1578
-	 * @param string $pattern
1579
-	 * @param array $searchProperties
1580
-	 * @param array $options
1581
-	 * @param integer|null $limit
1582
-	 * @param integer|null $offset
1583
-	 *
1584
-	 * @return array
1585
-	 */
1586
-	public function search(array $calendarInfo, $pattern, array $searchProperties,
1587
-						   array $options, $limit, $offset) {
1588
-		$outerQuery = $this->db->getQueryBuilder();
1589
-		$innerQuery = $this->db->getQueryBuilder();
1590
-
1591
-		$innerQuery->selectDistinct('op.objectid')
1592
-			->from($this->dbObjectPropertiesTable, 'op')
1593
-			->andWhere($innerQuery->expr()->eq('op.calendarid',
1594
-				$outerQuery->createNamedParameter($calendarInfo['id'])))
1595
-			->andWhere($innerQuery->expr()->eq('op.calendartype',
1596
-				$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1597
-
1598
-		// only return public items for shared calendars for now
1599
-		if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1600
-			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1601
-				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1602
-		}
1603
-
1604
-		$or = $innerQuery->expr()->orX();
1605
-		foreach ($searchProperties as $searchProperty) {
1606
-			$or->add($innerQuery->expr()->eq('op.name',
1607
-				$outerQuery->createNamedParameter($searchProperty)));
1608
-		}
1609
-		$innerQuery->andWhere($or);
1610
-
1611
-		if ($pattern !== '') {
1612
-			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1613
-				$outerQuery->createNamedParameter('%' .
1614
-					$this->db->escapeLikeParameter($pattern) . '%')));
1615
-		}
1616
-
1617
-		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1618
-			->from('calendarobjects', 'c');
1619
-
1620
-		if (isset($options['timerange'])) {
1621
-			if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) {
1622
-				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1623
-					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1624
-			}
1625
-			if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTime) {
1626
-				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1627
-					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1628
-			}
1629
-		}
1630
-
1631
-		if (isset($options['types'])) {
1632
-			$or = $outerQuery->expr()->orX();
1633
-			foreach ($options['types'] as $type) {
1634
-				$or->add($outerQuery->expr()->eq('componenttype',
1635
-					$outerQuery->createNamedParameter($type)));
1636
-			}
1637
-			$outerQuery->andWhere($or);
1638
-		}
1639
-
1640
-		$outerQuery->andWhere($outerQuery->expr()->in('c.id',
1641
-			$outerQuery->createFunction($innerQuery->getSQL())));
1642
-
1643
-		if ($offset) {
1644
-			$outerQuery->setFirstResult($offset);
1645
-		}
1646
-		if ($limit) {
1647
-			$outerQuery->setMaxResults($limit);
1648
-		}
1649
-
1650
-		$result = $outerQuery->executeQuery();
1651
-		$calendarObjects = $result->fetchAll();
1652
-
1653
-		return array_map(function ($o) {
1654
-			$calendarData = Reader::read($o['calendardata']);
1655
-			$comps = $calendarData->getComponents();
1656
-			$objects = [];
1657
-			$timezones = [];
1658
-			foreach ($comps as $comp) {
1659
-				if ($comp instanceof VTimeZone) {
1660
-					$timezones[] = $comp;
1661
-				} else {
1662
-					$objects[] = $comp;
1663
-				}
1664
-			}
1665
-
1666
-			return [
1667
-				'id' => $o['id'],
1668
-				'type' => $o['componenttype'],
1669
-				'uid' => $o['uid'],
1670
-				'uri' => $o['uri'],
1671
-				'objects' => array_map(function ($c) {
1672
-					return $this->transformSearchData($c);
1673
-				}, $objects),
1674
-				'timezones' => array_map(function ($c) {
1675
-					return $this->transformSearchData($c);
1676
-				}, $timezones),
1677
-			];
1678
-		}, $calendarObjects);
1679
-	}
1680
-
1681
-	/**
1682
-	 * @param Component $comp
1683
-	 * @return array
1684
-	 */
1685
-	private function transformSearchData(Component $comp) {
1686
-		$data = [];
1687
-		/** @var Component[] $subComponents */
1688
-		$subComponents = $comp->getComponents();
1689
-		/** @var Property[] $properties */
1690
-		$properties = array_filter($comp->children(), function ($c) {
1691
-			return $c instanceof Property;
1692
-		});
1693
-		$validationRules = $comp->getValidationRules();
1694
-
1695
-		foreach ($subComponents as $subComponent) {
1696
-			$name = $subComponent->name;
1697
-			if (!isset($data[$name])) {
1698
-				$data[$name] = [];
1699
-			}
1700
-			$data[$name][] = $this->transformSearchData($subComponent);
1701
-		}
1702
-
1703
-		foreach ($properties as $property) {
1704
-			$name = $property->name;
1705
-			if (!isset($validationRules[$name])) {
1706
-				$validationRules[$name] = '*';
1707
-			}
1708
-
1709
-			$rule = $validationRules[$property->name];
1710
-			if ($rule === '+' || $rule === '*') { // multiple
1711
-				if (!isset($data[$name])) {
1712
-					$data[$name] = [];
1713
-				}
1714
-
1715
-				$data[$name][] = $this->transformSearchProperty($property);
1716
-			} else { // once
1717
-				$data[$name] = $this->transformSearchProperty($property);
1718
-			}
1719
-		}
1720
-
1721
-		return $data;
1722
-	}
1723
-
1724
-	/**
1725
-	 * @param Property $prop
1726
-	 * @return array
1727
-	 */
1728
-	private function transformSearchProperty(Property $prop) {
1729
-		// No need to check Date, as it extends DateTime
1730
-		if ($prop instanceof Property\ICalendar\DateTime) {
1731
-			$value = $prop->getDateTime();
1732
-		} else {
1733
-			$value = $prop->getValue();
1734
-		}
1735
-
1736
-		return [
1737
-			$value,
1738
-			$prop->parameters()
1739
-		];
1740
-	}
1741
-
1742
-	/**
1743
-	 * @param string $principalUri
1744
-	 * @param string $pattern
1745
-	 * @param array $componentTypes
1746
-	 * @param array $searchProperties
1747
-	 * @param array $searchParameters
1748
-	 * @param array $options
1749
-	 * @return array
1750
-	 */
1751
-	public function searchPrincipalUri(string $principalUri,
1752
-									   string $pattern,
1753
-									   array $componentTypes,
1754
-									   array $searchProperties,
1755
-									   array $searchParameters,
1756
-									   array $options = []): array {
1757
-		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1758
-
1759
-		$calendarObjectIdQuery = $this->db->getQueryBuilder();
1760
-		$calendarOr = $calendarObjectIdQuery->expr()->orX();
1761
-		$searchOr = $calendarObjectIdQuery->expr()->orX();
1762
-
1763
-		// Fetch calendars and subscription
1764
-		$calendars = $this->getCalendarsForUser($principalUri);
1765
-		$subscriptions = $this->getSubscriptionsForUser($principalUri);
1766
-		foreach ($calendars as $calendar) {
1767
-			$calendarAnd = $calendarObjectIdQuery->expr()->andX();
1768
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
1769
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1770
-
1771
-			// If it's shared, limit search to public events
1772
-			if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
1773
-				&& $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
1774
-				$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1775
-			}
1776
-
1777
-			$calendarOr->add($calendarAnd);
1778
-		}
1779
-		foreach ($subscriptions as $subscription) {
1780
-			$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
1781
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
1782
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
1783
-
1784
-			// If it's shared, limit search to public events
1785
-			if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
1786
-				&& $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
1787
-				$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1788
-			}
1789
-
1790
-			$calendarOr->add($subscriptionAnd);
1791
-		}
1792
-
1793
-		foreach ($searchProperties as $property) {
1794
-			$propertyAnd = $calendarObjectIdQuery->expr()->andX();
1795
-			$propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
1796
-			$propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
1797
-
1798
-			$searchOr->add($propertyAnd);
1799
-		}
1800
-		foreach ($searchParameters as $property => $parameter) {
1801
-			$parameterAnd = $calendarObjectIdQuery->expr()->andX();
1802
-			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
1803
-			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
1804
-
1805
-			$searchOr->add($parameterAnd);
1806
-		}
1807
-
1808
-		if ($calendarOr->count() === 0) {
1809
-			return [];
1810
-		}
1811
-		if ($searchOr->count() === 0) {
1812
-			return [];
1813
-		}
1814
-
1815
-		$calendarObjectIdQuery->selectDistinct('cob.objectid')
1816
-			->from($this->dbObjectPropertiesTable, 'cob')
1817
-			->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
1818
-			->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
1819
-			->andWhere($calendarOr)
1820
-			->andWhere($searchOr);
1821
-
1822
-		if ('' !== $pattern) {
1823
-			if (!$escapePattern) {
1824
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
1825
-			} else {
1826
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1827
-			}
1828
-		}
1829
-
1830
-		if (isset($options['limit'])) {
1831
-			$calendarObjectIdQuery->setMaxResults($options['limit']);
1832
-		}
1833
-		if (isset($options['offset'])) {
1834
-			$calendarObjectIdQuery->setFirstResult($options['offset']);
1835
-		}
1836
-
1837
-		$result = $calendarObjectIdQuery->executeQuery();
1838
-		$matches = $result->fetchAll();
1839
-		$result->closeCursor();
1840
-		$matches = array_map(static function (array $match):int {
1841
-			return (int) $match['objectid'];
1842
-		}, $matches);
1843
-
1844
-		$query = $this->db->getQueryBuilder();
1845
-		$query->select('calendardata', 'uri', 'calendarid', 'calendartype')
1846
-			->from('calendarobjects')
1847
-			->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1848
-
1849
-		$result = $query->executeQuery();
1850
-		$calendarObjects = $result->fetchAll();
1851
-		$result->closeCursor();
1852
-
1853
-		return array_map(function (array $array): array {
1854
-			$array['calendarid'] = (int)$array['calendarid'];
1855
-			$array['calendartype'] = (int)$array['calendartype'];
1856
-			$array['calendardata'] = $this->readBlob($array['calendardata']);
1857
-
1858
-			return $array;
1859
-		}, $calendarObjects);
1860
-	}
1861
-
1862
-	/**
1863
-	 * Searches through all of a users calendars and calendar objects to find
1864
-	 * an object with a specific UID.
1865
-	 *
1866
-	 * This method should return the path to this object, relative to the
1867
-	 * calendar home, so this path usually only contains two parts:
1868
-	 *
1869
-	 * calendarpath/objectpath.ics
1870
-	 *
1871
-	 * If the uid is not found, return null.
1872
-	 *
1873
-	 * This method should only consider * objects that the principal owns, so
1874
-	 * any calendars owned by other principals that also appear in this
1875
-	 * collection should be ignored.
1876
-	 *
1877
-	 * @param string $principalUri
1878
-	 * @param string $uid
1879
-	 * @return string|null
1880
-	 */
1881
-	public function getCalendarObjectByUID($principalUri, $uid) {
1882
-		$query = $this->db->getQueryBuilder();
1883
-		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1884
-			->from('calendarobjects', 'co')
1885
-			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1886
-			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1887
-			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1888
-
1889
-		$stmt = $query->executeQuery();
1890
-		$row = $stmt->fetch();
1891
-		$stmt->closeCursor();
1892
-		if ($row) {
1893
-			return $row['calendaruri'] . '/' . $row['objecturi'];
1894
-		}
1895
-
1896
-		return null;
1897
-	}
1898
-
1899
-	/**
1900
-	 * The getChanges method returns all the changes that have happened, since
1901
-	 * the specified syncToken in the specified calendar.
1902
-	 *
1903
-	 * This function should return an array, such as the following:
1904
-	 *
1905
-	 * [
1906
-	 *   'syncToken' => 'The current synctoken',
1907
-	 *   'added'   => [
1908
-	 *      'new.txt',
1909
-	 *   ],
1910
-	 *   'modified'   => [
1911
-	 *      'modified.txt',
1912
-	 *   ],
1913
-	 *   'deleted' => [
1914
-	 *      'foo.php.bak',
1915
-	 *      'old.txt'
1916
-	 *   ]
1917
-	 * );
1918
-	 *
1919
-	 * The returned syncToken property should reflect the *current* syncToken
1920
-	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1921
-	 * property This is * needed here too, to ensure the operation is atomic.
1922
-	 *
1923
-	 * If the $syncToken argument is specified as null, this is an initial
1924
-	 * sync, and all members should be reported.
1925
-	 *
1926
-	 * The modified property is an array of nodenames that have changed since
1927
-	 * the last token.
1928
-	 *
1929
-	 * The deleted property is an array with nodenames, that have been deleted
1930
-	 * from collection.
1931
-	 *
1932
-	 * The $syncLevel argument is basically the 'depth' of the report. If it's
1933
-	 * 1, you only have to report changes that happened only directly in
1934
-	 * immediate descendants. If it's 2, it should also include changes from
1935
-	 * the nodes below the child collections. (grandchildren)
1936
-	 *
1937
-	 * The $limit argument allows a client to specify how many results should
1938
-	 * be returned at most. If the limit is not specified, it should be treated
1939
-	 * as infinite.
1940
-	 *
1941
-	 * If the limit (infinite or not) is higher than you're willing to return,
1942
-	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1943
-	 *
1944
-	 * If the syncToken is expired (due to data cleanup) or unknown, you must
1945
-	 * return null.
1946
-	 *
1947
-	 * The limit is 'suggestive'. You are free to ignore it.
1948
-	 *
1949
-	 * @param string $calendarId
1950
-	 * @param string $syncToken
1951
-	 * @param int $syncLevel
1952
-	 * @param int|null $limit
1953
-	 * @param int $calendarType
1954
-	 * @return array
1955
-	 */
1956
-	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1957
-		// Current synctoken
1958
-		$qb = $this->db->getQueryBuilder();
1959
-		$qb->select('synctoken')
1960
-			->from('calendars')
1961
-			->where(
1962
-				$qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
1963
-			);
1964
-		$stmt = $qb->executeQuery();
1965
-		$currentToken = $stmt->fetchOne();
1966
-
1967
-		if ($currentToken === false) {
1968
-			return null;
1969
-		}
1970
-
1971
-		$result = [
1972
-			'syncToken' => $currentToken,
1973
-			'added' => [],
1974
-			'modified' => [],
1975
-			'deleted' => [],
1976
-		];
1977
-
1978
-		if ($syncToken) {
1979
-			$qb = $this->db->getQueryBuilder();
1980
-
1981
-			$qb->select('uri', 'operation')
1982
-				->from('calendarchanges')
1983
-				->where(
1984
-					$qb->expr()->andX(
1985
-						$qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
1986
-						$qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
1987
-						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1988
-						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
1989
-					)
1990
-				)->orderBy('synctoken');
1991
-			if (is_int($limit) && $limit > 0) {
1992
-				$qb->setMaxResults($limit);
1993
-			}
1994
-
1995
-			// Fetching all changes
1996
-			$stmt = $qb->executeQuery();
1997
-			$changes = [];
1998
-
1999
-			// This loop ensures that any duplicates are overwritten, only the
2000
-			// last change on a node is relevant.
2001
-			while ($row = $stmt->fetch()) {
2002
-				$changes[$row['uri']] = $row['operation'];
2003
-			}
2004
-			$stmt->closeCursor();
2005
-
2006
-			foreach ($changes as $uri => $operation) {
2007
-				switch ($operation) {
2008
-					case 1:
2009
-						$result['added'][] = $uri;
2010
-						break;
2011
-					case 2:
2012
-						$result['modified'][] = $uri;
2013
-						break;
2014
-					case 3:
2015
-						$result['deleted'][] = $uri;
2016
-						break;
2017
-				}
2018
-			}
2019
-		} else {
2020
-			// No synctoken supplied, this is the initial sync.
2021
-			$qb = $this->db->getQueryBuilder();
2022
-			$qb->select('uri')
2023
-				->from('calendarobjects')
2024
-				->where(
2025
-					$qb->expr()->andX(
2026
-						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2027
-						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2028
-					)
2029
-				);
2030
-			$stmt = $qb->executeQuery();
2031
-			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2032
-			$stmt->closeCursor();
2033
-		}
2034
-		return $result;
2035
-	}
2036
-
2037
-	/**
2038
-	 * Returns a list of subscriptions for a principal.
2039
-	 *
2040
-	 * Every subscription is an array with the following keys:
2041
-	 *  * id, a unique id that will be used by other functions to modify the
2042
-	 *    subscription. This can be the same as the uri or a database key.
2043
-	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2044
-	 *  * principaluri. The owner of the subscription. Almost always the same as
2045
-	 *    principalUri passed to this method.
2046
-	 *
2047
-	 * Furthermore, all the subscription info must be returned too:
2048
-	 *
2049
-	 * 1. {DAV:}displayname
2050
-	 * 2. {http://apple.com/ns/ical/}refreshrate
2051
-	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2052
-	 *    should not be stripped).
2053
-	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2054
-	 *    should not be stripped).
2055
-	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2056
-	 *    attachments should not be stripped).
2057
-	 * 6. {http://calendarserver.org/ns/}source (Must be a
2058
-	 *     Sabre\DAV\Property\Href).
2059
-	 * 7. {http://apple.com/ns/ical/}calendar-color
2060
-	 * 8. {http://apple.com/ns/ical/}calendar-order
2061
-	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2062
-	 *    (should just be an instance of
2063
-	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2064
-	 *    default components).
2065
-	 *
2066
-	 * @param string $principalUri
2067
-	 * @return array
2068
-	 */
2069
-	public function getSubscriptionsForUser($principalUri) {
2070
-		$fields = array_values($this->subscriptionPropertyMap);
2071
-		$fields[] = 'id';
2072
-		$fields[] = 'uri';
2073
-		$fields[] = 'source';
2074
-		$fields[] = 'principaluri';
2075
-		$fields[] = 'lastmodified';
2076
-		$fields[] = 'synctoken';
2077
-
2078
-		$query = $this->db->getQueryBuilder();
2079
-		$query->select($fields)
2080
-			->from('calendarsubscriptions')
2081
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2082
-			->orderBy('calendarorder', 'asc');
2083
-		$stmt = $query->executeQuery();
2084
-
2085
-		$subscriptions = [];
2086
-		while ($row = $stmt->fetch()) {
2087
-			$subscription = [
2088
-				'id' => $row['id'],
2089
-				'uri' => $row['uri'],
2090
-				'principaluri' => $row['principaluri'],
2091
-				'source' => $row['source'],
2092
-				'lastmodified' => $row['lastmodified'],
2093
-
2094
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2095
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2096
-			];
2097
-
2098
-			foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
2099
-				if (!is_null($row[$dbName])) {
2100
-					$subscription[$xmlName] = $row[$dbName];
2101
-				}
2102
-			}
2103
-
2104
-			$subscriptions[] = $subscription;
2105
-		}
2106
-
2107
-		return $subscriptions;
2108
-	}
2109
-
2110
-	/**
2111
-	 * Creates a new subscription for a principal.
2112
-	 *
2113
-	 * If the creation was a success, an id must be returned that can be used to reference
2114
-	 * this subscription in other methods, such as updateSubscription.
2115
-	 *
2116
-	 * @param string $principalUri
2117
-	 * @param string $uri
2118
-	 * @param array $properties
2119
-	 * @return mixed
2120
-	 */
2121
-	public function createSubscription($principalUri, $uri, array $properties) {
2122
-		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2123
-			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2124
-		}
2125
-
2126
-		$values = [
2127
-			'principaluri' => $principalUri,
2128
-			'uri' => $uri,
2129
-			'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2130
-			'lastmodified' => time(),
2131
-		];
2132
-
2133
-		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2134
-
2135
-		foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
2136
-			if (array_key_exists($xmlName, $properties)) {
2137
-				$values[$dbName] = $properties[$xmlName];
2138
-				if (in_array($dbName, $propertiesBoolean)) {
2139
-					$values[$dbName] = true;
2140
-				}
2141
-			}
2142
-		}
2143
-
2144
-		$valuesToInsert = [];
2145
-
2146
-		$query = $this->db->getQueryBuilder();
2147
-
2148
-		foreach (array_keys($values) as $name) {
2149
-			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2150
-		}
2151
-
2152
-		$query->insert('calendarsubscriptions')
2153
-			->values($valuesToInsert)
2154
-			->executeStatement();
2155
-
2156
-		$subscriptionId = $query->getLastInsertId();
2157
-
2158
-		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2159
-		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2160
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
2161
-			'\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2162
-			[
2163
-				'subscriptionId' => $subscriptionId,
2164
-				'subscriptionData' => $subscriptionRow,
2165
-			]));
2166
-
2167
-		return $subscriptionId;
2168
-	}
2169
-
2170
-	/**
2171
-	 * Updates a subscription
2172
-	 *
2173
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2174
-	 * To do the actual updates, you must tell this object which properties
2175
-	 * you're going to process with the handle() method.
2176
-	 *
2177
-	 * Calling the handle method is like telling the PropPatch object "I
2178
-	 * promise I can handle updating this property".
2179
-	 *
2180
-	 * Read the PropPatch documentation for more info and examples.
2181
-	 *
2182
-	 * @param mixed $subscriptionId
2183
-	 * @param PropPatch $propPatch
2184
-	 * @return void
2185
-	 */
2186
-	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2187
-		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2188
-		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2189
-
2190
-		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2191
-			$newValues = [];
2192
-
2193
-			foreach ($mutations as $propertyName => $propertyValue) {
2194
-				if ($propertyName === '{http://calendarserver.org/ns/}source') {
2195
-					$newValues['source'] = $propertyValue->getHref();
2196
-				} else {
2197
-					$fieldName = $this->subscriptionPropertyMap[$propertyName];
2198
-					$newValues[$fieldName] = $propertyValue;
2199
-				}
2200
-			}
2201
-
2202
-			$query = $this->db->getQueryBuilder();
2203
-			$query->update('calendarsubscriptions')
2204
-				->set('lastmodified', $query->createNamedParameter(time()));
2205
-			foreach ($newValues as $fieldName => $value) {
2206
-				$query->set($fieldName, $query->createNamedParameter($value));
2207
-			}
2208
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2209
-				->executeStatement();
2210
-
2211
-			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2212
-			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2213
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2214
-				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2215
-				[
2216
-					'subscriptionId' => $subscriptionId,
2217
-					'subscriptionData' => $subscriptionRow,
2218
-					'propertyMutations' => $mutations,
2219
-				]));
2220
-
2221
-			return true;
2222
-		});
2223
-	}
2224
-
2225
-	/**
2226
-	 * Deletes a subscription.
2227
-	 *
2228
-	 * @param mixed $subscriptionId
2229
-	 * @return void
2230
-	 */
2231
-	public function deleteSubscription($subscriptionId) {
2232
-		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2233
-
2234
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
2235
-			'\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2236
-			[
2237
-				'subscriptionId' => $subscriptionId,
2238
-				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2239
-			]));
2240
-
2241
-		$query = $this->db->getQueryBuilder();
2242
-		$query->delete('calendarsubscriptions')
2243
-			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2244
-			->executeStatement();
2245
-
2246
-		$query = $this->db->getQueryBuilder();
2247
-		$query->delete('calendarobjects')
2248
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2249
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2250
-			->executeStatement();
2251
-
2252
-		$query->delete('calendarchanges')
2253
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2254
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2255
-			->executeStatement();
2256
-
2257
-		$query->delete($this->dbObjectPropertiesTable)
2258
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2259
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2260
-			->executeStatement();
2261
-
2262
-		if ($subscriptionRow) {
2263
-			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2264
-		}
2265
-	}
2266
-
2267
-	/**
2268
-	 * Returns a single scheduling object for the inbox collection.
2269
-	 *
2270
-	 * The returned array should contain the following elements:
2271
-	 *   * uri - A unique basename for the object. This will be used to
2272
-	 *           construct a full uri.
2273
-	 *   * calendardata - The iCalendar object
2274
-	 *   * lastmodified - The last modification date. Can be an int for a unix
2275
-	 *                    timestamp, or a PHP DateTime object.
2276
-	 *   * etag - A unique token that must change if the object changed.
2277
-	 *   * size - The size of the object, in bytes.
2278
-	 *
2279
-	 * @param string $principalUri
2280
-	 * @param string $objectUri
2281
-	 * @return array
2282
-	 */
2283
-	public function getSchedulingObject($principalUri, $objectUri) {
2284
-		$query = $this->db->getQueryBuilder();
2285
-		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2286
-			->from('schedulingobjects')
2287
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2288
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2289
-			->executeQuery();
2290
-
2291
-		$row = $stmt->fetch();
2292
-
2293
-		if (!$row) {
2294
-			return null;
2295
-		}
2296
-
2297
-		return [
2298
-			'uri' => $row['uri'],
2299
-			'calendardata' => $row['calendardata'],
2300
-			'lastmodified' => $row['lastmodified'],
2301
-			'etag' => '"' . $row['etag'] . '"',
2302
-			'size' => (int)$row['size'],
2303
-		];
2304
-	}
2305
-
2306
-	/**
2307
-	 * Returns all scheduling objects for the inbox collection.
2308
-	 *
2309
-	 * These objects should be returned as an array. Every item in the array
2310
-	 * should follow the same structure as returned from getSchedulingObject.
2311
-	 *
2312
-	 * The main difference is that 'calendardata' is optional.
2313
-	 *
2314
-	 * @param string $principalUri
2315
-	 * @return array
2316
-	 */
2317
-	public function getSchedulingObjects($principalUri) {
2318
-		$query = $this->db->getQueryBuilder();
2319
-		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2320
-				->from('schedulingobjects')
2321
-				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2322
-				->executeQuery();
2323
-
2324
-		$result = [];
2325
-		foreach ($stmt->fetchAll() as $row) {
2326
-			$result[] = [
2327
-				'calendardata' => $row['calendardata'],
2328
-				'uri' => $row['uri'],
2329
-				'lastmodified' => $row['lastmodified'],
2330
-				'etag' => '"' . $row['etag'] . '"',
2331
-				'size' => (int)$row['size'],
2332
-			];
2333
-		}
2334
-
2335
-		return $result;
2336
-	}
2337
-
2338
-	/**
2339
-	 * Deletes a scheduling object from the inbox collection.
2340
-	 *
2341
-	 * @param string $principalUri
2342
-	 * @param string $objectUri
2343
-	 * @return void
2344
-	 */
2345
-	public function deleteSchedulingObject($principalUri, $objectUri) {
2346
-		$query = $this->db->getQueryBuilder();
2347
-		$query->delete('schedulingobjects')
2348
-				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2349
-				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2350
-				->executeStatement();
2351
-	}
2352
-
2353
-	/**
2354
-	 * Creates a new scheduling object. This should land in a users' inbox.
2355
-	 *
2356
-	 * @param string $principalUri
2357
-	 * @param string $objectUri
2358
-	 * @param string $objectData
2359
-	 * @return void
2360
-	 */
2361
-	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2362
-		$query = $this->db->getQueryBuilder();
2363
-		$query->insert('schedulingobjects')
2364
-			->values([
2365
-				'principaluri' => $query->createNamedParameter($principalUri),
2366
-				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2367
-				'uri' => $query->createNamedParameter($objectUri),
2368
-				'lastmodified' => $query->createNamedParameter(time()),
2369
-				'etag' => $query->createNamedParameter(md5($objectData)),
2370
-				'size' => $query->createNamedParameter(strlen($objectData))
2371
-			])
2372
-			->executeStatement();
2373
-	}
2374
-
2375
-	/**
2376
-	 * Adds a change record to the calendarchanges table.
2377
-	 *
2378
-	 * @param mixed $calendarId
2379
-	 * @param string $objectUri
2380
-	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2381
-	 * @param int $calendarType
2382
-	 * @return void
2383
-	 */
2384
-	protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2385
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2386
-
2387
-		$query = $this->db->getQueryBuilder();
2388
-		$query->select('synctoken')
2389
-			->from($table)
2390
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2391
-		$result = $query->executeQuery();
2392
-		$syncToken = (int)$result->fetchOne();
2393
-		$result->closeCursor();
2394
-
2395
-		$query = $this->db->getQueryBuilder();
2396
-		$query->insert('calendarchanges')
2397
-			->values([
2398
-				'uri' => $query->createNamedParameter($objectUri),
2399
-				'synctoken' => $query->createNamedParameter($syncToken),
2400
-				'calendarid' => $query->createNamedParameter($calendarId),
2401
-				'operation' => $query->createNamedParameter($operation),
2402
-				'calendartype' => $query->createNamedParameter($calendarType),
2403
-			])
2404
-			->executeStatement();
2405
-
2406
-		$stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2407
-		$stmt->execute([
2408
-			$calendarId
2409
-		]);
2410
-	}
2411
-
2412
-	/**
2413
-	 * Parses some information from calendar objects, used for optimized
2414
-	 * calendar-queries.
2415
-	 *
2416
-	 * Returns an array with the following keys:
2417
-	 *   * etag - An md5 checksum of the object without the quotes.
2418
-	 *   * size - Size of the object in bytes
2419
-	 *   * componentType - VEVENT, VTODO or VJOURNAL
2420
-	 *   * firstOccurence
2421
-	 *   * lastOccurence
2422
-	 *   * uid - value of the UID property
2423
-	 *
2424
-	 * @param string $calendarData
2425
-	 * @return array
2426
-	 */
2427
-	public function getDenormalizedData($calendarData) {
2428
-		$vObject = Reader::read($calendarData);
2429
-		$vEvents = [];
2430
-		$componentType = null;
2431
-		$component = null;
2432
-		$firstOccurrence = null;
2433
-		$lastOccurrence = null;
2434
-		$uid = null;
2435
-		$classification = self::CLASSIFICATION_PUBLIC;
2436
-		$hasDTSTART = false;
2437
-		foreach ($vObject->getComponents() as $component) {
2438
-			if ($component->name !== 'VTIMEZONE') {
2439
-				// Finding all VEVENTs, and track them
2440
-				if ($component->name === 'VEVENT') {
2441
-					array_push($vEvents, $component);
2442
-					if ($component->DTSTART) {
2443
-						$hasDTSTART = true;
2444
-					}
2445
-				}
2446
-				// Track first component type and uid
2447
-				if ($uid === null) {
2448
-					$componentType = $component->name;
2449
-					$uid = (string)$component->UID;
2450
-				}
2451
-			}
2452
-		}
2453
-		if (!$componentType) {
2454
-			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2455
-		}
2456
-
2457
-		if ($hasDTSTART) {
2458
-			$component = $vEvents[0];
2459
-
2460
-			// Finding the last occurrence is a bit harder
2461
-			if (!isset($component->RRULE) && count($vEvents) === 1) {
2462
-				$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2463
-				if (isset($component->DTEND)) {
2464
-					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2465
-				} elseif (isset($component->DURATION)) {
2466
-					$endDate = clone $component->DTSTART->getDateTime();
2467
-					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2468
-					$lastOccurrence = $endDate->getTimeStamp();
2469
-				} elseif (!$component->DTSTART->hasTime()) {
2470
-					$endDate = clone $component->DTSTART->getDateTime();
2471
-					$endDate->modify('+1 day');
2472
-					$lastOccurrence = $endDate->getTimeStamp();
2473
-				} else {
2474
-					$lastOccurrence = $firstOccurrence;
2475
-				}
2476
-			} else {
2477
-				$it = new EventIterator($vEvents);
2478
-				$maxDate = new DateTime(self::MAX_DATE);
2479
-				$firstOccurrence = $it->getDtStart()->getTimestamp();
2480
-				if ($it->isInfinite()) {
2481
-					$lastOccurrence = $maxDate->getTimestamp();
2482
-				} else {
2483
-					$end = $it->getDtEnd();
2484
-					while ($it->valid() && $end < $maxDate) {
2485
-						$end = $it->getDtEnd();
2486
-						$it->next();
2487
-					}
2488
-					$lastOccurrence = $end->getTimestamp();
2489
-				}
2490
-			}
2491
-		}
2492
-
2493
-		if ($component->CLASS) {
2494
-			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2495
-			switch ($component->CLASS->getValue()) {
2496
-				case 'PUBLIC':
2497
-					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2498
-					break;
2499
-				case 'CONFIDENTIAL':
2500
-					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2501
-					break;
2502
-			}
2503
-		}
2504
-		return [
2505
-			'etag' => md5($calendarData),
2506
-			'size' => strlen($calendarData),
2507
-			'componentType' => $componentType,
2508
-			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2509
-			'lastOccurence' => $lastOccurrence,
2510
-			'uid' => $uid,
2511
-			'classification' => $classification
2512
-		];
2513
-	}
2514
-
2515
-	/**
2516
-	 * @param $cardData
2517
-	 * @return bool|string
2518
-	 */
2519
-	private function readBlob($cardData) {
2520
-		if (is_resource($cardData)) {
2521
-			return stream_get_contents($cardData);
2522
-		}
2523
-
2524
-		return $cardData;
2525
-	}
2526
-
2527
-	/**
2528
-	 * @param IShareable $shareable
2529
-	 * @param array $add
2530
-	 * @param array $remove
2531
-	 */
2532
-	public function updateShares($shareable, $add, $remove) {
2533
-		$calendarId = $shareable->getResourceId();
2534
-		$calendarRow = $this->getCalendarById($calendarId);
2535
-		$oldShares = $this->getShares($calendarId);
2536
-
2537
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2538
-			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2539
-			[
2540
-				'calendarId' => $calendarId,
2541
-				'calendarData' => $calendarRow,
2542
-				'shares' => $oldShares,
2543
-				'add' => $add,
2544
-				'remove' => $remove,
2545
-			]));
2546
-		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2547
-
2548
-		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2549
-	}
2550
-
2551
-	/**
2552
-	 * @param int $resourceId
2553
-	 * @param int $calendarType
2554
-	 * @return array
2555
-	 */
2556
-	public function getShares($resourceId, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2557
-		return $this->calendarSharingBackend->getShares($resourceId);
2558
-	}
2559
-
2560
-	/**
2561
-	 * @param boolean $value
2562
-	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2563
-	 * @return string|null
2564
-	 */
2565
-	public function setPublishStatus($value, $calendar) {
2566
-		$calendarId = $calendar->getResourceId();
2567
-		$calendarData = $this->getCalendarById($calendarId);
2568
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
2569
-			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2570
-			[
2571
-				'calendarId' => $calendarId,
2572
-				'calendarData' => $calendarData,
2573
-				'public' => $value,
2574
-			]));
2575
-
2576
-		$query = $this->db->getQueryBuilder();
2577
-		if ($value) {
2578
-			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2579
-			$query->insert('dav_shares')
2580
-				->values([
2581
-					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2582
-					'type' => $query->createNamedParameter('calendar'),
2583
-					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2584
-					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2585
-					'publicuri' => $query->createNamedParameter($publicUri)
2586
-				]);
2587
-			$query->executeStatement();
2588
-
2589
-			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2590
-			return $publicUri;
2591
-		}
2592
-		$query->delete('dav_shares')
2593
-			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2594
-			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2595
-		$query->executeStatement();
2596
-
2597
-		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2598
-		return null;
2599
-	}
2600
-
2601
-	/**
2602
-	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2603
-	 * @return mixed
2604
-	 */
2605
-	public function getPublishStatus($calendar) {
2606
-		$query = $this->db->getQueryBuilder();
2607
-		$result = $query->select('publicuri')
2608
-			->from('dav_shares')
2609
-			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2610
-			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2611
-			->executeQuery();
2612
-
2613
-		$row = $result->fetch();
2614
-		$result->closeCursor();
2615
-		return $row ? reset($row) : false;
2616
-	}
2617
-
2618
-	/**
2619
-	 * @param int $resourceId
2620
-	 * @param array $acl
2621
-	 * @return array
2622
-	 */
2623
-	public function applyShareAcl($resourceId, $acl) {
2624
-		return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2625
-	}
2626
-
2627
-
2628
-
2629
-	/**
2630
-	 * update properties table
2631
-	 *
2632
-	 * @param int $calendarId
2633
-	 * @param string $objectUri
2634
-	 * @param string $calendarData
2635
-	 * @param int $calendarType
2636
-	 */
2637
-	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2638
-		$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2639
-
2640
-		try {
2641
-			$vCalendar = $this->readCalendarData($calendarData);
2642
-		} catch (\Exception $ex) {
2643
-			return;
2644
-		}
2645
-
2646
-		$this->purgeProperties($calendarId, $objectId);
2647
-
2648
-		$query = $this->db->getQueryBuilder();
2649
-		$query->insert($this->dbObjectPropertiesTable)
2650
-			->values(
2651
-				[
2652
-					'calendarid' => $query->createNamedParameter($calendarId),
2653
-					'calendartype' => $query->createNamedParameter($calendarType),
2654
-					'objectid' => $query->createNamedParameter($objectId),
2655
-					'name' => $query->createParameter('name'),
2656
-					'parameter' => $query->createParameter('parameter'),
2657
-					'value' => $query->createParameter('value'),
2658
-				]
2659
-			);
2660
-
2661
-		$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2662
-		foreach ($vCalendar->getComponents() as $component) {
2663
-			if (!in_array($component->name, $indexComponents)) {
2664
-				continue;
2665
-			}
2666
-
2667
-			foreach ($component->children() as $property) {
2668
-				if (in_array($property->name, self::$indexProperties)) {
2669
-					$value = $property->getValue();
2670
-					// is this a shitty db?
2671
-					if (!$this->db->supports4ByteText()) {
2672
-						$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2673
-					}
2674
-					$value = mb_strcut($value, 0, 254);
2675
-
2676
-					$query->setParameter('name', $property->name);
2677
-					$query->setParameter('parameter', null);
2678
-					$query->setParameter('value', $value);
2679
-					$query->executeStatement();
2680
-				}
2681
-
2682
-				if (array_key_exists($property->name, self::$indexParameters)) {
2683
-					$parameters = $property->parameters();
2684
-					$indexedParametersForProperty = self::$indexParameters[$property->name];
2685
-
2686
-					foreach ($parameters as $key => $value) {
2687
-						if (in_array($key, $indexedParametersForProperty)) {
2688
-							// is this a shitty db?
2689
-							if ($this->db->supports4ByteText()) {
2690
-								$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2691
-							}
2692
-
2693
-							$query->setParameter('name', $property->name);
2694
-							$query->setParameter('parameter', mb_strcut($key, 0, 254));
2695
-							$query->setParameter('value', mb_strcut($value, 0, 254));
2696
-							$query->executeStatement();
2697
-						}
2698
-					}
2699
-				}
2700
-			}
2701
-		}
2702
-	}
2703
-
2704
-	/**
2705
-	 * deletes all birthday calendars
2706
-	 */
2707
-	public function deleteAllBirthdayCalendars() {
2708
-		$query = $this->db->getQueryBuilder();
2709
-		$result = $query->select(['id'])->from('calendars')
2710
-			->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
2711
-			->executeQuery();
2712
-
2713
-		$ids = $result->fetchAll();
2714
-		foreach ($ids as $id) {
2715
-			$this->deleteCalendar($id['id']);
2716
-		}
2717
-	}
2718
-
2719
-	/**
2720
-	 * @param $subscriptionId
2721
-	 */
2722
-	public function purgeAllCachedEventsForSubscription($subscriptionId) {
2723
-		$query = $this->db->getQueryBuilder();
2724
-		$query->select('uri')
2725
-			->from('calendarobjects')
2726
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2727
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2728
-		$stmt = $query->executeQuery();
2729
-
2730
-		$uris = [];
2731
-		foreach ($stmt->fetchAll() as $row) {
2732
-			$uris[] = $row['uri'];
2733
-		}
2734
-		$stmt->closeCursor();
2735
-
2736
-		$query = $this->db->getQueryBuilder();
2737
-		$query->delete('calendarobjects')
2738
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2739
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2740
-			->executeStatement();
2741
-
2742
-		$query->delete('calendarchanges')
2743
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2744
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2745
-			->executeStatement();
2746
-
2747
-		$query->delete($this->dbObjectPropertiesTable)
2748
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2749
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2750
-			->executeStatement();
2751
-
2752
-		foreach ($uris as $uri) {
2753
-			$this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
2754
-		}
2755
-	}
2756
-
2757
-	/**
2758
-	 * Move a calendar from one user to another
2759
-	 *
2760
-	 * @param string $uriName
2761
-	 * @param string $uriOrigin
2762
-	 * @param string $uriDestination
2763
-	 * @param string $newUriName (optional) the new uriName
2764
-	 */
2765
-	public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
2766
-		$query = $this->db->getQueryBuilder();
2767
-		$query->update('calendars')
2768
-			->set('principaluri', $query->createNamedParameter($uriDestination))
2769
-			->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
2770
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
2771
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
2772
-			->executeStatement();
2773
-	}
2774
-
2775
-	/**
2776
-	 * read VCalendar data into a VCalendar object
2777
-	 *
2778
-	 * @param string $objectData
2779
-	 * @return VCalendar
2780
-	 */
2781
-	protected function readCalendarData($objectData) {
2782
-		return Reader::read($objectData);
2783
-	}
2784
-
2785
-	/**
2786
-	 * delete all properties from a given calendar object
2787
-	 *
2788
-	 * @param int $calendarId
2789
-	 * @param int $objectId
2790
-	 */
2791
-	protected function purgeProperties($calendarId, $objectId) {
2792
-		$query = $this->db->getQueryBuilder();
2793
-		$query->delete($this->dbObjectPropertiesTable)
2794
-			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
2795
-			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2796
-		$query->executeStatement();
2797
-	}
2798
-
2799
-	/**
2800
-	 * get ID from a given calendar object
2801
-	 *
2802
-	 * @param int $calendarId
2803
-	 * @param string $uri
2804
-	 * @param int $calendarType
2805
-	 * @return int
2806
-	 */
2807
-	protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
2808
-		$query = $this->db->getQueryBuilder();
2809
-		$query->select('id')
2810
-			->from('calendarobjects')
2811
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
2812
-			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
2813
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
2814
-
2815
-		$result = $query->executeQuery();
2816
-		$objectIds = $result->fetch();
2817
-		$result->closeCursor();
2818
-
2819
-		if (!isset($objectIds['id'])) {
2820
-			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2821
-		}
2822
-
2823
-		return (int)$objectIds['id'];
2824
-	}
2825
-
2826
-	/**
2827
-	 * return legacy endpoint principal name to new principal name
2828
-	 *
2829
-	 * @param $principalUri
2830
-	 * @param $toV2
2831
-	 * @return string
2832
-	 */
2833
-	private function convertPrincipal($principalUri, $toV2) {
2834
-		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
2835
-			[, $name] = Uri\split($principalUri);
2836
-			if ($toV2 === true) {
2837
-				return "principals/users/$name";
2838
-			}
2839
-			return "principals/$name";
2840
-		}
2841
-		return $principalUri;
2842
-	}
2843
-
2844
-	/**
2845
-	 * adds information about an owner to the calendar data
2846
-	 *
2847
-	 * @param $calendarInfo
2848
-	 */
2849
-	private function addOwnerPrincipal(&$calendarInfo) {
2850
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2851
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2852
-		if (isset($calendarInfo[$ownerPrincipalKey])) {
2853
-			$uri = $calendarInfo[$ownerPrincipalKey];
2854
-		} else {
2855
-			$uri = $calendarInfo['principaluri'];
2856
-		}
2857
-
2858
-		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
2859
-		if (isset($principalInformation['{DAV:}displayname'])) {
2860
-			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
2861
-		}
2862
-	}
99
+    public const CALENDAR_TYPE_CALENDAR = 0;
100
+    public const CALENDAR_TYPE_SUBSCRIPTION = 1;
101
+
102
+    public const PERSONAL_CALENDAR_URI = 'personal';
103
+    public const PERSONAL_CALENDAR_NAME = 'Personal';
104
+
105
+    public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
106
+    public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
107
+
108
+    /**
109
+     * We need to specify a max date, because we need to stop *somewhere*
110
+     *
111
+     * On 32 bit system the maximum for a signed integer is 2147483647, so
112
+     * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
113
+     * in 2038-01-19 to avoid problems when the date is converted
114
+     * to a unix timestamp.
115
+     */
116
+    public const MAX_DATE = '2038-01-01';
117
+
118
+    public const ACCESS_PUBLIC = 4;
119
+    public const CLASSIFICATION_PUBLIC = 0;
120
+    public const CLASSIFICATION_PRIVATE = 1;
121
+    public const CLASSIFICATION_CONFIDENTIAL = 2;
122
+
123
+    /**
124
+     * List of CalDAV properties, and how they map to database field names
125
+     * Add your own properties by simply adding on to this array.
126
+     *
127
+     * Note that only string-based properties are supported here.
128
+     *
129
+     * @var array
130
+     */
131
+    public $propertyMap = [
132
+        '{DAV:}displayname' => 'displayname',
133
+        '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
134
+        '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
135
+        '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
136
+        '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
137
+    ];
138
+
139
+    /**
140
+     * List of subscription properties, and how they map to database field names.
141
+     *
142
+     * @var array
143
+     */
144
+    public $subscriptionPropertyMap = [
145
+        '{DAV:}displayname' => 'displayname',
146
+        '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate',
147
+        '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
148
+        '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
149
+        '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos',
150
+        '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms',
151
+        '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
152
+    ];
153
+
154
+    /** @var array properties to index */
155
+    public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION',
156
+        'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT',
157
+        'ORGANIZER'];
158
+
159
+    /** @var array parameters to index */
160
+    public static $indexParameters = [
161
+        'ATTENDEE' => ['CN'],
162
+        'ORGANIZER' => ['CN'],
163
+    ];
164
+
165
+    /**
166
+     * @var string[] Map of uid => display name
167
+     */
168
+    protected $userDisplayNames;
169
+
170
+    /** @var IDBConnection */
171
+    private $db;
172
+
173
+    /** @var Backend */
174
+    private $calendarSharingBackend;
175
+
176
+    /** @var Principal */
177
+    private $principalBackend;
178
+
179
+    /** @var IUserManager */
180
+    private $userManager;
181
+
182
+    /** @var ISecureRandom */
183
+    private $random;
184
+
185
+    /** @var ILogger */
186
+    private $logger;
187
+
188
+    /** @var IEventDispatcher */
189
+    private $dispatcher;
190
+
191
+    /** @var EventDispatcherInterface */
192
+    private $legacyDispatcher;
193
+
194
+    /** @var bool */
195
+    private $legacyEndpoint;
196
+
197
+    /** @var string */
198
+    private $dbObjectPropertiesTable = 'calendarobjects_props';
199
+
200
+    /**
201
+     * CalDavBackend constructor.
202
+     *
203
+     * @param IDBConnection $db
204
+     * @param Principal $principalBackend
205
+     * @param IUserManager $userManager
206
+     * @param IGroupManager $groupManager
207
+     * @param ISecureRandom $random
208
+     * @param ILogger $logger
209
+     * @param IEventDispatcher $dispatcher
210
+     * @param EventDispatcherInterface $legacyDispatcher
211
+     * @param bool $legacyEndpoint
212
+     */
213
+    public function __construct(IDBConnection $db,
214
+                                Principal $principalBackend,
215
+                                IUserManager $userManager,
216
+                                IGroupManager $groupManager,
217
+                                ISecureRandom $random,
218
+                                ILogger $logger,
219
+                                IEventDispatcher $dispatcher,
220
+                                EventDispatcherInterface $legacyDispatcher,
221
+                                bool $legacyEndpoint = false) {
222
+        $this->db = $db;
223
+        $this->principalBackend = $principalBackend;
224
+        $this->userManager = $userManager;
225
+        $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
226
+        $this->random = $random;
227
+        $this->logger = $logger;
228
+        $this->dispatcher = $dispatcher;
229
+        $this->legacyDispatcher = $legacyDispatcher;
230
+        $this->legacyEndpoint = $legacyEndpoint;
231
+    }
232
+
233
+    /**
234
+     * Return the number of calendars for a principal
235
+     *
236
+     * By default this excludes the automatically generated birthday calendar
237
+     *
238
+     * @param $principalUri
239
+     * @param bool $excludeBirthday
240
+     * @return int
241
+     */
242
+    public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
243
+        $principalUri = $this->convertPrincipal($principalUri, true);
244
+        $query = $this->db->getQueryBuilder();
245
+        $query->select($query->func()->count('*'))
246
+            ->from('calendars');
247
+
248
+        if ($principalUri === '') {
249
+            $query->where($query->expr()->emptyString('principaluri'));
250
+        } else {
251
+            $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
252
+        }
253
+
254
+        if ($excludeBirthday) {
255
+            $query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
256
+        }
257
+
258
+        $result = $query->executeQuery();
259
+        $column = (int)$result->fetchOne();
260
+        $result->closeCursor();
261
+        return $column;
262
+    }
263
+
264
+    /**
265
+     * Returns a list of calendars for a principal.
266
+     *
267
+     * Every project is an array with the following keys:
268
+     *  * id, a unique id that will be used by other functions to modify the
269
+     *    calendar. This can be the same as the uri or a database key.
270
+     *  * uri, which the basename of the uri with which the calendar is
271
+     *    accessed.
272
+     *  * principaluri. The owner of the calendar. Almost always the same as
273
+     *    principalUri passed to this method.
274
+     *
275
+     * Furthermore it can contain webdav properties in clark notation. A very
276
+     * common one is '{DAV:}displayname'.
277
+     *
278
+     * Many clients also require:
279
+     * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
280
+     * For this property, you can just return an instance of
281
+     * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
282
+     *
283
+     * If you return {http://sabredav.org/ns}read-only and set the value to 1,
284
+     * ACL will automatically be put in read-only mode.
285
+     *
286
+     * @param string $principalUri
287
+     * @return array
288
+     */
289
+    public function getCalendarsForUser($principalUri) {
290
+        $principalUriOriginal = $principalUri;
291
+        $principalUri = $this->convertPrincipal($principalUri, true);
292
+        $fields = array_values($this->propertyMap);
293
+        $fields[] = 'id';
294
+        $fields[] = 'uri';
295
+        $fields[] = 'synctoken';
296
+        $fields[] = 'components';
297
+        $fields[] = 'principaluri';
298
+        $fields[] = 'transparent';
299
+
300
+        // Making fields a comma-delimited list
301
+        $query = $this->db->getQueryBuilder();
302
+        $query->select($fields)
303
+            ->from('calendars')
304
+            ->orderBy('calendarorder', 'ASC');
305
+
306
+        if ($principalUri === '') {
307
+            $query->where($query->expr()->emptyString('principaluri'));
308
+        } else {
309
+            $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
310
+        }
311
+
312
+        $result = $query->executeQuery();
313
+
314
+        $calendars = [];
315
+        while ($row = $result->fetch()) {
316
+            $row['principaluri'] = (string) $row['principaluri'];
317
+            $components = [];
318
+            if ($row['components']) {
319
+                $components = explode(',',$row['components']);
320
+            }
321
+
322
+            $calendar = [
323
+                'id' => $row['id'],
324
+                'uri' => $row['uri'],
325
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
326
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
327
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
328
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
329
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
330
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
331
+            ];
332
+
333
+            foreach ($this->propertyMap as $xmlName => $dbName) {
334
+                $calendar[$xmlName] = $row[$dbName];
335
+            }
336
+
337
+            $this->addOwnerPrincipal($calendar);
338
+
339
+            if (!isset($calendars[$calendar['id']])) {
340
+                $calendars[$calendar['id']] = $calendar;
341
+            }
342
+        }
343
+        $result->closeCursor();
344
+
345
+        // query for shared calendars
346
+        $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
347
+        $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
348
+
349
+        $principals[] = $principalUri;
350
+
351
+        $fields = array_values($this->propertyMap);
352
+        $fields[] = 'a.id';
353
+        $fields[] = 'a.uri';
354
+        $fields[] = 'a.synctoken';
355
+        $fields[] = 'a.components';
356
+        $fields[] = 'a.principaluri';
357
+        $fields[] = 'a.transparent';
358
+        $fields[] = 's.access';
359
+        $query = $this->db->getQueryBuilder();
360
+        $query->select($fields)
361
+            ->from('dav_shares', 's')
362
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
363
+            ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
364
+            ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
365
+            ->setParameter('type', 'calendar')
366
+            ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
367
+
368
+        $result = $query->executeQuery();
369
+
370
+        $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
371
+        while ($row = $result->fetch()) {
372
+            $row['principaluri'] = (string) $row['principaluri'];
373
+            if ($row['principaluri'] === $principalUri) {
374
+                continue;
375
+            }
376
+
377
+            $readOnly = (int) $row['access'] === Backend::ACCESS_READ;
378
+            if (isset($calendars[$row['id']])) {
379
+                if ($readOnly) {
380
+                    // New share can not have more permissions then the old one.
381
+                    continue;
382
+                }
383
+                if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
384
+                    $calendars[$row['id']][$readOnlyPropertyName] === 0) {
385
+                    // Old share is already read-write, no more permissions can be gained
386
+                    continue;
387
+                }
388
+            }
389
+
390
+            [, $name] = Uri\split($row['principaluri']);
391
+            $uri = $row['uri'] . '_shared_by_' . $name;
392
+            $row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
393
+            $components = [];
394
+            if ($row['components']) {
395
+                $components = explode(',',$row['components']);
396
+            }
397
+            $calendar = [
398
+                'id' => $row['id'],
399
+                'uri' => $uri,
400
+                'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
401
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
402
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
403
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
404
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
405
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
406
+                $readOnlyPropertyName => $readOnly,
407
+            ];
408
+
409
+            foreach ($this->propertyMap as $xmlName => $dbName) {
410
+                $calendar[$xmlName] = $row[$dbName];
411
+            }
412
+
413
+            $this->addOwnerPrincipal($calendar);
414
+
415
+            $calendars[$calendar['id']] = $calendar;
416
+        }
417
+        $result->closeCursor();
418
+
419
+        return array_values($calendars);
420
+    }
421
+
422
+    /**
423
+     * @param $principalUri
424
+     * @return array
425
+     */
426
+    public function getUsersOwnCalendars($principalUri) {
427
+        $principalUri = $this->convertPrincipal($principalUri, true);
428
+        $fields = array_values($this->propertyMap);
429
+        $fields[] = 'id';
430
+        $fields[] = 'uri';
431
+        $fields[] = 'synctoken';
432
+        $fields[] = 'components';
433
+        $fields[] = 'principaluri';
434
+        $fields[] = 'transparent';
435
+        // Making fields a comma-delimited list
436
+        $query = $this->db->getQueryBuilder();
437
+        $query->select($fields)->from('calendars')
438
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
439
+            ->orderBy('calendarorder', 'ASC');
440
+        $stmt = $query->executeQuery();
441
+        $calendars = [];
442
+        while ($row = $stmt->fetch()) {
443
+            $row['principaluri'] = (string) $row['principaluri'];
444
+            $components = [];
445
+            if ($row['components']) {
446
+                $components = explode(',',$row['components']);
447
+            }
448
+            $calendar = [
449
+                'id' => $row['id'],
450
+                'uri' => $row['uri'],
451
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
452
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
453
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
454
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
455
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
456
+            ];
457
+            foreach ($this->propertyMap as $xmlName => $dbName) {
458
+                $calendar[$xmlName] = $row[$dbName];
459
+            }
460
+
461
+            $this->addOwnerPrincipal($calendar);
462
+
463
+            if (!isset($calendars[$calendar['id']])) {
464
+                $calendars[$calendar['id']] = $calendar;
465
+            }
466
+        }
467
+        $stmt->closeCursor();
468
+        return array_values($calendars);
469
+    }
470
+
471
+
472
+    /**
473
+     * @param $uid
474
+     * @return string
475
+     */
476
+    private function getUserDisplayName($uid) {
477
+        if (!isset($this->userDisplayNames[$uid])) {
478
+            $user = $this->userManager->get($uid);
479
+
480
+            if ($user instanceof IUser) {
481
+                $this->userDisplayNames[$uid] = $user->getDisplayName();
482
+            } else {
483
+                $this->userDisplayNames[$uid] = $uid;
484
+            }
485
+        }
486
+
487
+        return $this->userDisplayNames[$uid];
488
+    }
489
+
490
+    /**
491
+     * @return array
492
+     */
493
+    public function getPublicCalendars() {
494
+        $fields = array_values($this->propertyMap);
495
+        $fields[] = 'a.id';
496
+        $fields[] = 'a.uri';
497
+        $fields[] = 'a.synctoken';
498
+        $fields[] = 'a.components';
499
+        $fields[] = 'a.principaluri';
500
+        $fields[] = 'a.transparent';
501
+        $fields[] = 's.access';
502
+        $fields[] = 's.publicuri';
503
+        $calendars = [];
504
+        $query = $this->db->getQueryBuilder();
505
+        $result = $query->select($fields)
506
+            ->from('dav_shares', 's')
507
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
508
+            ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
509
+            ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
510
+            ->executeQuery();
511
+
512
+        while ($row = $result->fetch()) {
513
+            $row['principaluri'] = (string) $row['principaluri'];
514
+            [, $name] = Uri\split($row['principaluri']);
515
+            $row['displayname'] = $row['displayname'] . "($name)";
516
+            $components = [];
517
+            if ($row['components']) {
518
+                $components = explode(',',$row['components']);
519
+            }
520
+            $calendar = [
521
+                'id' => $row['id'],
522
+                'uri' => $row['publicuri'],
523
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
524
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
525
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
526
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
527
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
528
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
529
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
530
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
531
+            ];
532
+
533
+            foreach ($this->propertyMap as $xmlName => $dbName) {
534
+                $calendar[$xmlName] = $row[$dbName];
535
+            }
536
+
537
+            $this->addOwnerPrincipal($calendar);
538
+
539
+            if (!isset($calendars[$calendar['id']])) {
540
+                $calendars[$calendar['id']] = $calendar;
541
+            }
542
+        }
543
+        $result->closeCursor();
544
+
545
+        return array_values($calendars);
546
+    }
547
+
548
+    /**
549
+     * @param string $uri
550
+     * @return array
551
+     * @throws NotFound
552
+     */
553
+    public function getPublicCalendar($uri) {
554
+        $fields = array_values($this->propertyMap);
555
+        $fields[] = 'a.id';
556
+        $fields[] = 'a.uri';
557
+        $fields[] = 'a.synctoken';
558
+        $fields[] = 'a.components';
559
+        $fields[] = 'a.principaluri';
560
+        $fields[] = 'a.transparent';
561
+        $fields[] = 's.access';
562
+        $fields[] = 's.publicuri';
563
+        $query = $this->db->getQueryBuilder();
564
+        $result = $query->select($fields)
565
+            ->from('dav_shares', 's')
566
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
567
+            ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
568
+            ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
569
+            ->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
570
+            ->executeQuery();
571
+
572
+        $row = $result->fetch();
573
+
574
+        $result->closeCursor();
575
+
576
+        if ($row === false) {
577
+            throw new NotFound('Node with name \'' . $uri . '\' could not be found');
578
+        }
579
+
580
+        $row['principaluri'] = (string) $row['principaluri'];
581
+        [, $name] = Uri\split($row['principaluri']);
582
+        $row['displayname'] = $row['displayname'] . ' ' . "($name)";
583
+        $components = [];
584
+        if ($row['components']) {
585
+            $components = explode(',',$row['components']);
586
+        }
587
+        $calendar = [
588
+            'id' => $row['id'],
589
+            'uri' => $row['publicuri'],
590
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
591
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
592
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
593
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
594
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
595
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
596
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
597
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
598
+        ];
599
+
600
+        foreach ($this->propertyMap as $xmlName => $dbName) {
601
+            $calendar[$xmlName] = $row[$dbName];
602
+        }
603
+
604
+        $this->addOwnerPrincipal($calendar);
605
+
606
+        return $calendar;
607
+    }
608
+
609
+    /**
610
+     * @param string $principal
611
+     * @param string $uri
612
+     * @return array|null
613
+     */
614
+    public function getCalendarByUri($principal, $uri) {
615
+        $fields = array_values($this->propertyMap);
616
+        $fields[] = 'id';
617
+        $fields[] = 'uri';
618
+        $fields[] = 'synctoken';
619
+        $fields[] = 'components';
620
+        $fields[] = 'principaluri';
621
+        $fields[] = 'transparent';
622
+
623
+        // Making fields a comma-delimited list
624
+        $query = $this->db->getQueryBuilder();
625
+        $query->select($fields)->from('calendars')
626
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
627
+            ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
628
+            ->setMaxResults(1);
629
+        $stmt = $query->executeQuery();
630
+
631
+        $row = $stmt->fetch();
632
+        $stmt->closeCursor();
633
+        if ($row === false) {
634
+            return null;
635
+        }
636
+
637
+        $row['principaluri'] = (string) $row['principaluri'];
638
+        $components = [];
639
+        if ($row['components']) {
640
+            $components = explode(',',$row['components']);
641
+        }
642
+
643
+        $calendar = [
644
+            'id' => $row['id'],
645
+            'uri' => $row['uri'],
646
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
647
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
648
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
649
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
651
+        ];
652
+
653
+        foreach ($this->propertyMap as $xmlName => $dbName) {
654
+            $calendar[$xmlName] = $row[$dbName];
655
+        }
656
+
657
+        $this->addOwnerPrincipal($calendar);
658
+
659
+        return $calendar;
660
+    }
661
+
662
+    /**
663
+     * @param $calendarId
664
+     * @return array|null
665
+     */
666
+    public function getCalendarById($calendarId) {
667
+        $fields = array_values($this->propertyMap);
668
+        $fields[] = 'id';
669
+        $fields[] = 'uri';
670
+        $fields[] = 'synctoken';
671
+        $fields[] = 'components';
672
+        $fields[] = 'principaluri';
673
+        $fields[] = 'transparent';
674
+
675
+        // Making fields a comma-delimited list
676
+        $query = $this->db->getQueryBuilder();
677
+        $query->select($fields)->from('calendars')
678
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
679
+            ->setMaxResults(1);
680
+        $stmt = $query->executeQuery();
681
+
682
+        $row = $stmt->fetch();
683
+        $stmt->closeCursor();
684
+        if ($row === false) {
685
+            return null;
686
+        }
687
+
688
+        $row['principaluri'] = (string) $row['principaluri'];
689
+        $components = [];
690
+        if ($row['components']) {
691
+            $components = explode(',',$row['components']);
692
+        }
693
+
694
+        $calendar = [
695
+            'id' => $row['id'],
696
+            'uri' => $row['uri'],
697
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
698
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
699
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
700
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
701
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
702
+        ];
703
+
704
+        foreach ($this->propertyMap as $xmlName => $dbName) {
705
+            $calendar[$xmlName] = $row[$dbName];
706
+        }
707
+
708
+        $this->addOwnerPrincipal($calendar);
709
+
710
+        return $calendar;
711
+    }
712
+
713
+    /**
714
+     * @param $subscriptionId
715
+     */
716
+    public function getSubscriptionById($subscriptionId) {
717
+        $fields = array_values($this->subscriptionPropertyMap);
718
+        $fields[] = 'id';
719
+        $fields[] = 'uri';
720
+        $fields[] = 'source';
721
+        $fields[] = 'synctoken';
722
+        $fields[] = 'principaluri';
723
+        $fields[] = 'lastmodified';
724
+
725
+        $query = $this->db->getQueryBuilder();
726
+        $query->select($fields)
727
+            ->from('calendarsubscriptions')
728
+            ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
729
+            ->orderBy('calendarorder', 'asc');
730
+        $stmt = $query->executeQuery();
731
+
732
+        $row = $stmt->fetch();
733
+        $stmt->closeCursor();
734
+        if ($row === false) {
735
+            return null;
736
+        }
737
+
738
+        $row['principaluri'] = (string) $row['principaluri'];
739
+        $subscription = [
740
+            'id' => $row['id'],
741
+            'uri' => $row['uri'],
742
+            'principaluri' => $row['principaluri'],
743
+            'source' => $row['source'],
744
+            'lastmodified' => $row['lastmodified'],
745
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
746
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
747
+        ];
748
+
749
+        foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
750
+            if (!is_null($row[$dbName])) {
751
+                $subscription[$xmlName] = $row[$dbName];
752
+            }
753
+        }
754
+
755
+        return $subscription;
756
+    }
757
+
758
+    /**
759
+     * Creates a new calendar for a principal.
760
+     *
761
+     * If the creation was a success, an id must be returned that can be used to reference
762
+     * this calendar in other methods, such as updateCalendar.
763
+     *
764
+     * @param string $principalUri
765
+     * @param string $calendarUri
766
+     * @param array $properties
767
+     * @return int
768
+     */
769
+    public function createCalendar($principalUri, $calendarUri, array $properties) {
770
+        $values = [
771
+            'principaluri' => $this->convertPrincipal($principalUri, true),
772
+            'uri' => $calendarUri,
773
+            'synctoken' => 1,
774
+            'transparent' => 0,
775
+            'components' => 'VEVENT,VTODO',
776
+            'displayname' => $calendarUri
777
+        ];
778
+
779
+        // Default value
780
+        $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
781
+        if (isset($properties[$sccs])) {
782
+            if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
783
+                throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
784
+            }
785
+            $values['components'] = implode(',',$properties[$sccs]->getValue());
786
+        } elseif (isset($properties['components'])) {
787
+            // Allow to provide components internally without having
788
+            // to create a SupportedCalendarComponentSet object
789
+            $values['components'] = $properties['components'];
790
+        }
791
+
792
+        $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
793
+        if (isset($properties[$transp])) {
794
+            $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
795
+        }
796
+
797
+        foreach ($this->propertyMap as $xmlName => $dbName) {
798
+            if (isset($properties[$xmlName])) {
799
+                $values[$dbName] = $properties[$xmlName];
800
+            }
801
+        }
802
+
803
+        $query = $this->db->getQueryBuilder();
804
+        $query->insert('calendars');
805
+        foreach ($values as $column => $value) {
806
+            $query->setValue($column, $query->createNamedParameter($value));
807
+        }
808
+        $query->executeStatement();
809
+        $calendarId = $query->getLastInsertId();
810
+
811
+        $calendarData = $this->getCalendarById($calendarId);
812
+        $this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
813
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
814
+            '\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
815
+            [
816
+                'calendarId' => $calendarId,
817
+                'calendarData' => $calendarData,
818
+            ]));
819
+
820
+        return $calendarId;
821
+    }
822
+
823
+    /**
824
+     * Updates properties for a calendar.
825
+     *
826
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
827
+     * To do the actual updates, you must tell this object which properties
828
+     * you're going to process with the handle() method.
829
+     *
830
+     * Calling the handle method is like telling the PropPatch object "I
831
+     * promise I can handle updating this property".
832
+     *
833
+     * Read the PropPatch documentation for more info and examples.
834
+     *
835
+     * @param mixed $calendarId
836
+     * @param PropPatch $propPatch
837
+     * @return void
838
+     */
839
+    public function updateCalendar($calendarId, PropPatch $propPatch) {
840
+        $supportedProperties = array_keys($this->propertyMap);
841
+        $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
842
+
843
+        $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
844
+            $newValues = [];
845
+            foreach ($mutations as $propertyName => $propertyValue) {
846
+                switch ($propertyName) {
847
+                    case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
848
+                        $fieldName = 'transparent';
849
+                        $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
850
+                        break;
851
+                    default:
852
+                        $fieldName = $this->propertyMap[$propertyName];
853
+                        $newValues[$fieldName] = $propertyValue;
854
+                        break;
855
+                }
856
+            }
857
+            $query = $this->db->getQueryBuilder();
858
+            $query->update('calendars');
859
+            foreach ($newValues as $fieldName => $value) {
860
+                $query->set($fieldName, $query->createNamedParameter($value));
861
+            }
862
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
863
+            $query->executeStatement();
864
+
865
+            $this->addChange($calendarId, "", 2);
866
+
867
+            $calendarData = $this->getCalendarById($calendarId);
868
+            $shares = $this->getShares($calendarId);
869
+            $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
870
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
871
+                '\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
872
+                [
873
+                    'calendarId' => $calendarId,
874
+                    'calendarData' => $calendarData,
875
+                    'shares' => $shares,
876
+                    'propertyMutations' => $mutations,
877
+                ]));
878
+
879
+            return true;
880
+        });
881
+    }
882
+
883
+    /**
884
+     * Delete a calendar and all it's objects
885
+     *
886
+     * @param mixed $calendarId
887
+     * @return void
888
+     */
889
+    public function deleteCalendar($calendarId) {
890
+        $calendarData = $this->getCalendarById($calendarId);
891
+        $shares = $this->getShares($calendarId);
892
+
893
+        $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?');
894
+        $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
895
+
896
+        $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
897
+        $stmt->execute([$calendarId]);
898
+
899
+        $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?');
900
+        $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
901
+
902
+        $this->calendarSharingBackend->deleteAllShares($calendarId);
903
+
904
+        $query = $this->db->getQueryBuilder();
905
+        $query->delete($this->dbObjectPropertiesTable)
906
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
907
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
908
+            ->executeStatement();
909
+
910
+        // Only dispatch if we actually deleted anything
911
+        if ($calendarData) {
912
+            $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
913
+        }
914
+    }
915
+
916
+    /**
917
+     * Delete all of an user's shares
918
+     *
919
+     * @param string $principaluri
920
+     * @return void
921
+     */
922
+    public function deleteAllSharesByUser($principaluri) {
923
+        $this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
924
+    }
925
+
926
+    /**
927
+     * Returns all calendar objects within a calendar.
928
+     *
929
+     * Every item contains an array with the following keys:
930
+     *   * calendardata - The iCalendar-compatible calendar data
931
+     *   * uri - a unique key which will be used to construct the uri. This can
932
+     *     be any arbitrary string, but making sure it ends with '.ics' is a
933
+     *     good idea. This is only the basename, or filename, not the full
934
+     *     path.
935
+     *   * lastmodified - a timestamp of the last modification time
936
+     *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
937
+     *   '"abcdef"')
938
+     *   * size - The size of the calendar objects, in bytes.
939
+     *   * component - optional, a string containing the type of object, such
940
+     *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
941
+     *     the Content-Type header.
942
+     *
943
+     * Note that the etag is optional, but it's highly encouraged to return for
944
+     * speed reasons.
945
+     *
946
+     * The calendardata is also optional. If it's not returned
947
+     * 'getCalendarObject' will be called later, which *is* expected to return
948
+     * calendardata.
949
+     *
950
+     * If neither etag or size are specified, the calendardata will be
951
+     * used/fetched to determine these numbers. If both are specified the
952
+     * amount of times this is needed is reduced by a great degree.
953
+     *
954
+     * @param mixed $calendarId
955
+     * @param int $calendarType
956
+     * @return array
957
+     */
958
+    public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
959
+        $query = $this->db->getQueryBuilder();
960
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
961
+            ->from('calendarobjects')
962
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
963
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
964
+        $stmt = $query->executeQuery();
965
+
966
+        $result = [];
967
+        foreach ($stmt->fetchAll() as $row) {
968
+            $result[] = [
969
+                'id' => $row['id'],
970
+                'uri' => $row['uri'],
971
+                'lastmodified' => $row['lastmodified'],
972
+                'etag' => '"' . $row['etag'] . '"',
973
+                'calendarid' => $row['calendarid'],
974
+                'size' => (int)$row['size'],
975
+                'component' => strtolower($row['componenttype']),
976
+                'classification' => (int)$row['classification']
977
+            ];
978
+        }
979
+        $stmt->closeCursor();
980
+
981
+        return $result;
982
+    }
983
+
984
+    /**
985
+     * Returns information from a single calendar object, based on it's object
986
+     * uri.
987
+     *
988
+     * The object uri is only the basename, or filename and not a full path.
989
+     *
990
+     * The returned array must have the same keys as getCalendarObjects. The
991
+     * 'calendardata' object is required here though, while it's not required
992
+     * for getCalendarObjects.
993
+     *
994
+     * This method must return null if the object did not exist.
995
+     *
996
+     * @param mixed $calendarId
997
+     * @param string $objectUri
998
+     * @param int $calendarType
999
+     * @return array|null
1000
+     */
1001
+    public function getCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1002
+        $query = $this->db->getQueryBuilder();
1003
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1004
+            ->from('calendarobjects')
1005
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1006
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1007
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1008
+        $stmt = $query->executeQuery();
1009
+        $row = $stmt->fetch();
1010
+        $stmt->closeCursor();
1011
+
1012
+        if (!$row) {
1013
+            return null;
1014
+        }
1015
+
1016
+        return [
1017
+            'id' => $row['id'],
1018
+            'uri' => $row['uri'],
1019
+            'lastmodified' => $row['lastmodified'],
1020
+            'etag' => '"' . $row['etag'] . '"',
1021
+            'calendarid' => $row['calendarid'],
1022
+            'size' => (int)$row['size'],
1023
+            'calendardata' => $this->readBlob($row['calendardata']),
1024
+            'component' => strtolower($row['componenttype']),
1025
+            'classification' => (int)$row['classification']
1026
+        ];
1027
+    }
1028
+
1029
+    /**
1030
+     * Returns a list of calendar objects.
1031
+     *
1032
+     * This method should work identical to getCalendarObject, but instead
1033
+     * return all the calendar objects in the list as an array.
1034
+     *
1035
+     * If the backend supports this, it may allow for some speed-ups.
1036
+     *
1037
+     * @param mixed $calendarId
1038
+     * @param string[] $uris
1039
+     * @param int $calendarType
1040
+     * @return array
1041
+     */
1042
+    public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1043
+        if (empty($uris)) {
1044
+            return [];
1045
+        }
1046
+
1047
+        $chunks = array_chunk($uris, 100);
1048
+        $objects = [];
1049
+
1050
+        $query = $this->db->getQueryBuilder();
1051
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1052
+            ->from('calendarobjects')
1053
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1054
+            ->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1055
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1056
+
1057
+        foreach ($chunks as $uris) {
1058
+            $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1059
+            $result = $query->executeQuery();
1060
+
1061
+            while ($row = $result->fetch()) {
1062
+                $objects[] = [
1063
+                    'id' => $row['id'],
1064
+                    'uri' => $row['uri'],
1065
+                    'lastmodified' => $row['lastmodified'],
1066
+                    'etag' => '"' . $row['etag'] . '"',
1067
+                    'calendarid' => $row['calendarid'],
1068
+                    'size' => (int)$row['size'],
1069
+                    'calendardata' => $this->readBlob($row['calendardata']),
1070
+                    'component' => strtolower($row['componenttype']),
1071
+                    'classification' => (int)$row['classification']
1072
+                ];
1073
+            }
1074
+            $result->closeCursor();
1075
+        }
1076
+
1077
+        return $objects;
1078
+    }
1079
+
1080
+    /**
1081
+     * Creates a new calendar object.
1082
+     *
1083
+     * The object uri is only the basename, or filename and not a full path.
1084
+     *
1085
+     * It is possible return an etag from this function, which will be used in
1086
+     * the response to this PUT request. Note that the ETag must be surrounded
1087
+     * by double-quotes.
1088
+     *
1089
+     * However, you should only really return this ETag if you don't mangle the
1090
+     * calendar-data. If the result of a subsequent GET to this object is not
1091
+     * the exact same as this request body, you should omit the ETag.
1092
+     *
1093
+     * @param mixed $calendarId
1094
+     * @param string $objectUri
1095
+     * @param string $calendarData
1096
+     * @param int $calendarType
1097
+     * @return string
1098
+     */
1099
+    public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1100
+        $extraData = $this->getDenormalizedData($calendarData);
1101
+
1102
+        $q = $this->db->getQueryBuilder();
1103
+        $q->select($q->func()->count('*'))
1104
+            ->from('calendarobjects')
1105
+            ->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId)))
1106
+            ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid'])))
1107
+            ->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType)));
1108
+
1109
+        $result = $q->executeQuery();
1110
+        $count = (int) $result->fetchOne();
1111
+        $result->closeCursor();
1112
+
1113
+        if ($count !== 0) {
1114
+            throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.');
1115
+        }
1116
+
1117
+        $query = $this->db->getQueryBuilder();
1118
+        $query->insert('calendarobjects')
1119
+            ->values([
1120
+                'calendarid' => $query->createNamedParameter($calendarId),
1121
+                'uri' => $query->createNamedParameter($objectUri),
1122
+                'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1123
+                'lastmodified' => $query->createNamedParameter(time()),
1124
+                'etag' => $query->createNamedParameter($extraData['etag']),
1125
+                'size' => $query->createNamedParameter($extraData['size']),
1126
+                'componenttype' => $query->createNamedParameter($extraData['componentType']),
1127
+                'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1128
+                'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1129
+                'classification' => $query->createNamedParameter($extraData['classification']),
1130
+                'uid' => $query->createNamedParameter($extraData['uid']),
1131
+                'calendartype' => $query->createNamedParameter($calendarType),
1132
+            ])
1133
+            ->executeStatement();
1134
+
1135
+        $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1136
+        $this->addChange($calendarId, $objectUri, 1, $calendarType);
1137
+
1138
+        $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1139
+        if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1140
+            $calendarRow = $this->getCalendarById($calendarId);
1141
+            $shares = $this->getShares($calendarId);
1142
+
1143
+            $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1144
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1145
+                '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1146
+                [
1147
+                    'calendarId' => $calendarId,
1148
+                    'calendarData' => $calendarRow,
1149
+                    'shares' => $shares,
1150
+                    'objectData' => $objectRow,
1151
+                ]
1152
+            ));
1153
+        } else {
1154
+            $subscriptionRow = $this->getSubscriptionById($calendarId);
1155
+
1156
+            $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1157
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1158
+                '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1159
+                [
1160
+                    'subscriptionId' => $calendarId,
1161
+                    'calendarData' => $subscriptionRow,
1162
+                    'shares' => [],
1163
+                    'objectData' => $objectRow,
1164
+                ]
1165
+            ));
1166
+        }
1167
+
1168
+        return '"' . $extraData['etag'] . '"';
1169
+    }
1170
+
1171
+    /**
1172
+     * Updates an existing calendarobject, based on it's uri.
1173
+     *
1174
+     * The object uri is only the basename, or filename and not a full path.
1175
+     *
1176
+     * It is possible return an etag from this function, which will be used in
1177
+     * the response to this PUT request. Note that the ETag must be surrounded
1178
+     * by double-quotes.
1179
+     *
1180
+     * However, you should only really return this ETag if you don't mangle the
1181
+     * calendar-data. If the result of a subsequent GET to this object is not
1182
+     * the exact same as this request body, you should omit the ETag.
1183
+     *
1184
+     * @param mixed $calendarId
1185
+     * @param string $objectUri
1186
+     * @param string $calendarData
1187
+     * @param int $calendarType
1188
+     * @return string
1189
+     */
1190
+    public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1191
+        $extraData = $this->getDenormalizedData($calendarData);
1192
+        $query = $this->db->getQueryBuilder();
1193
+        $query->update('calendarobjects')
1194
+                ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1195
+                ->set('lastmodified', $query->createNamedParameter(time()))
1196
+                ->set('etag', $query->createNamedParameter($extraData['etag']))
1197
+                ->set('size', $query->createNamedParameter($extraData['size']))
1198
+                ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1199
+                ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1200
+                ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1201
+                ->set('classification', $query->createNamedParameter($extraData['classification']))
1202
+                ->set('uid', $query->createNamedParameter($extraData['uid']))
1203
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1204
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1205
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1206
+            ->executeStatement();
1207
+
1208
+        $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1209
+        $this->addChange($calendarId, $objectUri, 2, $calendarType);
1210
+
1211
+        $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1212
+        if (is_array($objectRow)) {
1213
+            if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1214
+                $calendarRow = $this->getCalendarById($calendarId);
1215
+                $shares = $this->getShares($calendarId);
1216
+
1217
+                $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1218
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1219
+                    '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1220
+                    [
1221
+                        'calendarId' => $calendarId,
1222
+                        'calendarData' => $calendarRow,
1223
+                        'shares' => $shares,
1224
+                        'objectData' => $objectRow,
1225
+                    ]
1226
+                ));
1227
+            } else {
1228
+                $subscriptionRow = $this->getSubscriptionById($calendarId);
1229
+
1230
+                $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1231
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1232
+                    '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1233
+                    [
1234
+                        'subscriptionId' => $calendarId,
1235
+                        'calendarData' => $subscriptionRow,
1236
+                        'shares' => [],
1237
+                        'objectData' => $objectRow,
1238
+                    ]
1239
+                ));
1240
+            }
1241
+        }
1242
+
1243
+        return '"' . $extraData['etag'] . '"';
1244
+    }
1245
+
1246
+    /**
1247
+     * @param int $calendarObjectId
1248
+     * @param int $classification
1249
+     */
1250
+    public function setClassification($calendarObjectId, $classification) {
1251
+        if (!in_array($classification, [
1252
+            self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1253
+        ])) {
1254
+            throw new \InvalidArgumentException();
1255
+        }
1256
+        $query = $this->db->getQueryBuilder();
1257
+        $query->update('calendarobjects')
1258
+            ->set('classification', $query->createNamedParameter($classification))
1259
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1260
+            ->executeStatement();
1261
+    }
1262
+
1263
+    /**
1264
+     * Deletes an existing calendar object.
1265
+     *
1266
+     * The object uri is only the basename, or filename and not a full path.
1267
+     *
1268
+     * @param mixed $calendarId
1269
+     * @param string $objectUri
1270
+     * @param int $calendarType
1271
+     * @return void
1272
+     */
1273
+    public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1274
+        $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1275
+        if (is_array($data)) {
1276
+            if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1277
+                $calendarRow = $this->getCalendarById($calendarId);
1278
+                $shares = $this->getShares($calendarId);
1279
+
1280
+                $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1281
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1282
+                    '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1283
+                    [
1284
+                        'calendarId' => $calendarId,
1285
+                        'calendarData' => $calendarRow,
1286
+                        'shares' => $shares,
1287
+                        'objectData' => $data,
1288
+                    ]
1289
+                ));
1290
+            } else {
1291
+                $subscriptionRow = $this->getSubscriptionById($calendarId);
1292
+
1293
+                $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1294
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1295
+                    '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1296
+                    [
1297
+                        'subscriptionId' => $calendarId,
1298
+                        'calendarData' => $subscriptionRow,
1299
+                        'shares' => [],
1300
+                        'objectData' => $data,
1301
+                    ]
1302
+                ));
1303
+            }
1304
+        }
1305
+
1306
+        $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1307
+        $stmt->execute([$calendarId, $objectUri, $calendarType]);
1308
+
1309
+        if (is_array($data)) {
1310
+            $this->purgeProperties($calendarId, $data['id'], $calendarType);
1311
+        }
1312
+
1313
+        $this->addChange($calendarId, $objectUri, 3, $calendarType);
1314
+    }
1315
+
1316
+    /**
1317
+     * Performs a calendar-query on the contents of this calendar.
1318
+     *
1319
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
1320
+     * calendar-query it is possible for a client to request a specific set of
1321
+     * object, based on contents of iCalendar properties, date-ranges and
1322
+     * iCalendar component types (VTODO, VEVENT).
1323
+     *
1324
+     * This method should just return a list of (relative) urls that match this
1325
+     * query.
1326
+     *
1327
+     * The list of filters are specified as an array. The exact array is
1328
+     * documented by Sabre\CalDAV\CalendarQueryParser.
1329
+     *
1330
+     * Note that it is extremely likely that getCalendarObject for every path
1331
+     * returned from this method will be called almost immediately after. You
1332
+     * may want to anticipate this to speed up these requests.
1333
+     *
1334
+     * This method provides a default implementation, which parses *all* the
1335
+     * iCalendar objects in the specified calendar.
1336
+     *
1337
+     * This default may well be good enough for personal use, and calendars
1338
+     * that aren't very large. But if you anticipate high usage, big calendars
1339
+     * or high loads, you are strongly advised to optimize certain paths.
1340
+     *
1341
+     * The best way to do so is override this method and to optimize
1342
+     * specifically for 'common filters'.
1343
+     *
1344
+     * Requests that are extremely common are:
1345
+     *   * requests for just VEVENTS
1346
+     *   * requests for just VTODO
1347
+     *   * requests with a time-range-filter on either VEVENT or VTODO.
1348
+     *
1349
+     * ..and combinations of these requests. It may not be worth it to try to
1350
+     * handle every possible situation and just rely on the (relatively
1351
+     * easy to use) CalendarQueryValidator to handle the rest.
1352
+     *
1353
+     * Note that especially time-range-filters may be difficult to parse. A
1354
+     * time-range filter specified on a VEVENT must for instance also handle
1355
+     * recurrence rules correctly.
1356
+     * A good example of how to interprete all these filters can also simply
1357
+     * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1358
+     * as possible, so it gives you a good idea on what type of stuff you need
1359
+     * to think of.
1360
+     *
1361
+     * @param mixed $calendarId
1362
+     * @param array $filters
1363
+     * @param int $calendarType
1364
+     * @return array
1365
+     */
1366
+    public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1367
+        $componentType = null;
1368
+        $requirePostFilter = true;
1369
+        $timeRange = null;
1370
+
1371
+        // if no filters were specified, we don't need to filter after a query
1372
+        if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1373
+            $requirePostFilter = false;
1374
+        }
1375
+
1376
+        // Figuring out if there's a component filter
1377
+        if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1378
+            $componentType = $filters['comp-filters'][0]['name'];
1379
+
1380
+            // Checking if we need post-filters
1381
+            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1382
+                $requirePostFilter = false;
1383
+            }
1384
+            // There was a time-range filter
1385
+            if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
1386
+                $timeRange = $filters['comp-filters'][0]['time-range'];
1387
+
1388
+                // If start time OR the end time is not specified, we can do a
1389
+                // 100% accurate mysql query.
1390
+                if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1391
+                    $requirePostFilter = false;
1392
+                }
1393
+            }
1394
+        }
1395
+        $columns = ['uri'];
1396
+        if ($requirePostFilter) {
1397
+            $columns = ['uri', 'calendardata'];
1398
+        }
1399
+        $query = $this->db->getQueryBuilder();
1400
+        $query->select($columns)
1401
+            ->from('calendarobjects')
1402
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1403
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1404
+
1405
+        if ($componentType) {
1406
+            $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1407
+        }
1408
+
1409
+        if ($timeRange && $timeRange['start']) {
1410
+            $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1411
+        }
1412
+        if ($timeRange && $timeRange['end']) {
1413
+            $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1414
+        }
1415
+
1416
+        $stmt = $query->executeQuery();
1417
+
1418
+        $result = [];
1419
+        while ($row = $stmt->fetch()) {
1420
+            if ($requirePostFilter) {
1421
+                // validateFilterForObject will parse the calendar data
1422
+                // catch parsing errors
1423
+                try {
1424
+                    $matches = $this->validateFilterForObject($row, $filters);
1425
+                } catch (ParseException $ex) {
1426
+                    $this->logger->logException($ex, [
1427
+                        'app' => 'dav',
1428
+                        'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1429
+                    ]);
1430
+                    continue;
1431
+                } catch (InvalidDataException $ex) {
1432
+                    $this->logger->logException($ex, [
1433
+                        'app' => 'dav',
1434
+                        'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1435
+                    ]);
1436
+                    continue;
1437
+                }
1438
+
1439
+                if (!$matches) {
1440
+                    continue;
1441
+                }
1442
+            }
1443
+            $result[] = $row['uri'];
1444
+        }
1445
+
1446
+        return $result;
1447
+    }
1448
+
1449
+    /**
1450
+     * custom Nextcloud search extension for CalDAV
1451
+     *
1452
+     * TODO - this should optionally cover cached calendar objects as well
1453
+     *
1454
+     * @param string $principalUri
1455
+     * @param array $filters
1456
+     * @param integer|null $limit
1457
+     * @param integer|null $offset
1458
+     * @return array
1459
+     */
1460
+    public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1461
+        $calendars = $this->getCalendarsForUser($principalUri);
1462
+        $ownCalendars = [];
1463
+        $sharedCalendars = [];
1464
+
1465
+        $uriMapper = [];
1466
+
1467
+        foreach ($calendars as $calendar) {
1468
+            if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1469
+                $ownCalendars[] = $calendar['id'];
1470
+            } else {
1471
+                $sharedCalendars[] = $calendar['id'];
1472
+            }
1473
+            $uriMapper[$calendar['id']] = $calendar['uri'];
1474
+        }
1475
+        if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1476
+            return [];
1477
+        }
1478
+
1479
+        $query = $this->db->getQueryBuilder();
1480
+        // Calendar id expressions
1481
+        $calendarExpressions = [];
1482
+        foreach ($ownCalendars as $id) {
1483
+            $calendarExpressions[] = $query->expr()->andX(
1484
+                $query->expr()->eq('c.calendarid',
1485
+                    $query->createNamedParameter($id)),
1486
+                $query->expr()->eq('c.calendartype',
1487
+                        $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1488
+        }
1489
+        foreach ($sharedCalendars as $id) {
1490
+            $calendarExpressions[] = $query->expr()->andX(
1491
+                $query->expr()->eq('c.calendarid',
1492
+                    $query->createNamedParameter($id)),
1493
+                $query->expr()->eq('c.classification',
1494
+                    $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1495
+                $query->expr()->eq('c.calendartype',
1496
+                    $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1497
+        }
1498
+
1499
+        if (count($calendarExpressions) === 1) {
1500
+            $calExpr = $calendarExpressions[0];
1501
+        } else {
1502
+            $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1503
+        }
1504
+
1505
+        // Component expressions
1506
+        $compExpressions = [];
1507
+        foreach ($filters['comps'] as $comp) {
1508
+            $compExpressions[] = $query->expr()
1509
+                ->eq('c.componenttype', $query->createNamedParameter($comp));
1510
+        }
1511
+
1512
+        if (count($compExpressions) === 1) {
1513
+            $compExpr = $compExpressions[0];
1514
+        } else {
1515
+            $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1516
+        }
1517
+
1518
+        if (!isset($filters['props'])) {
1519
+            $filters['props'] = [];
1520
+        }
1521
+        if (!isset($filters['params'])) {
1522
+            $filters['params'] = [];
1523
+        }
1524
+
1525
+        $propParamExpressions = [];
1526
+        foreach ($filters['props'] as $prop) {
1527
+            $propParamExpressions[] = $query->expr()->andX(
1528
+                $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1529
+                $query->expr()->isNull('i.parameter')
1530
+            );
1531
+        }
1532
+        foreach ($filters['params'] as $param) {
1533
+            $propParamExpressions[] = $query->expr()->andX(
1534
+                $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1535
+                $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1536
+            );
1537
+        }
1538
+
1539
+        if (count($propParamExpressions) === 1) {
1540
+            $propParamExpr = $propParamExpressions[0];
1541
+        } else {
1542
+            $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1543
+        }
1544
+
1545
+        $query->select(['c.calendarid', 'c.uri'])
1546
+            ->from($this->dbObjectPropertiesTable, 'i')
1547
+            ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1548
+            ->where($calExpr)
1549
+            ->andWhere($compExpr)
1550
+            ->andWhere($propParamExpr)
1551
+            ->andWhere($query->expr()->iLike('i.value',
1552
+                $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
1553
+
1554
+        if ($offset) {
1555
+            $query->setFirstResult($offset);
1556
+        }
1557
+        if ($limit) {
1558
+            $query->setMaxResults($limit);
1559
+        }
1560
+
1561
+        $stmt = $query->executeQuery();
1562
+
1563
+        $result = [];
1564
+        while ($row = $stmt->fetch()) {
1565
+            $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1566
+            if (!in_array($path, $result)) {
1567
+                $result[] = $path;
1568
+            }
1569
+        }
1570
+
1571
+        return $result;
1572
+    }
1573
+
1574
+    /**
1575
+     * used for Nextcloud's calendar API
1576
+     *
1577
+     * @param array $calendarInfo
1578
+     * @param string $pattern
1579
+     * @param array $searchProperties
1580
+     * @param array $options
1581
+     * @param integer|null $limit
1582
+     * @param integer|null $offset
1583
+     *
1584
+     * @return array
1585
+     */
1586
+    public function search(array $calendarInfo, $pattern, array $searchProperties,
1587
+                            array $options, $limit, $offset) {
1588
+        $outerQuery = $this->db->getQueryBuilder();
1589
+        $innerQuery = $this->db->getQueryBuilder();
1590
+
1591
+        $innerQuery->selectDistinct('op.objectid')
1592
+            ->from($this->dbObjectPropertiesTable, 'op')
1593
+            ->andWhere($innerQuery->expr()->eq('op.calendarid',
1594
+                $outerQuery->createNamedParameter($calendarInfo['id'])))
1595
+            ->andWhere($innerQuery->expr()->eq('op.calendartype',
1596
+                $outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1597
+
1598
+        // only return public items for shared calendars for now
1599
+        if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1600
+            $innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1601
+                $outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1602
+        }
1603
+
1604
+        $or = $innerQuery->expr()->orX();
1605
+        foreach ($searchProperties as $searchProperty) {
1606
+            $or->add($innerQuery->expr()->eq('op.name',
1607
+                $outerQuery->createNamedParameter($searchProperty)));
1608
+        }
1609
+        $innerQuery->andWhere($or);
1610
+
1611
+        if ($pattern !== '') {
1612
+            $innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1613
+                $outerQuery->createNamedParameter('%' .
1614
+                    $this->db->escapeLikeParameter($pattern) . '%')));
1615
+        }
1616
+
1617
+        $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1618
+            ->from('calendarobjects', 'c');
1619
+
1620
+        if (isset($options['timerange'])) {
1621
+            if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) {
1622
+                $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1623
+                    $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1624
+            }
1625
+            if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTime) {
1626
+                $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1627
+                    $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1628
+            }
1629
+        }
1630
+
1631
+        if (isset($options['types'])) {
1632
+            $or = $outerQuery->expr()->orX();
1633
+            foreach ($options['types'] as $type) {
1634
+                $or->add($outerQuery->expr()->eq('componenttype',
1635
+                    $outerQuery->createNamedParameter($type)));
1636
+            }
1637
+            $outerQuery->andWhere($or);
1638
+        }
1639
+
1640
+        $outerQuery->andWhere($outerQuery->expr()->in('c.id',
1641
+            $outerQuery->createFunction($innerQuery->getSQL())));
1642
+
1643
+        if ($offset) {
1644
+            $outerQuery->setFirstResult($offset);
1645
+        }
1646
+        if ($limit) {
1647
+            $outerQuery->setMaxResults($limit);
1648
+        }
1649
+
1650
+        $result = $outerQuery->executeQuery();
1651
+        $calendarObjects = $result->fetchAll();
1652
+
1653
+        return array_map(function ($o) {
1654
+            $calendarData = Reader::read($o['calendardata']);
1655
+            $comps = $calendarData->getComponents();
1656
+            $objects = [];
1657
+            $timezones = [];
1658
+            foreach ($comps as $comp) {
1659
+                if ($comp instanceof VTimeZone) {
1660
+                    $timezones[] = $comp;
1661
+                } else {
1662
+                    $objects[] = $comp;
1663
+                }
1664
+            }
1665
+
1666
+            return [
1667
+                'id' => $o['id'],
1668
+                'type' => $o['componenttype'],
1669
+                'uid' => $o['uid'],
1670
+                'uri' => $o['uri'],
1671
+                'objects' => array_map(function ($c) {
1672
+                    return $this->transformSearchData($c);
1673
+                }, $objects),
1674
+                'timezones' => array_map(function ($c) {
1675
+                    return $this->transformSearchData($c);
1676
+                }, $timezones),
1677
+            ];
1678
+        }, $calendarObjects);
1679
+    }
1680
+
1681
+    /**
1682
+     * @param Component $comp
1683
+     * @return array
1684
+     */
1685
+    private function transformSearchData(Component $comp) {
1686
+        $data = [];
1687
+        /** @var Component[] $subComponents */
1688
+        $subComponents = $comp->getComponents();
1689
+        /** @var Property[] $properties */
1690
+        $properties = array_filter($comp->children(), function ($c) {
1691
+            return $c instanceof Property;
1692
+        });
1693
+        $validationRules = $comp->getValidationRules();
1694
+
1695
+        foreach ($subComponents as $subComponent) {
1696
+            $name = $subComponent->name;
1697
+            if (!isset($data[$name])) {
1698
+                $data[$name] = [];
1699
+            }
1700
+            $data[$name][] = $this->transformSearchData($subComponent);
1701
+        }
1702
+
1703
+        foreach ($properties as $property) {
1704
+            $name = $property->name;
1705
+            if (!isset($validationRules[$name])) {
1706
+                $validationRules[$name] = '*';
1707
+            }
1708
+
1709
+            $rule = $validationRules[$property->name];
1710
+            if ($rule === '+' || $rule === '*') { // multiple
1711
+                if (!isset($data[$name])) {
1712
+                    $data[$name] = [];
1713
+                }
1714
+
1715
+                $data[$name][] = $this->transformSearchProperty($property);
1716
+            } else { // once
1717
+                $data[$name] = $this->transformSearchProperty($property);
1718
+            }
1719
+        }
1720
+
1721
+        return $data;
1722
+    }
1723
+
1724
+    /**
1725
+     * @param Property $prop
1726
+     * @return array
1727
+     */
1728
+    private function transformSearchProperty(Property $prop) {
1729
+        // No need to check Date, as it extends DateTime
1730
+        if ($prop instanceof Property\ICalendar\DateTime) {
1731
+            $value = $prop->getDateTime();
1732
+        } else {
1733
+            $value = $prop->getValue();
1734
+        }
1735
+
1736
+        return [
1737
+            $value,
1738
+            $prop->parameters()
1739
+        ];
1740
+    }
1741
+
1742
+    /**
1743
+     * @param string $principalUri
1744
+     * @param string $pattern
1745
+     * @param array $componentTypes
1746
+     * @param array $searchProperties
1747
+     * @param array $searchParameters
1748
+     * @param array $options
1749
+     * @return array
1750
+     */
1751
+    public function searchPrincipalUri(string $principalUri,
1752
+                                        string $pattern,
1753
+                                        array $componentTypes,
1754
+                                        array $searchProperties,
1755
+                                        array $searchParameters,
1756
+                                        array $options = []): array {
1757
+        $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1758
+
1759
+        $calendarObjectIdQuery = $this->db->getQueryBuilder();
1760
+        $calendarOr = $calendarObjectIdQuery->expr()->orX();
1761
+        $searchOr = $calendarObjectIdQuery->expr()->orX();
1762
+
1763
+        // Fetch calendars and subscription
1764
+        $calendars = $this->getCalendarsForUser($principalUri);
1765
+        $subscriptions = $this->getSubscriptionsForUser($principalUri);
1766
+        foreach ($calendars as $calendar) {
1767
+            $calendarAnd = $calendarObjectIdQuery->expr()->andX();
1768
+            $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
1769
+            $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1770
+
1771
+            // If it's shared, limit search to public events
1772
+            if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
1773
+                && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
1774
+                $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1775
+            }
1776
+
1777
+            $calendarOr->add($calendarAnd);
1778
+        }
1779
+        foreach ($subscriptions as $subscription) {
1780
+            $subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
1781
+            $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
1782
+            $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
1783
+
1784
+            // If it's shared, limit search to public events
1785
+            if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
1786
+                && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
1787
+                $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1788
+            }
1789
+
1790
+            $calendarOr->add($subscriptionAnd);
1791
+        }
1792
+
1793
+        foreach ($searchProperties as $property) {
1794
+            $propertyAnd = $calendarObjectIdQuery->expr()->andX();
1795
+            $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
1796
+            $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
1797
+
1798
+            $searchOr->add($propertyAnd);
1799
+        }
1800
+        foreach ($searchParameters as $property => $parameter) {
1801
+            $parameterAnd = $calendarObjectIdQuery->expr()->andX();
1802
+            $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
1803
+            $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
1804
+
1805
+            $searchOr->add($parameterAnd);
1806
+        }
1807
+
1808
+        if ($calendarOr->count() === 0) {
1809
+            return [];
1810
+        }
1811
+        if ($searchOr->count() === 0) {
1812
+            return [];
1813
+        }
1814
+
1815
+        $calendarObjectIdQuery->selectDistinct('cob.objectid')
1816
+            ->from($this->dbObjectPropertiesTable, 'cob')
1817
+            ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
1818
+            ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
1819
+            ->andWhere($calendarOr)
1820
+            ->andWhere($searchOr);
1821
+
1822
+        if ('' !== $pattern) {
1823
+            if (!$escapePattern) {
1824
+                $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
1825
+            } else {
1826
+                $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1827
+            }
1828
+        }
1829
+
1830
+        if (isset($options['limit'])) {
1831
+            $calendarObjectIdQuery->setMaxResults($options['limit']);
1832
+        }
1833
+        if (isset($options['offset'])) {
1834
+            $calendarObjectIdQuery->setFirstResult($options['offset']);
1835
+        }
1836
+
1837
+        $result = $calendarObjectIdQuery->executeQuery();
1838
+        $matches = $result->fetchAll();
1839
+        $result->closeCursor();
1840
+        $matches = array_map(static function (array $match):int {
1841
+            return (int) $match['objectid'];
1842
+        }, $matches);
1843
+
1844
+        $query = $this->db->getQueryBuilder();
1845
+        $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
1846
+            ->from('calendarobjects')
1847
+            ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1848
+
1849
+        $result = $query->executeQuery();
1850
+        $calendarObjects = $result->fetchAll();
1851
+        $result->closeCursor();
1852
+
1853
+        return array_map(function (array $array): array {
1854
+            $array['calendarid'] = (int)$array['calendarid'];
1855
+            $array['calendartype'] = (int)$array['calendartype'];
1856
+            $array['calendardata'] = $this->readBlob($array['calendardata']);
1857
+
1858
+            return $array;
1859
+        }, $calendarObjects);
1860
+    }
1861
+
1862
+    /**
1863
+     * Searches through all of a users calendars and calendar objects to find
1864
+     * an object with a specific UID.
1865
+     *
1866
+     * This method should return the path to this object, relative to the
1867
+     * calendar home, so this path usually only contains two parts:
1868
+     *
1869
+     * calendarpath/objectpath.ics
1870
+     *
1871
+     * If the uid is not found, return null.
1872
+     *
1873
+     * This method should only consider * objects that the principal owns, so
1874
+     * any calendars owned by other principals that also appear in this
1875
+     * collection should be ignored.
1876
+     *
1877
+     * @param string $principalUri
1878
+     * @param string $uid
1879
+     * @return string|null
1880
+     */
1881
+    public function getCalendarObjectByUID($principalUri, $uid) {
1882
+        $query = $this->db->getQueryBuilder();
1883
+        $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1884
+            ->from('calendarobjects', 'co')
1885
+            ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1886
+            ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1887
+            ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1888
+
1889
+        $stmt = $query->executeQuery();
1890
+        $row = $stmt->fetch();
1891
+        $stmt->closeCursor();
1892
+        if ($row) {
1893
+            return $row['calendaruri'] . '/' . $row['objecturi'];
1894
+        }
1895
+
1896
+        return null;
1897
+    }
1898
+
1899
+    /**
1900
+     * The getChanges method returns all the changes that have happened, since
1901
+     * the specified syncToken in the specified calendar.
1902
+     *
1903
+     * This function should return an array, such as the following:
1904
+     *
1905
+     * [
1906
+     *   'syncToken' => 'The current synctoken',
1907
+     *   'added'   => [
1908
+     *      'new.txt',
1909
+     *   ],
1910
+     *   'modified'   => [
1911
+     *      'modified.txt',
1912
+     *   ],
1913
+     *   'deleted' => [
1914
+     *      'foo.php.bak',
1915
+     *      'old.txt'
1916
+     *   ]
1917
+     * );
1918
+     *
1919
+     * The returned syncToken property should reflect the *current* syncToken
1920
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1921
+     * property This is * needed here too, to ensure the operation is atomic.
1922
+     *
1923
+     * If the $syncToken argument is specified as null, this is an initial
1924
+     * sync, and all members should be reported.
1925
+     *
1926
+     * The modified property is an array of nodenames that have changed since
1927
+     * the last token.
1928
+     *
1929
+     * The deleted property is an array with nodenames, that have been deleted
1930
+     * from collection.
1931
+     *
1932
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
1933
+     * 1, you only have to report changes that happened only directly in
1934
+     * immediate descendants. If it's 2, it should also include changes from
1935
+     * the nodes below the child collections. (grandchildren)
1936
+     *
1937
+     * The $limit argument allows a client to specify how many results should
1938
+     * be returned at most. If the limit is not specified, it should be treated
1939
+     * as infinite.
1940
+     *
1941
+     * If the limit (infinite or not) is higher than you're willing to return,
1942
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1943
+     *
1944
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
1945
+     * return null.
1946
+     *
1947
+     * The limit is 'suggestive'. You are free to ignore it.
1948
+     *
1949
+     * @param string $calendarId
1950
+     * @param string $syncToken
1951
+     * @param int $syncLevel
1952
+     * @param int|null $limit
1953
+     * @param int $calendarType
1954
+     * @return array
1955
+     */
1956
+    public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1957
+        // Current synctoken
1958
+        $qb = $this->db->getQueryBuilder();
1959
+        $qb->select('synctoken')
1960
+            ->from('calendars')
1961
+            ->where(
1962
+                $qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
1963
+            );
1964
+        $stmt = $qb->executeQuery();
1965
+        $currentToken = $stmt->fetchOne();
1966
+
1967
+        if ($currentToken === false) {
1968
+            return null;
1969
+        }
1970
+
1971
+        $result = [
1972
+            'syncToken' => $currentToken,
1973
+            'added' => [],
1974
+            'modified' => [],
1975
+            'deleted' => [],
1976
+        ];
1977
+
1978
+        if ($syncToken) {
1979
+            $qb = $this->db->getQueryBuilder();
1980
+
1981
+            $qb->select('uri', 'operation')
1982
+                ->from('calendarchanges')
1983
+                ->where(
1984
+                    $qb->expr()->andX(
1985
+                        $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
1986
+                        $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
1987
+                        $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1988
+                        $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
1989
+                    )
1990
+                )->orderBy('synctoken');
1991
+            if (is_int($limit) && $limit > 0) {
1992
+                $qb->setMaxResults($limit);
1993
+            }
1994
+
1995
+            // Fetching all changes
1996
+            $stmt = $qb->executeQuery();
1997
+            $changes = [];
1998
+
1999
+            // This loop ensures that any duplicates are overwritten, only the
2000
+            // last change on a node is relevant.
2001
+            while ($row = $stmt->fetch()) {
2002
+                $changes[$row['uri']] = $row['operation'];
2003
+            }
2004
+            $stmt->closeCursor();
2005
+
2006
+            foreach ($changes as $uri => $operation) {
2007
+                switch ($operation) {
2008
+                    case 1:
2009
+                        $result['added'][] = $uri;
2010
+                        break;
2011
+                    case 2:
2012
+                        $result['modified'][] = $uri;
2013
+                        break;
2014
+                    case 3:
2015
+                        $result['deleted'][] = $uri;
2016
+                        break;
2017
+                }
2018
+            }
2019
+        } else {
2020
+            // No synctoken supplied, this is the initial sync.
2021
+            $qb = $this->db->getQueryBuilder();
2022
+            $qb->select('uri')
2023
+                ->from('calendarobjects')
2024
+                ->where(
2025
+                    $qb->expr()->andX(
2026
+                        $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2027
+                        $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2028
+                    )
2029
+                );
2030
+            $stmt = $qb->executeQuery();
2031
+            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2032
+            $stmt->closeCursor();
2033
+        }
2034
+        return $result;
2035
+    }
2036
+
2037
+    /**
2038
+     * Returns a list of subscriptions for a principal.
2039
+     *
2040
+     * Every subscription is an array with the following keys:
2041
+     *  * id, a unique id that will be used by other functions to modify the
2042
+     *    subscription. This can be the same as the uri or a database key.
2043
+     *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2044
+     *  * principaluri. The owner of the subscription. Almost always the same as
2045
+     *    principalUri passed to this method.
2046
+     *
2047
+     * Furthermore, all the subscription info must be returned too:
2048
+     *
2049
+     * 1. {DAV:}displayname
2050
+     * 2. {http://apple.com/ns/ical/}refreshrate
2051
+     * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2052
+     *    should not be stripped).
2053
+     * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2054
+     *    should not be stripped).
2055
+     * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2056
+     *    attachments should not be stripped).
2057
+     * 6. {http://calendarserver.org/ns/}source (Must be a
2058
+     *     Sabre\DAV\Property\Href).
2059
+     * 7. {http://apple.com/ns/ical/}calendar-color
2060
+     * 8. {http://apple.com/ns/ical/}calendar-order
2061
+     * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2062
+     *    (should just be an instance of
2063
+     *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2064
+     *    default components).
2065
+     *
2066
+     * @param string $principalUri
2067
+     * @return array
2068
+     */
2069
+    public function getSubscriptionsForUser($principalUri) {
2070
+        $fields = array_values($this->subscriptionPropertyMap);
2071
+        $fields[] = 'id';
2072
+        $fields[] = 'uri';
2073
+        $fields[] = 'source';
2074
+        $fields[] = 'principaluri';
2075
+        $fields[] = 'lastmodified';
2076
+        $fields[] = 'synctoken';
2077
+
2078
+        $query = $this->db->getQueryBuilder();
2079
+        $query->select($fields)
2080
+            ->from('calendarsubscriptions')
2081
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2082
+            ->orderBy('calendarorder', 'asc');
2083
+        $stmt = $query->executeQuery();
2084
+
2085
+        $subscriptions = [];
2086
+        while ($row = $stmt->fetch()) {
2087
+            $subscription = [
2088
+                'id' => $row['id'],
2089
+                'uri' => $row['uri'],
2090
+                'principaluri' => $row['principaluri'],
2091
+                'source' => $row['source'],
2092
+                'lastmodified' => $row['lastmodified'],
2093
+
2094
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2095
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2096
+            ];
2097
+
2098
+            foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
2099
+                if (!is_null($row[$dbName])) {
2100
+                    $subscription[$xmlName] = $row[$dbName];
2101
+                }
2102
+            }
2103
+
2104
+            $subscriptions[] = $subscription;
2105
+        }
2106
+
2107
+        return $subscriptions;
2108
+    }
2109
+
2110
+    /**
2111
+     * Creates a new subscription for a principal.
2112
+     *
2113
+     * If the creation was a success, an id must be returned that can be used to reference
2114
+     * this subscription in other methods, such as updateSubscription.
2115
+     *
2116
+     * @param string $principalUri
2117
+     * @param string $uri
2118
+     * @param array $properties
2119
+     * @return mixed
2120
+     */
2121
+    public function createSubscription($principalUri, $uri, array $properties) {
2122
+        if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2123
+            throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2124
+        }
2125
+
2126
+        $values = [
2127
+            'principaluri' => $principalUri,
2128
+            'uri' => $uri,
2129
+            'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2130
+            'lastmodified' => time(),
2131
+        ];
2132
+
2133
+        $propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2134
+
2135
+        foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
2136
+            if (array_key_exists($xmlName, $properties)) {
2137
+                $values[$dbName] = $properties[$xmlName];
2138
+                if (in_array($dbName, $propertiesBoolean)) {
2139
+                    $values[$dbName] = true;
2140
+                }
2141
+            }
2142
+        }
2143
+
2144
+        $valuesToInsert = [];
2145
+
2146
+        $query = $this->db->getQueryBuilder();
2147
+
2148
+        foreach (array_keys($values) as $name) {
2149
+            $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2150
+        }
2151
+
2152
+        $query->insert('calendarsubscriptions')
2153
+            ->values($valuesToInsert)
2154
+            ->executeStatement();
2155
+
2156
+        $subscriptionId = $query->getLastInsertId();
2157
+
2158
+        $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2159
+        $this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2160
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
2161
+            '\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2162
+            [
2163
+                'subscriptionId' => $subscriptionId,
2164
+                'subscriptionData' => $subscriptionRow,
2165
+            ]));
2166
+
2167
+        return $subscriptionId;
2168
+    }
2169
+
2170
+    /**
2171
+     * Updates a subscription
2172
+     *
2173
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2174
+     * To do the actual updates, you must tell this object which properties
2175
+     * you're going to process with the handle() method.
2176
+     *
2177
+     * Calling the handle method is like telling the PropPatch object "I
2178
+     * promise I can handle updating this property".
2179
+     *
2180
+     * Read the PropPatch documentation for more info and examples.
2181
+     *
2182
+     * @param mixed $subscriptionId
2183
+     * @param PropPatch $propPatch
2184
+     * @return void
2185
+     */
2186
+    public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2187
+        $supportedProperties = array_keys($this->subscriptionPropertyMap);
2188
+        $supportedProperties[] = '{http://calendarserver.org/ns/}source';
2189
+
2190
+        $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2191
+            $newValues = [];
2192
+
2193
+            foreach ($mutations as $propertyName => $propertyValue) {
2194
+                if ($propertyName === '{http://calendarserver.org/ns/}source') {
2195
+                    $newValues['source'] = $propertyValue->getHref();
2196
+                } else {
2197
+                    $fieldName = $this->subscriptionPropertyMap[$propertyName];
2198
+                    $newValues[$fieldName] = $propertyValue;
2199
+                }
2200
+            }
2201
+
2202
+            $query = $this->db->getQueryBuilder();
2203
+            $query->update('calendarsubscriptions')
2204
+                ->set('lastmodified', $query->createNamedParameter(time()));
2205
+            foreach ($newValues as $fieldName => $value) {
2206
+                $query->set($fieldName, $query->createNamedParameter($value));
2207
+            }
2208
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2209
+                ->executeStatement();
2210
+
2211
+            $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2212
+            $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2213
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2214
+                '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2215
+                [
2216
+                    'subscriptionId' => $subscriptionId,
2217
+                    'subscriptionData' => $subscriptionRow,
2218
+                    'propertyMutations' => $mutations,
2219
+                ]));
2220
+
2221
+            return true;
2222
+        });
2223
+    }
2224
+
2225
+    /**
2226
+     * Deletes a subscription.
2227
+     *
2228
+     * @param mixed $subscriptionId
2229
+     * @return void
2230
+     */
2231
+    public function deleteSubscription($subscriptionId) {
2232
+        $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2233
+
2234
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
2235
+            '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2236
+            [
2237
+                'subscriptionId' => $subscriptionId,
2238
+                'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2239
+            ]));
2240
+
2241
+        $query = $this->db->getQueryBuilder();
2242
+        $query->delete('calendarsubscriptions')
2243
+            ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2244
+            ->executeStatement();
2245
+
2246
+        $query = $this->db->getQueryBuilder();
2247
+        $query->delete('calendarobjects')
2248
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2249
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2250
+            ->executeStatement();
2251
+
2252
+        $query->delete('calendarchanges')
2253
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2254
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2255
+            ->executeStatement();
2256
+
2257
+        $query->delete($this->dbObjectPropertiesTable)
2258
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2259
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2260
+            ->executeStatement();
2261
+
2262
+        if ($subscriptionRow) {
2263
+            $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2264
+        }
2265
+    }
2266
+
2267
+    /**
2268
+     * Returns a single scheduling object for the inbox collection.
2269
+     *
2270
+     * The returned array should contain the following elements:
2271
+     *   * uri - A unique basename for the object. This will be used to
2272
+     *           construct a full uri.
2273
+     *   * calendardata - The iCalendar object
2274
+     *   * lastmodified - The last modification date. Can be an int for a unix
2275
+     *                    timestamp, or a PHP DateTime object.
2276
+     *   * etag - A unique token that must change if the object changed.
2277
+     *   * size - The size of the object, in bytes.
2278
+     *
2279
+     * @param string $principalUri
2280
+     * @param string $objectUri
2281
+     * @return array
2282
+     */
2283
+    public function getSchedulingObject($principalUri, $objectUri) {
2284
+        $query = $this->db->getQueryBuilder();
2285
+        $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2286
+            ->from('schedulingobjects')
2287
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2288
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2289
+            ->executeQuery();
2290
+
2291
+        $row = $stmt->fetch();
2292
+
2293
+        if (!$row) {
2294
+            return null;
2295
+        }
2296
+
2297
+        return [
2298
+            'uri' => $row['uri'],
2299
+            'calendardata' => $row['calendardata'],
2300
+            'lastmodified' => $row['lastmodified'],
2301
+            'etag' => '"' . $row['etag'] . '"',
2302
+            'size' => (int)$row['size'],
2303
+        ];
2304
+    }
2305
+
2306
+    /**
2307
+     * Returns all scheduling objects for the inbox collection.
2308
+     *
2309
+     * These objects should be returned as an array. Every item in the array
2310
+     * should follow the same structure as returned from getSchedulingObject.
2311
+     *
2312
+     * The main difference is that 'calendardata' is optional.
2313
+     *
2314
+     * @param string $principalUri
2315
+     * @return array
2316
+     */
2317
+    public function getSchedulingObjects($principalUri) {
2318
+        $query = $this->db->getQueryBuilder();
2319
+        $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2320
+                ->from('schedulingobjects')
2321
+                ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2322
+                ->executeQuery();
2323
+
2324
+        $result = [];
2325
+        foreach ($stmt->fetchAll() as $row) {
2326
+            $result[] = [
2327
+                'calendardata' => $row['calendardata'],
2328
+                'uri' => $row['uri'],
2329
+                'lastmodified' => $row['lastmodified'],
2330
+                'etag' => '"' . $row['etag'] . '"',
2331
+                'size' => (int)$row['size'],
2332
+            ];
2333
+        }
2334
+
2335
+        return $result;
2336
+    }
2337
+
2338
+    /**
2339
+     * Deletes a scheduling object from the inbox collection.
2340
+     *
2341
+     * @param string $principalUri
2342
+     * @param string $objectUri
2343
+     * @return void
2344
+     */
2345
+    public function deleteSchedulingObject($principalUri, $objectUri) {
2346
+        $query = $this->db->getQueryBuilder();
2347
+        $query->delete('schedulingobjects')
2348
+                ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2349
+                ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2350
+                ->executeStatement();
2351
+    }
2352
+
2353
+    /**
2354
+     * Creates a new scheduling object. This should land in a users' inbox.
2355
+     *
2356
+     * @param string $principalUri
2357
+     * @param string $objectUri
2358
+     * @param string $objectData
2359
+     * @return void
2360
+     */
2361
+    public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2362
+        $query = $this->db->getQueryBuilder();
2363
+        $query->insert('schedulingobjects')
2364
+            ->values([
2365
+                'principaluri' => $query->createNamedParameter($principalUri),
2366
+                'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2367
+                'uri' => $query->createNamedParameter($objectUri),
2368
+                'lastmodified' => $query->createNamedParameter(time()),
2369
+                'etag' => $query->createNamedParameter(md5($objectData)),
2370
+                'size' => $query->createNamedParameter(strlen($objectData))
2371
+            ])
2372
+            ->executeStatement();
2373
+    }
2374
+
2375
+    /**
2376
+     * Adds a change record to the calendarchanges table.
2377
+     *
2378
+     * @param mixed $calendarId
2379
+     * @param string $objectUri
2380
+     * @param int $operation 1 = add, 2 = modify, 3 = delete.
2381
+     * @param int $calendarType
2382
+     * @return void
2383
+     */
2384
+    protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2385
+        $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2386
+
2387
+        $query = $this->db->getQueryBuilder();
2388
+        $query->select('synctoken')
2389
+            ->from($table)
2390
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2391
+        $result = $query->executeQuery();
2392
+        $syncToken = (int)$result->fetchOne();
2393
+        $result->closeCursor();
2394
+
2395
+        $query = $this->db->getQueryBuilder();
2396
+        $query->insert('calendarchanges')
2397
+            ->values([
2398
+                'uri' => $query->createNamedParameter($objectUri),
2399
+                'synctoken' => $query->createNamedParameter($syncToken),
2400
+                'calendarid' => $query->createNamedParameter($calendarId),
2401
+                'operation' => $query->createNamedParameter($operation),
2402
+                'calendartype' => $query->createNamedParameter($calendarType),
2403
+            ])
2404
+            ->executeStatement();
2405
+
2406
+        $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2407
+        $stmt->execute([
2408
+            $calendarId
2409
+        ]);
2410
+    }
2411
+
2412
+    /**
2413
+     * Parses some information from calendar objects, used for optimized
2414
+     * calendar-queries.
2415
+     *
2416
+     * Returns an array with the following keys:
2417
+     *   * etag - An md5 checksum of the object without the quotes.
2418
+     *   * size - Size of the object in bytes
2419
+     *   * componentType - VEVENT, VTODO or VJOURNAL
2420
+     *   * firstOccurence
2421
+     *   * lastOccurence
2422
+     *   * uid - value of the UID property
2423
+     *
2424
+     * @param string $calendarData
2425
+     * @return array
2426
+     */
2427
+    public function getDenormalizedData($calendarData) {
2428
+        $vObject = Reader::read($calendarData);
2429
+        $vEvents = [];
2430
+        $componentType = null;
2431
+        $component = null;
2432
+        $firstOccurrence = null;
2433
+        $lastOccurrence = null;
2434
+        $uid = null;
2435
+        $classification = self::CLASSIFICATION_PUBLIC;
2436
+        $hasDTSTART = false;
2437
+        foreach ($vObject->getComponents() as $component) {
2438
+            if ($component->name !== 'VTIMEZONE') {
2439
+                // Finding all VEVENTs, and track them
2440
+                if ($component->name === 'VEVENT') {
2441
+                    array_push($vEvents, $component);
2442
+                    if ($component->DTSTART) {
2443
+                        $hasDTSTART = true;
2444
+                    }
2445
+                }
2446
+                // Track first component type and uid
2447
+                if ($uid === null) {
2448
+                    $componentType = $component->name;
2449
+                    $uid = (string)$component->UID;
2450
+                }
2451
+            }
2452
+        }
2453
+        if (!$componentType) {
2454
+            throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2455
+        }
2456
+
2457
+        if ($hasDTSTART) {
2458
+            $component = $vEvents[0];
2459
+
2460
+            // Finding the last occurrence is a bit harder
2461
+            if (!isset($component->RRULE) && count($vEvents) === 1) {
2462
+                $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2463
+                if (isset($component->DTEND)) {
2464
+                    $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2465
+                } elseif (isset($component->DURATION)) {
2466
+                    $endDate = clone $component->DTSTART->getDateTime();
2467
+                    $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2468
+                    $lastOccurrence = $endDate->getTimeStamp();
2469
+                } elseif (!$component->DTSTART->hasTime()) {
2470
+                    $endDate = clone $component->DTSTART->getDateTime();
2471
+                    $endDate->modify('+1 day');
2472
+                    $lastOccurrence = $endDate->getTimeStamp();
2473
+                } else {
2474
+                    $lastOccurrence = $firstOccurrence;
2475
+                }
2476
+            } else {
2477
+                $it = new EventIterator($vEvents);
2478
+                $maxDate = new DateTime(self::MAX_DATE);
2479
+                $firstOccurrence = $it->getDtStart()->getTimestamp();
2480
+                if ($it->isInfinite()) {
2481
+                    $lastOccurrence = $maxDate->getTimestamp();
2482
+                } else {
2483
+                    $end = $it->getDtEnd();
2484
+                    while ($it->valid() && $end < $maxDate) {
2485
+                        $end = $it->getDtEnd();
2486
+                        $it->next();
2487
+                    }
2488
+                    $lastOccurrence = $end->getTimestamp();
2489
+                }
2490
+            }
2491
+        }
2492
+
2493
+        if ($component->CLASS) {
2494
+            $classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2495
+            switch ($component->CLASS->getValue()) {
2496
+                case 'PUBLIC':
2497
+                    $classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2498
+                    break;
2499
+                case 'CONFIDENTIAL':
2500
+                    $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2501
+                    break;
2502
+            }
2503
+        }
2504
+        return [
2505
+            'etag' => md5($calendarData),
2506
+            'size' => strlen($calendarData),
2507
+            'componentType' => $componentType,
2508
+            'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2509
+            'lastOccurence' => $lastOccurrence,
2510
+            'uid' => $uid,
2511
+            'classification' => $classification
2512
+        ];
2513
+    }
2514
+
2515
+    /**
2516
+     * @param $cardData
2517
+     * @return bool|string
2518
+     */
2519
+    private function readBlob($cardData) {
2520
+        if (is_resource($cardData)) {
2521
+            return stream_get_contents($cardData);
2522
+        }
2523
+
2524
+        return $cardData;
2525
+    }
2526
+
2527
+    /**
2528
+     * @param IShareable $shareable
2529
+     * @param array $add
2530
+     * @param array $remove
2531
+     */
2532
+    public function updateShares($shareable, $add, $remove) {
2533
+        $calendarId = $shareable->getResourceId();
2534
+        $calendarRow = $this->getCalendarById($calendarId);
2535
+        $oldShares = $this->getShares($calendarId);
2536
+
2537
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2538
+            '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2539
+            [
2540
+                'calendarId' => $calendarId,
2541
+                'calendarData' => $calendarRow,
2542
+                'shares' => $oldShares,
2543
+                'add' => $add,
2544
+                'remove' => $remove,
2545
+            ]));
2546
+        $this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2547
+
2548
+        $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2549
+    }
2550
+
2551
+    /**
2552
+     * @param int $resourceId
2553
+     * @param int $calendarType
2554
+     * @return array
2555
+     */
2556
+    public function getShares($resourceId, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2557
+        return $this->calendarSharingBackend->getShares($resourceId);
2558
+    }
2559
+
2560
+    /**
2561
+     * @param boolean $value
2562
+     * @param \OCA\DAV\CalDAV\Calendar $calendar
2563
+     * @return string|null
2564
+     */
2565
+    public function setPublishStatus($value, $calendar) {
2566
+        $calendarId = $calendar->getResourceId();
2567
+        $calendarData = $this->getCalendarById($calendarId);
2568
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
2569
+            '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2570
+            [
2571
+                'calendarId' => $calendarId,
2572
+                'calendarData' => $calendarData,
2573
+                'public' => $value,
2574
+            ]));
2575
+
2576
+        $query = $this->db->getQueryBuilder();
2577
+        if ($value) {
2578
+            $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2579
+            $query->insert('dav_shares')
2580
+                ->values([
2581
+                    'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2582
+                    'type' => $query->createNamedParameter('calendar'),
2583
+                    'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2584
+                    'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2585
+                    'publicuri' => $query->createNamedParameter($publicUri)
2586
+                ]);
2587
+            $query->executeStatement();
2588
+
2589
+            $this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2590
+            return $publicUri;
2591
+        }
2592
+        $query->delete('dav_shares')
2593
+            ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2594
+            ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2595
+        $query->executeStatement();
2596
+
2597
+        $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2598
+        return null;
2599
+    }
2600
+
2601
+    /**
2602
+     * @param \OCA\DAV\CalDAV\Calendar $calendar
2603
+     * @return mixed
2604
+     */
2605
+    public function getPublishStatus($calendar) {
2606
+        $query = $this->db->getQueryBuilder();
2607
+        $result = $query->select('publicuri')
2608
+            ->from('dav_shares')
2609
+            ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2610
+            ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2611
+            ->executeQuery();
2612
+
2613
+        $row = $result->fetch();
2614
+        $result->closeCursor();
2615
+        return $row ? reset($row) : false;
2616
+    }
2617
+
2618
+    /**
2619
+     * @param int $resourceId
2620
+     * @param array $acl
2621
+     * @return array
2622
+     */
2623
+    public function applyShareAcl($resourceId, $acl) {
2624
+        return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2625
+    }
2626
+
2627
+
2628
+
2629
+    /**
2630
+     * update properties table
2631
+     *
2632
+     * @param int $calendarId
2633
+     * @param string $objectUri
2634
+     * @param string $calendarData
2635
+     * @param int $calendarType
2636
+     */
2637
+    public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2638
+        $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2639
+
2640
+        try {
2641
+            $vCalendar = $this->readCalendarData($calendarData);
2642
+        } catch (\Exception $ex) {
2643
+            return;
2644
+        }
2645
+
2646
+        $this->purgeProperties($calendarId, $objectId);
2647
+
2648
+        $query = $this->db->getQueryBuilder();
2649
+        $query->insert($this->dbObjectPropertiesTable)
2650
+            ->values(
2651
+                [
2652
+                    'calendarid' => $query->createNamedParameter($calendarId),
2653
+                    'calendartype' => $query->createNamedParameter($calendarType),
2654
+                    'objectid' => $query->createNamedParameter($objectId),
2655
+                    'name' => $query->createParameter('name'),
2656
+                    'parameter' => $query->createParameter('parameter'),
2657
+                    'value' => $query->createParameter('value'),
2658
+                ]
2659
+            );
2660
+
2661
+        $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2662
+        foreach ($vCalendar->getComponents() as $component) {
2663
+            if (!in_array($component->name, $indexComponents)) {
2664
+                continue;
2665
+            }
2666
+
2667
+            foreach ($component->children() as $property) {
2668
+                if (in_array($property->name, self::$indexProperties)) {
2669
+                    $value = $property->getValue();
2670
+                    // is this a shitty db?
2671
+                    if (!$this->db->supports4ByteText()) {
2672
+                        $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2673
+                    }
2674
+                    $value = mb_strcut($value, 0, 254);
2675
+
2676
+                    $query->setParameter('name', $property->name);
2677
+                    $query->setParameter('parameter', null);
2678
+                    $query->setParameter('value', $value);
2679
+                    $query->executeStatement();
2680
+                }
2681
+
2682
+                if (array_key_exists($property->name, self::$indexParameters)) {
2683
+                    $parameters = $property->parameters();
2684
+                    $indexedParametersForProperty = self::$indexParameters[$property->name];
2685
+
2686
+                    foreach ($parameters as $key => $value) {
2687
+                        if (in_array($key, $indexedParametersForProperty)) {
2688
+                            // is this a shitty db?
2689
+                            if ($this->db->supports4ByteText()) {
2690
+                                $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2691
+                            }
2692
+
2693
+                            $query->setParameter('name', $property->name);
2694
+                            $query->setParameter('parameter', mb_strcut($key, 0, 254));
2695
+                            $query->setParameter('value', mb_strcut($value, 0, 254));
2696
+                            $query->executeStatement();
2697
+                        }
2698
+                    }
2699
+                }
2700
+            }
2701
+        }
2702
+    }
2703
+
2704
+    /**
2705
+     * deletes all birthday calendars
2706
+     */
2707
+    public function deleteAllBirthdayCalendars() {
2708
+        $query = $this->db->getQueryBuilder();
2709
+        $result = $query->select(['id'])->from('calendars')
2710
+            ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
2711
+            ->executeQuery();
2712
+
2713
+        $ids = $result->fetchAll();
2714
+        foreach ($ids as $id) {
2715
+            $this->deleteCalendar($id['id']);
2716
+        }
2717
+    }
2718
+
2719
+    /**
2720
+     * @param $subscriptionId
2721
+     */
2722
+    public function purgeAllCachedEventsForSubscription($subscriptionId) {
2723
+        $query = $this->db->getQueryBuilder();
2724
+        $query->select('uri')
2725
+            ->from('calendarobjects')
2726
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2727
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2728
+        $stmt = $query->executeQuery();
2729
+
2730
+        $uris = [];
2731
+        foreach ($stmt->fetchAll() as $row) {
2732
+            $uris[] = $row['uri'];
2733
+        }
2734
+        $stmt->closeCursor();
2735
+
2736
+        $query = $this->db->getQueryBuilder();
2737
+        $query->delete('calendarobjects')
2738
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2739
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2740
+            ->executeStatement();
2741
+
2742
+        $query->delete('calendarchanges')
2743
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2744
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2745
+            ->executeStatement();
2746
+
2747
+        $query->delete($this->dbObjectPropertiesTable)
2748
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2749
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2750
+            ->executeStatement();
2751
+
2752
+        foreach ($uris as $uri) {
2753
+            $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
2754
+        }
2755
+    }
2756
+
2757
+    /**
2758
+     * Move a calendar from one user to another
2759
+     *
2760
+     * @param string $uriName
2761
+     * @param string $uriOrigin
2762
+     * @param string $uriDestination
2763
+     * @param string $newUriName (optional) the new uriName
2764
+     */
2765
+    public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
2766
+        $query = $this->db->getQueryBuilder();
2767
+        $query->update('calendars')
2768
+            ->set('principaluri', $query->createNamedParameter($uriDestination))
2769
+            ->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
2770
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
2771
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
2772
+            ->executeStatement();
2773
+    }
2774
+
2775
+    /**
2776
+     * read VCalendar data into a VCalendar object
2777
+     *
2778
+     * @param string $objectData
2779
+     * @return VCalendar
2780
+     */
2781
+    protected function readCalendarData($objectData) {
2782
+        return Reader::read($objectData);
2783
+    }
2784
+
2785
+    /**
2786
+     * delete all properties from a given calendar object
2787
+     *
2788
+     * @param int $calendarId
2789
+     * @param int $objectId
2790
+     */
2791
+    protected function purgeProperties($calendarId, $objectId) {
2792
+        $query = $this->db->getQueryBuilder();
2793
+        $query->delete($this->dbObjectPropertiesTable)
2794
+            ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
2795
+            ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2796
+        $query->executeStatement();
2797
+    }
2798
+
2799
+    /**
2800
+     * get ID from a given calendar object
2801
+     *
2802
+     * @param int $calendarId
2803
+     * @param string $uri
2804
+     * @param int $calendarType
2805
+     * @return int
2806
+     */
2807
+    protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
2808
+        $query = $this->db->getQueryBuilder();
2809
+        $query->select('id')
2810
+            ->from('calendarobjects')
2811
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
2812
+            ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
2813
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
2814
+
2815
+        $result = $query->executeQuery();
2816
+        $objectIds = $result->fetch();
2817
+        $result->closeCursor();
2818
+
2819
+        if (!isset($objectIds['id'])) {
2820
+            throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2821
+        }
2822
+
2823
+        return (int)$objectIds['id'];
2824
+    }
2825
+
2826
+    /**
2827
+     * return legacy endpoint principal name to new principal name
2828
+     *
2829
+     * @param $principalUri
2830
+     * @param $toV2
2831
+     * @return string
2832
+     */
2833
+    private function convertPrincipal($principalUri, $toV2) {
2834
+        if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
2835
+            [, $name] = Uri\split($principalUri);
2836
+            if ($toV2 === true) {
2837
+                return "principals/users/$name";
2838
+            }
2839
+            return "principals/$name";
2840
+        }
2841
+        return $principalUri;
2842
+    }
2843
+
2844
+    /**
2845
+     * adds information about an owner to the calendar data
2846
+     *
2847
+     * @param $calendarInfo
2848
+     */
2849
+    private function addOwnerPrincipal(&$calendarInfo) {
2850
+        $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2851
+        $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2852
+        if (isset($calendarInfo[$ownerPrincipalKey])) {
2853
+            $uri = $calendarInfo[$ownerPrincipalKey];
2854
+        } else {
2855
+            $uri = $calendarInfo['principaluri'];
2856
+        }
2857
+
2858
+        $principalInformation = $this->principalBackend->getPrincipalByPath($uri);
2859
+        if (isset($principalInformation['{DAV:}displayname'])) {
2860
+            $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
2861
+        }
2862
+    }
2863 2863
 }
Please login to merge, or discard this patch.
Spacing   +112 added lines, -112 removed lines patch added patch discarded remove patch
@@ -256,7 +256,7 @@  discard block
 block discarded – undo
256 256
 		}
257 257
 
258 258
 		$result = $query->executeQuery();
259
-		$column = (int)$result->fetchOne();
259
+		$column = (int) $result->fetchOne();
260 260
 		$result->closeCursor();
261 261
 		return $column;
262 262
 	}
@@ -316,18 +316,18 @@  discard block
 block discarded – undo
316 316
 			$row['principaluri'] = (string) $row['principaluri'];
317 317
 			$components = [];
318 318
 			if ($row['components']) {
319
-				$components = explode(',',$row['components']);
319
+				$components = explode(',', $row['components']);
320 320
 			}
321 321
 
322 322
 			$calendar = [
323 323
 				'id' => $row['id'],
324 324
 				'uri' => $row['uri'],
325 325
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
326
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
327
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
328
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
329
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
330
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
326
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
327
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
328
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
329
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
330
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
331 331
 			];
332 332
 
333 333
 			foreach ($this->propertyMap as $xmlName => $dbName) {
@@ -367,7 +367,7 @@  discard block
 block discarded – undo
367 367
 
368 368
 		$result = $query->executeQuery();
369 369
 
370
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
370
+		$readOnlyPropertyName = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only';
371 371
 		while ($row = $result->fetch()) {
372 372
 			$row['principaluri'] = (string) $row['principaluri'];
373 373
 			if ($row['principaluri'] === $principalUri) {
@@ -388,21 +388,21 @@  discard block
 block discarded – undo
388 388
 			}
389 389
 
390 390
 			[, $name] = Uri\split($row['principaluri']);
391
-			$uri = $row['uri'] . '_shared_by_' . $name;
392
-			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
391
+			$uri = $row['uri'].'_shared_by_'.$name;
392
+			$row['displayname'] = $row['displayname'].' ('.$this->getUserDisplayName($name).')';
393 393
 			$components = [];
394 394
 			if ($row['components']) {
395
-				$components = explode(',',$row['components']);
395
+				$components = explode(',', $row['components']);
396 396
 			}
397 397
 			$calendar = [
398 398
 				'id' => $row['id'],
399 399
 				'uri' => $uri,
400 400
 				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
401
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
402
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
403
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
404
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
405
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
401
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
402
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
403
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
404
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
405
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
406 406
 				$readOnlyPropertyName => $readOnly,
407 407
 			];
408 408
 
@@ -443,16 +443,16 @@  discard block
 block discarded – undo
443 443
 			$row['principaluri'] = (string) $row['principaluri'];
444 444
 			$components = [];
445 445
 			if ($row['components']) {
446
-				$components = explode(',',$row['components']);
446
+				$components = explode(',', $row['components']);
447 447
 			}
448 448
 			$calendar = [
449 449
 				'id' => $row['id'],
450 450
 				'uri' => $row['uri'],
451 451
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
452
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
453
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
454
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
455
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
452
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
453
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
454
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
455
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
456 456
 			];
457 457
 			foreach ($this->propertyMap as $xmlName => $dbName) {
458 458
 				$calendar[$xmlName] = $row[$dbName];
@@ -512,22 +512,22 @@  discard block
 block discarded – undo
512 512
 		while ($row = $result->fetch()) {
513 513
 			$row['principaluri'] = (string) $row['principaluri'];
514 514
 			[, $name] = Uri\split($row['principaluri']);
515
-			$row['displayname'] = $row['displayname'] . "($name)";
515
+			$row['displayname'] = $row['displayname']."($name)";
516 516
 			$components = [];
517 517
 			if ($row['components']) {
518
-				$components = explode(',',$row['components']);
518
+				$components = explode(',', $row['components']);
519 519
 			}
520 520
 			$calendar = [
521 521
 				'id' => $row['id'],
522 522
 				'uri' => $row['publicuri'],
523 523
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
524
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
525
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
526
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
527
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
528
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
529
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
530
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
524
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
525
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
526
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
527
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
528
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
529
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only' => (int) $row['access'] === Backend::ACCESS_READ,
530
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}public' => (int) $row['access'] === self::ACCESS_PUBLIC,
531 531
 			];
532 532
 
533 533
 			foreach ($this->propertyMap as $xmlName => $dbName) {
@@ -574,27 +574,27 @@  discard block
 block discarded – undo
574 574
 		$result->closeCursor();
575 575
 
576 576
 		if ($row === false) {
577
-			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
577
+			throw new NotFound('Node with name \''.$uri.'\' could not be found');
578 578
 		}
579 579
 
580 580
 		$row['principaluri'] = (string) $row['principaluri'];
581 581
 		[, $name] = Uri\split($row['principaluri']);
582
-		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
582
+		$row['displayname'] = $row['displayname'].' '."($name)";
583 583
 		$components = [];
584 584
 		if ($row['components']) {
585
-			$components = explode(',',$row['components']);
585
+			$components = explode(',', $row['components']);
586 586
 		}
587 587
 		$calendar = [
588 588
 			'id' => $row['id'],
589 589
 			'uri' => $row['publicuri'],
590 590
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
591
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
592
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
593
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
594
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
595
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
596
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
597
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
591
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
592
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
593
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
594
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
595
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
596
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only' => (int) $row['access'] === Backend::ACCESS_READ,
597
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}public' => (int) $row['access'] === self::ACCESS_PUBLIC,
598 598
 		];
599 599
 
600 600
 		foreach ($this->propertyMap as $xmlName => $dbName) {
@@ -637,17 +637,17 @@  discard block
 block discarded – undo
637 637
 		$row['principaluri'] = (string) $row['principaluri'];
638 638
 		$components = [];
639 639
 		if ($row['components']) {
640
-			$components = explode(',',$row['components']);
640
+			$components = explode(',', $row['components']);
641 641
 		}
642 642
 
643 643
 		$calendar = [
644 644
 			'id' => $row['id'],
645 645
 			'uri' => $row['uri'],
646 646
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
647
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
648
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
649
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
647
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
648
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
649
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
651 651
 		];
652 652
 
653 653
 		foreach ($this->propertyMap as $xmlName => $dbName) {
@@ -688,17 +688,17 @@  discard block
 block discarded – undo
688 688
 		$row['principaluri'] = (string) $row['principaluri'];
689 689
 		$components = [];
690 690
 		if ($row['components']) {
691
-			$components = explode(',',$row['components']);
691
+			$components = explode(',', $row['components']);
692 692
 		}
693 693
 
694 694
 		$calendar = [
695 695
 			'id' => $row['id'],
696 696
 			'uri' => $row['uri'],
697 697
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
698
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
699
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
700
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
701
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
698
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
699
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
700
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
701
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
702 702
 		];
703 703
 
704 704
 		foreach ($this->propertyMap as $xmlName => $dbName) {
@@ -742,8 +742,8 @@  discard block
 block discarded – undo
742 742
 			'principaluri' => $row['principaluri'],
743 743
 			'source' => $row['source'],
744 744
 			'lastmodified' => $row['lastmodified'],
745
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
746
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
745
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
746
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
747 747
 		];
748 748
 
749 749
 		foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
@@ -780,16 +780,16 @@  discard block
 block discarded – undo
780 780
 		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
781 781
 		if (isset($properties[$sccs])) {
782 782
 			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
783
-				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
783
+				throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
784 784
 			}
785
-			$values['components'] = implode(',',$properties[$sccs]->getValue());
785
+			$values['components'] = implode(',', $properties[$sccs]->getValue());
786 786
 		} elseif (isset($properties['components'])) {
787 787
 			// Allow to provide components internally without having
788 788
 			// to create a SupportedCalendarComponentSet object
789 789
 			$values['components'] = $properties['components'];
790 790
 		}
791 791
 
792
-		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
792
+		$transp = '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp';
793 793
 		if (isset($properties[$transp])) {
794 794
 			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
795 795
 		}
@@ -809,7 +809,7 @@  discard block
 block discarded – undo
809 809
 		$calendarId = $query->getLastInsertId();
810 810
 
811 811
 		$calendarData = $this->getCalendarById($calendarId);
812
-		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
812
+		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int) $calendarId, $calendarData));
813 813
 		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
814 814
 			'\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
815 815
 			[
@@ -838,13 +838,13 @@  discard block
 block discarded – undo
838 838
 	 */
839 839
 	public function updateCalendar($calendarId, PropPatch $propPatch) {
840 840
 		$supportedProperties = array_keys($this->propertyMap);
841
-		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
841
+		$supportedProperties[] = '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp';
842 842
 
843
-		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
843
+		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
844 844
 			$newValues = [];
845 845
 			foreach ($mutations as $propertyName => $propertyValue) {
846 846
 				switch ($propertyName) {
847
-					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
847
+					case '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp':
848 848
 						$fieldName = 'transparent';
849 849
 						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
850 850
 						break;
@@ -866,7 +866,7 @@  discard block
 block discarded – undo
866 866
 
867 867
 			$calendarData = $this->getCalendarById($calendarId);
868 868
 			$shares = $this->getShares($calendarId);
869
-			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
869
+			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int) $calendarId, $calendarData, $shares, $mutations));
870 870
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
871 871
 				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
872 872
 				[
@@ -909,7 +909,7 @@  discard block
 block discarded – undo
909 909
 
910 910
 		// Only dispatch if we actually deleted anything
911 911
 		if ($calendarData) {
912
-			$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
912
+			$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int) $calendarId, $calendarData, $shares));
913 913
 		}
914 914
 	}
915 915
 
@@ -969,11 +969,11 @@  discard block
 block discarded – undo
969 969
 				'id' => $row['id'],
970 970
 				'uri' => $row['uri'],
971 971
 				'lastmodified' => $row['lastmodified'],
972
-				'etag' => '"' . $row['etag'] . '"',
972
+				'etag' => '"'.$row['etag'].'"',
973 973
 				'calendarid' => $row['calendarid'],
974
-				'size' => (int)$row['size'],
974
+				'size' => (int) $row['size'],
975 975
 				'component' => strtolower($row['componenttype']),
976
-				'classification' => (int)$row['classification']
976
+				'classification' => (int) $row['classification']
977 977
 			];
978 978
 		}
979 979
 		$stmt->closeCursor();
@@ -1017,12 +1017,12 @@  discard block
 block discarded – undo
1017 1017
 			'id' => $row['id'],
1018 1018
 			'uri' => $row['uri'],
1019 1019
 			'lastmodified' => $row['lastmodified'],
1020
-			'etag' => '"' . $row['etag'] . '"',
1020
+			'etag' => '"'.$row['etag'].'"',
1021 1021
 			'calendarid' => $row['calendarid'],
1022
-			'size' => (int)$row['size'],
1022
+			'size' => (int) $row['size'],
1023 1023
 			'calendardata' => $this->readBlob($row['calendardata']),
1024 1024
 			'component' => strtolower($row['componenttype']),
1025
-			'classification' => (int)$row['classification']
1025
+			'classification' => (int) $row['classification']
1026 1026
 		];
1027 1027
 	}
1028 1028
 
@@ -1063,12 +1063,12 @@  discard block
 block discarded – undo
1063 1063
 					'id' => $row['id'],
1064 1064
 					'uri' => $row['uri'],
1065 1065
 					'lastmodified' => $row['lastmodified'],
1066
-					'etag' => '"' . $row['etag'] . '"',
1066
+					'etag' => '"'.$row['etag'].'"',
1067 1067
 					'calendarid' => $row['calendarid'],
1068
-					'size' => (int)$row['size'],
1068
+					'size' => (int) $row['size'],
1069 1069
 					'calendardata' => $this->readBlob($row['calendardata']),
1070 1070
 					'component' => strtolower($row['componenttype']),
1071
-					'classification' => (int)$row['classification']
1071
+					'classification' => (int) $row['classification']
1072 1072
 				];
1073 1073
 			}
1074 1074
 			$result->closeCursor();
@@ -1140,7 +1140,7 @@  discard block
 block discarded – undo
1140 1140
 			$calendarRow = $this->getCalendarById($calendarId);
1141 1141
 			$shares = $this->getShares($calendarId);
1142 1142
 
1143
-			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1143
+			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int) $calendarId, $calendarRow, $shares, $objectRow));
1144 1144
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1145 1145
 				'\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1146 1146
 				[
@@ -1153,7 +1153,7 @@  discard block
 block discarded – undo
1153 1153
 		} else {
1154 1154
 			$subscriptionRow = $this->getSubscriptionById($calendarId);
1155 1155
 
1156
-			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1156
+			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int) $calendarId, $subscriptionRow, [], $objectRow));
1157 1157
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1158 1158
 				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1159 1159
 				[
@@ -1165,7 +1165,7 @@  discard block
 block discarded – undo
1165 1165
 			));
1166 1166
 		}
1167 1167
 
1168
-		return '"' . $extraData['etag'] . '"';
1168
+		return '"'.$extraData['etag'].'"';
1169 1169
 	}
1170 1170
 
1171 1171
 	/**
@@ -1214,7 +1214,7 @@  discard block
 block discarded – undo
1214 1214
 				$calendarRow = $this->getCalendarById($calendarId);
1215 1215
 				$shares = $this->getShares($calendarId);
1216 1216
 
1217
-				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1217
+				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int) $calendarId, $calendarRow, $shares, $objectRow));
1218 1218
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1219 1219
 					'\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1220 1220
 					[
@@ -1227,7 +1227,7 @@  discard block
 block discarded – undo
1227 1227
 			} else {
1228 1228
 				$subscriptionRow = $this->getSubscriptionById($calendarId);
1229 1229
 
1230
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1230
+				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int) $calendarId, $subscriptionRow, [], $objectRow));
1231 1231
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1232 1232
 					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1233 1233
 					[
@@ -1240,7 +1240,7 @@  discard block
 block discarded – undo
1240 1240
 			}
1241 1241
 		}
1242 1242
 
1243
-		return '"' . $extraData['etag'] . '"';
1243
+		return '"'.$extraData['etag'].'"';
1244 1244
 	}
1245 1245
 
1246 1246
 	/**
@@ -1277,7 +1277,7 @@  discard block
 block discarded – undo
1277 1277
 				$calendarRow = $this->getCalendarById($calendarId);
1278 1278
 				$shares = $this->getShares($calendarId);
1279 1279
 
1280
-				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1280
+				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int) $calendarId, $calendarRow, $shares, $data));
1281 1281
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1282 1282
 					'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1283 1283
 					[
@@ -1290,7 +1290,7 @@  discard block
 block discarded – undo
1290 1290
 			} else {
1291 1291
 				$subscriptionRow = $this->getSubscriptionById($calendarId);
1292 1292
 
1293
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1293
+				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int) $calendarId, $subscriptionRow, [], $data));
1294 1294
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1295 1295
 					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1296 1296
 					[
@@ -1562,7 +1562,7 @@  discard block
 block discarded – undo
1562 1562
 
1563 1563
 		$result = [];
1564 1564
 		while ($row = $stmt->fetch()) {
1565
-			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1565
+			$path = $uriMapper[$row['calendarid']].'/'.$row['uri'];
1566 1566
 			if (!in_array($path, $result)) {
1567 1567
 				$result[] = $path;
1568 1568
 			}
@@ -1610,8 +1610,8 @@  discard block
 block discarded – undo
1610 1610
 
1611 1611
 		if ($pattern !== '') {
1612 1612
 			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1613
-				$outerQuery->createNamedParameter('%' .
1614
-					$this->db->escapeLikeParameter($pattern) . '%')));
1613
+				$outerQuery->createNamedParameter('%'.
1614
+					$this->db->escapeLikeParameter($pattern).'%')));
1615 1615
 		}
1616 1616
 
1617 1617
 		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
@@ -1650,7 +1650,7 @@  discard block
 block discarded – undo
1650 1650
 		$result = $outerQuery->executeQuery();
1651 1651
 		$calendarObjects = $result->fetchAll();
1652 1652
 
1653
-		return array_map(function ($o) {
1653
+		return array_map(function($o) {
1654 1654
 			$calendarData = Reader::read($o['calendardata']);
1655 1655
 			$comps = $calendarData->getComponents();
1656 1656
 			$objects = [];
@@ -1668,10 +1668,10 @@  discard block
 block discarded – undo
1668 1668
 				'type' => $o['componenttype'],
1669 1669
 				'uid' => $o['uid'],
1670 1670
 				'uri' => $o['uri'],
1671
-				'objects' => array_map(function ($c) {
1671
+				'objects' => array_map(function($c) {
1672 1672
 					return $this->transformSearchData($c);
1673 1673
 				}, $objects),
1674
-				'timezones' => array_map(function ($c) {
1674
+				'timezones' => array_map(function($c) {
1675 1675
 					return $this->transformSearchData($c);
1676 1676
 				}, $timezones),
1677 1677
 			];
@@ -1687,7 +1687,7 @@  discard block
 block discarded – undo
1687 1687
 		/** @var Component[] $subComponents */
1688 1688
 		$subComponents = $comp->getComponents();
1689 1689
 		/** @var Property[] $properties */
1690
-		$properties = array_filter($comp->children(), function ($c) {
1690
+		$properties = array_filter($comp->children(), function($c) {
1691 1691
 			return $c instanceof Property;
1692 1692
 		});
1693 1693
 		$validationRules = $comp->getValidationRules();
@@ -1765,7 +1765,7 @@  discard block
 block discarded – undo
1765 1765
 		$subscriptions = $this->getSubscriptionsForUser($principalUri);
1766 1766
 		foreach ($calendars as $calendar) {
1767 1767
 			$calendarAnd = $calendarObjectIdQuery->expr()->andX();
1768
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
1768
+			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int) $calendar['id'])));
1769 1769
 			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1770 1770
 
1771 1771
 			// If it's shared, limit search to public events
@@ -1778,7 +1778,7 @@  discard block
 block discarded – undo
1778 1778
 		}
1779 1779
 		foreach ($subscriptions as $subscription) {
1780 1780
 			$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
1781
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
1781
+			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int) $subscription['id'])));
1782 1782
 			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
1783 1783
 
1784 1784
 			// If it's shared, limit search to public events
@@ -1823,7 +1823,7 @@  discard block
 block discarded – undo
1823 1823
 			if (!$escapePattern) {
1824 1824
 				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
1825 1825
 			} else {
1826
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1826
+				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%'.$this->db->escapeLikeParameter($pattern).'%')));
1827 1827
 			}
1828 1828
 		}
1829 1829
 
@@ -1837,7 +1837,7 @@  discard block
 block discarded – undo
1837 1837
 		$result = $calendarObjectIdQuery->executeQuery();
1838 1838
 		$matches = $result->fetchAll();
1839 1839
 		$result->closeCursor();
1840
-		$matches = array_map(static function (array $match):int {
1840
+		$matches = array_map(static function(array $match):int {
1841 1841
 			return (int) $match['objectid'];
1842 1842
 		}, $matches);
1843 1843
 
@@ -1850,9 +1850,9 @@  discard block
 block discarded – undo
1850 1850
 		$calendarObjects = $result->fetchAll();
1851 1851
 		$result->closeCursor();
1852 1852
 
1853
-		return array_map(function (array $array): array {
1854
-			$array['calendarid'] = (int)$array['calendarid'];
1855
-			$array['calendartype'] = (int)$array['calendartype'];
1853
+		return array_map(function(array $array): array {
1854
+			$array['calendarid'] = (int) $array['calendarid'];
1855
+			$array['calendartype'] = (int) $array['calendartype'];
1856 1856
 			$array['calendardata'] = $this->readBlob($array['calendardata']);
1857 1857
 
1858 1858
 			return $array;
@@ -1890,7 +1890,7 @@  discard block
 block discarded – undo
1890 1890
 		$row = $stmt->fetch();
1891 1891
 		$stmt->closeCursor();
1892 1892
 		if ($row) {
1893
-			return $row['calendaruri'] . '/' . $row['objecturi'];
1893
+			return $row['calendaruri'].'/'.$row['objecturi'];
1894 1894
 		}
1895 1895
 
1896 1896
 		return null;
@@ -2091,8 +2091,8 @@  discard block
 block discarded – undo
2091 2091
 				'source' => $row['source'],
2092 2092
 				'lastmodified' => $row['lastmodified'],
2093 2093
 
2094
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2095
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2094
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2095
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
2096 2096
 			];
2097 2097
 
2098 2098
 			foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
@@ -2187,7 +2187,7 @@  discard block
 block discarded – undo
2187 2187
 		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2188 2188
 		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2189 2189
 
2190
-		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2190
+		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
2191 2191
 			$newValues = [];
2192 2192
 
2193 2193
 			foreach ($mutations as $propertyName => $propertyValue) {
@@ -2209,7 +2209,7 @@  discard block
 block discarded – undo
2209 2209
 				->executeStatement();
2210 2210
 
2211 2211
 			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2212
-			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2212
+			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int) $subscriptionId, $subscriptionRow, [], $mutations));
2213 2213
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2214 2214
 				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2215 2215
 				[
@@ -2260,7 +2260,7 @@  discard block
 block discarded – undo
2260 2260
 			->executeStatement();
2261 2261
 
2262 2262
 		if ($subscriptionRow) {
2263
-			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2263
+			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int) $subscriptionId, $subscriptionRow, []));
2264 2264
 		}
2265 2265
 	}
2266 2266
 
@@ -2298,8 +2298,8 @@  discard block
 block discarded – undo
2298 2298
 			'uri' => $row['uri'],
2299 2299
 			'calendardata' => $row['calendardata'],
2300 2300
 			'lastmodified' => $row['lastmodified'],
2301
-			'etag' => '"' . $row['etag'] . '"',
2302
-			'size' => (int)$row['size'],
2301
+			'etag' => '"'.$row['etag'].'"',
2302
+			'size' => (int) $row['size'],
2303 2303
 		];
2304 2304
 	}
2305 2305
 
@@ -2327,8 +2327,8 @@  discard block
 block discarded – undo
2327 2327
 				'calendardata' => $row['calendardata'],
2328 2328
 				'uri' => $row['uri'],
2329 2329
 				'lastmodified' => $row['lastmodified'],
2330
-				'etag' => '"' . $row['etag'] . '"',
2331
-				'size' => (int)$row['size'],
2330
+				'etag' => '"'.$row['etag'].'"',
2331
+				'size' => (int) $row['size'],
2332 2332
 			];
2333 2333
 		}
2334 2334
 
@@ -2382,14 +2382,14 @@  discard block
 block discarded – undo
2382 2382
 	 * @return void
2383 2383
 	 */
2384 2384
 	protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2385
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2385
+		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars' : 'calendarsubscriptions';
2386 2386
 
2387 2387
 		$query = $this->db->getQueryBuilder();
2388 2388
 		$query->select('synctoken')
2389 2389
 			->from($table)
2390 2390
 			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2391 2391
 		$result = $query->executeQuery();
2392
-		$syncToken = (int)$result->fetchOne();
2392
+		$syncToken = (int) $result->fetchOne();
2393 2393
 		$result->closeCursor();
2394 2394
 
2395 2395
 		$query = $this->db->getQueryBuilder();
@@ -2446,7 +2446,7 @@  discard block
 block discarded – undo
2446 2446
 				// Track first component type and uid
2447 2447
 				if ($uid === null) {
2448 2448
 					$componentType = $component->name;
2449
-					$uid = (string)$component->UID;
2449
+					$uid = (string) $component->UID;
2450 2450
 				}
2451 2451
 			}
2452 2452
 		}
@@ -2545,7 +2545,7 @@  discard block
 block discarded – undo
2545 2545
 			]));
2546 2546
 		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2547 2547
 
2548
-		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2548
+		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int) $calendarId, $calendarRow, $oldShares, $add, $remove));
2549 2549
 	}
2550 2550
 
2551 2551
 	/**
@@ -2586,7 +2586,7 @@  discard block
 block discarded – undo
2586 2586
 				]);
2587 2587
 			$query->executeStatement();
2588 2588
 
2589
-			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2589
+			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int) $calendarId, $calendarData, $publicUri));
2590 2590
 			return $publicUri;
2591 2591
 		}
2592 2592
 		$query->delete('dav_shares')
@@ -2594,7 +2594,7 @@  discard block
 block discarded – undo
2594 2594
 			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2595 2595
 		$query->executeStatement();
2596 2596
 
2597
-		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2597
+		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int) $calendarId, $calendarData));
2598 2598
 		return null;
2599 2599
 	}
2600 2600
 
@@ -2817,10 +2817,10 @@  discard block
 block discarded – undo
2817 2817
 		$result->closeCursor();
2818 2818
 
2819 2819
 		if (!isset($objectIds['id'])) {
2820
-			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2820
+			throw new \InvalidArgumentException('Calendarobject does not exists: '.$uri);
2821 2821
 		}
2822 2822
 
2823
-		return (int)$objectIds['id'];
2823
+		return (int) $objectIds['id'];
2824 2824
 	}
2825 2825
 
2826 2826
 	/**
@@ -2847,8 +2847,8 @@  discard block
 block discarded – undo
2847 2847
 	 * @param $calendarInfo
2848 2848
 	 */
2849 2849
 	private function addOwnerPrincipal(&$calendarInfo) {
2850
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2851
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2850
+		$ownerPrincipalKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal';
2851
+		$displaynameKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}owner-displayname';
2852 2852
 		if (isset($calendarInfo[$ownerPrincipalKey])) {
2853 2853
 			$uri = $calendarInfo[$ownerPrincipalKey];
2854 2854
 		} else {
Please login to merge, or discard this patch.
apps/files_trashbin/lib/Trashbin.php 2 patches
Indentation   +1063 added lines, -1063 removed lines patch added patch discarded remove patch
@@ -64,1067 +64,1067 @@
 block discarded – undo
64 64
 
65 65
 class Trashbin {
66 66
 
67
-	// unit: percentage; 50% of available disk space/quota
68
-	public const DEFAULTMAXSIZE = 50;
69
-
70
-	/**
71
-	 * Whether versions have already be rescanned during this PHP request
72
-	 *
73
-	 * @var bool
74
-	 */
75
-	private static $scannedVersions = false;
76
-
77
-	/**
78
-	 * Ensure we don't need to scan the file during the move to trash
79
-	 * by triggering the scan in the pre-hook
80
-	 *
81
-	 * @param array $params
82
-	 */
83
-	public static function ensureFileScannedHook($params) {
84
-		try {
85
-			self::getUidAndFilename($params['path']);
86
-		} catch (NotFoundException $e) {
87
-			// nothing to scan for non existing files
88
-		}
89
-	}
90
-
91
-	/**
92
-	 * get the UID of the owner of the file and the path to the file relative to
93
-	 * owners files folder
94
-	 *
95
-	 * @param string $filename
96
-	 * @return array
97
-	 * @throws \OC\User\NoUserException
98
-	 */
99
-	public static function getUidAndFilename($filename) {
100
-		$uid = Filesystem::getOwner($filename);
101
-		$userManager = \OC::$server->getUserManager();
102
-		// if the user with the UID doesn't exists, e.g. because the UID points
103
-		// to a remote user with a federated cloud ID we use the current logged-in
104
-		// user. We need a valid local user to move the file to the right trash bin
105
-		if (!$userManager->userExists($uid)) {
106
-			$uid = User::getUser();
107
-		}
108
-		if (!$uid) {
109
-			// no owner, usually because of share link from ext storage
110
-			return [null, null];
111
-		}
112
-		Filesystem::initMountPoints($uid);
113
-		if ($uid !== User::getUser()) {
114
-			$info = Filesystem::getFileInfo($filename);
115
-			$ownerView = new View('/' . $uid . '/files');
116
-			try {
117
-				$filename = $ownerView->getPath($info['fileid']);
118
-			} catch (NotFoundException $e) {
119
-				$filename = null;
120
-			}
121
-		}
122
-		return [$uid, $filename];
123
-	}
124
-
125
-	/**
126
-	 * get original location of files for user
127
-	 *
128
-	 * @param string $user
129
-	 * @return array (filename => array (timestamp => original location))
130
-	 */
131
-	public static function getLocations($user) {
132
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
133
-		$query->select('id', 'timestamp', 'location')
134
-			->from('files_trash')
135
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)));
136
-		$result = $query->executeQuery();
137
-		$array = [];
138
-		while ($row = $result->fetch()) {
139
-			if (isset($array[$row['id']])) {
140
-				$array[$row['id']][$row['timestamp']] = $row['location'];
141
-			} else {
142
-				$array[$row['id']] = [$row['timestamp'] => $row['location']];
143
-			}
144
-		}
145
-		$result->closeCursor();
146
-		return $array;
147
-	}
148
-
149
-	/**
150
-	 * get original location of file
151
-	 *
152
-	 * @param string $user
153
-	 * @param string $filename
154
-	 * @param string $timestamp
155
-	 * @return string original location
156
-	 */
157
-	public static function getLocation($user, $filename, $timestamp) {
158
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
159
-		$query->select('location')
160
-			->from('files_trash')
161
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)))
162
-			->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
163
-			->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
164
-
165
-		$result = $query->executeQuery();
166
-		$row = $result->fetch();
167
-		$result->closeCursor();
168
-
169
-		if (isset($row['location'])) {
170
-			return $row['location'];
171
-		} else {
172
-			return false;
173
-		}
174
-	}
175
-
176
-	private static function setUpTrash($user) {
177
-		$view = new View('/' . $user);
178
-		if (!$view->is_dir('files_trashbin')) {
179
-			$view->mkdir('files_trashbin');
180
-		}
181
-		if (!$view->is_dir('files_trashbin/files')) {
182
-			$view->mkdir('files_trashbin/files');
183
-		}
184
-		if (!$view->is_dir('files_trashbin/versions')) {
185
-			$view->mkdir('files_trashbin/versions');
186
-		}
187
-		if (!$view->is_dir('files_trashbin/keys')) {
188
-			$view->mkdir('files_trashbin/keys');
189
-		}
190
-	}
191
-
192
-
193
-	/**
194
-	 * copy file to owners trash
195
-	 *
196
-	 * @param string $sourcePath
197
-	 * @param string $owner
198
-	 * @param string $targetPath
199
-	 * @param $user
200
-	 * @param integer $timestamp
201
-	 */
202
-	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
203
-		self::setUpTrash($owner);
204
-
205
-		$targetFilename = basename($targetPath);
206
-		$targetLocation = dirname($targetPath);
207
-
208
-		$sourceFilename = basename($sourcePath);
209
-
210
-		$view = new View('/');
211
-
212
-		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
213
-		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
214
-		$free = $view->free_space($target);
215
-		$isUnknownOrUnlimitedFreeSpace = $free < 0;
216
-		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
217
-		if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
218
-			self::copy_recursive($source, $target, $view);
219
-		}
220
-
221
-
222
-		if ($view->file_exists($target)) {
223
-			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
224
-			$query->insert('files_trash')
225
-				->setValue('id', $query->createNamedParameter($targetFilename))
226
-				->setValue('timestamp', $query->createNamedParameter($timestamp))
227
-				->setValue('location', $query->createNamedParameter($targetLocation))
228
-				->setValue('user', $query->createNamedParameter($user));
229
-			$result = $query->executeStatement();
230
-			if (!$result) {
231
-				\OC::$server->getLogger()->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
232
-			}
233
-		}
234
-	}
235
-
236
-
237
-	/**
238
-	 * move file to the trash bin
239
-	 *
240
-	 * @param string $file_path path to the deleted file/directory relative to the files root directory
241
-	 * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
242
-	 *
243
-	 * @return bool
244
-	 */
245
-	public static function move2trash($file_path, $ownerOnly = false) {
246
-		// get the user for which the filesystem is setup
247
-		$root = Filesystem::getRoot();
248
-		[, $user] = explode('/', $root);
249
-		[$owner, $ownerPath] = self::getUidAndFilename($file_path);
250
-
251
-		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
252
-		if (is_null($owner)) {
253
-			$owner = $user;
254
-			$ownerPath = $file_path;
255
-		}
256
-
257
-		$ownerView = new View('/' . $owner);
258
-		// file has been deleted in between
259
-		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
260
-			return true;
261
-		}
262
-
263
-		self::setUpTrash($user);
264
-		if ($owner !== $user) {
265
-			// also setup for owner
266
-			self::setUpTrash($owner);
267
-		}
268
-
269
-		$path_parts = pathinfo($ownerPath);
270
-
271
-		$filename = $path_parts['basename'];
272
-		$location = $path_parts['dirname'];
273
-		/** @var ITimeFactory $timeFactory */
274
-		$timeFactory = \OC::$server->query(ITimeFactory::class);
275
-		$timestamp = $timeFactory->getTime();
276
-
277
-		$lockingProvider = \OC::$server->getLockingProvider();
278
-
279
-		// disable proxy to prevent recursive calls
280
-		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
281
-		$gotLock = false;
282
-
283
-		while (!$gotLock) {
284
-			try {
285
-				/** @var \OC\Files\Storage\Storage $trashStorage */
286
-				[$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
287
-
288
-				$trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
289
-				$gotLock = true;
290
-			} catch (LockedException $e) {
291
-				// a file with the same name is being deleted concurrently
292
-				// nudge the timestamp a bit to resolve the conflict
293
-
294
-				$timestamp = $timestamp + 1;
295
-
296
-				$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
297
-			}
298
-		}
299
-
300
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
301
-		[$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath);
302
-
303
-
304
-		if ($trashStorage->file_exists($trashInternalPath)) {
305
-			$trashStorage->unlink($trashInternalPath);
306
-		}
307
-
308
-		$config = \OC::$server->getConfig();
309
-		$systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
310
-		$userTrashbinSize = (int)$config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1');
311
-		$configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize;
312
-		if ($configuredTrashbinSize >= 0 && $sourceStorage->filesize($sourceInternalPath) >= $configuredTrashbinSize) {
313
-			return false;
314
-		}
315
-
316
-		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
317
-
318
-		try {
319
-			$moveSuccessful = true;
320
-
321
-			// when moving within the same object store, the cache update done above is enough to move the file
322
-			if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) {
323
-				$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
324
-			}
325
-		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
326
-			$moveSuccessful = false;
327
-			if ($trashStorage->file_exists($trashInternalPath)) {
328
-				$trashStorage->unlink($trashInternalPath);
329
-			}
330
-			\OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
331
-		}
332
-
333
-		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
334
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
335
-				$sourceStorage->rmdir($sourceInternalPath);
336
-			} else {
337
-				$sourceStorage->unlink($sourceInternalPath);
338
-			}
339
-
340
-			if ($sourceStorage->file_exists($sourceInternalPath)) {
341
-				// undo the cache move
342
-				$sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
343
-			} else {
344
-				$trashStorage->getUpdater()->remove($trashInternalPath);
345
-			}
346
-			return false;
347
-		}
348
-
349
-		if ($moveSuccessful) {
350
-			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
351
-			$query->insert('files_trash')
352
-				->setValue('id', $query->createNamedParameter($filename))
353
-				->setValue('timestamp', $query->createNamedParameter($timestamp))
354
-				->setValue('location', $query->createNamedParameter($location))
355
-				->setValue('user', $query->createNamedParameter($owner));
356
-			$result = $query->executeStatement();
357
-			if (!$result) {
358
-				\OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
359
-			}
360
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
361
-				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
362
-
363
-			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
364
-
365
-			// if owner !== user we need to also add a copy to the users trash
366
-			if ($user !== $owner && $ownerOnly === false) {
367
-				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
368
-			}
369
-		}
370
-
371
-		$trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
372
-
373
-		self::scheduleExpire($user);
374
-
375
-		// if owner !== user we also need to update the owners trash size
376
-		if ($owner !== $user) {
377
-			self::scheduleExpire($owner);
378
-		}
379
-
380
-		return $moveSuccessful;
381
-	}
382
-
383
-	/**
384
-	 * Move file versions to trash so that they can be restored later
385
-	 *
386
-	 * @param string $filename of deleted file
387
-	 * @param string $owner owner user id
388
-	 * @param string $ownerPath path relative to the owner's home storage
389
-	 * @param integer $timestamp when the file was deleted
390
-	 */
391
-	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
392
-		if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
393
-			$user = User::getUser();
394
-			$rootView = new View('/');
395
-
396
-			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
397
-				if ($owner !== $user) {
398
-					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
399
-				}
400
-				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
401
-			} elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
402
-				foreach ($versions as $v) {
403
-					if ($owner !== $user) {
404
-						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
405
-					}
406
-					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
407
-				}
408
-			}
409
-		}
410
-	}
411
-
412
-	/**
413
-	 * Move a file or folder on storage level
414
-	 *
415
-	 * @param View $view
416
-	 * @param string $source
417
-	 * @param string $target
418
-	 * @return bool
419
-	 */
420
-	private static function move(View $view, $source, $target) {
421
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
422
-		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
423
-		/** @var \OC\Files\Storage\Storage $targetStorage */
424
-		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
425
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
426
-
427
-		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
428
-		if ($result) {
429
-			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
430
-		}
431
-		return $result;
432
-	}
433
-
434
-	/**
435
-	 * Copy a file or folder on storage level
436
-	 *
437
-	 * @param View $view
438
-	 * @param string $source
439
-	 * @param string $target
440
-	 * @return bool
441
-	 */
442
-	private static function copy(View $view, $source, $target) {
443
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
444
-		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
445
-		/** @var \OC\Files\Storage\Storage $targetStorage */
446
-		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
447
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
448
-
449
-		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
450
-		if ($result) {
451
-			$targetStorage->getUpdater()->update($targetInternalPath);
452
-		}
453
-		return $result;
454
-	}
455
-
456
-	/**
457
-	 * Restore a file or folder from trash bin
458
-	 *
459
-	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
460
-	 * including the timestamp suffix ".d12345678"
461
-	 * @param string $filename name of the file/folder
462
-	 * @param int $timestamp time when the file/folder was deleted
463
-	 *
464
-	 * @return bool true on success, false otherwise
465
-	 */
466
-	public static function restore($file, $filename, $timestamp) {
467
-		$user = User::getUser();
468
-		$view = new View('/' . $user);
469
-
470
-		$location = '';
471
-		if ($timestamp) {
472
-			$location = self::getLocation($user, $filename, $timestamp);
473
-			if ($location === false) {
474
-				\OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
475
-			} else {
476
-				// if location no longer exists, restore file in the root directory
477
-				if ($location !== '/' &&
478
-					(!$view->is_dir('files/' . $location) ||
479
-						!$view->isCreatable('files/' . $location))
480
-				) {
481
-					$location = '';
482
-				}
483
-			}
484
-		}
485
-
486
-		// we need a  extension in case a file/dir with the same name already exists
487
-		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
488
-
489
-		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
490
-		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
491
-		if (!$view->file_exists($source)) {
492
-			return false;
493
-		}
494
-		$mtime = $view->filemtime($source);
495
-
496
-		// restore file
497
-		if (!$view->isCreatable(dirname($target))) {
498
-			throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
499
-		}
500
-		$restoreResult = $view->rename($source, $target);
501
-
502
-		// handle the restore result
503
-		if ($restoreResult) {
504
-			$fakeRoot = $view->getRoot();
505
-			$view->chroot('/' . $user . '/files');
506
-			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
507
-			$view->chroot($fakeRoot);
508
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
509
-				'trashPath' => Filesystem::normalizePath($file)]);
510
-
511
-			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
512
-
513
-			if ($timestamp) {
514
-				$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
515
-				$query->delete('files_trash')
516
-					->where($query->expr()->eq('user', $query->createNamedParameter($user)))
517
-					->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
518
-					->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
519
-				$query->executeStatement();
520
-			}
521
-
522
-			return true;
523
-		}
524
-
525
-		return false;
526
-	}
527
-
528
-	/**
529
-	 * restore versions from trash bin
530
-	 *
531
-	 * @param View $view file view
532
-	 * @param string $file complete path to file
533
-	 * @param string $filename name of file once it was deleted
534
-	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
535
-	 * @param string $location location if file
536
-	 * @param int $timestamp deletion time
537
-	 * @return false|null
538
-	 */
539
-	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
540
-		if (\OCP\App::isEnabled('files_versions')) {
541
-			$user = User::getUser();
542
-			$rootView = new View('/');
543
-
544
-			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
545
-
546
-			[$owner, $ownerPath] = self::getUidAndFilename($target);
547
-
548
-			// file has been deleted in between
549
-			if (empty($ownerPath)) {
550
-				return false;
551
-			}
552
-
553
-			if ($timestamp) {
554
-				$versionedFile = $filename;
555
-			} else {
556
-				$versionedFile = $file;
557
-			}
558
-
559
-			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
560
-				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
561
-			} elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
562
-				foreach ($versions as $v) {
563
-					if ($timestamp) {
564
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
565
-					} else {
566
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
567
-					}
568
-				}
569
-			}
570
-		}
571
-	}
572
-
573
-	/**
574
-	 * delete all files from the trash
575
-	 */
576
-	public static function deleteAll() {
577
-		$user = User::getUser();
578
-		$userRoot = \OC::$server->getUserFolder($user)->getParent();
579
-		$view = new View('/' . $user);
580
-		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
581
-
582
-		try {
583
-			$trash = $userRoot->get('files_trashbin');
584
-		} catch (NotFoundException $e) {
585
-			return false;
586
-		}
587
-
588
-		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
589
-		$filePaths = [];
590
-		foreach ($fileInfos as $fileInfo) {
591
-			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
592
-		}
593
-		unset($fileInfos); // save memory
594
-
595
-		// Bulk PreDelete-Hook
596
-		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
597
-
598
-		// Single-File Hooks
599
-		foreach ($filePaths as $path) {
600
-			self::emitTrashbinPreDelete($path);
601
-		}
602
-
603
-		// actual file deletion
604
-		$trash->delete();
605
-
606
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
607
-		$query->delete('files_trash')
608
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)));
609
-		$query->executeStatement();
610
-
611
-		// Bulk PostDelete-Hook
612
-		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
613
-
614
-		// Single-File Hooks
615
-		foreach ($filePaths as $path) {
616
-			self::emitTrashbinPostDelete($path);
617
-		}
618
-
619
-		$trash = $userRoot->newFolder('files_trashbin');
620
-		$trash->newFolder('files');
621
-
622
-		return true;
623
-	}
624
-
625
-	/**
626
-	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
627
-	 *
628
-	 * @param string $path
629
-	 */
630
-	protected static function emitTrashbinPreDelete($path) {
631
-		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
632
-	}
633
-
634
-	/**
635
-	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
636
-	 *
637
-	 * @param string $path
638
-	 */
639
-	protected static function emitTrashbinPostDelete($path) {
640
-		\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
641
-	}
642
-
643
-	/**
644
-	 * delete file from trash bin permanently
645
-	 *
646
-	 * @param string $filename path to the file
647
-	 * @param string $user
648
-	 * @param int $timestamp of deletion time
649
-	 *
650
-	 * @return int size of deleted files
651
-	 */
652
-	public static function delete($filename, $user, $timestamp = null) {
653
-		$userRoot = \OC::$server->getUserFolder($user)->getParent();
654
-		$view = new View('/' . $user);
655
-		$size = 0;
656
-
657
-		if ($timestamp) {
658
-			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
659
-			$query->delete('files_trash')
660
-				->where($query->expr()->eq('user', $query->createNamedParameter($user)))
661
-				->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
662
-				->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
663
-			$query->executeStatement();
664
-
665
-			$file = $filename . '.d' . $timestamp;
666
-		} else {
667
-			$file = $filename;
668
-		}
669
-
670
-		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
671
-
672
-		try {
673
-			$node = $userRoot->get('/files_trashbin/files/' . $file);
674
-		} catch (NotFoundException $e) {
675
-			return $size;
676
-		}
677
-
678
-		if ($node instanceof Folder) {
679
-			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
680
-		} elseif ($node instanceof File) {
681
-			$size += $view->filesize('/files_trashbin/files/' . $file);
682
-		}
683
-
684
-		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
685
-		$node->delete();
686
-		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
687
-
688
-		return $size;
689
-	}
690
-
691
-	/**
692
-	 * @param View $view
693
-	 * @param string $file
694
-	 * @param string $filename
695
-	 * @param integer|null $timestamp
696
-	 * @param string $user
697
-	 * @return int
698
-	 */
699
-	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
700
-		$size = 0;
701
-		if (\OCP\App::isEnabled('files_versions')) {
702
-			if ($view->is_dir('files_trashbin/versions/' . $file)) {
703
-				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
704
-				$view->unlink('files_trashbin/versions/' . $file);
705
-			} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
706
-				foreach ($versions as $v) {
707
-					if ($timestamp) {
708
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
709
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
710
-					} else {
711
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
712
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
713
-					}
714
-				}
715
-			}
716
-		}
717
-		return $size;
718
-	}
719
-
720
-	/**
721
-	 * check to see whether a file exists in trashbin
722
-	 *
723
-	 * @param string $filename path to the file
724
-	 * @param int $timestamp of deletion time
725
-	 * @return bool true if file exists, otherwise false
726
-	 */
727
-	public static function file_exists($filename, $timestamp = null) {
728
-		$user = User::getUser();
729
-		$view = new View('/' . $user);
730
-
731
-		if ($timestamp) {
732
-			$filename = $filename . '.d' . $timestamp;
733
-		}
734
-
735
-		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
736
-		return $view->file_exists($target);
737
-	}
738
-
739
-	/**
740
-	 * deletes used space for trash bin in db if user was deleted
741
-	 *
742
-	 * @param string $uid id of deleted user
743
-	 * @return bool result of db delete operation
744
-	 */
745
-	public static function deleteUser($uid) {
746
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
747
-		$query->delete('files_trash')
748
-			->where($query->expr()->eq('user', $query->createNamedParameter($uid)));
749
-		return (bool) $query->executeStatement();
750
-	}
751
-
752
-	/**
753
-	 * calculate remaining free space for trash bin
754
-	 *
755
-	 * @param integer $trashbinSize current size of the trash bin
756
-	 * @param string $user
757
-	 * @return int available free space for trash bin
758
-	 */
759
-	private static function calculateFreeSpace($trashbinSize, $user) {
760
-		$config = \OC::$server->getConfig();
761
-		$userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
762
-		if ($userTrashbinSize > -1) {
763
-			return $userTrashbinSize - $trashbinSize;
764
-		}
765
-		$systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
766
-		if ($systemTrashbinSize > -1) {
767
-			return $systemTrashbinSize - $trashbinSize;
768
-		}
769
-
770
-		$softQuota = true;
771
-		$userObject = \OC::$server->getUserManager()->get($user);
772
-		if (is_null($userObject)) {
773
-			return 0;
774
-		}
775
-		$quota = $userObject->getQuota();
776
-		if ($quota === null || $quota === 'none') {
777
-			$quota = Filesystem::free_space('/');
778
-			$softQuota = false;
779
-			// inf or unknown free space
780
-			if ($quota < 0) {
781
-				$quota = PHP_INT_MAX;
782
-			}
783
-		} else {
784
-			$quota = \OCP\Util::computerFileSize($quota);
785
-		}
786
-
787
-		// calculate available space for trash bin
788
-		// subtract size of files and current trash bin size from quota
789
-		if ($softQuota) {
790
-			$userFolder = \OC::$server->getUserFolder($user);
791
-			if (is_null($userFolder)) {
792
-				return 0;
793
-			}
794
-			$free = $quota - $userFolder->getSize(false); // remaining free space for user
795
-			if ($free > 0) {
796
-				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
797
-			} else {
798
-				$availableSpace = $free - $trashbinSize;
799
-			}
800
-		} else {
801
-			$availableSpace = $quota;
802
-		}
803
-
804
-		return $availableSpace;
805
-	}
806
-
807
-	/**
808
-	 * resize trash bin if necessary after a new file was added to Nextcloud
809
-	 *
810
-	 * @param string $user user id
811
-	 */
812
-	public static function resizeTrash($user) {
813
-		$size = self::getTrashbinSize($user);
814
-
815
-		$freeSpace = self::calculateFreeSpace($size, $user);
816
-
817
-		if ($freeSpace < 0) {
818
-			self::scheduleExpire($user);
819
-		}
820
-	}
821
-
822
-	/**
823
-	 * clean up the trash bin
824
-	 *
825
-	 * @param string $user
826
-	 */
827
-	public static function expire($user) {
828
-		$trashBinSize = self::getTrashbinSize($user);
829
-		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
830
-
831
-		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
832
-
833
-		// delete all files older then $retention_obligation
834
-		[$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
835
-
836
-		$availableSpace += $delSize;
837
-
838
-		// delete files from trash until we meet the trash bin size limit again
839
-		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
840
-	}
841
-
842
-	/**
843
-	 * @param string $user
844
-	 */
845
-	private static function scheduleExpire($user) {
846
-		// let the admin disable auto expire
847
-		/** @var Application $application */
848
-		$application = \OC::$server->query(Application::class);
849
-		$expiration = $application->getContainer()->query('Expiration');
850
-		if ($expiration->isEnabled()) {
851
-			\OC::$server->getCommandBus()->push(new Expire($user));
852
-		}
853
-	}
854
-
855
-	/**
856
-	 * if the size limit for the trash bin is reached, we delete the oldest
857
-	 * files in the trash bin until we meet the limit again
858
-	 *
859
-	 * @param array $files
860
-	 * @param string $user
861
-	 * @param int $availableSpace available disc space
862
-	 * @return int size of deleted files
863
-	 */
864
-	protected static function deleteFiles($files, $user, $availableSpace) {
865
-		/** @var Application $application */
866
-		$application = \OC::$server->query(Application::class);
867
-		$expiration = $application->getContainer()->query('Expiration');
868
-		$size = 0;
869
-
870
-		if ($availableSpace < 0) {
871
-			foreach ($files as $file) {
872
-				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
873
-					$tmp = self::delete($file['name'], $user, $file['mtime']);
874
-					\OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
875
-					$availableSpace += $tmp;
876
-					$size += $tmp;
877
-				} else {
878
-					break;
879
-				}
880
-			}
881
-		}
882
-		return $size;
883
-	}
884
-
885
-	/**
886
-	 * delete files older then max storage time
887
-	 *
888
-	 * @param array $files list of files sorted by mtime
889
-	 * @param string $user
890
-	 * @return integer[] size of deleted files and number of deleted files
891
-	 */
892
-	public static function deleteExpiredFiles($files, $user) {
893
-		/** @var Expiration $expiration */
894
-		$expiration = \OC::$server->query(Expiration::class);
895
-		$size = 0;
896
-		$count = 0;
897
-		foreach ($files as $file) {
898
-			$timestamp = $file['mtime'];
899
-			$filename = $file['name'];
900
-			if ($expiration->isExpired($timestamp)) {
901
-				try {
902
-					$size += self::delete($filename, $user, $timestamp);
903
-					$count++;
904
-				} catch (\OCP\Files\NotPermittedException $e) {
905
-					\OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']);
906
-				}
907
-				\OC::$server->getLogger()->info(
908
-					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
909
-					['app' => 'files_trashbin']
910
-				);
911
-			} else {
912
-				break;
913
-			}
914
-		}
915
-
916
-		return [$size, $count];
917
-	}
918
-
919
-	/**
920
-	 * recursive copy to copy a whole directory
921
-	 *
922
-	 * @param string $source source path, relative to the users files directory
923
-	 * @param string $destination destination path relative to the users root directoy
924
-	 * @param View $view file view for the users root directory
925
-	 * @return int
926
-	 * @throws Exceptions\CopyRecursiveException
927
-	 */
928
-	private static function copy_recursive($source, $destination, View $view) {
929
-		$size = 0;
930
-		if ($view->is_dir($source)) {
931
-			$view->mkdir($destination);
932
-			$view->touch($destination, $view->filemtime($source));
933
-			foreach ($view->getDirectoryContent($source) as $i) {
934
-				$pathDir = $source . '/' . $i['name'];
935
-				if ($view->is_dir($pathDir)) {
936
-					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
937
-				} else {
938
-					$size += $view->filesize($pathDir);
939
-					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
940
-					if (!$result) {
941
-						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
942
-					}
943
-					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
944
-				}
945
-			}
946
-		} else {
947
-			$size += $view->filesize($source);
948
-			$result = $view->copy($source, $destination);
949
-			if (!$result) {
950
-				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
951
-			}
952
-			$view->touch($destination, $view->filemtime($source));
953
-		}
954
-		return $size;
955
-	}
956
-
957
-	/**
958
-	 * find all versions which belong to the file we want to restore
959
-	 *
960
-	 * @param string $filename name of the file which should be restored
961
-	 * @param int $timestamp timestamp when the file was deleted
962
-	 * @return array
963
-	 */
964
-	private static function getVersionsFromTrash($filename, $timestamp, $user) {
965
-		$view = new View('/' . $user . '/files_trashbin/versions');
966
-		$versions = [];
967
-
968
-		/** @var \OC\Files\Storage\Storage $storage */
969
-		[$storage,] = $view->resolvePath('/');
970
-
971
-		//force rescan of versions, local storage may not have updated the cache
972
-		if (!self::$scannedVersions) {
973
-			$storage->getScanner()->scan('files_trashbin/versions');
974
-			self::$scannedVersions = true;
975
-		}
976
-
977
-		$pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename));
978
-		if ($timestamp) {
979
-			// fetch for old versions
980
-			$escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
981
-			$pattern .= '.v%.d' . $escapedTimestamp;
982
-			$offset = -strlen($escapedTimestamp) - 2;
983
-		} else {
984
-			$pattern .= '.v%';
985
-		}
986
-
987
-		// Manually fetch all versions from the file cache to be able to filter them by their parent
988
-		$cache = $storage->getCache('');
989
-		$query = new CacheQueryBuilder(
990
-			\OC::$server->getDatabaseConnection(),
991
-			\OC::$server->getSystemConfig(),
992
-			\OC::$server->getLogger(),
993
-			$cache
994
-		);
995
-		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
996
-		$parentId = $cache->getId($normalizedParentPath);
997
-		if ($parentId === -1) {
998
-			return [];
999
-		}
1000
-
1001
-		$query->selectFileCache()
1002
-			->whereStorageId()
1003
-			->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
1004
-			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
1005
-
1006
-		$result = $query->executeQuery();
1007
-		$entries = $result->fetchAll();
1008
-		$result->closeCursor();
1009
-
1010
-		/** @var CacheEntry[] $matches */
1011
-		$matches = array_map(function (array $data) {
1012
-			return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
1013
-		}, $entries);
1014
-
1015
-		foreach ($matches as $ma) {
1016
-			if ($timestamp) {
1017
-				$parts = explode('.v', substr($ma['path'], 0, $offset));
1018
-				$versions[] = end($parts);
1019
-			} else {
1020
-				$parts = explode('.v', $ma['path']);
1021
-				$versions[] = end($parts);
1022
-			}
1023
-		}
1024
-
1025
-		return $versions;
1026
-	}
1027
-
1028
-	/**
1029
-	 * find unique extension for restored file if a file with the same name already exists
1030
-	 *
1031
-	 * @param string $location where the file should be restored
1032
-	 * @param string $filename name of the file
1033
-	 * @param View $view filesystem view relative to users root directory
1034
-	 * @return string with unique extension
1035
-	 */
1036
-	private static function getUniqueFilename($location, $filename, View $view) {
1037
-		$ext = pathinfo($filename, PATHINFO_EXTENSION);
1038
-		$name = pathinfo($filename, PATHINFO_FILENAME);
1039
-		$l = \OC::$server->getL10N('files_trashbin');
1040
-
1041
-		$location = '/' . trim($location, '/');
1042
-
1043
-		// if extension is not empty we set a dot in front of it
1044
-		if ($ext !== '') {
1045
-			$ext = '.' . $ext;
1046
-		}
1047
-
1048
-		if ($view->file_exists('files' . $location . '/' . $filename)) {
1049
-			$i = 2;
1050
-			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1051
-			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1052
-				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1053
-				$i++;
1054
-			}
1055
-
1056
-			return $uniqueName;
1057
-		}
1058
-
1059
-		return $filename;
1060
-	}
1061
-
1062
-	/**
1063
-	 * get the size from a given root folder
1064
-	 *
1065
-	 * @param View $view file view on the root folder
1066
-	 * @return integer size of the folder
1067
-	 */
1068
-	private static function calculateSize($view) {
1069
-		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1070
-		if (!file_exists($root)) {
1071
-			return 0;
1072
-		}
1073
-		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1074
-		$size = 0;
1075
-
1076
-		/**
1077
-		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1078
-		 * This bug is fixed in PHP 5.5.9 or before
1079
-		 * See #8376
1080
-		 */
1081
-		$iterator->rewind();
1082
-		while ($iterator->valid()) {
1083
-			$path = $iterator->current();
1084
-			$relpath = substr($path, strlen($root) - 1);
1085
-			if (!$view->is_dir($relpath)) {
1086
-				$size += $view->filesize($relpath);
1087
-			}
1088
-			$iterator->next();
1089
-		}
1090
-		return $size;
1091
-	}
1092
-
1093
-	/**
1094
-	 * get current size of trash bin from a given user
1095
-	 *
1096
-	 * @param string $user user who owns the trash bin
1097
-	 * @return integer trash bin size
1098
-	 */
1099
-	private static function getTrashbinSize($user) {
1100
-		$view = new View('/' . $user);
1101
-		$fileInfo = $view->getFileInfo('/files_trashbin');
1102
-		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1103
-	}
1104
-
1105
-	/**
1106
-	 * check if trash bin is empty for a given user
1107
-	 *
1108
-	 * @param string $user
1109
-	 * @return bool
1110
-	 */
1111
-	public static function isEmpty($user) {
1112
-		$view = new View('/' . $user . '/files_trashbin');
1113
-		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1114
-			while ($file = readdir($dh)) {
1115
-				if (!Filesystem::isIgnoredDir($file)) {
1116
-					return false;
1117
-				}
1118
-			}
1119
-		}
1120
-		return true;
1121
-	}
1122
-
1123
-	/**
1124
-	 * @param $path
1125
-	 * @return string
1126
-	 */
1127
-	public static function preview_icon($path) {
1128
-		return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1129
-	}
67
+    // unit: percentage; 50% of available disk space/quota
68
+    public const DEFAULTMAXSIZE = 50;
69
+
70
+    /**
71
+     * Whether versions have already be rescanned during this PHP request
72
+     *
73
+     * @var bool
74
+     */
75
+    private static $scannedVersions = false;
76
+
77
+    /**
78
+     * Ensure we don't need to scan the file during the move to trash
79
+     * by triggering the scan in the pre-hook
80
+     *
81
+     * @param array $params
82
+     */
83
+    public static function ensureFileScannedHook($params) {
84
+        try {
85
+            self::getUidAndFilename($params['path']);
86
+        } catch (NotFoundException $e) {
87
+            // nothing to scan for non existing files
88
+        }
89
+    }
90
+
91
+    /**
92
+     * get the UID of the owner of the file and the path to the file relative to
93
+     * owners files folder
94
+     *
95
+     * @param string $filename
96
+     * @return array
97
+     * @throws \OC\User\NoUserException
98
+     */
99
+    public static function getUidAndFilename($filename) {
100
+        $uid = Filesystem::getOwner($filename);
101
+        $userManager = \OC::$server->getUserManager();
102
+        // if the user with the UID doesn't exists, e.g. because the UID points
103
+        // to a remote user with a federated cloud ID we use the current logged-in
104
+        // user. We need a valid local user to move the file to the right trash bin
105
+        if (!$userManager->userExists($uid)) {
106
+            $uid = User::getUser();
107
+        }
108
+        if (!$uid) {
109
+            // no owner, usually because of share link from ext storage
110
+            return [null, null];
111
+        }
112
+        Filesystem::initMountPoints($uid);
113
+        if ($uid !== User::getUser()) {
114
+            $info = Filesystem::getFileInfo($filename);
115
+            $ownerView = new View('/' . $uid . '/files');
116
+            try {
117
+                $filename = $ownerView->getPath($info['fileid']);
118
+            } catch (NotFoundException $e) {
119
+                $filename = null;
120
+            }
121
+        }
122
+        return [$uid, $filename];
123
+    }
124
+
125
+    /**
126
+     * get original location of files for user
127
+     *
128
+     * @param string $user
129
+     * @return array (filename => array (timestamp => original location))
130
+     */
131
+    public static function getLocations($user) {
132
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
133
+        $query->select('id', 'timestamp', 'location')
134
+            ->from('files_trash')
135
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)));
136
+        $result = $query->executeQuery();
137
+        $array = [];
138
+        while ($row = $result->fetch()) {
139
+            if (isset($array[$row['id']])) {
140
+                $array[$row['id']][$row['timestamp']] = $row['location'];
141
+            } else {
142
+                $array[$row['id']] = [$row['timestamp'] => $row['location']];
143
+            }
144
+        }
145
+        $result->closeCursor();
146
+        return $array;
147
+    }
148
+
149
+    /**
150
+     * get original location of file
151
+     *
152
+     * @param string $user
153
+     * @param string $filename
154
+     * @param string $timestamp
155
+     * @return string original location
156
+     */
157
+    public static function getLocation($user, $filename, $timestamp) {
158
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
159
+        $query->select('location')
160
+            ->from('files_trash')
161
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
162
+            ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
163
+            ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
164
+
165
+        $result = $query->executeQuery();
166
+        $row = $result->fetch();
167
+        $result->closeCursor();
168
+
169
+        if (isset($row['location'])) {
170
+            return $row['location'];
171
+        } else {
172
+            return false;
173
+        }
174
+    }
175
+
176
+    private static function setUpTrash($user) {
177
+        $view = new View('/' . $user);
178
+        if (!$view->is_dir('files_trashbin')) {
179
+            $view->mkdir('files_trashbin');
180
+        }
181
+        if (!$view->is_dir('files_trashbin/files')) {
182
+            $view->mkdir('files_trashbin/files');
183
+        }
184
+        if (!$view->is_dir('files_trashbin/versions')) {
185
+            $view->mkdir('files_trashbin/versions');
186
+        }
187
+        if (!$view->is_dir('files_trashbin/keys')) {
188
+            $view->mkdir('files_trashbin/keys');
189
+        }
190
+    }
191
+
192
+
193
+    /**
194
+     * copy file to owners trash
195
+     *
196
+     * @param string $sourcePath
197
+     * @param string $owner
198
+     * @param string $targetPath
199
+     * @param $user
200
+     * @param integer $timestamp
201
+     */
202
+    private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
203
+        self::setUpTrash($owner);
204
+
205
+        $targetFilename = basename($targetPath);
206
+        $targetLocation = dirname($targetPath);
207
+
208
+        $sourceFilename = basename($sourcePath);
209
+
210
+        $view = new View('/');
211
+
212
+        $target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
213
+        $source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
214
+        $free = $view->free_space($target);
215
+        $isUnknownOrUnlimitedFreeSpace = $free < 0;
216
+        $isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
217
+        if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
218
+            self::copy_recursive($source, $target, $view);
219
+        }
220
+
221
+
222
+        if ($view->file_exists($target)) {
223
+            $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
224
+            $query->insert('files_trash')
225
+                ->setValue('id', $query->createNamedParameter($targetFilename))
226
+                ->setValue('timestamp', $query->createNamedParameter($timestamp))
227
+                ->setValue('location', $query->createNamedParameter($targetLocation))
228
+                ->setValue('user', $query->createNamedParameter($user));
229
+            $result = $query->executeStatement();
230
+            if (!$result) {
231
+                \OC::$server->getLogger()->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
232
+            }
233
+        }
234
+    }
235
+
236
+
237
+    /**
238
+     * move file to the trash bin
239
+     *
240
+     * @param string $file_path path to the deleted file/directory relative to the files root directory
241
+     * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
242
+     *
243
+     * @return bool
244
+     */
245
+    public static function move2trash($file_path, $ownerOnly = false) {
246
+        // get the user for which the filesystem is setup
247
+        $root = Filesystem::getRoot();
248
+        [, $user] = explode('/', $root);
249
+        [$owner, $ownerPath] = self::getUidAndFilename($file_path);
250
+
251
+        // if no owner found (ex: ext storage + share link), will use the current user's trashbin then
252
+        if (is_null($owner)) {
253
+            $owner = $user;
254
+            $ownerPath = $file_path;
255
+        }
256
+
257
+        $ownerView = new View('/' . $owner);
258
+        // file has been deleted in between
259
+        if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
260
+            return true;
261
+        }
262
+
263
+        self::setUpTrash($user);
264
+        if ($owner !== $user) {
265
+            // also setup for owner
266
+            self::setUpTrash($owner);
267
+        }
268
+
269
+        $path_parts = pathinfo($ownerPath);
270
+
271
+        $filename = $path_parts['basename'];
272
+        $location = $path_parts['dirname'];
273
+        /** @var ITimeFactory $timeFactory */
274
+        $timeFactory = \OC::$server->query(ITimeFactory::class);
275
+        $timestamp = $timeFactory->getTime();
276
+
277
+        $lockingProvider = \OC::$server->getLockingProvider();
278
+
279
+        // disable proxy to prevent recursive calls
280
+        $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
281
+        $gotLock = false;
282
+
283
+        while (!$gotLock) {
284
+            try {
285
+                /** @var \OC\Files\Storage\Storage $trashStorage */
286
+                [$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
287
+
288
+                $trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
289
+                $gotLock = true;
290
+            } catch (LockedException $e) {
291
+                // a file with the same name is being deleted concurrently
292
+                // nudge the timestamp a bit to resolve the conflict
293
+
294
+                $timestamp = $timestamp + 1;
295
+
296
+                $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
297
+            }
298
+        }
299
+
300
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
301
+        [$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath);
302
+
303
+
304
+        if ($trashStorage->file_exists($trashInternalPath)) {
305
+            $trashStorage->unlink($trashInternalPath);
306
+        }
307
+
308
+        $config = \OC::$server->getConfig();
309
+        $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
310
+        $userTrashbinSize = (int)$config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1');
311
+        $configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize;
312
+        if ($configuredTrashbinSize >= 0 && $sourceStorage->filesize($sourceInternalPath) >= $configuredTrashbinSize) {
313
+            return false;
314
+        }
315
+
316
+        $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
317
+
318
+        try {
319
+            $moveSuccessful = true;
320
+
321
+            // when moving within the same object store, the cache update done above is enough to move the file
322
+            if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) {
323
+                $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
324
+            }
325
+        } catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
326
+            $moveSuccessful = false;
327
+            if ($trashStorage->file_exists($trashInternalPath)) {
328
+                $trashStorage->unlink($trashInternalPath);
329
+            }
330
+            \OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
331
+        }
332
+
333
+        if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
334
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
335
+                $sourceStorage->rmdir($sourceInternalPath);
336
+            } else {
337
+                $sourceStorage->unlink($sourceInternalPath);
338
+            }
339
+
340
+            if ($sourceStorage->file_exists($sourceInternalPath)) {
341
+                // undo the cache move
342
+                $sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
343
+            } else {
344
+                $trashStorage->getUpdater()->remove($trashInternalPath);
345
+            }
346
+            return false;
347
+        }
348
+
349
+        if ($moveSuccessful) {
350
+            $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
351
+            $query->insert('files_trash')
352
+                ->setValue('id', $query->createNamedParameter($filename))
353
+                ->setValue('timestamp', $query->createNamedParameter($timestamp))
354
+                ->setValue('location', $query->createNamedParameter($location))
355
+                ->setValue('user', $query->createNamedParameter($owner));
356
+            $result = $query->executeStatement();
357
+            if (!$result) {
358
+                \OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
359
+            }
360
+            \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
361
+                'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
362
+
363
+            self::retainVersions($filename, $owner, $ownerPath, $timestamp);
364
+
365
+            // if owner !== user we need to also add a copy to the users trash
366
+            if ($user !== $owner && $ownerOnly === false) {
367
+                self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
368
+            }
369
+        }
370
+
371
+        $trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
372
+
373
+        self::scheduleExpire($user);
374
+
375
+        // if owner !== user we also need to update the owners trash size
376
+        if ($owner !== $user) {
377
+            self::scheduleExpire($owner);
378
+        }
379
+
380
+        return $moveSuccessful;
381
+    }
382
+
383
+    /**
384
+     * Move file versions to trash so that they can be restored later
385
+     *
386
+     * @param string $filename of deleted file
387
+     * @param string $owner owner user id
388
+     * @param string $ownerPath path relative to the owner's home storage
389
+     * @param integer $timestamp when the file was deleted
390
+     */
391
+    private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
392
+        if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
393
+            $user = User::getUser();
394
+            $rootView = new View('/');
395
+
396
+            if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
397
+                if ($owner !== $user) {
398
+                    self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
399
+                }
400
+                self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
401
+            } elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
402
+                foreach ($versions as $v) {
403
+                    if ($owner !== $user) {
404
+                        self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
405
+                    }
406
+                    self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
407
+                }
408
+            }
409
+        }
410
+    }
411
+
412
+    /**
413
+     * Move a file or folder on storage level
414
+     *
415
+     * @param View $view
416
+     * @param string $source
417
+     * @param string $target
418
+     * @return bool
419
+     */
420
+    private static function move(View $view, $source, $target) {
421
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
422
+        [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
423
+        /** @var \OC\Files\Storage\Storage $targetStorage */
424
+        [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
425
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
426
+
427
+        $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
428
+        if ($result) {
429
+            $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
430
+        }
431
+        return $result;
432
+    }
433
+
434
+    /**
435
+     * Copy a file or folder on storage level
436
+     *
437
+     * @param View $view
438
+     * @param string $source
439
+     * @param string $target
440
+     * @return bool
441
+     */
442
+    private static function copy(View $view, $source, $target) {
443
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
444
+        [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
445
+        /** @var \OC\Files\Storage\Storage $targetStorage */
446
+        [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
447
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
448
+
449
+        $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
450
+        if ($result) {
451
+            $targetStorage->getUpdater()->update($targetInternalPath);
452
+        }
453
+        return $result;
454
+    }
455
+
456
+    /**
457
+     * Restore a file or folder from trash bin
458
+     *
459
+     * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
460
+     * including the timestamp suffix ".d12345678"
461
+     * @param string $filename name of the file/folder
462
+     * @param int $timestamp time when the file/folder was deleted
463
+     *
464
+     * @return bool true on success, false otherwise
465
+     */
466
+    public static function restore($file, $filename, $timestamp) {
467
+        $user = User::getUser();
468
+        $view = new View('/' . $user);
469
+
470
+        $location = '';
471
+        if ($timestamp) {
472
+            $location = self::getLocation($user, $filename, $timestamp);
473
+            if ($location === false) {
474
+                \OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
475
+            } else {
476
+                // if location no longer exists, restore file in the root directory
477
+                if ($location !== '/' &&
478
+                    (!$view->is_dir('files/' . $location) ||
479
+                        !$view->isCreatable('files/' . $location))
480
+                ) {
481
+                    $location = '';
482
+                }
483
+            }
484
+        }
485
+
486
+        // we need a  extension in case a file/dir with the same name already exists
487
+        $uniqueFilename = self::getUniqueFilename($location, $filename, $view);
488
+
489
+        $source = Filesystem::normalizePath('files_trashbin/files/' . $file);
490
+        $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
491
+        if (!$view->file_exists($source)) {
492
+            return false;
493
+        }
494
+        $mtime = $view->filemtime($source);
495
+
496
+        // restore file
497
+        if (!$view->isCreatable(dirname($target))) {
498
+            throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
499
+        }
500
+        $restoreResult = $view->rename($source, $target);
501
+
502
+        // handle the restore result
503
+        if ($restoreResult) {
504
+            $fakeRoot = $view->getRoot();
505
+            $view->chroot('/' . $user . '/files');
506
+            $view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
507
+            $view->chroot($fakeRoot);
508
+            \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
509
+                'trashPath' => Filesystem::normalizePath($file)]);
510
+
511
+            self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
512
+
513
+            if ($timestamp) {
514
+                $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
515
+                $query->delete('files_trash')
516
+                    ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
517
+                    ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
518
+                    ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
519
+                $query->executeStatement();
520
+            }
521
+
522
+            return true;
523
+        }
524
+
525
+        return false;
526
+    }
527
+
528
+    /**
529
+     * restore versions from trash bin
530
+     *
531
+     * @param View $view file view
532
+     * @param string $file complete path to file
533
+     * @param string $filename name of file once it was deleted
534
+     * @param string $uniqueFilename new file name to restore the file without overwriting existing files
535
+     * @param string $location location if file
536
+     * @param int $timestamp deletion time
537
+     * @return false|null
538
+     */
539
+    private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
540
+        if (\OCP\App::isEnabled('files_versions')) {
541
+            $user = User::getUser();
542
+            $rootView = new View('/');
543
+
544
+            $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
545
+
546
+            [$owner, $ownerPath] = self::getUidAndFilename($target);
547
+
548
+            // file has been deleted in between
549
+            if (empty($ownerPath)) {
550
+                return false;
551
+            }
552
+
553
+            if ($timestamp) {
554
+                $versionedFile = $filename;
555
+            } else {
556
+                $versionedFile = $file;
557
+            }
558
+
559
+            if ($view->is_dir('/files_trashbin/versions/' . $file)) {
560
+                $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
561
+            } elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
562
+                foreach ($versions as $v) {
563
+                    if ($timestamp) {
564
+                        $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
565
+                    } else {
566
+                        $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
567
+                    }
568
+                }
569
+            }
570
+        }
571
+    }
572
+
573
+    /**
574
+     * delete all files from the trash
575
+     */
576
+    public static function deleteAll() {
577
+        $user = User::getUser();
578
+        $userRoot = \OC::$server->getUserFolder($user)->getParent();
579
+        $view = new View('/' . $user);
580
+        $fileInfos = $view->getDirectoryContent('files_trashbin/files');
581
+
582
+        try {
583
+            $trash = $userRoot->get('files_trashbin');
584
+        } catch (NotFoundException $e) {
585
+            return false;
586
+        }
587
+
588
+        // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
589
+        $filePaths = [];
590
+        foreach ($fileInfos as $fileInfo) {
591
+            $filePaths[] = $view->getRelativePath($fileInfo->getPath());
592
+        }
593
+        unset($fileInfos); // save memory
594
+
595
+        // Bulk PreDelete-Hook
596
+        \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
597
+
598
+        // Single-File Hooks
599
+        foreach ($filePaths as $path) {
600
+            self::emitTrashbinPreDelete($path);
601
+        }
602
+
603
+        // actual file deletion
604
+        $trash->delete();
605
+
606
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
607
+        $query->delete('files_trash')
608
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)));
609
+        $query->executeStatement();
610
+
611
+        // Bulk PostDelete-Hook
612
+        \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
613
+
614
+        // Single-File Hooks
615
+        foreach ($filePaths as $path) {
616
+            self::emitTrashbinPostDelete($path);
617
+        }
618
+
619
+        $trash = $userRoot->newFolder('files_trashbin');
620
+        $trash->newFolder('files');
621
+
622
+        return true;
623
+    }
624
+
625
+    /**
626
+     * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
627
+     *
628
+     * @param string $path
629
+     */
630
+    protected static function emitTrashbinPreDelete($path) {
631
+        \OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
632
+    }
633
+
634
+    /**
635
+     * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
636
+     *
637
+     * @param string $path
638
+     */
639
+    protected static function emitTrashbinPostDelete($path) {
640
+        \OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
641
+    }
642
+
643
+    /**
644
+     * delete file from trash bin permanently
645
+     *
646
+     * @param string $filename path to the file
647
+     * @param string $user
648
+     * @param int $timestamp of deletion time
649
+     *
650
+     * @return int size of deleted files
651
+     */
652
+    public static function delete($filename, $user, $timestamp = null) {
653
+        $userRoot = \OC::$server->getUserFolder($user)->getParent();
654
+        $view = new View('/' . $user);
655
+        $size = 0;
656
+
657
+        if ($timestamp) {
658
+            $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
659
+            $query->delete('files_trash')
660
+                ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
661
+                ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
662
+                ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
663
+            $query->executeStatement();
664
+
665
+            $file = $filename . '.d' . $timestamp;
666
+        } else {
667
+            $file = $filename;
668
+        }
669
+
670
+        $size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
671
+
672
+        try {
673
+            $node = $userRoot->get('/files_trashbin/files/' . $file);
674
+        } catch (NotFoundException $e) {
675
+            return $size;
676
+        }
677
+
678
+        if ($node instanceof Folder) {
679
+            $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
680
+        } elseif ($node instanceof File) {
681
+            $size += $view->filesize('/files_trashbin/files/' . $file);
682
+        }
683
+
684
+        self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
685
+        $node->delete();
686
+        self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
687
+
688
+        return $size;
689
+    }
690
+
691
+    /**
692
+     * @param View $view
693
+     * @param string $file
694
+     * @param string $filename
695
+     * @param integer|null $timestamp
696
+     * @param string $user
697
+     * @return int
698
+     */
699
+    private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
700
+        $size = 0;
701
+        if (\OCP\App::isEnabled('files_versions')) {
702
+            if ($view->is_dir('files_trashbin/versions/' . $file)) {
703
+                $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
704
+                $view->unlink('files_trashbin/versions/' . $file);
705
+            } elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
706
+                foreach ($versions as $v) {
707
+                    if ($timestamp) {
708
+                        $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
709
+                        $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
710
+                    } else {
711
+                        $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
712
+                        $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
713
+                    }
714
+                }
715
+            }
716
+        }
717
+        return $size;
718
+    }
719
+
720
+    /**
721
+     * check to see whether a file exists in trashbin
722
+     *
723
+     * @param string $filename path to the file
724
+     * @param int $timestamp of deletion time
725
+     * @return bool true if file exists, otherwise false
726
+     */
727
+    public static function file_exists($filename, $timestamp = null) {
728
+        $user = User::getUser();
729
+        $view = new View('/' . $user);
730
+
731
+        if ($timestamp) {
732
+            $filename = $filename . '.d' . $timestamp;
733
+        }
734
+
735
+        $target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
736
+        return $view->file_exists($target);
737
+    }
738
+
739
+    /**
740
+     * deletes used space for trash bin in db if user was deleted
741
+     *
742
+     * @param string $uid id of deleted user
743
+     * @return bool result of db delete operation
744
+     */
745
+    public static function deleteUser($uid) {
746
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
747
+        $query->delete('files_trash')
748
+            ->where($query->expr()->eq('user', $query->createNamedParameter($uid)));
749
+        return (bool) $query->executeStatement();
750
+    }
751
+
752
+    /**
753
+     * calculate remaining free space for trash bin
754
+     *
755
+     * @param integer $trashbinSize current size of the trash bin
756
+     * @param string $user
757
+     * @return int available free space for trash bin
758
+     */
759
+    private static function calculateFreeSpace($trashbinSize, $user) {
760
+        $config = \OC::$server->getConfig();
761
+        $userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
762
+        if ($userTrashbinSize > -1) {
763
+            return $userTrashbinSize - $trashbinSize;
764
+        }
765
+        $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
766
+        if ($systemTrashbinSize > -1) {
767
+            return $systemTrashbinSize - $trashbinSize;
768
+        }
769
+
770
+        $softQuota = true;
771
+        $userObject = \OC::$server->getUserManager()->get($user);
772
+        if (is_null($userObject)) {
773
+            return 0;
774
+        }
775
+        $quota = $userObject->getQuota();
776
+        if ($quota === null || $quota === 'none') {
777
+            $quota = Filesystem::free_space('/');
778
+            $softQuota = false;
779
+            // inf or unknown free space
780
+            if ($quota < 0) {
781
+                $quota = PHP_INT_MAX;
782
+            }
783
+        } else {
784
+            $quota = \OCP\Util::computerFileSize($quota);
785
+        }
786
+
787
+        // calculate available space for trash bin
788
+        // subtract size of files and current trash bin size from quota
789
+        if ($softQuota) {
790
+            $userFolder = \OC::$server->getUserFolder($user);
791
+            if (is_null($userFolder)) {
792
+                return 0;
793
+            }
794
+            $free = $quota - $userFolder->getSize(false); // remaining free space for user
795
+            if ($free > 0) {
796
+                $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
797
+            } else {
798
+                $availableSpace = $free - $trashbinSize;
799
+            }
800
+        } else {
801
+            $availableSpace = $quota;
802
+        }
803
+
804
+        return $availableSpace;
805
+    }
806
+
807
+    /**
808
+     * resize trash bin if necessary after a new file was added to Nextcloud
809
+     *
810
+     * @param string $user user id
811
+     */
812
+    public static function resizeTrash($user) {
813
+        $size = self::getTrashbinSize($user);
814
+
815
+        $freeSpace = self::calculateFreeSpace($size, $user);
816
+
817
+        if ($freeSpace < 0) {
818
+            self::scheduleExpire($user);
819
+        }
820
+    }
821
+
822
+    /**
823
+     * clean up the trash bin
824
+     *
825
+     * @param string $user
826
+     */
827
+    public static function expire($user) {
828
+        $trashBinSize = self::getTrashbinSize($user);
829
+        $availableSpace = self::calculateFreeSpace($trashBinSize, $user);
830
+
831
+        $dirContent = Helper::getTrashFiles('/', $user, 'mtime');
832
+
833
+        // delete all files older then $retention_obligation
834
+        [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
835
+
836
+        $availableSpace += $delSize;
837
+
838
+        // delete files from trash until we meet the trash bin size limit again
839
+        self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
840
+    }
841
+
842
+    /**
843
+     * @param string $user
844
+     */
845
+    private static function scheduleExpire($user) {
846
+        // let the admin disable auto expire
847
+        /** @var Application $application */
848
+        $application = \OC::$server->query(Application::class);
849
+        $expiration = $application->getContainer()->query('Expiration');
850
+        if ($expiration->isEnabled()) {
851
+            \OC::$server->getCommandBus()->push(new Expire($user));
852
+        }
853
+    }
854
+
855
+    /**
856
+     * if the size limit for the trash bin is reached, we delete the oldest
857
+     * files in the trash bin until we meet the limit again
858
+     *
859
+     * @param array $files
860
+     * @param string $user
861
+     * @param int $availableSpace available disc space
862
+     * @return int size of deleted files
863
+     */
864
+    protected static function deleteFiles($files, $user, $availableSpace) {
865
+        /** @var Application $application */
866
+        $application = \OC::$server->query(Application::class);
867
+        $expiration = $application->getContainer()->query('Expiration');
868
+        $size = 0;
869
+
870
+        if ($availableSpace < 0) {
871
+            foreach ($files as $file) {
872
+                if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
873
+                    $tmp = self::delete($file['name'], $user, $file['mtime']);
874
+                    \OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
875
+                    $availableSpace += $tmp;
876
+                    $size += $tmp;
877
+                } else {
878
+                    break;
879
+                }
880
+            }
881
+        }
882
+        return $size;
883
+    }
884
+
885
+    /**
886
+     * delete files older then max storage time
887
+     *
888
+     * @param array $files list of files sorted by mtime
889
+     * @param string $user
890
+     * @return integer[] size of deleted files and number of deleted files
891
+     */
892
+    public static function deleteExpiredFiles($files, $user) {
893
+        /** @var Expiration $expiration */
894
+        $expiration = \OC::$server->query(Expiration::class);
895
+        $size = 0;
896
+        $count = 0;
897
+        foreach ($files as $file) {
898
+            $timestamp = $file['mtime'];
899
+            $filename = $file['name'];
900
+            if ($expiration->isExpired($timestamp)) {
901
+                try {
902
+                    $size += self::delete($filename, $user, $timestamp);
903
+                    $count++;
904
+                } catch (\OCP\Files\NotPermittedException $e) {
905
+                    \OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']);
906
+                }
907
+                \OC::$server->getLogger()->info(
908
+                    'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
909
+                    ['app' => 'files_trashbin']
910
+                );
911
+            } else {
912
+                break;
913
+            }
914
+        }
915
+
916
+        return [$size, $count];
917
+    }
918
+
919
+    /**
920
+     * recursive copy to copy a whole directory
921
+     *
922
+     * @param string $source source path, relative to the users files directory
923
+     * @param string $destination destination path relative to the users root directoy
924
+     * @param View $view file view for the users root directory
925
+     * @return int
926
+     * @throws Exceptions\CopyRecursiveException
927
+     */
928
+    private static function copy_recursive($source, $destination, View $view) {
929
+        $size = 0;
930
+        if ($view->is_dir($source)) {
931
+            $view->mkdir($destination);
932
+            $view->touch($destination, $view->filemtime($source));
933
+            foreach ($view->getDirectoryContent($source) as $i) {
934
+                $pathDir = $source . '/' . $i['name'];
935
+                if ($view->is_dir($pathDir)) {
936
+                    $size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
937
+                } else {
938
+                    $size += $view->filesize($pathDir);
939
+                    $result = $view->copy($pathDir, $destination . '/' . $i['name']);
940
+                    if (!$result) {
941
+                        throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
942
+                    }
943
+                    $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
944
+                }
945
+            }
946
+        } else {
947
+            $size += $view->filesize($source);
948
+            $result = $view->copy($source, $destination);
949
+            if (!$result) {
950
+                throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
951
+            }
952
+            $view->touch($destination, $view->filemtime($source));
953
+        }
954
+        return $size;
955
+    }
956
+
957
+    /**
958
+     * find all versions which belong to the file we want to restore
959
+     *
960
+     * @param string $filename name of the file which should be restored
961
+     * @param int $timestamp timestamp when the file was deleted
962
+     * @return array
963
+     */
964
+    private static function getVersionsFromTrash($filename, $timestamp, $user) {
965
+        $view = new View('/' . $user . '/files_trashbin/versions');
966
+        $versions = [];
967
+
968
+        /** @var \OC\Files\Storage\Storage $storage */
969
+        [$storage,] = $view->resolvePath('/');
970
+
971
+        //force rescan of versions, local storage may not have updated the cache
972
+        if (!self::$scannedVersions) {
973
+            $storage->getScanner()->scan('files_trashbin/versions');
974
+            self::$scannedVersions = true;
975
+        }
976
+
977
+        $pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename));
978
+        if ($timestamp) {
979
+            // fetch for old versions
980
+            $escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
981
+            $pattern .= '.v%.d' . $escapedTimestamp;
982
+            $offset = -strlen($escapedTimestamp) - 2;
983
+        } else {
984
+            $pattern .= '.v%';
985
+        }
986
+
987
+        // Manually fetch all versions from the file cache to be able to filter them by their parent
988
+        $cache = $storage->getCache('');
989
+        $query = new CacheQueryBuilder(
990
+            \OC::$server->getDatabaseConnection(),
991
+            \OC::$server->getSystemConfig(),
992
+            \OC::$server->getLogger(),
993
+            $cache
994
+        );
995
+        $normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
996
+        $parentId = $cache->getId($normalizedParentPath);
997
+        if ($parentId === -1) {
998
+            return [];
999
+        }
1000
+
1001
+        $query->selectFileCache()
1002
+            ->whereStorageId()
1003
+            ->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
1004
+            ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
1005
+
1006
+        $result = $query->executeQuery();
1007
+        $entries = $result->fetchAll();
1008
+        $result->closeCursor();
1009
+
1010
+        /** @var CacheEntry[] $matches */
1011
+        $matches = array_map(function (array $data) {
1012
+            return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
1013
+        }, $entries);
1014
+
1015
+        foreach ($matches as $ma) {
1016
+            if ($timestamp) {
1017
+                $parts = explode('.v', substr($ma['path'], 0, $offset));
1018
+                $versions[] = end($parts);
1019
+            } else {
1020
+                $parts = explode('.v', $ma['path']);
1021
+                $versions[] = end($parts);
1022
+            }
1023
+        }
1024
+
1025
+        return $versions;
1026
+    }
1027
+
1028
+    /**
1029
+     * find unique extension for restored file if a file with the same name already exists
1030
+     *
1031
+     * @param string $location where the file should be restored
1032
+     * @param string $filename name of the file
1033
+     * @param View $view filesystem view relative to users root directory
1034
+     * @return string with unique extension
1035
+     */
1036
+    private static function getUniqueFilename($location, $filename, View $view) {
1037
+        $ext = pathinfo($filename, PATHINFO_EXTENSION);
1038
+        $name = pathinfo($filename, PATHINFO_FILENAME);
1039
+        $l = \OC::$server->getL10N('files_trashbin');
1040
+
1041
+        $location = '/' . trim($location, '/');
1042
+
1043
+        // if extension is not empty we set a dot in front of it
1044
+        if ($ext !== '') {
1045
+            $ext = '.' . $ext;
1046
+        }
1047
+
1048
+        if ($view->file_exists('files' . $location . '/' . $filename)) {
1049
+            $i = 2;
1050
+            $uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1051
+            while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1052
+                $uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1053
+                $i++;
1054
+            }
1055
+
1056
+            return $uniqueName;
1057
+        }
1058
+
1059
+        return $filename;
1060
+    }
1061
+
1062
+    /**
1063
+     * get the size from a given root folder
1064
+     *
1065
+     * @param View $view file view on the root folder
1066
+     * @return integer size of the folder
1067
+     */
1068
+    private static function calculateSize($view) {
1069
+        $root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1070
+        if (!file_exists($root)) {
1071
+            return 0;
1072
+        }
1073
+        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1074
+        $size = 0;
1075
+
1076
+        /**
1077
+         * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1078
+         * This bug is fixed in PHP 5.5.9 or before
1079
+         * See #8376
1080
+         */
1081
+        $iterator->rewind();
1082
+        while ($iterator->valid()) {
1083
+            $path = $iterator->current();
1084
+            $relpath = substr($path, strlen($root) - 1);
1085
+            if (!$view->is_dir($relpath)) {
1086
+                $size += $view->filesize($relpath);
1087
+            }
1088
+            $iterator->next();
1089
+        }
1090
+        return $size;
1091
+    }
1092
+
1093
+    /**
1094
+     * get current size of trash bin from a given user
1095
+     *
1096
+     * @param string $user user who owns the trash bin
1097
+     * @return integer trash bin size
1098
+     */
1099
+    private static function getTrashbinSize($user) {
1100
+        $view = new View('/' . $user);
1101
+        $fileInfo = $view->getFileInfo('/files_trashbin');
1102
+        return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1103
+    }
1104
+
1105
+    /**
1106
+     * check if trash bin is empty for a given user
1107
+     *
1108
+     * @param string $user
1109
+     * @return bool
1110
+     */
1111
+    public static function isEmpty($user) {
1112
+        $view = new View('/' . $user . '/files_trashbin');
1113
+        if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1114
+            while ($file = readdir($dh)) {
1115
+                if (!Filesystem::isIgnoredDir($file)) {
1116
+                    return false;
1117
+                }
1118
+            }
1119
+        }
1120
+        return true;
1121
+    }
1122
+
1123
+    /**
1124
+     * @param $path
1125
+     * @return string
1126
+     */
1127
+    public static function preview_icon($path) {
1128
+        return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1129
+    }
1130 1130
 }
Please login to merge, or discard this patch.
Spacing   +73 added lines, -73 removed lines patch added patch discarded remove patch
@@ -112,7 +112,7 @@  discard block
 block discarded – undo
112 112
 		Filesystem::initMountPoints($uid);
113 113
 		if ($uid !== User::getUser()) {
114 114
 			$info = Filesystem::getFileInfo($filename);
115
-			$ownerView = new View('/' . $uid . '/files');
115
+			$ownerView = new View('/'.$uid.'/files');
116 116
 			try {
117 117
 				$filename = $ownerView->getPath($info['fileid']);
118 118
 			} catch (NotFoundException $e) {
@@ -174,7 +174,7 @@  discard block
 block discarded – undo
174 174
 	}
175 175
 
176 176
 	private static function setUpTrash($user) {
177
-		$view = new View('/' . $user);
177
+		$view = new View('/'.$user);
178 178
 		if (!$view->is_dir('files_trashbin')) {
179 179
 			$view->mkdir('files_trashbin');
180 180
 		}
@@ -209,8 +209,8 @@  discard block
 block discarded – undo
209 209
 
210 210
 		$view = new View('/');
211 211
 
212
-		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
213
-		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
212
+		$target = $user.'/files_trashbin/files/'.$targetFilename.'.d'.$timestamp;
213
+		$source = $owner.'/files_trashbin/files/'.$sourceFilename.'.d'.$timestamp;
214 214
 		$free = $view->free_space($target);
215 215
 		$isUnknownOrUnlimitedFreeSpace = $free < 0;
216 216
 		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
@@ -254,9 +254,9 @@  discard block
 block discarded – undo
254 254
 			$ownerPath = $file_path;
255 255
 		}
256 256
 
257
-		$ownerView = new View('/' . $owner);
257
+		$ownerView = new View('/'.$owner);
258 258
 		// file has been deleted in between
259
-		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
259
+		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/'.$ownerPath)) {
260 260
 			return true;
261 261
 		}
262 262
 
@@ -277,7 +277,7 @@  discard block
 block discarded – undo
277 277
 		$lockingProvider = \OC::$server->getLockingProvider();
278 278
 
279 279
 		// disable proxy to prevent recursive calls
280
-		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
280
+		$trashPath = '/files_trashbin/files/'.$filename.'.d'.$timestamp;
281 281
 		$gotLock = false;
282 282
 
283 283
 		while (!$gotLock) {
@@ -293,12 +293,12 @@  discard block
 block discarded – undo
293 293
 
294 294
 				$timestamp = $timestamp + 1;
295 295
 
296
-				$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
296
+				$trashPath = '/files_trashbin/files/'.$filename.'.d'.$timestamp;
297 297
 			}
298 298
 		}
299 299
 
300 300
 		/** @var \OC\Files\Storage\Storage $sourceStorage */
301
-		[$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath);
301
+		[$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/'.$ownerPath);
302 302
 
303 303
 
304 304
 		if ($trashStorage->file_exists($trashInternalPath)) {
@@ -306,8 +306,8 @@  discard block
 block discarded – undo
306 306
 		}
307 307
 
308 308
 		$config = \OC::$server->getConfig();
309
-		$systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
310
-		$userTrashbinSize = (int)$config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1');
309
+		$systemTrashbinSize = (int) $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
310
+		$userTrashbinSize = (int) $config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1');
311 311
 		$configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize;
312 312
 		if ($configuredTrashbinSize >= 0 && $sourceStorage->filesize($sourceInternalPath) >= $configuredTrashbinSize) {
313 313
 			return false;
@@ -327,7 +327,7 @@  discard block
 block discarded – undo
327 327
 			if ($trashStorage->file_exists($trashInternalPath)) {
328 328
 				$trashStorage->unlink($trashInternalPath);
329 329
 			}
330
-			\OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
330
+			\OC::$server->getLogger()->error('Couldn\'t move '.$file_path.' to the trash bin', ['app' => 'files_trashbin']);
331 331
 		}
332 332
 
333 333
 		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
@@ -358,7 +358,7 @@  discard block
 block discarded – undo
358 358
 				\OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
359 359
 			}
360 360
 			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
361
-				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
361
+				'trashPath' => Filesystem::normalizePath($filename.'.d'.$timestamp)]);
362 362
 
363 363
 			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
364 364
 
@@ -393,17 +393,17 @@  discard block
 block discarded – undo
393 393
 			$user = User::getUser();
394 394
 			$rootView = new View('/');
395 395
 
396
-			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
396
+			if ($rootView->is_dir($owner.'/files_versions/'.$ownerPath)) {
397 397
 				if ($owner !== $user) {
398
-					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
398
+					self::copy_recursive($owner.'/files_versions/'.$ownerPath, $owner.'/files_trashbin/versions/'.basename($ownerPath).'.d'.$timestamp, $rootView);
399 399
 				}
400
-				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
400
+				self::move($rootView, $owner.'/files_versions/'.$ownerPath, $user.'/files_trashbin/versions/'.$filename.'.d'.$timestamp);
401 401
 			} elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
402 402
 				foreach ($versions as $v) {
403 403
 					if ($owner !== $user) {
404
-						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
404
+						self::copy($rootView, $owner.'/files_versions'.$v['path'].'.v'.$v['version'], $owner.'/files_trashbin/versions/'.$v['name'].'.v'.$v['version'].'.d'.$timestamp);
405 405
 					}
406
-					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
406
+					self::move($rootView, $owner.'/files_versions'.$v['path'].'.v'.$v['version'], $user.'/files_trashbin/versions/'.$filename.'.v'.$v['version'].'.d'.$timestamp);
407 407
 				}
408 408
 			}
409 409
 		}
@@ -465,18 +465,18 @@  discard block
 block discarded – undo
465 465
 	 */
466 466
 	public static function restore($file, $filename, $timestamp) {
467 467
 		$user = User::getUser();
468
-		$view = new View('/' . $user);
468
+		$view = new View('/'.$user);
469 469
 
470 470
 		$location = '';
471 471
 		if ($timestamp) {
472 472
 			$location = self::getLocation($user, $filename, $timestamp);
473 473
 			if ($location === false) {
474
-				\OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
474
+				\OC::$server->getLogger()->error('trash bin database inconsistent! ($user: '.$user.' $filename: '.$filename.', $timestamp: '.$timestamp.')', ['app' => 'files_trashbin']);
475 475
 			} else {
476 476
 				// if location no longer exists, restore file in the root directory
477 477
 				if ($location !== '/' &&
478
-					(!$view->is_dir('files/' . $location) ||
479
-						!$view->isCreatable('files/' . $location))
478
+					(!$view->is_dir('files/'.$location) ||
479
+						!$view->isCreatable('files/'.$location))
480 480
 				) {
481 481
 					$location = '';
482 482
 				}
@@ -486,8 +486,8 @@  discard block
 block discarded – undo
486 486
 		// we need a  extension in case a file/dir with the same name already exists
487 487
 		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
488 488
 
489
-		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
490
-		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
489
+		$source = Filesystem::normalizePath('files_trashbin/files/'.$file);
490
+		$target = Filesystem::normalizePath('files/'.$location.'/'.$uniqueFilename);
491 491
 		if (!$view->file_exists($source)) {
492 492
 			return false;
493 493
 		}
@@ -502,10 +502,10 @@  discard block
 block discarded – undo
502 502
 		// handle the restore result
503 503
 		if ($restoreResult) {
504 504
 			$fakeRoot = $view->getRoot();
505
-			$view->chroot('/' . $user . '/files');
506
-			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
505
+			$view->chroot('/'.$user.'/files');
506
+			$view->touch('/'.$location.'/'.$uniqueFilename, $mtime);
507 507
 			$view->chroot($fakeRoot);
508
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
508
+			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/'.$location.'/'.$uniqueFilename),
509 509
 				'trashPath' => Filesystem::normalizePath($file)]);
510 510
 
511 511
 			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
@@ -541,7 +541,7 @@  discard block
 block discarded – undo
541 541
 			$user = User::getUser();
542 542
 			$rootView = new View('/');
543 543
 
544
-			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
544
+			$target = Filesystem::normalizePath('/'.$location.'/'.$uniqueFilename);
545 545
 
546 546
 			[$owner, $ownerPath] = self::getUidAndFilename($target);
547 547
 
@@ -556,14 +556,14 @@  discard block
 block discarded – undo
556 556
 				$versionedFile = $file;
557 557
 			}
558 558
 
559
-			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
560
-				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
559
+			if ($view->is_dir('/files_trashbin/versions/'.$file)) {
560
+				$rootView->rename(Filesystem::normalizePath($user.'/files_trashbin/versions/'.$file), Filesystem::normalizePath($owner.'/files_versions/'.$ownerPath));
561 561
 			} elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
562 562
 				foreach ($versions as $v) {
563 563
 					if ($timestamp) {
564
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
564
+						$rootView->rename($user.'/files_trashbin/versions/'.$versionedFile.'.v'.$v.'.d'.$timestamp, $owner.'/files_versions/'.$ownerPath.'.v'.$v);
565 565
 					} else {
566
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
566
+						$rootView->rename($user.'/files_trashbin/versions/'.$versionedFile.'.v'.$v, $owner.'/files_versions/'.$ownerPath.'.v'.$v);
567 567
 					}
568 568
 				}
569 569
 			}
@@ -576,7 +576,7 @@  discard block
 block discarded – undo
576 576
 	public static function deleteAll() {
577 577
 		$user = User::getUser();
578 578
 		$userRoot = \OC::$server->getUserFolder($user)->getParent();
579
-		$view = new View('/' . $user);
579
+		$view = new View('/'.$user);
580 580
 		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
581 581
 
582 582
 		try {
@@ -651,7 +651,7 @@  discard block
 block discarded – undo
651 651
 	 */
652 652
 	public static function delete($filename, $user, $timestamp = null) {
653 653
 		$userRoot = \OC::$server->getUserFolder($user)->getParent();
654
-		$view = new View('/' . $user);
654
+		$view = new View('/'.$user);
655 655
 		$size = 0;
656 656
 
657 657
 		if ($timestamp) {
@@ -662,7 +662,7 @@  discard block
 block discarded – undo
662 662
 				->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
663 663
 			$query->executeStatement();
664 664
 
665
-			$file = $filename . '.d' . $timestamp;
665
+			$file = $filename.'.d'.$timestamp;
666 666
 		} else {
667 667
 			$file = $filename;
668 668
 		}
@@ -670,20 +670,20 @@  discard block
 block discarded – undo
670 670
 		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
671 671
 
672 672
 		try {
673
-			$node = $userRoot->get('/files_trashbin/files/' . $file);
673
+			$node = $userRoot->get('/files_trashbin/files/'.$file);
674 674
 		} catch (NotFoundException $e) {
675 675
 			return $size;
676 676
 		}
677 677
 
678 678
 		if ($node instanceof Folder) {
679
-			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
679
+			$size += self::calculateSize(new View('/'.$user.'/files_trashbin/files/'.$file));
680 680
 		} elseif ($node instanceof File) {
681
-			$size += $view->filesize('/files_trashbin/files/' . $file);
681
+			$size += $view->filesize('/files_trashbin/files/'.$file);
682 682
 		}
683 683
 
684
-		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
684
+		self::emitTrashbinPreDelete('/files_trashbin/files/'.$file);
685 685
 		$node->delete();
686
-		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
686
+		self::emitTrashbinPostDelete('/files_trashbin/files/'.$file);
687 687
 
688 688
 		return $size;
689 689
 	}
@@ -699,17 +699,17 @@  discard block
 block discarded – undo
699 699
 	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
700 700
 		$size = 0;
701 701
 		if (\OCP\App::isEnabled('files_versions')) {
702
-			if ($view->is_dir('files_trashbin/versions/' . $file)) {
703
-				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
704
-				$view->unlink('files_trashbin/versions/' . $file);
702
+			if ($view->is_dir('files_trashbin/versions/'.$file)) {
703
+				$size += self::calculateSize(new View('/'.$user.'/files_trashbin/versions/'.$file));
704
+				$view->unlink('files_trashbin/versions/'.$file);
705 705
 			} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
706 706
 				foreach ($versions as $v) {
707 707
 					if ($timestamp) {
708
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
709
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
708
+						$size += $view->filesize('/files_trashbin/versions/'.$filename.'.v'.$v.'.d'.$timestamp);
709
+						$view->unlink('/files_trashbin/versions/'.$filename.'.v'.$v.'.d'.$timestamp);
710 710
 					} else {
711
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
712
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
711
+						$size += $view->filesize('/files_trashbin/versions/'.$filename.'.v'.$v);
712
+						$view->unlink('/files_trashbin/versions/'.$filename.'.v'.$v);
713 713
 					}
714 714
 				}
715 715
 			}
@@ -726,13 +726,13 @@  discard block
 block discarded – undo
726 726
 	 */
727 727
 	public static function file_exists($filename, $timestamp = null) {
728 728
 		$user = User::getUser();
729
-		$view = new View('/' . $user);
729
+		$view = new View('/'.$user);
730 730
 
731 731
 		if ($timestamp) {
732
-			$filename = $filename . '.d' . $timestamp;
732
+			$filename = $filename.'.d'.$timestamp;
733 733
 		}
734 734
 
735
-		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
735
+		$target = Filesystem::normalizePath('files_trashbin/files/'.$filename);
736 736
 		return $view->file_exists($target);
737 737
 	}
738 738
 
@@ -758,11 +758,11 @@  discard block
 block discarded – undo
758 758
 	 */
759 759
 	private static function calculateFreeSpace($trashbinSize, $user) {
760 760
 		$config = \OC::$server->getConfig();
761
-		$userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
761
+		$userTrashbinSize = (int) $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
762 762
 		if ($userTrashbinSize > -1) {
763 763
 			return $userTrashbinSize - $trashbinSize;
764 764
 		}
765
-		$systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
765
+		$systemTrashbinSize = (int) $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
766 766
 		if ($systemTrashbinSize > -1) {
767 767
 			return $systemTrashbinSize - $trashbinSize;
768 768
 		}
@@ -871,7 +871,7 @@  discard block
 block discarded – undo
871 871
 			foreach ($files as $file) {
872 872
 				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
873 873
 					$tmp = self::delete($file['name'], $user, $file['mtime']);
874
-					\OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
874
+					\OC::$server->getLogger()->info('remove "'.$file['name'].'" ('.$tmp.'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
875 875
 					$availableSpace += $tmp;
876 876
 					$size += $tmp;
877 877
 				} else {
@@ -902,10 +902,10 @@  discard block
 block discarded – undo
902 902
 					$size += self::delete($filename, $user, $timestamp);
903 903
 					$count++;
904 904
 				} catch (\OCP\Files\NotPermittedException $e) {
905
-					\OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']);
905
+					\OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "'.$filename.'" from trashbin failed.']);
906 906
 				}
907 907
 				\OC::$server->getLogger()->info(
908
-					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
908
+					'Remove "'.$filename.'" from trashbin because it exceeds max retention obligation term.',
909 909
 					['app' => 'files_trashbin']
910 910
 				);
911 911
 			} else {
@@ -931,16 +931,16 @@  discard block
 block discarded – undo
931 931
 			$view->mkdir($destination);
932 932
 			$view->touch($destination, $view->filemtime($source));
933 933
 			foreach ($view->getDirectoryContent($source) as $i) {
934
-				$pathDir = $source . '/' . $i['name'];
934
+				$pathDir = $source.'/'.$i['name'];
935 935
 				if ($view->is_dir($pathDir)) {
936
-					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
936
+					$size += self::copy_recursive($pathDir, $destination.'/'.$i['name'], $view);
937 937
 				} else {
938 938
 					$size += $view->filesize($pathDir);
939
-					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
939
+					$result = $view->copy($pathDir, $destination.'/'.$i['name']);
940 940
 					if (!$result) {
941 941
 						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
942 942
 					}
943
-					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
943
+					$view->touch($destination.'/'.$i['name'], $view->filemtime($pathDir));
944 944
 				}
945 945
 			}
946 946
 		} else {
@@ -962,11 +962,11 @@  discard block
 block discarded – undo
962 962
 	 * @return array
963 963
 	 */
964 964
 	private static function getVersionsFromTrash($filename, $timestamp, $user) {
965
-		$view = new View('/' . $user . '/files_trashbin/versions');
965
+		$view = new View('/'.$user.'/files_trashbin/versions');
966 966
 		$versions = [];
967 967
 
968 968
 		/** @var \OC\Files\Storage\Storage $storage */
969
-		[$storage,] = $view->resolvePath('/');
969
+		[$storage, ] = $view->resolvePath('/');
970 970
 
971 971
 		//force rescan of versions, local storage may not have updated the cache
972 972
 		if (!self::$scannedVersions) {
@@ -978,7 +978,7 @@  discard block
 block discarded – undo
978 978
 		if ($timestamp) {
979 979
 			// fetch for old versions
980 980
 			$escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
981
-			$pattern .= '.v%.d' . $escapedTimestamp;
981
+			$pattern .= '.v%.d'.$escapedTimestamp;
982 982
 			$offset = -strlen($escapedTimestamp) - 2;
983 983
 		} else {
984 984
 			$pattern .= '.v%';
@@ -992,7 +992,7 @@  discard block
 block discarded – undo
992 992
 			\OC::$server->getLogger(),
993 993
 			$cache
994 994
 		);
995
-		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
995
+		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'.$filename)), '/');
996 996
 		$parentId = $cache->getId($normalizedParentPath);
997 997
 		if ($parentId === -1) {
998 998
 			return [];
@@ -1008,7 +1008,7 @@  discard block
 block discarded – undo
1008 1008
 		$result->closeCursor();
1009 1009
 
1010 1010
 		/** @var CacheEntry[] $matches */
1011
-		$matches = array_map(function (array $data) {
1011
+		$matches = array_map(function(array $data) {
1012 1012
 			return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
1013 1013
 		}, $entries);
1014 1014
 
@@ -1038,18 +1038,18 @@  discard block
 block discarded – undo
1038 1038
 		$name = pathinfo($filename, PATHINFO_FILENAME);
1039 1039
 		$l = \OC::$server->getL10N('files_trashbin');
1040 1040
 
1041
-		$location = '/' . trim($location, '/');
1041
+		$location = '/'.trim($location, '/');
1042 1042
 
1043 1043
 		// if extension is not empty we set a dot in front of it
1044 1044
 		if ($ext !== '') {
1045
-			$ext = '.' . $ext;
1045
+			$ext = '.'.$ext;
1046 1046
 		}
1047 1047
 
1048
-		if ($view->file_exists('files' . $location . '/' . $filename)) {
1048
+		if ($view->file_exists('files'.$location.'/'.$filename)) {
1049 1049
 			$i = 2;
1050
-			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1051
-			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1052
-				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1050
+			$uniqueName = $name." (".$l->t("restored").")".$ext;
1051
+			while ($view->file_exists('files'.$location.'/'.$uniqueName)) {
1052
+				$uniqueName = $name." (".$l->t("restored")." ".$i.")".$ext;
1053 1053
 				$i++;
1054 1054
 			}
1055 1055
 
@@ -1066,7 +1066,7 @@  discard block
 block discarded – undo
1066 1066
 	 * @return integer size of the folder
1067 1067
 	 */
1068 1068
 	private static function calculateSize($view) {
1069
-		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1069
+		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data').$view->getAbsolutePath('');
1070 1070
 		if (!file_exists($root)) {
1071 1071
 			return 0;
1072 1072
 		}
@@ -1097,7 +1097,7 @@  discard block
 block discarded – undo
1097 1097
 	 * @return integer trash bin size
1098 1098
 	 */
1099 1099
 	private static function getTrashbinSize($user) {
1100
-		$view = new View('/' . $user);
1100
+		$view = new View('/'.$user);
1101 1101
 		$fileInfo = $view->getFileInfo('/files_trashbin');
1102 1102
 		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1103 1103
 	}
@@ -1109,7 +1109,7 @@  discard block
 block discarded – undo
1109 1109
 	 * @return bool
1110 1110
 	 */
1111 1111
 	public static function isEmpty($user) {
1112
-		$view = new View('/' . $user . '/files_trashbin');
1112
+		$view = new View('/'.$user.'/files_trashbin');
1113 1113
 		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1114 1114
 			while ($file = readdir($dh)) {
1115 1115
 				if (!Filesystem::isIgnoredDir($file)) {
Please login to merge, or discard this patch.
apps/federatedfilesharing/lib/Controller/RequestHandlerController.php 1 patch
Indentation   +400 added lines, -400 removed lines patch added patch discarded remove patch
@@ -50,404 +50,404 @@
 block discarded – undo
50 50
 
51 51
 class RequestHandlerController extends OCSController {
52 52
 
53
-	/** @var FederatedShareProvider */
54
-	private $federatedShareProvider;
55
-
56
-	/** @var IDBConnection */
57
-	private $connection;
58
-
59
-	/** @var Share\IManager */
60
-	private $shareManager;
61
-
62
-	/** @var Notifications */
63
-	private $notifications;
64
-
65
-	/** @var AddressHandler */
66
-	private $addressHandler;
67
-
68
-	/** @var  IUserManager */
69
-	private $userManager;
70
-
71
-	/** @var string */
72
-	private $shareTable = 'share';
73
-
74
-	/** @var ICloudIdManager */
75
-	private $cloudIdManager;
76
-
77
-	/** @var LoggerInterface */
78
-	private $logger;
79
-
80
-	/** @var ICloudFederationFactory */
81
-	private $cloudFederationFactory;
82
-
83
-	/** @var ICloudFederationProviderManager */
84
-	private $cloudFederationProviderManager;
85
-
86
-	public function __construct(string $appName,
87
-								IRequest $request,
88
-								FederatedShareProvider $federatedShareProvider,
89
-								IDBConnection $connection,
90
-								Share\IManager $shareManager,
91
-								Notifications $notifications,
92
-								AddressHandler $addressHandler,
93
-								IUserManager $userManager,
94
-								ICloudIdManager $cloudIdManager,
95
-								LoggerInterface $logger,
96
-								ICloudFederationFactory $cloudFederationFactory,
97
-								ICloudFederationProviderManager $cloudFederationProviderManager
98
-	) {
99
-		parent::__construct($appName, $request);
100
-
101
-		$this->federatedShareProvider = $federatedShareProvider;
102
-		$this->connection = $connection;
103
-		$this->shareManager = $shareManager;
104
-		$this->notifications = $notifications;
105
-		$this->addressHandler = $addressHandler;
106
-		$this->userManager = $userManager;
107
-		$this->cloudIdManager = $cloudIdManager;
108
-		$this->logger = $logger;
109
-		$this->cloudFederationFactory = $cloudFederationFactory;
110
-		$this->cloudFederationProviderManager = $cloudFederationProviderManager;
111
-	}
112
-
113
-	/**
114
-	 * @NoCSRFRequired
115
-	 * @PublicPage
116
-	 *
117
-	 * create a new share
118
-	 *
119
-	 * @return Http\DataResponse
120
-	 * @throws OCSException
121
-	 */
122
-	public function createShare() {
123
-		$remote = isset($_POST['remote']) ? $_POST['remote'] : null;
124
-		$token = isset($_POST['token']) ? $_POST['token'] : null;
125
-		$name = isset($_POST['name']) ? $_POST['name'] : null;
126
-		$owner = isset($_POST['owner']) ? $_POST['owner'] : null;
127
-		$sharedBy = isset($_POST['sharedBy']) ? $_POST['sharedBy'] : null;
128
-		$shareWith = isset($_POST['shareWith']) ? $_POST['shareWith'] : null;
129
-		$remoteId = isset($_POST['remoteId']) ? (int)$_POST['remoteId'] : null;
130
-		$sharedByFederatedId = isset($_POST['sharedByFederatedId']) ? $_POST['sharedByFederatedId'] : null;
131
-		$ownerFederatedId = isset($_POST['ownerFederatedId']) ? $_POST['ownerFederatedId'] : null;
132
-
133
-		if ($ownerFederatedId === null) {
134
-			$ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId();
135
-		}
136
-		// if the owner of the share and the initiator are the same user
137
-		// we also complete the federated share ID for the initiator
138
-		if ($sharedByFederatedId === null && $owner === $sharedBy) {
139
-			$sharedByFederatedId = $ownerFederatedId;
140
-		}
141
-
142
-		$share = $this->cloudFederationFactory->getCloudFederationShare(
143
-			$shareWith,
144
-			$name,
145
-			'',
146
-			$remoteId,
147
-			$ownerFederatedId,
148
-			$owner,
149
-			$sharedByFederatedId,
150
-			$sharedBy,
151
-			$token,
152
-			'user',
153
-			'file'
154
-		);
155
-
156
-		try {
157
-			$provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
158
-			$provider->shareReceived($share);
159
-		} catch (ProviderDoesNotExistsException $e) {
160
-			throw new OCSException('Server does not support federated cloud sharing', 503);
161
-		} catch (ProviderCouldNotAddShareException $e) {
162
-			throw new OCSException($e->getMessage(), 400);
163
-		} catch (\Exception $e) {
164
-			throw new OCSException('internal server error, was not able to add share from ' . $remote, 500);
165
-		}
166
-
167
-		return new Http\DataResponse();
168
-	}
169
-
170
-	/**
171
-	 * @NoCSRFRequired
172
-	 * @PublicPage
173
-	 *
174
-	 * create re-share on behalf of another user
175
-	 *
176
-	 * @param int $id
177
-	 * @return Http\DataResponse
178
-	 * @throws OCSBadRequestException
179
-	 * @throws OCSException
180
-	 * @throws OCSForbiddenException
181
-	 */
182
-	public function reShare($id) {
183
-		$token = $this->request->getParam('token', null);
184
-		$shareWith = $this->request->getParam('shareWith', null);
185
-		$permission = (int)$this->request->getParam('permission', null);
186
-		$remoteId = (int)$this->request->getParam('remoteId', null);
187
-
188
-		if ($id === null ||
189
-			$token === null ||
190
-			$shareWith === null ||
191
-			$permission === null ||
192
-			$remoteId === null
193
-		) {
194
-			throw new OCSBadRequestException();
195
-		}
196
-
197
-		$notification = [
198
-			'sharedSecret' => $token,
199
-			'shareWith' => $shareWith,
200
-			'senderId' => $remoteId,
201
-			'message' => 'Recipient of a share ask the owner to reshare the file'
202
-		];
203
-
204
-		try {
205
-			$provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
206
-			[$newToken, $localId] = $provider->notificationReceived('REQUEST_RESHARE', $id, $notification);
207
-			return new Http\DataResponse([
208
-				'token' => $newToken,
209
-				'remoteId' => $localId
210
-			]);
211
-		} catch (ProviderDoesNotExistsException $e) {
212
-			throw new OCSException('Server does not support federated cloud sharing', 503);
213
-		} catch (ShareNotFound $e) {
214
-			$this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
215
-		} catch (\Exception $e) {
216
-			$this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
217
-		}
218
-
219
-		throw new OCSBadRequestException();
220
-	}
221
-
222
-
223
-	/**
224
-	 * @NoCSRFRequired
225
-	 * @PublicPage
226
-	 *
227
-	 * accept server-to-server share
228
-	 *
229
-	 * @param int $id
230
-	 * @return Http\DataResponse
231
-	 * @throws OCSException
232
-	 * @throws ShareNotFound
233
-	 * @throws \OC\HintException
234
-	 */
235
-	public function acceptShare($id) {
236
-		$token = isset($_POST['token']) ? $_POST['token'] : null;
237
-
238
-		$notification = [
239
-			'sharedSecret' => $token,
240
-			'message' => 'Recipient accept the share'
241
-		];
242
-
243
-		try {
244
-			$provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
245
-			$provider->notificationReceived('SHARE_ACCEPTED', $id, $notification);
246
-		} catch (ProviderDoesNotExistsException $e) {
247
-			throw new OCSException('Server does not support federated cloud sharing', 503);
248
-		} catch (ShareNotFound $e) {
249
-			$this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
250
-		} catch (\Exception $e) {
251
-			$this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
252
-		}
253
-
254
-		return new Http\DataResponse();
255
-	}
256
-
257
-	/**
258
-	 * @NoCSRFRequired
259
-	 * @PublicPage
260
-	 *
261
-	 * decline server-to-server share
262
-	 *
263
-	 * @param int $id
264
-	 * @return Http\DataResponse
265
-	 * @throws OCSException
266
-	 */
267
-	public function declineShare($id) {
268
-		$token = isset($_POST['token']) ? $_POST['token'] : null;
269
-
270
-		$notification = [
271
-			'sharedSecret' => $token,
272
-			'message' => 'Recipient declined the share'
273
-		];
274
-
275
-		try {
276
-			$provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
277
-			$provider->notificationReceived('SHARE_DECLINED', $id, $notification);
278
-		} catch (ProviderDoesNotExistsException $e) {
279
-			throw new OCSException('Server does not support federated cloud sharing', 503);
280
-		} catch (ShareNotFound $e) {
281
-			$this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
282
-		} catch (\Exception $e) {
283
-			$this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
284
-		}
285
-
286
-		return new Http\DataResponse();
287
-	}
288
-
289
-	/**
290
-	 * @NoCSRFRequired
291
-	 * @PublicPage
292
-	 *
293
-	 * remove server-to-server share if it was unshared by the owner
294
-	 *
295
-	 * @param int $id
296
-	 * @return Http\DataResponse
297
-	 * @throws OCSException
298
-	 */
299
-	public function unshare($id) {
300
-		if (!$this->isS2SEnabled()) {
301
-			throw new OCSException('Server does not support federated cloud sharing', 503);
302
-		}
303
-
304
-		$token = isset($_POST['token']) ? $_POST['token'] : null;
305
-
306
-		try {
307
-			$provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
308
-			$notification = ['sharedSecret' => $token];
309
-			$provider->notificationReceived('SHARE_UNSHARED', $id, $notification);
310
-		} catch (\Exception $e) {
311
-			$this->logger->debug('processing unshare notification failed: ' . $e->getMessage(), ['exception' => $e]);
312
-		}
313
-
314
-		return new Http\DataResponse();
315
-	}
316
-
317
-	private function cleanupRemote($remote) {
318
-		$remote = substr($remote, strpos($remote, '://') + 3);
319
-
320
-		return rtrim($remote, '/');
321
-	}
322
-
323
-
324
-	/**
325
-	 * @NoCSRFRequired
326
-	 * @PublicPage
327
-	 *
328
-	 * federated share was revoked, either by the owner or the re-sharer
329
-	 *
330
-	 * @param int $id
331
-	 * @return Http\DataResponse
332
-	 * @throws OCSBadRequestException
333
-	 */
334
-	public function revoke($id) {
335
-		$token = $this->request->getParam('token');
336
-
337
-		try {
338
-			$provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
339
-			$notification = ['sharedSecret' => $token];
340
-			$provider->notificationReceived('RESHARE_UNDO', $id, $notification);
341
-			return new Http\DataResponse();
342
-		} catch (\Exception $e) {
343
-			throw new OCSBadRequestException();
344
-		}
345
-	}
346
-
347
-	/**
348
-	 * check if server-to-server sharing is enabled
349
-	 *
350
-	 * @param bool $incoming
351
-	 * @return bool
352
-	 */
353
-	private function isS2SEnabled($incoming = false) {
354
-		$result = \OCP\App::isEnabled('files_sharing');
355
-
356
-		if ($incoming) {
357
-			$result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
358
-		} else {
359
-			$result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
360
-		}
361
-
362
-		return $result;
363
-	}
364
-
365
-	/**
366
-	 * @NoCSRFRequired
367
-	 * @PublicPage
368
-	 *
369
-	 * update share information to keep federated re-shares in sync
370
-	 *
371
-	 * @param int $id
372
-	 * @return Http\DataResponse
373
-	 * @throws OCSBadRequestException
374
-	 */
375
-	public function updatePermissions($id) {
376
-		$token = $this->request->getParam('token', null);
377
-		$ncPermissions = $this->request->getParam('permissions', null);
378
-
379
-		try {
380
-			$provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
381
-			$ocmPermissions = $this->ncPermissions2ocmPermissions((int)$ncPermissions);
382
-			$notification = ['sharedSecret' => $token, 'permission' => $ocmPermissions];
383
-			$provider->notificationReceived('RESHARE_CHANGE_PERMISSION', $id, $notification);
384
-		} catch (\Exception $e) {
385
-			$this->logger->debug($e->getMessage(), ['exception' => $e]);
386
-			throw new OCSBadRequestException();
387
-		}
388
-
389
-		return new Http\DataResponse();
390
-	}
391
-
392
-	/**
393
-	 * translate Nextcloud permissions to OCM Permissions
394
-	 *
395
-	 * @param $ncPermissions
396
-	 * @return array
397
-	 */
398
-	protected function ncPermissions2ocmPermissions($ncPermissions) {
399
-		$ocmPermissions = [];
400
-
401
-		if ($ncPermissions & Constants::PERMISSION_SHARE) {
402
-			$ocmPermissions[] = 'share';
403
-		}
404
-
405
-		if ($ncPermissions & Constants::PERMISSION_READ) {
406
-			$ocmPermissions[] = 'read';
407
-		}
408
-
409
-		if (($ncPermissions & Constants::PERMISSION_CREATE) ||
410
-			($ncPermissions & Constants::PERMISSION_UPDATE)) {
411
-			$ocmPermissions[] = 'write';
412
-		}
413
-
414
-		return $ocmPermissions;
415
-	}
416
-
417
-	/**
418
-	 * @NoCSRFRequired
419
-	 * @PublicPage
420
-	 *
421
-	 * change the owner of a server-to-server share
422
-	 *
423
-	 * @param int $id
424
-	 * @return Http\DataResponse
425
-	 * @throws \InvalidArgumentException
426
-	 * @throws OCSException
427
-	 */
428
-	public function move($id) {
429
-		if (!$this->isS2SEnabled()) {
430
-			throw new OCSException('Server does not support federated cloud sharing', 503);
431
-		}
432
-
433
-		$token = $this->request->getParam('token');
434
-		$remote = $this->request->getParam('remote');
435
-		$newRemoteId = $this->request->getParam('remote_id', $id);
436
-		$cloudId = $this->cloudIdManager->resolveCloudId($remote);
437
-
438
-		$qb = $this->connection->getQueryBuilder();
439
-		$query = $qb->update('share_external')
440
-			->set('remote', $qb->createNamedParameter($cloudId->getRemote()))
441
-			->set('owner', $qb->createNamedParameter($cloudId->getUser()))
442
-			->set('remote_id', $qb->createNamedParameter($newRemoteId))
443
-			->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id)))
444
-			->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token)));
445
-		$affected = $query->executeStatement();
446
-
447
-		if ($affected > 0) {
448
-			return new Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]);
449
-		} else {
450
-			throw new OCSBadRequestException('Share not found or token invalid');
451
-		}
452
-	}
53
+    /** @var FederatedShareProvider */
54
+    private $federatedShareProvider;
55
+
56
+    /** @var IDBConnection */
57
+    private $connection;
58
+
59
+    /** @var Share\IManager */
60
+    private $shareManager;
61
+
62
+    /** @var Notifications */
63
+    private $notifications;
64
+
65
+    /** @var AddressHandler */
66
+    private $addressHandler;
67
+
68
+    /** @var  IUserManager */
69
+    private $userManager;
70
+
71
+    /** @var string */
72
+    private $shareTable = 'share';
73
+
74
+    /** @var ICloudIdManager */
75
+    private $cloudIdManager;
76
+
77
+    /** @var LoggerInterface */
78
+    private $logger;
79
+
80
+    /** @var ICloudFederationFactory */
81
+    private $cloudFederationFactory;
82
+
83
+    /** @var ICloudFederationProviderManager */
84
+    private $cloudFederationProviderManager;
85
+
86
+    public function __construct(string $appName,
87
+                                IRequest $request,
88
+                                FederatedShareProvider $federatedShareProvider,
89
+                                IDBConnection $connection,
90
+                                Share\IManager $shareManager,
91
+                                Notifications $notifications,
92
+                                AddressHandler $addressHandler,
93
+                                IUserManager $userManager,
94
+                                ICloudIdManager $cloudIdManager,
95
+                                LoggerInterface $logger,
96
+                                ICloudFederationFactory $cloudFederationFactory,
97
+                                ICloudFederationProviderManager $cloudFederationProviderManager
98
+    ) {
99
+        parent::__construct($appName, $request);
100
+
101
+        $this->federatedShareProvider = $federatedShareProvider;
102
+        $this->connection = $connection;
103
+        $this->shareManager = $shareManager;
104
+        $this->notifications = $notifications;
105
+        $this->addressHandler = $addressHandler;
106
+        $this->userManager = $userManager;
107
+        $this->cloudIdManager = $cloudIdManager;
108
+        $this->logger = $logger;
109
+        $this->cloudFederationFactory = $cloudFederationFactory;
110
+        $this->cloudFederationProviderManager = $cloudFederationProviderManager;
111
+    }
112
+
113
+    /**
114
+     * @NoCSRFRequired
115
+     * @PublicPage
116
+     *
117
+     * create a new share
118
+     *
119
+     * @return Http\DataResponse
120
+     * @throws OCSException
121
+     */
122
+    public function createShare() {
123
+        $remote = isset($_POST['remote']) ? $_POST['remote'] : null;
124
+        $token = isset($_POST['token']) ? $_POST['token'] : null;
125
+        $name = isset($_POST['name']) ? $_POST['name'] : null;
126
+        $owner = isset($_POST['owner']) ? $_POST['owner'] : null;
127
+        $sharedBy = isset($_POST['sharedBy']) ? $_POST['sharedBy'] : null;
128
+        $shareWith = isset($_POST['shareWith']) ? $_POST['shareWith'] : null;
129
+        $remoteId = isset($_POST['remoteId']) ? (int)$_POST['remoteId'] : null;
130
+        $sharedByFederatedId = isset($_POST['sharedByFederatedId']) ? $_POST['sharedByFederatedId'] : null;
131
+        $ownerFederatedId = isset($_POST['ownerFederatedId']) ? $_POST['ownerFederatedId'] : null;
132
+
133
+        if ($ownerFederatedId === null) {
134
+            $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId();
135
+        }
136
+        // if the owner of the share and the initiator are the same user
137
+        // we also complete the federated share ID for the initiator
138
+        if ($sharedByFederatedId === null && $owner === $sharedBy) {
139
+            $sharedByFederatedId = $ownerFederatedId;
140
+        }
141
+
142
+        $share = $this->cloudFederationFactory->getCloudFederationShare(
143
+            $shareWith,
144
+            $name,
145
+            '',
146
+            $remoteId,
147
+            $ownerFederatedId,
148
+            $owner,
149
+            $sharedByFederatedId,
150
+            $sharedBy,
151
+            $token,
152
+            'user',
153
+            'file'
154
+        );
155
+
156
+        try {
157
+            $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
158
+            $provider->shareReceived($share);
159
+        } catch (ProviderDoesNotExistsException $e) {
160
+            throw new OCSException('Server does not support federated cloud sharing', 503);
161
+        } catch (ProviderCouldNotAddShareException $e) {
162
+            throw new OCSException($e->getMessage(), 400);
163
+        } catch (\Exception $e) {
164
+            throw new OCSException('internal server error, was not able to add share from ' . $remote, 500);
165
+        }
166
+
167
+        return new Http\DataResponse();
168
+    }
169
+
170
+    /**
171
+     * @NoCSRFRequired
172
+     * @PublicPage
173
+     *
174
+     * create re-share on behalf of another user
175
+     *
176
+     * @param int $id
177
+     * @return Http\DataResponse
178
+     * @throws OCSBadRequestException
179
+     * @throws OCSException
180
+     * @throws OCSForbiddenException
181
+     */
182
+    public function reShare($id) {
183
+        $token = $this->request->getParam('token', null);
184
+        $shareWith = $this->request->getParam('shareWith', null);
185
+        $permission = (int)$this->request->getParam('permission', null);
186
+        $remoteId = (int)$this->request->getParam('remoteId', null);
187
+
188
+        if ($id === null ||
189
+            $token === null ||
190
+            $shareWith === null ||
191
+            $permission === null ||
192
+            $remoteId === null
193
+        ) {
194
+            throw new OCSBadRequestException();
195
+        }
196
+
197
+        $notification = [
198
+            'sharedSecret' => $token,
199
+            'shareWith' => $shareWith,
200
+            'senderId' => $remoteId,
201
+            'message' => 'Recipient of a share ask the owner to reshare the file'
202
+        ];
203
+
204
+        try {
205
+            $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
206
+            [$newToken, $localId] = $provider->notificationReceived('REQUEST_RESHARE', $id, $notification);
207
+            return new Http\DataResponse([
208
+                'token' => $newToken,
209
+                'remoteId' => $localId
210
+            ]);
211
+        } catch (ProviderDoesNotExistsException $e) {
212
+            throw new OCSException('Server does not support federated cloud sharing', 503);
213
+        } catch (ShareNotFound $e) {
214
+            $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
215
+        } catch (\Exception $e) {
216
+            $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
217
+        }
218
+
219
+        throw new OCSBadRequestException();
220
+    }
221
+
222
+
223
+    /**
224
+     * @NoCSRFRequired
225
+     * @PublicPage
226
+     *
227
+     * accept server-to-server share
228
+     *
229
+     * @param int $id
230
+     * @return Http\DataResponse
231
+     * @throws OCSException
232
+     * @throws ShareNotFound
233
+     * @throws \OC\HintException
234
+     */
235
+    public function acceptShare($id) {
236
+        $token = isset($_POST['token']) ? $_POST['token'] : null;
237
+
238
+        $notification = [
239
+            'sharedSecret' => $token,
240
+            'message' => 'Recipient accept the share'
241
+        ];
242
+
243
+        try {
244
+            $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
245
+            $provider->notificationReceived('SHARE_ACCEPTED', $id, $notification);
246
+        } catch (ProviderDoesNotExistsException $e) {
247
+            throw new OCSException('Server does not support federated cloud sharing', 503);
248
+        } catch (ShareNotFound $e) {
249
+            $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
250
+        } catch (\Exception $e) {
251
+            $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
252
+        }
253
+
254
+        return new Http\DataResponse();
255
+    }
256
+
257
+    /**
258
+     * @NoCSRFRequired
259
+     * @PublicPage
260
+     *
261
+     * decline server-to-server share
262
+     *
263
+     * @param int $id
264
+     * @return Http\DataResponse
265
+     * @throws OCSException
266
+     */
267
+    public function declineShare($id) {
268
+        $token = isset($_POST['token']) ? $_POST['token'] : null;
269
+
270
+        $notification = [
271
+            'sharedSecret' => $token,
272
+            'message' => 'Recipient declined the share'
273
+        ];
274
+
275
+        try {
276
+            $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
277
+            $provider->notificationReceived('SHARE_DECLINED', $id, $notification);
278
+        } catch (ProviderDoesNotExistsException $e) {
279
+            throw new OCSException('Server does not support federated cloud sharing', 503);
280
+        } catch (ShareNotFound $e) {
281
+            $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
282
+        } catch (\Exception $e) {
283
+            $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
284
+        }
285
+
286
+        return new Http\DataResponse();
287
+    }
288
+
289
+    /**
290
+     * @NoCSRFRequired
291
+     * @PublicPage
292
+     *
293
+     * remove server-to-server share if it was unshared by the owner
294
+     *
295
+     * @param int $id
296
+     * @return Http\DataResponse
297
+     * @throws OCSException
298
+     */
299
+    public function unshare($id) {
300
+        if (!$this->isS2SEnabled()) {
301
+            throw new OCSException('Server does not support federated cloud sharing', 503);
302
+        }
303
+
304
+        $token = isset($_POST['token']) ? $_POST['token'] : null;
305
+
306
+        try {
307
+            $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
308
+            $notification = ['sharedSecret' => $token];
309
+            $provider->notificationReceived('SHARE_UNSHARED', $id, $notification);
310
+        } catch (\Exception $e) {
311
+            $this->logger->debug('processing unshare notification failed: ' . $e->getMessage(), ['exception' => $e]);
312
+        }
313
+
314
+        return new Http\DataResponse();
315
+    }
316
+
317
+    private function cleanupRemote($remote) {
318
+        $remote = substr($remote, strpos($remote, '://') + 3);
319
+
320
+        return rtrim($remote, '/');
321
+    }
322
+
323
+
324
+    /**
325
+     * @NoCSRFRequired
326
+     * @PublicPage
327
+     *
328
+     * federated share was revoked, either by the owner or the re-sharer
329
+     *
330
+     * @param int $id
331
+     * @return Http\DataResponse
332
+     * @throws OCSBadRequestException
333
+     */
334
+    public function revoke($id) {
335
+        $token = $this->request->getParam('token');
336
+
337
+        try {
338
+            $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
339
+            $notification = ['sharedSecret' => $token];
340
+            $provider->notificationReceived('RESHARE_UNDO', $id, $notification);
341
+            return new Http\DataResponse();
342
+        } catch (\Exception $e) {
343
+            throw new OCSBadRequestException();
344
+        }
345
+    }
346
+
347
+    /**
348
+     * check if server-to-server sharing is enabled
349
+     *
350
+     * @param bool $incoming
351
+     * @return bool
352
+     */
353
+    private function isS2SEnabled($incoming = false) {
354
+        $result = \OCP\App::isEnabled('files_sharing');
355
+
356
+        if ($incoming) {
357
+            $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
358
+        } else {
359
+            $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
360
+        }
361
+
362
+        return $result;
363
+    }
364
+
365
+    /**
366
+     * @NoCSRFRequired
367
+     * @PublicPage
368
+     *
369
+     * update share information to keep federated re-shares in sync
370
+     *
371
+     * @param int $id
372
+     * @return Http\DataResponse
373
+     * @throws OCSBadRequestException
374
+     */
375
+    public function updatePermissions($id) {
376
+        $token = $this->request->getParam('token', null);
377
+        $ncPermissions = $this->request->getParam('permissions', null);
378
+
379
+        try {
380
+            $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
381
+            $ocmPermissions = $this->ncPermissions2ocmPermissions((int)$ncPermissions);
382
+            $notification = ['sharedSecret' => $token, 'permission' => $ocmPermissions];
383
+            $provider->notificationReceived('RESHARE_CHANGE_PERMISSION', $id, $notification);
384
+        } catch (\Exception $e) {
385
+            $this->logger->debug($e->getMessage(), ['exception' => $e]);
386
+            throw new OCSBadRequestException();
387
+        }
388
+
389
+        return new Http\DataResponse();
390
+    }
391
+
392
+    /**
393
+     * translate Nextcloud permissions to OCM Permissions
394
+     *
395
+     * @param $ncPermissions
396
+     * @return array
397
+     */
398
+    protected function ncPermissions2ocmPermissions($ncPermissions) {
399
+        $ocmPermissions = [];
400
+
401
+        if ($ncPermissions & Constants::PERMISSION_SHARE) {
402
+            $ocmPermissions[] = 'share';
403
+        }
404
+
405
+        if ($ncPermissions & Constants::PERMISSION_READ) {
406
+            $ocmPermissions[] = 'read';
407
+        }
408
+
409
+        if (($ncPermissions & Constants::PERMISSION_CREATE) ||
410
+            ($ncPermissions & Constants::PERMISSION_UPDATE)) {
411
+            $ocmPermissions[] = 'write';
412
+        }
413
+
414
+        return $ocmPermissions;
415
+    }
416
+
417
+    /**
418
+     * @NoCSRFRequired
419
+     * @PublicPage
420
+     *
421
+     * change the owner of a server-to-server share
422
+     *
423
+     * @param int $id
424
+     * @return Http\DataResponse
425
+     * @throws \InvalidArgumentException
426
+     * @throws OCSException
427
+     */
428
+    public function move($id) {
429
+        if (!$this->isS2SEnabled()) {
430
+            throw new OCSException('Server does not support federated cloud sharing', 503);
431
+        }
432
+
433
+        $token = $this->request->getParam('token');
434
+        $remote = $this->request->getParam('remote');
435
+        $newRemoteId = $this->request->getParam('remote_id', $id);
436
+        $cloudId = $this->cloudIdManager->resolveCloudId($remote);
437
+
438
+        $qb = $this->connection->getQueryBuilder();
439
+        $query = $qb->update('share_external')
440
+            ->set('remote', $qb->createNamedParameter($cloudId->getRemote()))
441
+            ->set('owner', $qb->createNamedParameter($cloudId->getUser()))
442
+            ->set('remote_id', $qb->createNamedParameter($newRemoteId))
443
+            ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id)))
444
+            ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token)));
445
+        $affected = $query->executeStatement();
446
+
447
+        if ($affected > 0) {
448
+            return new Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]);
449
+        } else {
450
+            throw new OCSBadRequestException('Share not found or token invalid');
451
+        }
452
+    }
453 453
 }
Please login to merge, or discard this patch.
apps/sharebymail/lib/ShareByMailProvider.php 2 patches
Indentation   +1118 added lines, -1118 removed lines patch added patch discarded remove patch
@@ -72,1129 +72,1129 @@
 block discarded – undo
72 72
  */
73 73
 class ShareByMailProvider implements IShareProvider {
74 74
 
75
-	/** @var  IDBConnection */
76
-	private $dbConnection;
77
-
78
-	/** @var ILogger */
79
-	private $logger;
80
-
81
-	/** @var ISecureRandom */
82
-	private $secureRandom;
83
-
84
-	/** @var IUserManager */
85
-	private $userManager;
86
-
87
-	/** @var IRootFolder */
88
-	private $rootFolder;
89
-
90
-	/** @var IL10N */
91
-	private $l;
92
-
93
-	/** @var IMailer */
94
-	private $mailer;
95
-
96
-	/** @var IURLGenerator */
97
-	private $urlGenerator;
98
-
99
-	/** @var IManager  */
100
-	private $activityManager;
101
-
102
-	/** @var SettingsManager */
103
-	private $settingsManager;
104
-
105
-	/** @var Defaults */
106
-	private $defaults;
107
-
108
-	/** @var IHasher */
109
-	private $hasher;
110
-
111
-	/** @var IEventDispatcher */
112
-	private $eventDispatcher;
113
-
114
-	/** @var IShareManager */
115
-	private $shareManager;
116
-
117
-	/**
118
-	 * Return the identifier of this provider.
119
-	 *
120
-	 * @return string Containing only [a-zA-Z0-9]
121
-	 */
122
-	public function identifier() {
123
-		return 'ocMailShare';
124
-	}
125
-
126
-	public function __construct(IDBConnection $connection,
127
-								ISecureRandom $secureRandom,
128
-								IUserManager $userManager,
129
-								IRootFolder $rootFolder,
130
-								IL10N $l,
131
-								ILogger $logger,
132
-								IMailer $mailer,
133
-								IURLGenerator $urlGenerator,
134
-								IManager $activityManager,
135
-								SettingsManager $settingsManager,
136
-								Defaults $defaults,
137
-								IHasher $hasher,
138
-								IEventDispatcher $eventDispatcher,
139
-								IShareManager $shareManager) {
140
-		$this->dbConnection = $connection;
141
-		$this->secureRandom = $secureRandom;
142
-		$this->userManager = $userManager;
143
-		$this->rootFolder = $rootFolder;
144
-		$this->l = $l;
145
-		$this->logger = $logger;
146
-		$this->mailer = $mailer;
147
-		$this->urlGenerator = $urlGenerator;
148
-		$this->activityManager = $activityManager;
149
-		$this->settingsManager = $settingsManager;
150
-		$this->defaults = $defaults;
151
-		$this->hasher = $hasher;
152
-		$this->eventDispatcher = $eventDispatcher;
153
-		$this->shareManager = $shareManager;
154
-	}
155
-
156
-	/**
157
-	 * Share a path
158
-	 *
159
-	 * @param IShare $share
160
-	 * @return IShare The share object
161
-	 * @throws ShareNotFound
162
-	 * @throws \Exception
163
-	 */
164
-	public function create(IShare $share) {
165
-		$shareWith = $share->getSharedWith();
166
-		/*
75
+    /** @var  IDBConnection */
76
+    private $dbConnection;
77
+
78
+    /** @var ILogger */
79
+    private $logger;
80
+
81
+    /** @var ISecureRandom */
82
+    private $secureRandom;
83
+
84
+    /** @var IUserManager */
85
+    private $userManager;
86
+
87
+    /** @var IRootFolder */
88
+    private $rootFolder;
89
+
90
+    /** @var IL10N */
91
+    private $l;
92
+
93
+    /** @var IMailer */
94
+    private $mailer;
95
+
96
+    /** @var IURLGenerator */
97
+    private $urlGenerator;
98
+
99
+    /** @var IManager  */
100
+    private $activityManager;
101
+
102
+    /** @var SettingsManager */
103
+    private $settingsManager;
104
+
105
+    /** @var Defaults */
106
+    private $defaults;
107
+
108
+    /** @var IHasher */
109
+    private $hasher;
110
+
111
+    /** @var IEventDispatcher */
112
+    private $eventDispatcher;
113
+
114
+    /** @var IShareManager */
115
+    private $shareManager;
116
+
117
+    /**
118
+     * Return the identifier of this provider.
119
+     *
120
+     * @return string Containing only [a-zA-Z0-9]
121
+     */
122
+    public function identifier() {
123
+        return 'ocMailShare';
124
+    }
125
+
126
+    public function __construct(IDBConnection $connection,
127
+                                ISecureRandom $secureRandom,
128
+                                IUserManager $userManager,
129
+                                IRootFolder $rootFolder,
130
+                                IL10N $l,
131
+                                ILogger $logger,
132
+                                IMailer $mailer,
133
+                                IURLGenerator $urlGenerator,
134
+                                IManager $activityManager,
135
+                                SettingsManager $settingsManager,
136
+                                Defaults $defaults,
137
+                                IHasher $hasher,
138
+                                IEventDispatcher $eventDispatcher,
139
+                                IShareManager $shareManager) {
140
+        $this->dbConnection = $connection;
141
+        $this->secureRandom = $secureRandom;
142
+        $this->userManager = $userManager;
143
+        $this->rootFolder = $rootFolder;
144
+        $this->l = $l;
145
+        $this->logger = $logger;
146
+        $this->mailer = $mailer;
147
+        $this->urlGenerator = $urlGenerator;
148
+        $this->activityManager = $activityManager;
149
+        $this->settingsManager = $settingsManager;
150
+        $this->defaults = $defaults;
151
+        $this->hasher = $hasher;
152
+        $this->eventDispatcher = $eventDispatcher;
153
+        $this->shareManager = $shareManager;
154
+    }
155
+
156
+    /**
157
+     * Share a path
158
+     *
159
+     * @param IShare $share
160
+     * @return IShare The share object
161
+     * @throws ShareNotFound
162
+     * @throws \Exception
163
+     */
164
+    public function create(IShare $share) {
165
+        $shareWith = $share->getSharedWith();
166
+        /*
167 167
 		 * Check if file is not already shared with the remote user
168 168
 		 */
169
-		$alreadyShared = $this->getSharedWith($shareWith, IShare::TYPE_EMAIL, $share->getNode(), 1, 0);
170
-		if (!empty($alreadyShared)) {
171
-			$message = 'Sharing %1$s failed, because this item is already shared with user %2$s';
172
-			$message_t = $this->l->t('Sharing %1$s failed, because this item is already shared with user %2$s', [$share->getNode()->getName(), $shareWith]);
173
-			$this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
174
-			throw new \Exception($message_t);
175
-		}
176
-
177
-		// if the admin enforces a password for all mail shares we create a
178
-		// random password and send it to the recipient
179
-		$password = $share->getPassword() ?: '';
180
-		$passwordEnforced = $this->shareManager->shareApiLinkEnforcePassword();
181
-		if ($passwordEnforced && empty($password)) {
182
-			$password = $this->autoGeneratePassword($share);
183
-		}
184
-
185
-		if (!empty($password)) {
186
-			$share->setPassword($this->hasher->hash($password));
187
-		}
188
-
189
-		$shareId = $this->createMailShare($share);
190
-		$send = $this->sendPassword($share, $password);
191
-		if ($passwordEnforced && $send === false) {
192
-			$this->sendPasswordToOwner($share, $password);
193
-		}
194
-
195
-		$this->createShareActivity($share);
196
-		$data = $this->getRawShare($shareId);
197
-
198
-		return $this->createShareObject($data);
199
-	}
200
-
201
-	/**
202
-	 * auto generate password in case of password enforcement on mail shares
203
-	 *
204
-	 * @param IShare $share
205
-	 * @return string
206
-	 * @throws \Exception
207
-	 */
208
-	protected function autoGeneratePassword($share) {
209
-		$initiatorUser = $this->userManager->get($share->getSharedBy());
210
-		$initiatorEMailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
211
-		$allowPasswordByMail = $this->settingsManager->sendPasswordByMail();
212
-
213
-		if ($initiatorEMailAddress === null && !$allowPasswordByMail) {
214
-			throw new \Exception(
215
-				$this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.")
216
-			);
217
-		}
218
-
219
-		$passwordEvent = new GenerateSecurePasswordEvent();
220
-		$this->eventDispatcher->dispatchTyped($passwordEvent);
221
-
222
-		$password = $passwordEvent->getPassword();
223
-		if ($password === null) {
224
-			$password = $this->secureRandom->generate(8, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS);
225
-		}
226
-
227
-		return $password;
228
-	}
229
-
230
-	/**
231
-	 * create activity if a file/folder was shared by mail
232
-	 *
233
-	 * @param IShare $share
234
-	 * @param string $type
235
-	 */
236
-	protected function createShareActivity(IShare $share, string $type = 'share') {
237
-		$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
238
-
239
-		$this->publishActivity(
240
-			$type === 'share' ? Activity::SUBJECT_SHARED_EMAIL_SELF : Activity::SUBJECT_UNSHARED_EMAIL_SELF,
241
-			[$userFolder->getRelativePath($share->getNode()->getPath()), $share->getSharedWith()],
242
-			$share->getSharedBy(),
243
-			$share->getNode()->getId(),
244
-			(string) $userFolder->getRelativePath($share->getNode()->getPath())
245
-		);
246
-
247
-		if ($share->getShareOwner() !== $share->getSharedBy()) {
248
-			$ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
249
-			$fileId = $share->getNode()->getId();
250
-			$nodes = $ownerFolder->getById($fileId);
251
-			$ownerPath = $nodes[0]->getPath();
252
-			$this->publishActivity(
253
-				$type === 'share' ? Activity::SUBJECT_SHARED_EMAIL_BY : Activity::SUBJECT_UNSHARED_EMAIL_BY,
254
-				[$ownerFolder->getRelativePath($ownerPath), $share->getSharedWith(), $share->getSharedBy()],
255
-				$share->getShareOwner(),
256
-				$fileId,
257
-				(string) $ownerFolder->getRelativePath($ownerPath)
258
-			);
259
-		}
260
-	}
261
-
262
-	/**
263
-	 * create activity if a file/folder was shared by mail
264
-	 *
265
-	 * @param IShare $share
266
-	 * @param string $sharedWith
267
-	 * @param bool $sendToSelf
268
-	 */
269
-	protected function createPasswordSendActivity(IShare $share, $sharedWith, $sendToSelf) {
270
-		$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
271
-
272
-		if ($sendToSelf) {
273
-			$this->publishActivity(
274
-				Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND_SELF,
275
-				[$userFolder->getRelativePath($share->getNode()->getPath())],
276
-				$share->getSharedBy(),
277
-				$share->getNode()->getId(),
278
-				(string) $userFolder->getRelativePath($share->getNode()->getPath())
279
-			);
280
-		} else {
281
-			$this->publishActivity(
282
-				Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND,
283
-				[$userFolder->getRelativePath($share->getNode()->getPath()), $sharedWith],
284
-				$share->getSharedBy(),
285
-				$share->getNode()->getId(),
286
-				(string) $userFolder->getRelativePath($share->getNode()->getPath())
287
-			);
288
-		}
289
-	}
290
-
291
-
292
-	/**
293
-	 * publish activity if a file/folder was shared by mail
294
-	 *
295
-	 * @param string $subject
296
-	 * @param array $parameters
297
-	 * @param string $affectedUser
298
-	 * @param int $fileId
299
-	 * @param string $filePath
300
-	 */
301
-	protected function publishActivity(string $subject, array $parameters, string $affectedUser, int $fileId, string $filePath) {
302
-		$event = $this->activityManager->generateEvent();
303
-		$event->setApp('sharebymail')
304
-			->setType('shared')
305
-			->setSubject($subject, $parameters)
306
-			->setAffectedUser($affectedUser)
307
-			->setObject('files', $fileId, $filePath);
308
-		$this->activityManager->publish($event);
309
-	}
310
-
311
-	/**
312
-	 * @param IShare $share
313
-	 * @return int
314
-	 * @throws \Exception
315
-	 */
316
-	protected function createMailShare(IShare $share) {
317
-		$share->setToken($this->generateToken());
318
-		$shareId = $this->addShareToDB(
319
-			$share->getNodeId(),
320
-			$share->getNodeType(),
321
-			$share->getSharedWith(),
322
-			$share->getSharedBy(),
323
-			$share->getShareOwner(),
324
-			$share->getPermissions(),
325
-			$share->getToken(),
326
-			$share->getPassword(),
327
-			$share->getSendPasswordByTalk(),
328
-			$share->getHideDownload(),
329
-			$share->getLabel(),
330
-			$share->getExpirationDate()
331
-		);
332
-
333
-		try {
334
-			$link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare',
335
-				['token' => $share->getToken()]);
336
-			$this->sendMailNotification(
337
-				$share->getNode()->getName(),
338
-				$link,
339
-				$share->getSharedBy(),
340
-				$share->getSharedWith(),
341
-				$share->getExpirationDate()
342
-			);
343
-		} catch (HintException $hintException) {
344
-			$this->logger->logException($hintException, [
345
-				'message' => 'Failed to send share by mail.',
346
-				'level' => ILogger::ERROR,
347
-				'app' => 'sharebymail',
348
-			]);
349
-			$this->removeShareFromTable($shareId);
350
-			throw $hintException;
351
-		} catch (\Exception $e) {
352
-			$this->logger->logException($e, [
353
-				'message' => 'Failed to send share by mail.',
354
-				'level' => ILogger::ERROR,
355
-				'app' => 'sharebymail',
356
-			]);
357
-			$this->removeShareFromTable($shareId);
358
-			throw new HintException('Failed to send share by mail',
359
-				$this->l->t('Failed to send share by email'));
360
-		}
361
-
362
-		return $shareId;
363
-	}
364
-
365
-	/**
366
-	 * @param string $filename
367
-	 * @param string $link
368
-	 * @param string $initiator
369
-	 * @param string $shareWith
370
-	 * @param \DateTime|null $expiration
371
-	 * @throws \Exception If mail couldn't be sent
372
-	 */
373
-	protected function sendMailNotification($filename,
374
-											$link,
375
-											$initiator,
376
-											$shareWith,
377
-											\DateTime $expiration = null) {
378
-		$initiatorUser = $this->userManager->get($initiator);
379
-		$initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
380
-		$message = $this->mailer->createMessage();
381
-
382
-		$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [
383
-			'filename' => $filename,
384
-			'link' => $link,
385
-			'initiator' => $initiatorDisplayName,
386
-			'expiration' => $expiration,
387
-			'shareWith' => $shareWith,
388
-		]);
389
-
390
-		$emailTemplate->setSubject($this->l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]));
391
-		$emailTemplate->addHeader();
392
-		$emailTemplate->addHeading($this->l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]), false);
393
-		$text = $this->l->t('%1$s shared »%2$s« with you.', [$initiatorDisplayName, $filename]);
394
-
395
-		$emailTemplate->addBodyText(
396
-			htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')),
397
-			$text
398
-		);
399
-		$emailTemplate->addBodyButton(
400
-			$this->l->t('Open »%s«', [$filename]),
401
-			$link
402
-		);
403
-
404
-		$message->setTo([$shareWith]);
405
-
406
-		// The "From" contains the sharers name
407
-		$instanceName = $this->defaults->getName();
408
-		$senderName = $instanceName;
409
-		if ($this->settingsManager->replyToInitiator()) {
410
-			$senderName = $this->l->t(
411
-				'%1$s via %2$s',
412
-				[
413
-					$initiatorDisplayName,
414
-					$instanceName
415
-				]
416
-			);
417
-		}
418
-		$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
419
-
420
-		// The "Reply-To" is set to the sharer if an mail address is configured
421
-		// also the default footer contains a "Do not reply" which needs to be adjusted.
422
-		$initiatorEmail = $initiatorUser->getEMailAddress();
423
-		if ($this->settingsManager->replyToInitiator() && $initiatorEmail !== null) {
424
-			$message->setReplyTo([$initiatorEmail => $initiatorDisplayName]);
425
-			$emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''));
426
-		} else {
427
-			$emailTemplate->addFooter();
428
-		}
429
-
430
-		$message->useTemplate($emailTemplate);
431
-		$this->mailer->send($message);
432
-	}
433
-
434
-	/**
435
-	 * send password to recipient of a mail share
436
-	 *
437
-	 * @param IShare $share
438
-	 * @param string $password
439
-	 * @return bool
440
-	 */
441
-	protected function sendPassword(IShare $share, $password) {
442
-		$filename = $share->getNode()->getName();
443
-		$initiator = $share->getSharedBy();
444
-		$shareWith = $share->getSharedWith();
445
-
446
-		if ($password === '' || $this->settingsManager->sendPasswordByMail() === false || $share->getSendPasswordByTalk()) {
447
-			return false;
448
-		}
449
-
450
-		$initiatorUser = $this->userManager->get($initiator);
451
-		$initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
452
-		$initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
453
-
454
-		$plainBodyPart = $this->l->t("%1\$s shared »%2\$s« with you.\nYou should have already received a separate mail with a link to access it.\n", [$initiatorDisplayName, $filename]);
455
-		$htmlBodyPart = $this->l->t('%1$s shared »%2$s« with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]);
456
-
457
-		$message = $this->mailer->createMessage();
458
-
459
-		$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [
460
-			'filename' => $filename,
461
-			'password' => $password,
462
-			'initiator' => $initiatorDisplayName,
463
-			'initiatorEmail' => $initiatorEmailAddress,
464
-			'shareWith' => $shareWith,
465
-		]);
466
-
467
-		$emailTemplate->setSubject($this->l->t('Password to access »%1$s« shared to you by %2$s', [$filename, $initiatorDisplayName]));
468
-		$emailTemplate->addHeader();
469
-		$emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false);
470
-		$emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart);
471
-		$emailTemplate->addBodyText($this->l->t('It is protected with the following password:'));
472
-		$emailTemplate->addBodyText($password);
473
-
474
-		// The "From" contains the sharers name
475
-		$instanceName = $this->defaults->getName();
476
-		$senderName = $instanceName;
477
-		if ($this->settingsManager->replyToInitiator()) {
478
-			$senderName = $this->l->t(
479
-				'%1$s via %2$s',
480
-				[
481
-					$initiatorDisplayName,
482
-					$instanceName
483
-				]
484
-			);
485
-		}
486
-		$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
487
-		if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) {
488
-			$message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
489
-			$emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
490
-		} else {
491
-			$emailTemplate->addFooter();
492
-		}
493
-
494
-		$message->setTo([$shareWith]);
495
-		$message->useTemplate($emailTemplate);
496
-		$this->mailer->send($message);
497
-
498
-		$this->createPasswordSendActivity($share, $shareWith, false);
499
-
500
-		return true;
501
-	}
502
-
503
-	protected function sendNote(IShare $share) {
504
-		$recipient = $share->getSharedWith();
505
-
506
-
507
-		$filename = $share->getNode()->getName();
508
-		$initiator = $share->getSharedBy();
509
-		$note = $share->getNote();
510
-
511
-		$initiatorUser = $this->userManager->get($initiator);
512
-		$initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
513
-		$initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
514
-
515
-		$plainHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add:', [$initiatorDisplayName, $filename]);
516
-		$htmlHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add', [$initiatorDisplayName, $filename]);
517
-
518
-		$message = $this->mailer->createMessage();
519
-
520
-		$emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote');
521
-
522
-		$emailTemplate->setSubject($this->l->t('»%s« added a note to a file shared with you', [$initiatorDisplayName]));
523
-		$emailTemplate->addHeader();
524
-		$emailTemplate->addHeading(htmlspecialchars($htmlHeading), $plainHeading);
525
-		$emailTemplate->addBodyText(htmlspecialchars($note), $note);
526
-
527
-		$link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare',
528
-			['token' => $share->getToken()]);
529
-		$emailTemplate->addBodyButton(
530
-			$this->l->t('Open »%s«', [$filename]),
531
-			$link
532
-		);
533
-
534
-		// The "From" contains the sharers name
535
-		$instanceName = $this->defaults->getName();
536
-		$senderName = $instanceName;
537
-		if ($this->settingsManager->replyToInitiator()) {
538
-			$senderName = $this->l->t(
539
-				'%1$s via %2$s',
540
-				[
541
-					$initiatorDisplayName,
542
-					$instanceName
543
-				]
544
-			);
545
-		}
546
-		$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
547
-		if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) {
548
-			$message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
549
-			$emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
550
-		} else {
551
-			$emailTemplate->addFooter();
552
-		}
553
-
554
-		$message->setTo([$recipient]);
555
-		$message->useTemplate($emailTemplate);
556
-		$this->mailer->send($message);
557
-	}
558
-
559
-	/**
560
-	 * send auto generated password to the owner. This happens if the admin enforces
561
-	 * a password for mail shares and forbid to send the password by mail to the recipient
562
-	 *
563
-	 * @param IShare $share
564
-	 * @param string $password
565
-	 * @return bool
566
-	 * @throws \Exception
567
-	 */
568
-	protected function sendPasswordToOwner(IShare $share, $password) {
569
-		$filename = $share->getNode()->getName();
570
-		$initiator = $this->userManager->get($share->getSharedBy());
571
-		$initiatorEMailAddress = ($initiator instanceof IUser) ? $initiator->getEMailAddress() : null;
572
-		$initiatorDisplayName = ($initiator instanceof IUser) ? $initiator->getDisplayName() : $share->getSharedBy();
573
-		$shareWith = $share->getSharedWith();
574
-
575
-		if ($initiatorEMailAddress === null) {
576
-			throw new \Exception(
577
-				$this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.")
578
-			);
579
-		}
580
-
581
-		$bodyPart = $this->l->t('You just shared »%1$s« with %2$s. The share was already sent to the recipient. Due to the security policies defined by the administrator of %3$s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.', [$filename, $shareWith, $this->defaults->getName()]);
582
-
583
-		$message = $this->mailer->createMessage();
584
-		$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [
585
-			'filename' => $filename,
586
-			'password' => $password,
587
-			'initiator' => $initiatorDisplayName,
588
-			'initiatorEmail' => $initiatorEMailAddress,
589
-			'shareWith' => $shareWith,
590
-		]);
591
-
592
-		$emailTemplate->setSubject($this->l->t('Password to access »%1$s« shared by you with %2$s', [$filename, $shareWith]));
593
-		$emailTemplate->addHeader();
594
-		$emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false);
595
-		$emailTemplate->addBodyText($bodyPart);
596
-		$emailTemplate->addBodyText($this->l->t('This is the password:'));
597
-		$emailTemplate->addBodyText($password);
598
-		$emailTemplate->addBodyText($this->l->t('You can choose a different password at any time in the share dialog.'));
599
-		$emailTemplate->addFooter();
600
-
601
-		$instanceName = $this->defaults->getName();
602
-		$senderName = $this->l->t(
603
-			'%1$s via %2$s',
604
-			[
605
-				$initiatorDisplayName,
606
-				$instanceName
607
-			]
608
-		);
609
-		$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
610
-		$message->setTo([$initiatorEMailAddress => $initiatorDisplayName]);
611
-		$message->useTemplate($emailTemplate);
612
-		$this->mailer->send($message);
613
-
614
-		$this->createPasswordSendActivity($share, $shareWith, true);
615
-
616
-		return true;
617
-	}
618
-
619
-	/**
620
-	 * generate share token
621
-	 *
622
-	 * @return string
623
-	 */
624
-	protected function generateToken($size = 15) {
625
-		$token = $this->secureRandom->generate($size, ISecureRandom::CHAR_HUMAN_READABLE);
626
-		return $token;
627
-	}
628
-
629
-	/**
630
-	 * Get all children of this share
631
-	 *
632
-	 * @param IShare $parent
633
-	 * @return IShare[]
634
-	 */
635
-	public function getChildren(IShare $parent) {
636
-		$children = [];
637
-
638
-		$qb = $this->dbConnection->getQueryBuilder();
639
-		$qb->select('*')
640
-			->from('share')
641
-			->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
642
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
643
-			->orderBy('id');
644
-
645
-		$cursor = $qb->executeQuery();
646
-		while ($data = $cursor->fetch()) {
647
-			$children[] = $this->createShareObject($data);
648
-		}
649
-		$cursor->closeCursor();
650
-
651
-		return $children;
652
-	}
653
-
654
-	/**
655
-	 * add share to the database and return the ID
656
-	 *
657
-	 * @param int $itemSource
658
-	 * @param string $itemType
659
-	 * @param string $shareWith
660
-	 * @param string $sharedBy
661
-	 * @param string $uidOwner
662
-	 * @param int $permissions
663
-	 * @param string $token
664
-	 * @param string $password
665
-	 * @param bool $sendPasswordByTalk
666
-	 * @param bool $hideDownload
667
-	 * @param string $label
668
-	 * @param \DateTime|null $expirationTime
669
-	 * @return int
670
-	 */
671
-	protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password, $sendPasswordByTalk, $hideDownload, $label, $expirationTime) {
672
-		$qb = $this->dbConnection->getQueryBuilder();
673
-		$qb->insert('share')
674
-			->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))
675
-			->setValue('item_type', $qb->createNamedParameter($itemType))
676
-			->setValue('item_source', $qb->createNamedParameter($itemSource))
677
-			->setValue('file_source', $qb->createNamedParameter($itemSource))
678
-			->setValue('share_with', $qb->createNamedParameter($shareWith))
679
-			->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
680
-			->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
681
-			->setValue('permissions', $qb->createNamedParameter($permissions))
682
-			->setValue('token', $qb->createNamedParameter($token))
683
-			->setValue('password', $qb->createNamedParameter($password))
684
-			->setValue('password_by_talk', $qb->createNamedParameter($sendPasswordByTalk, IQueryBuilder::PARAM_BOOL))
685
-			->setValue('stime', $qb->createNamedParameter(time()))
686
-			->setValue('hide_download', $qb->createNamedParameter((int)$hideDownload, IQueryBuilder::PARAM_INT))
687
-			->setValue('label', $qb->createNamedParameter($label));
688
-
689
-		if ($expirationTime !== null) {
690
-			$qb->setValue('expiration', $qb->createNamedParameter($expirationTime, IQueryBuilder::PARAM_DATE));
691
-		}
692
-
693
-		/*
169
+        $alreadyShared = $this->getSharedWith($shareWith, IShare::TYPE_EMAIL, $share->getNode(), 1, 0);
170
+        if (!empty($alreadyShared)) {
171
+            $message = 'Sharing %1$s failed, because this item is already shared with user %2$s';
172
+            $message_t = $this->l->t('Sharing %1$s failed, because this item is already shared with user %2$s', [$share->getNode()->getName(), $shareWith]);
173
+            $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
174
+            throw new \Exception($message_t);
175
+        }
176
+
177
+        // if the admin enforces a password for all mail shares we create a
178
+        // random password and send it to the recipient
179
+        $password = $share->getPassword() ?: '';
180
+        $passwordEnforced = $this->shareManager->shareApiLinkEnforcePassword();
181
+        if ($passwordEnforced && empty($password)) {
182
+            $password = $this->autoGeneratePassword($share);
183
+        }
184
+
185
+        if (!empty($password)) {
186
+            $share->setPassword($this->hasher->hash($password));
187
+        }
188
+
189
+        $shareId = $this->createMailShare($share);
190
+        $send = $this->sendPassword($share, $password);
191
+        if ($passwordEnforced && $send === false) {
192
+            $this->sendPasswordToOwner($share, $password);
193
+        }
194
+
195
+        $this->createShareActivity($share);
196
+        $data = $this->getRawShare($shareId);
197
+
198
+        return $this->createShareObject($data);
199
+    }
200
+
201
+    /**
202
+     * auto generate password in case of password enforcement on mail shares
203
+     *
204
+     * @param IShare $share
205
+     * @return string
206
+     * @throws \Exception
207
+     */
208
+    protected function autoGeneratePassword($share) {
209
+        $initiatorUser = $this->userManager->get($share->getSharedBy());
210
+        $initiatorEMailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
211
+        $allowPasswordByMail = $this->settingsManager->sendPasswordByMail();
212
+
213
+        if ($initiatorEMailAddress === null && !$allowPasswordByMail) {
214
+            throw new \Exception(
215
+                $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.")
216
+            );
217
+        }
218
+
219
+        $passwordEvent = new GenerateSecurePasswordEvent();
220
+        $this->eventDispatcher->dispatchTyped($passwordEvent);
221
+
222
+        $password = $passwordEvent->getPassword();
223
+        if ($password === null) {
224
+            $password = $this->secureRandom->generate(8, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS);
225
+        }
226
+
227
+        return $password;
228
+    }
229
+
230
+    /**
231
+     * create activity if a file/folder was shared by mail
232
+     *
233
+     * @param IShare $share
234
+     * @param string $type
235
+     */
236
+    protected function createShareActivity(IShare $share, string $type = 'share') {
237
+        $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
238
+
239
+        $this->publishActivity(
240
+            $type === 'share' ? Activity::SUBJECT_SHARED_EMAIL_SELF : Activity::SUBJECT_UNSHARED_EMAIL_SELF,
241
+            [$userFolder->getRelativePath($share->getNode()->getPath()), $share->getSharedWith()],
242
+            $share->getSharedBy(),
243
+            $share->getNode()->getId(),
244
+            (string) $userFolder->getRelativePath($share->getNode()->getPath())
245
+        );
246
+
247
+        if ($share->getShareOwner() !== $share->getSharedBy()) {
248
+            $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
249
+            $fileId = $share->getNode()->getId();
250
+            $nodes = $ownerFolder->getById($fileId);
251
+            $ownerPath = $nodes[0]->getPath();
252
+            $this->publishActivity(
253
+                $type === 'share' ? Activity::SUBJECT_SHARED_EMAIL_BY : Activity::SUBJECT_UNSHARED_EMAIL_BY,
254
+                [$ownerFolder->getRelativePath($ownerPath), $share->getSharedWith(), $share->getSharedBy()],
255
+                $share->getShareOwner(),
256
+                $fileId,
257
+                (string) $ownerFolder->getRelativePath($ownerPath)
258
+            );
259
+        }
260
+    }
261
+
262
+    /**
263
+     * create activity if a file/folder was shared by mail
264
+     *
265
+     * @param IShare $share
266
+     * @param string $sharedWith
267
+     * @param bool $sendToSelf
268
+     */
269
+    protected function createPasswordSendActivity(IShare $share, $sharedWith, $sendToSelf) {
270
+        $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
271
+
272
+        if ($sendToSelf) {
273
+            $this->publishActivity(
274
+                Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND_SELF,
275
+                [$userFolder->getRelativePath($share->getNode()->getPath())],
276
+                $share->getSharedBy(),
277
+                $share->getNode()->getId(),
278
+                (string) $userFolder->getRelativePath($share->getNode()->getPath())
279
+            );
280
+        } else {
281
+            $this->publishActivity(
282
+                Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND,
283
+                [$userFolder->getRelativePath($share->getNode()->getPath()), $sharedWith],
284
+                $share->getSharedBy(),
285
+                $share->getNode()->getId(),
286
+                (string) $userFolder->getRelativePath($share->getNode()->getPath())
287
+            );
288
+        }
289
+    }
290
+
291
+
292
+    /**
293
+     * publish activity if a file/folder was shared by mail
294
+     *
295
+     * @param string $subject
296
+     * @param array $parameters
297
+     * @param string $affectedUser
298
+     * @param int $fileId
299
+     * @param string $filePath
300
+     */
301
+    protected function publishActivity(string $subject, array $parameters, string $affectedUser, int $fileId, string $filePath) {
302
+        $event = $this->activityManager->generateEvent();
303
+        $event->setApp('sharebymail')
304
+            ->setType('shared')
305
+            ->setSubject($subject, $parameters)
306
+            ->setAffectedUser($affectedUser)
307
+            ->setObject('files', $fileId, $filePath);
308
+        $this->activityManager->publish($event);
309
+    }
310
+
311
+    /**
312
+     * @param IShare $share
313
+     * @return int
314
+     * @throws \Exception
315
+     */
316
+    protected function createMailShare(IShare $share) {
317
+        $share->setToken($this->generateToken());
318
+        $shareId = $this->addShareToDB(
319
+            $share->getNodeId(),
320
+            $share->getNodeType(),
321
+            $share->getSharedWith(),
322
+            $share->getSharedBy(),
323
+            $share->getShareOwner(),
324
+            $share->getPermissions(),
325
+            $share->getToken(),
326
+            $share->getPassword(),
327
+            $share->getSendPasswordByTalk(),
328
+            $share->getHideDownload(),
329
+            $share->getLabel(),
330
+            $share->getExpirationDate()
331
+        );
332
+
333
+        try {
334
+            $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare',
335
+                ['token' => $share->getToken()]);
336
+            $this->sendMailNotification(
337
+                $share->getNode()->getName(),
338
+                $link,
339
+                $share->getSharedBy(),
340
+                $share->getSharedWith(),
341
+                $share->getExpirationDate()
342
+            );
343
+        } catch (HintException $hintException) {
344
+            $this->logger->logException($hintException, [
345
+                'message' => 'Failed to send share by mail.',
346
+                'level' => ILogger::ERROR,
347
+                'app' => 'sharebymail',
348
+            ]);
349
+            $this->removeShareFromTable($shareId);
350
+            throw $hintException;
351
+        } catch (\Exception $e) {
352
+            $this->logger->logException($e, [
353
+                'message' => 'Failed to send share by mail.',
354
+                'level' => ILogger::ERROR,
355
+                'app' => 'sharebymail',
356
+            ]);
357
+            $this->removeShareFromTable($shareId);
358
+            throw new HintException('Failed to send share by mail',
359
+                $this->l->t('Failed to send share by email'));
360
+        }
361
+
362
+        return $shareId;
363
+    }
364
+
365
+    /**
366
+     * @param string $filename
367
+     * @param string $link
368
+     * @param string $initiator
369
+     * @param string $shareWith
370
+     * @param \DateTime|null $expiration
371
+     * @throws \Exception If mail couldn't be sent
372
+     */
373
+    protected function sendMailNotification($filename,
374
+                                            $link,
375
+                                            $initiator,
376
+                                            $shareWith,
377
+                                            \DateTime $expiration = null) {
378
+        $initiatorUser = $this->userManager->get($initiator);
379
+        $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
380
+        $message = $this->mailer->createMessage();
381
+
382
+        $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [
383
+            'filename' => $filename,
384
+            'link' => $link,
385
+            'initiator' => $initiatorDisplayName,
386
+            'expiration' => $expiration,
387
+            'shareWith' => $shareWith,
388
+        ]);
389
+
390
+        $emailTemplate->setSubject($this->l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]));
391
+        $emailTemplate->addHeader();
392
+        $emailTemplate->addHeading($this->l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]), false);
393
+        $text = $this->l->t('%1$s shared »%2$s« with you.', [$initiatorDisplayName, $filename]);
394
+
395
+        $emailTemplate->addBodyText(
396
+            htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')),
397
+            $text
398
+        );
399
+        $emailTemplate->addBodyButton(
400
+            $this->l->t('Open »%s«', [$filename]),
401
+            $link
402
+        );
403
+
404
+        $message->setTo([$shareWith]);
405
+
406
+        // The "From" contains the sharers name
407
+        $instanceName = $this->defaults->getName();
408
+        $senderName = $instanceName;
409
+        if ($this->settingsManager->replyToInitiator()) {
410
+            $senderName = $this->l->t(
411
+                '%1$s via %2$s',
412
+                [
413
+                    $initiatorDisplayName,
414
+                    $instanceName
415
+                ]
416
+            );
417
+        }
418
+        $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
419
+
420
+        // The "Reply-To" is set to the sharer if an mail address is configured
421
+        // also the default footer contains a "Do not reply" which needs to be adjusted.
422
+        $initiatorEmail = $initiatorUser->getEMailAddress();
423
+        if ($this->settingsManager->replyToInitiator() && $initiatorEmail !== null) {
424
+            $message->setReplyTo([$initiatorEmail => $initiatorDisplayName]);
425
+            $emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''));
426
+        } else {
427
+            $emailTemplate->addFooter();
428
+        }
429
+
430
+        $message->useTemplate($emailTemplate);
431
+        $this->mailer->send($message);
432
+    }
433
+
434
+    /**
435
+     * send password to recipient of a mail share
436
+     *
437
+     * @param IShare $share
438
+     * @param string $password
439
+     * @return bool
440
+     */
441
+    protected function sendPassword(IShare $share, $password) {
442
+        $filename = $share->getNode()->getName();
443
+        $initiator = $share->getSharedBy();
444
+        $shareWith = $share->getSharedWith();
445
+
446
+        if ($password === '' || $this->settingsManager->sendPasswordByMail() === false || $share->getSendPasswordByTalk()) {
447
+            return false;
448
+        }
449
+
450
+        $initiatorUser = $this->userManager->get($initiator);
451
+        $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
452
+        $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
453
+
454
+        $plainBodyPart = $this->l->t("%1\$s shared »%2\$s« with you.\nYou should have already received a separate mail with a link to access it.\n", [$initiatorDisplayName, $filename]);
455
+        $htmlBodyPart = $this->l->t('%1$s shared »%2$s« with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]);
456
+
457
+        $message = $this->mailer->createMessage();
458
+
459
+        $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [
460
+            'filename' => $filename,
461
+            'password' => $password,
462
+            'initiator' => $initiatorDisplayName,
463
+            'initiatorEmail' => $initiatorEmailAddress,
464
+            'shareWith' => $shareWith,
465
+        ]);
466
+
467
+        $emailTemplate->setSubject($this->l->t('Password to access »%1$s« shared to you by %2$s', [$filename, $initiatorDisplayName]));
468
+        $emailTemplate->addHeader();
469
+        $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false);
470
+        $emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart);
471
+        $emailTemplate->addBodyText($this->l->t('It is protected with the following password:'));
472
+        $emailTemplate->addBodyText($password);
473
+
474
+        // The "From" contains the sharers name
475
+        $instanceName = $this->defaults->getName();
476
+        $senderName = $instanceName;
477
+        if ($this->settingsManager->replyToInitiator()) {
478
+            $senderName = $this->l->t(
479
+                '%1$s via %2$s',
480
+                [
481
+                    $initiatorDisplayName,
482
+                    $instanceName
483
+                ]
484
+            );
485
+        }
486
+        $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
487
+        if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) {
488
+            $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
489
+            $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
490
+        } else {
491
+            $emailTemplate->addFooter();
492
+        }
493
+
494
+        $message->setTo([$shareWith]);
495
+        $message->useTemplate($emailTemplate);
496
+        $this->mailer->send($message);
497
+
498
+        $this->createPasswordSendActivity($share, $shareWith, false);
499
+
500
+        return true;
501
+    }
502
+
503
+    protected function sendNote(IShare $share) {
504
+        $recipient = $share->getSharedWith();
505
+
506
+
507
+        $filename = $share->getNode()->getName();
508
+        $initiator = $share->getSharedBy();
509
+        $note = $share->getNote();
510
+
511
+        $initiatorUser = $this->userManager->get($initiator);
512
+        $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
513
+        $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
514
+
515
+        $plainHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add:', [$initiatorDisplayName, $filename]);
516
+        $htmlHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add', [$initiatorDisplayName, $filename]);
517
+
518
+        $message = $this->mailer->createMessage();
519
+
520
+        $emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote');
521
+
522
+        $emailTemplate->setSubject($this->l->t('»%s« added a note to a file shared with you', [$initiatorDisplayName]));
523
+        $emailTemplate->addHeader();
524
+        $emailTemplate->addHeading(htmlspecialchars($htmlHeading), $plainHeading);
525
+        $emailTemplate->addBodyText(htmlspecialchars($note), $note);
526
+
527
+        $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare',
528
+            ['token' => $share->getToken()]);
529
+        $emailTemplate->addBodyButton(
530
+            $this->l->t('Open »%s«', [$filename]),
531
+            $link
532
+        );
533
+
534
+        // The "From" contains the sharers name
535
+        $instanceName = $this->defaults->getName();
536
+        $senderName = $instanceName;
537
+        if ($this->settingsManager->replyToInitiator()) {
538
+            $senderName = $this->l->t(
539
+                '%1$s via %2$s',
540
+                [
541
+                    $initiatorDisplayName,
542
+                    $instanceName
543
+                ]
544
+            );
545
+        }
546
+        $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
547
+        if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) {
548
+            $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
549
+            $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
550
+        } else {
551
+            $emailTemplate->addFooter();
552
+        }
553
+
554
+        $message->setTo([$recipient]);
555
+        $message->useTemplate($emailTemplate);
556
+        $this->mailer->send($message);
557
+    }
558
+
559
+    /**
560
+     * send auto generated password to the owner. This happens if the admin enforces
561
+     * a password for mail shares and forbid to send the password by mail to the recipient
562
+     *
563
+     * @param IShare $share
564
+     * @param string $password
565
+     * @return bool
566
+     * @throws \Exception
567
+     */
568
+    protected function sendPasswordToOwner(IShare $share, $password) {
569
+        $filename = $share->getNode()->getName();
570
+        $initiator = $this->userManager->get($share->getSharedBy());
571
+        $initiatorEMailAddress = ($initiator instanceof IUser) ? $initiator->getEMailAddress() : null;
572
+        $initiatorDisplayName = ($initiator instanceof IUser) ? $initiator->getDisplayName() : $share->getSharedBy();
573
+        $shareWith = $share->getSharedWith();
574
+
575
+        if ($initiatorEMailAddress === null) {
576
+            throw new \Exception(
577
+                $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.")
578
+            );
579
+        }
580
+
581
+        $bodyPart = $this->l->t('You just shared »%1$s« with %2$s. The share was already sent to the recipient. Due to the security policies defined by the administrator of %3$s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.', [$filename, $shareWith, $this->defaults->getName()]);
582
+
583
+        $message = $this->mailer->createMessage();
584
+        $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [
585
+            'filename' => $filename,
586
+            'password' => $password,
587
+            'initiator' => $initiatorDisplayName,
588
+            'initiatorEmail' => $initiatorEMailAddress,
589
+            'shareWith' => $shareWith,
590
+        ]);
591
+
592
+        $emailTemplate->setSubject($this->l->t('Password to access »%1$s« shared by you with %2$s', [$filename, $shareWith]));
593
+        $emailTemplate->addHeader();
594
+        $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false);
595
+        $emailTemplate->addBodyText($bodyPart);
596
+        $emailTemplate->addBodyText($this->l->t('This is the password:'));
597
+        $emailTemplate->addBodyText($password);
598
+        $emailTemplate->addBodyText($this->l->t('You can choose a different password at any time in the share dialog.'));
599
+        $emailTemplate->addFooter();
600
+
601
+        $instanceName = $this->defaults->getName();
602
+        $senderName = $this->l->t(
603
+            '%1$s via %2$s',
604
+            [
605
+                $initiatorDisplayName,
606
+                $instanceName
607
+            ]
608
+        );
609
+        $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
610
+        $message->setTo([$initiatorEMailAddress => $initiatorDisplayName]);
611
+        $message->useTemplate($emailTemplate);
612
+        $this->mailer->send($message);
613
+
614
+        $this->createPasswordSendActivity($share, $shareWith, true);
615
+
616
+        return true;
617
+    }
618
+
619
+    /**
620
+     * generate share token
621
+     *
622
+     * @return string
623
+     */
624
+    protected function generateToken($size = 15) {
625
+        $token = $this->secureRandom->generate($size, ISecureRandom::CHAR_HUMAN_READABLE);
626
+        return $token;
627
+    }
628
+
629
+    /**
630
+     * Get all children of this share
631
+     *
632
+     * @param IShare $parent
633
+     * @return IShare[]
634
+     */
635
+    public function getChildren(IShare $parent) {
636
+        $children = [];
637
+
638
+        $qb = $this->dbConnection->getQueryBuilder();
639
+        $qb->select('*')
640
+            ->from('share')
641
+            ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
642
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
643
+            ->orderBy('id');
644
+
645
+        $cursor = $qb->executeQuery();
646
+        while ($data = $cursor->fetch()) {
647
+            $children[] = $this->createShareObject($data);
648
+        }
649
+        $cursor->closeCursor();
650
+
651
+        return $children;
652
+    }
653
+
654
+    /**
655
+     * add share to the database and return the ID
656
+     *
657
+     * @param int $itemSource
658
+     * @param string $itemType
659
+     * @param string $shareWith
660
+     * @param string $sharedBy
661
+     * @param string $uidOwner
662
+     * @param int $permissions
663
+     * @param string $token
664
+     * @param string $password
665
+     * @param bool $sendPasswordByTalk
666
+     * @param bool $hideDownload
667
+     * @param string $label
668
+     * @param \DateTime|null $expirationTime
669
+     * @return int
670
+     */
671
+    protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password, $sendPasswordByTalk, $hideDownload, $label, $expirationTime) {
672
+        $qb = $this->dbConnection->getQueryBuilder();
673
+        $qb->insert('share')
674
+            ->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))
675
+            ->setValue('item_type', $qb->createNamedParameter($itemType))
676
+            ->setValue('item_source', $qb->createNamedParameter($itemSource))
677
+            ->setValue('file_source', $qb->createNamedParameter($itemSource))
678
+            ->setValue('share_with', $qb->createNamedParameter($shareWith))
679
+            ->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
680
+            ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
681
+            ->setValue('permissions', $qb->createNamedParameter($permissions))
682
+            ->setValue('token', $qb->createNamedParameter($token))
683
+            ->setValue('password', $qb->createNamedParameter($password))
684
+            ->setValue('password_by_talk', $qb->createNamedParameter($sendPasswordByTalk, IQueryBuilder::PARAM_BOOL))
685
+            ->setValue('stime', $qb->createNamedParameter(time()))
686
+            ->setValue('hide_download', $qb->createNamedParameter((int)$hideDownload, IQueryBuilder::PARAM_INT))
687
+            ->setValue('label', $qb->createNamedParameter($label));
688
+
689
+        if ($expirationTime !== null) {
690
+            $qb->setValue('expiration', $qb->createNamedParameter($expirationTime, IQueryBuilder::PARAM_DATE));
691
+        }
692
+
693
+        /*
694 694
 		 * Added to fix https://github.com/owncloud/core/issues/22215
695 695
 		 * Can be removed once we get rid of ajax/share.php
696 696
 		 */
697
-		$qb->setValue('file_target', $qb->createNamedParameter(''));
698
-
699
-		$qb->executeStatement();
700
-		return $qb->getLastInsertId();
701
-	}
702
-
703
-	/**
704
-	 * Update a share
705
-	 *
706
-	 * @param IShare $share
707
-	 * @param string|null $plainTextPassword
708
-	 * @return IShare The share object
709
-	 */
710
-	public function update(IShare $share, $plainTextPassword = null) {
711
-		$originalShare = $this->getShareById($share->getId());
712
-
713
-		// a real password was given
714
-		$validPassword = $plainTextPassword !== null && $plainTextPassword !== '';
715
-
716
-		if ($validPassword && ($originalShare->getPassword() !== $share->getPassword() ||
717
-								($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()))) {
718
-			$this->sendPassword($share, $plainTextPassword);
719
-		}
720
-		/*
697
+        $qb->setValue('file_target', $qb->createNamedParameter(''));
698
+
699
+        $qb->executeStatement();
700
+        return $qb->getLastInsertId();
701
+    }
702
+
703
+    /**
704
+     * Update a share
705
+     *
706
+     * @param IShare $share
707
+     * @param string|null $plainTextPassword
708
+     * @return IShare The share object
709
+     */
710
+    public function update(IShare $share, $plainTextPassword = null) {
711
+        $originalShare = $this->getShareById($share->getId());
712
+
713
+        // a real password was given
714
+        $validPassword = $plainTextPassword !== null && $plainTextPassword !== '';
715
+
716
+        if ($validPassword && ($originalShare->getPassword() !== $share->getPassword() ||
717
+                                ($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()))) {
718
+            $this->sendPassword($share, $plainTextPassword);
719
+        }
720
+        /*
721 721
 		 * We allow updating the permissions and password of mail shares
722 722
 		 */
723
-		$qb = $this->dbConnection->getQueryBuilder();
724
-		$qb->update('share')
725
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
726
-			->set('permissions', $qb->createNamedParameter($share->getPermissions()))
727
-			->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
728
-			->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
729
-			->set('password', $qb->createNamedParameter($share->getPassword()))
730
-			->set('label', $qb->createNamedParameter($share->getLabel()))
731
-			->set('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL))
732
-			->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
733
-			->set('note', $qb->createNamedParameter($share->getNote()))
734
-			->set('hide_download', $qb->createNamedParameter((int)$share->getHideDownload(), IQueryBuilder::PARAM_INT))
735
-			->executeStatement();
736
-
737
-		if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') {
738
-			$this->sendNote($share);
739
-		}
740
-
741
-		return $share;
742
-	}
743
-
744
-	/**
745
-	 * @inheritdoc
746
-	 */
747
-	public function move(IShare $share, $recipient) {
748
-		/**
749
-		 * nothing to do here, mail shares are only outgoing shares
750
-		 */
751
-		return $share;
752
-	}
753
-
754
-	/**
755
-	 * Delete a share (owner unShares the file)
756
-	 *
757
-	 * @param IShare $share
758
-	 */
759
-	public function delete(IShare $share) {
760
-		try {
761
-			$this->createShareActivity($share, 'unshare');
762
-		} catch (\Exception $e) {
763
-		}
764
-
765
-		$this->removeShareFromTable($share->getId());
766
-	}
767
-
768
-	/**
769
-	 * @inheritdoc
770
-	 */
771
-	public function deleteFromSelf(IShare $share, $recipient) {
772
-		// nothing to do here, mail shares are only outgoing shares
773
-	}
774
-
775
-	public function restore(IShare $share, string $recipient): IShare {
776
-		throw new GenericShareException('not implemented');
777
-	}
778
-
779
-	/**
780
-	 * @inheritdoc
781
-	 */
782
-	public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
783
-		$qb = $this->dbConnection->getQueryBuilder();
784
-		$qb->select('*')
785
-			->from('share');
786
-
787
-		$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
788
-
789
-		/**
790
-		 * Reshares for this user are shares where they are the owner.
791
-		 */
792
-		if ($reshares === false) {
793
-			//Special case for old shares created via the web UI
794
-			$or1 = $qb->expr()->andX(
795
-				$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
796
-				$qb->expr()->isNull('uid_initiator')
797
-			);
798
-
799
-			$qb->andWhere(
800
-				$qb->expr()->orX(
801
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
802
-					$or1
803
-				)
804
-			);
805
-		} else {
806
-			$qb->andWhere(
807
-				$qb->expr()->orX(
808
-					$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
809
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
810
-				)
811
-			);
812
-		}
813
-
814
-		if ($node !== null) {
815
-			$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
816
-		}
817
-
818
-		if ($limit !== -1) {
819
-			$qb->setMaxResults($limit);
820
-		}
821
-
822
-		$qb->setFirstResult($offset);
823
-		$qb->orderBy('id');
824
-
825
-		$cursor = $qb->executeQuery();
826
-		$shares = [];
827
-		while ($data = $cursor->fetch()) {
828
-			$shares[] = $this->createShareObject($data);
829
-		}
830
-		$cursor->closeCursor();
831
-
832
-		return $shares;
833
-	}
834
-
835
-	/**
836
-	 * @inheritdoc
837
-	 */
838
-	public function getShareById($id, $recipientId = null) {
839
-		$qb = $this->dbConnection->getQueryBuilder();
840
-
841
-		$qb->select('*')
842
-			->from('share')
843
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
844
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
845
-
846
-		$cursor = $qb->executeQuery();
847
-		$data = $cursor->fetch();
848
-		$cursor->closeCursor();
849
-
850
-		if ($data === false) {
851
-			throw new ShareNotFound();
852
-		}
853
-
854
-		try {
855
-			$share = $this->createShareObject($data);
856
-		} catch (InvalidShare $e) {
857
-			throw new ShareNotFound();
858
-		}
859
-
860
-		return $share;
861
-	}
862
-
863
-	/**
864
-	 * Get shares for a given path
865
-	 *
866
-	 * @param \OCP\Files\Node $path
867
-	 * @return IShare[]
868
-	 */
869
-	public function getSharesByPath(Node $path) {
870
-		$qb = $this->dbConnection->getQueryBuilder();
871
-
872
-		$cursor = $qb->select('*')
873
-			->from('share')
874
-			->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
875
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
876
-			->executeQuery();
877
-
878
-		$shares = [];
879
-		while ($data = $cursor->fetch()) {
880
-			$shares[] = $this->createShareObject($data);
881
-		}
882
-		$cursor->closeCursor();
883
-
884
-		return $shares;
885
-	}
886
-
887
-	/**
888
-	 * @inheritdoc
889
-	 */
890
-	public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
891
-		/** @var IShare[] $shares */
892
-		$shares = [];
893
-
894
-		//Get shares directly with this user
895
-		$qb = $this->dbConnection->getQueryBuilder();
896
-		$qb->select('*')
897
-			->from('share');
898
-
899
-		// Order by id
900
-		$qb->orderBy('id');
901
-
902
-		// Set limit and offset
903
-		if ($limit !== -1) {
904
-			$qb->setMaxResults($limit);
905
-		}
906
-		$qb->setFirstResult($offset);
907
-
908
-		$qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
909
-		$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
910
-
911
-		// Filter by node if provided
912
-		if ($node !== null) {
913
-			$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
914
-		}
915
-
916
-		$cursor = $qb->executeQuery();
917
-
918
-		while ($data = $cursor->fetch()) {
919
-			$shares[] = $this->createShareObject($data);
920
-		}
921
-		$cursor->closeCursor();
922
-
923
-
924
-		return $shares;
925
-	}
926
-
927
-	/**
928
-	 * Get a share by token
929
-	 *
930
-	 * @param string $token
931
-	 * @return IShare
932
-	 * @throws ShareNotFound
933
-	 */
934
-	public function getShareByToken($token) {
935
-		$qb = $this->dbConnection->getQueryBuilder();
936
-
937
-		$cursor = $qb->select('*')
938
-			->from('share')
939
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
940
-			->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
941
-			->executeQuery();
942
-
943
-		$data = $cursor->fetch();
944
-
945
-		if ($data === false) {
946
-			throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
947
-		}
948
-
949
-		try {
950
-			$share = $this->createShareObject($data);
951
-		} catch (InvalidShare $e) {
952
-			throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
953
-		}
954
-
955
-		return $share;
956
-	}
957
-
958
-	/**
959
-	 * remove share from table
960
-	 *
961
-	 * @param string $shareId
962
-	 */
963
-	protected function removeShareFromTable($shareId) {
964
-		$qb = $this->dbConnection->getQueryBuilder();
965
-		$qb->delete('share')
966
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)));
967
-		$qb->executeStatement();
968
-	}
969
-
970
-	/**
971
-	 * Create a share object from an database row
972
-	 *
973
-	 * @param array $data
974
-	 * @return IShare
975
-	 * @throws InvalidShare
976
-	 * @throws ShareNotFound
977
-	 */
978
-	protected function createShareObject($data) {
979
-		$share = new Share($this->rootFolder, $this->userManager);
980
-		$share->setId((int)$data['id'])
981
-			->setShareType((int)$data['share_type'])
982
-			->setPermissions((int)$data['permissions'])
983
-			->setTarget($data['file_target'])
984
-			->setMailSend((bool)$data['mail_send'])
985
-			->setNote($data['note'])
986
-			->setToken($data['token']);
987
-
988
-		$shareTime = new \DateTime();
989
-		$shareTime->setTimestamp((int)$data['stime']);
990
-		$share->setShareTime($shareTime);
991
-		$share->setSharedWith($data['share_with']);
992
-		$share->setPassword($data['password']);
993
-		$share->setLabel($data['label']);
994
-		$share->setSendPasswordByTalk((bool)$data['password_by_talk']);
995
-		$share->setHideDownload((bool)$data['hide_download']);
996
-
997
-		if ($data['uid_initiator'] !== null) {
998
-			$share->setShareOwner($data['uid_owner']);
999
-			$share->setSharedBy($data['uid_initiator']);
1000
-		} else {
1001
-			//OLD SHARE
1002
-			$share->setSharedBy($data['uid_owner']);
1003
-			$path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
1004
-
1005
-			$owner = $path->getOwner();
1006
-			$share->setShareOwner($owner->getUID());
1007
-		}
1008
-
1009
-		if ($data['expiration'] !== null) {
1010
-			$expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']);
1011
-			if ($expiration !== false) {
1012
-				$share->setExpirationDate($expiration);
1013
-			}
1014
-		}
1015
-
1016
-		$share->setNodeId((int)$data['file_source']);
1017
-		$share->setNodeType($data['item_type']);
1018
-
1019
-		$share->setProviderId($this->identifier());
1020
-
1021
-		return $share;
1022
-	}
1023
-
1024
-	/**
1025
-	 * Get the node with file $id for $user
1026
-	 *
1027
-	 * @param string $userId
1028
-	 * @param int $id
1029
-	 * @return \OCP\Files\File|\OCP\Files\Folder
1030
-	 * @throws InvalidShare
1031
-	 */
1032
-	private function getNode($userId, $id) {
1033
-		try {
1034
-			$userFolder = $this->rootFolder->getUserFolder($userId);
1035
-		} catch (NoUserException $e) {
1036
-			throw new InvalidShare();
1037
-		}
1038
-
1039
-		$nodes = $userFolder->getById($id);
1040
-
1041
-		if (empty($nodes)) {
1042
-			throw new InvalidShare();
1043
-		}
1044
-
1045
-		return $nodes[0];
1046
-	}
1047
-
1048
-	/**
1049
-	 * A user is deleted from the system
1050
-	 * So clean up the relevant shares.
1051
-	 *
1052
-	 * @param string $uid
1053
-	 * @param int $shareType
1054
-	 */
1055
-	public function userDeleted($uid, $shareType) {
1056
-		$qb = $this->dbConnection->getQueryBuilder();
1057
-
1058
-		$qb->delete('share')
1059
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
1060
-			->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
1061
-			->executeStatement();
1062
-	}
1063
-
1064
-	/**
1065
-	 * This provider does not support group shares
1066
-	 *
1067
-	 * @param string $gid
1068
-	 */
1069
-	public function groupDeleted($gid) {
1070
-	}
1071
-
1072
-	/**
1073
-	 * This provider does not support group shares
1074
-	 *
1075
-	 * @param string $uid
1076
-	 * @param string $gid
1077
-	 */
1078
-	public function userDeletedFromGroup($uid, $gid) {
1079
-	}
1080
-
1081
-	/**
1082
-	 * get database row of a give share
1083
-	 *
1084
-	 * @param $id
1085
-	 * @return array
1086
-	 * @throws ShareNotFound
1087
-	 */
1088
-	protected function getRawShare($id) {
1089
-
1090
-		// Now fetch the inserted share and create a complete share object
1091
-		$qb = $this->dbConnection->getQueryBuilder();
1092
-		$qb->select('*')
1093
-			->from('share')
1094
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
1095
-
1096
-		$cursor = $qb->executeQuery();
1097
-		$data = $cursor->fetch();
1098
-		$cursor->closeCursor();
1099
-
1100
-		if ($data === false) {
1101
-			throw new ShareNotFound;
1102
-		}
1103
-
1104
-		return $data;
1105
-	}
1106
-
1107
-	public function getSharesInFolder($userId, Folder $node, $reshares) {
1108
-		$qb = $this->dbConnection->getQueryBuilder();
1109
-		$qb->select('*')
1110
-			->from('share', 's')
1111
-			->andWhere($qb->expr()->orX(
1112
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1113
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1114
-			))
1115
-			->andWhere(
1116
-				$qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))
1117
-			);
1118
-
1119
-		/**
1120
-		 * Reshares for this user are shares where they are the owner.
1121
-		 */
1122
-		if ($reshares === false) {
1123
-			$qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
1124
-		} else {
1125
-			$qb->andWhere(
1126
-				$qb->expr()->orX(
1127
-					$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
1128
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
1129
-				)
1130
-			);
1131
-		}
1132
-
1133
-		$qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
1134
-		$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
1135
-
1136
-		$qb->orderBy('id');
1137
-
1138
-		$cursor = $qb->executeQuery();
1139
-		$shares = [];
1140
-		while ($data = $cursor->fetch()) {
1141
-			$shares[$data['fileid']][] = $this->createShareObject($data);
1142
-		}
1143
-		$cursor->closeCursor();
1144
-
1145
-		return $shares;
1146
-	}
1147
-
1148
-	/**
1149
-	 * @inheritdoc
1150
-	 */
1151
-	public function getAccessList($nodes, $currentAccess) {
1152
-		$ids = [];
1153
-		foreach ($nodes as $node) {
1154
-			$ids[] = $node->getId();
1155
-		}
1156
-
1157
-		$qb = $this->dbConnection->getQueryBuilder();
1158
-		$qb->select('share_with')
1159
-			->from('share')
1160
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
1161
-			->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
1162
-			->andWhere($qb->expr()->orX(
1163
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1164
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1165
-			))
1166
-			->setMaxResults(1);
1167
-		$cursor = $qb->executeQuery();
1168
-
1169
-		$mail = $cursor->fetch() !== false;
1170
-		$cursor->closeCursor();
1171
-
1172
-		return ['public' => $mail];
1173
-	}
1174
-
1175
-	public function getAllShares(): iterable {
1176
-		$qb = $this->dbConnection->getQueryBuilder();
1177
-
1178
-		$qb->select('*')
1179
-			->from('share')
1180
-			->where(
1181
-				$qb->expr()->orX(
1182
-					$qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share\IShare::TYPE_EMAIL))
1183
-				)
1184
-			);
1185
-
1186
-		$cursor = $qb->executeQuery();
1187
-		while ($data = $cursor->fetch()) {
1188
-			try {
1189
-				$share = $this->createShareObject($data);
1190
-			} catch (InvalidShare $e) {
1191
-				continue;
1192
-			} catch (ShareNotFound $e) {
1193
-				continue;
1194
-			}
1195
-
1196
-			yield $share;
1197
-		}
1198
-		$cursor->closeCursor();
1199
-	}
723
+        $qb = $this->dbConnection->getQueryBuilder();
724
+        $qb->update('share')
725
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
726
+            ->set('permissions', $qb->createNamedParameter($share->getPermissions()))
727
+            ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
728
+            ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
729
+            ->set('password', $qb->createNamedParameter($share->getPassword()))
730
+            ->set('label', $qb->createNamedParameter($share->getLabel()))
731
+            ->set('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL))
732
+            ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
733
+            ->set('note', $qb->createNamedParameter($share->getNote()))
734
+            ->set('hide_download', $qb->createNamedParameter((int)$share->getHideDownload(), IQueryBuilder::PARAM_INT))
735
+            ->executeStatement();
736
+
737
+        if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') {
738
+            $this->sendNote($share);
739
+        }
740
+
741
+        return $share;
742
+    }
743
+
744
+    /**
745
+     * @inheritdoc
746
+     */
747
+    public function move(IShare $share, $recipient) {
748
+        /**
749
+         * nothing to do here, mail shares are only outgoing shares
750
+         */
751
+        return $share;
752
+    }
753
+
754
+    /**
755
+     * Delete a share (owner unShares the file)
756
+     *
757
+     * @param IShare $share
758
+     */
759
+    public function delete(IShare $share) {
760
+        try {
761
+            $this->createShareActivity($share, 'unshare');
762
+        } catch (\Exception $e) {
763
+        }
764
+
765
+        $this->removeShareFromTable($share->getId());
766
+    }
767
+
768
+    /**
769
+     * @inheritdoc
770
+     */
771
+    public function deleteFromSelf(IShare $share, $recipient) {
772
+        // nothing to do here, mail shares are only outgoing shares
773
+    }
774
+
775
+    public function restore(IShare $share, string $recipient): IShare {
776
+        throw new GenericShareException('not implemented');
777
+    }
778
+
779
+    /**
780
+     * @inheritdoc
781
+     */
782
+    public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
783
+        $qb = $this->dbConnection->getQueryBuilder();
784
+        $qb->select('*')
785
+            ->from('share');
786
+
787
+        $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
788
+
789
+        /**
790
+         * Reshares for this user are shares where they are the owner.
791
+         */
792
+        if ($reshares === false) {
793
+            //Special case for old shares created via the web UI
794
+            $or1 = $qb->expr()->andX(
795
+                $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
796
+                $qb->expr()->isNull('uid_initiator')
797
+            );
798
+
799
+            $qb->andWhere(
800
+                $qb->expr()->orX(
801
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
802
+                    $or1
803
+                )
804
+            );
805
+        } else {
806
+            $qb->andWhere(
807
+                $qb->expr()->orX(
808
+                    $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
809
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
810
+                )
811
+            );
812
+        }
813
+
814
+        if ($node !== null) {
815
+            $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
816
+        }
817
+
818
+        if ($limit !== -1) {
819
+            $qb->setMaxResults($limit);
820
+        }
821
+
822
+        $qb->setFirstResult($offset);
823
+        $qb->orderBy('id');
824
+
825
+        $cursor = $qb->executeQuery();
826
+        $shares = [];
827
+        while ($data = $cursor->fetch()) {
828
+            $shares[] = $this->createShareObject($data);
829
+        }
830
+        $cursor->closeCursor();
831
+
832
+        return $shares;
833
+    }
834
+
835
+    /**
836
+     * @inheritdoc
837
+     */
838
+    public function getShareById($id, $recipientId = null) {
839
+        $qb = $this->dbConnection->getQueryBuilder();
840
+
841
+        $qb->select('*')
842
+            ->from('share')
843
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
844
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
845
+
846
+        $cursor = $qb->executeQuery();
847
+        $data = $cursor->fetch();
848
+        $cursor->closeCursor();
849
+
850
+        if ($data === false) {
851
+            throw new ShareNotFound();
852
+        }
853
+
854
+        try {
855
+            $share = $this->createShareObject($data);
856
+        } catch (InvalidShare $e) {
857
+            throw new ShareNotFound();
858
+        }
859
+
860
+        return $share;
861
+    }
862
+
863
+    /**
864
+     * Get shares for a given path
865
+     *
866
+     * @param \OCP\Files\Node $path
867
+     * @return IShare[]
868
+     */
869
+    public function getSharesByPath(Node $path) {
870
+        $qb = $this->dbConnection->getQueryBuilder();
871
+
872
+        $cursor = $qb->select('*')
873
+            ->from('share')
874
+            ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
875
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
876
+            ->executeQuery();
877
+
878
+        $shares = [];
879
+        while ($data = $cursor->fetch()) {
880
+            $shares[] = $this->createShareObject($data);
881
+        }
882
+        $cursor->closeCursor();
883
+
884
+        return $shares;
885
+    }
886
+
887
+    /**
888
+     * @inheritdoc
889
+     */
890
+    public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
891
+        /** @var IShare[] $shares */
892
+        $shares = [];
893
+
894
+        //Get shares directly with this user
895
+        $qb = $this->dbConnection->getQueryBuilder();
896
+        $qb->select('*')
897
+            ->from('share');
898
+
899
+        // Order by id
900
+        $qb->orderBy('id');
901
+
902
+        // Set limit and offset
903
+        if ($limit !== -1) {
904
+            $qb->setMaxResults($limit);
905
+        }
906
+        $qb->setFirstResult($offset);
907
+
908
+        $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
909
+        $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
910
+
911
+        // Filter by node if provided
912
+        if ($node !== null) {
913
+            $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
914
+        }
915
+
916
+        $cursor = $qb->executeQuery();
917
+
918
+        while ($data = $cursor->fetch()) {
919
+            $shares[] = $this->createShareObject($data);
920
+        }
921
+        $cursor->closeCursor();
922
+
923
+
924
+        return $shares;
925
+    }
926
+
927
+    /**
928
+     * Get a share by token
929
+     *
930
+     * @param string $token
931
+     * @return IShare
932
+     * @throws ShareNotFound
933
+     */
934
+    public function getShareByToken($token) {
935
+        $qb = $this->dbConnection->getQueryBuilder();
936
+
937
+        $cursor = $qb->select('*')
938
+            ->from('share')
939
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
940
+            ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
941
+            ->executeQuery();
942
+
943
+        $data = $cursor->fetch();
944
+
945
+        if ($data === false) {
946
+            throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
947
+        }
948
+
949
+        try {
950
+            $share = $this->createShareObject($data);
951
+        } catch (InvalidShare $e) {
952
+            throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
953
+        }
954
+
955
+        return $share;
956
+    }
957
+
958
+    /**
959
+     * remove share from table
960
+     *
961
+     * @param string $shareId
962
+     */
963
+    protected function removeShareFromTable($shareId) {
964
+        $qb = $this->dbConnection->getQueryBuilder();
965
+        $qb->delete('share')
966
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)));
967
+        $qb->executeStatement();
968
+    }
969
+
970
+    /**
971
+     * Create a share object from an database row
972
+     *
973
+     * @param array $data
974
+     * @return IShare
975
+     * @throws InvalidShare
976
+     * @throws ShareNotFound
977
+     */
978
+    protected function createShareObject($data) {
979
+        $share = new Share($this->rootFolder, $this->userManager);
980
+        $share->setId((int)$data['id'])
981
+            ->setShareType((int)$data['share_type'])
982
+            ->setPermissions((int)$data['permissions'])
983
+            ->setTarget($data['file_target'])
984
+            ->setMailSend((bool)$data['mail_send'])
985
+            ->setNote($data['note'])
986
+            ->setToken($data['token']);
987
+
988
+        $shareTime = new \DateTime();
989
+        $shareTime->setTimestamp((int)$data['stime']);
990
+        $share->setShareTime($shareTime);
991
+        $share->setSharedWith($data['share_with']);
992
+        $share->setPassword($data['password']);
993
+        $share->setLabel($data['label']);
994
+        $share->setSendPasswordByTalk((bool)$data['password_by_talk']);
995
+        $share->setHideDownload((bool)$data['hide_download']);
996
+
997
+        if ($data['uid_initiator'] !== null) {
998
+            $share->setShareOwner($data['uid_owner']);
999
+            $share->setSharedBy($data['uid_initiator']);
1000
+        } else {
1001
+            //OLD SHARE
1002
+            $share->setSharedBy($data['uid_owner']);
1003
+            $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
1004
+
1005
+            $owner = $path->getOwner();
1006
+            $share->setShareOwner($owner->getUID());
1007
+        }
1008
+
1009
+        if ($data['expiration'] !== null) {
1010
+            $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']);
1011
+            if ($expiration !== false) {
1012
+                $share->setExpirationDate($expiration);
1013
+            }
1014
+        }
1015
+
1016
+        $share->setNodeId((int)$data['file_source']);
1017
+        $share->setNodeType($data['item_type']);
1018
+
1019
+        $share->setProviderId($this->identifier());
1020
+
1021
+        return $share;
1022
+    }
1023
+
1024
+    /**
1025
+     * Get the node with file $id for $user
1026
+     *
1027
+     * @param string $userId
1028
+     * @param int $id
1029
+     * @return \OCP\Files\File|\OCP\Files\Folder
1030
+     * @throws InvalidShare
1031
+     */
1032
+    private function getNode($userId, $id) {
1033
+        try {
1034
+            $userFolder = $this->rootFolder->getUserFolder($userId);
1035
+        } catch (NoUserException $e) {
1036
+            throw new InvalidShare();
1037
+        }
1038
+
1039
+        $nodes = $userFolder->getById($id);
1040
+
1041
+        if (empty($nodes)) {
1042
+            throw new InvalidShare();
1043
+        }
1044
+
1045
+        return $nodes[0];
1046
+    }
1047
+
1048
+    /**
1049
+     * A user is deleted from the system
1050
+     * So clean up the relevant shares.
1051
+     *
1052
+     * @param string $uid
1053
+     * @param int $shareType
1054
+     */
1055
+    public function userDeleted($uid, $shareType) {
1056
+        $qb = $this->dbConnection->getQueryBuilder();
1057
+
1058
+        $qb->delete('share')
1059
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
1060
+            ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
1061
+            ->executeStatement();
1062
+    }
1063
+
1064
+    /**
1065
+     * This provider does not support group shares
1066
+     *
1067
+     * @param string $gid
1068
+     */
1069
+    public function groupDeleted($gid) {
1070
+    }
1071
+
1072
+    /**
1073
+     * This provider does not support group shares
1074
+     *
1075
+     * @param string $uid
1076
+     * @param string $gid
1077
+     */
1078
+    public function userDeletedFromGroup($uid, $gid) {
1079
+    }
1080
+
1081
+    /**
1082
+     * get database row of a give share
1083
+     *
1084
+     * @param $id
1085
+     * @return array
1086
+     * @throws ShareNotFound
1087
+     */
1088
+    protected function getRawShare($id) {
1089
+
1090
+        // Now fetch the inserted share and create a complete share object
1091
+        $qb = $this->dbConnection->getQueryBuilder();
1092
+        $qb->select('*')
1093
+            ->from('share')
1094
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
1095
+
1096
+        $cursor = $qb->executeQuery();
1097
+        $data = $cursor->fetch();
1098
+        $cursor->closeCursor();
1099
+
1100
+        if ($data === false) {
1101
+            throw new ShareNotFound;
1102
+        }
1103
+
1104
+        return $data;
1105
+    }
1106
+
1107
+    public function getSharesInFolder($userId, Folder $node, $reshares) {
1108
+        $qb = $this->dbConnection->getQueryBuilder();
1109
+        $qb->select('*')
1110
+            ->from('share', 's')
1111
+            ->andWhere($qb->expr()->orX(
1112
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1113
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1114
+            ))
1115
+            ->andWhere(
1116
+                $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))
1117
+            );
1118
+
1119
+        /**
1120
+         * Reshares for this user are shares where they are the owner.
1121
+         */
1122
+        if ($reshares === false) {
1123
+            $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
1124
+        } else {
1125
+            $qb->andWhere(
1126
+                $qb->expr()->orX(
1127
+                    $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
1128
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
1129
+                )
1130
+            );
1131
+        }
1132
+
1133
+        $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
1134
+        $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
1135
+
1136
+        $qb->orderBy('id');
1137
+
1138
+        $cursor = $qb->executeQuery();
1139
+        $shares = [];
1140
+        while ($data = $cursor->fetch()) {
1141
+            $shares[$data['fileid']][] = $this->createShareObject($data);
1142
+        }
1143
+        $cursor->closeCursor();
1144
+
1145
+        return $shares;
1146
+    }
1147
+
1148
+    /**
1149
+     * @inheritdoc
1150
+     */
1151
+    public function getAccessList($nodes, $currentAccess) {
1152
+        $ids = [];
1153
+        foreach ($nodes as $node) {
1154
+            $ids[] = $node->getId();
1155
+        }
1156
+
1157
+        $qb = $this->dbConnection->getQueryBuilder();
1158
+        $qb->select('share_with')
1159
+            ->from('share')
1160
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
1161
+            ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
1162
+            ->andWhere($qb->expr()->orX(
1163
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1164
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1165
+            ))
1166
+            ->setMaxResults(1);
1167
+        $cursor = $qb->executeQuery();
1168
+
1169
+        $mail = $cursor->fetch() !== false;
1170
+        $cursor->closeCursor();
1171
+
1172
+        return ['public' => $mail];
1173
+    }
1174
+
1175
+    public function getAllShares(): iterable {
1176
+        $qb = $this->dbConnection->getQueryBuilder();
1177
+
1178
+        $qb->select('*')
1179
+            ->from('share')
1180
+            ->where(
1181
+                $qb->expr()->orX(
1182
+                    $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share\IShare::TYPE_EMAIL))
1183
+                )
1184
+            );
1185
+
1186
+        $cursor = $qb->executeQuery();
1187
+        while ($data = $cursor->fetch()) {
1188
+            try {
1189
+                $share = $this->createShareObject($data);
1190
+            } catch (InvalidShare $e) {
1191
+                continue;
1192
+            } catch (ShareNotFound $e) {
1193
+                continue;
1194
+            }
1195
+
1196
+            yield $share;
1197
+        }
1198
+        $cursor->closeCursor();
1199
+    }
1200 1200
 }
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -221,7 +221,7 @@  discard block
 block discarded – undo
221 221
 
222 222
 		$password = $passwordEvent->getPassword();
223 223
 		if ($password === null) {
224
-			$password = $this->secureRandom->generate(8, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS);
224
+			$password = $this->secureRandom->generate(8, ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS);
225 225
 		}
226 226
 
227 227
 		return $password;
@@ -393,7 +393,7 @@  discard block
 block discarded – undo
393 393
 		$text = $this->l->t('%1$s shared »%2$s« with you.', [$initiatorDisplayName, $filename]);
394 394
 
395 395
 		$emailTemplate->addBodyText(
396
-			htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')),
396
+			htmlspecialchars($text.' '.$this->l->t('Click the button below to open it.')),
397 397
 			$text
398 398
 		);
399 399
 		$emailTemplate->addBodyButton(
@@ -422,7 +422,7 @@  discard block
 block discarded – undo
422 422
 		$initiatorEmail = $initiatorUser->getEMailAddress();
423 423
 		if ($this->settingsManager->replyToInitiator() && $initiatorEmail !== null) {
424 424
 			$message->setReplyTo([$initiatorEmail => $initiatorDisplayName]);
425
-			$emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''));
425
+			$emailTemplate->addFooter($instanceName.($this->defaults->getSlogan() !== '' ? ' - '.$this->defaults->getSlogan() : ''));
426 426
 		} else {
427 427
 			$emailTemplate->addFooter();
428 428
 		}
@@ -486,7 +486,7 @@  discard block
 block discarded – undo
486 486
 		$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
487 487
 		if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) {
488 488
 			$message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
489
-			$emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
489
+			$emailTemplate->addFooter($instanceName.' - '.$this->defaults->getSlogan());
490 490
 		} else {
491 491
 			$emailTemplate->addFooter();
492 492
 		}
@@ -546,7 +546,7 @@  discard block
 block discarded – undo
546 546
 		$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
547 547
 		if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) {
548 548
 			$message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
549
-			$emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
549
+			$emailTemplate->addFooter($instanceName.' - '.$this->defaults->getSlogan());
550 550
 		} else {
551 551
 			$emailTemplate->addFooter();
552 552
 		}
@@ -683,7 +683,7 @@  discard block
 block discarded – undo
683 683
 			->setValue('password', $qb->createNamedParameter($password))
684 684
 			->setValue('password_by_talk', $qb->createNamedParameter($sendPasswordByTalk, IQueryBuilder::PARAM_BOOL))
685 685
 			->setValue('stime', $qb->createNamedParameter(time()))
686
-			->setValue('hide_download', $qb->createNamedParameter((int)$hideDownload, IQueryBuilder::PARAM_INT))
686
+			->setValue('hide_download', $qb->createNamedParameter((int) $hideDownload, IQueryBuilder::PARAM_INT))
687 687
 			->setValue('label', $qb->createNamedParameter($label));
688 688
 
689 689
 		if ($expirationTime !== null) {
@@ -731,7 +731,7 @@  discard block
 block discarded – undo
731 731
 			->set('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL))
732 732
 			->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
733 733
 			->set('note', $qb->createNamedParameter($share->getNote()))
734
-			->set('hide_download', $qb->createNamedParameter((int)$share->getHideDownload(), IQueryBuilder::PARAM_INT))
734
+			->set('hide_download', $qb->createNamedParameter((int) $share->getHideDownload(), IQueryBuilder::PARAM_INT))
735 735
 			->executeStatement();
736 736
 
737 737
 		if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') {
@@ -977,22 +977,22 @@  discard block
 block discarded – undo
977 977
 	 */
978 978
 	protected function createShareObject($data) {
979 979
 		$share = new Share($this->rootFolder, $this->userManager);
980
-		$share->setId((int)$data['id'])
981
-			->setShareType((int)$data['share_type'])
982
-			->setPermissions((int)$data['permissions'])
980
+		$share->setId((int) $data['id'])
981
+			->setShareType((int) $data['share_type'])
982
+			->setPermissions((int) $data['permissions'])
983 983
 			->setTarget($data['file_target'])
984
-			->setMailSend((bool)$data['mail_send'])
984
+			->setMailSend((bool) $data['mail_send'])
985 985
 			->setNote($data['note'])
986 986
 			->setToken($data['token']);
987 987
 
988 988
 		$shareTime = new \DateTime();
989
-		$shareTime->setTimestamp((int)$data['stime']);
989
+		$shareTime->setTimestamp((int) $data['stime']);
990 990
 		$share->setShareTime($shareTime);
991 991
 		$share->setSharedWith($data['share_with']);
992 992
 		$share->setPassword($data['password']);
993 993
 		$share->setLabel($data['label']);
994
-		$share->setSendPasswordByTalk((bool)$data['password_by_talk']);
995
-		$share->setHideDownload((bool)$data['hide_download']);
994
+		$share->setSendPasswordByTalk((bool) $data['password_by_talk']);
995
+		$share->setHideDownload((bool) $data['hide_download']);
996 996
 
997 997
 		if ($data['uid_initiator'] !== null) {
998 998
 			$share->setShareOwner($data['uid_owner']);
@@ -1000,7 +1000,7 @@  discard block
 block discarded – undo
1000 1000
 		} else {
1001 1001
 			//OLD SHARE
1002 1002
 			$share->setSharedBy($data['uid_owner']);
1003
-			$path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
1003
+			$path = $this->getNode($share->getSharedBy(), (int) $data['file_source']);
1004 1004
 
1005 1005
 			$owner = $path->getOwner();
1006 1006
 			$share->setShareOwner($owner->getUID());
@@ -1013,7 +1013,7 @@  discard block
 block discarded – undo
1013 1013
 			}
1014 1014
 		}
1015 1015
 
1016
-		$share->setNodeId((int)$data['file_source']);
1016
+		$share->setNodeId((int) $data['file_source']);
1017 1017
 		$share->setNodeType($data['item_type']);
1018 1018
 
1019 1019
 		$share->setProviderId($this->identifier());
@@ -1130,7 +1130,7 @@  discard block
 block discarded – undo
1130 1130
 			);
1131 1131
 		}
1132 1132
 
1133
-		$qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
1133
+		$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
1134 1134
 		$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
1135 1135
 
1136 1136
 		$qb->orderBy('id');
Please login to merge, or discard this patch.
lib/public/AppFramework/Db/QBMapper.php 1 patch
Indentation   +304 added lines, -304 removed lines patch added patch discarded remove patch
@@ -44,308 +44,308 @@
 block discarded – undo
44 44
  */
45 45
 abstract class QBMapper {
46 46
 
47
-	/** @var string */
48
-	protected $tableName;
49
-
50
-	/** @var string|class-string<T> */
51
-	protected $entityClass;
52
-
53
-	/** @var IDBConnection */
54
-	protected $db;
55
-
56
-	/**
57
-	 * @param IDBConnection $db Instance of the Db abstraction layer
58
-	 * @param string $tableName the name of the table. set this to allow entity
59
-	 * @param string|null $entityClass the name of the entity that the sql should be
60
-	 * @psalm-param class-string<T>|null $entityClass the name of the entity that the sql should be
61
-	 * mapped to queries without using sql
62
-	 * @since 14.0.0
63
-	 */
64
-	public function __construct(IDBConnection $db, string $tableName, string $entityClass = null) {
65
-		$this->db = $db;
66
-		$this->tableName = $tableName;
67
-
68
-		// if not given set the entity name to the class without the mapper part
69
-		// cache it here for later use since reflection is slow
70
-		if ($entityClass === null) {
71
-			$this->entityClass = str_replace('Mapper', '', \get_class($this));
72
-		} else {
73
-			$this->entityClass = $entityClass;
74
-		}
75
-	}
76
-
77
-
78
-	/**
79
-	 * @return string the table name
80
-	 * @since 14.0.0
81
-	 */
82
-	public function getTableName(): string {
83
-		return $this->tableName;
84
-	}
85
-
86
-
87
-	/**
88
-	 * Deletes an entity from the table
89
-	 * @param Entity $entity the entity that should be deleted
90
-	 * @psalm-param T $entity the entity that should be deleted
91
-	 * @return Entity the deleted entity
92
-	 * @psalm-return T the deleted entity
93
-	 * @since 14.0.0
94
-	 */
95
-	public function delete(Entity $entity): Entity {
96
-		$qb = $this->db->getQueryBuilder();
97
-
98
-		$idType = $this->getParameterTypeForProperty($entity, 'id');
99
-
100
-		$qb->delete($this->tableName)
101
-			->where(
102
-				$qb->expr()->eq('id', $qb->createNamedParameter($entity->getId(), $idType))
103
-			);
104
-		$qb->executeStatement();
105
-		return $entity;
106
-	}
107
-
108
-
109
-	/**
110
-	 * Creates a new entry in the db from an entity
111
-	 * @param Entity $entity the entity that should be created
112
-	 * @psalm-param T $entity the entity that should be created
113
-	 * @return Entity the saved entity with the set id
114
-	 * @psalm-return T the saved entity with the set id
115
-	 * @since 14.0.0
116
-	 */
117
-	public function insert(Entity $entity): Entity {
118
-		// get updated fields to save, fields have to be set using a setter to
119
-		// be saved
120
-		$properties = $entity->getUpdatedFields();
121
-
122
-		$qb = $this->db->getQueryBuilder();
123
-		$qb->insert($this->tableName);
124
-
125
-		// build the fields
126
-		foreach ($properties as $property => $updated) {
127
-			$column = $entity->propertyToColumn($property);
128
-			$getter = 'get' . ucfirst($property);
129
-			$value = $entity->$getter();
130
-
131
-			$type = $this->getParameterTypeForProperty($entity, $property);
132
-			$qb->setValue($column, $qb->createNamedParameter($value, $type));
133
-		}
134
-
135
-		$qb->executeStatement();
136
-
137
-		if ($entity->id === null) {
138
-			// When autoincrement is used id is always an int
139
-			$entity->setId($qb->getLastInsertId());
140
-		}
141
-
142
-		return $entity;
143
-	}
144
-
145
-	/**
146
-	 * Tries to creates a new entry in the db from an entity and
147
-	 * updates an existing entry if duplicate keys are detected
148
-	 * by the database
149
-	 *
150
-	 * @param Entity $entity the entity that should be created/updated
151
-	 * @psalm-param T $entity the entity that should be created/updated
152
-	 * @return Entity the saved entity with the (new) id
153
-	 * @psalm-return T the saved entity with the (new) id
154
-	 * @throws \InvalidArgumentException if entity has no id
155
-	 * @since 15.0.0
156
-	 */
157
-	public function insertOrUpdate(Entity $entity): Entity {
158
-		try {
159
-			return $this->insert($entity);
160
-		} catch (Exception $ex) {
161
-			if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
162
-				return $this->update($entity);
163
-			}
164
-			throw $ex;
165
-		}
166
-	}
167
-
168
-	/**
169
-	 * Updates an entry in the db from an entity
170
-	 * @throws \InvalidArgumentException if entity has no id
171
-	 * @param Entity $entity the entity that should be created
172
-	 * @psalm-param T $entity the entity that should be created
173
-	 * @return Entity the saved entity with the set id
174
-	 * @psalm-return T the saved entity with the set id
175
-	 * @since 14.0.0
176
-	 */
177
-	public function update(Entity $entity): Entity {
178
-		// if entity wasn't changed it makes no sense to run a db query
179
-		$properties = $entity->getUpdatedFields();
180
-		if (\count($properties) === 0) {
181
-			return $entity;
182
-		}
183
-
184
-		// entity needs an id
185
-		$id = $entity->getId();
186
-		if ($id === null) {
187
-			throw new \InvalidArgumentException(
188
-				'Entity which should be updated has no id');
189
-		}
190
-
191
-		// get updated fields to save, fields have to be set using a setter to
192
-		// be saved
193
-		// do not update the id field
194
-		unset($properties['id']);
195
-
196
-		$qb = $this->db->getQueryBuilder();
197
-		$qb->update($this->tableName);
198
-
199
-		// build the fields
200
-		foreach ($properties as $property => $updated) {
201
-			$column = $entity->propertyToColumn($property);
202
-			$getter = 'get' . ucfirst($property);
203
-			$value = $entity->$getter();
204
-
205
-			$type = $this->getParameterTypeForProperty($entity, $property);
206
-			$qb->set($column, $qb->createNamedParameter($value, $type));
207
-		}
208
-
209
-		$idType = $this->getParameterTypeForProperty($entity, 'id');
210
-
211
-		$qb->where(
212
-			$qb->expr()->eq('id', $qb->createNamedParameter($id, $idType))
213
-		);
214
-		$qb->executeStatement();
215
-
216
-		return $entity;
217
-	}
218
-
219
-	/**
220
-	 * Returns the type parameter for the QueryBuilder for a specific property
221
-	 * of the $entity
222
-	 *
223
-	 * @param Entity $entity   The entity to get the types from
224
-	 * @psalm-param T $entity
225
-	 * @param string $property The property of $entity to get the type for
226
-	 * @return int
227
-	 * @since 16.0.0
228
-	 */
229
-	protected function getParameterTypeForProperty(Entity $entity, string $property): int {
230
-		$types = $entity->getFieldTypes();
231
-
232
-		if (!isset($types[ $property ])) {
233
-			return IQueryBuilder::PARAM_STR;
234
-		}
235
-
236
-		switch ($types[ $property ]) {
237
-			case 'int':
238
-			case 'integer':
239
-				return IQueryBuilder::PARAM_INT;
240
-			case 'string':
241
-				return IQueryBuilder::PARAM_STR;
242
-			case 'bool':
243
-			case 'boolean':
244
-				return IQueryBuilder::PARAM_BOOL;
245
-			case 'blob':
246
-				return IQueryBuilder::PARAM_LOB;
247
-		}
248
-
249
-		return IQueryBuilder::PARAM_STR;
250
-	}
251
-
252
-	/**
253
-	 * Returns an db result and throws exceptions when there are more or less
254
-	 * results
255
-	 *
256
-	 * @see findEntity
257
-	 *
258
-	 * @param IQueryBuilder $query
259
-	 * @throws DoesNotExistException if the item does not exist
260
-	 * @throws MultipleObjectsReturnedException if more than one item exist
261
-	 * @return array the result as row
262
-	 * @since 14.0.0
263
-	 */
264
-	protected function findOneQuery(IQueryBuilder $query): array {
265
-		$result = $query->executeQuery();
266
-
267
-		$row = $result->fetch();
268
-		if ($row === false) {
269
-			$result->closeCursor();
270
-			$msg = $this->buildDebugMessage(
271
-				'Did expect one result but found none when executing', $query
272
-			);
273
-			throw new DoesNotExistException($msg);
274
-		}
275
-
276
-		$row2 = $result->fetch();
277
-		$result->closeCursor();
278
-		if ($row2 !== false) {
279
-			$msg = $this->buildDebugMessage(
280
-				'Did not expect more than one result when executing', $query
281
-			);
282
-			throw new MultipleObjectsReturnedException($msg);
283
-		}
284
-
285
-		return $row;
286
-	}
287
-
288
-	/**
289
-	 * @param string $msg
290
-	 * @param IQueryBuilder $sql
291
-	 * @return string
292
-	 * @since 14.0.0
293
-	 */
294
-	private function buildDebugMessage(string $msg, IQueryBuilder $sql): string {
295
-		return $msg .
296
-			': query "' . $sql->getSQL() . '"; ';
297
-	}
298
-
299
-
300
-	/**
301
-	 * Creates an entity from a row. Automatically determines the entity class
302
-	 * from the current mapper name (MyEntityMapper -> MyEntity)
303
-	 *
304
-	 * @param array $row the row which should be converted to an entity
305
-	 * @return Entity the entity
306
-	 * @psalm-return T the entity
307
-	 * @since 14.0.0
308
-	 */
309
-	protected function mapRowToEntity(array $row): Entity {
310
-		return \call_user_func($this->entityClass .'::fromRow', $row);
311
-	}
312
-
313
-
314
-	/**
315
-	 * Runs a sql query and returns an array of entities
316
-	 *
317
-	 * @param IQueryBuilder $query
318
-	 * @return Entity[] all fetched entities
319
-	 * @psalm-return T[] all fetched entities
320
-	 * @since 14.0.0
321
-	 */
322
-	protected function findEntities(IQueryBuilder $query): array {
323
-		$result = $query->executeQuery();
324
-
325
-		$entities = [];
326
-
327
-		while ($row = $result->fetch()) {
328
-			$entities[] = $this->mapRowToEntity($row);
329
-		}
330
-
331
-		$result->closeCursor();
332
-
333
-		return $entities;
334
-	}
335
-
336
-
337
-	/**
338
-	 * Returns an db result and throws exceptions when there are more or less
339
-	 * results
340
-	 *
341
-	 * @param IQueryBuilder $query
342
-	 * @throws DoesNotExistException if the item does not exist
343
-	 * @throws MultipleObjectsReturnedException if more than one item exist
344
-	 * @return Entity the entity
345
-	 * @psalm-return T the entity
346
-	 * @since 14.0.0
347
-	 */
348
-	protected function findEntity(IQueryBuilder $query): Entity {
349
-		return $this->mapRowToEntity($this->findOneQuery($query));
350
-	}
47
+    /** @var string */
48
+    protected $tableName;
49
+
50
+    /** @var string|class-string<T> */
51
+    protected $entityClass;
52
+
53
+    /** @var IDBConnection */
54
+    protected $db;
55
+
56
+    /**
57
+     * @param IDBConnection $db Instance of the Db abstraction layer
58
+     * @param string $tableName the name of the table. set this to allow entity
59
+     * @param string|null $entityClass the name of the entity that the sql should be
60
+     * @psalm-param class-string<T>|null $entityClass the name of the entity that the sql should be
61
+     * mapped to queries without using sql
62
+     * @since 14.0.0
63
+     */
64
+    public function __construct(IDBConnection $db, string $tableName, string $entityClass = null) {
65
+        $this->db = $db;
66
+        $this->tableName = $tableName;
67
+
68
+        // if not given set the entity name to the class without the mapper part
69
+        // cache it here for later use since reflection is slow
70
+        if ($entityClass === null) {
71
+            $this->entityClass = str_replace('Mapper', '', \get_class($this));
72
+        } else {
73
+            $this->entityClass = $entityClass;
74
+        }
75
+    }
76
+
77
+
78
+    /**
79
+     * @return string the table name
80
+     * @since 14.0.0
81
+     */
82
+    public function getTableName(): string {
83
+        return $this->tableName;
84
+    }
85
+
86
+
87
+    /**
88
+     * Deletes an entity from the table
89
+     * @param Entity $entity the entity that should be deleted
90
+     * @psalm-param T $entity the entity that should be deleted
91
+     * @return Entity the deleted entity
92
+     * @psalm-return T the deleted entity
93
+     * @since 14.0.0
94
+     */
95
+    public function delete(Entity $entity): Entity {
96
+        $qb = $this->db->getQueryBuilder();
97
+
98
+        $idType = $this->getParameterTypeForProperty($entity, 'id');
99
+
100
+        $qb->delete($this->tableName)
101
+            ->where(
102
+                $qb->expr()->eq('id', $qb->createNamedParameter($entity->getId(), $idType))
103
+            );
104
+        $qb->executeStatement();
105
+        return $entity;
106
+    }
107
+
108
+
109
+    /**
110
+     * Creates a new entry in the db from an entity
111
+     * @param Entity $entity the entity that should be created
112
+     * @psalm-param T $entity the entity that should be created
113
+     * @return Entity the saved entity with the set id
114
+     * @psalm-return T the saved entity with the set id
115
+     * @since 14.0.0
116
+     */
117
+    public function insert(Entity $entity): Entity {
118
+        // get updated fields to save, fields have to be set using a setter to
119
+        // be saved
120
+        $properties = $entity->getUpdatedFields();
121
+
122
+        $qb = $this->db->getQueryBuilder();
123
+        $qb->insert($this->tableName);
124
+
125
+        // build the fields
126
+        foreach ($properties as $property => $updated) {
127
+            $column = $entity->propertyToColumn($property);
128
+            $getter = 'get' . ucfirst($property);
129
+            $value = $entity->$getter();
130
+
131
+            $type = $this->getParameterTypeForProperty($entity, $property);
132
+            $qb->setValue($column, $qb->createNamedParameter($value, $type));
133
+        }
134
+
135
+        $qb->executeStatement();
136
+
137
+        if ($entity->id === null) {
138
+            // When autoincrement is used id is always an int
139
+            $entity->setId($qb->getLastInsertId());
140
+        }
141
+
142
+        return $entity;
143
+    }
144
+
145
+    /**
146
+     * Tries to creates a new entry in the db from an entity and
147
+     * updates an existing entry if duplicate keys are detected
148
+     * by the database
149
+     *
150
+     * @param Entity $entity the entity that should be created/updated
151
+     * @psalm-param T $entity the entity that should be created/updated
152
+     * @return Entity the saved entity with the (new) id
153
+     * @psalm-return T the saved entity with the (new) id
154
+     * @throws \InvalidArgumentException if entity has no id
155
+     * @since 15.0.0
156
+     */
157
+    public function insertOrUpdate(Entity $entity): Entity {
158
+        try {
159
+            return $this->insert($entity);
160
+        } catch (Exception $ex) {
161
+            if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
162
+                return $this->update($entity);
163
+            }
164
+            throw $ex;
165
+        }
166
+    }
167
+
168
+    /**
169
+     * Updates an entry in the db from an entity
170
+     * @throws \InvalidArgumentException if entity has no id
171
+     * @param Entity $entity the entity that should be created
172
+     * @psalm-param T $entity the entity that should be created
173
+     * @return Entity the saved entity with the set id
174
+     * @psalm-return T the saved entity with the set id
175
+     * @since 14.0.0
176
+     */
177
+    public function update(Entity $entity): Entity {
178
+        // if entity wasn't changed it makes no sense to run a db query
179
+        $properties = $entity->getUpdatedFields();
180
+        if (\count($properties) === 0) {
181
+            return $entity;
182
+        }
183
+
184
+        // entity needs an id
185
+        $id = $entity->getId();
186
+        if ($id === null) {
187
+            throw new \InvalidArgumentException(
188
+                'Entity which should be updated has no id');
189
+        }
190
+
191
+        // get updated fields to save, fields have to be set using a setter to
192
+        // be saved
193
+        // do not update the id field
194
+        unset($properties['id']);
195
+
196
+        $qb = $this->db->getQueryBuilder();
197
+        $qb->update($this->tableName);
198
+
199
+        // build the fields
200
+        foreach ($properties as $property => $updated) {
201
+            $column = $entity->propertyToColumn($property);
202
+            $getter = 'get' . ucfirst($property);
203
+            $value = $entity->$getter();
204
+
205
+            $type = $this->getParameterTypeForProperty($entity, $property);
206
+            $qb->set($column, $qb->createNamedParameter($value, $type));
207
+        }
208
+
209
+        $idType = $this->getParameterTypeForProperty($entity, 'id');
210
+
211
+        $qb->where(
212
+            $qb->expr()->eq('id', $qb->createNamedParameter($id, $idType))
213
+        );
214
+        $qb->executeStatement();
215
+
216
+        return $entity;
217
+    }
218
+
219
+    /**
220
+     * Returns the type parameter for the QueryBuilder for a specific property
221
+     * of the $entity
222
+     *
223
+     * @param Entity $entity   The entity to get the types from
224
+     * @psalm-param T $entity
225
+     * @param string $property The property of $entity to get the type for
226
+     * @return int
227
+     * @since 16.0.0
228
+     */
229
+    protected function getParameterTypeForProperty(Entity $entity, string $property): int {
230
+        $types = $entity->getFieldTypes();
231
+
232
+        if (!isset($types[ $property ])) {
233
+            return IQueryBuilder::PARAM_STR;
234
+        }
235
+
236
+        switch ($types[ $property ]) {
237
+            case 'int':
238
+            case 'integer':
239
+                return IQueryBuilder::PARAM_INT;
240
+            case 'string':
241
+                return IQueryBuilder::PARAM_STR;
242
+            case 'bool':
243
+            case 'boolean':
244
+                return IQueryBuilder::PARAM_BOOL;
245
+            case 'blob':
246
+                return IQueryBuilder::PARAM_LOB;
247
+        }
248
+
249
+        return IQueryBuilder::PARAM_STR;
250
+    }
251
+
252
+    /**
253
+     * Returns an db result and throws exceptions when there are more or less
254
+     * results
255
+     *
256
+     * @see findEntity
257
+     *
258
+     * @param IQueryBuilder $query
259
+     * @throws DoesNotExistException if the item does not exist
260
+     * @throws MultipleObjectsReturnedException if more than one item exist
261
+     * @return array the result as row
262
+     * @since 14.0.0
263
+     */
264
+    protected function findOneQuery(IQueryBuilder $query): array {
265
+        $result = $query->executeQuery();
266
+
267
+        $row = $result->fetch();
268
+        if ($row === false) {
269
+            $result->closeCursor();
270
+            $msg = $this->buildDebugMessage(
271
+                'Did expect one result but found none when executing', $query
272
+            );
273
+            throw new DoesNotExistException($msg);
274
+        }
275
+
276
+        $row2 = $result->fetch();
277
+        $result->closeCursor();
278
+        if ($row2 !== false) {
279
+            $msg = $this->buildDebugMessage(
280
+                'Did not expect more than one result when executing', $query
281
+            );
282
+            throw new MultipleObjectsReturnedException($msg);
283
+        }
284
+
285
+        return $row;
286
+    }
287
+
288
+    /**
289
+     * @param string $msg
290
+     * @param IQueryBuilder $sql
291
+     * @return string
292
+     * @since 14.0.0
293
+     */
294
+    private function buildDebugMessage(string $msg, IQueryBuilder $sql): string {
295
+        return $msg .
296
+            ': query "' . $sql->getSQL() . '"; ';
297
+    }
298
+
299
+
300
+    /**
301
+     * Creates an entity from a row. Automatically determines the entity class
302
+     * from the current mapper name (MyEntityMapper -> MyEntity)
303
+     *
304
+     * @param array $row the row which should be converted to an entity
305
+     * @return Entity the entity
306
+     * @psalm-return T the entity
307
+     * @since 14.0.0
308
+     */
309
+    protected function mapRowToEntity(array $row): Entity {
310
+        return \call_user_func($this->entityClass .'::fromRow', $row);
311
+    }
312
+
313
+
314
+    /**
315
+     * Runs a sql query and returns an array of entities
316
+     *
317
+     * @param IQueryBuilder $query
318
+     * @return Entity[] all fetched entities
319
+     * @psalm-return T[] all fetched entities
320
+     * @since 14.0.0
321
+     */
322
+    protected function findEntities(IQueryBuilder $query): array {
323
+        $result = $query->executeQuery();
324
+
325
+        $entities = [];
326
+
327
+        while ($row = $result->fetch()) {
328
+            $entities[] = $this->mapRowToEntity($row);
329
+        }
330
+
331
+        $result->closeCursor();
332
+
333
+        return $entities;
334
+    }
335
+
336
+
337
+    /**
338
+     * Returns an db result and throws exceptions when there are more or less
339
+     * results
340
+     *
341
+     * @param IQueryBuilder $query
342
+     * @throws DoesNotExistException if the item does not exist
343
+     * @throws MultipleObjectsReturnedException if more than one item exist
344
+     * @return Entity the entity
345
+     * @psalm-return T the entity
346
+     * @since 14.0.0
347
+     */
348
+    protected function findEntity(IQueryBuilder $query): Entity {
349
+        return $this->mapRowToEntity($this->findOneQuery($query));
350
+    }
351 351
 }
Please login to merge, or discard this patch.
lib/public/DB/QueryBuilder/IQueryBuilder.php 1 patch
Indentation   +970 added lines, -970 removed lines patch added patch discarded remove patch
@@ -40,974 +40,974 @@
 block discarded – undo
40 40
  */
41 41
 interface IQueryBuilder {
42 42
 
43
-	/**
44
-	 * @since 9.0.0
45
-	 */
46
-	public const PARAM_NULL = \PDO::PARAM_NULL;
47
-	/**
48
-	 * @since 9.0.0
49
-	 */
50
-	public const PARAM_BOOL = \PDO::PARAM_BOOL;
51
-	/**
52
-	 * @since 9.0.0
53
-	 */
54
-	public const PARAM_INT = \PDO::PARAM_INT;
55
-	/**
56
-	 * @since 9.0.0
57
-	 */
58
-	public const PARAM_STR = \PDO::PARAM_STR;
59
-	/**
60
-	 * @since 9.0.0
61
-	 */
62
-	public const PARAM_LOB = \PDO::PARAM_LOB;
63
-	/**
64
-	 * @since 9.0.0
65
-	 */
66
-	public const PARAM_DATE = 'datetime';
67
-
68
-	/**
69
-	 * @since 9.0.0
70
-	 */
71
-	public const PARAM_INT_ARRAY = Connection::PARAM_INT_ARRAY;
72
-	/**
73
-	 * @since 9.0.0
74
-	 */
75
-	public const PARAM_STR_ARRAY = Connection::PARAM_STR_ARRAY;
76
-
77
-
78
-	/**
79
-	 * Enable/disable automatic prefixing of table names with the oc_ prefix
80
-	 *
81
-	 * @param bool $enabled If set to true table names will be prefixed with the
82
-	 * owncloud database prefix automatically.
83
-	 * @since 8.2.0
84
-	 */
85
-	public function automaticTablePrefix($enabled);
86
-
87
-	/**
88
-	 * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
89
-	 * This producer method is intended for convenient inline usage. Example:
90
-	 *
91
-	 * <code>
92
-	 *     $qb = $conn->getQueryBuilder()
93
-	 *         ->select('u')
94
-	 *         ->from('users', 'u')
95
-	 *         ->where($qb->expr()->eq('u.id', 1));
96
-	 * </code>
97
-	 *
98
-	 * For more complex expression construction, consider storing the expression
99
-	 * builder object in a local variable.
100
-	 *
101
-	 * @return \OCP\DB\QueryBuilder\IExpressionBuilder
102
-	 * @since 8.2.0
103
-	 */
104
-	public function expr();
105
-
106
-	/**
107
-	 * Gets an FunctionBuilder used for object-oriented construction of query functions.
108
-	 * This producer method is intended for convenient inline usage. Example:
109
-	 *
110
-	 * <code>
111
-	 *     $qb = $conn->getQueryBuilder()
112
-	 *         ->select('u')
113
-	 *         ->from('users', 'u')
114
-	 *         ->where($qb->fun()->md5('u.id'));
115
-	 * </code>
116
-	 *
117
-	 * For more complex function construction, consider storing the function
118
-	 * builder object in a local variable.
119
-	 *
120
-	 * @return \OCP\DB\QueryBuilder\IFunctionBuilder
121
-	 * @since 12.0.0
122
-	 */
123
-	public function func();
124
-
125
-	/**
126
-	 * Gets the type of the currently built query.
127
-	 *
128
-	 * @return integer
129
-	 * @since 8.2.0
130
-	 */
131
-	public function getType();
132
-
133
-	/**
134
-	 * Gets the associated DBAL Connection for this query builder.
135
-	 *
136
-	 * @return \OCP\IDBConnection
137
-	 * @since 8.2.0
138
-	 */
139
-	public function getConnection();
140
-
141
-	/**
142
-	 * Gets the state of this query builder instance.
143
-	 *
144
-	 * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
145
-	 * @since 8.2.0
146
-	 */
147
-	public function getState();
148
-
149
-	/**
150
-	 * Executes this query using the bound parameters and their types.
151
-	 *
152
-	 * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeStatement}
153
-	 * for insert, update and delete statements.
154
-	 *
155
-	 * Warning: until Nextcloud 20, this method could return a \Doctrine\DBAL\Driver\Statement but since
156
-	 *          that interface changed in a breaking way the adapter \OCP\DB\QueryBuilder\IStatement is returned
157
-	 *          to bridge old code to the new API
158
-	 *
159
-	 * @return IResult|int
160
-	 * @throws Exception since 21.0.0
161
-	 * @since 8.2.0
162
-	 * @deprecated 22.0.0 Use executeQuery or executeUpdate
163
-	 */
164
-	public function execute();
165
-
166
-	/**
167
-	 * Execute for select statements
168
-	 *
169
-	 * @return IResult
170
-	 * @since 22.0.0
171
-	 *
172
-	 * @throws Exception
173
-	 * @throws \RuntimeException in case of usage with non select query
174
-	 */
175
-	public function executeQuery(): IResult;
176
-
177
-	/**
178
-	 * Execute insert, update and delete statements
179
-	 *
180
-	 * @return int the number of affected rows
181
-	 * @since 22.0.0
182
-	 *
183
-	 * @throws Exception
184
-	 * @throws \RuntimeException in case of usage with select query
185
-	 */
186
-	public function executeStatement(): int;
187
-
188
-	/**
189
-	 * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
190
-	 *
191
-	 * <code>
192
-	 *     $qb = $conn->getQueryBuilder()
193
-	 *         ->select('u')
194
-	 *         ->from('User', 'u')
195
-	 *     echo $qb->getSQL(); // SELECT u FROM User u
196
-	 * </code>
197
-	 *
198
-	 * @return string The SQL query string.
199
-	 * @since 8.2.0
200
-	 */
201
-	public function getSQL();
202
-
203
-	/**
204
-	 * Sets a query parameter for the query being constructed.
205
-	 *
206
-	 * <code>
207
-	 *     $qb = $conn->getQueryBuilder()
208
-	 *         ->select('u')
209
-	 *         ->from('users', 'u')
210
-	 *         ->where('u.id = :user_id')
211
-	 *         ->setParameter(':user_id', 1);
212
-	 * </code>
213
-	 *
214
-	 * @param string|integer $key The parameter position or name.
215
-	 * @param mixed $value The parameter value.
216
-	 * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants.
217
-	 *
218
-	 * @return $this This QueryBuilder instance.
219
-	 * @since 8.2.0
220
-	 */
221
-	public function setParameter($key, $value, $type = null);
222
-
223
-	/**
224
-	 * Sets a collection of query parameters for the query being constructed.
225
-	 *
226
-	 * <code>
227
-	 *     $qb = $conn->getQueryBuilder()
228
-	 *         ->select('u')
229
-	 *         ->from('users', 'u')
230
-	 *         ->where('u.id = :user_id1 OR u.id = :user_id2')
231
-	 *         ->setParameters(array(
232
-	 *             ':user_id1' => 1,
233
-	 *             ':user_id2' => 2
234
-	 *         ));
235
-	 * </code>
236
-	 *
237
-	 * @param array $params The query parameters to set.
238
-	 * @param array $types The query parameters types to set.
239
-	 *
240
-	 * @return $this This QueryBuilder instance.
241
-	 * @since 8.2.0
242
-	 */
243
-	public function setParameters(array $params, array $types = []);
244
-
245
-	/**
246
-	 * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
247
-	 *
248
-	 * @return array The currently defined query parameters indexed by parameter index or name.
249
-	 * @since 8.2.0
250
-	 */
251
-	public function getParameters();
252
-
253
-	/**
254
-	 * Gets a (previously set) query parameter of the query being constructed.
255
-	 *
256
-	 * @param mixed $key The key (index or name) of the bound parameter.
257
-	 *
258
-	 * @return mixed The value of the bound parameter.
259
-	 * @since 8.2.0
260
-	 */
261
-	public function getParameter($key);
262
-
263
-	/**
264
-	 * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
265
-	 *
266
-	 * @return array The currently defined query parameter types indexed by parameter index or name.
267
-	 * @since 8.2.0
268
-	 */
269
-	public function getParameterTypes();
270
-
271
-	/**
272
-	 * Gets a (previously set) query parameter type of the query being constructed.
273
-	 *
274
-	 * @param mixed $key The key (index or name) of the bound parameter type.
275
-	 *
276
-	 * @return mixed The value of the bound parameter type.
277
-	 * @since 8.2.0
278
-	 */
279
-	public function getParameterType($key);
280
-
281
-	/**
282
-	 * Sets the position of the first result to retrieve (the "offset").
283
-	 *
284
-	 * @param integer $firstResult The first result to return.
285
-	 *
286
-	 * @return $this This QueryBuilder instance.
287
-	 * @since 8.2.0
288
-	 */
289
-	public function setFirstResult($firstResult);
290
-
291
-	/**
292
-	 * Gets the position of the first result the query object was set to retrieve (the "offset").
293
-	 * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder.
294
-	 *
295
-	 * @return integer The position of the first result.
296
-	 * @since 8.2.0
297
-	 */
298
-	public function getFirstResult();
299
-
300
-	/**
301
-	 * Sets the maximum number of results to retrieve (the "limit").
302
-	 *
303
-	 * @param integer $maxResults The maximum number of results to retrieve.
304
-	 *
305
-	 * @return $this This QueryBuilder instance.
306
-	 * @since 8.2.0
307
-	 */
308
-	public function setMaxResults($maxResults);
309
-
310
-	/**
311
-	 * Gets the maximum number of results the query object was set to retrieve (the "limit").
312
-	 * Returns NULL if {@link setMaxResults} was not applied to this query builder.
313
-	 *
314
-	 * @return int|null The maximum number of results.
315
-	 * @since 8.2.0
316
-	 */
317
-	public function getMaxResults();
318
-
319
-	/**
320
-	 * Specifies an item that is to be returned in the query result.
321
-	 * Replaces any previously specified selections, if any.
322
-	 *
323
-	 * <code>
324
-	 *     $qb = $conn->getQueryBuilder()
325
-	 *         ->select('u.id', 'p.id')
326
-	 *         ->from('users', 'u')
327
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
328
-	 * </code>
329
-	 *
330
-	 * @param mixed ...$selects The selection expressions.
331
-	 *
332
-	 * @return $this This QueryBuilder instance.
333
-	 * @since 8.2.0
334
-	 *
335
-	 * @psalm-taint-sink sql $selects
336
-	 */
337
-	public function select(...$selects);
338
-
339
-	/**
340
-	 * Specifies an item that is to be returned with a different name in the query result.
341
-	 *
342
-	 * <code>
343
-	 *     $qb = $conn->getQueryBuilder()
344
-	 *         ->selectAlias('u.id', 'user_id')
345
-	 *         ->from('users', 'u')
346
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
347
-	 * </code>
348
-	 *
349
-	 * @param mixed $select The selection expressions.
350
-	 * @param string $alias The column alias used in the constructed query.
351
-	 *
352
-	 * @return $this This QueryBuilder instance.
353
-	 * @since 8.2.1
354
-	 *
355
-	 * @psalm-taint-sink sql $select
356
-	 * @psalm-taint-sink sql $alias
357
-	 */
358
-	public function selectAlias($select, $alias);
359
-
360
-	/**
361
-	 * Specifies an item that is to be returned uniquely in the query result.
362
-	 *
363
-	 * <code>
364
-	 *     $qb = $conn->getQueryBuilder()
365
-	 *         ->selectDistinct('type')
366
-	 *         ->from('users');
367
-	 * </code>
368
-	 *
369
-	 * @param mixed $select The selection expressions.
370
-	 *
371
-	 * @return $this This QueryBuilder instance.
372
-	 * @since 9.0.0
373
-	 *
374
-	 * @psalm-taint-sink sql $select
375
-	 */
376
-	public function selectDistinct($select);
377
-
378
-	/**
379
-	 * Adds an item that is to be returned in the query result.
380
-	 *
381
-	 * <code>
382
-	 *     $qb = $conn->getQueryBuilder()
383
-	 *         ->select('u.id')
384
-	 *         ->addSelect('p.id')
385
-	 *         ->from('users', 'u')
386
-	 *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
387
-	 * </code>
388
-	 *
389
-	 * @param mixed ...$select The selection expression.
390
-	 *
391
-	 * @return $this This QueryBuilder instance.
392
-	 * @since 8.2.0
393
-	 *
394
-	 * @psalm-taint-sink sql $select
395
-	 */
396
-	public function addSelect(...$select);
397
-
398
-	/**
399
-	 * Turns the query being built into a bulk delete query that ranges over
400
-	 * a certain table.
401
-	 *
402
-	 * <code>
403
-	 *     $qb = $conn->getQueryBuilder()
404
-	 *         ->delete('users', 'u')
405
-	 *         ->where('u.id = :user_id');
406
-	 *         ->setParameter(':user_id', 1);
407
-	 * </code>
408
-	 *
409
-	 * @param string $delete The table whose rows are subject to the deletion.
410
-	 * @param string $alias The table alias used in the constructed query.
411
-	 *
412
-	 * @return $this This QueryBuilder instance.
413
-	 * @since 8.2.0
414
-	 *
415
-	 * @psalm-taint-sink sql $delete
416
-	 */
417
-	public function delete($delete = null, $alias = null);
418
-
419
-	/**
420
-	 * Turns the query being built into a bulk update query that ranges over
421
-	 * a certain table
422
-	 *
423
-	 * <code>
424
-	 *     $qb = $conn->getQueryBuilder()
425
-	 *         ->update('users', 'u')
426
-	 *         ->set('u.password', md5('password'))
427
-	 *         ->where('u.id = ?');
428
-	 * </code>
429
-	 *
430
-	 * @param string $update The table whose rows are subject to the update.
431
-	 * @param string $alias The table alias used in the constructed query.
432
-	 *
433
-	 * @return $this This QueryBuilder instance.
434
-	 * @since 8.2.0
435
-	 *
436
-	 * @psalm-taint-sink sql $update
437
-	 */
438
-	public function update($update = null, $alias = null);
439
-
440
-	/**
441
-	 * Turns the query being built into an insert query that inserts into
442
-	 * a certain table
443
-	 *
444
-	 * <code>
445
-	 *     $qb = $conn->getQueryBuilder()
446
-	 *         ->insert('users')
447
-	 *         ->values(
448
-	 *             array(
449
-	 *                 'name' => '?',
450
-	 *                 'password' => '?'
451
-	 *             )
452
-	 *         );
453
-	 * </code>
454
-	 *
455
-	 * @param string $insert The table into which the rows should be inserted.
456
-	 *
457
-	 * @return $this This QueryBuilder instance.
458
-	 * @since 8.2.0
459
-	 *
460
-	 * @psalm-taint-sink sql $insert
461
-	 */
462
-	public function insert($insert = null);
463
-
464
-	/**
465
-	 * Creates and adds a query root corresponding to the table identified by the
466
-	 * given alias, forming a cartesian product with any existing query roots.
467
-	 *
468
-	 * <code>
469
-	 *     $qb = $conn->getQueryBuilder()
470
-	 *         ->select('u.id')
471
-	 *         ->from('users', 'u')
472
-	 * </code>
473
-	 *
474
-	 * @param string $from The table.
475
-	 * @param string|null $alias The alias of the table.
476
-	 *
477
-	 * @return $this This QueryBuilder instance.
478
-	 * @since 8.2.0
479
-	 *
480
-	 * @psalm-taint-sink sql $from
481
-	 */
482
-	public function from($from, $alias = null);
483
-
484
-	/**
485
-	 * Creates and adds a join to the query.
486
-	 *
487
-	 * <code>
488
-	 *     $qb = $conn->getQueryBuilder()
489
-	 *         ->select('u.name')
490
-	 *         ->from('users', 'u')
491
-	 *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
492
-	 * </code>
493
-	 *
494
-	 * @param string $fromAlias The alias that points to a from clause.
495
-	 * @param string $join The table name to join.
496
-	 * @param string $alias The alias of the join table.
497
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
498
-	 *
499
-	 * @return $this This QueryBuilder instance.
500
-	 * @since 8.2.0
501
-	 *
502
-	 * @psalm-taint-sink sql $fromAlias
503
-	 * @psalm-taint-sink sql $join
504
-	 * @psalm-taint-sink sql $alias
505
-	 * @psalm-taint-sink sql $condition
506
-	 */
507
-	public function join($fromAlias, $join, $alias, $condition = null);
508
-
509
-	/**
510
-	 * Creates and adds a join to the query.
511
-	 *
512
-	 * <code>
513
-	 *     $qb = $conn->getQueryBuilder()
514
-	 *         ->select('u.name')
515
-	 *         ->from('users', 'u')
516
-	 *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
517
-	 * </code>
518
-	 *
519
-	 * @param string $fromAlias The alias that points to a from clause.
520
-	 * @param string $join The table name to join.
521
-	 * @param string $alias The alias of the join table.
522
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
523
-	 *
524
-	 * @return $this This QueryBuilder instance.
525
-	 * @since 8.2.0
526
-	 *
527
-	 * @psalm-taint-sink sql $fromAlias
528
-	 * @psalm-taint-sink sql $join
529
-	 * @psalm-taint-sink sql $alias
530
-	 * @psalm-taint-sink sql $condition
531
-	 */
532
-	public function innerJoin($fromAlias, $join, $alias, $condition = null);
533
-
534
-	/**
535
-	 * Creates and adds a left join to the query.
536
-	 *
537
-	 * <code>
538
-	 *     $qb = $conn->getQueryBuilder()
539
-	 *         ->select('u.name')
540
-	 *         ->from('users', 'u')
541
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
542
-	 * </code>
543
-	 *
544
-	 * @param string $fromAlias The alias that points to a from clause.
545
-	 * @param string $join The table name to join.
546
-	 * @param string $alias The alias of the join table.
547
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
548
-	 *
549
-	 * @return $this This QueryBuilder instance.
550
-	 * @since 8.2.0
551
-	 *
552
-	 * @psalm-taint-sink sql $fromAlias
553
-	 * @psalm-taint-sink sql $join
554
-	 * @psalm-taint-sink sql $alias
555
-	 * @psalm-taint-sink sql $condition
556
-	 */
557
-	public function leftJoin($fromAlias, $join, $alias, $condition = null);
558
-
559
-	/**
560
-	 * Creates and adds a right join to the query.
561
-	 *
562
-	 * <code>
563
-	 *     $qb = $conn->getQueryBuilder()
564
-	 *         ->select('u.name')
565
-	 *         ->from('users', 'u')
566
-	 *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
567
-	 * </code>
568
-	 *
569
-	 * @param string $fromAlias The alias that points to a from clause.
570
-	 * @param string $join The table name to join.
571
-	 * @param string $alias The alias of the join table.
572
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
573
-	 *
574
-	 * @return $this This QueryBuilder instance.
575
-	 * @since 8.2.0
576
-	 *
577
-	 * @psalm-taint-sink sql $fromAlias
578
-	 * @psalm-taint-sink sql $join
579
-	 * @psalm-taint-sink sql $alias
580
-	 * @psalm-taint-sink sql $condition
581
-	 */
582
-	public function rightJoin($fromAlias, $join, $alias, $condition = null);
583
-
584
-	/**
585
-	 * Sets a new value for a column in a bulk update query.
586
-	 *
587
-	 * <code>
588
-	 *     $qb = $conn->getQueryBuilder()
589
-	 *         ->update('users', 'u')
590
-	 *         ->set('u.password', md5('password'))
591
-	 *         ->where('u.id = ?');
592
-	 * </code>
593
-	 *
594
-	 * @param string $key The column to set.
595
-	 * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc.
596
-	 *
597
-	 * @return $this This QueryBuilder instance.
598
-	 * @since 8.2.0
599
-	 *
600
-	 * @psalm-taint-sink sql $key
601
-	 * @psalm-taint-sink sql $value
602
-	 */
603
-	public function set($key, $value);
604
-
605
-	/**
606
-	 * Specifies one or more restrictions to the query result.
607
-	 * Replaces any previously specified restrictions, if any.
608
-	 *
609
-	 * <code>
610
-	 *     $qb = $conn->getQueryBuilder()
611
-	 *         ->select('u.name')
612
-	 *         ->from('users', 'u')
613
-	 *         ->where('u.id = ?');
614
-	 *
615
-	 *     // You can optionally programatically build and/or expressions
616
-	 *     $qb = $conn->getQueryBuilder();
617
-	 *
618
-	 *     $or = $qb->expr()->orx();
619
-	 *     $or->add($qb->expr()->eq('u.id', 1));
620
-	 *     $or->add($qb->expr()->eq('u.id', 2));
621
-	 *
622
-	 *     $qb->update('users', 'u')
623
-	 *         ->set('u.password', md5('password'))
624
-	 *         ->where($or);
625
-	 * </code>
626
-	 *
627
-	 * @param mixed $predicates The restriction predicates.
628
-	 *
629
-	 * @return $this This QueryBuilder instance.
630
-	 * @since 8.2.0
631
-	 *
632
-	 * @psalm-taint-sink sql $predicates
633
-	 */
634
-	public function where(...$predicates);
635
-
636
-	/**
637
-	 * Adds one or more restrictions to the query results, forming a logical
638
-	 * conjunction with any previously specified restrictions.
639
-	 *
640
-	 * <code>
641
-	 *     $qb = $conn->getQueryBuilder()
642
-	 *         ->select('u')
643
-	 *         ->from('users', 'u')
644
-	 *         ->where('u.username LIKE ?')
645
-	 *         ->andWhere('u.is_active = 1');
646
-	 * </code>
647
-	 *
648
-	 * @param mixed ...$where The query restrictions.
649
-	 *
650
-	 * @return $this This QueryBuilder instance.
651
-	 *
652
-	 * @see where()
653
-	 * @since 8.2.0
654
-	 *
655
-	 * @psalm-taint-sink sql $where
656
-	 */
657
-	public function andWhere(...$where);
658
-
659
-	/**
660
-	 * Adds one or more restrictions to the query results, forming a logical
661
-	 * disjunction with any previously specified restrictions.
662
-	 *
663
-	 * <code>
664
-	 *     $qb = $conn->getQueryBuilder()
665
-	 *         ->select('u.name')
666
-	 *         ->from('users', 'u')
667
-	 *         ->where('u.id = 1')
668
-	 *         ->orWhere('u.id = 2');
669
-	 * </code>
670
-	 *
671
-	 * @param mixed ...$where The WHERE statement.
672
-	 *
673
-	 * @return $this This QueryBuilder instance.
674
-	 *
675
-	 * @see where()
676
-	 * @since 8.2.0
677
-	 *
678
-	 * @psalm-taint-sink sql $where
679
-	 */
680
-	public function orWhere(...$where);
681
-
682
-	/**
683
-	 * Specifies a grouping over the results of the query.
684
-	 * Replaces any previously specified groupings, if any.
685
-	 *
686
-	 * <code>
687
-	 *     $qb = $conn->getQueryBuilder()
688
-	 *         ->select('u.name')
689
-	 *         ->from('users', 'u')
690
-	 *         ->groupBy('u.id');
691
-	 * </code>
692
-	 *
693
-	 * @param mixed ...$groupBys The grouping expression.
694
-	 *
695
-	 * @return $this This QueryBuilder instance.
696
-	 * @since 8.2.0
697
-	 *
698
-	 * @psalm-taint-sink sql $groupBys
699
-	 */
700
-	public function groupBy(...$groupBys);
701
-
702
-	/**
703
-	 * Adds a grouping expression to the query.
704
-	 *
705
-	 * <code>
706
-	 *     $qb = $conn->getQueryBuilder()
707
-	 *         ->select('u.name')
708
-	 *         ->from('users', 'u')
709
-	 *         ->groupBy('u.lastLogin');
710
-	 *         ->addGroupBy('u.createdAt')
711
-	 * </code>
712
-	 *
713
-	 * @param mixed ...$groupBy The grouping expression.
714
-	 *
715
-	 * @return $this This QueryBuilder instance.
716
-	 * @since 8.2.0
717
-	 *
718
-	 * @psalm-taint-sink sql $groupby
719
-	 */
720
-	public function addGroupBy(...$groupBy);
721
-
722
-	/**
723
-	 * Sets a value for a column in an insert query.
724
-	 *
725
-	 * <code>
726
-	 *     $qb = $conn->getQueryBuilder()
727
-	 *         ->insert('users')
728
-	 *         ->values(
729
-	 *             array(
730
-	 *                 'name' => '?'
731
-	 *             )
732
-	 *         )
733
-	 *         ->setValue('password', '?');
734
-	 * </code>
735
-	 *
736
-	 * @param string $column The column into which the value should be inserted.
737
-	 * @param IParameter|string $value The value that should be inserted into the column.
738
-	 *
739
-	 * @return $this This QueryBuilder instance.
740
-	 * @since 8.2.0
741
-	 *
742
-	 * @psalm-taint-sink sql $column
743
-	 * @psalm-taint-sink sql $value
744
-	 */
745
-	public function setValue($column, $value);
746
-
747
-	/**
748
-	 * Specifies values for an insert query indexed by column names.
749
-	 * Replaces any previous values, if any.
750
-	 *
751
-	 * <code>
752
-	 *     $qb = $conn->getQueryBuilder()
753
-	 *         ->insert('users')
754
-	 *         ->values(
755
-	 *             array(
756
-	 *                 'name' => '?',
757
-	 *                 'password' => '?'
758
-	 *             )
759
-	 *         );
760
-	 * </code>
761
-	 *
762
-	 * @param array $values The values to specify for the insert query indexed by column names.
763
-	 *
764
-	 * @return $this This QueryBuilder instance.
765
-	 * @since 8.2.0
766
-	 *
767
-	 * @psalm-taint-sink sql $values
768
-	 */
769
-	public function values(array $values);
770
-
771
-	/**
772
-	 * Specifies a restriction over the groups of the query.
773
-	 * Replaces any previous having restrictions, if any.
774
-	 *
775
-	 * @param mixed ...$having The restriction over the groups.
776
-	 *
777
-	 * @return $this This QueryBuilder instance.
778
-	 * @since 8.2.0
779
-	 *
780
-	 * @psalm-taint-sink sql $having
781
-	 */
782
-	public function having(...$having);
783
-
784
-	/**
785
-	 * Adds a restriction over the groups of the query, forming a logical
786
-	 * conjunction with any existing having restrictions.
787
-	 *
788
-	 * @param mixed ...$having The restriction to append.
789
-	 *
790
-	 * @return $this This QueryBuilder instance.
791
-	 * @since 8.2.0
792
-	 *
793
-	 * @psalm-taint-sink sql $andHaving
794
-	 */
795
-	public function andHaving(...$having);
796
-
797
-	/**
798
-	 * Adds a restriction over the groups of the query, forming a logical
799
-	 * disjunction with any existing having restrictions.
800
-	 *
801
-	 * @param mixed ...$having The restriction to add.
802
-	 *
803
-	 * @return $this This QueryBuilder instance.
804
-	 * @since 8.2.0
805
-	 *
806
-	 * @psalm-taint-sink sql $having
807
-	 */
808
-	public function orHaving(...$having);
809
-
810
-	/**
811
-	 * Specifies an ordering for the query results.
812
-	 * Replaces any previously specified orderings, if any.
813
-	 *
814
-	 * @param string $sort The ordering expression.
815
-	 * @param string $order The ordering direction.
816
-	 *
817
-	 * @return $this This QueryBuilder instance.
818
-	 * @since 8.2.0
819
-	 *
820
-	 * @psalm-taint-sink sql $sort
821
-	 * @psalm-taint-sink sql $order
822
-	 */
823
-	public function orderBy($sort, $order = null);
824
-
825
-	/**
826
-	 * Adds an ordering to the query results.
827
-	 *
828
-	 * @param string $sort The ordering expression.
829
-	 * @param string $order The ordering direction.
830
-	 *
831
-	 * @return $this This QueryBuilder instance.
832
-	 * @since 8.2.0
833
-	 *
834
-	 * @psalm-taint-sink sql $sort
835
-	 * @psalm-taint-sink sql $order
836
-	 */
837
-	public function addOrderBy($sort, $order = null);
838
-
839
-	/**
840
-	 * Gets a query part by its name.
841
-	 *
842
-	 * @param string $queryPartName
843
-	 *
844
-	 * @return mixed
845
-	 * @since 8.2.0
846
-	 */
847
-	public function getQueryPart($queryPartName);
848
-
849
-	/**
850
-	 * Gets all query parts.
851
-	 *
852
-	 * @return array
853
-	 * @since 8.2.0
854
-	 */
855
-	public function getQueryParts();
856
-
857
-	/**
858
-	 * Resets SQL parts.
859
-	 *
860
-	 * @param array|null $queryPartNames
861
-	 *
862
-	 * @return $this This QueryBuilder instance.
863
-	 * @since 8.2.0
864
-	 */
865
-	public function resetQueryParts($queryPartNames = null);
866
-
867
-	/**
868
-	 * Resets a single SQL part.
869
-	 *
870
-	 * @param string $queryPartName
871
-	 *
872
-	 * @return $this This QueryBuilder instance.
873
-	 * @since 8.2.0
874
-	 */
875
-	public function resetQueryPart($queryPartName);
876
-
877
-	/**
878
-	 * Creates a new named parameter and bind the value $value to it.
879
-	 *
880
-	 * This method provides a shortcut for PDOStatement::bindValue
881
-	 * when using prepared statements.
882
-	 *
883
-	 * The parameter $value specifies the value that you want to bind. If
884
-	 * $placeholder is not provided bindValue() will automatically create a
885
-	 * placeholder for you. An automatic placeholder will be of the name
886
-	 * ':dcValue1', ':dcValue2' etc.
887
-	 *
888
-	 * For more information see {@link https://www.php.net/pdostatement-bindparam}
889
-	 *
890
-	 * Example:
891
-	 * <code>
892
-	 * $value = 2;
893
-	 * $q->eq( 'id', $q->bindValue( $value ) );
894
-	 * $stmt = $q->executeQuery(); // executed with 'id = 2'
895
-	 * </code>
896
-	 *
897
-	 * @license New BSD License
898
-	 * @link http://www.zetacomponents.org
899
-	 *
900
-	 * @param mixed $value
901
-	 * @param mixed $type
902
-	 * @param string $placeHolder The name to bind with. The string must start with a colon ':'.
903
-	 *
904
-	 * @return IParameter
905
-	 * @since 8.2.0
906
-	 *
907
-	 * @psalm-taint-escape sql
908
-	 */
909
-	public function createNamedParameter($value, $type = self::PARAM_STR, $placeHolder = null);
910
-
911
-	/**
912
-	 * Creates a new positional parameter and bind the given value to it.
913
-	 *
914
-	 * Attention: If you are using positional parameters with the query builder you have
915
-	 * to be very careful to bind all parameters in the order they appear in the SQL
916
-	 * statement , otherwise they get bound in the wrong order which can lead to serious
917
-	 * bugs in your code.
918
-	 *
919
-	 * Example:
920
-	 * <code>
921
-	 *  $qb = $conn->getQueryBuilder();
922
-	 *  $qb->select('u.*')
923
-	 *     ->from('users', 'u')
924
-	 *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR))
925
-	 *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR))
926
-	 * </code>
927
-	 *
928
-	 * @param mixed $value
929
-	 * @param integer $type
930
-	 *
931
-	 * @return IParameter
932
-	 * @since 8.2.0
933
-	 *
934
-	 * @psalm-taint-escape sql
935
-	 */
936
-	public function createPositionalParameter($value, $type = self::PARAM_STR);
937
-
938
-	/**
939
-	 * Creates a new parameter
940
-	 *
941
-	 * Example:
942
-	 * <code>
943
-	 *  $qb = $conn->getQueryBuilder();
944
-	 *  $qb->select('u.*')
945
-	 *     ->from('users', 'u')
946
-	 *     ->where('u.username = ' . $qb->createParameter('name'))
947
-	 *     ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR))
948
-	 * </code>
949
-	 *
950
-	 * @param string $name
951
-	 *
952
-	 * @return IParameter
953
-	 * @since 8.2.0
954
-	 *
955
-	 * @psalm-taint-escape sql
956
-	 */
957
-	public function createParameter($name);
958
-
959
-	/**
960
-	 * Creates a new function
961
-	 *
962
-	 * Attention: Column names inside the call have to be quoted before hand
963
-	 *
964
-	 * Example:
965
-	 * <code>
966
-	 *  $qb = $conn->getQueryBuilder();
967
-	 *  $qb->select($qb->createFunction('COUNT(*)'))
968
-	 *     ->from('users', 'u')
969
-	 *  echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u
970
-	 * </code>
971
-	 * <code>
972
-	 *  $qb = $conn->getQueryBuilder();
973
-	 *  $qb->select($qb->createFunction('COUNT(`column`)'))
974
-	 *     ->from('users', 'u')
975
-	 *  echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u
976
-	 * </code>
977
-	 *
978
-	 * @param string $call
979
-	 *
980
-	 * @return IQueryFunction
981
-	 * @since 8.2.0
982
-	 *
983
-	 * @psalm-taint-sink sql
984
-	 */
985
-	public function createFunction($call);
986
-
987
-	/**
988
-	 * Used to get the id of the last inserted element
989
-	 * @return int
990
-	 * @throws \BadMethodCallException When being called before an insert query has been run.
991
-	 * @since 9.0.0
992
-	 */
993
-	public function getLastInsertId(): int;
994
-
995
-	/**
996
-	 * Returns the table name quoted and with database prefix as needed by the implementation
997
-	 *
998
-	 * @param string $table
999
-	 * @return string
1000
-	 * @since 9.0.0
1001
-	 */
1002
-	public function getTableName($table);
1003
-
1004
-	/**
1005
-	 * Returns the column name quoted and with table alias prefix as needed by the implementation
1006
-	 *
1007
-	 * @param string $column
1008
-	 * @param string $tableAlias
1009
-	 * @return string
1010
-	 * @since 9.0.0
1011
-	 */
1012
-	public function getColumnName($column, $tableAlias = '');
43
+    /**
44
+     * @since 9.0.0
45
+     */
46
+    public const PARAM_NULL = \PDO::PARAM_NULL;
47
+    /**
48
+     * @since 9.0.0
49
+     */
50
+    public const PARAM_BOOL = \PDO::PARAM_BOOL;
51
+    /**
52
+     * @since 9.0.0
53
+     */
54
+    public const PARAM_INT = \PDO::PARAM_INT;
55
+    /**
56
+     * @since 9.0.0
57
+     */
58
+    public const PARAM_STR = \PDO::PARAM_STR;
59
+    /**
60
+     * @since 9.0.0
61
+     */
62
+    public const PARAM_LOB = \PDO::PARAM_LOB;
63
+    /**
64
+     * @since 9.0.0
65
+     */
66
+    public const PARAM_DATE = 'datetime';
67
+
68
+    /**
69
+     * @since 9.0.0
70
+     */
71
+    public const PARAM_INT_ARRAY = Connection::PARAM_INT_ARRAY;
72
+    /**
73
+     * @since 9.0.0
74
+     */
75
+    public const PARAM_STR_ARRAY = Connection::PARAM_STR_ARRAY;
76
+
77
+
78
+    /**
79
+     * Enable/disable automatic prefixing of table names with the oc_ prefix
80
+     *
81
+     * @param bool $enabled If set to true table names will be prefixed with the
82
+     * owncloud database prefix automatically.
83
+     * @since 8.2.0
84
+     */
85
+    public function automaticTablePrefix($enabled);
86
+
87
+    /**
88
+     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
89
+     * This producer method is intended for convenient inline usage. Example:
90
+     *
91
+     * <code>
92
+     *     $qb = $conn->getQueryBuilder()
93
+     *         ->select('u')
94
+     *         ->from('users', 'u')
95
+     *         ->where($qb->expr()->eq('u.id', 1));
96
+     * </code>
97
+     *
98
+     * For more complex expression construction, consider storing the expression
99
+     * builder object in a local variable.
100
+     *
101
+     * @return \OCP\DB\QueryBuilder\IExpressionBuilder
102
+     * @since 8.2.0
103
+     */
104
+    public function expr();
105
+
106
+    /**
107
+     * Gets an FunctionBuilder used for object-oriented construction of query functions.
108
+     * This producer method is intended for convenient inline usage. Example:
109
+     *
110
+     * <code>
111
+     *     $qb = $conn->getQueryBuilder()
112
+     *         ->select('u')
113
+     *         ->from('users', 'u')
114
+     *         ->where($qb->fun()->md5('u.id'));
115
+     * </code>
116
+     *
117
+     * For more complex function construction, consider storing the function
118
+     * builder object in a local variable.
119
+     *
120
+     * @return \OCP\DB\QueryBuilder\IFunctionBuilder
121
+     * @since 12.0.0
122
+     */
123
+    public function func();
124
+
125
+    /**
126
+     * Gets the type of the currently built query.
127
+     *
128
+     * @return integer
129
+     * @since 8.2.0
130
+     */
131
+    public function getType();
132
+
133
+    /**
134
+     * Gets the associated DBAL Connection for this query builder.
135
+     *
136
+     * @return \OCP\IDBConnection
137
+     * @since 8.2.0
138
+     */
139
+    public function getConnection();
140
+
141
+    /**
142
+     * Gets the state of this query builder instance.
143
+     *
144
+     * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
145
+     * @since 8.2.0
146
+     */
147
+    public function getState();
148
+
149
+    /**
150
+     * Executes this query using the bound parameters and their types.
151
+     *
152
+     * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeStatement}
153
+     * for insert, update and delete statements.
154
+     *
155
+     * Warning: until Nextcloud 20, this method could return a \Doctrine\DBAL\Driver\Statement but since
156
+     *          that interface changed in a breaking way the adapter \OCP\DB\QueryBuilder\IStatement is returned
157
+     *          to bridge old code to the new API
158
+     *
159
+     * @return IResult|int
160
+     * @throws Exception since 21.0.0
161
+     * @since 8.2.0
162
+     * @deprecated 22.0.0 Use executeQuery or executeUpdate
163
+     */
164
+    public function execute();
165
+
166
+    /**
167
+     * Execute for select statements
168
+     *
169
+     * @return IResult
170
+     * @since 22.0.0
171
+     *
172
+     * @throws Exception
173
+     * @throws \RuntimeException in case of usage with non select query
174
+     */
175
+    public function executeQuery(): IResult;
176
+
177
+    /**
178
+     * Execute insert, update and delete statements
179
+     *
180
+     * @return int the number of affected rows
181
+     * @since 22.0.0
182
+     *
183
+     * @throws Exception
184
+     * @throws \RuntimeException in case of usage with select query
185
+     */
186
+    public function executeStatement(): int;
187
+
188
+    /**
189
+     * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
190
+     *
191
+     * <code>
192
+     *     $qb = $conn->getQueryBuilder()
193
+     *         ->select('u')
194
+     *         ->from('User', 'u')
195
+     *     echo $qb->getSQL(); // SELECT u FROM User u
196
+     * </code>
197
+     *
198
+     * @return string The SQL query string.
199
+     * @since 8.2.0
200
+     */
201
+    public function getSQL();
202
+
203
+    /**
204
+     * Sets a query parameter for the query being constructed.
205
+     *
206
+     * <code>
207
+     *     $qb = $conn->getQueryBuilder()
208
+     *         ->select('u')
209
+     *         ->from('users', 'u')
210
+     *         ->where('u.id = :user_id')
211
+     *         ->setParameter(':user_id', 1);
212
+     * </code>
213
+     *
214
+     * @param string|integer $key The parameter position or name.
215
+     * @param mixed $value The parameter value.
216
+     * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants.
217
+     *
218
+     * @return $this This QueryBuilder instance.
219
+     * @since 8.2.0
220
+     */
221
+    public function setParameter($key, $value, $type = null);
222
+
223
+    /**
224
+     * Sets a collection of query parameters for the query being constructed.
225
+     *
226
+     * <code>
227
+     *     $qb = $conn->getQueryBuilder()
228
+     *         ->select('u')
229
+     *         ->from('users', 'u')
230
+     *         ->where('u.id = :user_id1 OR u.id = :user_id2')
231
+     *         ->setParameters(array(
232
+     *             ':user_id1' => 1,
233
+     *             ':user_id2' => 2
234
+     *         ));
235
+     * </code>
236
+     *
237
+     * @param array $params The query parameters to set.
238
+     * @param array $types The query parameters types to set.
239
+     *
240
+     * @return $this This QueryBuilder instance.
241
+     * @since 8.2.0
242
+     */
243
+    public function setParameters(array $params, array $types = []);
244
+
245
+    /**
246
+     * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
247
+     *
248
+     * @return array The currently defined query parameters indexed by parameter index or name.
249
+     * @since 8.2.0
250
+     */
251
+    public function getParameters();
252
+
253
+    /**
254
+     * Gets a (previously set) query parameter of the query being constructed.
255
+     *
256
+     * @param mixed $key The key (index or name) of the bound parameter.
257
+     *
258
+     * @return mixed The value of the bound parameter.
259
+     * @since 8.2.0
260
+     */
261
+    public function getParameter($key);
262
+
263
+    /**
264
+     * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
265
+     *
266
+     * @return array The currently defined query parameter types indexed by parameter index or name.
267
+     * @since 8.2.0
268
+     */
269
+    public function getParameterTypes();
270
+
271
+    /**
272
+     * Gets a (previously set) query parameter type of the query being constructed.
273
+     *
274
+     * @param mixed $key The key (index or name) of the bound parameter type.
275
+     *
276
+     * @return mixed The value of the bound parameter type.
277
+     * @since 8.2.0
278
+     */
279
+    public function getParameterType($key);
280
+
281
+    /**
282
+     * Sets the position of the first result to retrieve (the "offset").
283
+     *
284
+     * @param integer $firstResult The first result to return.
285
+     *
286
+     * @return $this This QueryBuilder instance.
287
+     * @since 8.2.0
288
+     */
289
+    public function setFirstResult($firstResult);
290
+
291
+    /**
292
+     * Gets the position of the first result the query object was set to retrieve (the "offset").
293
+     * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder.
294
+     *
295
+     * @return integer The position of the first result.
296
+     * @since 8.2.0
297
+     */
298
+    public function getFirstResult();
299
+
300
+    /**
301
+     * Sets the maximum number of results to retrieve (the "limit").
302
+     *
303
+     * @param integer $maxResults The maximum number of results to retrieve.
304
+     *
305
+     * @return $this This QueryBuilder instance.
306
+     * @since 8.2.0
307
+     */
308
+    public function setMaxResults($maxResults);
309
+
310
+    /**
311
+     * Gets the maximum number of results the query object was set to retrieve (the "limit").
312
+     * Returns NULL if {@link setMaxResults} was not applied to this query builder.
313
+     *
314
+     * @return int|null The maximum number of results.
315
+     * @since 8.2.0
316
+     */
317
+    public function getMaxResults();
318
+
319
+    /**
320
+     * Specifies an item that is to be returned in the query result.
321
+     * Replaces any previously specified selections, if any.
322
+     *
323
+     * <code>
324
+     *     $qb = $conn->getQueryBuilder()
325
+     *         ->select('u.id', 'p.id')
326
+     *         ->from('users', 'u')
327
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
328
+     * </code>
329
+     *
330
+     * @param mixed ...$selects The selection expressions.
331
+     *
332
+     * @return $this This QueryBuilder instance.
333
+     * @since 8.2.0
334
+     *
335
+     * @psalm-taint-sink sql $selects
336
+     */
337
+    public function select(...$selects);
338
+
339
+    /**
340
+     * Specifies an item that is to be returned with a different name in the query result.
341
+     *
342
+     * <code>
343
+     *     $qb = $conn->getQueryBuilder()
344
+     *         ->selectAlias('u.id', 'user_id')
345
+     *         ->from('users', 'u')
346
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
347
+     * </code>
348
+     *
349
+     * @param mixed $select The selection expressions.
350
+     * @param string $alias The column alias used in the constructed query.
351
+     *
352
+     * @return $this This QueryBuilder instance.
353
+     * @since 8.2.1
354
+     *
355
+     * @psalm-taint-sink sql $select
356
+     * @psalm-taint-sink sql $alias
357
+     */
358
+    public function selectAlias($select, $alias);
359
+
360
+    /**
361
+     * Specifies an item that is to be returned uniquely in the query result.
362
+     *
363
+     * <code>
364
+     *     $qb = $conn->getQueryBuilder()
365
+     *         ->selectDistinct('type')
366
+     *         ->from('users');
367
+     * </code>
368
+     *
369
+     * @param mixed $select The selection expressions.
370
+     *
371
+     * @return $this This QueryBuilder instance.
372
+     * @since 9.0.0
373
+     *
374
+     * @psalm-taint-sink sql $select
375
+     */
376
+    public function selectDistinct($select);
377
+
378
+    /**
379
+     * Adds an item that is to be returned in the query result.
380
+     *
381
+     * <code>
382
+     *     $qb = $conn->getQueryBuilder()
383
+     *         ->select('u.id')
384
+     *         ->addSelect('p.id')
385
+     *         ->from('users', 'u')
386
+     *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
387
+     * </code>
388
+     *
389
+     * @param mixed ...$select The selection expression.
390
+     *
391
+     * @return $this This QueryBuilder instance.
392
+     * @since 8.2.0
393
+     *
394
+     * @psalm-taint-sink sql $select
395
+     */
396
+    public function addSelect(...$select);
397
+
398
+    /**
399
+     * Turns the query being built into a bulk delete query that ranges over
400
+     * a certain table.
401
+     *
402
+     * <code>
403
+     *     $qb = $conn->getQueryBuilder()
404
+     *         ->delete('users', 'u')
405
+     *         ->where('u.id = :user_id');
406
+     *         ->setParameter(':user_id', 1);
407
+     * </code>
408
+     *
409
+     * @param string $delete The table whose rows are subject to the deletion.
410
+     * @param string $alias The table alias used in the constructed query.
411
+     *
412
+     * @return $this This QueryBuilder instance.
413
+     * @since 8.2.0
414
+     *
415
+     * @psalm-taint-sink sql $delete
416
+     */
417
+    public function delete($delete = null, $alias = null);
418
+
419
+    /**
420
+     * Turns the query being built into a bulk update query that ranges over
421
+     * a certain table
422
+     *
423
+     * <code>
424
+     *     $qb = $conn->getQueryBuilder()
425
+     *         ->update('users', 'u')
426
+     *         ->set('u.password', md5('password'))
427
+     *         ->where('u.id = ?');
428
+     * </code>
429
+     *
430
+     * @param string $update The table whose rows are subject to the update.
431
+     * @param string $alias The table alias used in the constructed query.
432
+     *
433
+     * @return $this This QueryBuilder instance.
434
+     * @since 8.2.0
435
+     *
436
+     * @psalm-taint-sink sql $update
437
+     */
438
+    public function update($update = null, $alias = null);
439
+
440
+    /**
441
+     * Turns the query being built into an insert query that inserts into
442
+     * a certain table
443
+     *
444
+     * <code>
445
+     *     $qb = $conn->getQueryBuilder()
446
+     *         ->insert('users')
447
+     *         ->values(
448
+     *             array(
449
+     *                 'name' => '?',
450
+     *                 'password' => '?'
451
+     *             )
452
+     *         );
453
+     * </code>
454
+     *
455
+     * @param string $insert The table into which the rows should be inserted.
456
+     *
457
+     * @return $this This QueryBuilder instance.
458
+     * @since 8.2.0
459
+     *
460
+     * @psalm-taint-sink sql $insert
461
+     */
462
+    public function insert($insert = null);
463
+
464
+    /**
465
+     * Creates and adds a query root corresponding to the table identified by the
466
+     * given alias, forming a cartesian product with any existing query roots.
467
+     *
468
+     * <code>
469
+     *     $qb = $conn->getQueryBuilder()
470
+     *         ->select('u.id')
471
+     *         ->from('users', 'u')
472
+     * </code>
473
+     *
474
+     * @param string $from The table.
475
+     * @param string|null $alias The alias of the table.
476
+     *
477
+     * @return $this This QueryBuilder instance.
478
+     * @since 8.2.0
479
+     *
480
+     * @psalm-taint-sink sql $from
481
+     */
482
+    public function from($from, $alias = null);
483
+
484
+    /**
485
+     * Creates and adds a join to the query.
486
+     *
487
+     * <code>
488
+     *     $qb = $conn->getQueryBuilder()
489
+     *         ->select('u.name')
490
+     *         ->from('users', 'u')
491
+     *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
492
+     * </code>
493
+     *
494
+     * @param string $fromAlias The alias that points to a from clause.
495
+     * @param string $join The table name to join.
496
+     * @param string $alias The alias of the join table.
497
+     * @param string|ICompositeExpression|null $condition The condition for the join.
498
+     *
499
+     * @return $this This QueryBuilder instance.
500
+     * @since 8.2.0
501
+     *
502
+     * @psalm-taint-sink sql $fromAlias
503
+     * @psalm-taint-sink sql $join
504
+     * @psalm-taint-sink sql $alias
505
+     * @psalm-taint-sink sql $condition
506
+     */
507
+    public function join($fromAlias, $join, $alias, $condition = null);
508
+
509
+    /**
510
+     * Creates and adds a join to the query.
511
+     *
512
+     * <code>
513
+     *     $qb = $conn->getQueryBuilder()
514
+     *         ->select('u.name')
515
+     *         ->from('users', 'u')
516
+     *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
517
+     * </code>
518
+     *
519
+     * @param string $fromAlias The alias that points to a from clause.
520
+     * @param string $join The table name to join.
521
+     * @param string $alias The alias of the join table.
522
+     * @param string|ICompositeExpression|null $condition The condition for the join.
523
+     *
524
+     * @return $this This QueryBuilder instance.
525
+     * @since 8.2.0
526
+     *
527
+     * @psalm-taint-sink sql $fromAlias
528
+     * @psalm-taint-sink sql $join
529
+     * @psalm-taint-sink sql $alias
530
+     * @psalm-taint-sink sql $condition
531
+     */
532
+    public function innerJoin($fromAlias, $join, $alias, $condition = null);
533
+
534
+    /**
535
+     * Creates and adds a left join to the query.
536
+     *
537
+     * <code>
538
+     *     $qb = $conn->getQueryBuilder()
539
+     *         ->select('u.name')
540
+     *         ->from('users', 'u')
541
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
542
+     * </code>
543
+     *
544
+     * @param string $fromAlias The alias that points to a from clause.
545
+     * @param string $join The table name to join.
546
+     * @param string $alias The alias of the join table.
547
+     * @param string|ICompositeExpression|null $condition The condition for the join.
548
+     *
549
+     * @return $this This QueryBuilder instance.
550
+     * @since 8.2.0
551
+     *
552
+     * @psalm-taint-sink sql $fromAlias
553
+     * @psalm-taint-sink sql $join
554
+     * @psalm-taint-sink sql $alias
555
+     * @psalm-taint-sink sql $condition
556
+     */
557
+    public function leftJoin($fromAlias, $join, $alias, $condition = null);
558
+
559
+    /**
560
+     * Creates and adds a right join to the query.
561
+     *
562
+     * <code>
563
+     *     $qb = $conn->getQueryBuilder()
564
+     *         ->select('u.name')
565
+     *         ->from('users', 'u')
566
+     *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
567
+     * </code>
568
+     *
569
+     * @param string $fromAlias The alias that points to a from clause.
570
+     * @param string $join The table name to join.
571
+     * @param string $alias The alias of the join table.
572
+     * @param string|ICompositeExpression|null $condition The condition for the join.
573
+     *
574
+     * @return $this This QueryBuilder instance.
575
+     * @since 8.2.0
576
+     *
577
+     * @psalm-taint-sink sql $fromAlias
578
+     * @psalm-taint-sink sql $join
579
+     * @psalm-taint-sink sql $alias
580
+     * @psalm-taint-sink sql $condition
581
+     */
582
+    public function rightJoin($fromAlias, $join, $alias, $condition = null);
583
+
584
+    /**
585
+     * Sets a new value for a column in a bulk update query.
586
+     *
587
+     * <code>
588
+     *     $qb = $conn->getQueryBuilder()
589
+     *         ->update('users', 'u')
590
+     *         ->set('u.password', md5('password'))
591
+     *         ->where('u.id = ?');
592
+     * </code>
593
+     *
594
+     * @param string $key The column to set.
595
+     * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc.
596
+     *
597
+     * @return $this This QueryBuilder instance.
598
+     * @since 8.2.0
599
+     *
600
+     * @psalm-taint-sink sql $key
601
+     * @psalm-taint-sink sql $value
602
+     */
603
+    public function set($key, $value);
604
+
605
+    /**
606
+     * Specifies one or more restrictions to the query result.
607
+     * Replaces any previously specified restrictions, if any.
608
+     *
609
+     * <code>
610
+     *     $qb = $conn->getQueryBuilder()
611
+     *         ->select('u.name')
612
+     *         ->from('users', 'u')
613
+     *         ->where('u.id = ?');
614
+     *
615
+     *     // You can optionally programatically build and/or expressions
616
+     *     $qb = $conn->getQueryBuilder();
617
+     *
618
+     *     $or = $qb->expr()->orx();
619
+     *     $or->add($qb->expr()->eq('u.id', 1));
620
+     *     $or->add($qb->expr()->eq('u.id', 2));
621
+     *
622
+     *     $qb->update('users', 'u')
623
+     *         ->set('u.password', md5('password'))
624
+     *         ->where($or);
625
+     * </code>
626
+     *
627
+     * @param mixed $predicates The restriction predicates.
628
+     *
629
+     * @return $this This QueryBuilder instance.
630
+     * @since 8.2.0
631
+     *
632
+     * @psalm-taint-sink sql $predicates
633
+     */
634
+    public function where(...$predicates);
635
+
636
+    /**
637
+     * Adds one or more restrictions to the query results, forming a logical
638
+     * conjunction with any previously specified restrictions.
639
+     *
640
+     * <code>
641
+     *     $qb = $conn->getQueryBuilder()
642
+     *         ->select('u')
643
+     *         ->from('users', 'u')
644
+     *         ->where('u.username LIKE ?')
645
+     *         ->andWhere('u.is_active = 1');
646
+     * </code>
647
+     *
648
+     * @param mixed ...$where The query restrictions.
649
+     *
650
+     * @return $this This QueryBuilder instance.
651
+     *
652
+     * @see where()
653
+     * @since 8.2.0
654
+     *
655
+     * @psalm-taint-sink sql $where
656
+     */
657
+    public function andWhere(...$where);
658
+
659
+    /**
660
+     * Adds one or more restrictions to the query results, forming a logical
661
+     * disjunction with any previously specified restrictions.
662
+     *
663
+     * <code>
664
+     *     $qb = $conn->getQueryBuilder()
665
+     *         ->select('u.name')
666
+     *         ->from('users', 'u')
667
+     *         ->where('u.id = 1')
668
+     *         ->orWhere('u.id = 2');
669
+     * </code>
670
+     *
671
+     * @param mixed ...$where The WHERE statement.
672
+     *
673
+     * @return $this This QueryBuilder instance.
674
+     *
675
+     * @see where()
676
+     * @since 8.2.0
677
+     *
678
+     * @psalm-taint-sink sql $where
679
+     */
680
+    public function orWhere(...$where);
681
+
682
+    /**
683
+     * Specifies a grouping over the results of the query.
684
+     * Replaces any previously specified groupings, if any.
685
+     *
686
+     * <code>
687
+     *     $qb = $conn->getQueryBuilder()
688
+     *         ->select('u.name')
689
+     *         ->from('users', 'u')
690
+     *         ->groupBy('u.id');
691
+     * </code>
692
+     *
693
+     * @param mixed ...$groupBys The grouping expression.
694
+     *
695
+     * @return $this This QueryBuilder instance.
696
+     * @since 8.2.0
697
+     *
698
+     * @psalm-taint-sink sql $groupBys
699
+     */
700
+    public function groupBy(...$groupBys);
701
+
702
+    /**
703
+     * Adds a grouping expression to the query.
704
+     *
705
+     * <code>
706
+     *     $qb = $conn->getQueryBuilder()
707
+     *         ->select('u.name')
708
+     *         ->from('users', 'u')
709
+     *         ->groupBy('u.lastLogin');
710
+     *         ->addGroupBy('u.createdAt')
711
+     * </code>
712
+     *
713
+     * @param mixed ...$groupBy The grouping expression.
714
+     *
715
+     * @return $this This QueryBuilder instance.
716
+     * @since 8.2.0
717
+     *
718
+     * @psalm-taint-sink sql $groupby
719
+     */
720
+    public function addGroupBy(...$groupBy);
721
+
722
+    /**
723
+     * Sets a value for a column in an insert query.
724
+     *
725
+     * <code>
726
+     *     $qb = $conn->getQueryBuilder()
727
+     *         ->insert('users')
728
+     *         ->values(
729
+     *             array(
730
+     *                 'name' => '?'
731
+     *             )
732
+     *         )
733
+     *         ->setValue('password', '?');
734
+     * </code>
735
+     *
736
+     * @param string $column The column into which the value should be inserted.
737
+     * @param IParameter|string $value The value that should be inserted into the column.
738
+     *
739
+     * @return $this This QueryBuilder instance.
740
+     * @since 8.2.0
741
+     *
742
+     * @psalm-taint-sink sql $column
743
+     * @psalm-taint-sink sql $value
744
+     */
745
+    public function setValue($column, $value);
746
+
747
+    /**
748
+     * Specifies values for an insert query indexed by column names.
749
+     * Replaces any previous values, if any.
750
+     *
751
+     * <code>
752
+     *     $qb = $conn->getQueryBuilder()
753
+     *         ->insert('users')
754
+     *         ->values(
755
+     *             array(
756
+     *                 'name' => '?',
757
+     *                 'password' => '?'
758
+     *             )
759
+     *         );
760
+     * </code>
761
+     *
762
+     * @param array $values The values to specify for the insert query indexed by column names.
763
+     *
764
+     * @return $this This QueryBuilder instance.
765
+     * @since 8.2.0
766
+     *
767
+     * @psalm-taint-sink sql $values
768
+     */
769
+    public function values(array $values);
770
+
771
+    /**
772
+     * Specifies a restriction over the groups of the query.
773
+     * Replaces any previous having restrictions, if any.
774
+     *
775
+     * @param mixed ...$having The restriction over the groups.
776
+     *
777
+     * @return $this This QueryBuilder instance.
778
+     * @since 8.2.0
779
+     *
780
+     * @psalm-taint-sink sql $having
781
+     */
782
+    public function having(...$having);
783
+
784
+    /**
785
+     * Adds a restriction over the groups of the query, forming a logical
786
+     * conjunction with any existing having restrictions.
787
+     *
788
+     * @param mixed ...$having The restriction to append.
789
+     *
790
+     * @return $this This QueryBuilder instance.
791
+     * @since 8.2.0
792
+     *
793
+     * @psalm-taint-sink sql $andHaving
794
+     */
795
+    public function andHaving(...$having);
796
+
797
+    /**
798
+     * Adds a restriction over the groups of the query, forming a logical
799
+     * disjunction with any existing having restrictions.
800
+     *
801
+     * @param mixed ...$having The restriction to add.
802
+     *
803
+     * @return $this This QueryBuilder instance.
804
+     * @since 8.2.0
805
+     *
806
+     * @psalm-taint-sink sql $having
807
+     */
808
+    public function orHaving(...$having);
809
+
810
+    /**
811
+     * Specifies an ordering for the query results.
812
+     * Replaces any previously specified orderings, if any.
813
+     *
814
+     * @param string $sort The ordering expression.
815
+     * @param string $order The ordering direction.
816
+     *
817
+     * @return $this This QueryBuilder instance.
818
+     * @since 8.2.0
819
+     *
820
+     * @psalm-taint-sink sql $sort
821
+     * @psalm-taint-sink sql $order
822
+     */
823
+    public function orderBy($sort, $order = null);
824
+
825
+    /**
826
+     * Adds an ordering to the query results.
827
+     *
828
+     * @param string $sort The ordering expression.
829
+     * @param string $order The ordering direction.
830
+     *
831
+     * @return $this This QueryBuilder instance.
832
+     * @since 8.2.0
833
+     *
834
+     * @psalm-taint-sink sql $sort
835
+     * @psalm-taint-sink sql $order
836
+     */
837
+    public function addOrderBy($sort, $order = null);
838
+
839
+    /**
840
+     * Gets a query part by its name.
841
+     *
842
+     * @param string $queryPartName
843
+     *
844
+     * @return mixed
845
+     * @since 8.2.0
846
+     */
847
+    public function getQueryPart($queryPartName);
848
+
849
+    /**
850
+     * Gets all query parts.
851
+     *
852
+     * @return array
853
+     * @since 8.2.0
854
+     */
855
+    public function getQueryParts();
856
+
857
+    /**
858
+     * Resets SQL parts.
859
+     *
860
+     * @param array|null $queryPartNames
861
+     *
862
+     * @return $this This QueryBuilder instance.
863
+     * @since 8.2.0
864
+     */
865
+    public function resetQueryParts($queryPartNames = null);
866
+
867
+    /**
868
+     * Resets a single SQL part.
869
+     *
870
+     * @param string $queryPartName
871
+     *
872
+     * @return $this This QueryBuilder instance.
873
+     * @since 8.2.0
874
+     */
875
+    public function resetQueryPart($queryPartName);
876
+
877
+    /**
878
+     * Creates a new named parameter and bind the value $value to it.
879
+     *
880
+     * This method provides a shortcut for PDOStatement::bindValue
881
+     * when using prepared statements.
882
+     *
883
+     * The parameter $value specifies the value that you want to bind. If
884
+     * $placeholder is not provided bindValue() will automatically create a
885
+     * placeholder for you. An automatic placeholder will be of the name
886
+     * ':dcValue1', ':dcValue2' etc.
887
+     *
888
+     * For more information see {@link https://www.php.net/pdostatement-bindparam}
889
+     *
890
+     * Example:
891
+     * <code>
892
+     * $value = 2;
893
+     * $q->eq( 'id', $q->bindValue( $value ) );
894
+     * $stmt = $q->executeQuery(); // executed with 'id = 2'
895
+     * </code>
896
+     *
897
+     * @license New BSD License
898
+     * @link http://www.zetacomponents.org
899
+     *
900
+     * @param mixed $value
901
+     * @param mixed $type
902
+     * @param string $placeHolder The name to bind with. The string must start with a colon ':'.
903
+     *
904
+     * @return IParameter
905
+     * @since 8.2.0
906
+     *
907
+     * @psalm-taint-escape sql
908
+     */
909
+    public function createNamedParameter($value, $type = self::PARAM_STR, $placeHolder = null);
910
+
911
+    /**
912
+     * Creates a new positional parameter and bind the given value to it.
913
+     *
914
+     * Attention: If you are using positional parameters with the query builder you have
915
+     * to be very careful to bind all parameters in the order they appear in the SQL
916
+     * statement , otherwise they get bound in the wrong order which can lead to serious
917
+     * bugs in your code.
918
+     *
919
+     * Example:
920
+     * <code>
921
+     *  $qb = $conn->getQueryBuilder();
922
+     *  $qb->select('u.*')
923
+     *     ->from('users', 'u')
924
+     *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR))
925
+     *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR))
926
+     * </code>
927
+     *
928
+     * @param mixed $value
929
+     * @param integer $type
930
+     *
931
+     * @return IParameter
932
+     * @since 8.2.0
933
+     *
934
+     * @psalm-taint-escape sql
935
+     */
936
+    public function createPositionalParameter($value, $type = self::PARAM_STR);
937
+
938
+    /**
939
+     * Creates a new parameter
940
+     *
941
+     * Example:
942
+     * <code>
943
+     *  $qb = $conn->getQueryBuilder();
944
+     *  $qb->select('u.*')
945
+     *     ->from('users', 'u')
946
+     *     ->where('u.username = ' . $qb->createParameter('name'))
947
+     *     ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR))
948
+     * </code>
949
+     *
950
+     * @param string $name
951
+     *
952
+     * @return IParameter
953
+     * @since 8.2.0
954
+     *
955
+     * @psalm-taint-escape sql
956
+     */
957
+    public function createParameter($name);
958
+
959
+    /**
960
+     * Creates a new function
961
+     *
962
+     * Attention: Column names inside the call have to be quoted before hand
963
+     *
964
+     * Example:
965
+     * <code>
966
+     *  $qb = $conn->getQueryBuilder();
967
+     *  $qb->select($qb->createFunction('COUNT(*)'))
968
+     *     ->from('users', 'u')
969
+     *  echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u
970
+     * </code>
971
+     * <code>
972
+     *  $qb = $conn->getQueryBuilder();
973
+     *  $qb->select($qb->createFunction('COUNT(`column`)'))
974
+     *     ->from('users', 'u')
975
+     *  echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u
976
+     * </code>
977
+     *
978
+     * @param string $call
979
+     *
980
+     * @return IQueryFunction
981
+     * @since 8.2.0
982
+     *
983
+     * @psalm-taint-sink sql
984
+     */
985
+    public function createFunction($call);
986
+
987
+    /**
988
+     * Used to get the id of the last inserted element
989
+     * @return int
990
+     * @throws \BadMethodCallException When being called before an insert query has been run.
991
+     * @since 9.0.0
992
+     */
993
+    public function getLastInsertId(): int;
994
+
995
+    /**
996
+     * Returns the table name quoted and with database prefix as needed by the implementation
997
+     *
998
+     * @param string $table
999
+     * @return string
1000
+     * @since 9.0.0
1001
+     */
1002
+    public function getTableName($table);
1003
+
1004
+    /**
1005
+     * Returns the column name quoted and with table alias prefix as needed by the implementation
1006
+     *
1007
+     * @param string $column
1008
+     * @param string $tableAlias
1009
+     * @return string
1010
+     * @since 9.0.0
1011
+     */
1012
+    public function getColumnName($column, $tableAlias = '');
1013 1013
 }
Please login to merge, or discard this patch.
lib/private/DB/QueryBuilder/QueryBuilder.php 1 patch
Indentation   +1296 added lines, -1296 removed lines patch added patch discarded remove patch
@@ -57,1300 +57,1300 @@
 block discarded – undo
57 57
 
58 58
 class QueryBuilder implements IQueryBuilder {
59 59
 
60
-	/** @var ConnectionAdapter */
61
-	private $connection;
62
-
63
-	/** @var SystemConfig */
64
-	private $systemConfig;
65
-
66
-	/** @var ILogger */
67
-	private $logger;
68
-
69
-	/** @var \Doctrine\DBAL\Query\QueryBuilder */
70
-	private $queryBuilder;
71
-
72
-	/** @var QuoteHelper */
73
-	private $helper;
74
-
75
-	/** @var bool */
76
-	private $automaticTablePrefix = true;
77
-
78
-	/** @var string */
79
-	protected $lastInsertedTable;
80
-
81
-	/**
82
-	 * Initializes a new QueryBuilder.
83
-	 *
84
-	 * @param ConnectionAdapter $connection
85
-	 * @param SystemConfig $systemConfig
86
-	 * @param ILogger $logger
87
-	 */
88
-	public function __construct(ConnectionAdapter $connection, SystemConfig $systemConfig, ILogger $logger) {
89
-		$this->connection = $connection;
90
-		$this->systemConfig = $systemConfig;
91
-		$this->logger = $logger;
92
-		$this->queryBuilder = new \Doctrine\DBAL\Query\QueryBuilder($this->connection->getInner());
93
-		$this->helper = new QuoteHelper();
94
-	}
95
-
96
-	/**
97
-	 * Enable/disable automatic prefixing of table names with the oc_ prefix
98
-	 *
99
-	 * @param bool $enabled If set to true table names will be prefixed with the
100
-	 * owncloud database prefix automatically.
101
-	 * @since 8.2.0
102
-	 */
103
-	public function automaticTablePrefix($enabled) {
104
-		$this->automaticTablePrefix = (bool) $enabled;
105
-	}
106
-
107
-	/**
108
-	 * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
109
-	 * This producer method is intended for convenient inline usage. Example:
110
-	 *
111
-	 * <code>
112
-	 *     $qb = $conn->getQueryBuilder()
113
-	 *         ->select('u')
114
-	 *         ->from('users', 'u')
115
-	 *         ->where($qb->expr()->eq('u.id', 1));
116
-	 * </code>
117
-	 *
118
-	 * For more complex expression construction, consider storing the expression
119
-	 * builder object in a local variable.
120
-	 *
121
-	 * @return \OCP\DB\QueryBuilder\IExpressionBuilder
122
-	 */
123
-	public function expr() {
124
-		if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
125
-			return new OCIExpressionBuilder($this->connection, $this);
126
-		}
127
-		if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
128
-			return new PgSqlExpressionBuilder($this->connection, $this);
129
-		}
130
-		if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) {
131
-			return new MySqlExpressionBuilder($this->connection, $this);
132
-		}
133
-		if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
134
-			return new SqliteExpressionBuilder($this->connection, $this);
135
-		}
136
-
137
-		return new ExpressionBuilder($this->connection, $this);
138
-	}
139
-
140
-	/**
141
-	 * Gets an FunctionBuilder used for object-oriented construction of query functions.
142
-	 * This producer method is intended for convenient inline usage. Example:
143
-	 *
144
-	 * <code>
145
-	 *     $qb = $conn->getQueryBuilder()
146
-	 *         ->select('u')
147
-	 *         ->from('users', 'u')
148
-	 *         ->where($qb->fun()->md5('u.id'));
149
-	 * </code>
150
-	 *
151
-	 * For more complex function construction, consider storing the function
152
-	 * builder object in a local variable.
153
-	 *
154
-	 * @return \OCP\DB\QueryBuilder\IFunctionBuilder
155
-	 */
156
-	public function func() {
157
-		if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
158
-			return new OCIFunctionBuilder($this->helper);
159
-		}
160
-		if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
161
-			return new SqliteFunctionBuilder($this->helper);
162
-		}
163
-		if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
164
-			return new PgSqlFunctionBuilder($this->helper);
165
-		}
166
-
167
-		return new FunctionBuilder($this->helper);
168
-	}
169
-
170
-	/**
171
-	 * Gets the type of the currently built query.
172
-	 *
173
-	 * @return integer
174
-	 */
175
-	public function getType() {
176
-		return $this->queryBuilder->getType();
177
-	}
178
-
179
-	/**
180
-	 * Gets the associated DBAL Connection for this query builder.
181
-	 *
182
-	 * @return \OCP\IDBConnection
183
-	 */
184
-	public function getConnection() {
185
-		return $this->connection;
186
-	}
187
-
188
-	/**
189
-	 * Gets the state of this query builder instance.
190
-	 *
191
-	 * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
192
-	 */
193
-	public function getState() {
194
-		return $this->queryBuilder->getState();
195
-	}
196
-
197
-	/**
198
-	 * Executes this query using the bound parameters and their types.
199
-	 *
200
-	 * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate}
201
-	 * for insert, update and delete statements.
202
-	 *
203
-	 * @return IResult|int
204
-	 */
205
-	public function execute() {
206
-		if ($this->systemConfig->getValue('log_query', false)) {
207
-			try {
208
-				$params = [];
209
-				foreach ($this->getParameters() as $placeholder => $value) {
210
-					if ($value instanceof \DateTime) {
211
-						$params[] = $placeholder . ' => DateTime:\'' . $value->format('c') . '\'';
212
-					} elseif (is_array($value)) {
213
-						$params[] = $placeholder . ' => (\'' . implode('\', \'', $value) . '\')';
214
-					} else {
215
-						$params[] = $placeholder . ' => \'' . $value . '\'';
216
-					}
217
-				}
218
-				if (empty($params)) {
219
-					$this->logger->debug('DB QueryBuilder: \'{query}\'', [
220
-						'query' => $this->getSQL(),
221
-						'app' => 'core',
222
-					]);
223
-				} else {
224
-					$this->logger->debug('DB QueryBuilder: \'{query}\' with parameters: {params}', [
225
-						'query' => $this->getSQL(),
226
-						'params' => implode(', ', $params),
227
-						'app' => 'core',
228
-					]);
229
-				}
230
-			} catch (\Error $e) {
231
-				// likely an error during conversion of $value to string
232
-				$this->logger->debug('DB QueryBuilder: error trying to log SQL query');
233
-				$this->logger->logException($e);
234
-			}
235
-		}
236
-
237
-		if (!empty($this->getQueryPart('select'))) {
238
-			$select = $this->getQueryPart('select');
239
-			$hasSelectAll = array_filter($select, static function ($s) {
240
-				return $s === '*';
241
-			});
242
-			$hasSelectSpecific = array_filter($select, static function ($s) {
243
-				return $s !== '*';
244
-			});
245
-
246
-			if (empty($hasSelectAll) === empty($hasSelectSpecific)) {
247
-				$exception = new QueryException('Query is selecting * and specific values in the same query. This is not supported in Oracle.');
248
-				$this->logger->logException($exception, [
249
-					'message' => 'Query is selecting * and specific values in the same query. This is not supported in Oracle.',
250
-					'query' => $this->getSQL(),
251
-					'level' => ILogger::ERROR,
252
-					'app' => 'core',
253
-				]);
254
-			}
255
-		}
256
-
257
-		$numberOfParameters = 0;
258
-		$hasTooLargeArrayParameter = false;
259
-		foreach ($this->getParameters() as $parameter) {
260
-			if (is_array($parameter)) {
261
-				$count = count($parameter);
262
-				$numberOfParameters += $count;
263
-				$hasTooLargeArrayParameter = $hasTooLargeArrayParameter || ($count > 1000);
264
-			}
265
-		}
266
-
267
-		if ($hasTooLargeArrayParameter) {
268
-			$exception = new QueryException('More than 1000 expressions in a list are not allowed on Oracle.');
269
-			$this->logger->logException($exception, [
270
-				'message' => 'More than 1000 expressions in a list are not allowed on Oracle.',
271
-				'query' => $this->getSQL(),
272
-				'level' => ILogger::ERROR,
273
-				'app' => 'core',
274
-			]);
275
-		}
276
-
277
-		if ($numberOfParameters > 65535) {
278
-			$exception = new QueryException('The number of parameters must not exceed 65535. Restriction by PostgreSQL.');
279
-			$this->logger->logException($exception, [
280
-				'message' => 'The number of parameters must not exceed 65535. Restriction by PostgreSQL.',
281
-				'query' => $this->getSQL(),
282
-				'level' => ILogger::ERROR,
283
-				'app' => 'core',
284
-			]);
285
-		}
286
-
287
-		$result = $this->queryBuilder->execute();
288
-		if (is_int($result)) {
289
-			return $result;
290
-		}
291
-		return new ResultAdapter($result);
292
-	}
293
-
294
-	public function executeQuery(): IResult {
295
-		if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
296
-			throw new \RuntimeException('Invalid query type, expected SELECT query');
297
-		}
298
-
299
-		try {
300
-			$result = $this->execute();
301
-		} catch (\Doctrine\DBAL\Exception $e) {
302
-			throw \OC\DB\Exceptions\DbalException::wrap($e);
303
-		}
304
-
305
-		if ($result instanceof IResult) {
306
-			return $result;
307
-		}
308
-
309
-		throw new \RuntimeException('Invalid return type for query');
310
-	}
311
-
312
-	/**
313
-	 * Monkey-patched compatibility layer for apps that were adapted for Nextcloud 22 before
314
-	 * the first beta, where executeStatement was named executeUpdate.
315
-	 *
316
-	 * Static analysis should catch those misuses, but until then let's try to keep things
317
-	 * running.
318
-	 *
319
-	 * @internal
320
-	 * @deprecated
321
-	 * @todo drop ASAP
322
-	 */
323
-	public function executeUpdate(): int {
324
-		return $this->executeStatement();
325
-	}
326
-
327
-	public function executeStatement(): int {
328
-		if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
329
-			throw new \RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement');
330
-		}
331
-
332
-		try {
333
-			$result = $this->execute();
334
-		} catch (\Doctrine\DBAL\Exception $e) {
335
-			throw \OC\DB\Exceptions\DbalException::wrap($e);
336
-		}
337
-
338
-		if (!is_int($result)) {
339
-			throw new \RuntimeException('Invalid return type for statement');
340
-		}
341
-
342
-		return $result;
343
-	}
344
-
345
-
346
-	/**
347
-	 * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
348
-	 *
349
-	 * <code>
350
-	 *     $qb = $conn->getQueryBuilder()
351
-	 *         ->select('u')
352
-	 *         ->from('User', 'u')
353
-	 *     echo $qb->getSQL(); // SELECT u FROM User u
354
-	 * </code>
355
-	 *
356
-	 * @return string The SQL query string.
357
-	 */
358
-	public function getSQL() {
359
-		return $this->queryBuilder->getSQL();
360
-	}
361
-
362
-	/**
363
-	 * Sets a query parameter for the query being constructed.
364
-	 *
365
-	 * <code>
366
-	 *     $qb = $conn->getQueryBuilder()
367
-	 *         ->select('u')
368
-	 *         ->from('users', 'u')
369
-	 *         ->where('u.id = :user_id')
370
-	 *         ->setParameter(':user_id', 1);
371
-	 * </code>
372
-	 *
373
-	 * @param string|integer $key The parameter position or name.
374
-	 * @param mixed $value The parameter value.
375
-	 * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants.
376
-	 *
377
-	 * @return $this This QueryBuilder instance.
378
-	 */
379
-	public function setParameter($key, $value, $type = null) {
380
-		$this->queryBuilder->setParameter($key, $value, $type);
381
-
382
-		return $this;
383
-	}
384
-
385
-	/**
386
-	 * Sets a collection of query parameters for the query being constructed.
387
-	 *
388
-	 * <code>
389
-	 *     $qb = $conn->getQueryBuilder()
390
-	 *         ->select('u')
391
-	 *         ->from('users', 'u')
392
-	 *         ->where('u.id = :user_id1 OR u.id = :user_id2')
393
-	 *         ->setParameters(array(
394
-	 *             ':user_id1' => 1,
395
-	 *             ':user_id2' => 2
396
-	 *         ));
397
-	 * </code>
398
-	 *
399
-	 * @param array $params The query parameters to set.
400
-	 * @param array $types The query parameters types to set.
401
-	 *
402
-	 * @return $this This QueryBuilder instance.
403
-	 */
404
-	public function setParameters(array $params, array $types = []) {
405
-		$this->queryBuilder->setParameters($params, $types);
406
-
407
-		return $this;
408
-	}
409
-
410
-	/**
411
-	 * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
412
-	 *
413
-	 * @return array The currently defined query parameters indexed by parameter index or name.
414
-	 */
415
-	public function getParameters() {
416
-		return $this->queryBuilder->getParameters();
417
-	}
418
-
419
-	/**
420
-	 * Gets a (previously set) query parameter of the query being constructed.
421
-	 *
422
-	 * @param mixed $key The key (index or name) of the bound parameter.
423
-	 *
424
-	 * @return mixed The value of the bound parameter.
425
-	 */
426
-	public function getParameter($key) {
427
-		return $this->queryBuilder->getParameter($key);
428
-	}
429
-
430
-	/**
431
-	 * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
432
-	 *
433
-	 * @return array The currently defined query parameter types indexed by parameter index or name.
434
-	 */
435
-	public function getParameterTypes() {
436
-		return $this->queryBuilder->getParameterTypes();
437
-	}
438
-
439
-	/**
440
-	 * Gets a (previously set) query parameter type of the query being constructed.
441
-	 *
442
-	 * @param mixed $key The key (index or name) of the bound parameter type.
443
-	 *
444
-	 * @return mixed The value of the bound parameter type.
445
-	 */
446
-	public function getParameterType($key) {
447
-		return $this->queryBuilder->getParameterType($key);
448
-	}
449
-
450
-	/**
451
-	 * Sets the position of the first result to retrieve (the "offset").
452
-	 *
453
-	 * @param integer $firstResult The first result to return.
454
-	 *
455
-	 * @return $this This QueryBuilder instance.
456
-	 */
457
-	public function setFirstResult($firstResult) {
458
-		$this->queryBuilder->setFirstResult($firstResult);
459
-
460
-		return $this;
461
-	}
462
-
463
-	/**
464
-	 * Gets the position of the first result the query object was set to retrieve (the "offset").
465
-	 * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder.
466
-	 *
467
-	 * @return integer The position of the first result.
468
-	 */
469
-	public function getFirstResult() {
470
-		return $this->queryBuilder->getFirstResult();
471
-	}
472
-
473
-	/**
474
-	 * Sets the maximum number of results to retrieve (the "limit").
475
-	 *
476
-	 * NOTE: Setting max results to "0" will cause mixed behaviour. While most
477
-	 * of the databases will just return an empty result set, Oracle will return
478
-	 * all entries.
479
-	 *
480
-	 * @param integer $maxResults The maximum number of results to retrieve.
481
-	 *
482
-	 * @return $this This QueryBuilder instance.
483
-	 */
484
-	public function setMaxResults($maxResults) {
485
-		$this->queryBuilder->setMaxResults($maxResults);
486
-
487
-		return $this;
488
-	}
489
-
490
-	/**
491
-	 * Gets the maximum number of results the query object was set to retrieve (the "limit").
492
-	 * Returns NULL if {@link setMaxResults} was not applied to this query builder.
493
-	 *
494
-	 * @return int|null The maximum number of results.
495
-	 */
496
-	public function getMaxResults() {
497
-		return $this->queryBuilder->getMaxResults();
498
-	}
499
-
500
-	/**
501
-	 * Specifies an item that is to be returned in the query result.
502
-	 * Replaces any previously specified selections, if any.
503
-	 *
504
-	 * <code>
505
-	 *     $qb = $conn->getQueryBuilder()
506
-	 *         ->select('u.id', 'p.id')
507
-	 *         ->from('users', 'u')
508
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
509
-	 * </code>
510
-	 *
511
-	 * @param mixed ...$selects The selection expressions.
512
-	 *
513
-	 * '@return $this This QueryBuilder instance.
514
-	 */
515
-	public function select(...$selects) {
516
-		if (count($selects) === 1 && is_array($selects[0])) {
517
-			$selects = $selects[0];
518
-		}
519
-
520
-		$this->queryBuilder->select(
521
-			$this->helper->quoteColumnNames($selects)
522
-		);
523
-
524
-		return $this;
525
-	}
526
-
527
-	/**
528
-	 * Specifies an item that is to be returned with a different name in the query result.
529
-	 *
530
-	 * <code>
531
-	 *     $qb = $conn->getQueryBuilder()
532
-	 *         ->selectAlias('u.id', 'user_id')
533
-	 *         ->from('users', 'u')
534
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
535
-	 * </code>
536
-	 *
537
-	 * @param mixed $select The selection expressions.
538
-	 * @param string $alias The column alias used in the constructed query.
539
-	 *
540
-	 * @return $this This QueryBuilder instance.
541
-	 */
542
-	public function selectAlias($select, $alias) {
543
-		$this->queryBuilder->addSelect(
544
-			$this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias)
545
-		);
546
-
547
-		return $this;
548
-	}
549
-
550
-	/**
551
-	 * Specifies an item that is to be returned uniquely in the query result.
552
-	 *
553
-	 * <code>
554
-	 *     $qb = $conn->getQueryBuilder()
555
-	 *         ->selectDistinct('type')
556
-	 *         ->from('users');
557
-	 * </code>
558
-	 *
559
-	 * @param mixed $select The selection expressions.
560
-	 *
561
-	 * @return $this This QueryBuilder instance.
562
-	 */
563
-	public function selectDistinct($select) {
564
-		if (!is_array($select)) {
565
-			$select = [$select];
566
-		}
567
-
568
-		$quotedSelect = $this->helper->quoteColumnNames($select);
569
-
570
-		$this->queryBuilder->addSelect(
571
-			'DISTINCT ' . implode(', ', $quotedSelect)
572
-		);
573
-
574
-		return $this;
575
-	}
576
-
577
-	/**
578
-	 * Adds an item that is to be returned in the query result.
579
-	 *
580
-	 * <code>
581
-	 *     $qb = $conn->getQueryBuilder()
582
-	 *         ->select('u.id')
583
-	 *         ->addSelect('p.id')
584
-	 *         ->from('users', 'u')
585
-	 *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
586
-	 * </code>
587
-	 *
588
-	 * @param mixed ...$selects The selection expression.
589
-	 *
590
-	 * @return $this This QueryBuilder instance.
591
-	 */
592
-	public function addSelect(...$selects) {
593
-		if (count($selects) === 1 && is_array($selects[0])) {
594
-			$selects = $selects[0];
595
-		}
596
-
597
-		$this->queryBuilder->addSelect(
598
-			$this->helper->quoteColumnNames($selects)
599
-		);
600
-
601
-		return $this;
602
-	}
603
-
604
-	/**
605
-	 * Turns the query being built into a bulk delete query that ranges over
606
-	 * a certain table.
607
-	 *
608
-	 * <code>
609
-	 *     $qb = $conn->getQueryBuilder()
610
-	 *         ->delete('users', 'u')
611
-	 *         ->where('u.id = :user_id');
612
-	 *         ->setParameter(':user_id', 1);
613
-	 * </code>
614
-	 *
615
-	 * @param string $delete The table whose rows are subject to the deletion.
616
-	 * @param string $alias The table alias used in the constructed query.
617
-	 *
618
-	 * @return $this This QueryBuilder instance.
619
-	 */
620
-	public function delete($delete = null, $alias = null) {
621
-		$this->queryBuilder->delete(
622
-			$this->getTableName($delete),
623
-			$alias
624
-		);
625
-
626
-		return $this;
627
-	}
628
-
629
-	/**
630
-	 * Turns the query being built into a bulk update query that ranges over
631
-	 * a certain table
632
-	 *
633
-	 * <code>
634
-	 *     $qb = $conn->getQueryBuilder()
635
-	 *         ->update('users', 'u')
636
-	 *         ->set('u.password', md5('password'))
637
-	 *         ->where('u.id = ?');
638
-	 * </code>
639
-	 *
640
-	 * @param string $update The table whose rows are subject to the update.
641
-	 * @param string $alias The table alias used in the constructed query.
642
-	 *
643
-	 * @return $this This QueryBuilder instance.
644
-	 */
645
-	public function update($update = null, $alias = null) {
646
-		$this->queryBuilder->update(
647
-			$this->getTableName($update),
648
-			$alias
649
-		);
650
-
651
-		return $this;
652
-	}
653
-
654
-	/**
655
-	 * Turns the query being built into an insert query that inserts into
656
-	 * a certain table
657
-	 *
658
-	 * <code>
659
-	 *     $qb = $conn->getQueryBuilder()
660
-	 *         ->insert('users')
661
-	 *         ->values(
662
-	 *             array(
663
-	 *                 'name' => '?',
664
-	 *                 'password' => '?'
665
-	 *             )
666
-	 *         );
667
-	 * </code>
668
-	 *
669
-	 * @param string $insert The table into which the rows should be inserted.
670
-	 *
671
-	 * @return $this This QueryBuilder instance.
672
-	 */
673
-	public function insert($insert = null) {
674
-		$this->queryBuilder->insert(
675
-			$this->getTableName($insert)
676
-		);
677
-
678
-		$this->lastInsertedTable = $insert;
679
-
680
-		return $this;
681
-	}
682
-
683
-	/**
684
-	 * Creates and adds a query root corresponding to the table identified by the
685
-	 * given alias, forming a cartesian product with any existing query roots.
686
-	 *
687
-	 * <code>
688
-	 *     $qb = $conn->getQueryBuilder()
689
-	 *         ->select('u.id')
690
-	 *         ->from('users', 'u')
691
-	 * </code>
692
-	 *
693
-	 * @param string $from The table.
694
-	 * @param string|null $alias The alias of the table.
695
-	 *
696
-	 * @return $this This QueryBuilder instance.
697
-	 */
698
-	public function from($from, $alias = null) {
699
-		$this->queryBuilder->from(
700
-			$this->getTableName($from),
701
-			$this->quoteAlias($alias)
702
-		);
703
-
704
-		return $this;
705
-	}
706
-
707
-	/**
708
-	 * Creates and adds a join to the query.
709
-	 *
710
-	 * <code>
711
-	 *     $qb = $conn->getQueryBuilder()
712
-	 *         ->select('u.name')
713
-	 *         ->from('users', 'u')
714
-	 *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
715
-	 * </code>
716
-	 *
717
-	 * @param string $fromAlias The alias that points to a from clause.
718
-	 * @param string $join The table name to join.
719
-	 * @param string $alias The alias of the join table.
720
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
721
-	 *
722
-	 * @return $this This QueryBuilder instance.
723
-	 */
724
-	public function join($fromAlias, $join, $alias, $condition = null) {
725
-		$this->queryBuilder->join(
726
-			$this->quoteAlias($fromAlias),
727
-			$this->getTableName($join),
728
-			$this->quoteAlias($alias),
729
-			$condition
730
-		);
731
-
732
-		return $this;
733
-	}
734
-
735
-	/**
736
-	 * Creates and adds a join to the query.
737
-	 *
738
-	 * <code>
739
-	 *     $qb = $conn->getQueryBuilder()
740
-	 *         ->select('u.name')
741
-	 *         ->from('users', 'u')
742
-	 *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
743
-	 * </code>
744
-	 *
745
-	 * @param string $fromAlias The alias that points to a from clause.
746
-	 * @param string $join The table name to join.
747
-	 * @param string $alias The alias of the join table.
748
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
749
-	 *
750
-	 * @return $this This QueryBuilder instance.
751
-	 */
752
-	public function innerJoin($fromAlias, $join, $alias, $condition = null) {
753
-		$this->queryBuilder->innerJoin(
754
-			$this->quoteAlias($fromAlias),
755
-			$this->getTableName($join),
756
-			$this->quoteAlias($alias),
757
-			$condition
758
-		);
759
-
760
-		return $this;
761
-	}
762
-
763
-	/**
764
-	 * Creates and adds a left join to the query.
765
-	 *
766
-	 * <code>
767
-	 *     $qb = $conn->getQueryBuilder()
768
-	 *         ->select('u.name')
769
-	 *         ->from('users', 'u')
770
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
771
-	 * </code>
772
-	 *
773
-	 * @param string $fromAlias The alias that points to a from clause.
774
-	 * @param string $join The table name to join.
775
-	 * @param string $alias The alias of the join table.
776
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
777
-	 *
778
-	 * @return $this This QueryBuilder instance.
779
-	 */
780
-	public function leftJoin($fromAlias, $join, $alias, $condition = null) {
781
-		$this->queryBuilder->leftJoin(
782
-			$this->quoteAlias($fromAlias),
783
-			$this->getTableName($join),
784
-			$this->quoteAlias($alias),
785
-			$condition
786
-		);
787
-
788
-		return $this;
789
-	}
790
-
791
-	/**
792
-	 * Creates and adds a right join to the query.
793
-	 *
794
-	 * <code>
795
-	 *     $qb = $conn->getQueryBuilder()
796
-	 *         ->select('u.name')
797
-	 *         ->from('users', 'u')
798
-	 *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
799
-	 * </code>
800
-	 *
801
-	 * @param string $fromAlias The alias that points to a from clause.
802
-	 * @param string $join The table name to join.
803
-	 * @param string $alias The alias of the join table.
804
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
805
-	 *
806
-	 * @return $this This QueryBuilder instance.
807
-	 */
808
-	public function rightJoin($fromAlias, $join, $alias, $condition = null) {
809
-		$this->queryBuilder->rightJoin(
810
-			$this->quoteAlias($fromAlias),
811
-			$this->getTableName($join),
812
-			$this->quoteAlias($alias),
813
-			$condition
814
-		);
815
-
816
-		return $this;
817
-	}
818
-
819
-	/**
820
-	 * Sets a new value for a column in a bulk update query.
821
-	 *
822
-	 * <code>
823
-	 *     $qb = $conn->getQueryBuilder()
824
-	 *         ->update('users', 'u')
825
-	 *         ->set('u.password', md5('password'))
826
-	 *         ->where('u.id = ?');
827
-	 * </code>
828
-	 *
829
-	 * @param string $key The column to set.
830
-	 * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc.
831
-	 *
832
-	 * @return $this This QueryBuilder instance.
833
-	 */
834
-	public function set($key, $value) {
835
-		$this->queryBuilder->set(
836
-			$this->helper->quoteColumnName($key),
837
-			$this->helper->quoteColumnName($value)
838
-		);
839
-
840
-		return $this;
841
-	}
842
-
843
-	/**
844
-	 * Specifies one or more restrictions to the query result.
845
-	 * Replaces any previously specified restrictions, if any.
846
-	 *
847
-	 * <code>
848
-	 *     $qb = $conn->getQueryBuilder()
849
-	 *         ->select('u.name')
850
-	 *         ->from('users', 'u')
851
-	 *         ->where('u.id = ?');
852
-	 *
853
-	 *     // You can optionally programatically build and/or expressions
854
-	 *     $qb = $conn->getQueryBuilder();
855
-	 *
856
-	 *     $or = $qb->expr()->orx();
857
-	 *     $or->add($qb->expr()->eq('u.id', 1));
858
-	 *     $or->add($qb->expr()->eq('u.id', 2));
859
-	 *
860
-	 *     $qb->update('users', 'u')
861
-	 *         ->set('u.password', md5('password'))
862
-	 *         ->where($or);
863
-	 * </code>
864
-	 *
865
-	 * @param mixed ...$predicates The restriction predicates.
866
-	 *
867
-	 * @return $this This QueryBuilder instance.
868
-	 */
869
-	public function where(...$predicates) {
870
-		call_user_func_array(
871
-			[$this->queryBuilder, 'where'],
872
-			$predicates
873
-		);
874
-
875
-		return $this;
876
-	}
877
-
878
-	/**
879
-	 * Adds one or more restrictions to the query results, forming a logical
880
-	 * conjunction with any previously specified restrictions.
881
-	 *
882
-	 * <code>
883
-	 *     $qb = $conn->getQueryBuilder()
884
-	 *         ->select('u')
885
-	 *         ->from('users', 'u')
886
-	 *         ->where('u.username LIKE ?')
887
-	 *         ->andWhere('u.is_active = 1');
888
-	 * </code>
889
-	 *
890
-	 * @param mixed ...$where The query restrictions.
891
-	 *
892
-	 * @return $this This QueryBuilder instance.
893
-	 *
894
-	 * @see where()
895
-	 */
896
-	public function andWhere(...$where) {
897
-		call_user_func_array(
898
-			[$this->queryBuilder, 'andWhere'],
899
-			$where
900
-		);
901
-
902
-		return $this;
903
-	}
904
-
905
-	/**
906
-	 * Adds one or more restrictions to the query results, forming a logical
907
-	 * disjunction with any previously specified restrictions.
908
-	 *
909
-	 * <code>
910
-	 *     $qb = $conn->getQueryBuilder()
911
-	 *         ->select('u.name')
912
-	 *         ->from('users', 'u')
913
-	 *         ->where('u.id = 1')
914
-	 *         ->orWhere('u.id = 2');
915
-	 * </code>
916
-	 *
917
-	 * @param mixed ...$where The WHERE statement.
918
-	 *
919
-	 * @return $this This QueryBuilder instance.
920
-	 *
921
-	 * @see where()
922
-	 */
923
-	public function orWhere(...$where) {
924
-		call_user_func_array(
925
-			[$this->queryBuilder, 'orWhere'],
926
-			$where
927
-		);
928
-
929
-		return $this;
930
-	}
931
-
932
-	/**
933
-	 * Specifies a grouping over the results of the query.
934
-	 * Replaces any previously specified groupings, if any.
935
-	 *
936
-	 * <code>
937
-	 *     $qb = $conn->getQueryBuilder()
938
-	 *         ->select('u.name')
939
-	 *         ->from('users', 'u')
940
-	 *         ->groupBy('u.id');
941
-	 * </code>
942
-	 *
943
-	 * @param mixed ...$groupBys The grouping expression.
944
-	 *
945
-	 * @return $this This QueryBuilder instance.
946
-	 */
947
-	public function groupBy(...$groupBys) {
948
-		if (count($groupBys) === 1 && is_array($groupBys[0])) {
949
-			$groupBys = $groupBys[0];
950
-		}
951
-
952
-		call_user_func_array(
953
-			[$this->queryBuilder, 'groupBy'],
954
-			$this->helper->quoteColumnNames($groupBys)
955
-		);
956
-
957
-		return $this;
958
-	}
959
-
960
-	/**
961
-	 * Adds a grouping expression to the query.
962
-	 *
963
-	 * <code>
964
-	 *     $qb = $conn->getQueryBuilder()
965
-	 *         ->select('u.name')
966
-	 *         ->from('users', 'u')
967
-	 *         ->groupBy('u.lastLogin');
968
-	 *         ->addGroupBy('u.createdAt')
969
-	 * </code>
970
-	 *
971
-	 * @param mixed ...$groupBy The grouping expression.
972
-	 *
973
-	 * @return $this This QueryBuilder instance.
974
-	 */
975
-	public function addGroupBy(...$groupBys) {
976
-		if (count($groupBys) === 1 && is_array($groupBys[0])) {
977
-			$$groupBys = $groupBys[0];
978
-		}
979
-
980
-		call_user_func_array(
981
-			[$this->queryBuilder, 'addGroupBy'],
982
-			$this->helper->quoteColumnNames($groupBys)
983
-		);
984
-
985
-		return $this;
986
-	}
987
-
988
-	/**
989
-	 * Sets a value for a column in an insert query.
990
-	 *
991
-	 * <code>
992
-	 *     $qb = $conn->getQueryBuilder()
993
-	 *         ->insert('users')
994
-	 *         ->values(
995
-	 *             array(
996
-	 *                 'name' => '?'
997
-	 *             )
998
-	 *         )
999
-	 *         ->setValue('password', '?');
1000
-	 * </code>
1001
-	 *
1002
-	 * @param string $column The column into which the value should be inserted.
1003
-	 * @param IParameter|string $value The value that should be inserted into the column.
1004
-	 *
1005
-	 * @return $this This QueryBuilder instance.
1006
-	 */
1007
-	public function setValue($column, $value) {
1008
-		$this->queryBuilder->setValue(
1009
-			$this->helper->quoteColumnName($column),
1010
-			(string) $value
1011
-		);
1012
-
1013
-		return $this;
1014
-	}
1015
-
1016
-	/**
1017
-	 * Specifies values for an insert query indexed by column names.
1018
-	 * Replaces any previous values, if any.
1019
-	 *
1020
-	 * <code>
1021
-	 *     $qb = $conn->getQueryBuilder()
1022
-	 *         ->insert('users')
1023
-	 *         ->values(
1024
-	 *             array(
1025
-	 *                 'name' => '?',
1026
-	 *                 'password' => '?'
1027
-	 *             )
1028
-	 *         );
1029
-	 * </code>
1030
-	 *
1031
-	 * @param array $values The values to specify for the insert query indexed by column names.
1032
-	 *
1033
-	 * @return $this This QueryBuilder instance.
1034
-	 */
1035
-	public function values(array $values) {
1036
-		$quotedValues = [];
1037
-		foreach ($values as $key => $value) {
1038
-			$quotedValues[$this->helper->quoteColumnName($key)] = $value;
1039
-		}
1040
-
1041
-		$this->queryBuilder->values($quotedValues);
1042
-
1043
-		return $this;
1044
-	}
1045
-
1046
-	/**
1047
-	 * Specifies a restriction over the groups of the query.
1048
-	 * Replaces any previous having restrictions, if any.
1049
-	 *
1050
-	 * @param mixed ...$having The restriction over the groups.
1051
-	 *
1052
-	 * @return $this This QueryBuilder instance.
1053
-	 */
1054
-	public function having(...$having) {
1055
-		call_user_func_array(
1056
-			[$this->queryBuilder, 'having'],
1057
-			$having
1058
-		);
1059
-
1060
-		return $this;
1061
-	}
1062
-
1063
-	/**
1064
-	 * Adds a restriction over the groups of the query, forming a logical
1065
-	 * conjunction with any existing having restrictions.
1066
-	 *
1067
-	 * @param mixed ...$having The restriction to append.
1068
-	 *
1069
-	 * @return $this This QueryBuilder instance.
1070
-	 */
1071
-	public function andHaving(...$having) {
1072
-		call_user_func_array(
1073
-			[$this->queryBuilder, 'andHaving'],
1074
-			$having
1075
-		);
1076
-
1077
-		return $this;
1078
-	}
1079
-
1080
-	/**
1081
-	 * Adds a restriction over the groups of the query, forming a logical
1082
-	 * disjunction with any existing having restrictions.
1083
-	 *
1084
-	 * @param mixed ...$having The restriction to add.
1085
-	 *
1086
-	 * @return $this This QueryBuilder instance.
1087
-	 */
1088
-	public function orHaving(...$having) {
1089
-		call_user_func_array(
1090
-			[$this->queryBuilder, 'orHaving'],
1091
-			$having
1092
-		);
1093
-
1094
-		return $this;
1095
-	}
1096
-
1097
-	/**
1098
-	 * Specifies an ordering for the query results.
1099
-	 * Replaces any previously specified orderings, if any.
1100
-	 *
1101
-	 * @param string $sort The ordering expression.
1102
-	 * @param string $order The ordering direction.
1103
-	 *
1104
-	 * @return $this This QueryBuilder instance.
1105
-	 */
1106
-	public function orderBy($sort, $order = null) {
1107
-		$this->queryBuilder->orderBy(
1108
-			$this->helper->quoteColumnName($sort),
1109
-			$order
1110
-		);
1111
-
1112
-		return $this;
1113
-	}
1114
-
1115
-	/**
1116
-	 * Adds an ordering to the query results.
1117
-	 *
1118
-	 * @param string $sort The ordering expression.
1119
-	 * @param string $order The ordering direction.
1120
-	 *
1121
-	 * @return $this This QueryBuilder instance.
1122
-	 */
1123
-	public function addOrderBy($sort, $order = null) {
1124
-		$this->queryBuilder->addOrderBy(
1125
-			$this->helper->quoteColumnName($sort),
1126
-			$order
1127
-		);
1128
-
1129
-		return $this;
1130
-	}
1131
-
1132
-	/**
1133
-	 * Gets a query part by its name.
1134
-	 *
1135
-	 * @param string $queryPartName
1136
-	 *
1137
-	 * @return mixed
1138
-	 */
1139
-	public function getQueryPart($queryPartName) {
1140
-		return $this->queryBuilder->getQueryPart($queryPartName);
1141
-	}
1142
-
1143
-	/**
1144
-	 * Gets all query parts.
1145
-	 *
1146
-	 * @return array
1147
-	 */
1148
-	public function getQueryParts() {
1149
-		return $this->queryBuilder->getQueryParts();
1150
-	}
1151
-
1152
-	/**
1153
-	 * Resets SQL parts.
1154
-	 *
1155
-	 * @param array|null $queryPartNames
1156
-	 *
1157
-	 * @return $this This QueryBuilder instance.
1158
-	 */
1159
-	public function resetQueryParts($queryPartNames = null) {
1160
-		$this->queryBuilder->resetQueryParts($queryPartNames);
1161
-
1162
-		return $this;
1163
-	}
1164
-
1165
-	/**
1166
-	 * Resets a single SQL part.
1167
-	 *
1168
-	 * @param string $queryPartName
1169
-	 *
1170
-	 * @return $this This QueryBuilder instance.
1171
-	 */
1172
-	public function resetQueryPart($queryPartName) {
1173
-		$this->queryBuilder->resetQueryPart($queryPartName);
1174
-
1175
-		return $this;
1176
-	}
1177
-
1178
-	/**
1179
-	 * Creates a new named parameter and bind the value $value to it.
1180
-	 *
1181
-	 * This method provides a shortcut for PDOStatement::bindValue
1182
-	 * when using prepared statements.
1183
-	 *
1184
-	 * The parameter $value specifies the value that you want to bind. If
1185
-	 * $placeholder is not provided bindValue() will automatically create a
1186
-	 * placeholder for you. An automatic placeholder will be of the name
1187
-	 * ':dcValue1', ':dcValue2' etc.
1188
-	 *
1189
-	 * For more information see {@link https://www.php.net/pdostatement-bindparam}
1190
-	 *
1191
-	 * Example:
1192
-	 * <code>
1193
-	 * $value = 2;
1194
-	 * $q->eq( 'id', $q->bindValue( $value ) );
1195
-	 * $stmt = $q->executeQuery(); // executed with 'id = 2'
1196
-	 * </code>
1197
-	 *
1198
-	 * @license New BSD License
1199
-	 * @link http://www.zetacomponents.org
1200
-	 *
1201
-	 * @param mixed $value
1202
-	 * @param mixed $type
1203
-	 * @param string $placeHolder The name to bind with. The string must start with a colon ':'.
1204
-	 *
1205
-	 * @return IParameter the placeholder name used.
1206
-	 */
1207
-	public function createNamedParameter($value, $type = IQueryBuilder::PARAM_STR, $placeHolder = null) {
1208
-		return new Parameter($this->queryBuilder->createNamedParameter($value, $type, $placeHolder));
1209
-	}
1210
-
1211
-	/**
1212
-	 * Creates a new positional parameter and bind the given value to it.
1213
-	 *
1214
-	 * Attention: If you are using positional parameters with the query builder you have
1215
-	 * to be very careful to bind all parameters in the order they appear in the SQL
1216
-	 * statement , otherwise they get bound in the wrong order which can lead to serious
1217
-	 * bugs in your code.
1218
-	 *
1219
-	 * Example:
1220
-	 * <code>
1221
-	 *  $qb = $conn->getQueryBuilder();
1222
-	 *  $qb->select('u.*')
1223
-	 *     ->from('users', 'u')
1224
-	 *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR))
1225
-	 *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR))
1226
-	 * </code>
1227
-	 *
1228
-	 * @param mixed $value
1229
-	 * @param integer $type
1230
-	 *
1231
-	 * @return IParameter
1232
-	 */
1233
-	public function createPositionalParameter($value, $type = IQueryBuilder::PARAM_STR) {
1234
-		return new Parameter($this->queryBuilder->createPositionalParameter($value, $type));
1235
-	}
1236
-
1237
-	/**
1238
-	 * Creates a new parameter
1239
-	 *
1240
-	 * Example:
1241
-	 * <code>
1242
-	 *  $qb = $conn->getQueryBuilder();
1243
-	 *  $qb->select('u.*')
1244
-	 *     ->from('users', 'u')
1245
-	 *     ->where('u.username = ' . $qb->createParameter('name'))
1246
-	 *     ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR))
1247
-	 * </code>
1248
-	 *
1249
-	 * @param string $name
1250
-	 *
1251
-	 * @return IParameter
1252
-	 */
1253
-	public function createParameter($name) {
1254
-		return new Parameter(':' . $name);
1255
-	}
1256
-
1257
-	/**
1258
-	 * Creates a new function
1259
-	 *
1260
-	 * Attention: Column names inside the call have to be quoted before hand
1261
-	 *
1262
-	 * Example:
1263
-	 * <code>
1264
-	 *  $qb = $conn->getQueryBuilder();
1265
-	 *  $qb->select($qb->createFunction('COUNT(*)'))
1266
-	 *     ->from('users', 'u')
1267
-	 *  echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u
1268
-	 * </code>
1269
-	 * <code>
1270
-	 *  $qb = $conn->getQueryBuilder();
1271
-	 *  $qb->select($qb->createFunction('COUNT(`column`)'))
1272
-	 *     ->from('users', 'u')
1273
-	 *  echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u
1274
-	 * </code>
1275
-	 *
1276
-	 * @param string $call
1277
-	 *
1278
-	 * @return IQueryFunction
1279
-	 */
1280
-	public function createFunction($call) {
1281
-		return new QueryFunction($call);
1282
-	}
1283
-
1284
-	/**
1285
-	 * Used to get the id of the last inserted element
1286
-	 * @return int
1287
-	 * @throws \BadMethodCallException When being called before an insert query has been run.
1288
-	 */
1289
-	public function getLastInsertId(): int {
1290
-		if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT && $this->lastInsertedTable) {
1291
-			// lastInsertId() needs the prefix but no quotes
1292
-			$table = $this->prefixTableName($this->lastInsertedTable);
1293
-			return $this->connection->lastInsertId($table);
1294
-		}
1295
-
1296
-		throw new \BadMethodCallException('Invalid call to getLastInsertId without using insert() before.');
1297
-	}
1298
-
1299
-	/**
1300
-	 * Returns the table name quoted and with database prefix as needed by the implementation
1301
-	 *
1302
-	 * @param string $table
1303
-	 * @return string
1304
-	 */
1305
-	public function getTableName($table) {
1306
-		if ($table instanceof IQueryFunction) {
1307
-			return (string) $table;
1308
-		}
1309
-
1310
-		$table = $this->prefixTableName($table);
1311
-		return $this->helper->quoteColumnName($table);
1312
-	}
1313
-
1314
-	/**
1315
-	 * Returns the table name with database prefix as needed by the implementation
1316
-	 *
1317
-	 * @param string $table
1318
-	 * @return string
1319
-	 */
1320
-	protected function prefixTableName($table) {
1321
-		if ($this->automaticTablePrefix === false || strpos($table, '*PREFIX*') === 0) {
1322
-			return $table;
1323
-		}
1324
-
1325
-		return '*PREFIX*' . $table;
1326
-	}
1327
-
1328
-	/**
1329
-	 * Returns the column name quoted and with table alias prefix as needed by the implementation
1330
-	 *
1331
-	 * @param string $column
1332
-	 * @param string $tableAlias
1333
-	 * @return string
1334
-	 */
1335
-	public function getColumnName($column, $tableAlias = '') {
1336
-		if ($tableAlias !== '') {
1337
-			$tableAlias .= '.';
1338
-		}
1339
-
1340
-		return $this->helper->quoteColumnName($tableAlias . $column);
1341
-	}
1342
-
1343
-	/**
1344
-	 * Returns the column name quoted and with table alias prefix as needed by the implementation
1345
-	 *
1346
-	 * @param string $alias
1347
-	 * @return string
1348
-	 */
1349
-	public function quoteAlias($alias) {
1350
-		if ($alias === '' || $alias === null) {
1351
-			return $alias;
1352
-		}
1353
-
1354
-		return $this->helper->quoteColumnName($alias);
1355
-	}
60
+    /** @var ConnectionAdapter */
61
+    private $connection;
62
+
63
+    /** @var SystemConfig */
64
+    private $systemConfig;
65
+
66
+    /** @var ILogger */
67
+    private $logger;
68
+
69
+    /** @var \Doctrine\DBAL\Query\QueryBuilder */
70
+    private $queryBuilder;
71
+
72
+    /** @var QuoteHelper */
73
+    private $helper;
74
+
75
+    /** @var bool */
76
+    private $automaticTablePrefix = true;
77
+
78
+    /** @var string */
79
+    protected $lastInsertedTable;
80
+
81
+    /**
82
+     * Initializes a new QueryBuilder.
83
+     *
84
+     * @param ConnectionAdapter $connection
85
+     * @param SystemConfig $systemConfig
86
+     * @param ILogger $logger
87
+     */
88
+    public function __construct(ConnectionAdapter $connection, SystemConfig $systemConfig, ILogger $logger) {
89
+        $this->connection = $connection;
90
+        $this->systemConfig = $systemConfig;
91
+        $this->logger = $logger;
92
+        $this->queryBuilder = new \Doctrine\DBAL\Query\QueryBuilder($this->connection->getInner());
93
+        $this->helper = new QuoteHelper();
94
+    }
95
+
96
+    /**
97
+     * Enable/disable automatic prefixing of table names with the oc_ prefix
98
+     *
99
+     * @param bool $enabled If set to true table names will be prefixed with the
100
+     * owncloud database prefix automatically.
101
+     * @since 8.2.0
102
+     */
103
+    public function automaticTablePrefix($enabled) {
104
+        $this->automaticTablePrefix = (bool) $enabled;
105
+    }
106
+
107
+    /**
108
+     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
109
+     * This producer method is intended for convenient inline usage. Example:
110
+     *
111
+     * <code>
112
+     *     $qb = $conn->getQueryBuilder()
113
+     *         ->select('u')
114
+     *         ->from('users', 'u')
115
+     *         ->where($qb->expr()->eq('u.id', 1));
116
+     * </code>
117
+     *
118
+     * For more complex expression construction, consider storing the expression
119
+     * builder object in a local variable.
120
+     *
121
+     * @return \OCP\DB\QueryBuilder\IExpressionBuilder
122
+     */
123
+    public function expr() {
124
+        if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
125
+            return new OCIExpressionBuilder($this->connection, $this);
126
+        }
127
+        if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
128
+            return new PgSqlExpressionBuilder($this->connection, $this);
129
+        }
130
+        if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) {
131
+            return new MySqlExpressionBuilder($this->connection, $this);
132
+        }
133
+        if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
134
+            return new SqliteExpressionBuilder($this->connection, $this);
135
+        }
136
+
137
+        return new ExpressionBuilder($this->connection, $this);
138
+    }
139
+
140
+    /**
141
+     * Gets an FunctionBuilder used for object-oriented construction of query functions.
142
+     * This producer method is intended for convenient inline usage. Example:
143
+     *
144
+     * <code>
145
+     *     $qb = $conn->getQueryBuilder()
146
+     *         ->select('u')
147
+     *         ->from('users', 'u')
148
+     *         ->where($qb->fun()->md5('u.id'));
149
+     * </code>
150
+     *
151
+     * For more complex function construction, consider storing the function
152
+     * builder object in a local variable.
153
+     *
154
+     * @return \OCP\DB\QueryBuilder\IFunctionBuilder
155
+     */
156
+    public function func() {
157
+        if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
158
+            return new OCIFunctionBuilder($this->helper);
159
+        }
160
+        if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
161
+            return new SqliteFunctionBuilder($this->helper);
162
+        }
163
+        if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
164
+            return new PgSqlFunctionBuilder($this->helper);
165
+        }
166
+
167
+        return new FunctionBuilder($this->helper);
168
+    }
169
+
170
+    /**
171
+     * Gets the type of the currently built query.
172
+     *
173
+     * @return integer
174
+     */
175
+    public function getType() {
176
+        return $this->queryBuilder->getType();
177
+    }
178
+
179
+    /**
180
+     * Gets the associated DBAL Connection for this query builder.
181
+     *
182
+     * @return \OCP\IDBConnection
183
+     */
184
+    public function getConnection() {
185
+        return $this->connection;
186
+    }
187
+
188
+    /**
189
+     * Gets the state of this query builder instance.
190
+     *
191
+     * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
192
+     */
193
+    public function getState() {
194
+        return $this->queryBuilder->getState();
195
+    }
196
+
197
+    /**
198
+     * Executes this query using the bound parameters and their types.
199
+     *
200
+     * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate}
201
+     * for insert, update and delete statements.
202
+     *
203
+     * @return IResult|int
204
+     */
205
+    public function execute() {
206
+        if ($this->systemConfig->getValue('log_query', false)) {
207
+            try {
208
+                $params = [];
209
+                foreach ($this->getParameters() as $placeholder => $value) {
210
+                    if ($value instanceof \DateTime) {
211
+                        $params[] = $placeholder . ' => DateTime:\'' . $value->format('c') . '\'';
212
+                    } elseif (is_array($value)) {
213
+                        $params[] = $placeholder . ' => (\'' . implode('\', \'', $value) . '\')';
214
+                    } else {
215
+                        $params[] = $placeholder . ' => \'' . $value . '\'';
216
+                    }
217
+                }
218
+                if (empty($params)) {
219
+                    $this->logger->debug('DB QueryBuilder: \'{query}\'', [
220
+                        'query' => $this->getSQL(),
221
+                        'app' => 'core',
222
+                    ]);
223
+                } else {
224
+                    $this->logger->debug('DB QueryBuilder: \'{query}\' with parameters: {params}', [
225
+                        'query' => $this->getSQL(),
226
+                        'params' => implode(', ', $params),
227
+                        'app' => 'core',
228
+                    ]);
229
+                }
230
+            } catch (\Error $e) {
231
+                // likely an error during conversion of $value to string
232
+                $this->logger->debug('DB QueryBuilder: error trying to log SQL query');
233
+                $this->logger->logException($e);
234
+            }
235
+        }
236
+
237
+        if (!empty($this->getQueryPart('select'))) {
238
+            $select = $this->getQueryPart('select');
239
+            $hasSelectAll = array_filter($select, static function ($s) {
240
+                return $s === '*';
241
+            });
242
+            $hasSelectSpecific = array_filter($select, static function ($s) {
243
+                return $s !== '*';
244
+            });
245
+
246
+            if (empty($hasSelectAll) === empty($hasSelectSpecific)) {
247
+                $exception = new QueryException('Query is selecting * and specific values in the same query. This is not supported in Oracle.');
248
+                $this->logger->logException($exception, [
249
+                    'message' => 'Query is selecting * and specific values in the same query. This is not supported in Oracle.',
250
+                    'query' => $this->getSQL(),
251
+                    'level' => ILogger::ERROR,
252
+                    'app' => 'core',
253
+                ]);
254
+            }
255
+        }
256
+
257
+        $numberOfParameters = 0;
258
+        $hasTooLargeArrayParameter = false;
259
+        foreach ($this->getParameters() as $parameter) {
260
+            if (is_array($parameter)) {
261
+                $count = count($parameter);
262
+                $numberOfParameters += $count;
263
+                $hasTooLargeArrayParameter = $hasTooLargeArrayParameter || ($count > 1000);
264
+            }
265
+        }
266
+
267
+        if ($hasTooLargeArrayParameter) {
268
+            $exception = new QueryException('More than 1000 expressions in a list are not allowed on Oracle.');
269
+            $this->logger->logException($exception, [
270
+                'message' => 'More than 1000 expressions in a list are not allowed on Oracle.',
271
+                'query' => $this->getSQL(),
272
+                'level' => ILogger::ERROR,
273
+                'app' => 'core',
274
+            ]);
275
+        }
276
+
277
+        if ($numberOfParameters > 65535) {
278
+            $exception = new QueryException('The number of parameters must not exceed 65535. Restriction by PostgreSQL.');
279
+            $this->logger->logException($exception, [
280
+                'message' => 'The number of parameters must not exceed 65535. Restriction by PostgreSQL.',
281
+                'query' => $this->getSQL(),
282
+                'level' => ILogger::ERROR,
283
+                'app' => 'core',
284
+            ]);
285
+        }
286
+
287
+        $result = $this->queryBuilder->execute();
288
+        if (is_int($result)) {
289
+            return $result;
290
+        }
291
+        return new ResultAdapter($result);
292
+    }
293
+
294
+    public function executeQuery(): IResult {
295
+        if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
296
+            throw new \RuntimeException('Invalid query type, expected SELECT query');
297
+        }
298
+
299
+        try {
300
+            $result = $this->execute();
301
+        } catch (\Doctrine\DBAL\Exception $e) {
302
+            throw \OC\DB\Exceptions\DbalException::wrap($e);
303
+        }
304
+
305
+        if ($result instanceof IResult) {
306
+            return $result;
307
+        }
308
+
309
+        throw new \RuntimeException('Invalid return type for query');
310
+    }
311
+
312
+    /**
313
+     * Monkey-patched compatibility layer for apps that were adapted for Nextcloud 22 before
314
+     * the first beta, where executeStatement was named executeUpdate.
315
+     *
316
+     * Static analysis should catch those misuses, but until then let's try to keep things
317
+     * running.
318
+     *
319
+     * @internal
320
+     * @deprecated
321
+     * @todo drop ASAP
322
+     */
323
+    public function executeUpdate(): int {
324
+        return $this->executeStatement();
325
+    }
326
+
327
+    public function executeStatement(): int {
328
+        if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
329
+            throw new \RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement');
330
+        }
331
+
332
+        try {
333
+            $result = $this->execute();
334
+        } catch (\Doctrine\DBAL\Exception $e) {
335
+            throw \OC\DB\Exceptions\DbalException::wrap($e);
336
+        }
337
+
338
+        if (!is_int($result)) {
339
+            throw new \RuntimeException('Invalid return type for statement');
340
+        }
341
+
342
+        return $result;
343
+    }
344
+
345
+
346
+    /**
347
+     * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
348
+     *
349
+     * <code>
350
+     *     $qb = $conn->getQueryBuilder()
351
+     *         ->select('u')
352
+     *         ->from('User', 'u')
353
+     *     echo $qb->getSQL(); // SELECT u FROM User u
354
+     * </code>
355
+     *
356
+     * @return string The SQL query string.
357
+     */
358
+    public function getSQL() {
359
+        return $this->queryBuilder->getSQL();
360
+    }
361
+
362
+    /**
363
+     * Sets a query parameter for the query being constructed.
364
+     *
365
+     * <code>
366
+     *     $qb = $conn->getQueryBuilder()
367
+     *         ->select('u')
368
+     *         ->from('users', 'u')
369
+     *         ->where('u.id = :user_id')
370
+     *         ->setParameter(':user_id', 1);
371
+     * </code>
372
+     *
373
+     * @param string|integer $key The parameter position or name.
374
+     * @param mixed $value The parameter value.
375
+     * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants.
376
+     *
377
+     * @return $this This QueryBuilder instance.
378
+     */
379
+    public function setParameter($key, $value, $type = null) {
380
+        $this->queryBuilder->setParameter($key, $value, $type);
381
+
382
+        return $this;
383
+    }
384
+
385
+    /**
386
+     * Sets a collection of query parameters for the query being constructed.
387
+     *
388
+     * <code>
389
+     *     $qb = $conn->getQueryBuilder()
390
+     *         ->select('u')
391
+     *         ->from('users', 'u')
392
+     *         ->where('u.id = :user_id1 OR u.id = :user_id2')
393
+     *         ->setParameters(array(
394
+     *             ':user_id1' => 1,
395
+     *             ':user_id2' => 2
396
+     *         ));
397
+     * </code>
398
+     *
399
+     * @param array $params The query parameters to set.
400
+     * @param array $types The query parameters types to set.
401
+     *
402
+     * @return $this This QueryBuilder instance.
403
+     */
404
+    public function setParameters(array $params, array $types = []) {
405
+        $this->queryBuilder->setParameters($params, $types);
406
+
407
+        return $this;
408
+    }
409
+
410
+    /**
411
+     * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
412
+     *
413
+     * @return array The currently defined query parameters indexed by parameter index or name.
414
+     */
415
+    public function getParameters() {
416
+        return $this->queryBuilder->getParameters();
417
+    }
418
+
419
+    /**
420
+     * Gets a (previously set) query parameter of the query being constructed.
421
+     *
422
+     * @param mixed $key The key (index or name) of the bound parameter.
423
+     *
424
+     * @return mixed The value of the bound parameter.
425
+     */
426
+    public function getParameter($key) {
427
+        return $this->queryBuilder->getParameter($key);
428
+    }
429
+
430
+    /**
431
+     * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
432
+     *
433
+     * @return array The currently defined query parameter types indexed by parameter index or name.
434
+     */
435
+    public function getParameterTypes() {
436
+        return $this->queryBuilder->getParameterTypes();
437
+    }
438
+
439
+    /**
440
+     * Gets a (previously set) query parameter type of the query being constructed.
441
+     *
442
+     * @param mixed $key The key (index or name) of the bound parameter type.
443
+     *
444
+     * @return mixed The value of the bound parameter type.
445
+     */
446
+    public function getParameterType($key) {
447
+        return $this->queryBuilder->getParameterType($key);
448
+    }
449
+
450
+    /**
451
+     * Sets the position of the first result to retrieve (the "offset").
452
+     *
453
+     * @param integer $firstResult The first result to return.
454
+     *
455
+     * @return $this This QueryBuilder instance.
456
+     */
457
+    public function setFirstResult($firstResult) {
458
+        $this->queryBuilder->setFirstResult($firstResult);
459
+
460
+        return $this;
461
+    }
462
+
463
+    /**
464
+     * Gets the position of the first result the query object was set to retrieve (the "offset").
465
+     * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder.
466
+     *
467
+     * @return integer The position of the first result.
468
+     */
469
+    public function getFirstResult() {
470
+        return $this->queryBuilder->getFirstResult();
471
+    }
472
+
473
+    /**
474
+     * Sets the maximum number of results to retrieve (the "limit").
475
+     *
476
+     * NOTE: Setting max results to "0" will cause mixed behaviour. While most
477
+     * of the databases will just return an empty result set, Oracle will return
478
+     * all entries.
479
+     *
480
+     * @param integer $maxResults The maximum number of results to retrieve.
481
+     *
482
+     * @return $this This QueryBuilder instance.
483
+     */
484
+    public function setMaxResults($maxResults) {
485
+        $this->queryBuilder->setMaxResults($maxResults);
486
+
487
+        return $this;
488
+    }
489
+
490
+    /**
491
+     * Gets the maximum number of results the query object was set to retrieve (the "limit").
492
+     * Returns NULL if {@link setMaxResults} was not applied to this query builder.
493
+     *
494
+     * @return int|null The maximum number of results.
495
+     */
496
+    public function getMaxResults() {
497
+        return $this->queryBuilder->getMaxResults();
498
+    }
499
+
500
+    /**
501
+     * Specifies an item that is to be returned in the query result.
502
+     * Replaces any previously specified selections, if any.
503
+     *
504
+     * <code>
505
+     *     $qb = $conn->getQueryBuilder()
506
+     *         ->select('u.id', 'p.id')
507
+     *         ->from('users', 'u')
508
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
509
+     * </code>
510
+     *
511
+     * @param mixed ...$selects The selection expressions.
512
+     *
513
+     * '@return $this This QueryBuilder instance.
514
+     */
515
+    public function select(...$selects) {
516
+        if (count($selects) === 1 && is_array($selects[0])) {
517
+            $selects = $selects[0];
518
+        }
519
+
520
+        $this->queryBuilder->select(
521
+            $this->helper->quoteColumnNames($selects)
522
+        );
523
+
524
+        return $this;
525
+    }
526
+
527
+    /**
528
+     * Specifies an item that is to be returned with a different name in the query result.
529
+     *
530
+     * <code>
531
+     *     $qb = $conn->getQueryBuilder()
532
+     *         ->selectAlias('u.id', 'user_id')
533
+     *         ->from('users', 'u')
534
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
535
+     * </code>
536
+     *
537
+     * @param mixed $select The selection expressions.
538
+     * @param string $alias The column alias used in the constructed query.
539
+     *
540
+     * @return $this This QueryBuilder instance.
541
+     */
542
+    public function selectAlias($select, $alias) {
543
+        $this->queryBuilder->addSelect(
544
+            $this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias)
545
+        );
546
+
547
+        return $this;
548
+    }
549
+
550
+    /**
551
+     * Specifies an item that is to be returned uniquely in the query result.
552
+     *
553
+     * <code>
554
+     *     $qb = $conn->getQueryBuilder()
555
+     *         ->selectDistinct('type')
556
+     *         ->from('users');
557
+     * </code>
558
+     *
559
+     * @param mixed $select The selection expressions.
560
+     *
561
+     * @return $this This QueryBuilder instance.
562
+     */
563
+    public function selectDistinct($select) {
564
+        if (!is_array($select)) {
565
+            $select = [$select];
566
+        }
567
+
568
+        $quotedSelect = $this->helper->quoteColumnNames($select);
569
+
570
+        $this->queryBuilder->addSelect(
571
+            'DISTINCT ' . implode(', ', $quotedSelect)
572
+        );
573
+
574
+        return $this;
575
+    }
576
+
577
+    /**
578
+     * Adds an item that is to be returned in the query result.
579
+     *
580
+     * <code>
581
+     *     $qb = $conn->getQueryBuilder()
582
+     *         ->select('u.id')
583
+     *         ->addSelect('p.id')
584
+     *         ->from('users', 'u')
585
+     *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
586
+     * </code>
587
+     *
588
+     * @param mixed ...$selects The selection expression.
589
+     *
590
+     * @return $this This QueryBuilder instance.
591
+     */
592
+    public function addSelect(...$selects) {
593
+        if (count($selects) === 1 && is_array($selects[0])) {
594
+            $selects = $selects[0];
595
+        }
596
+
597
+        $this->queryBuilder->addSelect(
598
+            $this->helper->quoteColumnNames($selects)
599
+        );
600
+
601
+        return $this;
602
+    }
603
+
604
+    /**
605
+     * Turns the query being built into a bulk delete query that ranges over
606
+     * a certain table.
607
+     *
608
+     * <code>
609
+     *     $qb = $conn->getQueryBuilder()
610
+     *         ->delete('users', 'u')
611
+     *         ->where('u.id = :user_id');
612
+     *         ->setParameter(':user_id', 1);
613
+     * </code>
614
+     *
615
+     * @param string $delete The table whose rows are subject to the deletion.
616
+     * @param string $alias The table alias used in the constructed query.
617
+     *
618
+     * @return $this This QueryBuilder instance.
619
+     */
620
+    public function delete($delete = null, $alias = null) {
621
+        $this->queryBuilder->delete(
622
+            $this->getTableName($delete),
623
+            $alias
624
+        );
625
+
626
+        return $this;
627
+    }
628
+
629
+    /**
630
+     * Turns the query being built into a bulk update query that ranges over
631
+     * a certain table
632
+     *
633
+     * <code>
634
+     *     $qb = $conn->getQueryBuilder()
635
+     *         ->update('users', 'u')
636
+     *         ->set('u.password', md5('password'))
637
+     *         ->where('u.id = ?');
638
+     * </code>
639
+     *
640
+     * @param string $update The table whose rows are subject to the update.
641
+     * @param string $alias The table alias used in the constructed query.
642
+     *
643
+     * @return $this This QueryBuilder instance.
644
+     */
645
+    public function update($update = null, $alias = null) {
646
+        $this->queryBuilder->update(
647
+            $this->getTableName($update),
648
+            $alias
649
+        );
650
+
651
+        return $this;
652
+    }
653
+
654
+    /**
655
+     * Turns the query being built into an insert query that inserts into
656
+     * a certain table
657
+     *
658
+     * <code>
659
+     *     $qb = $conn->getQueryBuilder()
660
+     *         ->insert('users')
661
+     *         ->values(
662
+     *             array(
663
+     *                 'name' => '?',
664
+     *                 'password' => '?'
665
+     *             )
666
+     *         );
667
+     * </code>
668
+     *
669
+     * @param string $insert The table into which the rows should be inserted.
670
+     *
671
+     * @return $this This QueryBuilder instance.
672
+     */
673
+    public function insert($insert = null) {
674
+        $this->queryBuilder->insert(
675
+            $this->getTableName($insert)
676
+        );
677
+
678
+        $this->lastInsertedTable = $insert;
679
+
680
+        return $this;
681
+    }
682
+
683
+    /**
684
+     * Creates and adds a query root corresponding to the table identified by the
685
+     * given alias, forming a cartesian product with any existing query roots.
686
+     *
687
+     * <code>
688
+     *     $qb = $conn->getQueryBuilder()
689
+     *         ->select('u.id')
690
+     *         ->from('users', 'u')
691
+     * </code>
692
+     *
693
+     * @param string $from The table.
694
+     * @param string|null $alias The alias of the table.
695
+     *
696
+     * @return $this This QueryBuilder instance.
697
+     */
698
+    public function from($from, $alias = null) {
699
+        $this->queryBuilder->from(
700
+            $this->getTableName($from),
701
+            $this->quoteAlias($alias)
702
+        );
703
+
704
+        return $this;
705
+    }
706
+
707
+    /**
708
+     * Creates and adds a join to the query.
709
+     *
710
+     * <code>
711
+     *     $qb = $conn->getQueryBuilder()
712
+     *         ->select('u.name')
713
+     *         ->from('users', 'u')
714
+     *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
715
+     * </code>
716
+     *
717
+     * @param string $fromAlias The alias that points to a from clause.
718
+     * @param string $join The table name to join.
719
+     * @param string $alias The alias of the join table.
720
+     * @param string|ICompositeExpression|null $condition The condition for the join.
721
+     *
722
+     * @return $this This QueryBuilder instance.
723
+     */
724
+    public function join($fromAlias, $join, $alias, $condition = null) {
725
+        $this->queryBuilder->join(
726
+            $this->quoteAlias($fromAlias),
727
+            $this->getTableName($join),
728
+            $this->quoteAlias($alias),
729
+            $condition
730
+        );
731
+
732
+        return $this;
733
+    }
734
+
735
+    /**
736
+     * Creates and adds a join to the query.
737
+     *
738
+     * <code>
739
+     *     $qb = $conn->getQueryBuilder()
740
+     *         ->select('u.name')
741
+     *         ->from('users', 'u')
742
+     *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
743
+     * </code>
744
+     *
745
+     * @param string $fromAlias The alias that points to a from clause.
746
+     * @param string $join The table name to join.
747
+     * @param string $alias The alias of the join table.
748
+     * @param string|ICompositeExpression|null $condition The condition for the join.
749
+     *
750
+     * @return $this This QueryBuilder instance.
751
+     */
752
+    public function innerJoin($fromAlias, $join, $alias, $condition = null) {
753
+        $this->queryBuilder->innerJoin(
754
+            $this->quoteAlias($fromAlias),
755
+            $this->getTableName($join),
756
+            $this->quoteAlias($alias),
757
+            $condition
758
+        );
759
+
760
+        return $this;
761
+    }
762
+
763
+    /**
764
+     * Creates and adds a left join to the query.
765
+     *
766
+     * <code>
767
+     *     $qb = $conn->getQueryBuilder()
768
+     *         ->select('u.name')
769
+     *         ->from('users', 'u')
770
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
771
+     * </code>
772
+     *
773
+     * @param string $fromAlias The alias that points to a from clause.
774
+     * @param string $join The table name to join.
775
+     * @param string $alias The alias of the join table.
776
+     * @param string|ICompositeExpression|null $condition The condition for the join.
777
+     *
778
+     * @return $this This QueryBuilder instance.
779
+     */
780
+    public function leftJoin($fromAlias, $join, $alias, $condition = null) {
781
+        $this->queryBuilder->leftJoin(
782
+            $this->quoteAlias($fromAlias),
783
+            $this->getTableName($join),
784
+            $this->quoteAlias($alias),
785
+            $condition
786
+        );
787
+
788
+        return $this;
789
+    }
790
+
791
+    /**
792
+     * Creates and adds a right join to the query.
793
+     *
794
+     * <code>
795
+     *     $qb = $conn->getQueryBuilder()
796
+     *         ->select('u.name')
797
+     *         ->from('users', 'u')
798
+     *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
799
+     * </code>
800
+     *
801
+     * @param string $fromAlias The alias that points to a from clause.
802
+     * @param string $join The table name to join.
803
+     * @param string $alias The alias of the join table.
804
+     * @param string|ICompositeExpression|null $condition The condition for the join.
805
+     *
806
+     * @return $this This QueryBuilder instance.
807
+     */
808
+    public function rightJoin($fromAlias, $join, $alias, $condition = null) {
809
+        $this->queryBuilder->rightJoin(
810
+            $this->quoteAlias($fromAlias),
811
+            $this->getTableName($join),
812
+            $this->quoteAlias($alias),
813
+            $condition
814
+        );
815
+
816
+        return $this;
817
+    }
818
+
819
+    /**
820
+     * Sets a new value for a column in a bulk update query.
821
+     *
822
+     * <code>
823
+     *     $qb = $conn->getQueryBuilder()
824
+     *         ->update('users', 'u')
825
+     *         ->set('u.password', md5('password'))
826
+     *         ->where('u.id = ?');
827
+     * </code>
828
+     *
829
+     * @param string $key The column to set.
830
+     * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc.
831
+     *
832
+     * @return $this This QueryBuilder instance.
833
+     */
834
+    public function set($key, $value) {
835
+        $this->queryBuilder->set(
836
+            $this->helper->quoteColumnName($key),
837
+            $this->helper->quoteColumnName($value)
838
+        );
839
+
840
+        return $this;
841
+    }
842
+
843
+    /**
844
+     * Specifies one or more restrictions to the query result.
845
+     * Replaces any previously specified restrictions, if any.
846
+     *
847
+     * <code>
848
+     *     $qb = $conn->getQueryBuilder()
849
+     *         ->select('u.name')
850
+     *         ->from('users', 'u')
851
+     *         ->where('u.id = ?');
852
+     *
853
+     *     // You can optionally programatically build and/or expressions
854
+     *     $qb = $conn->getQueryBuilder();
855
+     *
856
+     *     $or = $qb->expr()->orx();
857
+     *     $or->add($qb->expr()->eq('u.id', 1));
858
+     *     $or->add($qb->expr()->eq('u.id', 2));
859
+     *
860
+     *     $qb->update('users', 'u')
861
+     *         ->set('u.password', md5('password'))
862
+     *         ->where($or);
863
+     * </code>
864
+     *
865
+     * @param mixed ...$predicates The restriction predicates.
866
+     *
867
+     * @return $this This QueryBuilder instance.
868
+     */
869
+    public function where(...$predicates) {
870
+        call_user_func_array(
871
+            [$this->queryBuilder, 'where'],
872
+            $predicates
873
+        );
874
+
875
+        return $this;
876
+    }
877
+
878
+    /**
879
+     * Adds one or more restrictions to the query results, forming a logical
880
+     * conjunction with any previously specified restrictions.
881
+     *
882
+     * <code>
883
+     *     $qb = $conn->getQueryBuilder()
884
+     *         ->select('u')
885
+     *         ->from('users', 'u')
886
+     *         ->where('u.username LIKE ?')
887
+     *         ->andWhere('u.is_active = 1');
888
+     * </code>
889
+     *
890
+     * @param mixed ...$where The query restrictions.
891
+     *
892
+     * @return $this This QueryBuilder instance.
893
+     *
894
+     * @see where()
895
+     */
896
+    public function andWhere(...$where) {
897
+        call_user_func_array(
898
+            [$this->queryBuilder, 'andWhere'],
899
+            $where
900
+        );
901
+
902
+        return $this;
903
+    }
904
+
905
+    /**
906
+     * Adds one or more restrictions to the query results, forming a logical
907
+     * disjunction with any previously specified restrictions.
908
+     *
909
+     * <code>
910
+     *     $qb = $conn->getQueryBuilder()
911
+     *         ->select('u.name')
912
+     *         ->from('users', 'u')
913
+     *         ->where('u.id = 1')
914
+     *         ->orWhere('u.id = 2');
915
+     * </code>
916
+     *
917
+     * @param mixed ...$where The WHERE statement.
918
+     *
919
+     * @return $this This QueryBuilder instance.
920
+     *
921
+     * @see where()
922
+     */
923
+    public function orWhere(...$where) {
924
+        call_user_func_array(
925
+            [$this->queryBuilder, 'orWhere'],
926
+            $where
927
+        );
928
+
929
+        return $this;
930
+    }
931
+
932
+    /**
933
+     * Specifies a grouping over the results of the query.
934
+     * Replaces any previously specified groupings, if any.
935
+     *
936
+     * <code>
937
+     *     $qb = $conn->getQueryBuilder()
938
+     *         ->select('u.name')
939
+     *         ->from('users', 'u')
940
+     *         ->groupBy('u.id');
941
+     * </code>
942
+     *
943
+     * @param mixed ...$groupBys The grouping expression.
944
+     *
945
+     * @return $this This QueryBuilder instance.
946
+     */
947
+    public function groupBy(...$groupBys) {
948
+        if (count($groupBys) === 1 && is_array($groupBys[0])) {
949
+            $groupBys = $groupBys[0];
950
+        }
951
+
952
+        call_user_func_array(
953
+            [$this->queryBuilder, 'groupBy'],
954
+            $this->helper->quoteColumnNames($groupBys)
955
+        );
956
+
957
+        return $this;
958
+    }
959
+
960
+    /**
961
+     * Adds a grouping expression to the query.
962
+     *
963
+     * <code>
964
+     *     $qb = $conn->getQueryBuilder()
965
+     *         ->select('u.name')
966
+     *         ->from('users', 'u')
967
+     *         ->groupBy('u.lastLogin');
968
+     *         ->addGroupBy('u.createdAt')
969
+     * </code>
970
+     *
971
+     * @param mixed ...$groupBy The grouping expression.
972
+     *
973
+     * @return $this This QueryBuilder instance.
974
+     */
975
+    public function addGroupBy(...$groupBys) {
976
+        if (count($groupBys) === 1 && is_array($groupBys[0])) {
977
+            $$groupBys = $groupBys[0];
978
+        }
979
+
980
+        call_user_func_array(
981
+            [$this->queryBuilder, 'addGroupBy'],
982
+            $this->helper->quoteColumnNames($groupBys)
983
+        );
984
+
985
+        return $this;
986
+    }
987
+
988
+    /**
989
+     * Sets a value for a column in an insert query.
990
+     *
991
+     * <code>
992
+     *     $qb = $conn->getQueryBuilder()
993
+     *         ->insert('users')
994
+     *         ->values(
995
+     *             array(
996
+     *                 'name' => '?'
997
+     *             )
998
+     *         )
999
+     *         ->setValue('password', '?');
1000
+     * </code>
1001
+     *
1002
+     * @param string $column The column into which the value should be inserted.
1003
+     * @param IParameter|string $value The value that should be inserted into the column.
1004
+     *
1005
+     * @return $this This QueryBuilder instance.
1006
+     */
1007
+    public function setValue($column, $value) {
1008
+        $this->queryBuilder->setValue(
1009
+            $this->helper->quoteColumnName($column),
1010
+            (string) $value
1011
+        );
1012
+
1013
+        return $this;
1014
+    }
1015
+
1016
+    /**
1017
+     * Specifies values for an insert query indexed by column names.
1018
+     * Replaces any previous values, if any.
1019
+     *
1020
+     * <code>
1021
+     *     $qb = $conn->getQueryBuilder()
1022
+     *         ->insert('users')
1023
+     *         ->values(
1024
+     *             array(
1025
+     *                 'name' => '?',
1026
+     *                 'password' => '?'
1027
+     *             )
1028
+     *         );
1029
+     * </code>
1030
+     *
1031
+     * @param array $values The values to specify for the insert query indexed by column names.
1032
+     *
1033
+     * @return $this This QueryBuilder instance.
1034
+     */
1035
+    public function values(array $values) {
1036
+        $quotedValues = [];
1037
+        foreach ($values as $key => $value) {
1038
+            $quotedValues[$this->helper->quoteColumnName($key)] = $value;
1039
+        }
1040
+
1041
+        $this->queryBuilder->values($quotedValues);
1042
+
1043
+        return $this;
1044
+    }
1045
+
1046
+    /**
1047
+     * Specifies a restriction over the groups of the query.
1048
+     * Replaces any previous having restrictions, if any.
1049
+     *
1050
+     * @param mixed ...$having The restriction over the groups.
1051
+     *
1052
+     * @return $this This QueryBuilder instance.
1053
+     */
1054
+    public function having(...$having) {
1055
+        call_user_func_array(
1056
+            [$this->queryBuilder, 'having'],
1057
+            $having
1058
+        );
1059
+
1060
+        return $this;
1061
+    }
1062
+
1063
+    /**
1064
+     * Adds a restriction over the groups of the query, forming a logical
1065
+     * conjunction with any existing having restrictions.
1066
+     *
1067
+     * @param mixed ...$having The restriction to append.
1068
+     *
1069
+     * @return $this This QueryBuilder instance.
1070
+     */
1071
+    public function andHaving(...$having) {
1072
+        call_user_func_array(
1073
+            [$this->queryBuilder, 'andHaving'],
1074
+            $having
1075
+        );
1076
+
1077
+        return $this;
1078
+    }
1079
+
1080
+    /**
1081
+     * Adds a restriction over the groups of the query, forming a logical
1082
+     * disjunction with any existing having restrictions.
1083
+     *
1084
+     * @param mixed ...$having The restriction to add.
1085
+     *
1086
+     * @return $this This QueryBuilder instance.
1087
+     */
1088
+    public function orHaving(...$having) {
1089
+        call_user_func_array(
1090
+            [$this->queryBuilder, 'orHaving'],
1091
+            $having
1092
+        );
1093
+
1094
+        return $this;
1095
+    }
1096
+
1097
+    /**
1098
+     * Specifies an ordering for the query results.
1099
+     * Replaces any previously specified orderings, if any.
1100
+     *
1101
+     * @param string $sort The ordering expression.
1102
+     * @param string $order The ordering direction.
1103
+     *
1104
+     * @return $this This QueryBuilder instance.
1105
+     */
1106
+    public function orderBy($sort, $order = null) {
1107
+        $this->queryBuilder->orderBy(
1108
+            $this->helper->quoteColumnName($sort),
1109
+            $order
1110
+        );
1111
+
1112
+        return $this;
1113
+    }
1114
+
1115
+    /**
1116
+     * Adds an ordering to the query results.
1117
+     *
1118
+     * @param string $sort The ordering expression.
1119
+     * @param string $order The ordering direction.
1120
+     *
1121
+     * @return $this This QueryBuilder instance.
1122
+     */
1123
+    public function addOrderBy($sort, $order = null) {
1124
+        $this->queryBuilder->addOrderBy(
1125
+            $this->helper->quoteColumnName($sort),
1126
+            $order
1127
+        );
1128
+
1129
+        return $this;
1130
+    }
1131
+
1132
+    /**
1133
+     * Gets a query part by its name.
1134
+     *
1135
+     * @param string $queryPartName
1136
+     *
1137
+     * @return mixed
1138
+     */
1139
+    public function getQueryPart($queryPartName) {
1140
+        return $this->queryBuilder->getQueryPart($queryPartName);
1141
+    }
1142
+
1143
+    /**
1144
+     * Gets all query parts.
1145
+     *
1146
+     * @return array
1147
+     */
1148
+    public function getQueryParts() {
1149
+        return $this->queryBuilder->getQueryParts();
1150
+    }
1151
+
1152
+    /**
1153
+     * Resets SQL parts.
1154
+     *
1155
+     * @param array|null $queryPartNames
1156
+     *
1157
+     * @return $this This QueryBuilder instance.
1158
+     */
1159
+    public function resetQueryParts($queryPartNames = null) {
1160
+        $this->queryBuilder->resetQueryParts($queryPartNames);
1161
+
1162
+        return $this;
1163
+    }
1164
+
1165
+    /**
1166
+     * Resets a single SQL part.
1167
+     *
1168
+     * @param string $queryPartName
1169
+     *
1170
+     * @return $this This QueryBuilder instance.
1171
+     */
1172
+    public function resetQueryPart($queryPartName) {
1173
+        $this->queryBuilder->resetQueryPart($queryPartName);
1174
+
1175
+        return $this;
1176
+    }
1177
+
1178
+    /**
1179
+     * Creates a new named parameter and bind the value $value to it.
1180
+     *
1181
+     * This method provides a shortcut for PDOStatement::bindValue
1182
+     * when using prepared statements.
1183
+     *
1184
+     * The parameter $value specifies the value that you want to bind. If
1185
+     * $placeholder is not provided bindValue() will automatically create a
1186
+     * placeholder for you. An automatic placeholder will be of the name
1187
+     * ':dcValue1', ':dcValue2' etc.
1188
+     *
1189
+     * For more information see {@link https://www.php.net/pdostatement-bindparam}
1190
+     *
1191
+     * Example:
1192
+     * <code>
1193
+     * $value = 2;
1194
+     * $q->eq( 'id', $q->bindValue( $value ) );
1195
+     * $stmt = $q->executeQuery(); // executed with 'id = 2'
1196
+     * </code>
1197
+     *
1198
+     * @license New BSD License
1199
+     * @link http://www.zetacomponents.org
1200
+     *
1201
+     * @param mixed $value
1202
+     * @param mixed $type
1203
+     * @param string $placeHolder The name to bind with. The string must start with a colon ':'.
1204
+     *
1205
+     * @return IParameter the placeholder name used.
1206
+     */
1207
+    public function createNamedParameter($value, $type = IQueryBuilder::PARAM_STR, $placeHolder = null) {
1208
+        return new Parameter($this->queryBuilder->createNamedParameter($value, $type, $placeHolder));
1209
+    }
1210
+
1211
+    /**
1212
+     * Creates a new positional parameter and bind the given value to it.
1213
+     *
1214
+     * Attention: If you are using positional parameters with the query builder you have
1215
+     * to be very careful to bind all parameters in the order they appear in the SQL
1216
+     * statement , otherwise they get bound in the wrong order which can lead to serious
1217
+     * bugs in your code.
1218
+     *
1219
+     * Example:
1220
+     * <code>
1221
+     *  $qb = $conn->getQueryBuilder();
1222
+     *  $qb->select('u.*')
1223
+     *     ->from('users', 'u')
1224
+     *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR))
1225
+     *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR))
1226
+     * </code>
1227
+     *
1228
+     * @param mixed $value
1229
+     * @param integer $type
1230
+     *
1231
+     * @return IParameter
1232
+     */
1233
+    public function createPositionalParameter($value, $type = IQueryBuilder::PARAM_STR) {
1234
+        return new Parameter($this->queryBuilder->createPositionalParameter($value, $type));
1235
+    }
1236
+
1237
+    /**
1238
+     * Creates a new parameter
1239
+     *
1240
+     * Example:
1241
+     * <code>
1242
+     *  $qb = $conn->getQueryBuilder();
1243
+     *  $qb->select('u.*')
1244
+     *     ->from('users', 'u')
1245
+     *     ->where('u.username = ' . $qb->createParameter('name'))
1246
+     *     ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR))
1247
+     * </code>
1248
+     *
1249
+     * @param string $name
1250
+     *
1251
+     * @return IParameter
1252
+     */
1253
+    public function createParameter($name) {
1254
+        return new Parameter(':' . $name);
1255
+    }
1256
+
1257
+    /**
1258
+     * Creates a new function
1259
+     *
1260
+     * Attention: Column names inside the call have to be quoted before hand
1261
+     *
1262
+     * Example:
1263
+     * <code>
1264
+     *  $qb = $conn->getQueryBuilder();
1265
+     *  $qb->select($qb->createFunction('COUNT(*)'))
1266
+     *     ->from('users', 'u')
1267
+     *  echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u
1268
+     * </code>
1269
+     * <code>
1270
+     *  $qb = $conn->getQueryBuilder();
1271
+     *  $qb->select($qb->createFunction('COUNT(`column`)'))
1272
+     *     ->from('users', 'u')
1273
+     *  echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u
1274
+     * </code>
1275
+     *
1276
+     * @param string $call
1277
+     *
1278
+     * @return IQueryFunction
1279
+     */
1280
+    public function createFunction($call) {
1281
+        return new QueryFunction($call);
1282
+    }
1283
+
1284
+    /**
1285
+     * Used to get the id of the last inserted element
1286
+     * @return int
1287
+     * @throws \BadMethodCallException When being called before an insert query has been run.
1288
+     */
1289
+    public function getLastInsertId(): int {
1290
+        if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT && $this->lastInsertedTable) {
1291
+            // lastInsertId() needs the prefix but no quotes
1292
+            $table = $this->prefixTableName($this->lastInsertedTable);
1293
+            return $this->connection->lastInsertId($table);
1294
+        }
1295
+
1296
+        throw new \BadMethodCallException('Invalid call to getLastInsertId without using insert() before.');
1297
+    }
1298
+
1299
+    /**
1300
+     * Returns the table name quoted and with database prefix as needed by the implementation
1301
+     *
1302
+     * @param string $table
1303
+     * @return string
1304
+     */
1305
+    public function getTableName($table) {
1306
+        if ($table instanceof IQueryFunction) {
1307
+            return (string) $table;
1308
+        }
1309
+
1310
+        $table = $this->prefixTableName($table);
1311
+        return $this->helper->quoteColumnName($table);
1312
+    }
1313
+
1314
+    /**
1315
+     * Returns the table name with database prefix as needed by the implementation
1316
+     *
1317
+     * @param string $table
1318
+     * @return string
1319
+     */
1320
+    protected function prefixTableName($table) {
1321
+        if ($this->automaticTablePrefix === false || strpos($table, '*PREFIX*') === 0) {
1322
+            return $table;
1323
+        }
1324
+
1325
+        return '*PREFIX*' . $table;
1326
+    }
1327
+
1328
+    /**
1329
+     * Returns the column name quoted and with table alias prefix as needed by the implementation
1330
+     *
1331
+     * @param string $column
1332
+     * @param string $tableAlias
1333
+     * @return string
1334
+     */
1335
+    public function getColumnName($column, $tableAlias = '') {
1336
+        if ($tableAlias !== '') {
1337
+            $tableAlias .= '.';
1338
+        }
1339
+
1340
+        return $this->helper->quoteColumnName($tableAlias . $column);
1341
+    }
1342
+
1343
+    /**
1344
+     * Returns the column name quoted and with table alias prefix as needed by the implementation
1345
+     *
1346
+     * @param string $alias
1347
+     * @return string
1348
+     */
1349
+    public function quoteAlias($alias) {
1350
+        if ($alias === '' || $alias === null) {
1351
+            return $alias;
1352
+        }
1353
+
1354
+        return $this->helper->quoteColumnName($alias);
1355
+    }
1356 1356
 }
Please login to merge, or discard this patch.