Last active
February 22, 2023 17:52
-
-
Save jthurteau/b84ff8089585daa1d02e268431f08a13 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* start by getting the event data from EMS we've mapped the following values into an array | |
* | |
* 'id' => BookingID from EMS | |
* 'fullStart' => TimeEventStart from EMS converted to a timestamp | |
* 'fullEnd' => TimeEventEnd from EMS converted to a timestamp | |
* 'now' => current time time() | |
* 'title' => EventName from EMS | |
* 'location' => some combination of building/floor/room name | |
* 'description' => optional content from elsewhere with more in-depth detail | |
* | |
* and group these booking events into an array, $icalEvents | |
* | |
*/ | |
const APPLICATION_ID = 'emsToIcalSample'; | |
const APPLICATION_INSTANCE_NAME = 'production'; | |
const APPLICATION_HOST_NAME = 'youremsdb.domain.com'; | |
$ical = new Sample_Ical(array('events' => $icalEvents, 'method' => Sample_Ical::METHOD_PUBLISH); | |
print($ical); | |
/* | |
* Class to build an iCal document from an map of data representing an event. | |
* This is used for both generating e-mail attachments, and documents served as | |
* a web service. | |
*/ | |
class Sample_Ical { | |
const METHOD_REQUEST = 'Request'; | |
const METHOD_PUBLISH = 'Publish'; | |
const METHOD_CANCEL = 'Cancel'; | |
const OUTPUT_COL_MAX = 70; | |
const OUTPUT_ESCAPE_CHARS = ':;,\\'; | |
const OUTPUT_REMOVE_CHARS = ""; | |
const OUTPUT_DATESTAMP = 'Ymd\THis\Z'; | |
const AUTO_SEQUENCE = '*'; | |
const CARDINALITY_OPTIONAL = '?'; | |
const CARDINALITY_ZEROORMORE = '*'; | |
const CARDINALITY_ONEORMORE = '+'; | |
const MIME_TYPE = 'text/calendar'; | |
protected $_ical = ''; | |
protected $_attachment = ''; | |
protected $_baseTime = NULL; | |
protected $_timeInterval = 60; | |
protected $_method = self::METHOD_REQUEST; | |
protected $_id = NULL; | |
protected $_filename = 'event.ics'; | |
protected $_hostId = ''; | |
protected $_productId = ''; | |
protected $_lang = 'EN'; | |
protected $_version = '2.0'; | |
protected static $_systemBaseTime = 0; | |
protected static $_itemProps = array( | |
'VEVENT' => array( | |
'UID' => 1, | |
'ORGANIZER' => self::CARDINALITY_OPTIONAL, | |
'ATTENDEE[]' => self::CARDINALITY_ZEROORMORE, | |
'DTSTART' => 1, | |
'DTEND' => 1, | |
'DTSTAMP' => 1, | |
'LOCATION' => self::CARDINALITY_OPTIONAL, | |
'SEQUENCE' => self::CARDINALITY_OPTIONAL, | |
'LAST-MODIFIED' => self::CARDINALITY_OPTIONAL, | |
'SUMMARY' => 1, | |
'DESCRIPTION' => self::CARDINALITY_OPTIONAL, | |
'STATUS' => self::CARDINALITY_OPTIONAL | |
) | |
); | |
protected static $_propHelperMap = array( | |
'start' => 'DTSTART', | |
'end' => 'DTEND', | |
'now' => 'DTSTAMP', | |
'modified' => 'LAST-MODIFIED', | |
'title' => 'SUMMARY', | |
'sequence' => 'SEQUENCE', | |
'userEmail' => 'ATTENDEE', | |
'attendeeEmail' => 'ATTENDEE', | |
'ownerEmail' => 'ORGANIZER', | |
'location' => 'LOCATION', | |
); | |
public function __construct($config = array()) | |
{ | |
$this->_hostId = | |
array_key_exists('hostId', $config) | |
? $config['hostId'] | |
: APPLICATION_INSTANCE_NAME . '.' . APPLICATION_HOST_NAME; | |
$this->_productId = | |
array_key_exists('productId', $config) | |
? $config['productId'] | |
: APPLICATION_ID . '.' . APPLICATION_INSTANCE; | |
if (array_key_exists('baseTime', $config)) { | |
$this->_baseTime = $config['baseTime']; | |
} | |
if (array_key_exists('timeInterval', $config)) { | |
$this->_timeInterval = $config['timeInterval']; | |
} | |
$method = | |
array_key_exists('method', $config) | |
? $config['method'] | |
: self::METHOD_REQUEST; | |
$this->_id = | |
array_key_exists('events', $config) | |
? Saf_Array::extractOptional('id', $config) | |
: NULL; | |
$events = array_key_exists('events', $config) | |
? $config['events'] | |
: NULL; | |
switch ($method) { | |
case self::METHOD_REQUEST: | |
$this->setMethodRequest(); | |
break; | |
case self::METHOD_PUBLISH: | |
$this->setMethodPublish(); | |
break; | |
case self::METHOD_CANCEL: | |
$this->setMethodCancel(); | |
break; | |
} | |
if ( | |
!is_null($this->_id) | |
) { | |
$this->_generate($config); | |
} else if (!is_null($events) { | |
$this->_generateMulti($events); | |
} | |
} | |
public function __toString() | |
{ | |
return $this->getIcal(); | |
} | |
public function setMethodRequest() | |
{ | |
$this->_method = self::METHOD_REQUEST; | |
} | |
public function setMethodPublish() | |
{ | |
$this->_method = self::METHOD_PUBLISH; | |
} | |
public function setMethodCancel() | |
{ | |
$this->_method = self::METHOD_PUBLISH; | |
} | |
public function generate($data) | |
{ | |
if(is_array($this->_id)) { | |
$this->_generateMulti($data); | |
} else { | |
$this->_generate($data); | |
} | |
} | |
public function getIcal() | |
{ | |
return $this->_ical; | |
} | |
protected function _getHeader() | |
{ | |
$method = strtoupper($this->_method); | |
$product = self::escapeText("{$this->_productId}//{$this->_hostId}"); | |
return "BEGIN:VCALENDAR\n" | |
. "METHOD:{$method}\n" | |
. "PRODID:-//{$product}//{$this->_lang}\n" | |
. "VERSION:{$this->_version}\n"; | |
} | |
protected function _getFooter() | |
{ | |
return "END:VCALENDAR"; | |
} | |
protected function _generate($data, $id = NULL) | |
{ | |
$type = 'VEVENT'; //#TODO #2.0.0 support other types | |
$itemData = $data; | |
$this->_ical = | |
(is_null($id) ? $this->_getHeader() : '') | |
. "BEGIN:{$type}\n" | |
. $this->_renderItem($type, is_null($id) ? $this->_id : $id , $itemData) | |
. "END:{$type}\n" | |
. (is_null($id) ? $this->_getFooter() : ''); | |
/* -- not needed for this example | |
$this->_generateAttachment(); | |
*/ | |
} | |
protected function _generateMulti($data) | |
{ | |
$items = array(); | |
foreach($this->_id as $id => $item) { | |
$confirmedId = array_key_exists('id', $item) | |
? $item['id'] | |
: $id; | |
$items[] = $this->_generate($item, $confirmedId); | |
} | |
$this->_ical = | |
$this->_getHeader() | |
. implode("\n", $items) | |
. $this->_getFooter(); | |
$this->_generateAttachment(); | |
} | |
protected function _renderItem($type, $id, $data) | |
{ | |
$out = ''; | |
$outType = strtolower(substr($type, 1)); | |
$outProp = "$prop:"; | |
$dateStamp = gmdate(self::OUTPUT_DATESTAMP, Saf_Time::time()); | |
$version = $this->getVersion($data); | |
$atProps = 'CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE'; //or just ROLE=REQ-PARTICIPANT; | |
$uid = "{$outType}{$id}@{$this->_hostId}"; | |
//#TODO #2.0.0 see what more advanced attendee properties we can support... | |
foreach(self::$_itemProps[$type] as $prop => $card) { | |
$value = NULL; | |
$propKey = array_key_exists($prop, $data) | |
? $prop | |
: $this->_resolvePropKey($prop, $data); | |
$outProp = "$prop:"; | |
switch ($prop){ | |
case 'ORGANIZER': | |
case 'ATTENDEE': | |
$attendeeMail = | |
$propKey | |
&& array_key_exists($propKey, $data) | |
? $data[$propKey] | |
: NULL; | |
$cnSource = | |
array_key_exists('USER:CN', $data) | |
? $data['USER:CN'] | |
: ( | |
array_key_exists('userFullname', $data) | |
? $data['userFullname'] | |
: NULL | |
); | |
if (is_array($attendeeMail)) { | |
foreach($attendeeMail as $email) { | |
$outProp = array(); | |
if ( | |
is_array($cnSource) && array_key_exists($email, $cnSource) | |
) { | |
$cn = ";CN={$cnSource[$email]}"; | |
} else { | |
$cn = ''; | |
} | |
$outProp = 'ATTENDEE;'; | |
$value[] = "{$atProps}{$cn}:MAILTO:{$email}"; | |
} | |
} else if (!is_null($attendeeMail)) { | |
$attendeeCn = | |
is_array($cnSource) | |
? '' | |
: ";CN={$cnSource}"; | |
$outProp = 'ATTENDEE;'; | |
$value = "{$atProps}{$attendeeCn}:MAILTO:{$attendeeMail}"; | |
} | |
break; | |
case 'DTSTAMP': | |
case 'DTSTART': | |
case 'DTEND': | |
case 'LAST-MODIFIED': | |
$value = | |
$propKey | |
&& array_key_exists($propKey, $data) | |
? $data[$propKey] | |
: NULL; | |
if (!is_null($value)) { | |
if (Saf_Time::isTimeStamp($value)) { | |
$value = gmdate(self::OUTPUT_DATESTAMP, $value); | |
} else { | |
//#TODO #2.0.0 detect non-GMT and convert | |
} | |
} else if ($prop == 'DTSTAMP') { | |
$value = gmdate(self::OUTPUT_DATESTAMP, Saf_Time::time()); | |
} | |
break; | |
case 'UID': | |
$value = $uid; | |
break; | |
case 'SEQUENCE': | |
if ( | |
$propKey | |
&& array_key_exists($propKey, $data) | |
&& $data[$propKey] == self::AUTO_SEQUENCE | |
) { | |
$value = Saf_Time::time() - $this->_baseTime; | |
} else if ( | |
$propKey | |
&& array_key_exists($propKey, $data) | |
) { | |
$value = (int)$data[$propKey]; | |
} | |
break; | |
case 'STATUS': | |
switch (strtoupper($this->_method)) { | |
case 'CANCEL' : | |
$value = | |
$propKey | |
&& array_key_exists($propKey, $data) | |
? strtoupper($data[$propKey]) | |
: NULL; | |
break; | |
case 'PUBLISH' : | |
case 'REQUEST' : | |
default: | |
$value = | |
$propKey | |
&& array_key_exists($propKey, $data) | |
? strtoupper($data[$propKey]) | |
: 'CONFIRMED'; | |
} | |
break; | |
default: | |
if ($propKey && array_key_exists($propKey, $data)) { | |
$value = self::escapeText($data[$propKey]); | |
} | |
} | |
if ($card === self::CARDINALITY_ONEORMORE || $card === 1) { | |
if (is_null($value)) { | |
throw new Exception("Required value {$prop} missing to generate iCal for {$uid}"); | |
} | |
} else if ($card === self::CARDINALITY_OPTIONAL || $card === 1) { | |
if (is_array($value)) { | |
throw new Exception("Too many values provided for {$prop} to generate iCal for {$uid}"); | |
} | |
} | |
if (is_array($value) && count($value) > 0) { | |
foreach($value as $subValue) { | |
$out .= "{$outProp}{$subValue}\n"; | |
} | |
} else if (!is_array($value) && !is_null($value)) { | |
$out .= "{$outProp}{$value}\n"; | |
} | |
} | |
return $out; | |
} | |
protected function _resolvePropKey($key, $data) | |
{ | |
foreach(self::$_propHelperMap as $dataKey => $propKey){ | |
if ( | |
array_key_exists($dataKey, $data) | |
&& self::$_propHelperMap[$dataKey] == $key | |
) { | |
return $dataKey; | |
} | |
} | |
return NULL; | |
} | |
/* -- not needed for this example | |
protected function _generateAttachment() | |
{ | |
$this->_attachment = new Zend_Mime_Part($this->_ical); | |
$method = strtoupper($this->_method); | |
$this->_attachment->type = "text/calendar; method={$method}"; | |
$this->_attachment->disposition = Zend_Mime::DISPOSITION_INLINE; | |
$this->_attachment->encoding = Zend_Mime::ENCODING_8BIT; | |
$this->_attachment->filename = $this->_filename; | |
} | |
public function getAttachment() | |
{ | |
return $this->_attachment; | |
} | |
*/ | |
public static function escapeText($string) | |
{ | |
$removeChars = str_split(self::OUTPUT_REMOVE_CHARS); | |
$filtered = addcslashes( | |
str_replace($removeChars, '', $string), | |
self::OUTPUT_ESCAPE_CHARS | |
); | |
$lines = str_split($filtered, self::OUTPUT_COL_MAX); | |
if (strlen($lines[count($lines) - 1]) == 0) { //#TODO #2.0.0 research if this is needed | |
unset($lines[count($lines) - 1]); | |
} | |
return implode("\n ", $lines); | |
} | |
public function getVersion() | |
{ | |
return $this->_version; | |
} | |
public function getTimedVersion() | |
{ | |
$versionTime = time() - $this->_baseTime; | |
return ( | |
$versionTime - ($versionTime % $this->_timeInterval) | |
) / $this->_timeInterval; | |
} | |
public static function setBaseTime($timestamp) | |
{ | |
self::$_systemBaseTime = $timestamp; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment