/** * Default constructor for calendar synchronization adapter. * * @param int Calendar id. * @param array Hash array with ical properties: * url: Absolute URL to iCAL resource. */ public function __construct($cal_id, $props) { $this->ical = libcalendaring::get_ical(); $this->cal_id = $cal_id; $this->url = $props["url"]; $this->user = isset($props["user"]) ? $props["user"] : null; $this->pass = isset($props["pass"]) ? $props["pass"] : null; $this->sync = isset($props["sync"]) ? $props["sync"] : 0; $this->etag = isset($props["tag"]) ? $props["tag"] : null; }
/** * Get the next recurring instance of this event * * @return mixed Array with event properties or False if recurrence ended */ public function next_instance() { if ($next_start = $this->next()) { $next = $this->event; $next['start'] = $next_start; if ($this->duration) { $next['end'] = clone $next_start; $next['end']->add($this->duration); } $next['recurrence_date'] = clone $next_start; $next['_instance'] = libcalendaring::recurrence_instance_identifier($next); unset($next['_formatobj']); return $next; } return false; }
/** * Initialize recurrence engine * * @param array The recurrence properties * @param object DateTime The recurrence start date */ public function init($recurrence, $start = null) { $this->recurrence = $recurrence; $this->engine = new Horde_Date_Recurrence($start); $this->engine->fromRRule20(libcalendaring::to_rrule($recurrence)); $this->set_start($start); if (is_array($recurrence['EXDATE'])) { foreach ($recurrence['EXDATE'] as $exdate) { if (is_a($exdate, 'DateTime')) { $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j')); } } } if (is_array($recurrence['RDATE'])) { foreach ($recurrence['RDATE'] as $rdate) { if (is_a($rdate, 'DateTime')) { $this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j')); } } } }
/** * Build a valid iCal format block from the given event * * @param array Hash array with event/task properties from libkolab * @param object VCalendar object to append event to or false for directly sending data to stdout * @param callable Callback function to fetch attachment contents, false if no attachment export * @param object RECURRENCE-ID property when serializing a recurrence exception */ private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null) { $type = $event['_type'] ?: 'event'; $vcal_creator = new VObject\Component\VCalendar(); $ve = $vcal_creator->createComponent($this->type_component_map[$type]); $ve->add('UID', $event['uid']); // set DTSTAMP according to RFC 5545, 3.8.7.2. $dtstamp = !empty($event['changed']) && !empty($this->method) ? $event['changed'] : new DateTime(); $ve->add('DTSTAMP', $dtstamp); if ($event['allday']) { $ve->DTSTAMP['VALUE'] = 'DATE'; } if (!empty($event['created'])) { $ve->add('CREATED', $event['created']); } if (!empty($event['changed'])) { $ve->add('LAST-MODIFIED', $event['changed']); } if (!empty($event['start'])) { $ve->add('DTSTART', $event['start']); } if ($event['allday']) { $ve->DTSTART['VALUE'] = 'DATE'; } if (!empty($event['end'])) { $ve->add('DTEND', $event['end']); } if ($event['allday']) { $ve->DTEND['VALUE'] = 'DATE'; } if (!empty($event['due'])) { $ve->add('DUE', $event['due']); } // we're exporting a recurrence instance only if (!$recurrence_id && $event['recurrence_date'] && $event['recurrence_date'] instanceof DateTime) { $recurrence_id = $vcal_creator->createProperty('RECURRENCE-ID'); $recurrence_id->setDateTime($event['recurrence_date']); if ($event['allday']) { $recurrence_id['VALUE'] = 'DATE'; } if ($event['thisandfuture']) { $recurrence_id->add('RANGE', 'THISANDFUTURE'); } } if ($recurrence_id) { $ve->add($recurrence_id); } $ve->add('SUMMARY', $event['title']); if ($event['location']) { $ve->add('LOCATION', $event['location']); } if ($event['description']) { $ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); } // normalize line endings if (isset($event['sequence'])) { $ve->add('SEQUENCE', $event['sequence']); } if ($event['recurrence'] && !$recurrence_id) { $exdates = $rdates = null; if (isset($event['recurrence']['EXDATE'])) { $exdates = $event['recurrence']['EXDATE']; unset($event['recurrence']['EXDATE']); // don't serialize EXDATEs into RRULE value } if (isset($event['recurrence']['RDATE'])) { $rdates = $event['recurrence']['RDATE']; unset($event['recurrence']['RDATE']); // don't serialize RDATEs into RRULE value } if ($event['recurrence']['FREQ']) { $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'], (bool) $event['allday'])); } // add EXDATEs each one per line (for Thunderbird Lightning) if (is_array($exdates)) { foreach ($exdates as $ex) { $ve->add('EXDATE', $ex); } } // add RDATEs if (is_array($rdates) && !empty($rdates)) { $ve->RDATE = $rdates; } } if ($event['categories']) { $ve->add('CATEGORIES', (array) $event['categories']); } if (!empty($event['free_busy'])) { $ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE'); // for Outlook clients we provide the X-MICROSOFT-CDO-BUSYSTATUS property if (stripos($this->agent, 'outlook') !== false) { $ve->add('X-MICROSOFT-CDO-BUSYSTATUS', $event['free_busy'] == 'outofoffice' ? 'OOF' : strtoupper($event['free_busy'])); } } if ($event['priority']) { $ve->add('PRIORITY', $event['priority']); } if ($event['cancelled']) { $ve->add('STATUS', 'CANCELLED'); } else { if ($event['free_busy'] == 'tentative') { $ve->add('STATUS', 'TENTATIVE'); } else { if ($event['complete'] == 100) { $ve->add('STATUS', 'COMPLETED'); } else { if (!empty($event['status'])) { $ve->add('STATUS', $event['status']); } } } } if (!empty($event['sensitivity'])) { $ve->add('CLASS', strtoupper($event['sensitivity'])); } if (!empty($event['complete'])) { $ve->add('PERCENT-COMPLETE', intval($event['complete'])); } // Apple iCal and BusyCal required the COMPLETED date to be set in order to consider a task complete if ($event['status'] == 'COMPLETED' || $event['complete'] == 100) { $ve->add('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true); } if ($event['valarms']) { foreach ($event['valarms'] as $alarm) { $va = $vcal_creator->createComponent('VALARM'); $va->ACTION = $alarm['action']; if ($alarm['trigger'] instanceof DateTime) { $va->add('TRIGGER', $alarm['trigger']); } else { $va->add('TRIGGER', $alarm['trigger']); if (strtoupper($alarm['related']) == 'END') { $va->TRIGGER['RELATED'] = 'END'; } } if ($alarm['action'] == 'EMAIL') { foreach ((array) $alarm['attendees'] as $attendee) { $va->add('ATTENDEE', 'mailto:' . $attendee); } } if ($alarm['description']) { $va->add('DESCRIPTION', $alarm['description'] ?: $event['title']); } if ($alarm['summary']) { $va->add('SUMMARY', $alarm['summary']); } if ($alarm['duration']) { $va->add('DURATION', $alarm['duration']); $va->add('REPEAT', intval($alarm['repeat'])); } if ($alarm['uri']) { $va->add('ATTACH', $alarm['uri'], array('VALUE' => 'URI')); } $ve->add($va); } } else { if ($event['alarms']) { $va = $vcal_creator->createComponent('VALARM'); list($trigger, $va->action) = explode(':', $event['alarms']); $val = libcalendaring::parse_alarm_value($trigger); if ($val[3]) { $va->add('TRIGGER', $val[3]); } else { if ($val[0] instanceof DateTime) { $va->add('TRIGGER', $val[0]); } } $ve->add($va); } } foreach ((array) $event['attendees'] as $attendee) { if ($attendee['role'] == 'ORGANIZER') { if (empty($event['organizer'])) { $event['organizer'] = $attendee; } } else { if (!empty($attendee['email'])) { if (isset($attendee['rsvp'])) { $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null; } $ve->add('ATTENDEE', 'mailto:' . $attendee['email'], array_filter(self::map_keys($attendee, $this->attendee_keymap))); } } } if ($event['organizer']) { $ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], self::map_keys($event['organizer'], array('name' => 'CN'))); } foreach ((array) $event['url'] as $url) { if (!empty($url)) { $ve->add('URL', $url); } } if (!empty($event['parent_id'])) { $ve->add('RELATED-TO', $event['parent_id'], array('RELTYPE' => 'PARENT')); } if ($event['comment']) { $ve->add('COMMENT', $event['comment']); } $memory_limit = parse_bytes(ini_get('memory_limit')); // export attachments if (!empty($event['attachments'])) { foreach ((array) $event['attachments'] as $attach) { // check available memory and skip attachment export if we can't buffer it // @todo: use rcube_utils::mem_check() if (is_callable($get_attachment) && $memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16 * 1024 * 1024) && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) { continue; } // embed attachments using the given callback function if (is_callable($get_attachment) && ($data = call_user_func($get_attachment, $attach['id'], $event))) { // embed attachments for iCal $ve->add('ATTACH', base64_encode($data), array_filter(array('VALUE' => 'BINARY', 'ENCODING' => 'BASE64', 'FMTTYPE' => $attach['mimetype'], 'X-LABEL' => $attach['name']))); unset($data); // attempt to free memory } else { if (!empty($this->attach_uri)) { $ve->add('ATTACH', strtr($this->attach_uri, array('{{id}}' => urlencode($attach['id']), '{{name}}' => urlencode($attach['name']), '{{mimetype}}' => urlencode($attach['mimetype']))), array('FMTTYPE' => $attach['mimetype'], 'VALUE' => 'URI')); } } } } foreach ((array) $event['links'] as $uri) { $ve->add('ATTACH', $uri); } // add custom properties foreach ((array) $event['x-custom'] as $prop) { $ve->add($prop[0], $prop[1]); } // append to vcalendar container if ($vcal) { $vcal->add($ve); } else { // serialize and send to stdout echo $ve->serialize(); } // append recurrence exceptions if (is_array($event['recurrence']) && $event['recurrence']['EXCEPTIONS']) { foreach ($event['recurrence']['EXCEPTIONS'] as $ex) { $exdate = $ex['recurrence_date'] ?: $ex['start']; $recurrence_id = $vcal_crator->createProperty('RECURRENCE-ID'); $recurrence_id->setDateTime($exdate); if ($event['allday']) { $recurrence_id['VALUE'] = 'DATE'; } if ($ex['thisandfuture']) { $recurrence_id->add('RANGE', 'THISANDFUTURE'); } $this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id); } } }
/** * Render localized text describing the recurrence rule of an event */ private function _recurrence_text($rrule) { // derive missing FREQ and INTERVAL from RDATE list if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) { $first = $rrule['RDATE'][0]; $second = $rrule['RDATE'][1]; $third = $rrule['RDATE'][2]; if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) { $diff = $first->diff($second); foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) { if ($diff->{$k} != 0) { $rrule['FREQ'] = $freq; $rrule['INTERVAL'] = $diff->{$k}; // verify interval with next item if (is_a($third, 'DateTime')) { $diff2 = $second->diff($third); if ($diff2->{$k} != $diff->{$k}) { unset($rrule['INTERVAL']); } } break; } } } if (!$rrule['INTERVAL']) { $rrule['FREQ'] = 'RDATE'; } $rrule['UNTIL'] = end($rrule['RDATE']); } // TODO: finish this $freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']); $details = ''; switch ($rrule['FREQ']) { case 'DAILY': $freq .= $this->gettext('days'); break; case 'WEEKLY': $freq .= $this->gettext('weeks'); break; case 'MONTHLY': $freq .= $this->gettext('months'); break; case 'YEARLY': $freq .= $this->gettext('years'); break; } if ($rrule['INTERVAL'] <= 1) { $freq = $this->gettext(strtolower($rrule['FREQ'])); } if ($rrule['COUNT']) { $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT']))); } else { if ($rrule['UNTIL']) { $until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], libcalendaring::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']))); } else { $until = $this->gettext('forever'); } } return rtrim($freq . $details . ', ' . $until); }
/** * Load iCalendar functions */ public function get_ical() { if (!$this->ical) { $this->ical = libcalendaring::get_ical(); } return $this->ical; }
/** * Feedback after showing/sending an alarm notification * * @see calendar_driver::dismiss_alarm() */ public function dismiss_alarm($event_id, $snooze = 0) { $notify_at = null; //default $stz = date_default_timezone_get(); date_default_timezone_set($this->cal->timezone->getName()); $event = $this->get_master(array('id' => $event_id)); $dismissed_alarm = $event['notifyat']; $dismissed_alarm = strtotime($dismissed_alarm) <= time() ? $dismissed_alarm : null; if ($snooze > 0) { $notify_at = date(self::DB_DATE_FORMAT, time() + $snooze); } else { if ($event['recurrence'] && $event['id'] == $event_id) { if ($event['recurrence']) { $base_alarm = libcalendaring::get_next_alarm($event); $before = $event['start']->format('U') - $base_alarm['time']; $this->_get_recurrences($event, time() + $before, false, 'alarms'); if ($this->last_clone) { $dismissed = $event['notifyat']; if (substr($event['alarms'], 0, 1) == '@') { $notify_at = null; } else { $notify_at = $this->last_clone['notifyat']; } } } } } $now = gmdate(self::DB_DATE_FORMAT); if ($dismissed_alarm) { $query = $this->rc->db->query("UPDATE " . $this->_get_table($this->db_events) . "\n SET changed=?, alarms=?, notifyat=?, dismissed=?\n WHERE event_id=?\n AND calendar_id IN (" . $this->calendar_ids . ")", $now, $event['alarms'], $notify_at, $dismissed_alarm, $event_id); } else { $query = $this->rc->db->query("UPDATE " . $this->_get_table($this->db_events) . "\n SET changed=?, alarms=?, notifyat=?\n WHERE event_id=?\n AND calendar_id IN (" . $this->calendar_ids . ")", $now, $event['alarms'], $notify_at, $event_id); } date_default_timezone_set($stz); return $this->rc->db->affected_rows($query); }
/** * */ public function mail_messages_list($p) { if (in_array('attachment', (array) $p['cols']) && !empty($p['messages'])) { foreach ($p['messages'] as $header) { $part = new StdClass(); $part->mimetype = $header->ctype; if (libcalendaring::part_is_vcalendar($part)) { $header->list_flags['attachmentClass'] = 'ical'; } else { if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) { // TODO: fetch bodystructure and search for ical parts. Maybe too expensive? if (!empty($header->structure) && is_array($header->structure->parts)) { foreach ($header->structure->parts as $part) { if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) { $header->list_flags['attachmentClass'] = 'ical'; break; } } } } } } } }
/** * Compute absolute time to notify the user */ private function _get_notification($task) { if ($task['alarms'] && $task['complete'] < 1 || strpos($task['alarms'], '@') !== false) { $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm['time'] && $alarm['action'] == 'DISPLAY') { return date('Y-m-d H:i:s', $alarm['time']); } } return null; }
/** * Helper method to decode a serialized list of alarms */ private function unserialize_alarms($alarms) { // decode json serialized alarms if ($alarms && $alarms[0] == '[') { $valarms = json_decode($alarms, true); foreach ($valarms as $i => $alarm) { if ($alarm['trigger'][0] == '@') { try { $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1)); } catch (Exception $e) { unset($valarms[$i]); } } } } else { if (strlen($alarms)) { list($trigger, $action) = explode(':', $alarms, 2); if ($trigger = libcalendaring::parse_alarm_value($trigger)) { $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0])); } } } return $valarms; }
/** * Build a valid iCal format block from the given event * * @param array Hash array with event/task properties from libkolab * @param object VCalendar object to append event to or false for directly sending data to stdout * @param callable Callback function to fetch attachment contents, false if no attachment export * @param object RECURRENCE-ID property when serializing a recurrence exception */ private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null) { $type = $event['_type'] ?: 'event'; $ve = VObject\Component::create($this->type_component_map[$type]); $ve->add('UID', $event['uid']); // set DTSTAMP according to RFC 5545, 3.8.7.2. $dtstamp = !empty($event['changed']) && !empty($this->method) ? $event['changed'] : new DateTime(); $ve->add(self::datetime_prop('DTSTAMP', $dtstamp, true)); // all-day events end the next day if ($event['allday'] && !empty($event['end'])) { $event['end'] = clone $event['end']; $event['end']->add(new \DateInterval('P1D')); $event['end']->_dateonly = true; } if (!empty($event['created'])) { $ve->add(self::datetime_prop('CREATED', $event['created'], true)); } if (!empty($event['changed'])) { $ve->add(self::datetime_prop('LAST-MODIFIED', $event['changed'], true)); } if (!empty($event['start'])) { $ve->add(self::datetime_prop('DTSTART', $event['start'], false, (bool) $event['allday'])); } if (!empty($event['end'])) { $ve->add(self::datetime_prop('DTEND', $event['end'], false, (bool) $event['allday'])); } if (!empty($event['due'])) { $ve->add(self::datetime_prop('DUE', $event['due'], false)); } if ($recurrence_id) { $ve->add($recurrence_id); } $ve->add('SUMMARY', $event['title']); if ($event['location']) { $ve->add($this->is_apple() ? new vobject_location_property('LOCATION', $event['location']) : new VObject\Property('LOCATION', $event['location'])); } if ($event['description']) { $ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); } // normalize line endings if ($event['sequence']) { $ve->add('SEQUENCE', $event['sequence']); } if ($event['recurrence'] && !$recurrence_id) { if ($exdates = $event['recurrence']['EXDATE']) { unset($event['recurrence']['EXDATE']); // don't serialize EXDATEs into RRULE value } if ($rdates = $event['recurrence']['RDATE']) { unset($event['recurrence']['RDATE']); // don't serialize RDATEs into RRULE value } if ($event['recurrence']['FREQ']) { $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'])); } // add EXDATEs each one per line (for Thunderbird Lightning) // Begin mod by Rosali (handle EXDATE the same as RDATE) if (!empty($exdates)) { $exdates = array_values($exdates); $sample = self::datetime_prop('EXDATE', $exdates[0]); $edprop = new VObject\Property\MultiDateTime('EXDATE', null); $edprop->setDateTimes($exdates, $sample->getDateType()); $ve->add($edprop); } // End mod by Rosali // add RDATEs if (!empty($rdates)) { $sample = self::datetime_prop('RDATE', $rdates[0]); $rdprop = new VObject\Property\MultiDateTime('RDATE', null); $rdprop->setDateTimes($rdates, $sample->getDateType()); $ve->add($rdprop); } } if ($event['categories']) { $cat = VObject\Property::create('CATEGORIES'); $cat->setParts((array) $event['categories']); $ve->add($cat); } if (!empty($event['free_busy'])) { $ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE'); // for Outlook clients we provide the X-MICROSOFT-CDO-BUSYSTATUS property if (stripos($this->agent, 'outlook') !== false) { $ve->add('X-MICROSOFT-CDO-BUSYSTATUS', $event['free_busy'] == 'outofoffice' ? 'OOF' : strtoupper($event['free_busy'])); } } // Begin mod by Rosali (adjust float value to percent) if ($event['complete'] && $event['complete'] <= 1) { $event['complete'] = round($event['complete'] * 100, 0); } // End mod by Rosali if ($event['priority']) { $ve->add('PRIORITY', $event['priority']); } if ($event['cancelled']) { $ve->add('STATUS', 'CANCELLED'); } else { if ($event['free_busy'] == 'tentative') { $ve->add('STATUS', 'TENTATIVE'); } else { if ($event['complete'] == 100) { $ve->add('STATUS', 'COMPLETED'); } else { if ($event['status']) { $ve->add('STATUS', $event['status']); } } } } // End mod by Rosali if (!empty($event['sensitivity'])) { $ve->add('CLASS', strtoupper($event['sensitivity'])); } if (!empty($event['complete'])) { $ve->add('PERCENT-COMPLETE', intval($event['complete'])); // Apple iCal required the COMPLETED date to be set in order to consider a task complete if ($event['complete'] == 100) { $ve->add(self::datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true)); } } if ($event['alarms']) { $va = VObject\Component::create('VALARM'); list($trigger, $va->action) = explode(':', $event['alarms']); $val = libcalendaring::parse_alaram_value($trigger); $period = $val[1] && preg_match('/[HMS]$/', $val[1]) ? 'PT' : 'P'; if ($val[1]) { $va->add('TRIGGER', preg_replace('/^([-+])P?T?(.+)/', "\\1{$period}\\2", $trigger)); } else { $va->add('TRIGGER', gmdate('Ymd\\THis\\Z', $val[0]), array('VALUE' => 'DATE-TIME')); } $ve->add($va); } foreach ((array) $event['attendees'] as $attendee) { if (is_array($attendee)) { // Mod by Rosali (check type array) if ($attendee['role'] == 'ORGANIZER') { if (empty($event['organizer'])) { $event['organizer'] = $attendee; } } else { if (!empty($attendee['email'])) { $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null; $ve->add('ATTENDEE', 'mailto:' . $attendee['email'], array_filter(self::map_keys($attendee, $this->attendee_keymap))); } } } } if ($event['organizer']) { $ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], self::map_keys($event['organizer'], array('name' => 'CN'))); } foreach ((array) $event['url'] as $url) { if (!empty($url)) { $ve->add('URL', $url); } } if (!empty($event['parent_id'])) { $ve->add('RELATED-TO', $event['parent_id'], array('RELTYPE' => 'PARENT')); } // export attachments if (!empty($event['attachments'])) { foreach ((array) $event['attachments'] as $attach) { // check available memory and skip attachment export if we can't buffer it if (is_callable($get_attachment) && $memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16 * 1024 * 1024) && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) { continue; } // embed attachments using the given callback function if (is_callable($get_attachment) && ($data = call_user_func($get_attachment, $attach['id'], $event))) { // embed attachments for iCal $ve->add('ATTACH', base64_encode($data), array_filter(array('VALUE' => 'BINARY', 'ENCODING' => 'BASE64', 'FMTTYPE' => $attach['mimetype'], 'X-LABEL' => $attach['name']))); unset($data); // attempt to free memory } else { if (!empty($this->attach_uri)) { $ve->add('ATTACH', strtr($this->attach_uri, array('{{id}}' => urlencode($attach['id']), '{{name}}' => urlencode($attach['name']), '{{mimetype}}' => urlencode($attach['mimetype']))), array('FMTTYPE' => $attach['mimetype'], 'VALUE' => 'URI')); } } } } foreach ((array) $event['links'] as $uri) { $ve->add('ATTACH', $uri); } // add custom properties foreach ((array) $event['x-custom'] as $prop) { $ve->add($prop[0], $prop[1]); } // append to vcalendar container if ($vcal) { $vcal->add($ve); } else { // serialize and send to stdout echo $ve->serialize(); } // append recurrence exceptions if (isset($event['recurrence']['EXCEPTIONS']) && is_array($event['recurrence']['EXCEPTIONS'])) { foreach ($event['recurrence']['EXCEPTIONS'] as $ex) { $exdate = clone $event['start']; $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j')); $recurrence_id = self::datetime_prop('RECURRENCE-ID', $exdate, true); // if ($ex['thisandfuture']) // not supported by any client :-( // $recurrence_id->add('RANGE', 'THISANDFUTURE'); $this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id); } } }
/** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array with task properties * @see tasklist_driver::pending_alarms() */ public function pending_alarms($time, $lists = null) { $interval = 300; $time -= $time % 60; $slot = $time; $slot -= $slot % $interval; $last = $time - max(60, $this->rc->config->get('refresh_interval', 0)); $last -= $last % $interval; // only check for alerts once in 5 minutes if ($last == $slot) { return array(); } if ($lists && is_string($lists)) { $lists = explode(',', $lists); } $time = $slot + $interval; $candidates = array(); $query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete')); foreach ($this->lists as $lid => $list) { // skip lists with alarms disabled if (!$list['showalarms'] || $lists && !in_array($lid, $lists)) { continue; } $folder = $this->get_folder($lid); foreach ($folder->select($query) as $record) { if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) { // don't trust query :-) continue; } $task = $this->_to_rcube_task($record, $lid, false); // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) { $id = $alarm['id']; // use alarm-id as primary identifier $candidates[$id] = array('id' => $id, 'title' => $task['title'], 'date' => $task['date'], 'time' => $task['time'], 'notifyat' => $alarm['time'], 'action' => $alarm['action']); } } } // get alarm information stored in local database if (!empty($candidates)) { $alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates)); $result = $this->rc->db->query("SELECT *" . " FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")" . " AND `user_id` = ?", $this->rc->user->ID); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $dbdata[$rec['alarm_id']] = $rec; } } $alarms = array(); foreach ($candidates as $id => $task) { // skip dismissed if ($dbdata[$id]['dismissed']) { continue; } // snooze function may have shifted alarm time $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat']; if ($notifyat <= $time) { $alarms[] = $task; } } return $alarms; }
/** * Create instances of a recurring event * * @param array Hash array with event properties * @param object DateTime Start date of the recurrence window * @param object DateTime End date of the recurrence window * @return array List of recurring event instances */ public function get_recurring_events($event, $start, $end = null) { $events = array(); if ($event['recurrence']) { // include library class require_once dirname(__FILE__) . '/../lib/calendar_recurrence.php'; $rcmail = rcmail::get_instance(); $recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event); $recurrence_id_format = libcalendaring::recurrence_id_format($event); // determine a reasonable end date if none given if (!$end) { switch ($event['recurrence']['FREQ']) { case 'YEARLY': $intvl = 'P100Y'; break; case 'MONTHLY': $intvl = 'P20Y'; break; default: $intvl = 'P10Y'; break; } $end = clone $event['start']; $end->add(new DateInterval($intvl)); } $i = 0; while ($next_event = $recurrence->next_instance()) { // add to output if in range if ($next_event['start'] <= $end && $next_event['end'] >= $start) { $next_event['_instance'] = $next_event['start']->format($recurrence_id_format); $next_event['id'] = $next_event['uid'] . '-' . $exception['_instance']; $next_event['recurrence_id'] = $event['uid']; $events[] = $next_event; } else { if ($next_event['start'] > $end) { // stop loop if out of range break; } } // avoid endless recursion loops if (++$i > 1000) { break; } } } return $events; }
/** * Set CSS class according to the event's attendde partstat */ public static function add_partstat_class($event, $partstats, $user = null) { // set classes according to PARTSTAT if (is_array($event['attendees'])) { $user_emails = libcalendaring::get_instance()->get_user_emails($user); $partstat = 'UNKNOWN'; foreach ($event['attendees'] as $attendee) { if (in_array($attendee['email'], $user_emails)) { $partstat = $attendee['status']; break; } } if (in_array($partstat, $partstats)) { $event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat)); } } return $event; }
/** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array with task properties * @see tasklist_driver::pending_alarms() */ public function pending_alarms($time, $lists = null) { $interval = 300; $time -= $time % 60; $slot = $time; $slot -= $slot % $interval; $last = $time - max(60, $this->rc->config->get('refresh_interval', 0)); $last -= $last % $interval; // only check for alerts once in 5 minutes if ($last == $slot) { return array(); } if ($lists && is_string($lists)) { $lists = explode(',', $lists); } $time = $slot + $interval; $tasks = array(); $query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete')); foreach ($this->lists as $lid => $list) { // skip lists with alarms disabled if (!$list['showalarms'] || $lists && !in_array($lid, $lists)) { continue; } $folder = $this->folders[$lid]; foreach ((array) $folder->select($query) as $record) { if (!$record['alarms']) { // don't trust query :-) continue; } $task = $this->_to_rcube_task($record); // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') { $id = $task['id']; $tasks[$id] = $task; $tasks[$id]['notifyat'] = $alarm['time']; } } } // get alarm information stored in local database if (!empty($tasks)) { $task_ids = array_map(array($this->rc->db, 'quote'), array_keys($tasks)); $result = $this->rc->db->query(sprintf("SELECT * FROM kolab_alarms\n WHERE event_id IN (%s) AND user_id=?", join(',', $task_ids), $this->rc->db->now()), $this->rc->user->ID); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $dbdata[$rec['event_id']] = $rec; } } $alarms = array(); foreach ($tasks as $id => $task) { // skip dismissed if ($dbdata[$id]['dismissed']) { continue; } // snooze function may have shifted alarm time $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat']; if ($notifyat <= $time) { $alarms[] = $task; } } return $alarms; }
/** * libcalendaring::to_rrule() */ function test_to_rrule() { $rrule = array('FREQ' => 'MONTHLY', 'BYDAY' => '2WE', 'INTERVAL' => 2, 'UNTIL' => new DateTime('2025-05-01 18:00:00 CEST')); $s = libcalendaring::to_rrule($rrule); $this->assertRegExp('/FREQ=' . $rrule['FREQ'] . '/', $s, "Recurrence Frequence"); $this->assertRegExp('/INTERVAL=' . $rrule['INTERVAL'] . '/', $s, "Recurrence Interval"); $this->assertRegExp('/BYDAY=' . $rrule['BYDAY'] . '/', $s, "Recurrence BYDAY"); $this->assertRegExp('/UNTIL=20250501T160000Z/', $s, "Recurrence End date (in UTC)"); }
/** * @depends test_import */ function test_apple_alarms() { $ical = new libvcalendar(); $events = $ical->import_from_file(__DIR__ . '/resources/apple-alarms.ics', 'UTF-8'); $event = $events[0]; // alarms $this->assertEquals('-45M:AUDIO', $event['alarms'], "Relative alarm string"); $alarm = libcalendaring::parse_alarm_value($event['alarms']); $this->assertEquals('45', $alarm[0], "Alarm value"); $this->assertEquals('-M', $alarm[1], "Alarm unit"); $this->assertEquals(1, count($event['valarms']), "Ignore invalid alarm blocks"); $this->assertEquals('AUDIO', $event['valarms'][0]['action'], "Full alarm item (action)"); $this->assertEquals('-PT45M', $event['valarms'][0]['trigger'], "Full alarm item (trigger)"); $this->assertEquals('Basso', $event['valarms'][0]['uri'], "Full alarm item (attachment)"); }
/** * Prepare the given task record before sending it to the client */ private function encode_task(&$rec) { $rec['mask'] = $this->filter_mask($rec); $rec['flagged'] = intval($rec['flagged']); $rec['complete'] = floatval($rec['complete']); $rec['changed'] = is_object($rec['changed']) ? $rec['changed']->format('U') : null; if ($rec['date']) { try { $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone); $rec['datetime'] = intval($date->format('U')); $rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d')); $rec['_hasdate'] = 1; } catch (Exception $e) { $rec['date'] = $rec['datetime'] = null; } } else { $rec['date'] = $rec['datetime'] = null; $rec['_hasdate'] = 0; } if ($rec['startdate']) { try { $date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone); $rec['startdatetime'] = intval($date->format('U')); $rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d')); } catch (Exception $e) { $rec['startdate'] = $rec['startdatetime'] = null; } } if ($rec['alarms']) { $rec['alarms_text'] = libcalendaring::alarms_text($rec['alarms']); } foreach ((array) $rec['attachments'] as $k => $attachment) { $rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); } if (in_array($rec['id'], $this->collapsed_tasks)) { $rec['collapsed'] = true; } $this->task_titles[$rec['id']] = $rec['title']; }
/** * Helper function to build a Mail_mime object to send an iTip message * * @param array Event object to send * @param string iTip method (REQUEST|REPLY|CANCEL) * @param boolean Request RSVP * @return object Mail_mime object with message data */ public function compose_itip_message($event, $method, $rsvp = true) { $from = rcube_utils::idn_to_ascii($this->sender['email']); $from_utf = rcube_utils::idn_to_utf8($from); $sender = format_email_recipient($from, $this->sender['name']); // truncate list attendees down to the recipient of the iTip Reply. // constraints for a METHOD:REPLY according to RFC 5546 if ($method == 'REPLY') { $replying_attendee = null; $reply_attendees = array(); foreach ($event['attendees'] as $attendee) { if ($attendee['role'] == 'ORGANIZER') { $reply_attendees[] = $attendee; } else { if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) { $replying_attendee = $attendee; if ($attendee['status'] != 'DELEGATED') { unset($replying_attendee['rsvp']); // unset the RSVP attribute } } else { if (!empty($attendee['delegated-to']) && (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0) || !empty($attendee['delegated-from']) && (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0)) { $reply_attendees[] = $attendee; } } } } if ($replying_attendee) { array_unshift($reply_attendees, $replying_attendee); $event['attendees'] = $reply_attendees; } if ($event['recurrence']) { unset($event['recurrence']['EXCEPTIONS']); } } else { if ($method == 'REQUEST') { foreach ($event['attendees'] as $i => $attendee) { if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) { $event['attendees'][$i]['rsvp'] = (bool) $rsvp; } } } else { if ($method == 'CANCEL') { if ($event['recurrence']) { unset($event['recurrence']['EXCEPTIONS']); } } } } // compose multipart message using PEAR:Mail_Mime $message = new Mail_mime("\r\n"); $message->setParam('text_encoding', 'quoted-printable'); $message->setParam('head_encoding', 'quoted-printable'); $message->setParam('head_charset', RCUBE_CHARSET); $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed"); $message->setContentType('multipart/alternative'); // compose common headers array $headers = array('From' => $sender, 'Date' => $this->rc->user_date(), 'Message-ID' => $this->rc->gen_message_id(), 'X-Sender' => $from); if ($agent = $this->rc->config->get('useragent')) { $headers['User-Agent'] = $agent; } $message->headers($headers); // attach ics file for this event $ical = libcalendaring::get_ical(); $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false); $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics'; $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCUBE_CHARSET . "; method=" . $method); return $message; }
/** * Process the alarms values submitted by the client */ public static function from_client_alarms($valarms) { return array_map(function ($alarm) { if ($alarm['trigger'][0] == '@') { try { $alarm['trigger'] = new DateTime($alarm['trigger']); $alarm['trigger']->setTimezone(new DateTimeZone('UTC')); } catch (Exception $e) { /* handle this ? */ } } else { if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) { $alarm['trigger'] = $trigger[3]; } } return $alarm; }, (array) $valarms); }
/** * Convert from Kolab_Format to internal representation */ private function _to_driver_event($record, $noinst = false) { $record['calendar'] = $this->id; $record['links'] = $this->get_links($record['uid']); if ($this->get_namespace() == 'other') { $record['className'] = 'fc-event-ns-other'; $record = kolab_driver::add_partstat_class($record, array('NEEDS-ACTION', 'DECLINED'), $this->get_owner()); } // add instance identifier to first occurrence (master event) $recurrence_id_format = libcalendaring::recurrence_id_format($record); if (!$noinst && $record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) { $record['_instance'] = $record['start']->format($recurrence_id_format); } else { if (is_a($record['recurrence_date'], 'DateTime')) { $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format); } } // clean up exception data if ($record['recurrence'] && is_array($record['recurrence']['EXCEPTIONS'])) { array_walk($record['recurrence']['EXCEPTIONS'], function (&$exception) { unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']); }); } return $record; }