aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/CalDAV/Plugin.php
blob: 1b257ae21a7028620996ac958c38bfbfe06b89d8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
<?php

/**
 * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OCA\DAV\CalDAV;

use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;

class Plugin extends \Sabre\CalDAV\Plugin {
	public const SYSTEM_CALENDAR_ROOT = 'system-calendars';

	/**
	 * Initializes the plugin
	 *
	 * @param Server $server
	 * @return void
	 */
	public function initialize(Server $server) {

		parent::initialize($server);

		$server->on('calendarObjectChange', [$this, 'calendarObjectChange'], 90);
		
	}

	/**
	 * Returns the path to a principal's calendar home.
	 *
	 * The return url must not end with a slash.
	 * This function should return null in case a principal did not have
	 * a calendar home.
	 *
	 * @param string $principalUrl
	 * @return string|null
	 */
	public function getCalendarHomeForPrincipal($principalUrl) {
		if (strrpos($principalUrl, 'principals/users', -strlen($principalUrl)) !== false) {
			[, $principalId] = \Sabre\Uri\split($principalUrl);
			return self::CALENDAR_ROOT . '/' . $principalId;
		}
		if (strrpos($principalUrl, 'principals/calendar-resources', -strlen($principalUrl)) !== false) {
			[, $principalId] = \Sabre\Uri\split($principalUrl);
			return self::SYSTEM_CALENDAR_ROOT . '/calendar-resources/' . $principalId;
		}
		if (strrpos($principalUrl, 'principals/calendar-rooms', -strlen($principalUrl)) !== false) {
			[, $principalId] = \Sabre\Uri\split($principalUrl);
			return self::SYSTEM_CALENDAR_ROOT . '/calendar-rooms/' . $principalId;
		}
	}

	/**
	 * @param RequestInterface $request
	 * @param ResponseInterface $response
	 * @param VCalendar $vCal
	 * @param mixed $calendarPath
	 * @param mixed $modified
	 * @param mixed $isNew
	 */
	public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $alteredObject, $calendarPath, &$modified, $isNew) {
		
		// determine if the calendar has an event
		// if there is no event there is nothing to do
		if (!$alteredObject->VEVENT) {
			return;
		}
		// determine if altered calendar event is a new event
		// if calendar event is new sanitize and return
		if ($isNew) {
			$this->sanitizeCreatedInstance($alteredObject->VEVENT, $modified);
			return;
		}
		// retrieve current calendar event node
		/** @var \OCA\DAV\CalDAV\CalendarObject $currentNode */
		$currentNode = $this->server->tree->getNodeForPath($request->getPath());
		// convert calendar event string data to VCalendar object
		/** @var \Sabre\VObject\Component\VCalendar $currentObject */
		$currentObject = Reader::read($currentNode->get());
		// find what has changed (base, recurrence, both) between altered and current calendar event
		$delta = $this->findEventInstanceDelta($alteredObject->VEVENT, $currentObject->VEVENT);
		//
		foreach ($delta as $entry) {
			// determine if this instance was created or updated
			if ($entry['current'] !== null) {
				$this->sanitizeUpdatedInstance($entry['altered'], $entry['current'], $modified);
			} else {
				$this->sanitizeCreatedInstance($entry['altered'], $modified);
			}
		}

	}

	public function sanitizeCreatedInstance(VEvent $altered, $modified): void {
		
		// sanitize attendees
		if (isset($altered->ATTENDEE)) {
			$this->sanitizeEventAttendees($altered, $modified);
		}

	}

	public function sanitizeUpdatedInstance(VEvent $altered, VEvent $current, $modified): void {
		
		// find differences in properties
		$delta = $this->findEventPropertyDelta($altered, $current, $modified);
		// determine if any important properties have changed sanitize attendees
		if (isset($delta['DTSTART']) || isset($delta['DTEND']) || isset($delta['LOCATION']) || isset($delta['RRULE'])) {
			$this->sanitizeEventAttendees($altered, $modified);
		}

	}

	public function sanitizeEventAttendees(VEvent $event, $modified): void {
		
		// iterate thought attendees
		foreach ($event->ATTENDEE as $id => $entry) {
			// determine attendee participation status
			// if status is missing or NOT set correctly change the status
			if (!isset($entry['PARTSTAT']) || $entry['PARTSTAT']->getValue() !== 'NEEDS-ACTION') {
				$event->ATTENDEE[$id]['PARTSTAT']->setValue('NEEDS-ACTION');
				$modified = true;
			}
		}

	}

	protected function findEventInstanceDelta(VEvent $altered, VEvent $current): array {
		
		$list = [];
		// iterate through altered event instances
		foreach ($altered as $event) {
			// create instance id
			if (!isset($event->{'RECURRENCE-ID'})) {
				$id = $event->UID->getValue() . ':Base';
			} else {
				$id = $event->UID->getValue() . ':' . $event->{'RECURRENCE-ID'}->getValue();
			}
			// add instance to list
			$list[$id] = ['altered' => $event, 'current' => null];
		}
		// iterate through current event instances
		foreach ($current as $event) {
			// create instance id
			if (!isset($event->{'RECURRENCE-ID'})) {
				$id = $event->UID->getValue() . ':Base';
			} else {
				$id = $event->UID->getValue() . ':' . $event->{'RECURRENCE-ID'}->getValue();
			}
			// determine if id exists in list
			if (isset($list[$id])) {
				// compare altered instance to current instance
				if ($list[$id]['altered']->{'LAST-MODIFIED'}->getValue() == $event->{'LAST-MODIFIED'}->getValue() &&
					$list[$id]['altered']->SEQUENCE->getValue() == $event->SEQUENCE->getValue()) {
					// remove entry from list if instance has not changed
					unset($list[$id]);
				} else {
					// update entry in list with current instance
					$list[$id]['current'] = $event;
				}
			} else {
				// add entry to list
				$list[$id] = ['altered' => null, 'current' => $event];
			}
		}

		return $list;

	}

	protected function findEventPropertyDelta(VEvent $altered, VEvent $current): array {
		
		$list = [];
		// iterate through altered event properties
		foreach ($altered->children() as $property) {
			// add property to list
			$list[$property->name] = ['altered' => $property->getValue(), 'current' => null];
		}
		// iterate through altered event properties
		foreach ($current->children() as $property) {
			if (isset($list[$property->name])) {
				if ($list[$property->name]['altered'] == $property->getValue()) {
					// remove entry from list if instance has not changed
					unset($list[$property->name]);
				} else {
					// update entry in list with current instance
					$list[$property->name]['current'] = $property->getValue();
				}
			} else {
				// add entry to list
				$list[$property->name] = ['altered' => null, 'current' => $property->getValue()];
			}
		}
		
		return $list;
	}

}