@@ -563,15 +563,34 @@ public function revertMultipleUserStatus(array $userIds, string $messageId): voi
563563 $ this ->mapper ->restoreBackupStatuses ($ restoreIds );
564564 }
565565
566- private function processCalendarAvailability (string $ userId ) {
566+ /**
567+ * Calculate a users' status according to their availabilit settings and their calendar
568+ * events
569+ *
570+ * There are 4 predefined types of FBTYPE - 'FREE', 'BUSY', 'BUSY-UNAVAILABLE', 'BUSY-TENTATIVE',
571+ * but 'X-' properties are possible
572+ * @link https://icalendar.org/iCalendar-RFC-5545/3-2-9-free-busy-time-type.html
573+ *
574+ * The status will be changed for types
575+ * - 'BUSY'
576+ * - 'BUSY-UNAVAILABLE' (ex.: when a VAVILABILITY setting is in effect)
577+ * - 'BUSY-TENTATIVE' (ex.: an event has been accepted tentatively)
578+ * and all FREEBUSY components without a type (implicitly a 'BUSY' status)
579+ *
580+ * 'X-' properties are not handled for now
581+ *
582+ * @param string $userId
583+ * @return void
584+ */
585+ private function processCalendarAvailability (string $ userId ): void {
567586 $ user = $ this ->userManager ->get ($ userId );
568587 if ($ user === null ) {
569- return false ;
588+ return ;
570589 }
571590
572591 $ email = $ user ->getEMailAddress ();
573592 if ($ email === null ) {
574- return false ;
593+ return ;
575594 }
576595
577596 $ server = new InvitationResponseServer ();
@@ -598,7 +617,7 @@ private function processCalendarAvailability(string $userId) {
598617 );
599618
600619 if (!count ($ result ) || !isset ($ result [0 ][200 ][$ caldavNS .'schedule-inbox-URL ' ])) {
601- return false ;
620+ return ;
602621 }
603622
604623 $ inboxUrl = $ result [0 ][200 ][$ caldavNS .'schedule-inbox-URL ' ]->getHref ();
@@ -607,13 +626,13 @@ private function processCalendarAvailability(string $userId) {
607626 try {
608627 $ aclPlugin ->checkPrivileges ($ inboxUrl , $ caldavNS .'schedule-query-freebusy ' );
609628 } catch (NeedPrivileges | NotAuthenticated $ exception ) {
610- return false ;
629+ return ;
611630 }
612631
613632 $ calendarTimeZone = new DateTimeZone ('UTC ' );
614633 $ calendars = $ this ->calendarManager ->getCalendarsForPrincipal ('principals/users/ ' . $ userId );
615634 if (empty ($ calendars )) {
616- return false ;
635+ return ;
617636 }
618637
619638 $ query = new CalendarQuery ('principals/users/ ' . $ userId );
@@ -624,7 +643,6 @@ private function processCalendarAvailability(string $userId) {
624643 }
625644
626645 $ sct = $ calendarObjects ->getSchedulingTransparency ();
627-
628646 if (!empty ($ sct ) && ScheduleCalendarTransp::TRANSPARENT == $ sct ->getValue ()) {
629647 // If a calendar is marked as 'transparent', it means we must
630648 // ignore it for free-busy purposes.
@@ -635,7 +653,6 @@ private function processCalendarAvailability(string $userId) {
635653 if (!empty ($ ctz )) {
636654 $ vtimezoneObj = Reader::read ($ ctz );
637655 $ calendarTimeZone = $ vtimezoneObj ->VTIMEZONE ->getTimeZone ();
638-
639656 // Destroy circular references so PHP can garbage collect the object.
640657 $ vtimezoneObj ->destroy ();
641658 }
@@ -647,15 +664,18 @@ private function processCalendarAvailability(string $userId) {
647664 $ dtEnd = new \DateTimeImmutable ('+1 hours ' );
648665 $ query ->setTimerangeStart ($ dtStart );
649666 $ query ->setTimerangeEnd ($ dtEnd );
650- $ results = $ this ->calendarManager ->searchForPrincipal ($ query );
651- if (empty ($ results )) {
652- return false ;
667+ $ calendarEvents = $ this ->calendarManager ->searchForPrincipal ($ query );
668+ // @todo we can cache that
669+ $ vavilability = $ this ->mapper ->getAvailabilityFromPropertiesTable ($ userId );
670+ if (empty ($ vavilability ) && empty ($ calendarEvents )) {
671+ // No availability settings and no calendar events, we can stop here
672+ return ;
653673 }
654674
655675 $ calendarObjects = new VCalendar ();
656- foreach ($ results as $ objectInfo ) {
676+ foreach ($ calendarEvents as $ calendarEvent ) {
657677 $ vEvent = new VEvent ($ calendarObjects , 'VEVENT ' );
658- foreach ($ objectInfo ['objects ' ] as $ component ) {
678+ foreach ($ calendarEvent ['objects ' ] as $ component ) {
659679 foreach ($ component as $ key => $ value ) {
660680 $ vEvent ->add ($ key , $ value [0 ]);
661681 }
@@ -672,50 +692,56 @@ private function processCalendarAvailability(string $userId) {
672692 $ generator ->setBaseObject ($ vcalendar );
673693 $ generator ->setTimeZone ($ calendarTimeZone );
674694
675- $ vavilability = $ this ->mapper ->getAvailabilityFromPropertiesTable ($ userId );
676695 if (!empty ($ vavilability )) {
677696 $ generator ->setVAvailability (
678697 Reader::read (
679698 $ vavilability
680699 )
681700 );
682701 }
702+ // Generate the intersection of VAVILABILITY and all VEVENTS in all calendars
683703 $ result = $ generator ->getResult ();
684704
685- // We have the intersection of VAVILABILITY and all VEVENTS in all calendars now
686- // We only need to handle the first result.
687705 if (!isset ($ result ->VFREEBUSY )) {
688- return false ;
706+ return ;
689707 }
690708
691709 /** @var Component $freeBusyComponent */
692710 $ freeBusyComponent = $ result ->VFREEBUSY ;
693711 $ freeBusyProperties = $ freeBusyComponent ->select ('FREEBUSY ' );
694- // If there is no Free-busy property at all, the time-range is empty and available
712+ // If there is no FreeBusy property, the time-range is empty and available
713+ // so there is no need to influence the current status
695714 if (count ($ freeBusyProperties ) === 0 ) {
696- return false ;
697- }
698-
699- // If more than one Free-Busy property was returned, it means that an event
700- // starts or ends inside this time-range, so it's not available and we return false
701- if (count ($ freeBusyProperties ) > 1 ) {
702- return true ;
715+ return ;
703716 }
704717
705718 /** @var Property $freeBusyProperty */
706719 $ freeBusyProperty = $ freeBusyProperties [0 ];
707720 if (!$ freeBusyProperty ->offsetExists ('FBTYPE ' )) {
708- // If there is no FBTYPE, it means it's busy
709- return true ;
721+ // If there is no FBTYPE, it means it's busy from a regular event
722+ // @todo check if it would be better to set the timestamp to the start of the event
723+ $ this ->setUserStatus ($ userId , IUserStatus::AWAY , IUserStatus::MESSAGE_CALENDAR_BUSY , false );
724+ return ;
710725 }
711726
727+ // If we can't deal with the FBTYPE (custom properties are a possibility)
728+ // we should ignore it and leave the current status
712729 $ fbTypeParameter = $ freeBusyProperty ->offsetGet ('FBTYPE ' );
713730 if (!($ fbTypeParameter instanceof Parameter)) {
714- return false ;
731+ return ;
732+ }
733+ $ fbType = $ fbTypeParameter ->getValue ();
734+ switch ($ fbType ) {
735+ case 'BUSY ' :
736+ $ this ->setUserStatus ($ userId , IUserStatus::AWAY , IUserStatus::MESSAGE_CALENDAR_BUSY , false );
737+ return ;
738+ case 'BUSY-UNAVAILABLE ' :
739+ $ this ->setUserStatus ($ userId , IUserStatus::AWAY , IUserStatus::MESSAGE_AVAILABILITY , false );
740+ return ;
741+ case 'BUSY-TENTATIVE ' :
742+ $ this ->setUserStatus ($ userId , IUserStatus::AWAY , IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE , false );
743+ return ;
744+ default :
715745 }
716-
717- $ free = (strcasecmp ($ fbTypeParameter ->getValue (), 'FREE ' ) === 0 );
718-
719- return $ free ;
720746 }
721747}
0 commit comments