|
10 | 10 | namespace OCA\DAV\CalDAV\Schedule; |
11 | 11 |
|
12 | 12 | use OCA\DAV\CalDAV\CalendarObject; |
13 | | -use OCA\DAV\CalDAV\EventComparisonService; |
14 | 13 | use OCP\Accounts\IAccountManager; |
15 | 14 | use OCP\AppFramework\Utility\ITimeFactory; |
16 | 15 | use OCP\Defaults; |
@@ -65,7 +64,6 @@ public function __construct( |
65 | 64 | private Defaults $defaults, |
66 | 65 | private IUserSession $userSession, |
67 | 66 | private IMipService $imipService, |
68 | | - private EventComparisonService $eventComparisonService, |
69 | 67 | private IMailManager $mailManager, |
70 | 68 | private IEmailValidator $emailValidator, |
71 | 69 | private IAccountManager $accountManager, |
@@ -146,29 +144,82 @@ public function schedule(Message $iTipMessage) { |
146 | 144 |
|
147 | 145 | $recipientName = $iTipMessage->recipientName ? (string)$iTipMessage->recipientName : null; |
148 | 146 |
|
149 | | - $newEvents = $iTipMessage->message; |
150 | | - $oldEvents = $this->getVCalendar(); |
| 147 | + $newObjects = $iTipMessage->message; |
| 148 | + $oldObjects = $this->getVCalendar(); |
| 149 | + |
| 150 | + $method = match (strtolower($iTipMessage->method)) { |
| 151 | + 'reply' => self::METHOD_REPLY, |
| 152 | + 'cancel' => self::METHOD_CANCEL, |
| 153 | + default => self::METHOD_REQUEST, |
| 154 | + }; |
| 155 | + |
| 156 | + // For REQUEST method, we need to determine which instances have changed by comparing new and old events |
| 157 | + if ($method === self::METHOD_REQUEST) { |
| 158 | + $newEvents = $this->imipService->eventInstances($newObjects); |
| 159 | + $oldEventsByInstance = []; |
| 160 | + if ($oldObjects !== null) { |
| 161 | + foreach ($this->imipService->eventInstances($oldObjects) as $oldEvent) { |
| 162 | + $oldEventsByInstance[$this->imipService->instanceKey($oldEvent)] = $oldEvent; |
| 163 | + } |
| 164 | + } |
151 | 165 |
|
152 | | - $modified = $this->eventComparisonService->findModified($newEvents, $oldEvents); |
153 | | - /** @var VEvent $vEvent */ |
154 | | - $vEvent = array_pop($modified['new']); |
155 | | - /** @var VEvent $oldVevent */ |
156 | | - $oldVevent = !empty($modified['old']) && is_array($modified['old']) ? array_pop($modified['old']) : null; |
157 | | - $isModified = isset($oldVevent); |
| 166 | + $allInstances = array_map( |
| 167 | + fn (VEvent $newEvent) => ['new' => $newEvent, 'old' => $oldEventsByInstance[$this->imipService->instanceKey($newEvent)] ?? null], |
| 168 | + $newEvents, |
| 169 | + ); |
158 | 170 |
|
159 | | - // No changed events after all - this shouldn't happen if there is significant change yet here we are |
| 171 | + $changedInstances = array_values(array_filter( |
| 172 | + $allInstances, |
| 173 | + fn (array $pair) => $pair['old'] === null || $this->imipService->diffInstance($pair['new'], $pair['old']) !== [], |
| 174 | + )); |
| 175 | + |
| 176 | + $modifiedInstances = $changedInstances !== [] ? $changedInstances : $allInstances; |
| 177 | + } |
| 178 | + // For CANCEL/REPLY only ever consume the new instance, so there's no old instance to diff against or compare |
| 179 | + else { |
| 180 | + $modifiedInstances = array_map( |
| 181 | + static fn (VEvent $event) => ['new' => $event, 'old' => null], |
| 182 | + $this->imipService->eventInstances($newObjects), |
| 183 | + ); |
| 184 | + } |
| 185 | + |
| 186 | + // No VEvents in the message at all - this shouldn't happen if there is significant change yet here we are |
160 | 187 | // The scheduling status is debatable |
161 | | - if (empty($vEvent)) { |
162 | | - $this->logger->warning('iTip message said the change was significant but comparison did not detect any updated VEvents'); |
| 188 | + if (empty($modifiedInstances)) { |
| 189 | + $this->logger->warning('iTip message said the change was significant but the message contained no VEvents'); |
163 | 190 | $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; |
164 | 191 | return; |
165 | 192 | } |
166 | 193 |
|
167 | | - // we (should) have one event component left |
168 | | - // as the ITip\Broker creates one iTip message per change |
169 | | - // and triggers the "schedule" event once per message |
170 | | - // we also might not have an old event as this could be a new |
171 | | - // invitation, or a new recurrence exception |
| 194 | + if (count($modifiedInstances) > 1) { |
| 195 | + $this->logger->debug('iTip message contains multiple instances; only the primary instance is reflected in the invitation email', [ |
| 196 | + 'uid' => $iTipMessage->uid, |
| 197 | + 'instanceCount' => count($modifiedInstances), |
| 198 | + ]); |
| 199 | + } |
| 200 | + |
| 201 | + // we (should) have one instance per message, as the ITip\Broker creates |
| 202 | + // one iTip message per attendee and triggers the "schedule" event once |
| 203 | + // per message; a message can still bundle several instances (e.g. |
| 204 | + // primary + overrides edited together), in which case only the primary |
| 205 | + // instance is reflected in the email for now |
| 206 | + $primaryInstance = null; |
| 207 | + foreach ($modifiedInstances as $instance) { |
| 208 | + if (!isset($instance['new']->{'RECURRENCE-ID'})) { |
| 209 | + $primaryInstance = $instance; |
| 210 | + break; |
| 211 | + } |
| 212 | + } |
| 213 | + $primaryInstance ??= $modifiedInstances[0]; |
| 214 | + |
| 215 | + /** @var VEvent $vEvent */ |
| 216 | + $vEvent = $primaryInstance['new']; |
| 217 | + /** @var VEvent|null $oldVevent */ |
| 218 | + $oldVevent = $primaryInstance['old']; |
| 219 | + $isModified = $oldVevent !== null; |
| 220 | + |
| 221 | + // we might not have an old event as this could be a new invitation, |
| 222 | + // or a new recurrence exception |
172 | 223 | $attendee = $this->imipService->getCurrentAttendee($iTipMessage); |
173 | 224 | if ($attendee === null) { |
174 | 225 | $uid = $vEvent->UID ?? 'no UID found'; |
@@ -199,18 +250,15 @@ public function schedule(Message $iTipMessage) { |
199 | 250 | } |
200 | 251 |
|
201 | 252 | $replyingAttendee = null; |
202 | | - switch (strtolower($iTipMessage->method)) { |
| 253 | + switch ($method) { |
203 | 254 | case self::METHOD_REPLY: |
204 | | - $method = self::METHOD_REPLY; |
205 | 255 | $data = $this->imipService->buildReplyBodyData($vEvent); |
206 | 256 | $replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage); |
207 | 257 | break; |
208 | 258 | case self::METHOD_CANCEL: |
209 | | - $method = self::METHOD_CANCEL; |
210 | 259 | $data = $this->imipService->buildCancelledBodyData($vEvent); |
211 | 260 | break; |
212 | 261 | default: |
213 | | - $method = self::METHOD_REQUEST; |
214 | 262 | $data = $this->imipService->buildBodyData($vEvent, $oldVevent); |
215 | 263 | break; |
216 | 264 | } |
|
0 commit comments