Total Complexity | 77 |
Total Lines | 573 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Plugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Plugin, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
58 | class Plugin extends \Sabre\CalDAV\Schedule\Plugin { |
||
59 | |||
60 | /** |
||
61 | * @var IConfig |
||
62 | */ |
||
63 | private $config; |
||
64 | |||
65 | /** @var ITip\Message[] */ |
||
66 | private $schedulingResponses = []; |
||
67 | |||
68 | /** @var string|null */ |
||
69 | private $pathOfCalendarObjectChange = null; |
||
70 | |||
71 | public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type'; |
||
72 | public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL'; |
||
73 | private LoggerInterface $logger; |
||
74 | |||
75 | /** |
||
76 | * @param IConfig $config |
||
77 | */ |
||
78 | public function __construct(IConfig $config, LoggerInterface $logger) { |
||
79 | $this->config = $config; |
||
80 | $this->logger = $logger; |
||
81 | } |
||
82 | |||
83 | /** |
||
84 | * Initializes the plugin |
||
85 | * |
||
86 | * @param Server $server |
||
87 | * @return void |
||
88 | */ |
||
89 | public function initialize(Server $server) { |
||
90 | parent::initialize($server); |
||
91 | $server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90); |
||
92 | $server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']); |
||
93 | $server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']); |
||
94 | } |
||
95 | |||
96 | /** |
||
97 | * Allow manual setting of the object change URL |
||
98 | * to support public write |
||
99 | * |
||
100 | * @param string $path |
||
101 | */ |
||
102 | public function setPathOfCalendarObjectChange(string $path): void { |
||
103 | $this->pathOfCalendarObjectChange = $path; |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * This method handler is invoked during fetching of properties. |
||
108 | * |
||
109 | * We use this event to add calendar-auto-schedule-specific properties. |
||
110 | * |
||
111 | * @param PropFind $propFind |
||
112 | * @param INode $node |
||
113 | * @return void |
||
114 | */ |
||
115 | public function propFind(PropFind $propFind, INode $node) { |
||
116 | if ($node instanceof IPrincipal) { |
||
117 | // overwrite Sabre/Dav's implementation |
||
118 | $propFind->handle(self::CALENDAR_USER_TYPE, function () use ($node) { |
||
119 | if ($node instanceof IProperties) { |
||
120 | $props = $node->getProperties([self::CALENDAR_USER_TYPE]); |
||
121 | |||
122 | if (isset($props[self::CALENDAR_USER_TYPE])) { |
||
123 | return $props[self::CALENDAR_USER_TYPE]; |
||
124 | } |
||
125 | } |
||
126 | |||
127 | return 'INDIVIDUAL'; |
||
128 | }); |
||
129 | } |
||
130 | |||
131 | parent::propFind($propFind, $node); |
||
132 | } |
||
133 | |||
134 | /** |
||
135 | * Returns a list of addresses that are associated with a principal. |
||
136 | * |
||
137 | * @param string $principal |
||
138 | * @return array |
||
139 | */ |
||
140 | protected function getAddressesForPrincipal($principal) { |
||
141 | $result = parent::getAddressesForPrincipal($principal); |
||
142 | |||
143 | if ($result === null) { |
||
144 | $result = []; |
||
145 | } |
||
146 | |||
147 | return $result; |
||
148 | } |
||
149 | |||
150 | /** |
||
151 | * @param RequestInterface $request |
||
152 | * @param ResponseInterface $response |
||
153 | * @param VCalendar $vCal |
||
154 | * @param mixed $calendarPath |
||
155 | * @param mixed $modified |
||
156 | * @param mixed $isNew |
||
157 | */ |
||
158 | public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) { |
||
159 | // Save the first path we get as a calendar-object-change request |
||
160 | if (!$this->pathOfCalendarObjectChange) { |
||
161 | $this->pathOfCalendarObjectChange = $request->getPath(); |
||
162 | } |
||
163 | |||
164 | parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew); |
||
165 | } |
||
166 | |||
167 | /** |
||
168 | * @inheritDoc |
||
169 | */ |
||
170 | public function scheduleLocalDelivery(ITip\Message $iTipMessage):void { |
||
171 | /** @var VEvent|null $vevent */ |
||
172 | $vevent = $iTipMessage->message->VEVENT ?? null; |
||
173 | |||
174 | // Strip VALARMs from incoming VEVENT |
||
175 | if ($vevent && isset($vevent->VALARM)) { |
||
176 | $vevent->remove('VALARM'); |
||
177 | } |
||
178 | |||
179 | parent::scheduleLocalDelivery($iTipMessage); |
||
180 | // We only care when the message was successfully delivered locally |
||
181 | // Log all possible codes returned from the parent method that mean something went wrong |
||
182 | // 3.7, 3.8, 5.0, 5.2 |
||
183 | if ($iTipMessage->scheduleStatus !== '1.2;Message delivered locally') { |
||
184 | $this->logger->debug('Message not delivered locally with status: ' . $iTipMessage->scheduleStatus); |
||
185 | return; |
||
186 | } |
||
187 | // We only care about request. reply and cancel are properly handled |
||
188 | // by parent::scheduleLocalDelivery already |
||
189 | if (strcasecmp($iTipMessage->method, 'REQUEST') !== 0) { |
||
190 | return; |
||
191 | } |
||
192 | |||
193 | // If parent::scheduleLocalDelivery set scheduleStatus to 1.2, |
||
194 | // it means that it was successfully delivered locally. |
||
195 | // Meaning that the ACL plugin is loaded and that a principal |
||
196 | // exists for the given recipient id, no need to double check |
||
197 | /** @var \Sabre\DAVACL\Plugin $aclPlugin */ |
||
198 | $aclPlugin = $this->server->getPlugin('acl'); |
||
199 | $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient); |
||
200 | $calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri); |
||
201 | if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) { |
||
202 | $this->logger->debug('Calendar user type is room or resource, not processing further'); |
||
203 | return; |
||
204 | } |
||
205 | |||
206 | $attendee = $this->getCurrentAttendee($iTipMessage); |
||
207 | if (!$attendee) { |
||
208 | $this->logger->debug('No attendee set for scheduling message'); |
||
209 | return; |
||
210 | } |
||
211 | |||
212 | // We only respond when a response was actually requested |
||
213 | $rsvp = $this->getAttendeeRSVP($attendee); |
||
214 | if (!$rsvp) { |
||
215 | $this->logger->debug('No RSVP requested for attendee ' . $attendee->getValue()); |
||
216 | return; |
||
217 | } |
||
218 | |||
219 | if (!$vevent) { |
||
220 | $this->logger->debug('No VEVENT set to process on scheduling message'); |
||
221 | return; |
||
222 | } |
||
223 | |||
224 | // We don't support autoresponses for recurrencing events for now |
||
225 | if (isset($vevent->RRULE) || isset($vevent->RDATE)) { |
||
226 | $this->logger->debug('VEVENT is a recurring event, autoresponding not supported'); |
||
227 | return; |
||
228 | } |
||
229 | |||
230 | $dtstart = $vevent->DTSTART; |
||
231 | $dtend = $this->getDTEndFromVEvent($vevent); |
||
232 | $uid = $vevent->UID->getValue(); |
||
233 | $sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->getValue() : 0; |
||
234 | $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : ''; |
||
235 | |||
236 | $message = <<<EOF |
||
237 | BEGIN:VCALENDAR |
||
238 | PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN |
||
239 | METHOD:REPLY |
||
240 | VERSION:2.0 |
||
241 | BEGIN:VEVENT |
||
242 | ATTENDEE;PARTSTAT=%s:%s |
||
243 | ORGANIZER:%s |
||
244 | UID:%s |
||
245 | SEQUENCE:%s |
||
246 | REQUEST-STATUS:2.0;Success |
||
247 | %sEND:VEVENT |
||
248 | END:VCALENDAR |
||
249 | EOF; |
||
250 | |||
251 | if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->getDateTime(), $dtend->getDateTime(), $uid)) { |
||
252 | $partStat = 'ACCEPTED'; |
||
253 | } else { |
||
254 | $partStat = 'DECLINED'; |
||
255 | } |
||
256 | |||
257 | $vObject = Reader::read(vsprintf($message, [ |
||
258 | $partStat, |
||
259 | $iTipMessage->recipient, |
||
260 | $iTipMessage->sender, |
||
261 | $uid, |
||
262 | $sequence, |
||
263 | $recurrenceId |
||
264 | ])); |
||
265 | |||
266 | $responseITipMessage = new ITip\Message(); |
||
267 | $responseITipMessage->uid = $uid; |
||
268 | $responseITipMessage->component = 'VEVENT'; |
||
269 | $responseITipMessage->method = 'REPLY'; |
||
270 | $responseITipMessage->sequence = $sequence; |
||
271 | $responseITipMessage->sender = $iTipMessage->recipient; |
||
272 | $responseITipMessage->recipient = $iTipMessage->sender; |
||
273 | $responseITipMessage->message = $vObject; |
||
274 | |||
275 | // We can't dispatch them now already, because the organizers calendar-object |
||
276 | // was not yet created. Hence Sabre/DAV won't find a calendar-object, when we |
||
277 | // send our reply. |
||
278 | $this->schedulingResponses[] = $responseITipMessage; |
||
279 | } |
||
280 | |||
281 | /** |
||
282 | * @param string $uri |
||
283 | */ |
||
284 | public function dispatchSchedulingResponses(string $uri):void { |
||
285 | if ($uri !== $this->pathOfCalendarObjectChange) { |
||
286 | return; |
||
287 | } |
||
288 | |||
289 | foreach ($this->schedulingResponses as $schedulingResponse) { |
||
290 | $this->scheduleLocalDelivery($schedulingResponse); |
||
291 | } |
||
292 | } |
||
293 | |||
294 | /** |
||
295 | * Always use the personal calendar as target for scheduled events |
||
296 | * |
||
297 | * @param PropFind $propFind |
||
298 | * @param INode $node |
||
299 | * @return void |
||
300 | */ |
||
301 | public function propFindDefaultCalendarUrl(PropFind $propFind, INode $node) { |
||
302 | if ($node instanceof IPrincipal) { |
||
303 | $propFind->handle(self::SCHEDULE_DEFAULT_CALENDAR_URL, function () use ($node) { |
||
304 | /** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */ |
||
305 | $caldavPlugin = $this->server->getPlugin('caldav'); |
||
306 | $principalUrl = $node->getPrincipalUrl(); |
||
307 | |||
308 | $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); |
||
309 | if (!$calendarHomePath) { |
||
310 | return null; |
||
311 | } |
||
312 | |||
313 | $isResourceOrRoom = strpos($principalUrl, 'principals/calendar-resources') === 0 || |
||
314 | strpos($principalUrl, 'principals/calendar-rooms') === 0; |
||
315 | |||
316 | if (strpos($principalUrl, 'principals/users') === 0) { |
||
317 | [, $userId] = split($principalUrl); |
||
318 | $uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI); |
||
319 | $displayName = CalDavBackend::PERSONAL_CALENDAR_NAME; |
||
320 | } elseif ($isResourceOrRoom) { |
||
321 | $uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI; |
||
322 | $displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME; |
||
323 | } else { |
||
324 | // How did we end up here? |
||
325 | // TODO - throw exception or just ignore? |
||
326 | return null; |
||
327 | } |
||
328 | |||
329 | /** @var CalendarHome $calendarHome */ |
||
330 | $calendarHome = $this->server->tree->getNodeForPath($calendarHomePath); |
||
331 | $currentCalendarDeleted = false; |
||
332 | if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) { |
||
333 | // If the default calendar doesn't exist |
||
334 | if ($isResourceOrRoom) { |
||
335 | // Resources or rooms can't be in the trashbin, so we're fine |
||
336 | $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName); |
||
337 | } else { |
||
338 | // And we're not handling scheduling on resource/room booking |
||
339 | $userCalendars = []; |
||
340 | /** |
||
341 | * If the default calendar of the user isn't set and the |
||
342 | * fallback doesn't match any of the user's calendar |
||
343 | * try to find the first "personal" calendar we can write to |
||
344 | * instead of creating a new one. |
||
345 | * A appropriate personal calendar to receive invites: |
||
346 | * - isn't a calendar subscription |
||
347 | * - user can write to it (no virtual/3rd-party calendars) |
||
348 | * - calendar isn't a share |
||
349 | */ |
||
350 | foreach ($calendarHome->getChildren() as $node) { |
||
351 | if ($node instanceof Calendar && !$node->isSubscription() && $node->canWrite() && !$node->isShared() && !$node->isDeleted()) { |
||
352 | $userCalendars[] = $node; |
||
353 | } |
||
354 | } |
||
355 | |||
356 | if (count($userCalendars) > 0) { |
||
357 | // Calendar backend returns calendar by calendarorder property |
||
358 | $uri = $userCalendars[0]->getName(); |
||
359 | } else { |
||
360 | // Otherwise if we have really nothing, create a new calendar |
||
361 | if ($currentCalendarDeleted) { |
||
362 | // If the calendar exists but is deleted, we need to purge it first |
||
363 | // This may cause some issues in a non synchronous database setup |
||
364 | $calendar = $this->getCalendar($calendarHome, $uri); |
||
365 | if ($calendar instanceof Calendar) { |
||
366 | $calendar->disableTrashbin(); |
||
367 | $calendar->delete(); |
||
368 | } |
||
369 | } |
||
370 | $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName); |
||
371 | } |
||
372 | } |
||
373 | } |
||
374 | |||
375 | $result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1); |
||
376 | if (empty($result)) { |
||
377 | return null; |
||
378 | } |
||
379 | |||
380 | return new LocalHref($result[0]['href']); |
||
381 | }); |
||
382 | } |
||
383 | } |
||
384 | |||
385 | /** |
||
386 | * Returns a list of addresses that are associated with a principal. |
||
387 | * |
||
388 | * @param string $principal |
||
389 | * @return string|null |
||
390 | */ |
||
391 | protected function getCalendarUserTypeForPrincipal($principal):?string { |
||
392 | $calendarUserType = '{' . self::NS_CALDAV . '}calendar-user-type'; |
||
393 | $properties = $this->server->getProperties( |
||
394 | $principal, |
||
395 | [$calendarUserType] |
||
396 | ); |
||
397 | |||
398 | // If we can't find this information, we'll stop processing |
||
399 | if (!isset($properties[$calendarUserType])) { |
||
400 | return null; |
||
401 | } |
||
402 | |||
403 | return $properties[$calendarUserType]; |
||
404 | } |
||
405 | |||
406 | /** |
||
407 | * @param ITip\Message $iTipMessage |
||
408 | * @return null|Property |
||
409 | */ |
||
410 | private function getCurrentAttendee(ITip\Message $iTipMessage):?Property { |
||
411 | /** @var VEvent $vevent */ |
||
412 | $vevent = $iTipMessage->message->VEVENT; |
||
413 | $attendees = $vevent->select('ATTENDEE'); |
||
414 | foreach ($attendees as $attendee) { |
||
415 | /** @var Property $attendee */ |
||
416 | if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) { |
||
417 | return $attendee; |
||
418 | } |
||
419 | } |
||
420 | return null; |
||
421 | } |
||
422 | |||
423 | /** |
||
424 | * @param Property|null $attendee |
||
425 | * @return bool |
||
426 | */ |
||
427 | private function getAttendeeRSVP(Property $attendee = null):bool { |
||
428 | if ($attendee !== null) { |
||
429 | $rsvp = $attendee->offsetGet('RSVP'); |
||
430 | if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) { |
||
431 | return true; |
||
432 | } |
||
433 | } |
||
434 | // RFC 5545 3.2.17: default RSVP is false |
||
435 | return false; |
||
436 | } |
||
437 | |||
438 | /** |
||
439 | * @param VEvent $vevent |
||
440 | * @return Property\ICalendar\DateTime |
||
441 | */ |
||
442 | private function getDTEndFromVEvent(VEvent $vevent):Property\ICalendar\DateTime { |
||
468 | } |
||
469 | |||
470 | /** |
||
471 | * @param string $email |
||
472 | * @param \DateTimeInterface $start |
||
473 | * @param \DateTimeInterface $end |
||
474 | * @param string $ignoreUID |
||
475 | * @return bool |
||
476 | */ |
||
477 | private function isAvailableAtTime(string $email, \DateTimeInterface $start, \DateTimeInterface $end, string $ignoreUID):bool { |
||
605 | } |
||
606 | |||
607 | /** |
||
608 | * @param string $email |
||
609 | * @return string |
||
610 | */ |
||
611 | private function stripOffMailTo(string $email): string { |
||
612 | if (stripos($email, 'mailto:') === 0) { |
||
613 | return substr($email, 7); |
||
614 | } |
||
615 | |||
616 | return $email; |
||
617 | } |
||
618 | |||
619 | private function getCalendar(CalendarHome $calendarHome, string $uri): INode { |
||
620 | return $calendarHome->getChild($uri); |
||
621 | } |
||
622 | |||
623 | private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool { |
||
626 | } |
||
627 | |||
628 | private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void { |
||
631 | ]); |
||
632 | } |
||
633 | } |
||
634 |