TipBroker.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\DAV\CalDAV;
  8. use Sabre\VObject\Component\VCalendar;
  9. use Sabre\VObject\ITip\Broker;
  10. use Sabre\VObject\ITip\Message;
  11. class TipBroker extends Broker {
  12. public $significantChangeProperties = [
  13. 'DTSTART',
  14. 'DTEND',
  15. 'DURATION',
  16. 'DUE',
  17. 'RRULE',
  18. 'RDATE',
  19. 'EXDATE',
  20. 'STATUS',
  21. 'SUMMARY',
  22. 'DESCRIPTION',
  23. 'LOCATION',
  24. ];
  25. /**
  26. * This method is used in cases where an event got updated, and we
  27. * potentially need to send emails to attendees to let them know of updates
  28. * in the events.
  29. *
  30. * We will detect which attendees got added, which got removed and create
  31. * specific messages for these situations.
  32. *
  33. * @return array
  34. */
  35. protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) {
  36. // Merging attendee lists.
  37. $attendees = [];
  38. foreach ($oldEventInfo['attendees'] as $attendee) {
  39. $attendees[$attendee['href']] = [
  40. 'href' => $attendee['href'],
  41. 'oldInstances' => $attendee['instances'],
  42. 'newInstances' => [],
  43. 'name' => $attendee['name'],
  44. 'forceSend' => null,
  45. ];
  46. }
  47. foreach ($eventInfo['attendees'] as $attendee) {
  48. if (isset($attendees[$attendee['href']])) {
  49. $attendees[$attendee['href']]['name'] = $attendee['name'];
  50. $attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
  51. $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
  52. } else {
  53. $attendees[$attendee['href']] = [
  54. 'href' => $attendee['href'],
  55. 'oldInstances' => [],
  56. 'newInstances' => $attendee['instances'],
  57. 'name' => $attendee['name'],
  58. 'forceSend' => $attendee['forceSend'],
  59. ];
  60. }
  61. }
  62. $messages = [];
  63. foreach ($attendees as $attendee) {
  64. // An organizer can also be an attendee. We should not generate any
  65. // messages for those.
  66. if ($attendee['href'] === $eventInfo['organizer']) {
  67. continue;
  68. }
  69. $message = new Message();
  70. $message->uid = $eventInfo['uid'];
  71. $message->component = 'VEVENT';
  72. $message->sequence = $eventInfo['sequence'];
  73. $message->sender = $eventInfo['organizer'];
  74. $message->senderName = $eventInfo['organizerName'];
  75. $message->recipient = $attendee['href'];
  76. $message->recipientName = $attendee['name'];
  77. // Creating the new iCalendar body.
  78. $icalMsg = new VCalendar();
  79. foreach ($calendar->select('VTIMEZONE') as $timezone) {
  80. $icalMsg->add(clone $timezone);
  81. }
  82. // If there are no instances the attendee is a part of, it means
  83. // the attendee was removed and we need to send them a CANCEL message.
  84. // Also If the meeting STATUS property was changed to CANCELLED
  85. // we need to send the attendee a CANCEL message.
  86. if (!$attendee['newInstances'] || $eventInfo['status'] === 'CANCELLED') {
  87. $message->method = $icalMsg->METHOD = 'CANCEL';
  88. $message->significantChange = true;
  89. // clone base event
  90. $event = clone $eventInfo['instances']['master'];
  91. // alter some properties
  92. unset($event->ATTENDEE);
  93. $event->add('ATTENDEE', $attendee['href'], ['CN' => $attendee['name'],]);
  94. $event->DTSTAMP = gmdate('Ymd\\THis\\Z');
  95. $event->SEQUENCE = $message->sequence;
  96. $icalMsg->add($event);
  97. } else {
  98. // The attendee gets the updated event body
  99. $message->method = $icalMsg->METHOD = 'REQUEST';
  100. // We need to find out that this change is significant. If it's
  101. // not, systems may opt to not send messages.
  102. //
  103. // We do this based on the 'significantChangeHash' which is
  104. // some value that changes if there's a certain set of
  105. // properties changed in the event, or simply if there's a
  106. // difference in instances that the attendee is invited to.
  107. $oldAttendeeInstances = array_keys($attendee['oldInstances']);
  108. $newAttendeeInstances = array_keys($attendee['newInstances']);
  109. $message->significantChange =
  110. $attendee['forceSend'] === 'REQUEST' ||
  111. count($oldAttendeeInstances) !== count($newAttendeeInstances) ||
  112. count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 ||
  113. $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
  114. foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
  115. $currentEvent = clone $eventInfo['instances'][$instanceId];
  116. if ($instanceId === 'master') {
  117. // We need to find a list of events that the attendee
  118. // is not a part of to add to the list of exceptions.
  119. $exceptions = [];
  120. foreach ($eventInfo['instances'] as $instanceId => $vevent) {
  121. if (!isset($attendee['newInstances'][$instanceId])) {
  122. $exceptions[] = $instanceId;
  123. }
  124. }
  125. // If there were exceptions, we need to add it to an
  126. // existing EXDATE property, if it exists.
  127. if ($exceptions) {
  128. if (isset($currentEvent->EXDATE)) {
  129. $currentEvent->EXDATE->setParts(array_merge(
  130. $currentEvent->EXDATE->getParts(),
  131. $exceptions
  132. ));
  133. } else {
  134. $currentEvent->EXDATE = $exceptions;
  135. }
  136. }
  137. // Cleaning up any scheduling information that
  138. // shouldn't be sent along.
  139. unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
  140. unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
  141. foreach ($currentEvent->ATTENDEE as $attendee) {
  142. unset($attendee['SCHEDULE-FORCE-SEND']);
  143. unset($attendee['SCHEDULE-STATUS']);
  144. // We're adding PARTSTAT=NEEDS-ACTION to ensure that
  145. // iOS shows an "Inbox Item"
  146. if (!isset($attendee['PARTSTAT'])) {
  147. $attendee['PARTSTAT'] = 'NEEDS-ACTION';
  148. }
  149. }
  150. }
  151. $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
  152. $icalMsg->add($currentEvent);
  153. }
  154. }
  155. $message->message = $icalMsg;
  156. $messages[] = $message;
  157. }
  158. return $messages;
  159. }
  160. }