Skip to content

Instantly share code, notes, and snippets.

@jthurteau
Last active February 22, 2023 17:52
Show Gist options
  • Save jthurteau/b84ff8089585daa1d02e268431f08a13 to your computer and use it in GitHub Desktop.
Save jthurteau/b84ff8089585daa1d02e268431f08a13 to your computer and use it in GitHub Desktop.
/*
* 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