diff --git a/lib/filter/mapistore/common.php b/lib/filter/mapistore/common.php index 952d55e..7f81655 100644 --- a/lib/filter/mapistore/common.php +++ b/lib/filter/mapistore/common.php @@ -1,1048 +1,1065 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_common { // Common properties [MS-OXCMSG] protected static $common_map = array( // 'PidTagAccess' => '', // 'PidTagAccessLevel' => '', // 0 - read-only, 1 - modify // 'PidTagChangeKey' => '', 'PidTagCreationTime' => 'creation-date', // PtypTime, UTC 'PidTagLastModificationTime' => 'last-modification-date', // PtypTime, UTC // 'PidTagLastModifierName' => '', // 'PidTagObjectType' => '', // @TODO 'PidTagHasAttachments' => 'attach', // PtypBoolean // 'PidTagRecordKey' => '', // 'PidTagSearchKey' => '', 'PidNameKeywords' => 'categories', ); protected $recipient_track_status_map = array( 'TENTATIVE' => 0x00000002, 'ACCEPTED' => 0x00000003, 'DECLINED' => 0x00000004, ); protected $recipient_type_map = array( 'NON-PARTICIPANT' => 0x00000004, 'OPT-PARTICIPANT' => 0x00000002, 'REQ-PARTICIPANT' => 0x00000001, 'CHAIR' => 0x00000001, ); /** * Mapping of weekdays */ protected static $recurrence_day_map = array( 'SU' => 0x00000000, 'MO' => 0x00000001, 'TU' => 0x00000002, 'WE' => 0x00000003, 'TH' => 0x00000004, 'FR' => 0x00000005, 'SA' => 0x00000006, 'BYDAY-SU' => 0x00000001, 'BYDAY-MO' => 0x00000002, 'BYDAY-TU' => 0x00000004, 'BYDAY-WE' => 0x00000008, 'BYDAY-TH' => 0x00000010, 'BYDAY-FR' => 0x00000020, 'BYDAY-SA' => 0x00000040, ); /** * Extracts data from kolab data array */ public static function get_kolab_value($data, $name) { $name_items = explode('.', $name); $count = count($name_items); $value = $data[$name_items[0]]; // special handling of x-custom properties if ($name_items[0] === 'x-custom') { foreach ((array) $value as $custom) { if ($custom['identifier'] === $name_items[1]) { return $custom['value']; } } return null; } for ($i = 1; $i < $count; $i++) { if (!is_array($value)) { return null; } list($key, $num) = explode(':', $name_items[$i]); $value = $value[$key]; if ($num !== null && $value !== null) { $value = is_array($value) ? $value[$num] : null; } } return $value; } /** * Sets specified kolab data item */ public static function set_kolab_value(&$data, $name, $value) { $name_items = explode('.', $name); $count = count($name_items); $element = &$data; // x-custom properties if ($name_items[0] === 'x-custom') { // this is supposed to be converted later by parse_common_props() $data[$name] = $value; return; } if ($count > 1) { for ($i = 0; $i < $count - 1; $i++) { $key = $name_items[$i]; if (!array_key_exists($key, $element)) { $element[$key] = array(); } $element = &$element[$key]; } } $element[$name_items[$count - 1]] = $value; } /** * Parse common properties in object data (convert into MAPI format) */ protected function parse_common_props(&$result, $data, $context = array()) { if (empty($context)) { // @TODO: throw exception? return; } if ($data['uid'] && $context['folder_uid']) { $result['id'] = kolab_api_filter_mapistore::uid_encode($context['folder_uid'], $data['uid']); } if ($context['folder_uid']) { $result['parent_id'] = $context['folder_uid']; } foreach (self::$common_map as $mapi_idx => $kolab_idx) { if (!isset($result[$mapi_idx]) && ($value = $data[$kolab_idx]) !== null) { switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': $result[$mapi_idx] = self::date_php2mapi($value, true); break; case 'PidTagHasAttachments': if (!empty($value) && $this->model != 'note') { $result[$mapi_idx] = true; } break; case 'PidNameKeywords': $result[$mapi_idx] = self::parse_categories((array) $value); break; } } } } /** * Convert common properties into kolab format */ protected function convert_common_props(&$result, $data, $original) { // @TODO: id, parent_id? foreach (self::$common_map as $mapi_idx => $kolab_idx) { if (array_key_exists($mapi_idx, $data) && !array_key_exists($kolab_idx, $result)) { $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $dt = self::date_mapi2php($value); $result[$kolab_idx] = $dt->format('Y-m-d\TH:i:s\Z'); } break; default: if ($value) { $result[$kolab_idx] = $value; } break; } } } // Handle x-custom fields foreach ((array) $result as $key => $value) { if (strpos($key, 'x-custom.') === 0) { unset($result[$key]); $key = substr($key, 9); foreach ((array) $original['x-custom'] as $idx => $custom) { if ($custom['identifier'] == $key) { if ($value) { $original['x-custom'][$idx]['value'] = $value; } else { unset($original['x-custom'][$idx]); } $x_custom_update = true; continue 2; } } if ($value) { $original['x-custom'][] = array( 'identifier' => $key, 'value' => $value, ); } $x_custom_update = true; } } if ($x_custom_update) { $result['x-custom'] = array_values($original['x-custom']); } } /** * Filter property names with mapping (kolab <> MAPI) * * @param array $attrs Property names * @param bool $reverse Reverse mapping * * @return array Property names */ public function attributes_filter($attrs, $reverse = false) { $map = array_merge(self::$common_map, $this->map()); $result = array(); // add some special common attributes $map['PidTagMessageClass'] = 'PidTagMessageClass'; $map['collection'] = 'collection'; $map['id'] = 'uid'; foreach ($attrs as $attr) { if ($reverse) { if ($name = array_search($attr, $map)) { $result[] = $name; } } else if ($name = $map[$attr]) { $result[] = $name; } } return $result; } /** * Return properties map */ protected function map() { return array(); } /** * Parse categories according to [MS-OXCICAL 2.1.3.1.1.20.3] * * @param array Categories * * @return array Categories */ public static function parse_categories($categories) { if (!is_array($categories)) { return; } $result = array(); foreach ($categories as $idx => $val) { $val = preg_replace('/(\x3B|\x2C|\x06\x1B|\xFE\x54|\xFF\x1B)/', '', $val); $val = preg_replace('/\s+/', ' ', $val); $val = trim($val); $len = mb_strlen($val); if ($len) { if ($len > 255) { $val = mb_substr($val, 0, 255); } $result[mb_strtolower($val)] = $val; } } return array_values($result); } /** * Convert Kolab 'attendee' specification into MAPI recipient * and add it to the result */ protected function attendee_to_recipient($attendee, &$result, $is_organizer = false) { $email = $attendee['cal-address']; $params = (array) $attendee['parameters']; // parse mailto string if (strpos($email, 'mailto:') === 0) { $email = urldecode(substr($email, 7)); } $emails = rcube_mime::decode_address_list($email, 1); if (!empty($email)) { $email = $emails[key($emails)]; $recipient = array( 'PidTagAddressType' => 'SMTP', 'PidTagDisplayName' => $params['cn'] ?: $email['name'], 'PidTagDisplayType' => 0, 'PidTagEmailAddress' => $email['mailto'], ); if ($is_organizer) { $recipient['PidTagRecipientFlags'] = 0x00000003; $recipient['PidTagRecipientType'] = 0x00000001; } else { $recipient['PidTagRecipientFlags'] = 0x00000001; $recipient['PidTagRecipientTrackStatus'] = (int) $this->recipient_track_status_map[$params['partstat']]; $recipient['PidTagRecipientType'] = $this->to_recipient_type($params['cutype'], $params['role']); } $recipient['PidTagRecipientDisplayName'] = $recipient['PidTagDisplayName']; $result['recipients'][] = $recipient; if (strtoupper($params['rsvp']) == 'TRUE') { $result['PidTagReplyRequested'] = true; $result['PidTagResponseRequested'] = true; } } } /** * Convert MAPI recipient into Kolab attendee */ protected function recipient_to_attendee($recipient, &$result) { if ($email = $recipient['PidTagEmailAddress']) { $mailto = 'mailto:' . rawurlencode($email); $attendee = array( 'cal-address' => $mailto, 'parameters' => array( 'cn' => $recipient['PidTagDisplayName'] ?: $recipient['PidTagRecipientDisplayName'], ), ); if ($recipient['PidTagRecipientFlags'] == 0x00000003) { $result['organizer'] = $attendee; } else { switch ($recipient['PidTagRecipientType']) { case 0x00000004: $role = 'NON-PARTICIPANT'; break; case 0x00000003: $cutype = 'RESOURCE'; break; case 0x00000002: $role = 'OPT-PARTICIPANT'; break; case 0x00000001: $role = 'REQ-PARTICIPANT'; break; } $map = array_flip($this->recipient_track_status_map); $partstat = $map[$recipient['PidTagRecipientTrackStatus']] ?: 'NEEDS-ACTION'; // @TODO: rsvp? $attendee['parameters']['cutype'] = $cutype; $attendee['parameters']['role'] = $role; $attendee['parameters']['partstat'] = $partstat; $result['attendee'][] = $attendee; } } } /** * Convert Kolab valarm specification into MAPI properties * * @param array $data Kolab object * @param array $result Object data (MAPI format) */ protected function alarm_from_kolab($data, &$result) { // [MS-OXCICAL] 2.1.3.1.1.20.62 foreach ((array) $data['valarm'] as $alarm) { if (!empty($alarm['properties']) && $alarm['properties']['action'] != 'DISPLAY') { continue; } // @TODO alarms with Date-Time instead of Duration $trigger = $alarm['properties']['trigger']; if ($trigger['duration'] && $trigger['parameters']['related'] != 'END' && ($delta = self::reminder_duration_to_delta($trigger['duration'])) ) { // Find next instance of the appointment (in UTC) $now = kolab_api::$now ?: new DateTime('now', new DateTimeZone('UTC')); if ($data['dtstart']) { $dtstart = kolab_api_input_json::to_datetime($data['dtstart']); // check if start date is from the future if ($dtstart > $now) { $reminder_time = $dtstart; } // find next occurence else { kolab_api_input_json::parse_recurrence($data, $res); if (!empty($res['recurrence'])) { $recurlib = libcalendaring::get_recurrence(); $recurlib->init($res['recurrence'], $now); $next = $recurlib->next(); if ($next) { $reminder_time = $next; } } } } $result['PidLidReminderDelta'] = $delta; // If all instances are in the past, don't set ReminderTime nor ReminderSet if ($reminder_time) { $signal_time = clone $reminder_time; $signal_time->sub(new DateInterval('PT' . $delta . 'M')); $result['PidLidReminderSet'] = true; $result['PidLidReminderTime'] = $this->date_php2mapi($reminder_time, true); $result['PidLidReminderSignalTime'] = $this->date_php2mapi($signal_time, true); } // MAPI supports only one alarm break; } } } /** * Convert MAPI recurrence into Kolab (MS-OXICAL: 2.1.3.2.2) * * @param string $data MAPI object * @param array $result Kolab object */ protected function alarm_to_kolab($data, &$result) { if ($data['PidLidReminderSet'] && ($delta = $data['PidLidReminderDelta'])) { $duration = self::reminder_delta_to_duration($delta); $alarm = array( 'action' => 'DISPLAY', 'trigger' => array('duration' => $duration), // 'description' => 'Reminder', ); $result['valarm'] = array(array('properties' => $alarm)); } else if (array_key_exists('PidLidReminderSet', $data) || array_key_exists('PidLidReminderDelta', $data)) { $result['valarm'] = array(); } } /** * Convert PidLidReminderDelta value into xCal duration */ protected static function reminder_delta_to_duration($delta) { if ($delta == 0x5AE980E1) { $delta = 15; } $delta = (int) $delta; return "-PT{$delta}M"; } /** * Convert Kolab alarm duration into PidLidReminderDelta */ protected static function reminder_duration_to_delta($duration) { if ($duration && preg_match('/^-[PT]*([0-9]+)([WDHMS])$/', $duration, $matches)) { $value = intval($matches[1]); switch ($matches[2]) { case 'S': $value = intval(round($value/60)); break; case 'H': $value *= 60; break; case 'D': $value *= 24 * 60; break; case 'W': $value *= 7 * 24 * 60; break; } return $value; } } /** * Convert Kolab recurrence specification into MAPI properties * * @param array $data Kolab object * @param array $object Object data (MAPI format) * * @return object MAPI recurrence in binary format */ protected function recurrence_from_kolab($data, $object = array()) { if ((empty($data['rrule']) || empty($data['rrule']['recur'])) && (empty($data['rdate']) || empty($data['rdate']['date'])) ) { return null; } $type = $this->model; // Get event/task start date for FirstDateTime calculations if ($dtstart = kolab_api_input_json::to_datetime($data['dtstart'])) { // StartDate: Set to the date portion of DTSTART, in the time zone specified // by PidLidTimeZoneStruct. This date is stored in minutes after // midnight Jan 1, 1601. Note that this value MUST always be // evenly divisible by 1440. // EndDate: Set to the start date of the last instance of a recurrence, in the // time zone specified by PidLidTimeZoneStruct. This date is // stored in minutes after midnight January 1, 1601. If the // recurrence is infinite, set EndDate to 0x5AE980DF. Note that // this value MUST always be evenly divisible by 1440, except for // the special value 0x5AE980DF. $startdate = clone $dtstart; $startdate->setTime(0, 0, 0); $startdate = self::date_php2mapi($startdate, true); $startdate = intval($startdate / 60); if ($mod = ($startdate % 1440)) { $startdate -= $mod; } // @TODO: get first occurrence of the event using libcalendaring_recurrence class ? } else { rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Found recurring $type without start date, skipping recurrence", ), true, false); return; } $rule = (array) ($data['rrule'] ? $data['rrule']['recur'] : null); $result = array( 'Period' => $rule && $rule['interval'] ? $rule['interval'] : 1, 'FirstDOW' => self::day2bitmask($rule['wkst'] ?: 'MO'), 'OccurrenceCount' => 0x0000000A, 'StartDate' => $startdate, 'EndDate' => 0x5AE980DF, 'FirstDateTime' => $startdate, 'CalendarType' => kolab_api_filter_mapistore_structure_recurrencepattern::CALENDARTYPE_DEFAULT, 'ModifiedInstanceDates' => array(), 'DeletedInstanceDates' => array(), ); switch ($rule['freq']) { case 'DAILY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY; $result['Period'] *= 1440; break; case 'WEEKLY': // if BYDAY does not exist use day from DTSTART if (empty($rule['byday'])) { $rule['byday'] = strtoupper($startdate->format('S')); } $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_WEEKLY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK; $result['PatternTypeSpecific'] = self::day2bitmask($rule['byday'], 'BYDAY-'); break; case 'MONTHLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY; if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH; $result['PatternTypeSpecific'] = $month_day == -1 ? 0x0000001F : $month_day; } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; case 'YEARLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY; $result['Period'] *= 12; // MAPI doesn't support multi-valued months if ($rule['bymonth']) { // @TODO: set $startdate } if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'] = array(0, $month_day == -1 ? 0x0000001F : $month_day); } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; } $exception_info = array(); $extended_exception = array(); // Custom occurrences (RDATE) if (!empty($data['rdate'])) { foreach ((array) $data['rdate']['date'] as $dt) { try { $dt = new DateTime($dt, $dtstart->getTimezone()); $dt->setTime(0, 0, 0); $dt = self::date_php2minutes($dt); $result['ModifiedInstanceDates'][] = $dt; $result['DeletedInstanceDates'][] = $dt; $exception_info[] = new kolab_api_filter_mapistore_structure_exceptioninfo(array( 'StartDateTime' => $dt, 'EndDateTime' => $dt + $object['PidLidAppointmentDuration'], 'OriginalStartDate' => $dt, 'OverrideFlags' => 0, )); $extended_exception[] = kolab_api_filter_mapistore_structure_extendedexception::get_empty(); } catch (Exception $e) { } } $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurenceCount'] = count($result['ModifiedInstanceDates']); // @FIXME: Kolab format says there can be RDATE and/or RRULE // MAPI specification says there must be RRULE if RDATE is specified if (!$result['RecurFrequency']) { $result['RecurFrequency'] = 0; $result['PatternType'] = 0; } } if ($rule && !empty($rule['until'])) { - $result['EndDate'] = intval(self::date_php2mapi($rule['until']) / 60); - // @TODO: calculate OccurrenceCount? $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER; } else if ($rule && !empty($rule['count'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurrenceCount'] = $rule['count']; - // @TODO: set EndDate } else if (!isset($result['EndType'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER; } + // calculate EndDate + if ($rule && $result['EndType'] != kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER) { + kolab_api_input_json::parse_recurrence($data, $res); + + if (!empty($res['recurrence'])) { + try { + $recurlib = libcalendaring::get_recurrence(); + $recurlib->init($res['recurrence'], $dtstart); + $end = $recurlib->end(); + + if ($end) { + $result['EndDate'] = intval(self::date_php2mapi($end) / 60); + } + } + catch (Exception $e) { + rcube::raise_error($e, true, false); + } + } + } + // Deleted instances (EXDATE) if (!empty($data['exdate'])) { if (!empty($data['exdate']['date'])) { $exceptions = (array) $data['exdate']['date']; } else if (!empty($data['exdate']['date-time'])) { $exceptions = (array) $data['exdate']['date-time']; } else { $exceptions = array(); } // convert date(-time)s to numbers foreach ($exceptions as $idx => $dt) { try { $dt = new DateTime($dt, $dtstart->getTimezone()); $dt->setTime(0, 0, 0); $result['DeletedInstanceDates'][] = self::date_php2minutes($dt); } catch (Exception $e) { } } } // [MS-OXCICAL] 2.1.3.1.1.20.13: Sort and make exceptions valid foreach (array('DeletedInstanceDates', 'ModifiedInstanceDates') as $key) { if (!empty($result[$key])) { sort($result[$key]); $result[$key] = array_values(array_unique(array_filter($result[$key]))); } } $result = new kolab_api_filter_mapistore_structure_recurrencepattern($result); if ($type == 'task') { return $result->output(true); } // @TODO: exceptions $byhour = $rule['byhour'] ? min(explode(',', $rule['byhour'])) : 0; $byminute = $rule['byminute'] ? min(explode(',', $rule['byminute'])) : 0; $offset = 60 * intval($byhour) + intval($byminute); $arp = array( 'RecurrencePattern' => $result, 'StartTimeOffset' => $offset, 'EndTimeOffset' => $offset + $object['PidLidAppointmentDuration'], 'ExceptionInfo' => $exception_info, 'ExtendedException' => $extended_exception, ); $result = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern($arp); return $result->output(true); } /** * Convert MAPI recurrence into Kolab (MS-OXICAL: 2.1.3.2.2) * * @param string $rule MAPI binary representation of recurrence rule * @param array $object Kolab object */ protected function recurrence_to_kolab($rule, &$object) { if (empty($rule)) { return array(); } // parse binary (Appointment)RecurrencePattern if ($this->model == 'event') { $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern(); $arp->input($rule, true); $rp = $arp->RecurrencePattern; } else { $rp = new kolab_api_filter_mapistore_structure_recurrencepattern(); $rp->input($rule, true); } $result = array( 'interval' => $rp->Period, ); switch ($rp->PatternType) { case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY: $result['freq'] = 'DAILY'; $result['interval'] /= 1440; if ($arp) { $result['byhour'] = floor($arp->StartTimeOffset / 60); $result['byminute'] = $arp->StartTimeOffset - $result['byhour'] * 60; } break; case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK: $result['freq'] = 'WEEKLY'; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific); if ($rp->Period >= 1) { $result['wkst'] = self::bitmask2day($rp->FirstDOW); } break; default: // monthly/yearly $evenly_divisible = $rp->Period % 12 == 0; switch ($rp->PatternType) { case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH: case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHEND: $result['freq'] = $evenly_divisible ? 'YEARLY' : 'MONTHLY'; break; case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH: case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_HJMONTHNTH: $result['freq'] = $evenly_divisible ? 'YEARLY-NTH' : 'MONTHLY-NTH'; break; default: // not-supported return; } if ($result['freq'] = 'MONTHLY') { $rule['bymonthday'] = intval($rp->PatternTypeSpecific == 0x0000001F ? -1 : $rp->PatternTypeSpecific); } else if ($result['freq'] = 'MONTHLY-NTH') { $result['freq'] = 'MONTHLY'; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific[0]); if ($rp->PatternTypeSpecific[1]) { $result['bysetpos'] = intval($rp->PatternTypeSpecific[1] == 0x00000005 ? -1 : $rp->PatternTypeSpecific[1]); } } else if ($result['freq'] = 'YEARLY') { $result['interval'] /= 12; $rule['bymonthday'] = intval($rp->PatternTypeSpecific == 0x0000001F ? -1 : $rp->PatternTypeSpecific); $rule['bymonth'] = 0;// @TODO: month from FirstDateTime } else if ($result['freq'] = 'YEARLY-NTH') { $result['freq'] = 'YEARLY'; $result['interval'] /= 12; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific[0]); $result['bymonth'] = 0;// @TODO: month from FirstDateTime if ($rp->PatternTypeSpecific[1]) { $result['bysetpos'] = intval($rp->PatternTypeSpecific[1] == 0x00000005 ? -1 : $rp->PatternTypeSpecific[1]); } } } if ($rp->EndType == kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER) { // @TODO: set UNTIL to EndDate + StartTimeOffset, or the midnight of EndDate } else if ($rp->EndType == kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC) { $result['count'] = $rp->OccurrenceCount; } if ($result['interval'] == 1) { unset($result['interval']); } $object['rrule']['recur'] = $result; $object['exdate'] = array(); $object['rdate'] = array(); // $exception_info = (array) $rp->ExceptionInfo; // $extended_exception = (array) $rp->ExtendedException; $modified_dates = (array) $rp->ModifiedInstanceDates; $deleted_dates = (array) $rp->DeletedInstanceDates; // Deleted/Modified exceptions (EXDATE/RDATE) foreach ($deleted_dates as $date) { $idx = in_array($date, $modified_dates) ? 'rdate' : 'exdate'; $dt = self::date_minutes2php($date); if ($dt) { $object[$idx]['date'][] = $dt->format('Y-m-d'); } } } /** * Returns number of minutes between midnight 1601-01-01 * and specified UTC DateTime */ public static function date_php2minutes($date) { $start = new DateTime('1601-01-01 00:00:00 UTC'); // make sure the specified date is in UTC $date->setTimezone(new DateTimeZone('UTC')); return (int) round(($date->getTimestamp() - $start->getTimestamp()) / 60); } /** * Convert number of minutes between midnight 1601-01-01 (UTC) into PHP DateTime * * @return DateTime|bool DateTime object or False on failure */ public static function date_minutes2php($minutes) { $datetime = new DateTime('1601-01-01 00:00:00 UTC'); $interval = new DateInterval(sprintf('PT%dM', $minutes)); return $datetime->add($interval); } /** * Convert DateTime object to MAPI date format */ public static function date_php2mapi($date, $utc = true, $time = null) { // convert string to DateTime if (!is_object($date) && !empty($date)) { // convert date to datetime on 00:00:00 if (preg_match('/^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/', $date, $m)) { $date = $m[1] . '-' . $m[2] . '-' . $m[3] . 'T00:00:00+00:00'; } $date = new DateTime($date); } else if (is_object($date) && $utc) { // clone the date object if we're going to change timezone $date = clone $date; } else { return; } if ($utc) { $date->setTimezone(new DateTimeZone('UTC')); } if (!empty($time)) { $date->setTime((int) $time['hour'], (int) $time['minute'], (int) $time['second']); } // MAPI PTypTime is 64-bit integer representing the number // of 100-nanosecond intervals since January 1, 1601. // Mapistore format for this type is a float number // seconds since 1601-01-01 00:00:00 $seconds = floatval($date->format('U')) + 11644473600; /* if ($microseconds = intval($date->format('u'))) { $seconds += $microseconds/1000000; } */ return $seconds; } /** * Convert date-time from MAPI format to DateTime */ public static function date_mapi2php($date) { $seconds = floatval(sprintf('%.0f', $date)); // assumes we're working with dates after 1970-01-01 $dt = new DateTime('@' . intval($seconds - 11644473600), new DateTimeZone('UTC')); /* if ($microseconds = intval(($date - $seconds) * 1000000)) { $dt = new DateTime($dt->format('Y-m-d H:i:s') . '.' . $microseconds, $dt->getTimezone()); } */ return $dt; } /** * Setting PidTagRecipientType according to [MS-OXCICAL 2.1.3.1.1.20.2] */ protected function to_recipient_type($cutype, $role) { if ($cutype && in_array($cutype, array('RESOURCE', 'ROOM'))) { return 0x00000003; } if ($role && ($type = $this->recipient_type_map[$role])) { return $type; } return 0x00000001; } /** * Converts string of days (TU,TH) to bitmask used by MAPI * * @param string $days * * @return int */ protected static function day2bitmask($days, $prefix = '') { $days = explode(',', $days); $result = 0; foreach ($days as $day) { $result = $result + self::$recurrence_day_map[$prefix.$day]; } return $result; } /** * Convert bitmask used by MAPI to string of days (TU,TH) * * @param int $days * * @return string */ protected static function bitmask2day($days) { $days_arr = array(); foreach (self::$recurrence_day_map as $day => $bit) { if (($days & $bit) === $bit) { $days_arr[] = preg_replace('/^BYDAY-/', '', $day); } } $result = implode(',', $days_arr); return $result; } } diff --git a/lib/input/json.php b/lib/input/json.php index 3283f26..0cfd96d 100644 --- a/lib/input/json.php +++ b/lib/input/json.php @@ -1,300 +1,306 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json extends kolab_api_input { /** * Get request data (JSON) * * @param string Expected object type * @param bool Disable filters application * @param array Original object data (set on update requests) * * @return array Request data */ public function input($type = null, $disable_filters = false, $original = null) { if ($this->input_body === null) { $data = file_get_contents('php://input'); $data = trim($data); $data = json_decode($data, true); $this->input_body = $data; } if (!$disable_filters) { if ($this->filter) { if (!empty($original)) { // convert object data into API format $data = $this->api->get_object_data($original, $type); } $this->filter->input_body($this->input_body, $type, $data); } // convert input to internal kolab_storage format if ($type) { $class = "kolab_api_input_json_$type"; $model = new $class; $model->input($this->input_body, $original); } } return $this->input_body; } /** * Convert xCard/xCal date and date-time into internal DateTime * * @param array|string Date or Date-Time * * @return DateTime */ public static function to_datetime($input) { if (empty($input)) { return; } if (is_array($input)) { if ($input['date-time']) { if ($input['parameters']['tzid']) { $tzid = str_replace('/kolab.org/', '', $input['parameters']['tzid']); } else { $tzid = 'UTC'; } $datetime = $input['date-time']; try { $timezone = new DateTimeZone($tzid); } catch (Exception $e) {} } else if ($input['timestamp']) { $datetime = $input['timestamp']; } else if ($input['date']) { $datetime = $input['date']; $is_date = true; } else { return; } } else { $datetime = $input; $is_date = preg_match('/^[0-9]{4}-?[0-9]{2}-?[0-9]{2}$/', $input); } try { $dt = new DateTime($datetime, $timezone ?: new DateTimeZone('UTC')); } catch (Exception $e) { return; } + // Horde_Date (via libcalendaring) doesn't like some timezones + // @TODO: don't use Horde_Date or fix it there + if ($dt->getTimezone()->getName() == 'Z') { + $dt->setTimezone(new DateTimeZone('UTC')); + } + if ($is_date) { $dt->_dateonly = true; $dt->setTime(0, 0, 0); } return $dt; } /** * Convert PHP DateTime into xCard/xCal date or date-time * * @param DateTime $datetime DateTime object * @param string $timezone Timezone identifier * * @return array */ public static function from_datetime($datetime, $timezone = null) { $format = $datetime->_dateonly ? 'Y-m-d' : 'Y-m-d\TH:i:s'; $type = $datetime->_dateonly ? 'date' : 'date-time'; if ($timezone && $timezone != 'UTC') { try { $tz = new DateTimeZone($timezone); $datetime->setTimezone($tz); $result['parameters'] = array( 'tzid' => '/kolab.org/' . $timezone ); } catch (Exception $e) { } } else if (!$datetime->_dateonly) { $format .= '\Z'; } $result[$type] = $datetime->format($format); return $result; } /** * Add x-custom fields to the result */ public static function add_x_custom($data, &$result) { if (array_key_exists('x-custom', (array) $data)) { $value = (array) $data['x-custom']; foreach ((array) $value as $idx => $v) { if ($v['identifier'] && $v['value'] !== null) { $value[$idx] = array($v['identifier'], $v['value']); } else { unset($value[$idx]); } } $result['x-custom'] = $value; } } /** * Parse mailto URI, e.g. attendee/cal-address property * * @param string $uri Mailto: uri * @param string $params Element parameters * * @return string E-mail address */ public static function parse_mailto_uri($uri, &$params = array()) { if (strpos($uri, 'mailto:') === 0) { $uri = substr($uri, 7); $uri = rawurldecode($uri); $emails = rcube_mime::decode_address_list($uri, 1, true, null, false); $email = $emails[1]; if (!empty($email['mailto'])) { if (empty($params['cn']) && !empty($email['name'])) { $params['cn'] = $email['name']; } return $email['mailto']; } } } /** * Parse attendees property input * * @param array $attendees Attendees list * * @return array Attendees list in kolab_format_xcal format */ public static function parse_attendees($attendees) { foreach ((array) $attendees as $idx => $attendee) { $params = $attendee['parameters']; $email = kolab_api_input_json::parse_mailto_uri($attendee['cal-address'], $params); foreach (array('to', 'from') as $val) { foreach ((array) $params['delegated-' . $val] as $del) { if ($del_email = kolab_api_input_json::parse_mailto_uri($del, $params)) { $delegated[$val][] = $del_email; } } } if ($email) { $attendees[$idx] = array_filter(array( 'email' => $email, 'name' => $params['cn'], 'status' => $params['partstat'], 'role' => $params['role'], 'rsvp' => (bool) $params['rsvp'] || strtoupper($params['rsvp']) === 'TRUE', 'cutype' => $params['cutype'], 'dir' => $params['dir'], 'delegated-to' => $delegated['to'], 'delegated-from' => $delegated['from'], )); } else { unset($attendees[$idx]); } } return $attendees; } /** * Handle recurrence rule input * * @param array $data Input data * @param array $result Result data */ public static function parse_recurrence($data, &$result) { if (array_key_exists('rrule', $data)) { $result['recurrence'] = array(); $recur_keys = array( 'freq', 'interval', 'count', 'bymonth', 'bymonthday', 'byday', 'byyearday', 'bysetpos', 'byhour', 'byminute', 'bysecond', 'wkst', ); foreach ($recur_keys as $key) { if ($data['rrule']['recur'][$key]) { $result['recurrence'][strtoupper($key)] = $data['rrule']['recur'][$key]; } } if ($data['rrule']['recur']['until']) { $result['recurrence']['UNTIL'] = self::to_datetime($data['rrule']['recur']['until']); } } // Recurrence: deleted exceptions (EXDATE) if (array_key_exists('exdate', $data)) { $result['recurrence']['EXDATE'] = array(); if (!empty($data['exdate']['date'])) { $result['recurrence']['EXDATE'] = (array) $data['exdate']['date']; } else if (!empty($data['exdate']['date-time'])) { $result['recurrence']['EXDATE'] = (array) $data['exdate']['date-time']; } } // Recurrence (RDATE) if (array_key_exists('rdate', $data)) { $result['recurrence']['RDATE'] = array(); if (!empty($data['rdate']['date'])) { $result['recurrence']['RDATE'] = (array) $data['rdate']['date']; } else if (!empty($data['exdate']['date-time'])) { $result['recurrence']['RDATE'] = (array) $data['rdate']['date-time']; } } } }