. * * Consult LICENSE file for details ************************************************/ // config file require_once("backend/caldav/config.php"); class BackendCalDAV extends BackendDiff { /** * @var CalDAVClient */ private $_caldav; private $_caldav_path; private $_collection = array(); private $changessinkinit; private $sinkdata; private $sinkmax; /** * Constructor */ public function BackendCalDAV() { if (!function_exists("curl_init")) { throw new FatalException("BackendCalDAV(): php-curl is not found", 0, null, LOGLEVEL_FATAL); } $this->changessinkinit = false; $this->sinkdata = array(); $this->sinkmax = array(); } /** * Login to the CalDAV backend * @see IBackend::Logon() */ public function Logon($username, $domain, $password) { $this->_caldav_path = str_replace('%u', $username, CALDAV_PATH); $url = sprintf("%s://%s:%d%s", CALDAV_PROTOCOL, CALDAV_SERVER, CALDAV_PORT, $this->_caldav_path); $this->_caldav = new CalDAVClient($url, $username, $password); if ($connected = $this->_caldav->CheckConnection()) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->Logon(): User '%s' is authenticated on CalDAV '%s'", $username, $url)); } else { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendCalDAV->Logon(): User '%s' is not authenticated on CalDAV '%s'", $username, $url)); } return $connected; } /** * The connections to CalDAV are always directly closed. So nothing special needs to happen here. * @see IBackend::Logoff() */ public function Logoff() { if ($this->_caldav != null) { $this->_caldav->Disconnect(); unset($this->_caldav); } $this->SaveStorages(); unset($this->sinkdata); unset($this->sinkmax); ZLog::Write(LOGLEVEL_DEBUG, "BackendCalDAV->Logoff(): disconnected from CALDAV server"); return true; } /** * CalDAV doesn't need to handle SendMail * @see IBackend::SendMail() */ public function SendMail($sm) { return false; } /** * No attachments in CalDAV * @see IBackend::GetAttachmentData() */ public function GetAttachmentData($attname) { return false; } /** * Deletes are always permanent deletes. Messages doesn't get moved. * @see IBackend::GetWasteBasket() */ public function GetWasteBasket() { return false; } /** * Get a list of all the folders we are going to sync. * Each caldav calendar can contain tasks (prefix T) and events (prefix C), so duplicate each calendar found. * @see BackendDiff::GetFolderList() */ public function GetFolderList() { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->GetFolderList(): Getting all folders.")); $folders = array(); $calendars = $this->_caldav->FindCalendars(); foreach ($calendars as $val) { $folder = array(); $fpath = explode("/", $val->url, -1); if (is_array($fpath)) { $folderid = array_pop($fpath); $id = "C" . $folderid; $folders[] = $this->StatFolder($id); $id = "T" . $folderid; $folders[] = $this->StatFolder($id); } } return $folders; } /** * Returning a SyncFolder * @see BackendDiff::GetFolder() */ public function GetFolder($id) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->GetFolder('%s')", $id)); $val = $this->_caldav->GetCalendarDetails($this->_caldav_path . substr($id, 1) . "/"); $folder = new SyncFolder(); $folder->parentid = "0"; $folder->displayname = $val->displayname; $folder->serverid = $id; if ($id[0] == "C") { if (defined('CALDAV_PERSONAL') && strtolower(substr($id, 1)) == CALDAV_PERSONAL) { $folder->type = SYNC_FOLDER_TYPE_APPOINTMENT; } else { $folder->type = SYNC_FOLDER_TYPE_USER_APPOINTMENT; } } else { if (defined('CALDAV_PERSONAL') && strtolower(substr($id, 1)) == CALDAV_PERSONAL) { $folder->type = SYNC_FOLDER_TYPE_TASK; } else { $folder->type = SYNC_FOLDER_TYPE_USER_TASK; } } return $folder; } /** * Returns information on the folder. * @see BackendDiff::StatFolder() */ public function StatFolder($id) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->StatFolder('%s')", $id)); $val = $this->GetFolder($id); $folder = array(); $folder["id"] = $id; $folder["parent"] = $val->parentid; $folder["mod"] = $val->serverid; return $folder; } /** * ChangeFolder is not supported under CalDAV * @see BackendDiff::ChangeFolder() */ public function ChangeFolder($folderid, $oldid, $displayname, $type) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangeFolder('%s','%s','%s','%s')", $folderid, $oldid, $displayname, $type)); return false; } /** * DeleteFolder is not supported under CalDAV * @see BackendDiff::DeleteFolder() */ public function DeleteFolder($id, $parentid) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->DeleteFolder('%s','%s')", $id, $parentid)); return false; } /** * Get a list of all the messages. * @see BackendDiff::GetMessageList() */ public function GetMessageList($folderid, $cutoffdate) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->GetMessageList('%s','%s')", $folderid, $cutoffdate)); /* Calculating the range of events we want to sync */ $begin = gmdate("Ymd\THis\Z", $cutoffdate); $finish = gmdate("Ymd\THis\Z", 2147483647); $path = $this->_caldav_path . substr($folderid, 1) . "/"; if ($folderid[0] == "C") { $msgs = $this->_caldav->GetEvents($begin, $finish, $path); } else { $msgs = $this->_caldav->GetTodos($begin, $finish, false, false, $path); } $messages = array(); foreach ($msgs as $e) { $id = $e['href']; $this->_collection[$id] = $e; $messages[] = $this->StatMessage($folderid, $id); } return $messages; } /** * Get a SyncObject by its ID * @see BackendDiff::GetMessage() */ public function GetMessage($folderid, $id, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->GetMessage('%s','%s')", $folderid, $id)); $data = $this->_collection[$id]['data']; if ($folderid[0] == "C") { return $this->_ParseVEventToAS($data, $contentparameters); } if ($folderid[0] == "T") { return $this->_ParseVTodoToAS($data, $contentparameters); } return false; } /** * Return id, flags and mod of a messageid * @see BackendDiff::StatMessage() */ public function StatMessage($folderid, $id) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->StatMessage('%s','%s')", $folderid, $id)); $type = "VEVENT"; if ($folderid[0] == "T") { $type = "VTODO"; } $data = null; if (array_key_exists($id, $this->_collection)) { $data = $this->_collection[$id]; } else { $path = $this->_caldav_path . substr($folderid, 1) . "/"; $e = $this->_caldav->GetEntryByUid(substr($id, 0, strlen($id)-4), $path, $type); if ($e == null && count($e) <= 0) return; $data = $e[0]; } $message = array(); $message['id'] = $data['href']; $message['flags'] = "1"; $message['mod'] = $data['etag']; return $message; } /** * Change/Add a message with contents received from ActiveSync * @see BackendDiff::ChangeMessage() */ public function ChangeMessage($folderid, $id, $message, $contentParameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangeMessage('%s','%s')", $folderid, $id)); if ($id) { $mod = $this->StatMessage($folderid, $id); $etag = $mod['mod']; } else { $etag = "*"; $id = sprintf("%s-%s.ics", gmdate("Ymd\THis\Z"), hash("md5", microtime())); } $url = $this->_caldav_path . substr($folderid, 1) . "/" . $id; $data = $this->_ParseASToVCalendar($message, $folderid, substr($id, 0, strlen($id) - 4)); $etag_new = $this->CreateUpdateCalendar($data, $url, $etag); $item = array(); $item['href'] = $id; $item['etag'] = $etag_new; $item['data'] = $data; $this->_collection[$id] = $item; return $this->StatMessage($folderid, $id); } /** * Change the read flag is not supported. * @see BackendDiff::SetReadFlag() */ public function SetReadFlag($folderid, $id, $flags, $contentParameters) { return false; } /** * Delete a message from the CalDAV server. * @see BackendDiff::DeleteMessage() */ public function DeleteMessage($folderid, $id, $contentParameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->DeleteMessage('%s','%s')", $folderid, $id)); $url = $this->_caldav_path . substr($folderid, 1) . "/" . $id; $http_status_code = $this->_caldav->DoDELETERequest($url); return $http_status_code == "204"; } /** * Move a message is not supported by CalDAV. * @see BackendDiff::MoveMessage() */ public function MoveMessage($folderid, $id, $newfolderid, $contentParameters) { return false; } /** * Create or Update one event * * @access public * @param $data string VCALENDAR text * @param $url string URL for the calendar, if false a new calendar object is created * @param $etag string ETAG for the calendar, if '*' is a new object * @return array */ public function CreateUpdateCalendar($data, $url = false, $etag = "*") { if ($url === false) { $url = sprintf("%s%s/%s-%s.ics", $this->_caldav_path, CALDAV_PERSONAL, gmdate("Ymd\THis\Z"), hash("md5", microtime())); $etag = "*"; } return $this->_caldav->DoPUTRequest($url, $data, $etag); } /** * Deletes one VCALENDAR * * @access public * @param $id string ID of the VCALENDAR * @return boolean */ public function DeleteCalendar($id) { $http_status_code = $this->_caldav->DoDELETERequest(sprintf("%s%s/%s", $this->_caldav_path, CALDAV_PERSONAL, $id)); return $http_status_code == "204"; } /** * Finds one VCALENDAR * * @access public * @param $uid string UID attribute * @return array */ public function FindCalendar($uid) { $filter = sprintf("%s", $uid); $events = $this->_caldav->DoCalendarQuery($filter, sprintf("%s%s", $this->_caldav_path, CALDAV_PERSONAL)); return $events; } /** * Indicates which AS version is supported by the backend. * * @access public * @return string AS version constant */ public function GetSupportedASVersion() { return ZPush::ASV_14; } /** * Indicates if the backend has a ChangesSink. * A sink is an active notification mechanism which does not need polling. * The CalDAV backend simulates a sink by polling revision dates from the events or use the native sync-collection. * * @access public * @return boolean */ public function HasChangesSink() { return true; } /** * The folder should be considered by the sink. * Folders which were not initialized should not result in a notification * of IBackend->ChangesSink(). * * @param string $folderid * * @access public * @return boolean false if found can not be found */ public function ChangesSinkInitialize($folderid) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSinkInitialize(): folderid '%s'", $folderid)); // We don't need the actual events, we only need to get the changes since this moment $init_ok = true; $url = $this->_caldav_path . substr($folderid, 1) . "/"; $this->sinkdata[$folderid] = $this->_caldav->GetSync($url, true, CALDAV_SUPPORTS_SYNC); if (CALDAV_SUPPORTS_SYNC) { // we don't need to store the sinkdata if the caldav server supports native sync unset($this->sinkdata[$url]); $this->sinkdata[$folderid] = array(); } $this->changessinkinit = $init_ok; $this->sinkmax = array(); return $this->changessinkinit; } /** * The actual ChangesSink. * For max. the $timeout value this method should block and if no changes * are available return an empty array. * If changes are available a list of folderids is expected. * * @param int $timeout max. amount of seconds to block * * @access public * @return array */ public function ChangesSink($timeout = 30) { $notifications = array(); $stopat = time() + $timeout - 1; //We can get here and the ChangesSink not be initialized yet if (!$this->changessinkinit) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSink - Not initialized ChangesSink, sleep and exit")); // We sleep and do nothing else sleep($timeout); return $notifications; } // only check once to reduce pressure in the DAV server foreach ($this->sinkdata as $k => $v) { $changed = false; $url = $this->_caldav_path . substr($k, 1) . "/"; $response = $this->_caldav->GetSync($url, false, CALDAV_SUPPORTS_SYNC); if (CALDAV_SUPPORTS_SYNC) { if (count($response) > 0) { $changed = true; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSink - Changes detected")); } } else { // If the numbers of events are different, we know for sure, there are changes if (count($response) != count($v)) { $changed = true; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSink - Changes detected")); } else { // If the numbers of events are equals, we compare the biggest date // FIXME: we are comparing strings no dates if (!isset($this->sinkmax[$k])) { $this->sinkmax[$k] = ''; for ($i = 0; $i < count($v); $i++) { if ($v[$i]['getlastmodified'] > $this->sinkmax[$k]) { $this->sinkmax[$k] = $v[$i]['getlastmodified']; } } } for ($i = 0; $i < count($response); $i++) { if ($response[$i]['getlastmodified'] > $this->sinkmax[$k]) { $changed = true; } } if ($changed) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->ChangesSink - Changes detected")); } } } if ($changed) { $notifications[] = $k; } } // Wait to timeout if (empty($notifications)) { while ($stopat > time()) { sleep(1); } } return $notifications; } /** * Convert a iCAL VEvent to ActiveSync format * @param ical_vevent $data * @param ContentParameters $contentparameters * @return SyncAppointment */ private function _ParseVEventToAS($data, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, "BackendCalDAV->_ParseVEventToAS(): Parsing VEvent"); $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); $message = new SyncAppointment(); $ical = new iCalComponent($data); $timezones = $ical->GetComponents("VTIMEZONE"); $timezone = ""; if (count($timezones) > 0) { $timezone = Utils::ParseTimezone($timezones[0]->GetPValue("TZID")); } if (!$timezone) { $timezone = date_default_timezone_get(); } $message->timezone = $this->_GetTimezoneString($timezone); $vevents = $ical->GetComponents("VTIMEZONE", false); foreach ($vevents as $event) { $rec = $event->GetProperties("RECURRENCE-ID"); if (count($rec) > 0) { $recurrence_id = reset($rec); $exception = new SyncAppointmentException(); $tzid = Utils::ParseTimezone($recurrence_id->GetParameterValue("TZID")); if (!$tzid) { $tzid = $timezone; } $exception->exceptionstarttime = Utils::MakeUTCDate($recurrence_id->Value(), $tzid); $exception->deleted = "0"; $exception = $this->_ParseVEventToSyncObject($event, $exception, $truncsize); if (!isset($message->exceptions)) { $message->exceptions = array(); } $message->exceptions[] = $exception; } else { $message = $this->_ParseVEventToSyncObject($event, $message, $truncsize); } } return $message; } /** * Parse 1 VEvent * @param ical_vevent $event * @param SyncAppointment(Exception) $message * @param int $truncsize */ private function _ParseVEventToSyncObject($event, $message, $truncsize) { //Defaults $message->busystatus = "2"; $properties = $event->GetProperties(); foreach ($properties as $property) { switch ($property->Name()) { case "LAST-MODIFIED": $message->dtstamp = Utils::MakeUTCDate($property->Value()); break; case "DTSTART": $message->starttime = Utils::MakeUTCDate($property->Value(), Utils::ParseTimezone($property->GetParameterValue("TZID"))); if (strlen($property->Value()) == 8) { $message->alldayevent = "1"; } break; case "SUMMARY": $message->subject = $property->Value(); break; case "UID": $message->uid = $property->Value(); break; case "ORGANIZER": $org_mail = str_ireplace("MAILTO:", "", $property->Value()); $message->organizeremail = $org_mail; $org_cn = $property->GetParameterValue("CN"); if ($org_cn) { $message->organizername = $org_cn; } break; case "LOCATION": $message->location = $property->Value(); break; case "DTEND": $message->endtime = Utils::MakeUTCDate($property->Value(), Utils::ParseTimezone($property->GetParameterValue("TZID"))); if (strlen($property->Value()) == 8) { $message->alldayevent = "1"; } break; case "DURATION": if (!isset($message->endtime)) { $start = date_create("@" . $message->starttime); $val = str_replace("+", "", $property->Value()); $interval = new DateInterval($val); $message->endtime = date_timestamp_get(date_add($start, $interval)); } break; case "RRULE": $message->recurrence = $this->_ParseRecurrence($property->Value(), "vevent"); break; case "CLASS": switch ($property->Value()) { case "PUBLIC": $message->sensitivity = "0"; break; case "PRIVATE": $message->sensitivity = "2"; break; case "CONFIDENTIAL": $message->sensitivity = "3"; break; } break; case "TRANSP": switch ($property->Value()) { case "TRANSPARENT": $message->busystatus = "0"; break; case "OPAQUE": $message->busystatus = "2"; break; } break; // SYNC_POOMCAL_MEETINGSTATUS // Meetingstatus values // 0 = is not a meeting // 1 = is a meeting // 3 = Meeting received // 5 = Meeting is canceled // 7 = Meeting is canceled and received // 9 = as 1 // 11 = as 3 // 13 = as 5 // 15 = as 7 case "STATUS": switch ($property->Value()) { case "TENTATIVE": $message->meetingstatus = "3"; // was 1 break; case "CONFIRMED": $message->meetingstatus = "1"; // was 3 break; case "CANCELLED": $message->meetingstatus = "5"; // could also be 7 break; } break; case "ATTENDEE": $attendee = new SyncAttendee(); $att_email = str_ireplace("MAILTO:", "", $property->Value()); $attendee->email = $att_email; $att_cn = $property->GetParameterValue("CN"); if ($att_cn) { $attendee->name = $att_cn; } if (isset($message->attendees) && is_array($message->attendees)) { $message->attendees[] = $attendee; } else { $message->attendees = array($attendee); } break; case "DESCRIPTION": if (Request::GetProtocolVersion() >= 12.0) { $message->asbody = new SyncBaseBody(); $message->asbody->data = str_replace("\n","\r\n", str_replace("\r","",Utils::ConvertHtmlToText($property->Value()))); // truncate body, if requested if (strlen($message->asbody->data) > $truncsize) { $message->asbody->truncated = 1; $message->asbody->data = Utils::Utf8_truncate($message->asbody->data, $truncsize); } else { $message->asbody->truncated = 0; } $message->nativebodytype = SYNC_BODYPREFERENCE_PLAIN; } else { $body = $property->Value(); // truncate body, if requested if(strlen($body) > $truncsize) { $body = Utils::Utf8_truncate($body, $truncsize); $message->bodytruncated = 1; } else { $message->bodytruncated = 0; } $body = str_replace("\n","\r\n", str_replace("\r","",$body)); $message->body = $body; } break; case "CATEGORIES": $categories = explode(",", $property->Value()); $message->categories = $categories; break; case "EXDATE": $exception = new SyncAppointmentException(); $exception->deleted = "1"; $exception->exceptionstarttime = Utils::MakeUTCDate($property->Value()); if (!isset($message->exceptions)) { $message->exceptions = array(); } $message->exceptions[] = $exception; break; //We can ignore the following case "PRIORITY": case "SEQUENCE": case "CREATED": case "DTSTAMP": case "X-MOZ-GENERATION": case "X-MOZ-LASTACK": case "X-LIC-ERROR": case "RECURRENCE-ID": break; default: ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_ParseVEventToSyncObject(): '%s' is not yet supported.", $property->Name())); } } // Workaround #127 - No organizeremail defined if (!isset($message->organizeremail)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_ParseVEventToSyncObject(): No organizeremail defined, using username")); $message->organizeremail = $this->originalUsername; } $valarm = current($event->GetComponents("VALARM")); if ($valarm) { $properties = $valarm->GetProperties(); foreach ($properties as $property) { if ($property->Name() == "TRIGGER") { $parameters = $property->Parameters(); if (array_key_exists("VALUE", $parameters) && $parameters["VALUE"] == "DATE-TIME") { $trigger = date_create("@" . Utils::MakeUTCDate($property->Value())); $begin = date_create("@" . $message->starttime); $interval = date_diff($begin, $trigger); $message->reminder = $interval->format("%i") + $interval->format("%h") * 60 + $interval->format("%a") * 60 * 24; } elseif (!array_key_exists("VALUE", $parameters) || $parameters["VALUE"] == "DURATION") { $val = str_replace("-", "", $property->Value()); $interval = new DateInterval($val); $message->reminder = $interval->format("%i") + $interval->format("%h") * 60 + $interval->format("%a") * 60 * 24; } } } } return $message; } /** * Parse a RRULE * @param string $rrulestr */ private function _ParseRecurrence($rrulestr, $type) { $recurrence = new SyncRecurrence(); if ($type == "vtodo") { $recurrence = new SyncTaskRecurrence(); } $rrules = explode(";", $rrulestr); foreach ($rrules as $rrule) { $rule = explode("=", $rrule); switch ($rule[0]) { case "FREQ": switch ($rule[1]) { case "DAILY": $recurrence->type = "0"; break; case "WEEKLY": $recurrence->type = "1"; break; case "MONTHLY": $recurrence->type = "2"; break; case "YEARLY": $recurrence->type = "5"; } break; case "UNTIL": $recurrence->until = Utils::MakeUTCDate($rule[1]); break; case "COUNT": $recurrence->occurrences = $rule[1]; break; case "INTERVAL": $recurrence->interval = $rule[1]; break; case "BYDAY": $dval = 0; $days = explode(",", $rule[1]); foreach ($days as $day) { if ($recurrence->type == "2") { if (strlen($day) > 2) { $recurrence->weekofmonth = intval($day); $day = substr($day,-2); } else { $recurrence->weekofmonth = 1; } $recurrence->type = "3"; } switch ($day) { // 1 = Sunday // 2 = Monday // 4 = Tuesday // 8 = Wednesday // 16 = Thursday // 32 = Friday // 62 = Weekdays // not in spec: daily weekday recurrence // 64 = Saturday case "SU": $dval += 1; break; case "MO": $dval += 2; break; case "TU": $dval += 4; break; case "WE": $dval += 8; break; case "TH": $dval += 16; break; case "FR": $dval += 32; break; case "SA": $dval += 64; break; } } $recurrence->dayofweek = $dval; break; //Only 1 BYMONTHDAY is supported, so BYMONTHDAY=2,3 will only include 2 case "BYMONTHDAY": $days = explode(",", $rule[1]); $recurrence->dayofmonth = $days[0]; break; case "BYMONTH": $recurrence->monthofyear = $rule[1]; break; default: ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_ParseRecurrence(): '%s' is not yet supported.", $rule[0])); } } return $recurrence; } /** * Generate a iCAL VCalendar from ActiveSync object. * @param string $data * @param string $folderid * @param string $id */ private function _ParseASToVCalendar($data, $folderid, $id) { $ical = new iCalComponent(); $ical->SetType("VCALENDAR"); $ical->AddProperty("VERSION", "2.0"); $ical->AddProperty("PRODID", "-//z-push-contrib//NONSGML Z-Push-contrib Calendar//EN"); $ical->AddProperty("CALSCALE", "GREGORIAN"); if ($folderid[0] == "C") { $vevent = $this->_ParseASEventToVEvent($data, $id); $vevent->AddProperty("UID", $id); $ical->AddComponent($vevent); if (isset($data->exceptions) && is_array($data->exceptions)) { foreach ($data->exceptions as $ex) { $exception = $this->_ParseASEventToVEvent($ex, $id); if ($data->alldayevent == 1) { $exception->AddProperty("RECURRENCE-ID", $this->_GetDateFromUTC("Ymd", $ex->exceptionstarttime, $data->timezone), array("VALUE" => "DATE")); } else { $exception->AddProperty("RECURRENCE-ID", gmdate("Ymd\THis\Z", $ex->exceptionstarttime)); } $exception->AddProperty("UID", $id); $ical->AddComponent($exception); } } } if ($folderid[0] == "T") { $vtodo = $this->_ParseASTaskToVTodo($data, $id); $vtodo->AddProperty("UID", $id); $vtodo->AddProperty("DTSTAMP", gmdate("Ymd\THis\Z")); $ical->AddComponent($vtodo); } return $ical->Render(); } /** * Generate a VEVENT from a SyncAppointment(Exception). * @param string $data * @param string $id * @return iCalComponent */ private function _ParseASEventToVEvent($data, $id) { $vevent = new iCalComponent(); $vevent->SetType("VEVENT"); if (isset($data->dtstamp)) { $vevent->AddProperty("DTSTAMP", gmdate("Ymd\THis\Z", $data->dtstamp)); $vevent->AddProperty("LAST-MODIFIED", gmdate("Ymd\THis\Z", $data->dtstamp)); } if (isset($data->starttime)) { if ($data->alldayevent == 1) { $vevent->AddProperty("DTSTART", $this->_GetDateFromUTC("Ymd", $data->starttime, $data->timezone), array("VALUE" => "DATE")); } else { $vevent->AddProperty("DTSTART", gmdate("Ymd\THis\Z", $data->starttime)); } } if (isset($data->subject)) { $vevent->AddProperty("SUMMARY", $data->subject); } if (isset($data->organizeremail)) { if (isset($data->organizername)) { $vevent->AddProperty("ORGANIZER", sprintf("MAILTO:%s", $data->organizeremail), array("CN" => $data->organizername)); } else { $vevent->AddProperty("ORGANIZER", sprintf("MAILTO:%s", $data->organizeremail)); } } if (isset($data->location)) { $vevent->AddProperty("LOCATION", $data->location); } if (isset($data->endtime)) { if ($data->alldayevent == 1) { $vevent->AddProperty("DTEND", $this->_GetDateFromUTC("Ymd", $data->endtime, $data->timezone), array("VALUE" => "DATE")); $vevent->AddProperty("X-MICROSOFT-CDO-ALLDAYEVENT", "TRUE"); } else { $vevent->AddProperty("DTEND", gmdate("Ymd\THis\Z", $data->endtime)); $vevent->AddProperty("X-MICROSOFT-CDO-ALLDAYEVENT", "FALSE"); } } if (isset($data->recurrence)) { $vevent->AddProperty("RRULE", $this->_GenerateRecurrence($data->recurrence)); } if (isset($data->sensitivity)) { switch ($data->sensitivity) { case "0": $vevent->AddProperty("CLASS", "PUBLIC"); break; case "2": $vevent->AddProperty("CLASS", "PRIVATE"); break; case "3": $vevent->AddProperty("CLASS", "CONFIDENTIAL"); break; } } if (isset($data->busystatus)) { switch ($data->busystatus) { case "0": case "1": $vevent->AddProperty("TRANSP", "TRANSPARENT"); break; case "2": case "3": $vevent->AddProperty("TRANSP", "OPAQUE"); break; } } if (isset($data->reminder)) { $valarm = new iCalComponent(); $valarm->SetType("VALARM"); $valarm->AddProperty("ACTION", "DISPLAY"); $trigger = "-PT" . $data->reminder . "M"; $valarm->AddProperty("TRIGGER", $trigger); $vevent->AddComponent($valarm); } if (isset($data->rtf)) { $rtfparser = new rtf(); $rtfparser->loadrtf(base64_decode($data->rtf)); $rtfparser->output("ascii"); $rtfparser->parse(); $vevent->AddProperty("DESCRIPTION", $rtfparser->out); } if (isset($data->meetingstatus)) { switch ($data->meetingstatus) { case "1": $vevent->AddProperty("STATUS", "TENTATIVE"); $vevent->AddProperty("X-MICROSOFT-CDO-BUSYSTATUS", "TENTATIVE"); $vevent->AddProperty("X-MICROSOFT-DISALLOW-COUNTER", "FALSE"); break; case "3": $vevent->AddProperty("STATUS", "CONFIRMED"); $vevent->AddProperty("X-MICROSOFT-CDO-BUSYSTATUS", "CONFIRMED"); $vevent->AddProperty("X-MICROSOFT-DISALLOW-COUNTER", "FALSE"); break; case "5": case "7": $vevent->AddProperty("STATUS", "CANCELLED"); $vevent->AddProperty("X-MICROSOFT-CDO-BUSYSTATUS", "CANCELLED"); $vevent->AddProperty("X-MICROSOFT-DISALLOW-COUNTER", "TRUE"); break; } } if (isset($data->attendees) && is_array($data->attendees)) { //If there are attendees, we need to set ORGANIZER //Some phones doesn't send the organizeremail, so we gotto get it somewhere else. //Lets use the login here ($username) if (!isset($data->organizeremail)) { $vevent->AddProperty("ORGANIZER", sprintf("MAILTO:%s", $this->originalUsername)); } foreach ($data->attendees as $att) { if (isset($att->name)) { $vevent->AddProperty("ATTENDEE", sprintf("MAILTO:%s", $att->email), array("CN" => $att->name)); } else { $vevent->AddProperty("ATTENDEE", sprintf("MAILTO:%s", $att->email)); } } } if (isset($data->body)) { $vevent->AddProperty("DESCRIPTION", $data->body); } if (isset($data->asbody->data)) { $vevent->AddProperty("DESCRIPTION", $data->asbody->data); } if (isset($data->categories) && is_array($data->categories)) { $vevent->AddProperty("CATEGORIES", implode(",", $data->categories)); } // X-MICROSOFT-CDO-APPT-SEQUENCE:0 // X-MICROSOFT-CDO-OWNERAPPTID:2113393086 // X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY // X-MICROSOFT-CDO-IMPORTANCE:1 // X-MICROSOFT-CDO-INSTTYPE:0 return $vevent; } /** * Generate Recurrence * @param string $rec */ private function _GenerateRecurrence($rec) { $rrule = array(); if (isset($rec->type)) { $freq = ""; switch ($rec->type) { case "0": $freq = "DAILY"; break; case "1": $freq = "WEEKLY"; break; case "2": case "3": $freq = "MONTHLY"; break; case "5": $freq = "YEARLY"; break; } $rrule[] = "FREQ=" . $freq; } if (isset($rec->until)) { $rrule[] = "UNTIL=" . gmdate("Ymd\THis\Z", $rec->until); } if (isset($rec->occurrences)) { $rrule[] = "COUNT=" . $rec->occurrences; } if (isset($rec->interval)) { $rrule[] = "INTERVAL=" . $rec->interval; } if (isset($rec->dayofweek)) { $week = ''; if (isset($rec->weekofmonth)) { $week = $rec->weekofmonth; } $days = array(); if (($rec->dayofweek & 1) == 1) { if (empty($week)) { $days[] = "SU"; } else { $days[] = $week . "SU"; } } if (($rec->dayofweek & 2) == 2) { if (empty($week)) { $days[] = "MO"; } else { $days[] = $week . "MO"; } } if (($rec->dayofweek & 4) == 4) { if (empty($week)) { $days[] = "TU"; } else { $days[] = $week . "TU"; } } if (($rec->dayofweek & 8) == 8) { if (empty($week)) { $days[] = "WE"; } else { $days[] = $week . "WE"; } } if (($rec->dayofweek & 16) == 16) { if (empty($week)) { $days[] = "TH"; } else { $days[] = $week . "TH"; } } if (($rec->dayofweek & 32) == 32) { if (empty($week)) { $days[] = "FR"; } else { $days[] = $week . "FR"; } } if (($rec->dayofweek & 64) == 64) { if (empty($week)) { $days[] = "SA"; } else { $days[] = $week . "SA"; } } $rrule[] = "BYDAY=" . implode(",", $days); } if (isset($rec->dayofmonth)) { $rrule[] = "BYMONTHDAY=" . $rec->dayofmonth; } if (isset($rec->monthofyear)) { $rrule[] = "BYMONTH=" . $rec->monthofyear; } return implode(";", $rrule); } /** * Convert a iCAL VTodo to ActiveSync format * @param string $data * @param ContentParameters $contentparameters */ private function _ParseVTodoToAS($data, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_ParseVTodoToAS(): Parsing VTodo")); $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); $message = new SyncTask(); $ical = new iCalComponent($data); $vtodos = $ical->GetComponents("VTODO"); //Should only loop once foreach ($vtodos as $vtodo) { $message = $this->_ParseVTodoToSyncObject($vtodo, $message, $truncsize); } return $message; } /** * Parse 1 VEvent * @param ical_vtodo $vtodo * @param SyncAppointment(Exception) $message * @param int $truncsize */ private function _ParseVTodoToSyncObject($vtodo, $message, $truncsize) { //Default $message->reminderset = "0"; $message->importance = "1"; $message->complete = "0"; $properties = $vtodo->GetProperties(); foreach ($properties as $property) { switch ($property->Name()) { case "SUMMARY": $message->subject = $property->Value(); break; case "STATUS": switch ($property->Value()) { case "NEEDS-ACTION": case "IN-PROCESS": $message->complete = "0"; break; case "COMPLETED": case "CANCELLED": $message->complete = "1"; break; } break; case "COMPLETED": $message->datecompleted = Utils::MakeUTCDate($property->Value()); break; case "DUE": $message->utcduedate = Utils::MakeUTCDate($property->Value()); break; case "PRIORITY": $priority = $property->Value(); if ($priority <= 3) $message->importance = "0"; if ($priority <= 6) $message->importance = "1"; if ($priority > 6) $message->importance = "2"; break; case "RRULE": $message->recurrence = $this->_ParseRecurrence($property->Value(), "vtodo"); break; case "CLASS": switch ($property->Value()) { case "PUBLIC": $message->sensitivity = "0"; break; case "PRIVATE": $message->sensitivity = "2"; break; case "CONFIDENTIAL": $message->sensitivity = "3"; break; } break; case "DTSTART": $message->utcstartdate = Utils::MakeUTCDate($property->Value()); break; case "SUMMARY": $message->subject = $property->Value(); break; case "CATEGORIES": $categories = explode(",", $property->Value()); $message->categories = $categories; break; } } if (isset($message->recurrence)) { $message->recurrence->start = $message->utcstartdate; } $valarm = current($vtodo->GetComponents("VALARM")); if ($valarm) { $properties = $valarm->GetProperties(); foreach ($properties as $property) { if ($property->Name() == "TRIGGER") { $parameters = $property->Parameters(); if (array_key_exists("VALUE", $parameters) && $parameters["VALUE"] == "DATE-TIME") { $message->remindertime = Utils::MakeUTCDate($property->Value()); $message->reminderset = "1"; } elseif (!array_key_exists("VALUE", $parameters) || $parameters["VALUE"] == "DURATION") { $val = str_replace("-", "", $property->Value()); $interval = new DateInterval($val); $start = date_create("@" . $message->utcstartdate); $message->remindertime = date_timestamp_get(date_sub($start, $interval)); $message->reminderset = "1"; } } } } return $message; } /** * Generate a VTODO from a SyncAppointment(Exception) * @param string $data * @param string $id * @return iCalComponent */ private function _ParseASTaskToVTodo($data, $id) { $vtodo = new iCalComponent(); $vtodo->SetType("VTODO"); if (isset($data->body)) { $vtodo->AddProperty("DESCRIPTION", $data->body); } if (isset($data->asbody->data)) { if (isset($data->nativebodytype) && $data->nativebodytype == SYNC_BODYPREFERENCE_RTF) { $rtfparser = new rtf(); $rtfparser->loadrtf(base64_decode($data->asbody->data)); $rtfparser->output("ascii"); $rtfparser->parse(); $vtodo->AddProperty("DESCRIPTION", $rtfparser->out); } else { $vtodo->AddProperty("DESCRIPTION", $data->asbody->data); } } if (isset($data->complete)) { if ($data->complete == "0") { $vtodo->AddProperty("STATUS", "NEEDS-ACTION"); } else { $vtodo->AddProperty("STATUS", "COMPLETED"); } } if (isset($data->datecompleted)) { $vtodo->AddProperty("COMPLETED", gmdate("Ymd\THis\Z", $data->datecompleted)); } if ($data->utcduedate) { $vtodo->AddProperty("DUE", gmdate("Ymd\THis\Z", $data->utcduedate)); } if (isset($data->importance)) { if ($data->importance == "1") { $vtodo->AddProperty("PRIORITY", 6); } elseif ($data->importance == "2") { $vtodo->AddProperty("PRIORITY", 9); } else { $vtodo->AddProperty("PRIORITY", 1); } } if (isset($data->recurrence)) { $vtodo->AddProperty("RRULE", $this->_GenerateRecurrence($data->recurrence)); } if ($data->reminderset && $data->remindertime) { $valarm = new iCalComponent(); $valarm->SetType("VALARM"); $valarm->AddProperty("ACTION", "DISPLAY"); $valarm->AddProperty("TRIGGER;VALUE=DATE-TIME", gmdate("Ymd\THis\Z", $data->remindertime)); $vtodo->AddComponent($valarm); } if (isset($data->sensitivity)) { switch ($data->sensitivity) { case "0": $vtodo->AddProperty("CLASS", "PUBLIC"); break; case "2": $vtodo->AddProperty("CLASS", "PRIVATE"); break; case "3": $vtodo->AddProperty("CLASS", "CONFIDENTIAL"); break; } } if (isset($data->utcstartdate)) { $vtodo->AddProperty("DTSTART", gmdate("Ymd\THis\Z", $data->utcstartdate)); } if (isset($data->subject)) { $vtodo->AddProperty("SUMMARY", $data->subject); } if (isset($data->rtf)) { $rtfparser = new rtf(); $rtfparser->loadrtf(base64_decode($data->rtf)); $rtfparser->output("ascii"); $rtfparser->parse(); $vtodo->AddProperty("DESCRIPTION", $rtfparser->out); } if (isset($data->categories) && is_array($data->categories)) { $vtodo->AddProperty("CATEGORIES", implode(",", $data->categories)); } return $vtodo; } private function _GetDateFromUTC($format, $date, $tz_str) { $timezone = $this->_GetTimezoneFromString($tz_str); $dt = date_create('@' . $date); date_timezone_set($dt, timezone_open($timezone)); return date_format($dt, $format); } //This returns a timezone that matches the timezonestring. //We can't be sure this is the one you chose, as multiple timezones have same timezonestring private function _GetTimezoneFromString($tz_string) { //Get a list of all timezones $identifiers = DateTimeZone::listIdentifiers(); //Try the default timezone first array_unshift($identifiers, date_default_timezone_get()); foreach ($identifiers as $tz) { $str = $this->_GetTimezoneString($tz, false); if ($str == $tz_string) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->_GetTimezoneFromString(): Found timezone: '%s'.", $tz)); return $tz; } } return date_default_timezone_get(); } /** * Generate ActiveSync Timezone Packed String. * @param string $timezone * @param string $with_names * @throws Exception */ private function _GetTimezoneString($timezone, $with_names = true) { // UTC needs special handling if ($timezone == "UTC") return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0)); try { //Generate a timezone string (PHP 5.3 needed for this) $timezone = new DateTimeZone($timezone); $trans = $timezone->getTransitions(time()); $stdTime = null; $dstTime = null; if (count($trans) < 3) { throw new Exception(); } if ($trans[1]['isdst'] == 1) { $dstTime = $trans[1]; $stdTime = $trans[2]; } else { $dstTime = $trans[2]; $stdTime = $trans[1]; } $stdTimeO = new DateTime($stdTime['time']); $stdFirst = new DateTime(sprintf("first sun of %s %s", $stdTimeO->format('F'), $stdTimeO->format('Y')), timezone_open("UTC")); $stdBias = $stdTime['offset'] / -60; $stdName = $stdTime['abbr']; $stdYear = 0; $stdMonth = $stdTimeO->format('n'); $stdWeek = floor(($stdTimeO->format("j")-$stdFirst->format("j"))/7)+1; $stdDay = $stdTimeO->format('w'); $stdHour = $stdTimeO->format('H'); $stdMinute = $stdTimeO->format('i'); $stdTimeO->add(new DateInterval('P7D')); if ($stdTimeO->format('n') != $stdMonth) { $stdWeek = 5; } $dstTimeO = new DateTime($dstTime['time']); $dstFirst = new DateTime(sprintf("first sun of %s %s", $dstTimeO->format('F'), $dstTimeO->format('Y')), timezone_open("UTC")); $dstName = $dstTime['abbr']; $dstYear = 0; $dstMonth = $dstTimeO->format('n'); $dstWeek = floor(($dstTimeO->format("j")-$dstFirst->format("j"))/7)+1; $dstDay = $dstTimeO->format('w'); $dstHour = $dstTimeO->format('H'); $dstMinute = $dstTimeO->format('i'); $dstTimeO->add(new DateInterval('P7D')); if ($dstTimeO->format('n') != $dstMonth) { $dstWeek = 5; } $dstBias = ($dstTime['offset'] - $stdTime['offset']) / -60; if ($with_names) { return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', $stdBias, $stdName, 0, $stdMonth, $stdDay, $stdWeek, $stdHour, $stdMinute, 0, 0, 0, $dstName, 0, $dstMonth, $dstDay, $dstWeek, $dstHour, $dstMinute, 0, 0, $dstBias)); } else { return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', $stdBias, '', 0, $stdMonth, $stdDay, $stdWeek, $stdHour, $stdMinute, 0, 0, 0, '', 0, $dstMonth, $dstDay, $dstWeek, $dstHour, $dstMinute, 0, 0, $dstBias)); } } catch (Exception $e) { // If invalid timezone is given, we return UTC return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0)); } return base64_encode(pack('la64vvvvvvvvla64vvvvvvvvl', 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0)); } }