GrommunioCalDavBackend::getSchedulingObjects()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

427
		$ok = /** @scrutinizer ignore-call */ mapi_icaltomapi($session, $store, $ab, $mapimessage, $ics, false);
Loading history...
428
		if (!$ok && mapi_last_hresult()) {
429
			$this->logger->error("Error updating mapi object, error code: 0x%08X", mapi_last_hresult());
430
431
			return null;
432
		}
433
		if (!$ok) {
434
			$this->logger->error("Error updating mapi object, unknown error");
435
436
			return null;
437
		}
438
439
		if (stripos($ics, 'BEGIN:VTODO') !== false) {
440
			$this->applyVtodoSpecificProperties($store, $mapimessage, $ics);
441
		}
442
443
		// Set default properties only for VEVENTs. VTODOs use different property sets.
444
		if (stripos($ics, 'BEGIN:VEVENT') !== false) {
445
			$propList = MapiProps::GetAppointmentProperties();
446
			$defaultProps = MapiProps::GetDefaultAppoinmentProperties();
447
			$propsToSet = $this->gDavBackend->GetPropsToSet($calendarId, $mapimessage, $propList, $defaultProps);
448
			if (!empty($propsToSet)) {
449
				mapi_setprops($mapimessage, $propsToSet);
450
			}
451
		}
452
453
		mapi_savechanges($mapimessage);
454
		$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...
455
456
		return $props[PR_LAST_MODIFICATION_TIME];
457
	}
458
459
	/**
460
	 * Ensures VTODO-specific fields such as start date, due date, priority and estimated duration
461
	 * are populated even when mapi_icaltomapi leaves them unset.
462
	 *
463
	 * @param mixed  $store
464
	 * @param mixed  $mapimessage
465
	 * @param string $ics
466
	 */
467
	private function applyVtodoSpecificProperties($store, $mapimessage, $ics) {
468
		try {
469
			$vcalendar = Reader::read($ics);
470
		}
471
		catch (\Throwable $throwable) {
472
			$this->logger->debug("Unable to parse VTODO data for supplemental property mapping: %s", $throwable->getMessage());
473
474
			return;
475
		}
476
477
		$vtodos = $vcalendar->select('VTODO');
478
		if (empty($vtodos)) {
479
			return;
480
		}
481
482
		/** @var \Sabre\VObject\Component\VTodo $vtodo */
483
		$vtodo = reset($vtodos);
484
485
		$propMap = getPropIdsFromStrings($store, [
0 ignored issues
show
Bug introduced by
The function getPropIdsFromStrings 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

485
		$propMap = /** @scrutinizer ignore-call */ getPropIdsFromStrings($store, [
Loading history...
486
			'taskStart' => 'PT_SYSTIME:PSETID_Task:' . PidLidTaskStartDate,
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PidLidTaskStartDate was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
487
			'taskDue' => 'PT_SYSTIME:PSETID_Task:' . PidLidTaskDueDate,
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PidLidTaskDueDate was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
488
			'commonStart' => 'PT_SYSTIME:PSETID_Common:' . PidLidCommonStart,
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PidLidCommonStart was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
489
			'commonEnd' => 'PT_SYSTIME:PSETID_Common:' . PidLidCommonEnd,
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PidLidCommonEnd was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
490
			'estimatedEffort' => 'PT_LONG:PSETID_Task:' . PidLidTaskEstimatedEffort,
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PidLidTaskEstimatedEffort was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
491
		]);
492
493
		$propsToUpdate = [];
494
495
		if (isset($vtodo->DTSTART)) {
496
			$timestamp = $this->timestampFromVObjectProperty($vtodo->DTSTART);
497
			if ($timestamp !== null) {
498
				if (isset($propMap['taskStart'])) {
499
					$propsToUpdate[$propMap['taskStart']] = $timestamp;
500
				}
501
				if (isset($propMap['commonStart'])) {
502
					$propsToUpdate[$propMap['commonStart']] = $timestamp;
503
				}
504
			}
505
		}
506
507
		if (isset($vtodo->DUE)) {
508
			$timestamp = $this->timestampFromVObjectProperty($vtodo->DUE);
509
			if ($timestamp !== null) {
510
				if (isset($propMap['taskDue'])) {
511
					$propsToUpdate[$propMap['taskDue']] = $timestamp;
512
				}
513
				if (isset($propMap['commonEnd'])) {
514
					$propsToUpdate[$propMap['commonEnd']] = $timestamp;
515
				}
516
			}
517
		}
518
519
		if (isset($vtodo->{'ESTIMATED-DURATION'})) {
520
			$minutes = $this->minutesFromIsoDuration($vtodo->{'ESTIMATED-DURATION'}->getValue());
521
			if ($minutes !== null && isset($propMap['estimatedEffort'])) {
522
				$propsToUpdate[$propMap['estimatedEffort']] = $minutes;
523
			}
524
		}
525
526
		if (isset($vtodo->PRIORITY)) {
527
			$priorityMapping = $this->mapPriorityValues($vtodo->PRIORITY->getValue());
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

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

527
			$priorityMapping = $this->mapPriorityValues($vtodo->PRIORITY->/** @scrutinizer ignore-call */ getValue());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
528
			if ($priorityMapping !== null) {
529
				if (isset($priorityMapping['importance'])) {
530
					$propsToUpdate[PR_IMPORTANCE] = $priorityMapping['importance'];
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PR_IMPORTANCE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
531
				}
532
				if (isset($priorityMapping['priority'])) {
533
					$propsToUpdate[PR_PRIORITY] = $priorityMapping['priority'];
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PR_PRIORITY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
534
				}
535
			}
536
		}
537
538
		if (!empty($propsToUpdate)) {
539
			mapi_setprops($mapimessage, $propsToUpdate);
540
		}
541
	}
542
543
	/**
544
	 * Converts a VObject datetime property into a UTC timestamp suitable for PT_SYSTIME fields.
545
	 *
546
	 * @param \Sabre\VObject\Property $property
547
	 *
548
	 * @return int|null
549
	 */
550
	private function timestampFromVObjectProperty($property) {
551
		if (!$property instanceof \Sabre\VObject\Property) {
0 ignored issues
show
introduced by
$property is always a sub-type of Sabre\VObject\Property.
Loading history...
552
			return null;
553
		}
554
555
		try {
556
			if ($property instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
557
				$dateTime = $property->getDateTime(new \DateTimeZone('UTC'));
558
			}
559
			else {
560
				$dateTime = new \DateTimeImmutable((string) $property, new \DateTimeZone('UTC'));
561
			}
562
		}
563
		catch (\Throwable $throwable) {
564
			$this->logger->debug("Unable to interpret iCalendar date value: %s", $throwable->getMessage());
565
566
			return null;
567
		}
568
569
		return (int) $dateTime->getTimestamp();
570
	}
571
572
	/**
573
	 * Converts an ISO-8601 duration string into minutes as expected by PidLidTaskEstimatedEffort.
574
	 *
575
	 * @param string $duration
576
	 *
577
	 * @return int|null
578
	 */
579
	private function minutesFromIsoDuration($duration) {
580
		$duration = trim((string) $duration);
581
		if ($duration === '') {
582
			return null;
583
		}
584
585
		$rawDuration = $duration;
586
		$isNegative = false;
587
		if ($duration[0] === '-') {
588
			$isNegative = true;
589
			$duration = substr($duration, 1);
590
		}
591
592
		try {
593
			$interval = new \DateInterval($duration);
594
		}
595
		catch (\Throwable $throwable) {
596
			$this->logger->debug("Unable to parse ISO duration '%s': %s", $rawDuration, $throwable->getMessage());
597
598
			return null;
599
		}
600
601
		$reference = new \DateTimeImmutable('@0');
602
		$target = $reference->add($interval);
603
		$seconds = $target->getTimestamp() - $reference->getTimestamp();
604
		$minutes = (int) round($seconds / 60);
605
606
		return $isNegative ? -$minutes : $minutes;
607
	}
608
609
	/**
610
	 * Maps RFC5545 PRIORITY values onto the MAPI importance/priority fields.
611
	 *
612
	 * @param string $priority
613
	 *
614
	 * @return array|null
615
	 */
616
	private function mapPriorityValues($priority) {
617
		if ($priority === null || $priority === '') {
618
			return null;
619
		}
620
621
		$priority = (int) $priority;
622
		if ($priority >= 1 && $priority <= 4) {
623
			return ['importance' => IMPORTANCE_HIGH, 'priority' => 1];
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\IMPORTANCE_HIGH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
624
		}
625
		if ($priority === 5) {
626
			return ['importance' => IMPORTANCE_NORMAL, 'priority' => 0];
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\IMPORTANCE_NORMAL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
627
		}
628
		if ($priority >= 6 && $priority <= 9) {
629
			return ['importance' => IMPORTANCE_LOW, 'priority' => -1];
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\IMPORTANCE_LOW was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
630
		}
631
632
		return null;
633
	}
634
635
	/**
636
	 * Deletes an existing calendar object.
637
	 *
638
	 * The object uri is only the basename, or filename and not a full path.
639
	 *
640
	 * @param string $calendarId
641
	 * @param string $objectUri
642
	 */
643
	public function deleteCalendarObject($calendarId, $objectUri) {
644
		$this->logger->trace("calendarId: %s - objectUri: %s", $calendarId, $objectUri);
645
646
		$mapifolder = $this->gDavBackend->GetMapiFolder($calendarId);
647
648
		// to delete we need the PR_ENTRYID of the message
649
		// TODO move this part to GrommunioDavBackend
650
		$mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, $mapifolder, static::FILE_EXTENSION);
651
		$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...
652
		mapi_folder_deletemessages($mapifolder, [$props[PR_ENTRYID]]);
653
	}
654
655
	/**
656
	 * Return a single scheduling object.
657
	 *
658
	 * TODO: Add implementation.
659
	 *
660
	 * @param string $principalUri
661
	 * @param string $objectUri
662
	 *
663
	 * @return array
664
	 */
665
	public function getSchedulingObject($principalUri, $objectUri) {
666
		$this->logger->trace("principalUri: %s - objectUri: %s", $principalUri, $objectUri);
667
668
		return [];
669
	}
670
671
	/**
672
	 * Returns scheduling objects for the principal URI.
673
	 *
674
	 * TODO: Add implementation.
675
	 *
676
	 * @param string $principalUri
677
	 *
678
	 * @return array
679
	 */
680
	public function getSchedulingObjects($principalUri) {
681
		$this->logger->trace("principalUri: %s", $principalUri);
682
683
		return [];
684
	}
685
686
	/**
687
	 * Delete scheduling object.
688
	 *
689
	 * TODO: Add implementation.
690
	 *
691
	 * @param string $principalUri
692
	 * @param string $objectUri
693
	 */
694
	public function deleteSchedulingObject($principalUri, $objectUri) {
695
		$this->logger->trace("principalUri: %s - objectUri: %s", $principalUri, $objectUri);
696
	}
697
698
	/**
699
	 * Create a new scheduling object.
700
	 *
701
	 * TODO: Add implementation.
702
	 *
703
	 * @param string $principalUri
704
	 * @param string $objectUri
705
	 * @param string $objectData
706
	 */
707
	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
708
		$this->logger->trace("principalUri: %s - objectUri: %s - objectData: %s", $principalUri, $objectUri, $objectData);
709
	}
710
711
	/**
712
	 * Return CTAG for scheduling inbox.
713
	 *
714
	 * TODO: Add implementation.
715
	 *
716
	 * @param string $principalUri
717
	 *
718
	 * @return string
719
	 */
720
	public function getSchedulingInboxCtag($principalUri) {
721
		$this->logger->trace("principalUri: %s", $principalUri);
722
723
		return "empty";
724
	}
725
726
	/**
727
	 * The getChanges method returns all the changes that have happened, since
728
	 * the specified syncToken in the specified calendar.
729
	 *
730
	 * This function should return an array, such as the following:
731
	 *
732
	 * [
733
	 *   'syncToken' => 'The current synctoken',
734
	 *   'added'   => [
735
	 *      'new.txt',
736
	 *   ],
737
	 *   'modified'   => [
738
	 *      'modified.txt',
739
	 *   ],
740
	 *   'deleted' => [
741
	 *      'foo.php.bak',
742
	 *      'old.txt'
743
	 *   ]
744
	 * );
745
	 *
746
	 * The returned syncToken property should reflect the *current* syncToken
747
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
748
	 * property This is * needed here too, to ensure the operation is atomic.
749
	 *
750
	 * If the $syncToken argument is specified as null, this is an initial
751
	 * sync, and all members should be reported.
752
	 *
753
	 * The modified property is an array of nodenames that have changed since
754
	 * the last token.
755
	 *
756
	 * The deleted property is an array with nodenames, that have been deleted
757
	 * from collection.
758
	 *
759
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
760
	 * 1, you only have to report changes that happened only directly in
761
	 * immediate descendants. If it's 2, it should also include changes from
762
	 * the nodes below the child collections. (grandchildren)
763
	 *
764
	 * The $limit argument allows a client to specify how many results should
765
	 * be returned at most. If the limit is not specified, it should be treated
766
	 * as infinite.
767
	 *
768
	 * If the limit (infinite or not) is higher than you're willing to return,
769
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
770
	 *
771
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
772
	 * return null.
773
	 *
774
	 * The limit is 'suggestive'. You are free to ignore it.
775
	 *
776
	 * @param string $calendarId
777
	 * @param string $syncToken
778
	 * @param int    $syncLevel
779
	 * @param int    $limit
780
	 *
781
	 * @return array
782
	 */
783
	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
784
		$this->logger->trace("calendarId: %s - syncToken: %s - syncLevel: %d - limit: %d", $calendarId, $syncToken, $syncLevel, $limit);
785
786
		return $this->gDavBackend->Sync($calendarId, $syncToken, static::FILE_EXTENSION, $limit, ['types' => static::MESSAGE_CLASSES]);
787
	}
788
}
789