Passed
Push — master ( 79883e...5852a4 )
by
unknown
14:34
created

GrommunioCalDavBackend::createCalendar()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 5
nop 3
dl 0
loc 20
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * SPDX-License-Identifier: AGPL-3.0-only
5
 * SPDX-FileCopyrightText: Copyright 2016 - 2018 Kopano b.v.
6
 * SPDX-FileCopyrightText: Copyright 2020 - 2024 grommunio GmbH
7
 *
8
 * grommunio CalDAV backend class which handles calendar related activities.
9
 */
10
11
namespace grommunio\DAV;
12
13
use Sabre\CalDAV\Backend\AbstractBackend;
14
use Sabre\CalDAV\Backend\SchedulingSupport;
15
use Sabre\CalDAV\Backend\SyncSupport;
16
17
class GrommunioCalDavBackend extends AbstractBackend implements SchedulingSupport, SyncSupport {
18
	/*
19
	 * TODO IMPLEMENT
20
	 *
21
	 * SubscriptionSupport,
22
	 * SharingSupport,
23
	 *
24
	 */
25
26
	private $logger;
27
	protected $gDavBackend;
28
29
	public const FILE_EXTENSION = '.ics';
30
	// Include both appointments and tasks so task lists sync properly.
31
	public const MESSAGE_CLASSES = ['IPM.Appointment', 'IPM.Task'];
32
	public const CONTAINER_CLASS = 'IPF.Appointment';
33
	public const CONTAINER_CLASSES = ['IPF.Appointment', 'IPF.Task'];
34
35
	/**
36
	 * Constructor.
37
	 */
38
	public function __construct(GrommunioDavBackend $gDavBackend, GLogger $glogger) {
39
		$this->gDavBackend = $gDavBackend;
40
		$this->logger = $glogger;
41
	}
42
43
	/**
44
	 * Returns a list of calendars for a principal.
45
	 *
46
	 * Every project is an array with the following keys:
47
	 *  * id, a unique id that will be used by other functions to modify the
48
	 *    calendar. This can be the same as the uri or a database key.
49
	 *  * uri. This is just the 'base uri' or 'filename' of the calendar.
50
	 *  * principaluri. The owner of the calendar. Almost always the same as
51
	 *    principalUri passed to this method.
52
	 *
53
	 * Furthermore it can contain webdav properties in clark notation. A very
54
	 * common one is '{DAV:}displayname'.
55
	 *
56
	 * Many clients also require:
57
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
58
	 * For this property, you can just return an instance of
59
	 * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet.
60
	 *
61
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
62
	 * ACL will automatically be put in read-only mode.
63
	 *
64
	 * @param string $principalUri
65
	 *
66
	 * @return array
67
	 */
68
	public function getCalendarsForUser($principalUri) {
69
		$this->logger->trace("principalUri: %s", $principalUri);
70
71
		return $this->gDavBackend->GetFolders($principalUri, static::CONTAINER_CLASSES);
72
	}
73
74
	/**
75
	 * Creates a new calendar for a principal.
76
	 *
77
	 * If the creation was a success, an id must be returned that can be used
78
	 * to reference this calendar in other methods, such as updateCalendar.
79
	 *
80
	 * @param string $principalUri
81
	 * @param string $calendarUri
82
	 *
83
	 * @return string
84
	 */
85
	public function createCalendar($principalUri, $calendarUri, array $properties) {
86
		$this->logger->trace("principalUri: %s - calendarUri: %s - properties: %s", $principalUri, $calendarUri, $properties);
87
88
		// Determine requested component set to choose proper container class.
89
		$containerClass = static::CONTAINER_CLASS; // default to appointments
90
		$key = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
91
		if (isset($properties[$key]) && method_exists($properties[$key], 'getValue')) {
92
			$components = $properties[$key]->getValue();
93
			if (is_array($components)) {
94
				if (in_array('VTODO', $components, true)) {
95
					$containerClass = 'IPF.Task';
96
				}
97
				elseif (in_array('VEVENT', $components, true)) {
98
					$containerClass = 'IPF.Appointment';
99
				}
100
			}
101
		}
102
103
		// TODO Add displayname
104
		return $this->gDavBackend->CreateFolder($principalUri, $calendarUri, $containerClass, "");
105
	}
106
107
	/**
108
	 * Delete a calendar and all its objects.
109
	 *
110
	 * @param string $calendarId
111
	 */
112
	public function deleteCalendar($calendarId) {
113
		$this->logger->trace("calendarId: %s", $calendarId);
114
		$success = $this->gDavBackend->DeleteFolder($calendarId);
0 ignored issues
show
Unused Code introduced by
The assignment to $success is dead and can be removed.
Loading history...
115
		// TODO evaluate $success
116
	}
117
118
	/**
119
	 * Returns all calendar objects within a calendar.
120
	 *
121
	 * Every item contains an array with the following keys:
122
	 *   * calendardata - The iCalendar-compatible calendar data
123
	 *   * uri - a unique key which will be used to construct the uri. This can
124
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
125
	 *     good idea. This is only the basename, or filename, not the full
126
	 *     path.
127
	 *   * lastmodified - a timestamp of the last modification time
128
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
129
	 *   '  "abcdef"')
130
	 *   * size - The size of the calendar objects, in bytes.
131
	 *   * component - optional, a string containing the type of object, such
132
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
133
	 *     the Content-Type header.
134
	 *
135
	 * Note that the etag is optional, but it's highly encouraged to return for
136
	 * speed reasons.
137
	 *
138
	 * The calendardata is also optional. If it's not returned
139
	 * 'getCalendarObject' will be called later, which *is* expected to return
140
	 * calendardata.
141
	 *
142
	 * If neither etag or size are specified, the calendardata will be
143
	 * used/fetched to determine these numbers. If both are specified the
144
	 * amount of times this is needed is reduced by a great degree.
145
	 *
146
	 * @param string $calendarId
147
	 *
148
	 * @return array
149
	 */
150
	public function getCalendarObjects($calendarId) {
151
		$result = $this->gDavBackend->GetObjects($calendarId, static::FILE_EXTENSION, ['types' => static::MESSAGE_CLASSES]);
152
		$this->logger->trace("calendarId: %s found %d objects", $calendarId, count($result));
153
154
		return $result;
155
	}
156
157
	/**
158
	 * Performs a calendar-query on the contents of this calendar.
159
	 *
160
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
161
	 * calendar-query it is possible for a client to request a specific set of
162
	 * object, based on contents of iCalendar properties, date-ranges and
163
	 * iCalendar component types (VTODO, VEVENT).
164
	 *
165
	 * This method should just return a list of (relative) urls that match this
166
	 * query.
167
	 *
168
	 * The list of filters are specified as an array. The exact array is
169
	 * documented by \Sabre\CalDAV\CalendarQueryParser.
170
	 *
171
	 * Note that it is extremely likely that getCalendarObject for every path
172
	 * returned from this method will be called almost immediately after. You
173
	 * may want to anticipate this to speed up these requests.
174
	 *
175
	 * This method provides a default implementation, which parses *all* the
176
	 * iCalendar objects in the specified calendar.
177
	 *
178
	 * This default may well be good enough for personal use, and calendars
179
	 * that aren't very large. But if you anticipate high usage, big calendars
180
	 * or high loads, you are strongly advised to optimize certain paths.
181
	 *
182
	 * The best way to do so is override this method and to optimize
183
	 * specifically for 'common filters'.
184
	 *
185
	 * Requests that are extremely common are:
186
	 *   * requests for just VEVENTS
187
	 *   * requests for just VTODO
188
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
189
	 *
190
	 * ..and combinations of these requests. It may not be worth it to try to
191
	 * handle every possible situation and just rely on the (relatively
192
	 * easy to use) CalendarQueryValidator to handle the rest.
193
	 *
194
	 * Note that especially time-range-filters may be difficult to parse. A
195
	 * time-range filter specified on a VEVENT must for instance also handle
196
	 * recurrence rules correctly.
197
	 * A good example of how to interpret all these filters can also simply
198
	 * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
199
	 * as possible, so it gives you a good idea on what type of stuff you need
200
	 * to think of.
201
	 *
202
	 * @param mixed $calendarId
203
	 *
204
	 * @return array
205
	 */
206
	public function calendarQuery($calendarId, array $filters) {
207
		$start = $end = null;
208
		$types = [];
209
		foreach ($filters['comp-filters'] as $filter) {
210
			if ($filter['name'] == 'VEVENT') {
211
				$types[] = 'IPM.Appointment';
212
			}
213
			elseif ($filter['name'] == 'VTODO') {
214
				$types[] = 'IPM.Task';
215
			}
216
217
			/* will this work on tasks? */
218
			if (is_array($filter['time-range']) && isset($filter['time-range']['start'], $filter['time-range']['end'])) {
219
				$start = $filter['time-range']['start']->getTimestamp();
220
				$end = $filter['time-range']['end']->getTimestamp();
221
			}
222
		}
223
224
		$objfilters = [];
225
		if ($start != null && $end != null) {
226
			$objfilters["start"] = $start;
227
			$objfilters["end"] = $end;
228
		}
229
		if (!empty($types)) {
230
			$objfilters["types"] = $types;
231
		}
232
233
		$objects = $this->gDavBackend->GetObjects($calendarId, static::FILE_EXTENSION, $objfilters);
234
		$result = [];
235
		foreach ($objects as $object) {
236
			$result[] = $object['uri'];
237
		}
238
239
		return $result;
240
	}
241
242
	/**
243
	 * Returns information from a single calendar object, based on its object uri.
244
	 *
245
	 * The object uri is only the basename, or filename and not a full path.
246
	 *
247
	 * The returned array must have the same keys as getCalendarObjects. The
248
	 * 'calendardata' object is required here though, while it's not required
249
	 * for getCalendarObjects.
250
	 *
251
	 * This method must return null if the object did not exist.
252
	 *
253
	 * @param string   $calendarId
254
	 * @param string   $objectUri
255
	 * @param resource $mapifolder optional mapifolder resource, used if available
256
	 *
257
	 * @return null|array
258
	 */
259
	public function getCalendarObject($calendarId, $objectUri, $mapifolder = null) {
260
		$this->logger->trace("calendarId: %s - objectUri: %s - mapifolder: %s", $calendarId, $objectUri, $mapifolder);
261
262
		if (!$mapifolder) {
0 ignored issues
show
introduced by
$mapifolder is of type null|resource, thus it always evaluated to false.
Loading history...
263
			$mapifolder = $this->gDavBackend->GetMapiFolder($calendarId);
264
		}
265
266
		$mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, $mapifolder, static::FILE_EXTENSION);
267
		if (!$mapimessage) {
268
			$this->logger->info("Object NOT FOUND");
269
270
			return null;
271
		}
272
273
		$realId = $this->gDavBackend->GetIdOfMapiMessage($calendarId, $mapimessage);
274
275
		// this should be cached or moved to gDavBackend
276
		$session = $this->gDavBackend->GetSession();
277
		$ab = $this->gDavBackend->GetAddressBook();
278
279
		$ics = mapi_mapitoical($session, $ab, $mapimessage, []);
0 ignored issues
show
Bug introduced by
The function mapi_mapitoical was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

279
		$ics = /** @scrutinizer ignore-call */ mapi_mapitoical($session, $ab, $mapimessage, []);
Loading history...
280
		if (!$ics && mapi_last_hresult()) {
281
			$this->logger->error("Error generating ical, error code: 0x%08X", mapi_last_hresult());
282
			$ics = null;
283
		}
284
		elseif (!$ics) {
285
			$this->logger->error("Error generating ical, unknown error");
286
			$ics = null;
287
		}
288
289
		$props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]);
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PR_LAST_MODIFICATION_TIME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
290
291
		$r = [
292
			'id' => $realId,
293
			'uri' => $realId . static::FILE_EXTENSION,
294
			'etag' => '"' . $props[PR_LAST_MODIFICATION_TIME] . '"',
295
			'lastmodified' => $props[PR_LAST_MODIFICATION_TIME],
296
			'calendarid' => $calendarId,
297
			'size' => ($ics !== null ? strlen($ics) : 0),
298
			'calendardata' => ($ics !== null ? $ics : ''),
299
		];
300
		$this->logger->trace("returned data id: %s - size: %d - etag: %s", $r['id'], $r['size'], $r['etag']);
301
302
		return $r;
303
	}
304
305
	/**
306
	 * Creates a new calendar object.
307
	 *
308
	 * The object uri is only the basename, or filename and not a full path.
309
	 *
310
	 * It is possible return an etag from this function, which will be used in
311
	 * the response to this PUT request. Note that the ETag must be surrounded
312
	 * by double-quotes.
313
	 *
314
	 * However, you should only really return this ETag if you don't mangle the
315
	 * calendar-data. If the result of a subsequent GET to this object is not
316
	 * the exact same as this request body, you should omit the ETag.
317
	 *
318
	 * @param mixed  $calendarId
319
	 * @param string $objectUri
320
	 * @param string $calendarData
321
	 *
322
	 * @return null|string
323
	 */
324
	public function createCalendarObject($calendarId, $objectUri, $calendarData) {
325
		$this->logger->trace("calendarId: %s - objectUri: %s", $calendarId, $objectUri);
326
		$objectId = $this->gDavBackend->GetObjectIdFromObjectUri($objectUri, static::FILE_EXTENSION);
327
		$folder = $this->gDavBackend->GetMapiFolder($calendarId);
328
		$mapimessage = $this->gDavBackend->CreateObject($calendarId, $folder, $objectId);
329
		$retval = $this->setData($calendarId, $mapimessage, $calendarData);
330
		if (!$retval) {
331
			return null;
332
		}
333
334
		return '"' . $retval . '"';
335
	}
336
337
	/**
338
	 * Updates an existing calendarobject, based on its uri.
339
	 *
340
	 * The object uri is only the basename, or filename and not a full path.
341
	 *
342
	 * It is possible return an etag from this function, which will be used in
343
	 * the response to this PUT request. Note that the ETag must be surrounded
344
	 * by double-quotes.
345
	 *
346
	 * However, you should only really return this ETag if you don't mangle the
347
	 * calendar-data. If the result of a subsequent GET to this object is not
348
	 * the exact same as this request body, you should omit the ETag.
349
	 *
350
	 * @param mixed  $calendarId
351
	 * @param string $objectUri
352
	 * @param string $calendarData
353
	 *
354
	 * @return null|string
355
	 */
356
	public function updateCalendarObject($calendarId, $objectUri, $calendarData) {
357
		$this->logger->trace("calendarId: %s - objectUri: %s", $calendarId, $objectUri);
358
359
		$folder = $this->gDavBackend->GetMapiFolder($calendarId);
0 ignored issues
show
Unused Code introduced by
The assignment to $folder is dead and can be removed.
Loading history...
360
		$mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, null, static::FILE_EXTENSION);
361
		$retval = $this->setData($calendarId, $mapimessage, $calendarData);
362
		if (!$retval) {
363
			return null;
364
		}
365
366
		return '"' . $retval . '"';
367
	}
368
369
	/**
370
	 * Sets data for a calendar item.
371
	 *
372
	 * @param mixed  $calendarId
373
	 * @param mixed  $mapimessage
374
	 * @param string $ics
375
	 *
376
	 * @return null|string
377
	 */
378
	private function setData($calendarId, $mapimessage, $ics) {
379
		// this should be cached or moved to gDavBackend
380
		$store = $this->gDavBackend->GetStoreById($calendarId);
381
		$session = $this->gDavBackend->GetSession();
382
		$ab = $this->gDavBackend->GetAddressBook();
383
384
		// Evolution sends daylight/standard information in the ical data
385
		// and some values are not supported by Outlook/Exchange.
386
		// Strip that data and leave only the last occurrences of
387
		// daylight/standard information.
388
		// @see GRAM-52
389
390
		$xLicLocation = stripos($ics, 'X-LIC-LOCATION:');
391
		if (($xLicLocation !== false) &&
392
				(
393
					substr_count($ics, 'BEGIN:DAYLIGHT', $xLicLocation) > 0 ||
394
					substr_count($ics, 'BEGIN:STANDARD', $xLicLocation) > 0
395
				)) {
396
			$firstDaytime = stripos($ics, 'BEGIN:DAYLIGHT', $xLicLocation);
397
			$firstStandard = stripos($ics, 'BEGIN:STANDARD', $xLicLocation);
398
399
			$lastDaytime = strripos($ics, 'BEGIN:DAYLIGHT', $xLicLocation);
400
			$lastStandard = strripos($ics, 'BEGIN:STANDARD', $xLicLocation);
401
402
			// the first part of ics until the first piece of standard/daytime information
403
			$cutStart = $firstDaytime < $firstStandard ? $firstDaytime : $firstStandard;
404
405
			if ($lastDaytime > $lastStandard) {
406
				// the part of the ics with the last piece of standard/daytime information
407
				$cutEnd = $lastDaytime;
408
409
				// the positions of the last piece of standard information
410
				$cut1 = $lastStandard;
411
				$cut2 = strripos($ics, 'END:STANDARD', $lastStandard) + 14; // strlen('END:STANDARD')
412
			}
413
			else {
414
				// the part of the ics with the last piece of standard/daytime information
415
				$cutEnd = $lastStandard;
416
417
				// the positions of the last piece of daylight information
418
				$cut1 = $lastDaytime;
419
				$cut2 = strripos($ics, 'END:DAYLIGHT', $lastDaytime) + 14; // strlen('END:DAYLIGHT')
420
			}
421
422
			$ics = substr($ics, 0, $cutStart) . substr($ics, $cut1, $cut2 - $cut1) . substr($ics, $cutEnd);
423
			$this->logger->trace("newics: %s", $ics);
424
		}
425
426
		$ok = mapi_icaltomapi($session, $store, $ab, $mapimessage, $ics, false);
0 ignored issues
show
Bug introduced by
The function mapi_icaltomapi was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

426
		$ok = /** @scrutinizer ignore-call */ mapi_icaltomapi($session, $store, $ab, $mapimessage, $ics, false);
Loading history...
427
		if (!$ok && mapi_last_hresult()) {
428
			$this->logger->error("Error updating mapi object, error code: 0x%08X", mapi_last_hresult());
429
430
			return null;
431
		}
432
		if (!$ok) {
433
			$this->logger->error("Error updating mapi object, unknown error");
434
435
			return null;
436
		}
437
438
		// Set default properties only for VEVENTs. VTODOs use different property sets.
439
		if (stripos($ics, 'BEGIN:VEVENT') !== false) {
440
			$propList = MapiProps::GetAppointmentProperties();
441
			$defaultProps = MapiProps::GetDefaultAppoinmentProperties();
442
			$propsToSet = $this->gDavBackend->GetPropsToSet($calendarId, $mapimessage, $propList, $defaultProps);
443
			if (!empty($propsToSet)) {
444
				mapi_setprops($mapimessage, $propsToSet);
445
			}
446
		}
447
448
		mapi_savechanges($mapimessage);
449
		$props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]);
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PR_LAST_MODIFICATION_TIME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
450
451
		return $props[PR_LAST_MODIFICATION_TIME];
452
	}
453
454
	/**
455
	 * Deletes an existing calendar object.
456
	 *
457
	 * The object uri is only the basename, or filename and not a full path.
458
	 *
459
	 * @param string $calendarId
460
	 * @param string $objectUri
461
	 */
462
	public function deleteCalendarObject($calendarId, $objectUri) {
463
		$this->logger->trace("calendarId: %s - objectUri: %s", $calendarId, $objectUri);
464
465
		$mapifolder = $this->gDavBackend->GetMapiFolder($calendarId);
466
467
		// to delete we need the PR_ENTRYID of the message
468
		// TODO move this part to GrommunioDavBackend
469
		$mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, $mapifolder, static::FILE_EXTENSION);
470
		$props = mapi_getprops($mapimessage, [PR_ENTRYID]);
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PR_ENTRYID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
471
		mapi_folder_deletemessages($mapifolder, [$props[PR_ENTRYID]]);
472
	}
473
474
	/**
475
	 * Return a single scheduling object.
476
	 *
477
	 * TODO: Add implementation.
478
	 *
479
	 * @param string $principalUri
480
	 * @param string $objectUri
481
	 *
482
	 * @return array
483
	 */
484
	public function getSchedulingObject($principalUri, $objectUri) {
485
		$this->logger->trace("principalUri: %s - objectUri: %s", $principalUri, $objectUri);
486
487
		return [];
488
	}
489
490
	/**
491
	 * Returns scheduling objects for the principal URI.
492
	 *
493
	 * TODO: Add implementation.
494
	 *
495
	 * @param string $principalUri
496
	 *
497
	 * @return array
498
	 */
499
	public function getSchedulingObjects($principalUri) {
500
		$this->logger->trace("principalUri: %s", $principalUri);
501
502
		return [];
503
	}
504
505
	/**
506
	 * Delete scheduling object.
507
	 *
508
	 * TODO: Add implementation.
509
	 *
510
	 * @param string $principalUri
511
	 * @param string $objectUri
512
	 */
513
	public function deleteSchedulingObject($principalUri, $objectUri) {
514
		$this->logger->trace("principalUri: %s - objectUri: %s", $principalUri, $objectUri);
515
	}
516
517
	/**
518
	 * Create a new scheduling object.
519
	 *
520
	 * TODO: Add implementation.
521
	 *
522
	 * @param string $principalUri
523
	 * @param string $objectUri
524
	 * @param string $objectData
525
	 */
526
	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
527
		$this->logger->trace("principalUri: %s - objectUri: %s - objectData: %s", $principalUri, $objectUri, $objectData);
528
	}
529
530
	/**
531
	 * Return CTAG for scheduling inbox.
532
	 *
533
	 * TODO: Add implementation.
534
	 *
535
	 * @param string $principalUri
536
	 *
537
	 * @return string
538
	 */
539
	public function getSchedulingInboxCtag($principalUri) {
540
		$this->logger->trace("principalUri: %s", $principalUri);
541
542
		return "empty";
543
	}
544
545
	/**
546
	 * The getChanges method returns all the changes that have happened, since
547
	 * the specified syncToken in the specified calendar.
548
	 *
549
	 * This function should return an array, such as the following:
550
	 *
551
	 * [
552
	 *   'syncToken' => 'The current synctoken',
553
	 *   'added'   => [
554
	 *      'new.txt',
555
	 *   ],
556
	 *   'modified'   => [
557
	 *      'modified.txt',
558
	 *   ],
559
	 *   'deleted' => [
560
	 *      'foo.php.bak',
561
	 *      'old.txt'
562
	 *   ]
563
	 * );
564
	 *
565
	 * The returned syncToken property should reflect the *current* syncToken
566
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
567
	 * property This is * needed here too, to ensure the operation is atomic.
568
	 *
569
	 * If the $syncToken argument is specified as null, this is an initial
570
	 * sync, and all members should be reported.
571
	 *
572
	 * The modified property is an array of nodenames that have changed since
573
	 * the last token.
574
	 *
575
	 * The deleted property is an array with nodenames, that have been deleted
576
	 * from collection.
577
	 *
578
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
579
	 * 1, you only have to report changes that happened only directly in
580
	 * immediate descendants. If it's 2, it should also include changes from
581
	 * the nodes below the child collections. (grandchildren)
582
	 *
583
	 * The $limit argument allows a client to specify how many results should
584
	 * be returned at most. If the limit is not specified, it should be treated
585
	 * as infinite.
586
	 *
587
	 * If the limit (infinite or not) is higher than you're willing to return,
588
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
589
	 *
590
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
591
	 * return null.
592
	 *
593
	 * The limit is 'suggestive'. You are free to ignore it.
594
	 *
595
	 * @param string $calendarId
596
	 * @param string $syncToken
597
	 * @param int    $syncLevel
598
	 * @param int    $limit
599
	 *
600
	 * @return array
601
	 */
602
	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
603
		$this->logger->trace("calendarId: %s - syncToken: %s - syncLevel: %d - limit: %d", $calendarId, $syncToken, $syncLevel, $limit);
604
605
		return $this->gDavBackend->Sync($calendarId, $syncToken, static::FILE_EXTENSION, $limit, ['types' => static::MESSAGE_CLASSES]);
606
	}
607
}
608