ZP-685 New backend CalDAV. Released under the Affero GNU General Public License (AGPL) version 3.

parent 6a1891a9
The Author of this backend is dupondje, I could have modified it.
You can found the original code here:
https://github.com/dupondje/PHP-Push-2
REQUIREMENTS:
php-curl
libawl-php
INSTALL:
Add your awl folder to the include_path variable (/etc/php.ini or similar)
CalDAV server (DAViCal, Sabredav, Sogo, Owncloud...)
<?php
/***********************************************
* File : caldav.php
* Project : PHP-Push
* Descr : This backend is based on 'BackendDiff' and implements a CalDAV interface
*
* Created : 29.03.2012
*
* Copyright 2012 - 2014 Jean-Louis Dupond
*
* Jean-Louis Dupond released this code as AGPLv3 here: https://github.com/dupondje/PHP-Push-2/issues/93
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult LICENSE file for details
************************************************/
// config file
require_once("backend/caldav/config.php");
// TODO: remove this after ZP-682 merge
require_once("include/z_caldav.php");
require_once("include/iCalendar.php");
require_once("include/z_RTF.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", CALDAV_MAX_SYNC_PERIOD);
$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("<C:filter><C:comp-filter name=\"VCALENDAR\"><C:comp-filter name=\"VEVENT\"><C:prop-filter name=\"UID\"><C:text-match>%s</C:text-match></C:prop-filter></C:comp-filter></C:comp-filter></C:filter>", $uid);
$events = $this->_caldav->DoCalendarQuery($filter, sprintf("%s%s", $this->_caldav_path, CALDAV_PERSONAL));
return $events;
}
/**
* Resolves recipients
*
* @param SyncObject $resolveRecipients
*
* @access public
* @return SyncObject $resolveRecipients
*/
public function ResolveRecipients($resolveRecipients) {
// TODO:
return false;
}
/**
* 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));
}
}
\ No newline at end of file
<?php
/***********************************************
* File : config.php
* Project : Z-Push
* Descr : CalDAV backend configuration file
*
* Created : 27.11.2012
*
* Copyright 2012 - 2014 Jean-Louis Dupond
*
* Jean-Louis Dupond released this code as AGPLv3 here: https://github.com/dupondje/PHP-Push-2/issues/93
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult LICENSE file for details
************************************************/
// ************************
// BackendCalDAV settings
// ************************
// Server protocol: http or https
define('CALDAV_PROTOCOL', 'https');
// Server name
define('CALDAV_SERVER', 'caldavserver.domain.com');
// Server port
define('CALDAV_PORT', '443');
// Path
define('CALDAV_PATH', '/caldav.php/%u/');
// Default CalDAV folder (calendar folder/principal). This will be marked as the default calendar in the mobile
define('CALDAV_PERSONAL', 'PRINCIPAL');
// If the CalDAV server supports the sync-collection operation
// DAViCal, SOGo and SabreDav support it
// SabreDav version must be at least 1.9.0, otherwise set this to false
// Setting this to false will work with most servers, but it will be slower
define('CALDAV_SUPPORTS_SYNC', false);
// Maximum period to sync.
// Some servers don't support more than 10 years so you will need to change this
define('CALDAV_MAX_SYNC_PERIOD', 2147483647);
\ No newline at end of file
......@@ -74,6 +74,9 @@ class BackendCombinedConfig {
'v' => array(
'name' => 'BackendVCardDir',
),
'c' => array(
'name' => 'BackendCalDAV',
),
),
'delimiter' => '/',
//force one type of folder to one backend
......
<?php
/**
* A Class for handling iCalendar data.
*
* When parsed the underlying structure is roughly as follows:
*
* iCalendar( array(iCalComponent), array(iCalProp) )
*
* each iCalComponent is similarly structured:
*
* iCalComponent( array(iCalComponent), array(iCalProp) )
*
* Once parsed, $ical->component will point to the wrapping VCALENDAR component of
* the iCalendar. This will be fine for simple iCalendar usage as sampled below,
* but more complex iCalendar such as a VEVENT with RRULE which has repeat overrides
* will need quite a bit more thought to process correctly.
*
* @example
* To create a new iCalendar from several data values:
* $ical = new iCalendar( array('DTSTART' => $dtstart, 'SUMMARY' => $summary, 'DURATION' => $duration ) );
*
* @example
* To render it as an iCalendar string:
* echo $ical->Render();
*
* @example
* To render just the VEVENTs in the iCalendar with a restricted list of properties:
* echo $ical->Render( false, 'VEVENT', array( 'DTSTART', 'DURATION', 'DTEND', 'RRULE', 'SUMMARY') );
*
* @example
* To parse an existing iCalendar string for manipulation:
* $ical = new iCalendar( array('icalendar' => $icalendar_text ) );
*
* @example
* To clear any 'VALARM' components in an iCalendar object
* $ical->component->ClearComponents('VALARM');
*
* @example
* To replace any 'RRULE' property in an iCalendar object
* $ical->component->SetProperties( 'RRULE', $rrule_definition );
*
* @package awl
* @subpackage iCalendar
* @author Andrew McMillan <andrew@mcmillan.net.nz>
* @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
* @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
*
*/
require_once('XMLElement.php');
/* Commented out, only needed by deprecated functions
require_once('AwlQuery.php');
*/
/**
* A Class for representing properties within an iCalendar
*
* @package awl
*/
class iCalProp {
/**#@+
* @access private
*/
/**
* The name of this property
*
* @var string
*/
var $name;
/**
* An array of parameters to this property, represented as key/value pairs.
*
* @var array
*/
var $parameters;
/**
* The value of this property.
*
* @var string
*/
var $content;
/**
* The original value that this was parsed from, if that's the way it happened.
*
* @var string
*/
var $rendered;
/**#@-*/
/**
* The constructor parses the incoming string, which is formatted as per RFC2445 as a
* propname[;param1=pval1[; ... ]]:propvalue
* however we allow ourselves to assume that the RFC2445 content unescaping has already
* happened when iCalComponent::ParseFrom() called iCalComponent::UnwrapComponent().
*
* @param string $propstring The string from the iCalendar which contains this property.
*/
function iCalProp( $propstring = null ) {
$this->name = "";
$this->content = "";
$this->parameters = array();
unset($this->rendered);
if ( $propstring != null && gettype($propstring) == 'string' ) {
$this->ParseFrom($propstring);
}
}
/**
* The constructor parses the incoming string, which is formatted as per RFC2445 as a
* propname[;param1=pval1[; ... ]]:propvalue
* however we allow ourselves to assume that the RFC2445 content unescaping has already
* happened when iCalComponent::ParseFrom() called iCalComponent::UnwrapComponent().
*
* @param string $propstring The string from the iCalendar which contains this property.
*/
function ParseFrom( $propstring ) {
// $this->rendered = (strlen($propstring) < 72 ? $propstring : null); // Only pre-rendered if we didn't unescape it
// FMBIETE - unset rendered content; if we alter some properties inside an object (VEVENT/ATTENDEE for example) we won't see the changes calling Render
// FIXME: if you find the bug, let me know
//$ical = new iCalComponent();
//$ical->ParseFrom(VCALENDAR DATA);
// Doing this will refresh the rendered data, but if this line is not executed, you won't see PARTSTAT changed
//$ical->SetPValue("METHOD", "REPLY");
//$ical->SetCPParameterValue("VEVENT", "ATTENDEE", "PARTSTAT", "ACCEPTED");
//printf("%s\n", $ical->Render());
unset($this->rendered);
$unescaped = preg_replace( '{\\\\[nN]}', "\n", $propstring);
// Split into two parts on : which is not preceded by a \
list( $start, $values) = preg_split( '{(?<!\\\\):}', $unescaped, 2);
$this->content = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $values);
// Split on ; which is not preceded by a \
$parameters = preg_split( '{(?<!\\\\);}', $start);
$parameters = explode(';',$start);
$this->name = array_shift( $parameters );
$this->parameters = array();
foreach( $parameters AS $k => $v ) {
$pos = strpos($v,'=');
$name = substr( $v, 0, $pos);
$value = substr( $v, $pos + 1);
$this->parameters[$name] = $value;
}
// dbg_error_log('iCalendar', " iCalProp::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) );
}
/**
* Get/Set name property
*
* @param string $newname [optional] A new name for the property
*
* @return string The name for the property.
*/
function Name( $newname = null ) {
if ( $newname != null ) {
$this->name = $newname;
if ( isset($this->rendered) ) unset($this->rendered);
// dbg_error_log('iCalendar', " iCalProp::Name(%s)", $this->name );
}
return $this->name;
}
/**
* Get/Set the content of the property
*
* @param string $newvalue [optional] A new value for the property
*
* @return string The value of the property.
*/
function Value( $newvalue = null ) {
if ( $newvalue != null ) {
$this->content = $newvalue;
if ( isset($this->rendered) ) unset($this->rendered);
}
return $this->content;
}
/**
* Get/Set parameters in their entirety
*
* @param array $newparams An array of new parameter key/value pairs
*
* @return array The current array of parameters for the property.
*/
function Parameters( $newparams = null ) {
if ( $newparams != null ) {
$this->parameters = $newparams;
if ( isset($this->rendered) ) unset($this->rendered);
}
return $this->parameters;
}
/**
* Test if our value contains a string
*
* @param string $search The needle which we shall search the haystack for.
*
* @return string The name for the property.
*/
function TextMatch( $search ) {
if ( isset($this->content) ) {
return (stristr( $this->content, $search ) !== false);
}
return false;
}
/**
* Get the value of a parameter
*
* @param string $name The name of the parameter to retrieve the value for
*
* @return string The value of the parameter
*/
function GetParameterValue( $name ) {
if ( isset($this->parameters[$name]) ) return $this->parameters[$name];
}
/**
* Set the value of a parameter
*
* @param string $name The name of the parameter to set the value for
*
* @param string $value The value of the parameter
*/
function SetParameterValue( $name, $value ) {
if ( isset($this->rendered) ) unset($this->rendered);
// Unset parameter
if ($value === null) {
unset($this->parameters[$name]);
}
else {
$this->parameters[$name] = $value;
}
}
/**
* Render the set of parameters as key1=value1[;key2=value2[; ...]] with
* any colons or semicolons escaped.
*/
function RenderParameters() {
$rendered = "";
foreach( $this->parameters AS $k => $v ) {
$escaped = preg_replace( "/([;:])/", '\\\\$1', $v);
$rendered .= sprintf( ";%s=%s", $k, $escaped );
}
return $rendered;
}
/**
* Render a suitably escaped RFC2445 content string.
*/
function Render() {
// If we still have the string it was parsed in from, it hasn't been screwed with
// and we can just return that without modification.
if ( isset($this->rendered) ) return $this->rendered;
$property = preg_replace( '/[;].*$/', '', $this->name );
$escaped = $this->content;
switch( $property ) {
/** Content escaping does not apply to these properties culled from RFC2445 */
case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY':
case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO':
case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID':
case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED':
case 'RRULE': case 'REPEAT': case 'TRIGGER':
break;
case 'COMPLETED': case 'DTEND':
case 'DUE': case 'DTSTART':
case 'DTSTAMP': case 'LAST-MODIFIED':
case 'CREATED': case 'EXDATE':
case 'RDATE':
if ( isset($this->parameters['VALUE']) && $this->parameters['VALUE'] == 'DATE' ) {
$escaped = substr( $escaped, 0, 8);
}
break;
/** Content escaping applies by default to other properties */
default:
$escaped = str_replace( '\\', '\\\\', $escaped);
$escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
$escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped);
}
$property = sprintf( "%s%s:", $this->name, $this->RenderParameters() );
if ( (strlen($property) + strlen($escaped)) <= 72 ) {
$this->rendered = $property . $escaped;
}
else if ( (strlen($property) + strlen($escaped)) > 72 && (strlen($property) < 72) && (strlen($escaped) < 72) ) {
$this->rendered = $property . "\r\n " . $escaped;
}
else {
$this->rendered = preg_replace( '/(.{72})/u', '$1'."\r\n ", $property . $escaped );
}
return $this->rendered;
}
}
/**
* A Class for representing components within an iCalendar
*
* @package awl
*/
class iCalComponent {
/**#@+
* @access private
*/
/**
* The type of this component, such as 'VEVENT', 'VTODO', 'VTIMEZONE', etc.
*
* @var string
*/
var $type;
/**
* An array of properties, which are iCalProp objects
*
* @var array
*/
var $properties;
/**
* An array of (sub-)components, which are iCalComponent objects
*
* @var array
*/
var $components;
/**
* The rendered result (or what was originally parsed, if there have been no changes)
*
* @var array
*/
var $rendered;
/**#@-*/
/**
* A basic constructor
*/
function iCalComponent( $content = null ) {
$this->type = "";
$this->properties = array();
$this->components = array();
$this->rendered = "";
if ( $content != null && (gettype($content) == 'string' || gettype($content) == 'array') ) {
$this->ParseFrom($content);
}
}
/**
* Apply standard properties for a VCalendar
* @param array $extra_properties Key/value pairs of additional properties
*/
function VCalendar( $extra_properties = null ) {
$this->SetType('VCALENDAR');
$this->AddProperty('PRODID', '-//davical.org//NONSGML AWL Calendar//EN');
$this->AddProperty('VERSION', '2.0');
$this->AddProperty('CALSCALE', 'GREGORIAN');
if ( is_array($extra_properties) ) {
foreach( $extra_properties AS $k => $v ) {
$this->AddProperty($k,$v);
}
}
}
/**
* Collect an array of all parameters of our properties which are the specified type
* Mainly used for collecting the full variety of references TZIDs
*/
function CollectParameterValues( $parameter_name ) {
$values = array();
foreach( $this->components AS $k => $v ) {
$also = $v->CollectParameterValues($parameter_name);
$values = array_merge( $values, $also );
}
foreach( $this->properties AS $k => $v ) {
$also = $v->GetParameterValue($parameter_name);
if ( isset($also) && $also != "" ) {
// dbg_error_log( 'iCalendar', "::CollectParameterValues(%s) : Found '%s'", $parameter_name, $also);
$values[$also] = 1;
}
}
return $values;
}
/**
* Parse the text $content into sets of iCalProp & iCalComponent within this iCalComponent
* @param string $content The raw RFC2445-compliant iCalendar component, including BEGIN:TYPE & END:TYPE
*/
function ParseFrom( $content ) {
$this->rendered = $content;
$content = $this->UnwrapComponent($content);
$type = false;
$subtype = false;
$finish = null;
$subfinish = null;
$length = strlen($content);
$linefrom = 0;
while( $linefrom < $length ) {
$lineto = strpos( $content, "\n", $linefrom );
if ( $lineto === false ) {
$lineto = strpos( $content, "\r", $linefrom );
}
if ( $lineto > 0 ) {
$line = substr( $content, $linefrom, $lineto - $linefrom);
$linefrom = $lineto + 1;
}
else {
$line = substr( $content, $linefrom );
$linefrom = $length;
}
if ( preg_match('/^\s*$/', $line ) ) continue;
$line = rtrim( $line, "\r\n" );
// dbg_error_log( 'iCalendar', "::ParseFrom: Parsing line: $line");
if ( $type === false ) {
if ( preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
// We have found the start of the main component
$type = $matches[1];
$finish = "END:$type";
$this->type = $type;
dbg_error_log( 'iCalendar', "::ParseFrom: Start component of type '%s'", $type);
}
else {
dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap before start of component: $line");
// unset($lines[$k]); // The content has crap before the start
if ( $line != "" ) $this->rendered = null;
}
}
else if ( $type == null ) {
dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap after end of component");
if ( $line != "" ) $this->rendered = null;
}
else if ( $line == $finish ) {
dbg_error_log( 'iCalendar', "::ParseFrom: End of component");
$type = null; // We have reached the end of our component
}
else {
if ( $subtype === false && preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
// We have found the start of a sub-component
$subtype = $matches[1];
$subfinish = "END:$subtype";
$subcomponent = $line . "\r\n";
dbg_error_log( 'iCalendar', "::ParseFrom: Found a subcomponent '%s'", $subtype);
}
else if ( $subtype ) {
// We are inside a sub-component
$subcomponent .= $this->WrapComponent($line);
if ( $line == $subfinish ) {
dbg_error_log( 'iCalendar', "::ParseFrom: End of subcomponent '%s'", $subtype);
// We have found the end of a sub-component
$this->components[] = new iCalComponent($subcomponent);
$subtype = false;
}
// else
// dbg_error_log( 'iCalendar', "::ParseFrom: Inside a subcomponent '%s'", $subtype );
}
else {
// dbg_error_log( 'iCalendar', "::ParseFrom: Parse property of component");
// It must be a normal property line within a component.
$this->properties[] = new iCalProp($line);
}
}
}
}
/**
* This unescapes the (CRLF + linear space) wrapping specified in RFC2445. According
* to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising
* XML parsers often muck with it and may remove the CR. We accept either case.
*/
function UnwrapComponent( $content ) {
return preg_replace('/\r?\n[ \t]/', '', $content );
}
/**
* This imposes the (CRLF + linear space) wrapping specified in RFC2445. According
* to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising
* XML parsers often muck with it and may remove the CR. We output RFC2445 compliance.
*
* In order to preserve pre-existing wrapping in the component, we split the incoming
* string on line breaks before running wordwrap over each component of that.
*/
function WrapComponent( $content ) {
$strs = preg_split( "/\r?\n/", $content );
$wrapped = "";
foreach ($strs as $str) {
$wrapped .= preg_replace( '/(.{72})/u', '$1'."\r\n ", $str ) ."\r\n";
}
return $wrapped;
}
/**
* Return the type of component which this is
*/
function GetType() {
return $this->type;
}
/**
* Set the type of component which this is
*/
function SetType( $type ) {
if ( isset($this->rendered) ) unset($this->rendered);
$this->type = $type;
return $this->type;
}
/**
* Get all properties, or the properties matching a particular type
*/
function GetProperties( $type = null ) {
$properties = array();
foreach( $this->properties AS $k => $v ) {
if ( $type == null || $v->Name() == $type ) {
$properties[$k] = $v;
}
}
return $properties;
}
/**
* Get the value of the first property matching the name. Obviously this isn't
* so useful for properties which may occur multiply, but most don't.
*
* @param string $type The type of property we are after.
* @return string The value of the property, or null if there was no such property.
*/
function GetPValue( $type ) {
foreach( $this->properties AS $k => $v ) {
if ( $v->Name() == $type ) return $v->Value();
}
return null;
}
/**
* Set the value of all properties matching the name.
*
* @param string $type The type/name of property we are after
* @param string $value The value of the property
*/
function SetPValue( $type, $value ) {
for ( $i = 0; $i < count($this->properties); $i++ ) {
if ( $this->properties[$i]->Name() == $type ) {
if ( isset($this->rendered) ) unset($this->rendered);
// FMBIETE - unset property
if ($value == null) {
unset($this->properties[$i]);
}
else {
$this->properties[$i]->Value($value);
}
}
}
}
/**
* Set the value of all the parameters matching the name. Component -> Property -> Parameter
*
* @param string $component_type Type of the component
* @param string $property_name Type/Name of the property
* @param string $parameter_name Type/Name of the parameter
* @param string $value New value of the parameter
* @param string $condition_value Change the parameter_value only if the property_value is equals to condition_value
*/
function SetCPParameterValue( $component_type, $property_name, $parameter_name, $value, $condition_value = null ) {
for ( $j = 0; $j < count($this->components); $j++ ) {
if ( $this->components[$j]->GetType() == $component_type ) {
for ( $i = 0; $i < count($this->components[$j]->properties); $i++ ) {
if ( $this->components[$j]->properties[$i]->Name() == $property_name ) {
if ( isset($this->components[$j]->rendered) ) unset($this->components[$j]->rendered);
if ($condition_value === null) {
$this->components[$j]->properties[$i]->SetParameterValue($parameter_name, $value);
}
else {
if (strcasecmp($this->components[$j]->properties[$i]->Value(), $condition_value) == 0) {
$this->components[$j]->properties[$i]->SetParameterValue($parameter_name, $value);
}
}
}
}
}
}
}
/**
* Get the value of the specified parameter for the first property matching the
* name. Obviously this isn't so useful for properties which may occur multiply, but most don't.
*
* @param string $type The type of property we are after.
* @param string $type The name of the parameter we are after.
* @return string The value of the parameter for the property, or null in the case that there was no such property, or no such parameter.
*/
function GetPParamValue( $type, $parameter_name ) {
foreach( $this->properties AS $k => $v ) {
if ( $v->Name() == $type ) return $v->GetParameterValue($parameter_name);
}
return null;
}
/**
* Clear all properties, or the properties matching a particular type
* @param string $type The type of property - omit for all properties
*/
function ClearProperties( $type = null ) {
if ( $type != null ) {
// First remove all the existing ones of that type
foreach( $this->properties AS $k => $v ) {
if ( $v->Name() == $type ) {
unset($this->properties[$k]);
if ( isset($this->rendered) ) unset($this->rendered);
}
}
$this->properties = array_values($this->properties);
}
else {
if ( isset($this->rendered) ) unset($this->rendered);
$this->properties = array();
}
}
/**
* Set all properties, or the ones matching a particular type
*/
function SetProperties( $new_properties, $type = null ) {
if ( isset($this->rendered) && count($new_properties) > 0 ) unset($this->rendered);
$this->ClearProperties($type);
foreach( $new_properties AS $k => $v ) {
$this->AddProperty($v);
}
}
/**
* Adds a new property
*
* @param iCalProp $new_property The new property to append to the set, or a string with the name
* @param string $value The value of the new property (default: param 1 is an iCalProp with everything
* @param array $parameters The key/value parameter pairs (default: none, or param 1 is an iCalProp with everything)
*/
function AddProperty( $new_property, $value = null, $parameters = null ) {
if ( isset($this->rendered) ) unset($this->rendered);
if ( isset($value) && gettype($new_property) == 'string' ) {
$new_prop = new iCalProp();
$new_prop->Name($new_property);
$new_prop->Value($value);
if ( $parameters != null ) $new_prop->Parameters($parameters);
dbg_error_log('iCalendar'," Adding new property '%s'", $new_prop->Render() );
$this->properties[] = $new_prop;
}
else if ( gettype($new_property) ) {
$this->properties[] = $new_property;
}
}
/**
* Get all sub-components, or at least get those matching a type
* @return array an array of the sub-components
*/
function &FirstNonTimezone( $type = null ) {
foreach( $this->components AS $k => $v ) {
if ( $v->GetType() != 'VTIMEZONE' ) return $this->components[$k];
}
$result = false;
return $result;
}
/**
* Return true if the person identified by the email address is down as an
* organizer for this meeting.
* @param string $email The e-mail address of the person we're seeking.
* @return boolean true if we found 'em, false if we didn't.
*/
function IsOrganizer( $email ) {
if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:$email';
$props = $this->GetPropertiesByPath('!VTIMEZONE/ORGANIZER');
foreach( $props AS $k => $prop ) {
if ( $prop->Value() == $email ) return true;
}
return false;
}
/**
* Return true if the person identified by the email address is down as an
* attendee or organizer for this meeting.
* @param string $email The e-mail address of the person we're seeking.
* @return boolean true if we found 'em, false if we didn't.
*/
function IsAttendee( $email ) {
if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:$email';
if ( $this->IsOrganizer($email) ) return true; /** an organizer is an attendee, as far as we're concerned */
$props = $this->GetPropertiesByPath('!VTIMEZONE/ATTENDEE');
foreach( $props AS $k => $prop ) {
if ( $prop->Value() == $email ) return true;
}
return false;
}
/**
* Get all sub-components, or at least get those matching a type, or failling to match,
* should the second parameter be set to false.
*
* @param string $type The type to match (default: All)
* @param boolean $normal_match Set to false to invert the match (default: true)
* @return array an array of the sub-components
*/
function GetComponents( $type = null, $normal_match = true ) {
$components = $this->components;
if ( $type != null ) {
foreach( $components AS $k => $v ) {
if ( ($v->GetType() != $type) === $normal_match ) {
unset($components[$k]);
}
}
$components = array_values($components);
}
return $components;
}
/**
* Clear all components, or the components matching a particular type
* @param string $type The type of component - omit for all components
*/
function ClearComponents( $type = null ) {
if ( $type != null ) {
// First remove all the existing ones of that type
foreach( $this->components AS $k => $v ) {
if ( $v->GetType() == $type ) {
unset($this->components[$k]);
if ( isset($this->rendered) ) unset($this->rendered);
}
else {
if ( ! $this->components[$k]->ClearComponents($type) ) {
if ( isset($this->rendered) ) unset($this->rendered);
}
}
}
return isset($this->rendered);
}
else {
if ( isset($this->rendered) ) unset($this->rendered);
$this->components = array();
}
}
/**
* Sets some or all sub-components of the component to the supplied new components
*
* @param array of iCalComponent $new_components The new components to replace the existing ones
* @param string $type The type of components to be replaced. Defaults to null, which means all components will be replaced.
*/
function SetComponents( $new_component, $type = null ) {
if ( isset($this->rendered) ) unset($this->rendered);
if ( count($new_component) > 0 ) $this->ClearComponents($type);
foreach( $new_component AS $k => $v ) {
$this->components[] = $v;
}
}
/**
* Adds a new subcomponent
*
* @param iCalComponent $new_component The new component to append to the set
*/
function AddComponent( $new_component ) {
if ( is_array($new_component) && count($new_component) == 0 ) return;
if ( isset($this->rendered) ) unset($this->rendered);
if ( is_array($new_component) ) {
foreach( $new_component AS $k => $v ) {
$this->components[] = $v;
}
}
else {
$this->components[] = $new_component;
}
}
/**
* Mask components, removing any that are not of the types in the list
* @param array $keep An array of component types to be kept
*/
function MaskComponents( $keep ) {
foreach( $this->components AS $k => $v ) {
if ( ! in_array( $v->GetType(), $keep ) ) {
unset($this->components[$k]);
if ( isset($this->rendered) ) unset($this->rendered);
}
else {
$v->MaskComponents($keep);
}
}
}
/**
* Mask properties, removing any that are not in the list
* @param array $keep An array of property names to be kept
* @param array $component_list An array of component types to check within
*/
function MaskProperties( $keep, $component_list=null ) {
foreach( $this->components AS $k => $v ) {
$v->MaskProperties($keep, $component_list);
}
if ( !isset($component_list) || in_array($this->GetType(),$component_list) ) {
foreach( $this->components AS $k => $v ) {
if ( ! in_array( $v->GetType(), $keep ) ) {
unset($this->components[$k]);
if ( isset($this->rendered) ) unset($this->rendered);
}
}
}
}
/**
* Clone this component (and subcomponents) into a confidential version of it. A confidential
* event will be scrubbed of any identifying characteristics other than time/date, repeat, uid
* and a summary which is just a translated 'Busy'.
*/
function CloneConfidential() {
$confidential = clone($this);
$keep_properties = array( 'DTSTAMP', 'DTSTART', 'RRULE', 'DURATION', 'DTEND', 'DUE', 'UID', 'CLASS', 'TRANSP', 'CREATED', 'LAST-MODIFIED' );
$resource_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' );
$confidential->MaskComponents(array( 'VTIMEZONE', 'VEVENT', 'VTODO', 'VJOURNAL' ));
$confidential->MaskProperties($keep_properties, $resource_components );
if ( in_array( $confidential->GetType(), $resource_components ) ) {
$confidential->AddProperty( 'SUMMARY', translate('Busy') );
}
foreach( $confidential->components AS $k => $v ) {
if ( in_array( $v->GetType(), $resource_components ) ) {
$v->AddProperty( 'SUMMARY', translate('Busy') );
}
}
return $confidential;
}
/**
* Renders the component, possibly restricted to only the listed properties
*/
function Render( $restricted_properties = null) {
$unrestricted = (!isset($restricted_properties) || count($restricted_properties) == 0);
if ( isset($this->rendered) && $unrestricted )
return $this->rendered;
$rendered = "BEGIN:$this->type\r\n";
foreach( $this->properties AS $k => $v ) {
if ( method_exists($v, 'Render') ) {
if ( $unrestricted || isset($restricted_properties[$v]) ) $rendered .= $v->Render() . "\r\n";
}
}
foreach( $this->components AS $v ) { $rendered .= $v->Render(); }
$rendered .= "END:$this->type\r\n";
$rendered = preg_replace('{(?<!\r)\n}', "\r\n", $rendered);
if ( $unrestricted ) $this->rendered = $rendered;
return $rendered;
}
/**
* Return an array of properties matching the specified path
*
* @return array An array of iCalProp within the tree which match the path given, in the form
* [/]COMPONENT[/...]/PROPERTY in a syntax kind of similar to our poor man's XML queries. We
* also allow COMPONENT and PROPERTY to be !COMPONENT and !PROPERTY for ++fun.
*
* @note At some point post PHP4 this could be re-done with an iterator, which should be more efficient for common use cases.
*/
function GetPropertiesByPath( $path ) {
$properties = array();
dbg_error_log( 'iCalendar', "GetPropertiesByPath: Querying within '%s' for path '%s'", $this->type, $path );
if ( !preg_match( '#(/?)(!?)([^/]+)(/?.*)$#', $path, $matches ) ) return $properties;
$adrift = ($matches[1] == '');
$normal = ($matches[2] == '');
$ourtest = $matches[3];
$therest = $matches[4];
dbg_error_log( 'iCalendar', "GetPropertiesByPath: Matches: %s -- %s -- %s -- %s\n", $matches[1], $matches[2], $matches[3], $matches[4] );
if ( $ourtest == '*' || (($ourtest == $this->type) === $normal) && $therest != '' ) {
if ( preg_match( '#^/(!?)([^/]+)$#', $therest, $matches ) ) {
$normmatch = ($matches[1] =='');
$proptest = $matches[2];
foreach( $this->properties AS $k => $v ) {
if ( $proptest == '*' || (($v->Name() == $proptest) === $normmatch ) ) {
$properties[] = $v;
}
}
}
else {
/**
* There is more to the path, so we recurse into that sub-part
*/
foreach( $this->components AS $k => $v ) {
$properties = array_merge( $properties, $v->GetPropertiesByPath($therest) );
}
}
}
if ( $adrift ) {
/**
* Our input $path was not rooted, so we recurse further
*/
foreach( $this->components AS $k => $v ) {
$properties = array_merge( $properties, $v->GetPropertiesByPath($path) );
}
}
dbg_error_log('iCalendar', "GetPropertiesByPath: Found %d within '%s' for path '%s'\n", count($properties), $this->type, $path );
return $properties;
}
}
/**
************************************************************************************
* Everything below here is deprecated and should be avoided in favour
* of using, improving and enhancing the more sensible structures above.
************************************************************************************
*/
/**
* A Class for handling Events on a calendar (DEPRECATED)
*
* @package awl
*/
class iCalendar { // DEPRECATED
/**#@+
* @access private
*/
/**
* The component-ised version of the iCalendar
* @var component iCalComponent
*/
var $component;
/**
* An array of arbitrary properties, containing arbitrary arrays of arbitrary properties
* @var properties array
*/
var $properties;
/**
* An array of the lines of this iCalendar resource
* @var lines array
*/
var $lines;
/**
* The typical location name for the standard timezone such as "Pacific/Auckland"
* @var tz_locn string
*/
var $tz_locn;
/**
* The type of iCalendar data VEVENT/VTODO/VJOURNAL
* @var type string
*/
var $type;
/**#@-*/
/**
* @DEPRECATED: This class will be removed soon.
* The constructor takes an array of args. If there is an element called 'icalendar'
* then that will be parsed into the iCalendar object. Otherwise the array elements
* are converted into properties of the iCalendar object directly.
*/
function iCalendar( $args ) {
global $c;
deprecated('iCalendar::iCalendar');
$this->tz_locn = "";
if ( !isset($args) || !(is_array($args) || is_object($args)) ) return;
if ( is_object($args) ) {
settype($args,'array');
}
$this->component = new iCalComponent();
if ( isset($args['icalendar']) ) {
$this->component->ParseFrom($args['icalendar']);
$this->lines = preg_split('/\r?\n/', $args['icalendar'] );
$this->SaveTimeZones();
$first =& $this->component->FirstNonTimezone();
if ( $first ) {
$this->type = $first->GetType();
$this->properties = $first->GetProperties();
}
else {
$this->properties = array();
}
$this->properties['VCALENDAR'] = array('***ERROR*** This class is being referenced in an unsupported way!');
return;
}
if ( isset($args['type'] ) ) {
$this->type = $args['type'];
unset( $args['type'] );
}
else {
$this->type = 'VEVENT'; // Default to event
}
$this->component->SetType('VCALENDAR');
$this->component->SetProperties(
array(
new iCalProp('PRODID:-//davical.org//NONSGML AWL Calendar//EN'),
new iCalProp('VERSION:2.0'),
new iCalProp('CALSCALE:GREGORIAN')
)
);
$first = new iCalComponent();
$first->SetType($this->type);
$this->properties = array();
foreach( $args AS $k => $v ) {
dbg_error_log( 'iCalendar', ":Initialise: %s to >>>%s<<<", $k, $v );
$property = new iCalProp();
$property->Name($k);
$property->Value($v);
$this->properties[] = $property;
}
$first->SetProperties($this->properties);
$this->component->SetComponents( array($first) );
$this->properties['VCALENDAR'] = array('***ERROR*** This class is being referenced in an unsupported way!');
/**
* @todo Need to handle timezones!!!
*/
if ( $this->tz_locn == "" ) {
$this->tz_locn = $this->Get("tzid");
if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) {
$this->tz_locn = $c->local_tzid;
}
}
}
/**
* @DEPRECATED: This class will be removed soon.
* Save any timezones by TZID in the PostgreSQL database for future re-use.
*/
function SaveTimeZones() {
global $c;
deprecated('iCalendar::SaveTimeZones');
$this->tzid_list = array_keys($this->component->CollectParameterValues('TZID'));
if ( ! isset($this->tzid) && count($this->tzid_list) > 0 ) {
dbg_error_log( 'iCalendar', "::TZID_List[0] = '%s', count=%d", $this->tzid_list[0], count($this->tzid_list) );
$this->tzid = $this->tzid_list[0];
}
$timezones = $this->component->GetComponents('VTIMEZONE');
if ( $timezones === false || count($timezones) == 0 ) return;
$this->vtimezone = $timezones[0]->Render(); // Backward compatibility
$tzid = $this->Get('TZID');
if ( isset($c->save_time_zone_defs) && $c->save_time_zone_defs ) {
foreach( $timezones AS $k => $tz ) {
$tzid = $tz->GetPValue('TZID');
$qry = new AwlQuery( "SELECT tz_locn FROM time_zone WHERE tz_id = ?;", $tzid );
if ( $qry->Exec('iCalendar') && $qry->rows() == 1 ) {
$row = $qry->Fetch();
if ( !isset($first_tzid) ) $first_tzid = $row->tz_locn;
continue;
}
if ( $tzid != "" && $qry->rows() == 0 ) {
$tzname = $tz->GetPValue('X-LIC-LOCATION');
if ( !isset($tzname) ) $tzname = olson_from_tzstring($tzid);
$qry2 = new AwlQuery( "INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES( ?, ?, ? );",
$tzid, $tzname, $tz->Render() );
$qry2->Exec('iCalendar');
}
}
}
if ( ! isset($this->tzid) && isset($first_tzid) ) $this->tzid = $first_tzid;
if ( (!isset($this->tz_locn) || $this->tz_locn == '') && isset($first_tzid) && $first_tzid != '' ) {
$tzname = preg_replace('#^(.*[^a-z])?([a-z]+/[a-z]+)$#i','$2', $first_tzid );
if ( preg_match( '#\S+/\S+#', $tzname) ) {
$this->tz_locn = $tzname;
}
dbg_error_log( 'iCalendar', " TZCrap1: TZID '%s', Location '%s', Perhaps: %s", $tzid, $this->tz_locn, $tzname );
}
if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) {
$this->tz_locn = $c->local_tzid;
}
if ( ! isset($this->tzid) && isset($this->tz_locn) ) $this->tzid = $this->tz_locn;
}
/**
* An array of property names that we should always want when rendering an iCalendar
*
* @DEPRECATED: This class will be removed soon.
* @todo Remove this function.
*/
function DefaultPropertyList() {
dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'DefaultPropertyList' );
return array( "UID" => 1, "DTSTAMP" => 1, "DTSTART" => 1, "DURATION" => 1,
"LAST-MODIFIED" => 1,"CLASS" => 1, "TRANSP" => 1, "SEQUENCE" => 1,
"DUE" => 1, "SUMMARY" => 1, "RRULE" => 1 );
}
/**
* A function to extract the contents of a BEGIN:SOMETHING to END:SOMETHING (perhaps multiply)
* and return just that bit (or, of course, those bits :-)
*
* @var string The type of thing(s) we want returned.
* @var integer The number of SOMETHINGS we want to get.
*
* @return string A string from BEGIN:SOMETHING to END:SOMETHING, possibly multiple of these
*
* @DEPRECATED: This class will be removed soon.
* @todo Remove this function.
*/
function JustThisBitPlease( $type, $count=1 ) {
deprecated('iCalendar::JustThisBitPlease' );
$answer = "";
$intags = false;
$start = "BEGIN:$type";
$finish = "END:$type";
dbg_error_log( 'iCalendar', ":JTBP: Looking for %d subsets of type %s", $count, $type );
reset($this->lines);
foreach( $this->lines AS $k => $v ) {
if ( !$intags && $v == $start ) {
$answer .= $v . "\n";
$intags = true;
}
else if ( $intags && $v == $finish ) {
$answer .= $v . "\n";
$intags = false;
}
else if ( $intags ) {
$answer .= $v . "\n";
}
}
return $answer;
}
/**
* Function to parse lines from BEGIN:SOMETHING to END:SOMETHING into a nested array structure
*
* @var string The "SOMETHING" from the BEGIN:SOMETHING line we just met
* @return arrayref An array of the things we found between (excluding) the BEGIN & END, some of which might be sub-arrays
*
* @DEPRECATED: This class will be removed soon.
* @todo Remove this function.
*/
function &ParseSomeLines( $type ) {
deprecated('iCalendar::ParseSomeLines' );
$props = array();
$properties =& $props;
while( isset($this->lines[$this->_current_parse_line]) ) {
$i = $this->_current_parse_line++;
$line =& $this->lines[$i];
dbg_error_log( 'iCalendar', ":Parse:%s LINE %03d: >>>%s<<<", $type, $i, $line );
if ( $this->parsing_vtimezone ) {
$this->vtimezone .= $line."\n";
}
if ( preg_match( '/^(BEGIN|END):([^:]+)$/', $line, $matches ) ) {
if ( $matches[1] == 'END' && $matches[2] == $type ) {
if ( $type == 'VTIMEZONE' ) {
$this->parsing_vtimezone = false;
}
return $properties;
}
else if( $matches[1] == 'END' ) {
dbg_error_log("ERROR"," iCalendar: parse error: Unexpected END:%s when we were looking for END:%s", $matches[2], $type );
return $properties;
}
else if( $matches[1] == 'BEGIN' ) {
$subtype = $matches[2];
if ( $subtype == 'VTIMEZONE' ) {
$this->parsing_vtimezone = true;
$this->vtimezone = $line."\n";
}
if ( !isset($properties['INSIDE']) ) $properties['INSIDE'] = array();
$properties['INSIDE'][] = $subtype;
if ( !isset($properties[$subtype]) ) $properties[$subtype] = array();
$properties[$subtype][] = $this->ParseSomeLines($subtype);
}
}
else {
// Parse the property
@list( $property, $value ) = explode(':', $line, 2 );
if ( strpos( $property, ';' ) > 0 ) {
$parameterlist = explode(';', $property );
$property = array_shift($parameterlist);
foreach( $parameterlist AS $pk => $pv ) {
if ( $pv == "VALUE=DATE" ) {
$value .= 'T000000';
}
elseif ( preg_match('/^([^;:=]+)=([^;:=]+)$/', $pv, $matches) ) {
switch( $matches[1] ) {
case 'TZID': $properties['TZID'] = $matches[2]; break;
default:
dbg_error_log( 'iCalendar', " FYI: Ignoring Resource '%s', Property '%s', Parameter '%s', Value '%s'", $type, $property, $matches[1], $matches[2] );
}
}
}
}
if ( $this->parsing_vtimezone && (!isset($this->tz_locn) || $this->tz_locn == "") && $property == 'X-LIC-LOCATION' ) {
$this->tz_locn = $value;
}
$properties[strtoupper($property)] = $this->RFC2445ContentUnescape($value);
}
}
return $properties;
}
/**
* Build the iCalendar object from a text string which is a single iCalendar resource
*
* @var string The RFC2445 iCalendar resource to be parsed
*
* @DEPRECATED: This class will be removed soon.
* @todo Remove this function.
*/
function BuildFromText( $icalendar ) {
deprecated('iCalendar::BuildFromText' );
/**
* This unescapes the (CRLF + linear space) wrapping specified in RFC2445. According
* to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising
* XML parsers often muck with it and may remove the CR.
*/
$icalendar = preg_replace('/\r?\n[ \t]/', '', $icalendar );
$this->lines = preg_split('/\r?\n/', $icalendar );
$this->_current_parse_line = 0;
$this->properties = $this->ParseSomeLines('');
/**
* Our 'type' is the type of non-timezone inside a VCALENDAR
*/
if ( isset($this->properties['VCALENDAR'][0]['INSIDE']) ) {
foreach ( $this->properties['VCALENDAR'][0]['INSIDE'] AS $k => $v ) {
if ( $v == 'VTIMEZONE' ) continue;
$this->type = $v;
break;
}
}
}
/**
* Returns a content string with the RFC2445 escaping removed
*
* @param string $escaped The incoming string to be escaped.
* @return string The string with RFC2445 content escaping removed.
*
* @DEPRECATED: This class will be removed soon.
* @todo Remove this function.
*/
function RFC2445ContentUnescape( $escaped ) {
deprecated( 'RFC2445ContentUnescape' );
$unescaped = str_replace( '\\n', "\n", $escaped);
$unescaped = str_replace( '\\N', "\n", $unescaped);
$unescaped = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $unescaped);
return $unescaped;
}
/**
* Do what must be done with time zones from on file. Attempt to turn
* them into something that PostgreSQL can understand...
*
* @DEPRECATED: This class will be removed soon.
* @todo Remove this function.
*/
function DealWithTimeZones() {
global $c;
deprecated('iCalendar::DealWithTimeZones' );
$tzid = $this->Get('TZID');
if ( isset($c->save_time_zone_defs) && $c->save_time_zone_defs ) {
$qry = new AwlQuery( "SELECT tz_locn FROM time_zone WHERE tz_id = ?;", $tzid );
if ( $qry->Exec('iCalendar') && $qry->rows() == 1 ) {
$row = $qry->Fetch();
$this->tz_locn = $row->tz_locn;
}
dbg_error_log( 'iCalendar', " TZCrap2: TZID '%s', DB Rows=%d, Location '%s'", $tzid, $qry->rows(), $this->tz_locn );
}
if ( (!isset($this->tz_locn) || $this->tz_locn == '') && $tzid != '' ) {
/**
* In case there was no X-LIC-LOCATION defined, let's hope there is something in the TZID
* that we can use. We are looking for a string like "Pacific/Auckland" if possible.
*/
$tzname = preg_replace('#^(.*[^a-z])?([a-z]+/[a-z]+)$#i','$1',$tzid );
/**
* Unfortunately this kind of thing will never work well :-(
*
if ( strstr( $tzname, ' ' ) ) {
$words = preg_split('/\s/', $tzname );
$tzabbr = '';
foreach( $words AS $i => $word ) {
$tzabbr .= substr( $word, 0, 1);
}
$this->tz_locn = $tzabbr;
}
*/
if ( preg_match( '#\S+/\S+#', $tzname) ) {
$this->tz_locn = $tzname;
}
dbg_error_log( 'iCalendar', " TZCrap3: TZID '%s', Location '%s', Perhaps: %s", $tzid, $this->tz_locn, $tzname );
}
if ( $tzid != '' && isset($c->save_time_zone_defs) && $c->save_time_zone_defs && $qry->rows() != 1 && isset($this->vtimezone) && $this->vtimezone != "" ) {
$qry2 = new AwlQuery( "INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES( ?, ?, ? );",
$tzid, $this->tz_locn, $this->vtimezone );
$qry2->Exec('iCalendar');
}
if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) {
$this->tz_locn = $c->local_tzid;
}
}
/**
* Get the value of a property in the first non-VTIMEZONE
* @DEPRECATED: This class will be removed soon.
*/
function Get( $key ) {
deprecated('iCalendar::Get' );
if ( strtoupper($key) == 'TZID' ) {
// backward compatibility hack
dbg_error_log( 'iCalendar', " Get(TZID): TZID '%s', Location '%s'", (isset($this->tzid)?$this->tzid:"[not set]"), $this->tz_locn );
if ( isset($this->tzid) ) return $this->tzid;
return $this->tz_locn;
}
/**
* The property we work on is the first non-VTIMEZONE we find.
*/
$component =& $this->component->FirstNonTimezone();
if ( $component === false ) return null;
return $component->GetPValue(strtoupper($key));
}
/**
* Set the value of a property
* @DEPRECATED: This class will be removed soon.
*/
function Set( $key, $value ) {
deprecated('iCalendar::Set' );
if ( $value == "" ) return;
$key = strtoupper($key);
$property = new iCalProp();
$property->Name($key);
$property->Value($value);
if (isset($this->component->rendered) ) unset( $this->component->rendered );
$component =& $this->component->FirstNonTimezone();
$component->SetProperties( array($property), $key);
return $this->Get($key);
}
/**
* @DEPRECATED: This class will be removed soon.
* Add a new property/value, regardless of whether it exists already
*
* @param string $key The property key
* @param string $value The property value
* @param string $parameters Any parameters to set for the property, as an array of key/value pairs
*/
function Add( $key, $value, $parameters = null ) {
deprecated('iCalendar::Add' );
if ( $value == "" ) return;
$key = strtoupper($key);
$property = new iCalProp();
$property->Name($key);
$property->Value($value);
if ( isset($parameters) && is_array($parameters) ) {
$property->parameters = $parameters;
}
$component =& $this->component->FirstNonTimezone();
$component->AddProperty($property);
if (isset($this->component->rendered) ) unset( $this->component->rendered );
}
/**
* @DEPRECATED: This class will be removed soon.
* Get all sub-components, or at least get those matching a type, or failling to match,
* should the second parameter be set to false.
*
* @param string $type The type to match (default: All)
* @param boolean $normal_match Set to false to invert the match (default: true)
* @return array an array of the sub-components
*/
function GetComponents( $type = null, $normal_match = true ) {
deprecated('iCalendar::GetComponents' );
return $this->component->GetComponents($type,$normal_match);
}
/**
* @DEPRECATED: This class will be removed soon.
* Clear all components, or the components matching a particular type
* @param string $type The type of component - omit for all components
*/
function ClearComponents( $type = null ) {
deprecated('iCalendar::ClearComponents' );
$this->component->ClearComponents($type);
}
/**
* @DEPRECATED: This class will be removed soon.
* Sets some or all sub-components of the component to the supplied new components
*
* @param array of iCalComponent $new_components The new components to replace the existing ones
* @param string $type The type of components to be replaced. Defaults to null, which means all components will be replaced.
*/
function SetComponents( $new_component, $type = null ) {
deprecated('iCalendar::SetComponents' );
$this->component->SetComponents( $new_component, $type );
}
/**
* @DEPRECATED: This class will be removed soon.
* Adds a new subcomponent
*
* @param iCalComponent $new_component The new component to append to the set
*/
function AddComponent( $new_component ) {
deprecated('iCalendar::AddComponent' );
$this->component->AddComponent($new_component);
}
/**
* @DEPRECATED: This class will be removed soon.
* Mask components, removing any that are not of the types in the list
* @param array $keep An array of component types to be kept
*/
function MaskComponents( $keep ) {
deprecated('iCalendar::MaskComponents' );
$this->component->MaskComponents($keep);
}
/**
* @DEPRECATED: This class will be removed soon.
* Returns a PostgreSQL Date Format string suitable for returning HTTP (RFC2068) dates
* Preferred is "Sun, 06 Nov 1994 08:49:37 GMT" so we do that.
*/
static function HttpDateFormat() {
return "'Dy, DD Mon IYYY HH24:MI:SS \"GMT\"'";
}
/**
* @DEPRECATED: This class will be removed soon.
* Returns a PostgreSQL Date Format string suitable for returning iCal dates
*/
static function SqlDateFormat() {
return "'YYYYMMDD\"T\"HH24MISS'";
}
/**
* @DEPRECATED: This class will be removed soon.
* Returns a PostgreSQL Date Format string suitable for returning dates which
* have been cast to UTC
*/
static function SqlUTCFormat() {
return "'YYYYMMDD\"T\"HH24MISS\"Z\"'";
}
/**
* @DEPRECATED: This class will be removed soon.
* Returns a PostgreSQL Date Format string suitable for returning iCal durations
* - this doesn't work for negative intervals, but events should not have such!
*/
static function SqlDurationFormat() {
return "'\"PT\"HH24\"H\"MI\"M\"'";
}
/**
* @DEPRECATED: This class will be removed soon.
* Returns a suitably escaped RFC2445 content string.
*
* @param string $name The incoming name[;param] prefixing the string.
* @param string $value The incoming string to be escaped.
*
* @deprecated This function is deprecated and will be removed eventually.
* @todo Remove this function.
*/
function RFC2445ContentEscape( $name, $value ) {
deprecated('iCalendar::RFC2445ContentEscape' );
$property = preg_replace( '/[;].*$/', '', $name );
switch( $property ) {
/** Content escaping does not apply to these properties culled from RFC2445 */
case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY':
case 'COMPLETED': case 'DTEND': case 'DUE': case 'DTSTART':
case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO':
case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID':
case 'URL': case 'EXDATE': case 'EXRULE': case 'RDATE':
case 'RRULE': case 'REPEAT': case 'TRIGGER': case 'CREATED':
case 'DTSTAMP': case 'LAST-MODIFIED': case 'SEQUENCE':
break;
/** Content escaping applies by default to other properties */
default:
$value = str_replace( '\\', '\\\\', $value);
$value = preg_replace( '/\r?\n/', '\\n', $value);
$value = preg_replace( "/([,;:\"])/", '\\\\$1', $value);
}
$result = preg_replace( '/(.{72})/u', '$1'."\r\n ", $name.':'.$value ) ."\r\n";
return $result;
}
/**
* @DEPRECATED: This class will be removed soon.
* Return all sub-components of the given type, which are part of the
* component we pass in as an array of lines.
*
* @param array $component The component to be parsed
* @param string $type The type of sub-components to be extracted
* @param int $count The number of sub-components to extract (default: 9999)
*
* @return array The sub-component lines
*/
function ExtractSubComponent( $component, $type, $count=9999 ) {
deprecated('iCalendar::ExtractSubComponent' );
$answer = array();
$intags = false;
$start = "BEGIN:$type";
$finish = "END:$type";
dbg_error_log( 'iCalendar', ":ExtractSubComponent: Looking for %d subsets of type %s", $count, $type );
reset($component);
foreach( $component AS $k => $v ) {
if ( !$intags && $v == $start ) {
$answer[] = $v;
$intags = true;
}
else if ( $intags && $v == $finish ) {
$answer[] = $v;
$intags = false;
}
else if ( $intags ) {
$answer[] = $v;
}
}
return $answer;
}
/**
* @DEPRECATED: This class will be removed soon.
* Extract a particular property from the provided component. In doing so we
* assume that the content was unescaped when iCalComponent::ParseFrom()
* called iCalComponent::UnwrapComponent().
*
* @param array $component An array of lines of this component
* @param string $type The type of parameter
*
* @return array An array of iCalProperty objects
*/
function ExtractProperty( $component, $type, $count=9999 ) {
deprecated('iCalendar::ExtractProperty' );
$answer = array();
dbg_error_log( 'iCalendar', ":ExtractProperty: Looking for %d properties of type %s", $count, $type );
reset($component);
foreach( $component AS $k => $v ) {
if ( preg_match( "/$type"."[;:]/i", $v ) ) {
$answer[] = new iCalProp($v);
dbg_error_log( 'iCalendar', ":ExtractProperty: Found property %s", $type );
if ( --$count < 1 ) return $answer;
}
}
return $answer;
}
/**
* @DEPRECATED: This class will be removed soon.
* Applies the filter conditions, possibly recursively, to the value which will be either
* a single property, or an array of lines of the component under test.
*
* @todo Eventually we need to handle all of these possibilities, which will mean writing
* several routines:
* - Get Property from Component
* - Get Parameter from Property
* - Test TimeRange
* For the moment we will leave these, until there is a perceived need.
*
* @param array $filter An array of XMLElement defining the filter(s)
* @param mixed $value Either a string which is the single property, or an array of lines, for the component.
* @return boolean Whether the filter passed / failed.
*/
function ApplyFilter( $filter, $value ) {
deprecated('iCalendar::ApplyFilter' );
foreach( $filter AS $k => $v ) {
$tag = $v->GetTag();
$value_type = gettype($value);
$value_defined = (isset($value) && $value_type == 'string') || ($value_type == 'array' && count($value) > 0 );
if ( $tag == 'urn:ietf:params:xml:ns:caldav:is-not-defined' && $value_defined ) {
dbg_error_log( 'iCalendar', ":ApplyFilter: Value is set ('%s'), want unset, for filter %s", count($value), $tag );
return false;
}
elseif ( $tag == 'urn:ietf:params:xml:ns:caldav:is-defined' && !$value_defined ) {
dbg_error_log( 'iCalendar', ":ApplyFilter: Want value, but it is not set for filter %s", $tag );
return false;
}
else {
dbg_error_log( 'iCalendar', ":ApplyFilter: Have values for '%s' filter", $tag );
switch( $tag ) {
case 'urn:ietf:params:xml:ns:caldav:time-range':
/** todo:: While this is unimplemented here at present, most time-range tests should occur at the SQL level. */
break;
case 'urn:ietf:params:xml:ns:caldav:text-match':
$search = $v->GetContent();
// In this case $value will either be a string, or an array of iCalProp objects
// since TEXT-MATCH does not apply to COMPONENT level - only property/parameter
if ( !is_string($value) ) {
if ( is_array($value) ) {
$match = false;
foreach( $value AS $k1 => $v1 ) {
// $v1 MUST be an iCalProp object
if ( $match = $v1->TextMatch($search)) break;
}
}
else {
dbg_error_log( 'iCalendar', ":ApplyFilter: TEXT-MATCH will only work on strings or arrays of iCalProp. %s unsupported", gettype($value) );
return true; // We return _true_ in this case, so the client sees the item
}
}
else {
$match = (stristr( $value, $search ) !== false);
}
$negate = $v->GetAttribute("negate-condition");
if ( isset($negate) && strtolower($negate) == "yes" ) $match = !$match;
// dbg_error_log( 'iCalendar', ":ApplyFilter: TEXT-MATCH returning %s", ($match?"yes":"no") );
return $match;
break;
case 'urn:ietf:params:xml:ns:caldav:comp-filter':
$subfilter = $v->GetContent();
$component = $this->ExtractSubComponent($value,$v->GetAttribute("name"));
if ( ! $this->ApplyFilter($subfilter,$component) ) return false;
break;
case 'urn:ietf:params:xml:ns:caldav:prop-filter':
$subfilter = $v->GetContent();
$properties = $this->ExtractProperty($value,$v->GetAttribute("name"));
if ( ! $this->ApplyFilter($subfilter,$properties) ) return false;
break;
case 'urn:ietf:params:xml:ns:caldav:param-filter':
$subfilter = $v->GetContent();
$parameter = $this->ExtractParameter($value,$v->GetAttribute("NAME"));
if ( ! $this->ApplyFilter($subfilter,$parameter) ) return false;
break;
}
}
}
return true;
}
/**
* @DEPRECATED: This class will be removed soon.
* Test a PROP-FILTER or COMP-FILTER and return a true/false
* COMP-FILTER (is-defined | is-not-defined | (time-range?, prop-filter*, comp-filter*))
* PROP-FILTER (is-defined | is-not-defined | ((time-range | text-match)?, param-filter*))
*
* @param array $filter An array of XMLElement defining the filter
*
* @return boolean Whether or not this iCalendar passes the test
*/
function TestFilter( $filters ) {
deprecated('iCalendar::TestFilter' );
// dbg_error_log('iCalendar', ':TestFilter we have %d filters to test', count($filters) );
foreach( $filters AS $k => $v ) {
$tag = $v->GetTag();
// dbg_error_log('iCalendar', ':TestFilter working on tag "%s" %s"', $k, $tag );
$name = $v->GetAttribute("name");
$filter = $v->GetContent();
if ( $tag == "urn:ietf:params:xml:ns:caldav:prop-filter" ) {
$value = $this->ExtractProperty($this->lines,$name);
}
else {
$value = $this->ExtractSubComponent($this->lines,$v->GetAttribute("name"));
}
if ( count($value) == 0 ) unset($value);
if ( ! $this->ApplyFilter($filter,$value) ) return false;
}
return true;
}
/**
* @DEPRECATED: This class will be removed soon.
* Returns the header we always use at the start of our iCalendar resources
*
* @todo Remove this function.
*/
static function iCalHeader() {
deprecated('iCalendar::iCalHeader' );
return <<<EOTXT
BEGIN:VCALENDAR\r
PRODID:-//davical.org//NONSGML AWL Calendar//EN\r
VERSION:2.0\r
EOTXT;
}
/**
* @DEPRECATED: This class will be removed soon.
* Returns the footer we always use at the finish of our iCalendar resources
*
* @todo Remove this function.
*/
static function iCalFooter() {
deprecated('iCalendar::iCalFooter' );
return "END:VCALENDAR\r\n";
}
/**
* @DEPRECATED: This class will be removed soon.
* Render the iCalendar object as a text string which is a single VEVENT (or other)
*
* @param boolean $as_calendar Whether or not to wrap the event in a VCALENDAR
* @param string $type The type of iCalendar object (VEVENT, VTODO, VFREEBUSY etc.)
* @param array $restrict_properties The names of the properties we want in our rendered result.
*/
function Render( $as_calendar = true, $type = null, $restrict_properties = null ) {
deprecated('iCalendar::Render' );
if ( $as_calendar ) {
return $this->component->Render();
}
else {
$components = $this->component->GetComponents($type);
$rendered = "";
foreach( $components AS $k => $v ) {
$rendered .= $v->Render($restrict_properties);
}
return $rendered;
}
}
}
<?php
/*
This class contains code from rtfclass.php that was written by Markus Fischer and placed by him under
GPLv2 License.
=======================================NOTES FROM ORIGINAL AUTHOR====================================
Rich Text Format - Parsing Class
================================
(c) 2000 Markus Fischer
<mfischer@josefine.ben.tuwien.ac.at>
http://josefine.ben.tuwien.ac.at/~mfischer/
Latest versions of this class can always be found at
http://josefine.ben.tuwien.ac.at/~mfischer/developing/php/rtf/rtfclass.phps
Testing suite is available at
http://josefine.ben.tuwien.ac.at/~mfischer/developing/php/rtf/
License: GPLv2
Specification:
http://msdn.microsoft.com/library/default.asp?URL=/library/specs/rtfspec.htm
General Notes:
==============
Unknown or unspupported control symbols are silently gnored
Group stacking is still not supported :(
group stack logic implemented; however not really used yet
=====================================================================================================
It was modified by me (Andreas Brodowski) to allow compressed RTF being uncompressed by code I ported from
Java to PHP and adapted according the needs of Z-Push.
Currently it is being used to detect empty RTF Streams from Nokia Phones in MfE Clients
It needs to be used by other backend writers that needs to have notes in calendar, appointment or tasks
objects to be written to their databases since devices send them usually in RTF Format... With Zarafa
you can write them directly to DB and Zarafa is doing the conversion job. Other Groupware systems usually
don't have this possibility...
*/
class rtf {
var $LZRTF_HDR_DATA = "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\n\r\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx";
var $LZRTF_HDR_LEN = 207;
var $CRC32_TABLE = array( 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3,
0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91,
0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7,
0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5,
0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B,
0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59,
0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F,
0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D,
0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433,
0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01,
0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457,
0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65,
0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB,
0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9,
0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F,
0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD,
0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683,
0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1,
0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7,
0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5,
0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B,
0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79,
0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F,
0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D,
0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713,
0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21,
0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777,
0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45,
0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB,
0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9,
0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF,
0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D,
);
var $rtf; // rtf core stream
var $rtf_len; // length in characters of the stream (get performace due avoiding calling strlen everytime)
var $err = array(); // array of error message, no entities on no error
var $wantXML = false; // convert to XML
var $wantHTML = false; // convert to HTML
var $wantASCII = false; // convert to HTML
// the only variable which should be accessed from the outside
var $out; // output data stream (depends on which $wantXXXXX is set to true
var $outstyles; // htmlified styles (generated after parsing if wantHTML
var $styles; // if wantHTML, stylesheet definitions are put in here
// internal parser variables --------------------------------
// control word variables
var $cword; // holds the current (or last) control word, depending on $cw
var $cw; // are we currently parsing a control word ?
var $cfirst; // could this be the first character ? so watch out for control symbols
var $flags = array(); // parser flags
var $queue; // every character which is no sepcial char, not belongs to a control word/symbol; is generally considered being 'plain'
var $stack = array(); // group stack
/* keywords which don't follw the specification (used by Word '97 - 2000) */
// not yet used
var $control_exception = array(
"clFitText",
"clftsWidth(-?[0-9]+)?",
"clNoWrap(-?[0-9]+)?",
"clwWidth(-?[0-9]+)?",
"tdfrmtxtBottom(-?[0-9]+)?",
"tdfrmtxtLeft(-?[0-9]+)?",
"tdfrmtxtRight(-?[0-9]+)?",
"tdfrmtxtTop(-?[0-9]+)?",
"trftsWidthA(-?[0-9]+)?",
"trftsWidthB(-?[0-9]+)?",
"trftsWidth(-?[0-9]+)?",
"trwWithA(-?[0-9]+)?",
"trwWithB(-?[0-9]+)?",
"trwWith(-?[0-9]+)?",
"spectspecifygen(-?[0-9]+)?",
);
var $charset_table = array(
"0" => "ANSI",
"1" => "Default",
"2" => "Symbol",
"77" => "Mac",
"128" => "Shift Jis",
"129" => "Hangul",
"130" => "Johab",
"134" => "GB2312",
"136" => "Big5",
"161" => "Greek",
"162" => "Turkish",
"163" => "Vietnamese",
"177" => "Hebrew",
"178" => "Arabic",
"179" => "Arabic Traditional",
"180" => "Arabic user",
"181" => "Hebrew user",
"186" => "Baltic",
"204" => "Russian",
"222" => "Thai",
"238" => "Eastern European",
"255" => "PC 437",
"255" => "OEM",
);
/* note: the only conversion table used */
var $fontmodifier_table = array(
"bold" => "b",
"italic" => "i",
"underlined" => "u",
"strikethru" => "strike",
);
function rtf() {
$this->rtf_len = 0;
$this->rtf = '';
$this->out = '';
}
// loadrtf - load the raw rtf data to be converted by this class
// data = the raw rtf
function loadrtf($data) {
if (($this->rtf = $this->uncompress($data))) {
$this->rtf_len = strlen($this->rtf);
};
if($this->rtf_len == 0) {
debugLog("No data in stream found");
return false;
};
return true;
}
function output($typ) {
switch($typ) {
case "ascii": $this->wantASCII = true; break;
case "xml": $this->wantXML = true; break;
case "html": $this->wantHTML = true; break;
default: break;
}
}
// uncompress - uncompress compressed rtf data
// src = the compressed raw rtf in LZRTF format
function uncompress($src) {
$header = unpack("LcSize/LuSize/Lmagic/Lcrc32",substr($src,0,16));
$in = 16;
if ($header['cSize'] != strlen($src)-4) {
debugLog("Stream too short");
return false;
}
if ($header['crc32'] != $this->LZRTFCalcCRC32($src,16,(($header['cSize']+4))-16)) {
debugLog("CRC MISMATCH");
return false;
}
if ($header['magic'] == 0x414c454d) { // uncompressed RTF - return as is.
$dest = substr($src,$in,$header['uSize']);
} else if ($header['magic'] == 0x75465a4c) { // compressed RTF - uncompress.
$dst = $this->LZRTF_HDR_DATA;
$out = $this->LZRTF_HDR_LEN;
$oblen = $this->LZRTF_HDR_LEN + $header['uSize'];
$flagCount = 0;
$flags = 0;
while ($out<$oblen) {
$flags = ($flagCount++ % 8 == 0) ? ord($src{$in++}) : $flags >> 1;
if (($flags & 1) == 1) {
$offset = ord($src{$in++});
$length = ord($src{$in++});
$offset = ($offset << 4) | ($length >> 4);
$length = ($length & 0xF) + 2;
$offset = (int)($out / 4096) * 4096 + $offset;
if ($offset >= $out) $offset -= 4096;
$end = $offset + $length;
while ($offset < $end) {
$dst{$out++} = $dst{$offset++};
};
} else {
$dst{$out++} = $src{$in++};
}
}
$src = $dst;
$dest = substr($src,$this->LZRTF_HDR_LEN,$header['uSize']);
} else { // unknown magic - returfn false (please report if this ever happens)
debugLog("Unknown Magic");
return false;
}
return $dest;
}
// LZRTFCalcCRC32 - calculates the CRC32 of the LZRTF data part
// buf = the whole rtf data part
// off = start point of crc calculation
// len = length of data to calculate CRC for
// function is necessary since in RTF there is no XOR 0xffffffff being done (said to be 0x00 unsafe CRC32 calculation
function LZRTFCalcCRC32($buf, $off, $len) {
$c=0;
$end = $off + $len;
for($i=$off;$i < $end;$i++) {
$c=$this->CRC32_TABLE[($c ^ ord($buf{$i})) & 0xFF] ^ (($c >> 8) & 0x00ffffff);
}
return $c;
}
function parserInit() { /* Default values according to the specs */
$this->flags = array(
"fontsize" => 24,
"beginparagraph" => true,
);
}
function parseControl($control, $parameter) {
switch ($control) {
case "fonttbl": // font table definition start
$this->flags["fonttbl"] = true; // signal fonttable control words they are allowed to behave as expected
break;
case "f": // define or set font
if($this->flags["fonttbl"]) { // if its set, the fonttable definition is written to; else its read from
$this->flags["fonttbl_current_write"] = $parameter;
} else {
$this->flags["fonttbl_current_read"] = $parameter;
}
break;
case "fcharset": // this is for preparing flushQueue; it then moves the Queue to $this->fonttable .. instead to formatted output
$this->flags["fonttbl_want_fcharset"] = $parameter;
break;
case "fs": // sets the current fontsize; is used by stylesheets (which are therefore generated on the fly
$this->flags["fontsize"] = $parameter;
break;
case "qc": // handle center alignment
$this->flags["alignment"] = "center";
break;
case "qr": // handle right alignment
$this->flags["alignment"] = "right";
break;
case "pard": // reset paragraph settings (only alignment)
$this->flags["alignment"] = "";
break;
case "par": // define new paragraph (for now, thats a simple break in html) begin new line
$this->flags["beginparagraph"] = true;
if($this->wantHTML) {
$this->out .= "</div>";
}
if($this->wantASCII) {
$this->out .= "\n";
}
break;
case "bnone": // bold
$parameter = "0";
case "b":
// haven'y yet figured out WHY I need a (string)-cast here ... hm
if((string)$parameter == "0")
$this->flags["bold"] = false;
else
$this->flags["bold"] = true;
break;
case "ulnone": // underlined
$parameter = "0";
case "ul":
if((string)$parameter == "0")
$this->flags["underlined"] = false;
else
$this->flags["underlined"] = true;
break;
case "inone": // italic
$parameter = "0";
case "i":
if((string)$parameter == "0")
$this->flags["italic"] = false;
else
$this->flags["italic"] = true;
break;
case "strikenone": // strikethru
$parameter = "0";
case "strike":
if((string)$parameter == "0")
$this->flags["strikethru"] = false;
else
$this->flags["strikethru"] = true;
break;
case "plain": // reset all font modifiers and fontsize to 12
$this->flags["bold"] = false;
$this->flags["italic"] = false;
$this->flags["underlined"] = false;
$this->flags["strikethru"] = false;
$this->flags["fontsize"] = 12;
$this->flags["subscription"] = false;
$this->flags["superscription"] = false;
break;
case "subnone": // subscription
$parameter = "0";
case "sub":
if((string)$parameter == "0")
$this->flags["subscription"] = false;
else
$this->flags["subscription"] = true;
break;
case "supernone": // superscription
$parameter = "0";
case "super":
if((string)$parameter == "0")
$this->flags["superscription"] = false;
else
$this->flags["superscription"] = true;
break;
}
}
/*
Dispatch the control word to the output stream
*/
function flushControl() {
if(preg_match("/^([A-Za-z]+)(-?[0-9]*) ?$/", $this->cword, $match)) {
$this->parseControl($match[1], $match[2]);
if($this->wantXML) {
$this->out.="<control word=\"".$match[1]."\"";
if(strlen($match[2]) > 0)
$this->out.=" param=\"".$match[2]."\"";
$this->out.="/>";
}
}
}
/*
If output stream supports comments, dispatch it
*/
function flushComment($comment) {
if($this->wantXML || $this->wantHTML) {
$this->out.="<!-- ".$comment." -->";
}
}
/*
Dispatch start/end of logical rtf groups (not every output type needs it; merely debugging purpose)
*/
function flushGroup($state) {
if($state == "open") { /* push onto the stack */
array_push($this->stack, $this->flags);
if($this->wantXML)
$this->out.="<group>";
}
if($state == "close") { /* pop from the stack */
$this->last_flags = $this->flags;
$this->flags = array_pop($this->stack);
$this->flags["fonttbl_current_write"] = ""; // on group close, no more fontdefinition will be written to this id
// this is not really the right way to do it !
// of course a '}' not necessarily donates a fonttable end; a fonttable
// group at least *can* contain sub-groups
// therefore an stacked approach is heavily needed
$this->flags["fonttbl"] = false; // no matter what you do, if a group closes, its fonttbl definition is closed too
if($this->wantXML)
$this->out.="</group>";
}
}
function flushHead() {
if($this->wantXML)
$this->out.="<rtf>";
}
function flushBottom() {
if($this->wantXML)
$this->out.="</rtf>";
}
function checkHtmlSpanContent($command) {
reset($this->fontmodifier_table);
while(list($rtf, $html) = each($this->fontmodifier_table)) {
if($this->flags[$rtf] == true) {
if($command == "start")
$this->out .= "<".$html.">";
else
$this->out .= "</".$html.">";
}
}
}
/*
flush text in queue
*/
function flushQueue() {
if(strlen($this->queue)) {
// processing logic
if (isset($this->flags["fonttbl_want_fcharset"]) &&
preg_match("/^[0-9]+$/", $this->flags["fonttbl_want_fcharset"])) {
$this->fonttable[$this->flags["fonttbl_want_fcharset"]]["charset"] = $this->queue;
$this->flags["fonttbl_want_fcharset"] = "";
$this->queue = "";
}
// output logic
if (strlen($this->queue)) {
/*
Everything which passes this is (or, at leat, *should*) be only outputted plaintext
Thats why we can safely add the css-stylesheet when using wantHTML
*/
if($this->wantXML)
$this->out.= "<plain>".$this->queue."</plain>";
else if($this->wantHTML) {
// only output html if a valid (for now, just numeric;) fonttable is given
if (!isset($this->flags["fonttbl_current_read"])) $this->flags["fonttbl_current_read"] = "";
if(preg_match("/^[0-9]+$/", $this->flags["fonttbl_current_read"])) {
if($this->flags["beginparagraph"] == true) {
$this->flags["beginparagraph"] = false;
$this->out .= "<div align=\"";
switch($this->flags["alignment"]) {
case "right":
$this->out .= "right";
break;
case "center":
$this->out .= "center";
break;
case "left":
default:
$this->out .= "left";
}
$this->out .= "\">";
}
/* define new style for that span */
$this->styles["f".$this->flags["fonttbl_current_read"]."s".$this->flags["fontsize"]] = "font-family:".$this->fonttable[$this->flags["fonttbl_current_read"]]["charset"]." font-size:".$this->flags["fontsize"].";";
/* write span start */
$this->out .= "<span class=\"f".$this->flags["fonttbl_current_read"]."s".$this->flags["fontsize"]."\">";
/* check if the span content has a modifier */
$this->checkHtmlSpanContent("start");
/* write span content */
$this->out .= $this->queue;
/* close modifiers */
$this->checkHtmlSpanContent("stop");
/* close span */
"</span>";
}
}
$this->queue = "";
}
}
}
/*
handle special charactes like \'ef
*/
function flushSpecial($special) {
if(strlen($special) == 2) {
if($this->wantASCII)
$this->out .= chr(hexdec('0x'.$special));
else if($this->wantXML)
$this->out .= "<special value=\"".$special."\"/>";
else if($this->wantHTML){
$this->out .= "<special value=\"".$special."\"/>";
switch($special) {
case "c1": $this->out .= "&Aacute;"; break;
case "e1": $this->out .= "&aacute;"; break;
case "c0": $this->out .= "&Agrave;"; break;
case "e0": $this->out .= "&agrave;"; break;
case "c9": $this->out .= "&Eacute;"; break;
case "e9": $this->out .= "&eacute;"; break;
case "c8": $this->out .= "&Egrave;"; break;
case "e8": $this->out .= "&egrave;"; break;
case "cd": $this->out .= "&Iacute;"; break;
case "ed": $this->out .= "&iacute;"; break;
case "cc": $this->out .= "&Igrave;"; break;
case "ec": $this->out .= "&igrave;"; break;
case "d3": $this->out .= "&Oacute;"; break;
case "f3": $this->out .= "&oacute;"; break;
case "d2": $this->out .= "&Ograve;"; break;
case "f2": $this->out .= "&ograve;"; break;
case "da": $this->out .= "&Uacute;"; break;
case "fa": $this->out .= "&uacute;"; break;
case "d9": $this->out .= "&Ugrave;"; break;
case "f9": $this->out .= "&ugrave;"; break;
case "80": $this->out .= "&#8364;"; break;
case "d1": $this->out .= "&Ntilde;"; break;
case "f1": $this->out .= "&ntilde;"; break;
case "c7": $this->out .= "&Ccedil;"; break;
case "e7": $this->out .= "&ccedil;"; break;
case "dc": $this->out .= "&Uuml;"; break;
case "fc": $this->out .= "&uuml;"; break;
case "bf": $this->out .= "&#191;"; break;
case "a1": $this->out .= "&#161;"; break;
case "b7": $this->out .= "&middot;"; break;
case "a9": $this->out .= "&copy;"; break;
case "ae": $this->out .= "&reg;"; break;
case "ba": $this->out .= "&ordm;"; break;
case "aa": $this->out .= "&ordf;"; break;
case "b2": $this->out .= "&sup2;"; break;
case "b3": $this->out .= "&sup3;"; break;
}
}
}
}
/*
Output errors at end
*/
function flushErrors() {
if(count($this->err) > 0) {
if($this->wantXML) {
$this->out .= "<errors>";
while(list($num,$value) = each($this->err)) {
$this->out .= "<message>".$value."</message>";
}
$this->out .= "</errors>";
}
}
}
function makeStyles() {
$this->outstyles = "<style type=\"text/css\"><!--\n";
reset($this->styles);
while(list($stylename, $styleattrib) = each($this->styles)) {
$this->outstyles .= ".".$stylename." { ".$styleattrib." }\n";
}
$this->outstyles .= "--></style>\n";
}
function parse() {
$this->parserInit();
$i = 0;
$this->cw= false; // flag if control word is currently parsed
$this->cfirst = false; // first control character ?
$this->cword = ""; // last or current control word (depends on $this->cw
$this->queue = ""; // plain text data found during parsing
$this->flushHead();
while($i < $this->rtf_len) {
switch($this->rtf[$i]) {
case "{":
if($this->cw) {
$this->flushControl();
$this->cw = false;
$this->cfirst = false;
} else
$this->flushQueue();
$this->flushGroup("open");
break;
case "}":
if($this->cw) {
$this->flushControl();
$this->cw = false;
$this->cfirst = false;
} else
$this->flushQueue();
$this->flushGroup("close");
break;
case "\\":
if($this->cfirst) { // catches '\\'
$this->queue .= "\\"; // replaced single quotes
$this->cfirst = false;
$this->cw = false;
break;
}
if($this->cw) {
$this->flushControl();
} else
$this->flushQueue();
$this->cw = true;
$this->cfirst = true;
$this->cword = "";
break;
default:
if((ord($this->rtf[$i]) == 10) || (ord($this->rtf[$i]) == 13)) break; // eat line breaks
if($this->cw) { // active control word ?
/*
Watch the RE: there's an optional space at the end which IS part of
the control word (but actually its ignored by flushControl)
*/
if(preg_match("/^[a-zA-Z0-9-]?$/", $this->rtf[$i])) { // continue parsing
$this->cword .= $this->rtf[$i];
$this->cfirst = false;
} else {
/*
Control word could be a 'control symbol', like \~ or \* etc.
*/
$specialmatch = false;
if($this->cfirst) {
if($this->rtf[$i] == '\'') { // expect to get some special chars
$this->flushQueue();
$this->flushSpecial($this->rtf[$i+1].$this->rtf[$i+2]);
$i+=2;
$specialmatch = true;
$this->cw = false;
$this->cfirst = false;
$this->cword = "";
} else
if(preg_match("/^[{}\*]$/", $this->rtf[$i])) {
$this->flushComment("control symbols not yet handled");
$specialmatch = true;
}
$this->cfirst = false;
} else {
if($this->rtf[$i] == ' ') { // space delimtes control words, so just discard it and flush the controlword
$this->cw = false;
$this->flushControl();
break;
}
}
if(!$specialmatch) {
$this->flushControl();
$this->cw = false;
$this->cfirst = false;
/*
The current character is a delimeter, but is NOT
part of the control word so we hop one step back
in the stream and process it again
*/
$i--;
}
}
} else {
// < and > need translation before putting into queue when XML or HTML is wanted
if(($this->wantHTML) || ($this->wantXML)) {
switch($this->rtf[$i]) {
case "<":
$this->queue .= "&lt;";
break;
case ">":
$this->queue .= "&gt;";
break;
default:
$this->queue .= $this->rtf[$i];
break;
}
} else
$this->queue .= $this->rtf[$i];
}
}
$i++;
}
$this->flushQueue();
$this->flushErrors();
$this->flushBottom();
if($this->wantHTML) {
$this->makeStyles();
}
}
}
\ No newline at end of file
<?php
/**
* A Class for connecting to a caldav server
*
* Based on caldav-client-v2.php by Andrew McMillan <andrew@mcmillan.net.nz>
* but using cURL instead of home-brew request construction. cURL code re-used
* from carddav.php by Jean-Louis Dupond. Additional bugfixes to
* caldav-client-v2.php by xbgmsharp <xbgmsharp@gmail.com>.
*
* Copyright Andrew McMillan (original caldav-client-v2.php), Jean-Louis Dupond (cURL code), xbgmsharp (bugfixes)
* Copyright Thorsten Köster
* License GNU LGPL version 3 or later (http://www.gnu.org/licenses/lgpl-3.0.txt)
*/
require_once('XMLDocument.php');
/**
* A class for holding basic calendar information
*/
class CalendarInfo {
public $url;
public $displayname;
public $getctag;
public $id;
function __construct( $url, $displayname = null, $getctag = null, $id = null ) {
$this->url = $url;
$this->displayname = $displayname;
$this->getctag = $getctag;
$this->id = $id;
}
function __toString() {
return( '(URL: '.$this->url.' Ctag: '.$this->getctag.' Displayname: '.$this->displayname .')'. "\n" );
}
}
/**
* A class for accessing DAViCal via CalDAV, as a client
*
* @package awl
*/
class CalDAVClient {
/**
* Server, username, password, calendar
*
* @var string
*/
protected $server, $base_url, $user, $pass, $auth;
/**
* The principal-URL we're using
*/
protected $principal_url;
/**
* The calendar-URL we're using
*/
protected $calendar_url;
/**
* The calendar-home-set we're using
*/
protected $calendar_home_set;
/**
* The calendar_urls we have discovered
*/
protected $calendar_urls;
/**
* Construct URL
*/
protected $url;
/**
* The useragent which is send to the caldav server
*
* @var string
*/
const USERAGENT = 'ModifiedDAViCalClient';
protected $headers = array();
protected $xmlResponse = ""; // xml received
protected $httpResponseCode = 0; // http response code
protected $httpResponseHeaders = "";
protected $httpResponseBody = "";
protected $parser; // our XML parser object
/**
* CardDAV server connection (curl handle)
*
* @var resource
*/
private $curl = false;
private $synctoken = array();
/**
* Constructor, initialises the class
*
* @param string $caldav_url The URL for the calendar server
* @param string $user The name of the user logging in
* @param string $pass The password for that user
*/
function __construct( $caldav_url, $user, $pass ) {
$this->url = $caldav_url;
$this->user = $user;
$this->pass = $pass;
$this->auth = $user . ':' . $pass;
$this->headers = array();
$parsed_url = parse_url($caldav_url);
if ($parsed_url === false) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCalDAV->caldav_backend(): Couldn't parse URL: %s", $caldav_url));
return;
}
$this->server = $parsed_url['scheme'] . '://' . $parsed_url['host'] . ':' . $parsed_url['port'];
$this->base_url = $parsed_url['path'];
//ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCalDAV->caldav_backend(): base_url '%s'", $this->base_url));
//$this->base_url .= !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
//$this->base_url .= !empty($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
if (substr($this->base_url, -1) !== '/') {
$this->base_url = $this->base_url . '/';
}
}
/**
* Checks if the CalDAV server is reachable
*
* @return boolean
*/
public function CheckConnection() {
$result = $this->DoRequest($this->url, 'OPTIONS');
switch ($this->httpResponseCode) {
case 200:
case 207:
case 401:
$status = true;
break;
default:
$status = false;
}
return $status;
}
/**
* Disconnect curl connection
*
*/
public function Disconnect() {
if ($this->curl !== false) {
curl_close($this->curl);
$this->curl = false;
}
}
/**
* Adds an If-Match or If-None-Match header
*
* @param bool $match to Match or Not to Match, that is the question!
* @param string $etag The etag to match / not match against.
*/
function SetMatch( $match, $etag = '*' ) {
$this->headers['match'] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), trim($etag,'"'));
}
/**
* Add a Depth: header. Valid values are 0, 1 or infinity
*
* @param int $depth The depth, default to infinity
*/
function SetDepth( $depth = '0' ) {
$this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
}
/**
* Set the calendar_url we will be using for a while.
*
* @param string $url The calendar_url
*/
function SetCalendar( $url ) {
$this->calendar_url = $url;
}
/**
* Split response into httpResponse and xmlResponse
*
* @param string Response from server
*/
function ParseResponse( $response ) {
$pos = strpos($response, '<?xml');
if ($pos !== false) {
$this->xmlResponse = trim(substr($response, $pos));
$this->xmlResponse = preg_replace('{>[^>]*$}s', '>',$this->xmlResponse );
$parser = xml_parser_create_ns('UTF-8');
xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 );
if ( xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) === 0 ) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("XML parsing error: %s - %s", xml_get_error_code($parser), xml_error_string(xml_get_error_code($parser))));
// debug_print_backtrace();
// echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes );
// echo "\nTags array............................................................\n"; print_r( $this->xmltags );
ZLog::Write(LOGLEVEL_DEBUG, sprintf("XML Reponse:\n%s\n", $this->xmlResponse));
}
xml_parser_free($parser);
}
}
public function curl_init() {
if ($this->curl === false) {
$this->curl = curl_init();
curl_setopt($this->curl, CURLOPT_HEADER, true);
curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->curl, CURLOPT_USERAGENT, self::USERAGENT);
if ($this->auth !== null) {
curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_setopt($this->curl, CURLOPT_USERPWD, $this->auth);
}
}
}
/**
* Send a request to the server
*
* @param string $url The URL to make the request to
*
* @return string The content of the response from the server
*/
function DoRequest($url, $method, $content = null, $content_type = "text/plain") {
$this->curl_init();
if ( !isset($url) ) $url = $this->base_url;
$url = preg_replace('{^https?://[^/]+}', '', $url);
$url = $this->server . $url;
curl_setopt($this->curl, CURLOPT_URL, $url);
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($this->curl, CURLOPT_CONNECTTIMEOUT, 30); // 30 seconds it's already too big
if ($content !== null)
{
curl_setopt($this->curl, CURLOPT_POST, true);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, $content);
}
else
{
curl_setopt($this->curl, CURLOPT_POST, false);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, null);
}
$headers = array();
$headers['content-type'] = 'Content-type: ' . $content_type;
foreach( $this->headers as $ii => $head ) {
$headers[$ii] = $head;
}
curl_setopt($this->curl, CURLOPT_HTTPHEADER, $headers);
$this->xmlResponse = '';
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("Request:\n%s\n", $content));
$response = curl_exec($this->curl);
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("Reponse:\n%s\n", $response));
$header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE);
$this->httpResponseCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
$this->httpResponseHeaders = trim(substr($response, 0, $header_size));
$this->httpResponseBody = substr($response, $header_size);
$this->headers = array(); // reset the headers array for our next request
$this->ParseResponse($this->httpResponseBody);
return $response;
}
/**
* Send an OPTIONS request to the server
*
* @param string $url The URL to make the request to
*
* @return array The allowed options
*/
function DoOptionsRequest( $url = null ) {
$headers = $this->DoRequest($url === null ? $this->url : $url, "OPTIONS");
$options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers );
$options = array_flip( preg_split( '/[, ]+/', $options_header ));
return $options;
}
/**
* Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR)
*
* @param string $method The method (PROPFIND, REPORT, etc) to use with the request
* @param string $xml The XML to send along with the request
* @param string $url The URL to make the request to
*
* @return array An array of the allowed methods
*/
function DoXMLRequest( $request_method, $xml, $url = null ) {
return $this->DoRequest($url, $request_method, $xml, "text/xml");
}
/**
* Get a single item from the server.
*
* @param string $url The URL to GET
*/
function DoGETRequest( $url ) {
return $this->DoRequest($url, "GET");
}
/**
* Get the HEAD of a single item from the server.
*
* @param string $url The URL to HEAD
*/
function DoHEADRequest( $url ) {
return $this->DoRequest($url, "HEAD");
}
/**
* PUT a text/icalendar resource, returning the etag
*
* @param string $url The URL to make the request to
* @param string $icalendar The iCalendar resource to send to the server
* @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource.
*
* @return string The content of the response from the server
*/
function DoPUTRequest( $url, $icalendar, $etag = null ) {
if ( $etag != null ) {
$this->SetMatch( ($etag != '*'), $etag );
}
$this->DoRequest($url, "PUT", $icalendar, 'text/calendar; encoding="utf-8"');
$etag = null;
if ( preg_match( '{^ETag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) {
$etag = $matches[1];
}
if ( !isset($etag) || $etag == '' ) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("No etag in:\n%s\n", $this->httpResponseHeaders));
$save_response_headers = $this->httpResponseHeaders;
$this->DoHEADRequest( $url );
if ( preg_match( '{^Etag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) {
$etag = $matches[1];
}
if ( !isset($etag) || $etag == '' ) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("Still No etag in:\n%s\n", $this->httpResponseHeaders));
}
$this->httpResponseHeaders = $save_response_headers;
}
return $etag;
}
/**
* DELETE a text/icalendar resource
*
* @param string $url The URL to make the request to
* @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL.
*
* @return int The HTTP Result Code for the DELETE
*/
function DoDELETERequest( $url, $etag = null ) {
if ( $etag != null ) {
$this->SetMatch( true, $etag );
}
$this->DoRequest($url, "DELETE");
return $this->httpResponseCode;
}
/**
* Get a single item from the server.
*
* @param string $url The URL to PROPFIND on
*/
function DoPROPFINDRequest( $url, $props, $depth = 0 ) {
$this->SetDepth($depth);
$xml = new XMLDocument( array( 'DAV:' => '', 'urn:ietf:params:xml:ns:caldav' => 'C' ) );
$prop = new XMLElement('prop');
foreach( $props AS $v ) {
$xml->NSElement($prop,$v);
}
$this->DoRequest($url, "PROPFIND", $xml->Render('propfind',$prop), "text/xml");
return $this->xmlResponse;
}
/**
* Get/Set the Principal URL
*
* @param $url string The Principal URL to set
*/
function PrincipalURL( $url = null ) {
if ( isset($url) ) {
$this->principal_url = $url;
}
return $this->principal_url;
}
/**
* Get/Set the calendar-home-set URL
*
* @param $url array of string The calendar-home-set URLs to set
*/
function CalendarHomeSet( $urls = null ) {
if ( isset($urls) ) {
if ( !is_array($urls) ) {
$urls = array($urls);
}
$this->calendar_home_set = $urls;
}
return $this->calendar_home_set;
}
/**
* Get/Set the calendar-home-set URL
*
* @param $urls array of string The calendar URLs to set
*/
function CalendarUrls( $urls = null ) {
if ( isset($urls) ) {
if ( !is_array($urls) ) {
$urls = array($urls);
}
$this->calendar_urls = $urls;
}
return $this->calendar_urls;
}
/**
* Return the first occurrence of an href inside the named tag.
*
* @param string $tagname The tag name to find the href inside of
*/
function HrefValueInside( $tagname ) {
foreach( $this->xmltags[$tagname] AS $k => $v ) {
$j = $v + 1;
if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) {
return rawurldecode($this->xmlnodes[$j]['value']);
}
}
return null;
}
/**
* Return the href containing this property. Except only if it's inside a status != 200
*
* @param string $tagname The tag name of the property to find the href for
* @param integer $which Which instance of the tag should we use
*/
function HrefForProp( $tagname, $i = 0 ) {
if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
$j = $this->xmltags[$tagname][$i];
while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ) {
// printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) {
return null;
}
}
// printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
// printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']);
return rawurldecode($this->xmlnodes[$j]['value']);
}
}
else {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("xmltags[$tagname] or xmltags[$tagname][$i] is not set."));
}
return null;
}
/**
* Return the href which has a resourcetype of the specified type
*
* @param string $tagname The tag name of the resourcetype to find the href for
* @param integer $which Which instance of the tag should we use
*/
function HrefForResourcetype( $tagname, $i = 0 ) {
if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
$j = $this->xmltags[$tagname][$i];
while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::resourcetype' );
if ( $j > 0 ) {
while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' );
if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
return rawurldecode($this->xmlnodes[$j]['value']);
}
}
}
return null;
}
/**
* Return the <prop> ... </prop> of a propstat where the status is OK
*
* @param string $nodenum The node number in the xmlnodes which is the href
*/
function GetOKProps( $nodenum ) {
$props = null;
$level = $this->xmlnodes[$nodenum]['level'];
$status = '';
while ( $this->xmlnodes[++$nodenum]['level'] >= $level ) {
if ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::propstat' ) {
if ( $this->xmlnodes[$nodenum]['type'] == 'open' ) {
$props = array();
$status = '';
} else {
if ( $status == 'HTTP/1.1 200 OK' ) {
break;
}
}
} elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) {
break;
} elseif ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::status' ) {
$status = $this->xmlnodes[$nodenum]['value'];
} else {
$props[] = $this->xmlnodes[$nodenum];
}
}
return $props;
}
/**
* Attack the given URL in an attempt to find a principal URL
*
* @param string $url The URL to find the principal-URL from
*/
function FindPrincipal( $url=null ) {
$xml = $this->DoPROPFINDRequest( $url, array('resourcetype', 'current-user-principal', 'owner', 'principal-URL', 'urn:ietf:params:xml:ns:caldav:calendar-home-set'), 1);
$principal_url = $this->HrefForProp('DAV::principal');
if ( !isset($principal_url) ) {
foreach( array('DAV::current-user-principal', 'DAV::principal-URL', 'DAV::owner') AS $href ) {
if ( !isset($principal_url) ) {
$principal_url = $this->HrefValueInside($href);
}
}
}
return $this->PrincipalURL($principal_url);
}
/**
* Attack the given URL in an attempt to find a principal URL
*
* @param string $url The URL to find the calendar-home-set from
*/
function FindCalendarHome( $recursed=false ) {
if ( !isset($this->principal_url) ) {
$this->FindPrincipal();
}
if ( $recursed ) {
$this->DoPROPFINDRequest( $this->principal_url, array('urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0);
}
$calendar_home = array();
foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-home-set'] AS $k => $v ) {
if ( $this->xmlnodes[$v]['type'] != 'open' ) {
continue;
}
while( $this->xmlnodes[++$v]['type'] != 'close' && $this->xmlnodes[$v]['tag'] != 'urn:ietf:params:xml:ns:caldav:calendar-home-set' ) {
// printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']);
if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) ) {
$calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']);
}
}
}
if ( !$recursed && count($calendar_home) < 1 ) {
$calendar_home = $this->FindCalendarHome(true);
}
return $this->CalendarHomeSet($calendar_home);
}
/**
* Find the calendars, from the calendar_home_set
*/
function FindCalendars( $recursed=false ) {
if ( !isset($this->calendar_home_set[0]) ) {
$this->FindCalendarHome();
}
$this->DoPROPFINDRequest( $this->calendar_home_set[0], array('resourcetype','displayname','http://calendarserver.org/ns/:getctag'), 1);
$calendars = array();
if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar']) ) {
$calendar_urls = array();
foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar'] AS $k => $v ) {
$calendar_urls[$this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar', $k)] = 1;
}
foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) {
$href = rawurldecode($this->xmlnodes[$hnode]['value']);
if ( !isset($calendar_urls[$href]) ) {
continue;
}
// printf("Seems '%s' is a calendar.\n", $href );
$calendar = new CalendarInfo($href);
$ok_props = $this->GetOKProps($hnode);
foreach( $ok_props AS $v ) {
// printf("Looking at: %s[%s]\n", $href, $v['tag'] );
switch( $v['tag'] ) {
case 'http://calendarserver.org/ns/:getctag':
$calendar->getctag = $v['value'];
break;
case 'DAV::displayname':
$calendar->displayname = $v['value'];
break;
}
}
$calendar->id = rtrim(str_replace($this->calendar_home_set[0], "", $calendar->url), "/");
$calendars[] = $calendar;
}
}
return $this->CalendarUrls($calendars);
}
/**
* Find the calendars, from the calendar_home_set
*/
function GetCalendarDetails( $url = null ) {
if ( isset($url) ) {
$this->SetCalendar($url);
}
if ( !isset($this->calendar_home_set[0]) ) {
$this->FindCalendarHome();
}
$calendar_properties = array( 'resourcetype', 'displayname', 'http://calendarserver.org/ns/:getctag', 'urn:ietf:params:xml:ns:caldav:calendar-timezone', 'supported-report-set' );
$this->DoPROPFINDRequest( $this->calendar_url, $calendar_properties, 0);
$hnode = $this->xmltags['DAV::href'][0];
$href = rawurldecode($this->xmlnodes[$hnode]['value']);
$calendar = new CalendarInfo($href);
$ok_props = $this->GetOKProps($hnode);
foreach( $ok_props AS $k => $v ) {
$name = preg_replace( '{^.*:}', '', $v['tag'] );
if ( isset($v['value'] ) ) {
$calendar->{$name} = $v['value'];
} /* else {
printf( "Calendar property '%s' has no text content\n", $v['tag'] );
}*/
}
$calendar->id = rtrim(str_replace($this->calendar_home_set[0], "", $calendar->url), "/");
return $calendar;
}
/**
* Get all etags for a calendar
*/
function GetCollectionETags( $url = null ) {
if ( isset($url) ) {
$this->SetCalendar($url);
}
$this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1);
$etags = array();
if ( isset($this->xmltags['DAV::getetag']) ) {
foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) {
$href = $this->HrefForProp('DAV::getetag', $k);
if ( isset($href) && isset($this->xmlnodes[$v]['value']) ) {
$etags[$href] = $this->xmlnodes[$v]['value'];
}
}
}
return $etags;
}
/**
* Get a bunch of events for a calendar with a calendar-multiget report
*/
function CalendarMultiget( $event_hrefs, $url = null ) {
if ( isset($url) ) {
$this->SetCalendar($url);
}
$hrefs = '';
foreach( $event_hrefs AS $k => $href ) {
$href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
$hrefs .= '<href>'.$href.'</href>';
}
$body = <<<EOXML
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-multiget xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<prop><getetag/><C:calendar-data/></prop>
$hrefs
</C:calendar-multiget>
EOXML;
$this->DoRequest($this->calendar_url, "REPORT", $body, "text/xml");
$events = array();
if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data']) ) {
foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data'] AS $k => $v ) {
$href = $this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar-data', $k);
// echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]);
$events[$href] = $this->xmlnodes[$v]['value'];
}
} else {
foreach( $event_hrefs AS $k => $href ) {
$this->DoGETRequest($href);
$events[$href] = $this->httpResponseBody;
}
}
return $events;
}
/**
* Given XML for a calendar query, return an array of the events (/todos) in the
* response. Each event in the array will have a 'href', 'etag' and '$response_type'
* part, where the 'href' is relative to the calendar and the '$response_type' contains the
* definition of the calendar data in iCalendar format.
*
* @param string $filter XML fragment which is the <filter> element of a calendar-query
* @param string $url The URL of the calendar, or empty/null to use the 'current' calendar_url
*
* @return array An array of the relative URLs, etags, and events from the server. Each element of the array will
* be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied
* etag (which only varies when the data changes) and the calendar data in iCalendar format.
*/
function DoCalendarQuery( $filter, $url = null ) {
if ( !empty($url) ) {
$this->SetCalendar($url);
}
$body = <<<EOXML
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data/>
<D:getetag/>
</D:prop>
$filter
</C:calendar-query>
EOXML;
$this->SetDepth(1);
$this->DoRequest($this->calendar_url, "REPORT", $body, "text/xml");
$report = array();
foreach( $this->xmlnodes as $k => $v ) {
switch( $v['tag'] ) {
case 'DAV::response':
if ( $v['type'] == 'open' ) {
$response = array();
} elseif ( $v['type'] == 'close' ) {
$report[] = $response;
}
break;
case 'DAV::href':
$response['href'] = basename( rawurldecode($v['value']) );
break;
case 'DAV::getetag':
$response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
break;
case 'urn:ietf:params:xml:ns:caldav:calendar-data':
$response['data'] = $v['value'];
break;
}
}
return $report;
}
/**
* Get the events in a range from $start to $finish. The dates should be in the
* format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
* array of event arrays. Each event array will have a 'href', 'etag' and 'event'
* part, where the 'href' is relative to the calendar and the event contains the
* definition of the event in iCalendar format.
*
* @param timestamp $start The start time for the period
* @param timestamp $finish The finish time for the period
* @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
*
* @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
*/
function GetEvents( $start = null, $finish = null, $relative_url = null ) {
$filter = "";
if ( isset($start) && isset($finish) ) {
$range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
} else {
$range = '';
}
$filter = <<<EOFILTER
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
$range
</C:comp-filter>
</C:comp-filter>
</C:filter>
EOFILTER;
return $this->DoCalendarQuery($filter, $relative_url);
}
/**
* Get the todo's in a range from $start to $finish. The dates should be in the
* format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
* array of event arrays. Each event array will have a 'href', 'etag' and 'event'
* part, where the 'href' is relative to the calendar and the event contains the
* definition of the event in iCalendar format.
*
* @param timestamp $start The start time for the period
* @param timestamp $finish The finish time for the period
* @param boolean $completed Whether to include completed tasks
* @param boolean $cancelled Whether to include cancelled tasks
* @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
*
* @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
*/
function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = null ) {
if ( $start && $finish ) {
$time_range = <<<EOTIME
<C:time-range start="$start" end="$finish"/>
EOTIME;
} else {
$time_range = "";
}
// Warning! May contain traces of double negatives...
$neg_cancelled = ( $cancelled === true ? "no" : "yes" );
$neg_completed = ( $cancelled === true ? "no" : "yes" );
$filter = <<<EOFILTER
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:prop-filter name="STATUS">
<C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match>
</C:prop-filter>
<C:prop-filter name="STATUS">
<C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
</C:prop-filter>$time_range
</C:comp-filter>
</C:comp-filter>
</C:filter>
EOFILTER;
return $this->DoCalendarQuery($filter, $relative_url);
}
/**
* Get the calendar entry by UID
*
* @param uid
* @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
* @param string $component_type The component type inside the VCALENDAR. Default 'VEVENT'.
*
* @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
*/
function GetEntryByUid( $uid, $relative_url = null, $component_type = 'VEVENT' ) {
$filter = "";
if ( $uid ) {
$filter = <<<EOFILTER
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="$component_type">
<C:prop-filter name="UID">
<C:text-match icollation="i;octet">$uid</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>
</C:filter>
EOFILTER;
}
return $this->DoCalendarQuery($filter, $relative_url);
}
/**
* Get the calendar entry by HREF
*
* @param string $href The href from a call to GetEvents or GetTodos etc.
*
* @return string The iCalendar of the calendar entry
*/
function GetEntryByHref( $href ) {
$href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
return $this->DoGETRequest( $href );
}
/**
* Do a Sync operation. This is the fastest way to detect changes.
*
* @param string $url URL for the calendar
* @param boolean $initial It's the first synchronization
* @param boolean $support_dav_sync The CalDAV server supports sync-collection
*
* @return array of responses
*/
public function GetSync($relative_url = null, $initial = true, $support_dav_sync = false) {
if (!empty($relative_url)) {
$this->SetCalendar($relative_url);
}
$hasToken = !$initial && isset($this->synctoken[$this->calendar_url]);
if ($support_dav_sync) {
$token = ($hasToken ? $this->synctoken[$this->calendar_url] : "");
$body = <<<EOXML
<?xml version="1.0" encoding="utf-8"?>
<D:sync-collection xmlns:D="DAV:">
<D:sync-token>$token</D:sync-token>
<D:sync-level>1</D:sync-level>
<D:prop>
<D:getetag/>
<D:getlastmodified/>
</D:prop>
</D:sync-collection>
EOXML;
}
else {
$body = <<<EOXML
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
<D:getlastmodified/>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR" />
</C:filter>
</C:calendar-query>
EOXML;
}
$this->SetDepth(1);
$this->DoRequest($this->calendar_url, "REPORT", $body, "text/xml");
$report = array();
foreach ($this->xmlnodes as $k => $v) {
switch ($v['tag']) {
case 'DAV::response':
if ($v['type'] == 'open') {
$response = array();
}
elseif ($v['type'] == 'close') {
$report[] = $response;
}
break;
case 'DAV::href':
$response['href'] = basename( rawurldecode($v['value']) );
break;
case 'DAV::getlastmodified':
if (isset($v['value'])) {
$response['getlastmodified'] = $v['value'];
}
else {
$response['getlastmodified'] = '';
}
break;
case 'DAV::getetag':
$response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
break;
case 'DAV::sync-token':
$this->synctoken[$this->calendar_url] = $v['value'];
break;
}
}
// Report sync-token support on initial sync
if ($initial && $support_dav_sync && !isset($this->synctoken[$this->calendar_url])) {
ZLog::Write(LOGLEVEL_WARN, 'CalDAVClient->GetSync(): no DAV::sync-token received; did you set CALDAV_SUPPORTS_SYNC correctly?');
}
return $report;
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment