diff --git a/calendar_ui.js b/calendar_ui.js index 4d6bb2a..0a04879 100644 --- a/calendar_ui.js +++ b/calendar_ui.js @@ -2035,12 +2035,17 @@ function rcube_calendar_ui(settings) } icon = $(icon).attr('class', 'availabilityicon loading'); - + + var start = event.allDay ? '00:00' : $('#edit-starttime').val(); + var end = event.allDay ? '23:59' : $('#edit-endtime').val(); + var start = me.parse_datetime(start, $('#edit-startdate').val()); + var end = me.parse_datetime(end, $('#edit-enddate').val()); + $.ajax({ type: 'GET', dataType: 'html', url: rcmail.url('freebusy-status'), - data: { email:email, start:date2servertime(clone_date(event.start, event.allDay?1:0)), end:date2servertime(clone_date(event.end, event.allDay?2:0)), _remote: 1 }, + data: { email:email, start:date2servertime(start), end:date2servertime(end), _remote: 1 }, success: function(status){ var avail = String(status).toLowerCase(); icon.removeClass('loading').addClass(avail).attr('title', rcmail.gettext('avail' + avail, 'calendar')); diff --git a/drivers/caldav/SQL/mysql.initial.sql b/drivers/caldav/SQL/mysql.initial.sql index 92004b2..c41d620 100644 --- a/drivers/caldav/SQL/mysql.initial.sql +++ b/drivers/caldav/SQL/mysql.initial.sql @@ -107,4 +107,4 @@ CREATE TABLE IF NOT EXISTS `caldav_attachments` ( CONSTRAINT `fk_caldav_attachments_event_id` FOREIGN KEY (`event_id`) REFERENCES `caldav_events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; -REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-caldav-version', '2021082400'); \ No newline at end of file +REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-caldav-version', '2024112200'); \ No newline at end of file diff --git a/drivers/caldav/SQL/postgres.initial.sql b/drivers/caldav/SQL/postgres.initial.sql index 80ee79f..c8515b2 100644 --- a/drivers/caldav/SQL/postgres.initial.sql +++ b/drivers/caldav/SQL/postgres.initial.sql @@ -122,4 +122,4 @@ CREATE TABLE IF NOT EXISTS caldav_attachments ( REFERENCES caldav_events(event_id) ON DELETE CASCADE ON UPDATE CASCADE ) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */; -INSERT INTO system (name, value) VALUES ('calendar-caldav-version', '2021082400') ON CONFLICT (name) DO UPDATE SET value = excluded.value; +INSERT INTO system (name, value) VALUES ('calendar-caldav-version', '2024112200') ON CONFLICT (name) DO UPDATE SET value = excluded.value; diff --git a/drivers/caldav/SQL/sqlite.initial.sql b/drivers/caldav/SQL/sqlite.initial.sql index 3c06c1f..68ad8f7 100644 --- a/drivers/caldav/SQL/sqlite.initial.sql +++ b/drivers/caldav/SQL/sqlite.initial.sql @@ -93,7 +93,7 @@ CREATE TABLE IF NOT EXISTS `caldav_attachments` ( FOREIGN KEY (`event_id`) REFERENCES `caldav_events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE ); -REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-caldav-version', '2021082400'); +REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-caldav-version', '2024112200'); CREATE INDEX caldav_user_name_idx ON caldav_calendars(user_id, name); CREATE INDEX caldav_uid_idx ON caldav_events(uid); diff --git a/drivers/caldav/caldav_client.php b/drivers/caldav/caldav_client.php index 95f6789..7799ab7 100644 --- a/drivers/caldav/caldav_client.php +++ b/drivers/caldav/caldav_client.php @@ -400,5 +400,31 @@ public function delete_calendar() { return $response["statusCode"] === 204 || // 204 (no content, successfully deleted) $response["statusCode"] === 200; // 200 (OK, successfully deleted) } + + /** + * Fetch freebusy info. + * + * @return Sabre\VObject\Component\VFreeBusy + */ + public function get_freebusy_info($path, $body) { + $headers = ['Content-Type' => 'text/calendar; charset=utf-8']; + + $response = $this->request('POST', $path, $body, $headers); + if ($response['statusCode'] !== 200) { + rcmail::console('Could not get freebusy info for user. Response: ' . print_r($response, true)); + + return false; + } + + $schedule_response = $this->xml->expect('{urn:ietf:params:xml:ns:caldav}schedule-response', $response['body'])[0]['value']; + $calendar_data = array_values(array_filter( + $schedule_response, + function ($arr) { + return $arr['name'] == '{urn:ietf:params:xml:ns:caldav}calendar-data'; + } + ))[0]['value']; + + return Sabre\VObject\Reader::read($calendar_data)->VFREEBUSY; + } }; ?> diff --git a/drivers/caldav/caldav_driver.php b/drivers/caldav/caldav_driver.php index 71a2162..4a2f36c 100644 --- a/drivers/caldav/caldav_driver.php +++ b/drivers/caldav/caldav_driver.php @@ -36,7 +36,7 @@ class caldav_driver extends calendar_driver // features this backend supports public $alarms = true; public $attendees = true; - public $freebusy = false; + public $freebusy = true; public $attachments = true; public $alarm_types = array('DISPLAY'); @@ -1784,6 +1784,88 @@ public function replace_category($oldname, $name, $color) return $this->rc->db->affected_rows($query); } + public function get_freebusy_list($email, $start, $end) { + $result = $this->rc->db->query( + 'SELECT source_id, caldav_url, caldav_user, caldav_pass + FROM ' . $this->db_sources . ' + WHERE user_id=?', + $this->rc->user->ID, + ); + + $fbtypemap = [ + 'UNKNOWN' => calendar::FREEBUSY_UNKNOWN, + 'FREE' => calendar::FREEBUSY_FREE, + 'BUSY' => calendar::FREEBUSY_BUSY, + 'TENTATIVE' => calendar::FREEBUSY_TENTATIVE, + 'OUT-OF-OFFICE' => calendar::FREEBUSY_OOF, + ]; + $freebusy_list = []; + $success = false; + while ($result && ($source = $this->rc->db->fetch_assoc($result))) { + $source['caldav_pass'] = $this->_decrypt_pass($source['caldav_pass']); + $server_url = self::_encode_url($source['caldav_url']); + + $caldav = new caldav_client($server_url, $source['caldav_user'], $source['caldav_pass']); + $response = $caldav->prop_find($server_url, ['{DAV:}current-user-principal'], 0); + + if ( + $response + && array_key_exists('{DAV:}current-user-principal', $response) + && count($response['{DAV:}current-user-principal']) > 0 + && array_key_exists('value', $response['{DAV:}current-user-principal'][0]) + ) { + $response = $caldav->prop_find( + $response['{DAV:}current-user-principal'][0]['value'], + ['{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL'], + 0, + ); + + if ( + $response + && array_key_exists('{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL', $response) + && count($response['{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL']) > 0 + && array_key_exists('value', $response['{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL'][0]) + ) { + $vcalendar = new Sabre\VObject\Component\VCalendar([ + 'METHOD' => 'REQUEST', + 'VFREEBUSY' => [ + 'ORGANIZER' => 'mailto:' . $this->rc->get_user_name(), + 'ATTENDEE' => $email, + 'DTSTART' => DateTime::createFromFormat('U', $start), + 'DTEND' => DateTime::createFromFormat('U', $end), + ] + ]); + $freebusy_info = $caldav->get_freebusy_info( + $response['{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL'][0]['value'], + $vcalendar->serialize() + ); + + if ($freebusy_info) { + $success = true; + if (isset($freebusy_info->FREEBUSY)) { + foreach ($freebusy_info->FREEBUSY as $freebusy) { + $fbType = isset($freebusy['FBTYPE']) ? (string) $freebusy['FBTYPE'] : 'BUSY'; + [$startTime, $endTime] = explode('/', $freebusy->getValue()); + + $freebusy_list[] = [ + strtotime($startTime), + strtotime($endTime), + $fbtypemap[strtoupper($fbType)] ?? 0, + ]; + } + } + } + } + } + } + + if ($success) { + return $freebusy_list; + } else { + return false; + } + } + /** * Helper method to serialize the list of alarms into a string */