diff --git a/lib/filter/mapistore/common.php b/lib/filter/mapistore/common.php index 46cc3d7..bee6e12 100644 --- a/lib/filter/mapistore/common.php +++ b/lib/filter/mapistore/common.php @@ -1,919 +1,922 @@ | +--------------------------------------------------------------------------+ | 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' => '', // @TODO // '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 static 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 'PidNameKeywords': $result[$mapi_idx] = self::parse_categories((array) $value); break; } } } } /** * Convert common properties into kolab format */ protected static 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 */ public 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 */ public 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 recurrence specification into MAPI properties * * @param array $data Kolab object * @param array $type Object data (MAPI format) * @param string $type Object type (event, task) * * @return object MAPI recurrence in binary format */ public static function recurrence_from_kolab($data, $object = array(), $type = 'event') { if ((empty($data['rrule']) || empty($data['rrule']['recur'])) && (empty($data['rdate']) || empty($data['rdate']['date'])) ) { return null; } // 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']) / 10000000 / 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; } // 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 * @param string $type Object type (task, event) */ public static function recurrence_to_kolab($rule, &$object, $type = 'event') { if (empty($rule)) { return array(); } // parse binary (Appointment)RecurrencePattern if ($type == '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 +00:00'); // 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 * and specified UTC DateTime 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', new DateTimeZone('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); } - - if (!is_object($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)); /* 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/filter/mapistore/event.php b/lib/filter/mapistore/event.php index b9e73a4..39b1a3b 100644 --- a/lib/filter/mapistore/event.php +++ b/lib/filter/mapistore/event.php @@ -1,378 +1,483 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_event extends kolab_api_filter_mapistore_common { protected $map = array( // common properties [MS-OXOCAL] 'PidLidAppointmentSequence' => 'sequence', // PtypInteger32 'PidLidBusyStatus' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-BUSYSTATUS 'PidLidAppointmentAuxiliaryFlags' => '', // PtypInteger32 'PidLidLocation' => 'location', // PtypString 'PidLidAppointmentStartWhole' => 'dtstart', // PtypTime, UTC 'PidLidAppointmentEndWhole' => 'dtend', // PtypTime, UTC 'PidLidAppointmentDuration' => '', // PtypInteger32, optional 'PidLidAppointmentSubType' => '', // PtypBoolean 'PidLidAppointmentStateFlags' => '', // PtypInteger32 'PidLidResponseStatus' => '', // PtypInteger32 'PidLidRecurring' => '', // PtypBoolean 'PidLidIsRecurring' => '', // PtypBoolean 'PidLidClipStart' => '', // PtypTime 'PidLidClipEnd' => '', // PtypTime 'PidLidAllAttendeesString' => '', // PtypString 'PidLidToAttendeesString' => '', // PtypString 'PidLidCCAttendeesString' => '', // PtypString 'PidLidNonSendableTo' => '', // PtypString 'PidLidNonSendableCc' => '', // PtypString 'PidLidNonSendableBcc' => '', // PtypString 'PidLidNonSendToTrackStatus' => '', // PtypMultipleInteger32 'PidLidNonSendCcTrackStatus' => '', // PtypMultipleInteger32 'PidLidNonSendBccTrackStatus' => '', // PtypMultipleInteger32 'PidLidAppointmentUnsendableRecipients' => '', // PtypBinary, optional 'PidLidAppointmentNotAllowPropose' => '', // PtypBoolean, @TODO: X-MICROSOFT-CDO-DISALLOW-COUNTER 'PidLidGlobalObjectId' => '', // PtypBinary 'PidLidCleanGlobalObjectId' => '', // PtypBinary 'PidTagOwnerAppointmentId' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-OWNERAPPTID 'PidTagStartDate' => '', // PtypTime 'PidTagEndDate' => '', // PtypTime 'PidLidCommonStart' => '', // PtypTime 'PidLidCommonEnd' => '', // PtypTime 'PidLidOwnerCriticalChange' => '', // PtypTime, @TODO: X-MICROSOFT-CDO-CRITICAL-CHANGE 'PidLidIsException' => '', // PtypBoolean 'PidTagResponseRequested' => '', // PtypBoolean 'PidTagReplyRequested' => '', // PtypBoolean 'PidLidTimeZoneStruct' => '', // PtypBinary 'PidLidTimeZoneDescription' => '', // PtypString 'PidLidAppointmentTimeZoneDefinitionRecur' => '', // PtypBinary 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => '', // PtypBinary 'PidLidAppointmentTimeZoneDefinitionEndDisplay' => '', // PtypBinary 'PidLidAppointmentRecur' => '', // PtypBinary 'PidLidRecurrenceType' => '', // PtypInteger32 'PidLidRecurrencePattern' => '', // PtypString 'PidLidLinkedTaskItems' => '', // PtypMultipleBinary 'PidLidMeetingWorkspaceUrl' => '', // PtypString 'PidTagIconIndex' => '', // PtypInteger32 'PidLidAppointmentColor' => '', // PtypInteger32 'PidLidAppointmentReplyTime' => '', // @TODO: X-MICROSOFT-CDO-REPLYTIME 'PidLidIntendedBusyStatus' => '', // @TODO: X-MICROSOFT-CDO-INTENDEDSTATUS // calendar object properties [MS-OXOCAL] 'PidTagMessageClass' => '', 'PidLidSideEffects' => '', // PtypInteger32 'PidLidFExceptionAttendees' => '', // PtypBoolean 'PidLidClientIntent' => '', // PtypInteger32 // common props [MS-OXCMSG] 'PidTagSubject' => 'summary', 'PidTagBody' => 'description', 'PidTagHtml' => '', // @TODO: (?) 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagContentId' => '', 'PidTagBodyContentLocation' => '', 'PidTagImportance' => 'priority', 'PidTagSensitivity' => 'class', 'PidLidPrivate' => '', 'PidTagCreationTime' => 'created', 'PidTagLastModificationTime' => 'dtstamp', // reminder properties [MS-OXORMDR] 'PidLidReminderSet' => '', // PtypBoolean 'PidLidReminderSignalTime' => '', // PtypTime 'PidLidReminderDelta' => '', // PtypInteger32 'PidLidReminderTime' => '', // PtypTime 'PidLidReminderOverride' => '', // PtypBoolean 'PidLidReminderPlaySound' => '', // PtypBoolean 'PidLidReminderFileParameter' => '', // PtypString 'PidTagReplyTime' => '', // PtypTime 'PidLidReminderType' => '', // PtypInteger32 ); /** * Message importance for PidTagImportance as defined in [MS-OXCMSG] */ protected $importance = array( 0 => 0x00000000, 1 => 0x00000002, 2 => 0x00000002, 3 => 0x00000002, 4 => 0x00000002, 5 => 0x00000001, 6 => 0x00000000, 7 => 0x00000000, 8 => 0x00000000, 9 => 0x00000000, ); /** * Message sesnitivity for PidTagSensitivity as defined in [MS-OXCMSG] */ protected $sensitivity = array( 'public' => 0x00000000, 'personal' => 0x00000001, 'private' => 0x00000002, 'confidential' => 0x00000003, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => 'IPM.Appointment', // mapistore REST API specific properties 'collection' => 'calendars', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagSensitivity': $value = (int) $this->sensitivity[strtolower($value)]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = $this->date_php2mapi($value, true); break; case 'PidTagImportance': $value = (int) $this->importance[(int) $value]; break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': $dt = kolab_api_input_json::to_datetime($value); $value = $this->date_php2mapi($dt, true); - // PidLidAppointmentTimeZoneDefinitionStartDisplay - // PidLidAppointmentTimeZoneDefinitionEndDisplay - // this is all-day event if ($dt->_dateonly) { $result['PidLidAppointmentSubType'] = 0x00000001; } + else if (empty($data['rrule']) && $dt->getTimezone()->getName() != 'UTC') { + $idx = sprintf('PidLidAppointmentTimeZoneDefinition%sDisplay', + strpos($mapi_idx, 'Start') ? 'Start' : 'End'); + + $result[$idx] = $this->timezone_definition($dt); + } break; } $result[$mapi_idx] = $value; } // Organizer if (!empty($data['organizer'])) { $this->attendee_to_recipient($data['organizer'], $result, true); } // Attendees [MS-OXCICAL 2.1.3.1.1.20.2] foreach ((array) $data['attendee'] as $attendee) { $this->attendee_to_recipient($attendee, $result); } // Alarms (MAPI supports only one) foreach ((array) $data['valarm'] as $alarm) { if ($alarm['properties'] && $alarm['properties']['action'] == 'DISPLAY' && ($duration = $alarm['properties']['trigger']['duration']) && ($delta = self::reminder_duration_to_delta($duration)) ) { $result['PidLidReminderDelta'] = $delta; $result['PidLidReminderSet'] = true; // PidLidReminderTime // PidLidReminderSignalTime break; } } // @TODO: PidLidAppointmentDuration // @TODO: exceptions, resources // Recurrence if ($rule = $this->recurrence_from_kolab($data, $result)) { $result['PidLidAppointmentRecur'] = $rule; + if ($dt && $dt->getTimezone()->getName() != 'UTC') { + $result['PidLidTimeZoneStruct'] = $this->timezone_structure($dt); + $result['PidLidTimeZoneDescription'] = $this->timezone_description($dt); + } } $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); + if ($data['PidLidTimeZoneStruct']) { + $timezone = $this->timezone_structure_to_tzname($data['PidLidTimeZoneStruct']); + } + foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagImportance': $map = array( 0x00000002 => 1, 0x00000001 => 5, 0x00000000 => 9, ); $value = (int) $map[(int) $value]; break; case 'PidTagSensitivity': $map = array_flip($this->sensitivity); $value = $map[$value]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': if ($value) { - $value = $this->date_mapi2php($value); - $format = $data['PidLidAppointmentSubType'] ? 'Y-m-d' : 'Y-m-d\TH:i:s\Z'; - $value = $value->format($format); + $datetime = $this->date_mapi2php($value); + $datetime->_dateonly = !empty($data['PidLidAppointmentSubType']); + + $tz_idx = sprintf('PidLidAppointmentTimeZoneDefinition%sDisplay', + strpos($mapi_idx, 'Start') ? 'Start' : 'End'); + + if ($data[$tz_idx]) { + $tz = $this->timezone_definition_to_tzname($data[$tz_idx]); + } + else { + $tz = $timezone; + } + + $value = kolab_api_input_json::from_datetime($datetime, $tz); } break; } $result[$kolab_idx] = $value; } // Alarms (MAPI supports only one, DISPLAY) 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(); } // Recurrence if (array_key_exists('PidLidAppointmentRecur', $data)) { $this->recurrence_to_kolab($data['PidLidAppointmentRecur'], $result, 'event'); } if (array_key_exists('recipients', $data)) { $result['attendee'] = array(); $result['organizer'] = array(); foreach ((array) $data['recipients'] as $recipient) { $this->recipient_to_attendee($recipient, $result); } } // @TODO: PidLidAppointmentDuration (?) // @TODO: exception, resources $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); // @TODO: add properties that are not in the map $map['PidLidAppointmentRecur'] = 'rrule'; return $map; } /** * 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; } } + + /** + * Generate PidLidTimeZoneDescription string for a timezone + * specified in a DateTime object + */ + protected static function timezone_description($datetime) + { + $timezone = $datetime->getTimezone(); + $location = $timezone->getLocation(); + $description = $location['comments']; + $offset = $timezone->getOffset($datetime); + + // some location descriptions are useful, but some are not really + // replace with timezone name in such cases + if (!$description || strpos($description, 'location') !== false) { + $description = $timezone->getName(); + } + + // convert seconds into hours offset format + $hours = round(abs($offset)/3600); + $minutes = round((abs($offset) - $hours * 3600) / 60); + $offset = sprintf('%s%02d:%02d', $offset < 0 ? '-' : '+', $hours, $minutes); + + return sprintf('(GMT%s) %s', $offset, $description); + } + + /** + * Generate PidLidTimeZoneDefinitionRecur blob for a timezone + * specified in a DateTime object + */ + protected static function timezone_definition($datetime) + { + $timezone = $datetime->getTimezone(); + $tzrule = kolab_api_filter_mapistore_structure_tzrule::from_datetime($datetime); + $tzrule->Flags = kolab_api_filter_mapistore_structure_tzrule::FLAG_EFFECTIVE; // @FIXME + + $tzdef = new kolab_api_filter_mapistore_structure_timezonedefinition(array( + 'TZRules' => array($tzrule), + 'KeyName' => $timezone->getName(), + )); + + return $tzdef->output(true); + } + + /** + * Generate PidLidTimeZoneStruct blob for a timezone + * specified in a DateTime object + */ + protected static function timezone_structure($datetime) + { + $tzs = kolab_api_filter_mapistore_structure_timezonestruct::from_datetime($datetime, true); + return $tzs->output(true); + } + + /** + * Parse PidLidTimeZoneStruct blob and convert to timezone name + */ + protected static function timezone_structure_to_tzname($data) + { + $api = kolab_api::get_instance(); + $tzs = new kolab_api_filter_mapistore_structure_timezonestruct; + $tzs->input($data, true); + + return $tzs->to_tzname($api->config->get('timezone')); + } + + /** + * Parse PidLidTimeZoneDefinitionRecur blob and convert to timezone name + */ + protected static function timezone_definition_to_tzname($data) + { + $api = kolab_api::get_instance(); + $tzdef = new kolab_api_filter_mapistore_structure_timezonedefinition; + $tzdef->input($data, true); + + // Note: we ignore KeyName as it most likely will not contain Olson TZ name + + foreach ($tzdef->TZRules as $tzrule) { + if ($tzname = $tzrule->to_tzname($api->config->get('timezone'))) { + return $tzname; + } + } + } } diff --git a/lib/filter/mapistore/structure.php b/lib/filter/mapistore/structure.php index ff75c5c..4ad7911 100644 --- a/lib/filter/mapistore/structure.php +++ b/lib/filter/mapistore/structure.php @@ -1,333 +1,340 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * MAPI structures handler */ class kolab_api_filter_mapistore_structure { protected $structure = array(); protected $data = array(); protected $lengths = array( 'BYTE' => 1, 'WORD' => 2, 'LONG' => 4, 'ULONG' => 4, 'SYSTEMTIME' => 16, ); + const WSTRING_CHARSET = 'UCS-2LE'; // UTF-16LE (?) + + /** * Class constructor * * @param array Structure data properties */ public function __construct($data = array()) { if (!empty($data)) { $this->data = $data; } } /** * Convert binary input into internal structure * * @param string $input Binary representation of the structure * @param bool $base64 Set to TRUE if the input is base64-encoded * @param object $parent Parent structure * * @return int Number of bytes read from the binary input */ public function input($input, $base64 = false, $parent = null) { if ($base64) { $input = base64_decode($input); } $input_length = strlen($input); $position = 0; $counter = 0; foreach ($this->structure as $idx => $struct) { $length = 0; $class = null; $is_array = false; $count = 1; switch ($struct['type']) { case 'EMPTY': continue 2; case 'STRING': $length = $struct['length'] ?: (int) $this->data[$struct['counter']]; break; case 'WSTRING': $length = $struct['length'] ?: ((int) $this->data[$struct['counter']]) * 2; break; case 'BYTE': case 'WORD': case 'LONG': case 'ULONG': case 'SYSTEMTIME': default: if (preg_match('/^(LONG|ULONG|WORD|BYTE)\[([0-9]*)\]$/', $struct['type'], $m)) { $is_array = true; $count = $m[2] ? $m[2] : (int) $this->data[$struct['counter']]; $struct['type'] = $m[1]; $length = $this->lengths[$struct['type']]; } else if (preg_match('/^(\[?)(kolab_api_[a-z_]+)\]?$/', $struct['type'], $m)) { $length = 0; $class = $m[2]; $is_array = !empty($m[1]); $count = $is_array ? (int) $this->data[$struct['counter']] : 1; } else { $length = $this->lengths[$struct['type']]; } } if ($length && $position >= $input_length) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => 'Invalid MAPI structure for ' . get_class($this) )); } for ($i = 0; $i < $count; $i++) { if ($length) { $value = substr($input, $position, $length); $position += $length; } else { $value = null; } switch ($struct['type']) { case 'WSTRING': - $value = rcube_charset::convert($value, 'UTF-16LE', RCUBE_CHARSET); + $value = rcube_charset::convert($value, self::WSTRING_CHARSET, RCUBE_CHARSET); // no-break case 'STRING': break; case 'BYTE': $value = ord($value); break; case 'WORD': $unpack = unpack('v', $value); $value = $unpack[1]; break; case 'LONG': $unpack = unpack('l', $value); $value = $unpack[1]; break; case 'ULONG': $unpack = unpack('V', $value); $value = $unpack[1]; break; case 'SYSTEMTIME': $structure = new kolab_api_filter_mapistore_structure_systemtime; $structure->input($value, false, $this, $is_array ? $i : null); $value = $structure; break; default: $structure = new $class; $position += $structure->input(substr($input, $position), false, $this, $is_array ? $i : null); $value = $structure; } if ($value !== null) { if ($is_array) { $this->data[$idx][] = $value; } else { $this->data[$idx] = $value; } } } } return $position; } /** * Convert internal structure into binary string * * @param bool $base64 Enables base64 encoding of the output * * @return string Binary representation of the structure */ public function output($base64 = false) { $output = ''; foreach ($this->structure as $idx => $struct) { if (!array_key_exists($idx, $this->data)) { if ($struct['counter'] && !$this->data[$struct['counter']]) { continue; } else if (!isset($struct['default']) && $struct['type'] !== 'STRING' && $struct['type'] !== 'EMPTY') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Missing property " . get_class($this) . "::$idx", )); } else { $value = $struct['default']; } } else { $value = $this->data[$idx]; } switch ($struct['type']) { case 'EMPTY': break; case 'WSTRING': - $value = rcube_charset::convert($value, RCUBE_CHARSET, 'UTF-16LE'); + $value = rcube_charset::convert($value, RCUBE_CHARSET, self::WSTRING_CHARSET); // no-break case 'STRING': $output .= $value; break; case 'BYTE': $output .= chr((int) $value); break; case 'WORD': $output .= pack('v', $value); break; case 'LONG': $output .= pack('l', $value); break; case 'ULONG': $output .= pack('V', (int) $value); break; case 'SYSTEMTIME': if ($value instanceof kolab_api_filter_mapistore_structure_systemtime) { $output .= $value->output(); } else { $output .= pack('llll', 0, 0, 0, 0); } break; default: if (preg_match('/^(LONG|WORD|ULONG|BYTE)\[([0-9]*)\]$/', $struct['type'], $m)) { $count = $m[2] ? $m[2] : count((array) $value); for ($x = 0; $x < $count; $x++) { switch ($m[1]) { case 'BYTE': $output .= chr((int) $value[$x]); break; case 'WORD': $output .= pack('v', $value[$x]); break; case 'LONG': $output .= pack('l', $value[$x]); break; case 'ULONG': $output .= pack('V', $value[$x]); break; } } } else if (preg_match('/^\[?(kolab_api_[a-z_]+)\]?$/', $struct['type'], $m)) { $type = $m[1]; if (!is_array($value)) { $value = !empty($value) ? array($value) : array(); } foreach ($value as $v) { if (!($v instanceof $type)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Expected object of type $type" )); } $output .= $v->output(); } } } } if ($base64) { $output = base64_encode($output); } return $output; } /** * Sets class data item */ public function __set($name, $value) { if (!array_key_exists($name, $this->structure)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => 'Invalid member of MAPI structure: ' . get_class($this) . '::' . $name )); } $this->data[$name] = $value; } /** * Gets class data item */ public function __get($name) { if (!array_key_exists($name, $this->structure)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => 'Invalid member of MAPI structure: ' . get_class($this) . '::' . $name )); } - return $this->data[$name]; + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + return $this->structure[$name]['default']; } } diff --git a/lib/filter/mapistore/structure/systemtime.php b/lib/filter/mapistore/structure/systemtime.php index acd2a16..fa596d1 100644 --- a/lib/filter/mapistore/structure/systemtime.php +++ b/lib/filter/mapistore/structure/systemtime.php @@ -1,41 +1,41 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Systemtime structure definition according to MS-DTYP 2.3.13 */ class kolab_api_filter_mapistore_structure_systemtime extends kolab_api_filter_mapistore_structure { protected $structure = array( - 'WYear' => array('type' => 'WORD'), - 'WMonth' => array('type' => 'WORD'), - 'WDayOfWeek' => array('type' => 'WORD'), - 'WDay' => array('type' => 'WORD'), - 'WHour' => array('type' => 'WORD'), - 'WMinute' => array('type' => 'WORD', 'default' => 0), - 'WSecond' => array('type' => 'WORD', 'default' => 0), - 'WMilliseconds' => array('type' => 'WORD', 'default' => 0), + 'Year' => array('type' => 'WORD', 'default' => 0), + 'Month' => array('type' => 'WORD'), + 'DayOfWeek' => array('type' => 'WORD'), + 'Day' => array('type' => 'WORD'), + 'Hour' => array('type' => 'WORD'), + 'Minute' => array('type' => 'WORD', 'default' => 0), + 'Second' => array('type' => 'WORD', 'default' => 0), + 'Milliseconds' => array('type' => 'WORD', 'default' => 0), ); } diff --git a/lib/filter/mapistore/structure/timezonedefinition.php b/lib/filter/mapistore/structure/timezonedefinition.php new file mode 100644 index 0000000..6a0dcb4 --- /dev/null +++ b/lib/filter/mapistore/structure/timezonedefinition.php @@ -0,0 +1,106 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +/** + * PidLidAppointmentTimeZoneDefinitionRecur structure definition according to MS-OXOCAL 2.2.1.41 + */ +class kolab_api_filter_mapistore_structure_timezonedefinition extends kolab_api_filter_mapistore_structure +{ + protected $structure = array( + 'MajorVersion' => array('type' => 'BYTE', 'default' => 0x02), + 'MinorVersion' => array('type' => 'BYTE', 'default' => 0x01), + 'cbHeader' => array('type' => 'WORD'), + 'Reserved' => array('type' => 'WORD', 'default' => 0x0002), + 'cchKeyName' => array('type' => 'WORD'), + 'KeyName' => array('type' => 'WSTRING', 'counter' => 'cchKeyName', 'default' => ''), + 'cRules' => array('type' => 'WORD'), + 'TZRules' => array('type' => '[kolab_api_filter_mapistore_structure_tzrule]', 'counter' => 'cRules'), + ); + + + /** + * Convert binary input into internal structure + * + * @param string $input Binary representation of the structure + * @param bool $base64 Set to TRUE if the input is base64-encoded + * + * @return int Number of bytes read from the binary input + */ +/* + public function input($input, $base64 = false) + { + if ($base64) { + $input = base64_decode($input); + } + + // Read KeyName field size + $unpack = unpack('V', substr($input, 6, 2)); + $value = $unpack[1]; + + $this->structure['KeyName']['length'] = $value; + + return parent::input($input, false); + } +*/ + /** + * Convert internal structure into binary string + * + * @param bool $base64 Enables base64 encoding of the output + * + * @return string Binary representation of the structure + */ + public function output($base64 = false) + { + $this->data['cRules'] = count($this->data['TZRules']); + + if ($this->data['cRules'] < 1) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => 'No TZRules specified in ' . get_class($this), + )); + } + + if ($this->data['cRules'] > 1024) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => 'Too many TZRules specified in ' . get_class($this) + . ' (' . $this->data['cRules'] . ' > 1024)', + )); + } + + $this->data['cchKeyName'] = mb_strlen($this->data['KeyName']); + + if ($this->data['cchKeyName'] > 260) { + $this->data['cchKeyName'] = 260; + $this->data['KeyName'] = mb_substr($this->data['KeyName'], 0, 260); + } + + $this->data['cbHeader'] = 6 + $this->data['cchKeyName'] * 2; + + return parent::output($base64); + } +} diff --git a/lib/filter/mapistore/structure/timezonestruct.php b/lib/filter/mapistore/structure/timezonestruct.php index b85506e..448db14 100644 --- a/lib/filter/mapistore/structure/timezonestruct.php +++ b/lib/filter/mapistore/structure/timezonestruct.php @@ -1,40 +1,109 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * TimeZoneStruct structure definition according to MS-OXOCAL 2.2.1.39 */ class kolab_api_filter_mapistore_structure_timezonestruct extends kolab_api_filter_mapistore_structure { protected $structure = array( - 'IBias' => array('type' => 'LONG'), - 'IStandardBias' => array('type' => 'LONG'), - 'IDaylightBias' => array('type' => 'LONG'), - 'WStandardYear' => array('type' => 'WORD'), - 'StStandardDate' => array('type' => 'SYSTEMTIME'), - 'WDaylightYear' => array('type' => 'WORD'), - 'StDaylightDate' => array('type' => 'SYSTEMTIME'), + 'Bias' => array('type' => 'LONG'), + 'StandardBias' => array('type' => 'LONG'), + 'DaylightBias' => array('type' => 'LONG'), + 'StandardYear' => array('type' => 'WORD', 'default' => 0), + 'StandardDate' => array('type' => 'SYSTEMTIME'), + 'DaylightYear' => array('type' => 'WORD', 'default' => 0), + 'DaylightDate' => array('type' => 'SYSTEMTIME'), ); + + /** + * Convert object data into timezone name. + * If conversion is not possible 'UTC' will be returned. + * + * @param string $preferred Preferred timezone, used when MAPI structure + * resolves to more than one timezone. + * + * @return sting Timezone name + */ + public function to_tzname($preferred = null) + { + $tz = new kolab_api_filter_mapistore_timezone; + + foreach (array('Bias', 'StandardBias', 'DaylightBias') as $key) { + $tz->{$key} = $this->{$key}; + } + + foreach (array('Standard', 'Daylight') as $key) { + if ($systime = $this->{$key . 'Date'}) { + $tz->{$key . 'Year'} = $systime->{'Year'}; + $tz->{$key . 'Month'} = $systime->{'Month'}; + $tz->{$key . 'DayOfWeek'} = $systime->{'DayOfWeek'}; + $tz->{$key . 'Day'} = $systime->{'Day'}; + $tz->{$key . 'Hour'} = $systime->{'Hour'}; + $tz->{$key . 'Minute'} = $systime->{'Minute'}; +// $tz->{$key . 'Second'} = $systime->{'Second'}; +// $tz->{$key . 'Milliseconds'} = $systime->{'Milliseconds'}; + } + } + + return $tz->get_timezone($preferred); + } + + /** + * Create class instance from PHP DateTime + * + * @param DateTime $date PHP DateTime object + * @param bool $no_year Reset the Year to 0 + * + * @return kolab_api_filter_mapistore_structure_timezonestruct + */ + public static function from_datetime($date, $no_year = false) + { + $tz = kolab_api_filter_mapistore_timezone::from_date($date); + $result = new self; + + foreach (array('Bias', 'StandardBias', 'DaylightBias') as $key) { + $result->{$key} = $tz->{$key}; + } + + foreach (array('Standard', 'Daylight') as $key) { + $result->{$key . 'Date'} = new kolab_api_filter_mapistore_structure_systemtime(array( + 'Year' => $no_year ? 0 : $tz->{$key . 'Year'}, + 'Month' => $tz->{$key . 'Month'}, + 'DayOfWeek' => $tz->{$key . 'DayOfWeek'}, + 'Day' => $tz->{$key . 'Day'}, + 'Hour' => $tz->{$key . 'Hour'}, + 'Minute' => $tz->{$key . 'Minute'}, +// 'Second' => 0, +// 'Milliseconds' => 0, + )); + } + + $result->StandardYear = $result->StandardDate->Year; + $result->DaylightYear = $result->DaylightDate->Year; + + return $result; + } } diff --git a/lib/filter/mapistore/structure/tzrule.php b/lib/filter/mapistore/structure/tzrule.php index 660bdb9..64f5cf4 100644 --- a/lib/filter/mapistore/structure/tzrule.php +++ b/lib/filter/mapistore/structure/tzrule.php @@ -1,47 +1,116 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * TZRule structure definition according to MS-OXOCAL 2.2.1.41.1 */ class kolab_api_filter_mapistore_structure_tzrule extends kolab_api_filter_mapistore_structure { protected $structure = array( - 'MajorVersion' => array('type' => 'BYTE', 'default' => 0x01), - 'MinorVersion' => array('type' => 'BYTE', 'default' => 0x01), - 'Reserved' => array('type' => 'WORD', 'default' => 0x003E), - 'Flags' => array('type' => 'WORD', 'default' => 0), - 'WYear' => array('type' => 'WORD'), - 'X' => array('type' => 'BYTE[14]', 'default' => 0), - 'IBias' => array('type' => 'LONG'), - 'IStandardBias' => array('type' => 'LONG'), - 'IDaylightBias' => array('type' => 'LONG'), - 'StStandardDate' => array('type' => 'SYSTEMTIME'), - 'StDaylightDate' => array('type' => 'SYSTEMTIME'), + 'MajorVersion' => array('type' => 'BYTE', 'default' => 0x02), + 'MinorVersion' => array('type' => 'BYTE', 'default' => 0x01), + 'Reserved' => array('type' => 'WORD', 'default' => 0x003E), + 'Flags' => array('type' => 'WORD', 'default' => 0), + 'Year' => array('type' => 'WORD', 'default' => 1601), + 'X' => array('type' => 'BYTE[14]', 'default' => 0), + 'Bias' => array('type' => 'LONG'), + 'StandardBias' => array('type' => 'LONG'), + 'DaylightBias' => array('type' => 'LONG'), + 'StandardDate' => array('type' => 'SYSTEMTIME'), + 'DaylightDate' => array('type' => 'SYSTEMTIME'), ); - const FLAG_RECUR_CURRENT = 1; - const FLAG_EFFECTIVE = 2; + const FLAG_RECUR_CURRENT = 0x0001; + const FLAG_EFFECTIVE = 0x0002; + + + /** + * Convert object data into timezone name. + * If conversion is not possible 'UTC' will be returned. + * + * @param string $preferred Preferred timezone, used when MAPI structure + * resolves to more than one timezone. + * + * @return sting Timezone name + */ + public function to_tzname($preferred = null) + { + $tz = new kolab_api_filter_mapistore_timezone; + + foreach (array('Bias', 'StandardBias', 'DaylightBias') as $key) { + $tz->{$key} = $this->{$key}; + } + + foreach (array('Standard', 'Daylight') as $key) { + if ($systime = $this->{$key . 'Date'}) { + $tz->{$key . 'Year'} = $systime->{'Year'}; + $tz->{$key . 'Month'} = $systime->{'Month'}; + $tz->{$key . 'DayOfWeek'} = $systime->{'DayOfWeek'}; + $tz->{$key . 'Day'} = $systime->{'Day'}; + $tz->{$key . 'Hour'} = $systime->{'Hour'}; + $tz->{$key . 'Minute'} = $systime->{'Minute'}; +// $tz->{$key . 'Second'} = $systime->{'Second'}; +// $tz->{$key . 'Milliseconds'} = $systime->{'Milliseconds'}; + } + } + + return $tz->get_timezone($preferred); + } + + /** + * Create class instance from PHP DateTime + * + * @param DateTime $date PHP DateTime object + * @param bool $no_year Reset the Year to 0. + * + * @return kolab_api_filetr_mapistore_structure_tzrule + */ + public static function from_datetime($date, $year = null) + { + $tz = kolab_api_filter_mapistore_timezone::from_date($date); + $result = new self; + + foreach (array('Bias', 'StandardBias', 'DaylightBias') as $key) { + $result->{$key} = $tz->{$key}; + } + + foreach (array('Standard', 'Daylight') as $key) { + $result->{$key . 'Date'} = new kolab_api_filter_mapistore_structure_systemtime(array( + 'Year' => $year ? $year : $tz->{$key . 'Year'}, + 'Month' => $tz->{$key . 'Month'}, + 'DayOfWeek' => $tz->{$key . 'DayOfWeek'}, + 'Day' => $tz->{$key . 'Day'}, + 'Hour' => $tz->{$key . 'Hour'}, + 'Minute' => $tz->{$key . 'Minute'}, +// 'Second' => 0, +// 'Milliseconds' => 0, + )); + } + + $result->Year = $year ? $year : $result->StandardDate->Year; + + return $result; + } } diff --git a/lib/filter/mapistore/timezone.php b/lib/filter/mapistore/timezone.php new file mode 100644 index 0000000..ea378c1 --- /dev/null +++ b/lib/filter/mapistore/timezone.php @@ -0,0 +1,457 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +/** + * Utility functions for converting Timezone from/to MAPI format(s). + */ +class kolab_api_filter_mapistore_timezone +{ + public $Bias = 0; + public $StandardYear = 0; + public $StandardMonth = 0; + public $StandardDayOfWeek = 0; + public $StandardDay = 0; + public $StandardHour = 0; + public $StandardMinute = 0; + public $StandardSecond = 0; + public $StandardMilliseconds = 0; + public $StandardBias = 0; + public $DaylightYear = 0; + public $DaylightMonth = 0; + public $DaylightDay = 0; + public $DaylightDayOfWeek = 0; + public $DaylightHour = 0; + public $DaylightMinute = 0; + public $DaylightSecond = 0; + public $DaylightMilliseconds = 0; + public $DaylightBias = 0; + + /** + * PidLidTimeZone property values + * + * @var array + */ + protected static $tz_data = array( + 0 => array(0, null, null), + 1 => array(720, array(10, 0, 5, 2), array(3, 0, 5, 1)), + 2 => array(660, array(9, 0, 5, 2), array(3, 0, 5, 1)), + 3 => array(660, array(10, 0, 5, 3), array(3, 0, 5, 2)), + 4 => array(660, array(10, 0, 5, 3), array(3, 0, 5, 2)), + 5 => array(600, array(9, 0, 5, 1), array(3, 0, 5, 0)), + 6 => array(660, array(9, 0, 5, 1), array(3, 0, 5, 0)), + 7 => array(600, array(10, 0, 5, 4), array(3, 0, 5, 3)), + 8 => array(900, array(2, 0, 2, 2), array(10, 0, 3, 2)), + 9 => array(960, array(11, 0, 1, 2), array(3, 0, 2, 2)), + 10 => array(1020, array(11, 0, 1, 2), array(3, 0, 2, 2)), + 11 => array(1080, array(11, 0, 1, 2), array(3, 0, 2, 2)), + 12 => array(1140, array(11, 0, 1, 2), array(3, 0, 2, 2)), + 13 => array(1200, array(11, 0, 1, 2), array(3, 0, 2, 2)), + 14 => array(1260, array(11, 0, 1, 2), array(3, 0, 2, 2)), + 15 => array(1320, null, null), + 16 => array(1380, null, null), + 17 => array(0, array(4, 0, 1, 3), array(9, 0, 5, 2)), + 18 => array(120, array(3, 0, 5, 3), array(10, 0, 5, 2)), + 19 => array(150, array(3, 0, 5, 3), array(10, 0, 5, 2)), + 20 => array(180, null, null), + 21 => array(240, null, null), + 22 => array(300, null, null), + 23 => array(390, null, null), + 24 => array(480, null, null), + 25 => array(510, array(9, 2, 4, 2), array(3, 0, 1, 2)), + 26 => array(540, null, null), + 27 => array(600, array(9, 0, 3, 2), array(3, 5, 5, 2)), + 28 => array(930, array(11, 0, 1, 0), array(3, 0, 2, 0)), + 29 => array(780, array(10, 0, 5, 1), array(3, 0, 5, 0)), + 30 => array(840, array(10, 0, 5, 1), array(3, 0, 5, 0)), + 31 => array(720, null, null), + 32 => array(900, null, null), + 33 => array(960, null, null), + 34 => array(1020, null, null), + 35 => array(10200, null, null), + 36 => array(1080, null, null), + 37 => array(1080, array(10, 0, 5, 2), array(4, 0, 1, 2)), + 38 => array(1140, null, null), + 39 => array(1440, null, null), + 40 => array(0, null, null), + 41 => array(60, null, null), + 42 => array(120, array(3, 0, 5, 2), array(10, 0, 1, 2)), + 43 => array(120, null, null), + 44 => array(150, null, null), + 45 => array(240, array(9, 0, 2, 2), array(4, 0, 2, 2)), + 46 => array(360, null, null), + 47 => array(420, null, null), + 48 => array(450, null, null), + 49 => array(600, array(9, 4, 5, 2), array(5, 5, 1, 2)), + 50 => array(600, null, null), + 51 => array(540, array(10, 0, 5, 1), array(3, 0, 5, 0)), + 52 => array(120, array(3, 0, 5, 2), array(8, 0, 5, 2)), + 53 => array(120, array(4, 0, 1, 3), array(10, 0, 5, 2)), + 54 => array(150, array(4, 0, 1, 3), array(10, 0, 5, 2)), + 55 => array(120, array(4, 0, 1, 3), array(10, 0, 1, 2)), + 56 => array(960, array(3, 6, 2, 23), array(10, 6, 2, 23)), + 57 => array(240, array(3, 0, 5, 3), array(10, 0, 5, 2)), + 58 => array(1140, array(10, 0, 5, 2), array(4, 0, 1, 2)), + 59 => array(1200, array(10, 0, 5, 2), array(4, 0, 1, 2)), + ); + + + /** + * Create class instance from DateTime object + * + * @param DateTime $date A date object. + * + * @return kolab_api_filter_mapistore_timezone + */ + public static function from_date($date) + { + $result = new self; + + list($standard, $daylight) = self::get_transitions($date); + + if (!empty($standard)) { + $result->Bias = $standard['offset'] / 60 * -1; + + if (!empty($daylight)) { + self::set_transition($result, $standard, 'Standard'); + self::set_transition($result, $daylight, 'Daylight'); + + $result->StandardHour += $daylight['offset'] / 3600; + $result->DaylightHour += $standard['offset'] / 3600; + $result->DaylightBias = ($daylight['offset'] - $standard['offset']) / 60 * -1; + } + } + + return $result; + } + + /** + * Attempt to guess the timezone identifier from the current object data. + * + * If preferred timezone is specified and it matches current data + * it will be returned, otherwise it returns any matching timezone or UTC. + * + * @param string $preferred The preferred timezone. + * + * @return string The timezone identifier + */ + public function get_timezone($preferred = null) + { + // Tries to guess the correct start date depending on object + // property, falls back to current date. + if (!empty($this->StandardYear)) { + $datetime = new DateTime($this->StandardYear . '-01-01'); + } + else { + $datetime = new DateTime('1 year ago'); + } + + // If preferred TZ is not specified, try one configured on the server + // it's likely better than just selecting the first matching TZ. + if (!$preferred) { + $preferred = date_default_timezone_get(); + } + + // First check the preferred timezone + if ($preferred && $this->check_timezone($preferred, $datetime)) { + return $preferred; + } + + // @TODO: for better results we should first check + // more common areas of the globe and maybe skip some e.g. Arctica. + + foreach (DateTimeZone::listIdentifiers() as $timezone) { + if ($this->check_timezone($timezone, $datetime)) { + return $timezone; + } + } + + return 'UTC'; + } + + /** + * Get PidLidTimeZone value from PHP DateTime + * + * @param DateTime $date A date object. + * + * @return int Timezone identifier, NULL if not found + */ + public static function get_int_from_date($date) + { + $tz = self::from_date($date); + + foreach (self::$tz_data as $int => $data) { + if ($tz->Bias == self::from_mapi_offset($data[0]) + && $tz->StandardMonth == $data[1][0] + && $tz->StandardDayOfWeek == $data[1][1] + && $tz->StandardDay == $data[1][2] + && $tz->StandardHour == $data[1][3] + && $tz->DaylightMonth == $data[2][0] + && $tz->DaylightDayOfWeek == $data[2][1] + && $tz->DaylightDay == $data[2][2] + && $tz->DaylightHour == $data[2][3] + ) { + return $int; + } + } + } + + /** + * Attempt to guess the timezone identifier from PidLidTimeZone value. + * + * If preferred timezone is specified and it matches current data + * it will be returned, otherwise it returns any matching timezone or UTC. + * + * @param int $tz_id MAPI PidLidTimeZone property value + * @param string $preferred The preferred timezone + * + * @return string The timezone identifier + */ + public static function get_timezone_from_int($tz_id, $preferred = null) + { + $data = self::$tz_data[(int) $tz_id]; + + if (empty($data)) { + return 'UTC'; + } + + $tz = new self; + + // fill self object with data from timezone definition + if (!empty($data[1])) { + foreach (array('Standard', 'Daylight') as $idx => $type) { + foreach (array('Month', 'DayOfWeek', 'Day', 'Hour') as $i => $item) { + $tz->{$type . $item} = $data[$idx + 1][$i]; + } + } + } + + $tz->Bias = self::from_mapi_offset($data[0]); + $tz->DaylightBias = -60; // @FIXME + + return $tz->get_timezone($preferred); + } + + /** + * Converts PidLidTimeZone offset for comparison with Bias value + */ + protected static function from_mapi_offset($offset) + { + // MAPI's PidLidTimeZone offset is in minutes from UTC+12 + // and it's always positive and <= 24 * 60. + if ($offset >= 12 * 60) { + $offset -= 12 * 60; + } + else { + $offset = (12 * 60 - $offset) * -1; + } + + return $offset; + } + + /** + * Get the transition data for moving from DST to STD time. + * + * @param DateTime $date The date to start from and specified timezone. + * + * @return array An array containing the the STD and DST transitions + */ + protected static function get_transitions($date) + { + $standard = array(); + $daylight = array(); + $timezone = $date->getTimezone(); + $date_year = $date->format('Y'); + + // get timezone transitions in specified year + $transitions = $timezone->getTransitions( + mktime(0, 0, 0, 12, 1, $date_year - 1), + mktime(24, 0, 0, 12, 31, $date_year) + ); + + foreach ($transitions as $i => $transition) { + try { + $d = new DateTime($transition['time']); + } + catch (Exception $e) { + continue; + } + + $year = $d->format('Y'); + + if ($year == $date_year && isset($transitions[$i + 1])) { + $next = new DateTime($transitions[$i + 1]['time']); + if ($year == $next->format('Y')) { + $daylight = $transition['isdst'] ? $transition : $transitions[$i + 1]; + $standard = $transition['isdst'] ? $transitions[$i + 1] : $transition; + } + else { + $daylight = $transition['isdst'] ? $transition: null; + $standard = $transition['isdst'] ? null : $transition; + } + + break; + } + else if ($i == count($transitions) - 1) { + $standard = $transition; + } + } + + return array($standard, $daylight); + } + + /** + * Calculate and set the offsets for the specified transition + * + * @param self $object Self class instance + * @param array $transition A transition hash from DateTimeZone::getTransitions() + * @param string $type Transition type - daylight or standard + */ + protected static function set_transition($object, $transition, $type) + { + $date = new DateTime($transition['time']); + + $object->{$type . 'Year'} = (int) $date->format('Y'); + $object->{$type . 'Month'} = (int) $date->format('n'); + $object->{$type . 'DayOfWeek'} = (int) $date->format('w'); + $object->{$type . 'Hour'} = (int) $date->format('H'); + $object->{$type . 'Minute'} = (int) $date->format('i'); + + for ($i = 5; $i > 0; $i--) { + if (self::is_nth_occurrence_in_month($transition['time'], $i)) { + $object->{$type . 'Day'} = $i; + break; + } + } + } + + /** + * Check if the given timezone matches the current object and also + * evaluate the daylight saving time transitions for this timezone if necessary. + * + * @param string $timezone The timezone identifier + * @param DateTime $datetime The date to check + * + * @return boolean + */ + protected function check_timezone($timezone, $datetime) + { + try { + $datetime->setTimezone(new DateTimeZone($timezone)); + + list($standard, $daylight) = self::get_transitions($datetime); + + return $this->check_transition($standard, $daylight); + } + catch (Exception $e) { + } + + return false; + } + + /** + * Check if the given Standard and Daylight time transitions match + * current object data. + * + * @param array $sandard The Standard time transition data. + * @param array $daylight The Daylight time transition data. + * + * @return boolean + */ + protected function check_transition($standard, $daylight) + { + if (empty($standard)) { + return false; + } + + $standard_offset = ($this->Bias + $this->StandardBias) * 60 * -1; + + if ($standard_offset == $standard['offset']) { + // There's no DST to compare + if (empty($daylight) || empty($daylight['isdst'])) { + return empty($this->DaylightMonth); + } + + // the milestone is sending a positive value for DaylightBias while it should send a negative value + $daylight_offset = ($this->Bias + $this->DaylightBias) * 60 * -1; + $daylight_offset_milestone = ($this->Bias + ($this->DaylightBias * -1)) * 60 * -1; + + if ($daylight_offset == $daylight['offset'] || $daylight_offset_milestone == $daylight['offset']) { + $standard_dt = new DateTime($standard['time']); + $daylight_dt = new DateTime($daylight['time']); + + if ($standard_dt->format('n') == $this->StandardMonth + && $daylight_dt->format('n') == $this->DaylightMonth + && $standard_dt->format('w') == $this->StandardDayOfWeek + && $daylight_dt->format('w') == $this->DaylightDayOfWeek + ) { + return self::is_nth_occurrence_in_month($daylight['time'], $this->DaylightDay) + && self::is_nth_occurrence_in_month($standard['time'], $this->StandardDay); + } + } + } + + return false; + } + + /** + * Test if the weekday of the given timestamp is the nth occurrence of this + * weekday within its month, where '5' indicates the last occurrence even if + * there is less than five occurences. + * + * @param string $datetime The datetime string representation + * @param integer $occurrence 1 to 5, where 5 indicates the final occurrence + * during the month if that day of the week does + * not occur 5 times + * @return boolean + */ + protected static function is_nth_occurrence_in_month($datetime, $occurrence) + { + $original = new DateTime($datetime); + $orig_month = $original->format('n'); + $modified = clone $original; + + if ($occurrence == 5) { + $modified = $modified->modify('1 week'); + $mod_month = $modified->format('n'); + + // modified month is after the original + return $mod_month > $orig_month || ($mod_month == 1 && $orig_month == 12); + } + + $modified->modify(sprintf('-%d weeks', $occurrence - 1)); + $mod_month = $modified->format('n'); + + if ($mod_month != $orig_month) { + return false; + } + + $modified = clone($original); + $modified->modify(sprintf('-%d weeks', $occurrence)); + $mod_month = $modified->format('n'); + + // modified month is earlier than original + return $mod_month < $orig_month || ($mod_month == 12 && $orig_month == 1); + } +} diff --git a/lib/input/json.php b/lib/input/json.php index 9f01206..0b0b357 100644 --- a/lib/input/json.php +++ b/lib/input/json.php @@ -1,248 +1,282 @@ | +--------------------------------------------------------------------------+ | 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; } 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) { // 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']; } } } } diff --git a/tests/Unit/Filter/Mapistore/Event.php b/tests/Unit/Filter/Mapistore/Event.php index a67a5f0..d5c7a5b 100644 --- a/tests/Unit/Filter/Mapistore/Event.php +++ b/tests/Unit/Filter/Mapistore/Event.php @@ -1,222 +1,266 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '100-100-100-100'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), $result['PidTagCreationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), $result['PidTagLastModificationTime']); $this->assertSame(2, $result['PidLidAppointmentSequence']); $this->assertSame(3, $result['PidTagSensitivity']); $this->assertSame('Work', $result['PidNameKeywords'][0]); /* $this->assertSame('/kolab.org/Europe/Berlin', $result['dtstart']['parameters']['tzid']); $this->assertSame('2015-05-15T10:00:00', $result['dtstart']['date-time']); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtend']['parameters']['tzid']); $this->assertSame('2015-05-15T10:30:00', $result['dtend']['date-time']); $this->assertSame('https://some.url', $result['url']); */ $this->assertSame('Summary', $result['PidTagSubject']); $this->assertSame('Description', $result['PidTagBody']); $this->assertSame(2, $result['PidTagImportance']); $this->assertSame('Location', $result['PidLidLocation']); $this->assertSame('German, Mark', $result['recipients'][0]['PidTagDisplayName']); $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagEmailAddress']); $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); $this->assertSame(3, $result['recipients'][0]['PidTagRecipientFlags']); $this->assertSame('Manager, Jane', $result['recipients'][1]['PidTagDisplayName']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientType']); $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); $this->assertSame(0, $result['recipients'][1]['PidTagRecipientTrackStatus']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientFlags']); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); + $this->assertTrue($result['PidLidAppointmentTimeZoneDefinitionStartDisplay'] == $result['PidLidAppointmentTimeZoneDefinitionStartDisplay']); + + // PidLidTimeZoneDefinition + $tzd = new kolab_api_filter_mapistore_structure_timezonedefinition; + $tzd->input($result['PidLidAppointmentTimeZoneDefinitionStartDisplay'], true); + + $this->assertSame('Europe/Berlin', $tzd->KeyName); + $this->assertCount(1, $tzd->TZRules); + $this->assertSame(2015, $tzd->TZRules[0]->Year); + $this->assertSame(-60, $tzd->TZRules[0]->Bias); $data = kolab_api_tests::get_data('101-101-101-101', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '101-101-101-101'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(0, $result['PidTagSensitivity']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentStartWhole']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentEndWhole']); $this->assertSame(1, $result['PidLidAppointmentSubType']); // EXDATE $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(1, $arp->RecurrencePattern->Period); $this->assertSame(0x200B, $arp->RecurrencePattern->RecurFrequency); $this->assertSame(1, $arp->RecurrencePattern->PatternType); $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); // RDATE $data = kolab_api_tests::get_data('102-102-102-102', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); // recurrence $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); $this->assertSame(2, $arp->RecurrencePattern->ModifiedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->ModifiedInstanceDates); $this->assertSame(2, $arp->ExceptionCount); $this->assertCount(2, $arp->ExceptionInfo); $this->assertCount(2, $arp->ExtendedException); + + // PidLidTimeZoneStruct + $tz = new kolab_api_filter_mapistore_structure_timezonestruct; + $tz->input($result['PidLidTimeZoneStruct'], true); + + $this->assertSame(-60, $tz->Bias); + $this->assertSame(0, $tz->StandardYear); + $this->assertSame(10, $tz->StandardDate->Month); + $this->assertSame('(GMT+01:00) Europe/Berlin', $result['PidLidTimeZoneDescription']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_event; + $tzs = new kolab_api_filter_mapistore_structure_timezonestruct(array( + 'Bias' => -60, + 'StandardBias' => 0, + 'DaylightBias' => -60, + 'StandardDate' => new kolab_api_filter_mapistore_structure_systemtime(array( + 'Month' => 10, + 'DayOfWeek' => 0, + 'Day' => 5, + 'Hour' => 3, + )), + 'DaylightDate' => new kolab_api_filter_mapistore_structure_systemtime(array( + 'Month' => 3, + 'Day' => 5, + 'DayOfWeek' => 0, + 'Hour' => 2, + )), + )); + $data = array( 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 10, 'PidTagSensitivity' => 3, 'PidNameKeywords' => array('work'), 'PidTagSubject' => 'subject', 'PidTagBody' => 'body', 'PidTagImportance' => 2, 'PidLidLocation' => 'location', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T16:00:00Z'), 'PidLidReminderDelta' => 15, 'PidLidReminderSet' => true, + 'PidLidTimeZoneStruct' => $tzs->output(true), 'recipients' => array( array( 'PidTagDisplayName' => 'German, Mark', 'PidTagEmailAddress' => 'mark.german@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientFlags' => 3, ), array( 'PidTagDisplayName' => 'Manager, Jane', 'PidTagEmailAddress' => 'manager@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientTrackStatus' => 2, ), ), ); $result = $api->input($data); $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame(10, $result['sequence']); $this->assertSame('confidential', $result['class']); $this->assertSame(array('work'), $result['categories']); $this->assertSame('location', $result['location']); $this->assertSame(1, $result['priority']); $this->assertSame('2015-05-14T13:03:33Z', $result['created']); $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); - $this->assertSame('2015-05-14T13:03:33Z', $result['dtstart']); - $this->assertSame('2015-05-14T16:00:00Z', $result['dtend']); + $this->assertSame('2015-05-14T15:03:33', $result['dtstart']['date-time']); + $this->assertSame('2015-05-14T18:00:00', $result['dtend']['date-time']); + $this->assertRegexp('/kolab.org/', $result['dtstart']['parameters']['tzid']); + $this->assertRegexp('/kolab.org/', $result['dtend']['parameters']['tzid']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Reminder', $result['valarm'][0]['properties']['description']); $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('TENTATIVE', $result['attendee'][0]['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); // $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:manager%40example.org', $result['attendee'][0]['cal-address']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:mark.german%40example.org', $result['organizer']['cal-address']); self::$original = $result; + $tzdef = base64_encode(pack("H*", '0201300002001500' + . '500061006300690066006900630020005300740061006E0064006100720064002000540069006D006500' + . '0100' + . '02013E000000D6070000000000000000000000000000E001000000000000C4FFFFFF00000A0000000500020000000000000000000400000001000200000000000000' + )); $data = array( - // all-day event - 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T00:00:00Z'), - 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T00:00:00Z'), - 'PidLidAppointmentSubType' => 1, + 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T05:00:00Z'), + 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T05:00:00Z'), + 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => $tzdef, 'PidLidReminderSet' => false, // @TODO: recurrence, exceptions, alarms ); $result = $api->input($data); - $this->assertSame('2015-05-14', $result['dtstart']); - $this->assertSame('2015-05-14', $result['dtend']); + $this->assertSame('2015-05-13T22:00:00', $result['dtstart']['date-time']); + $this->assertSame('2015-05-14T05:00:00Z', $result['dtend']['date-time']); $this->assertSame(array(), $result['valarm']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_event; $data = array( // 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 20, 'PidTagSensitivity' => 2, 'PidNameKeywords' => array('work1'), 'PidTagSubject' => 'subject1', 'PidTagBody' => 'body1', 'PidTagImportance' => 1, 'PidLidLocation' => 'location1', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T16:00:00Z'), 'PidLidReminderDelta' => 25, 'PidLidReminderSet' => true, ); $result = $api->input($data, self::$original); $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame(20, $result['sequence']); $this->assertSame('private', $result['class']); $this->assertSame(array('work1'), $result['categories']); $this->assertSame('location1', $result['location']); $this->assertSame(5, $result['priority']); // $this->assertSame('2015-05-14T13:03:33Z', $result['created']); // $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); - $this->assertSame('2015-05-15T13:03:33Z', $result['dtstart']); - $this->assertSame('2015-05-15T16:00:00Z', $result['dtend']); + $this->assertSame('2015-05-15T13:03:33Z', $result['dtstart']['date-time']); + $this->assertSame('2015-05-15T16:00:00Z', $result['dtend']['date-time']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Reminder', $result['valarm'][0]['properties']['description']); $this->assertSame('-PT25M', $result['valarm'][0]['properties']['trigger']['duration']); // @TODO: recurrence, exceptions, attendees } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_event; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Structure/Systemtime.php b/tests/Unit/Filter/Mapistore/Structure/Systemtime.php index 42aea38..5b2e54a 100644 --- a/tests/Unit/Filter/Mapistore/Structure/Systemtime.php +++ b/tests/Unit/Filter/Mapistore/Structure/Systemtime.php @@ -1,31 +1,53 @@ markTestIncomplete('TODO'); + $structure = new kolab_api_filter_mapistore_structure_systemtime; + $structure->Year = 0; + $structure->Month = 10; + $structure->DayOfWeek = 0; + $structure->Day = 5; + $structure->Hour = 2; + + $result = $structure->output(); + + $this->assertSame(strtoupper(bin2hex($result)), self::$sample); } /** * Test input method */ function test_input() { - // @TODO - $this->markTestIncomplete('TODO'); + $structure = new kolab_api_filter_mapistore_structure_systemtime; + + // convert input data into binary format + $in = pack("H*" , self::$sample); + $len = strlen($in); + + $result = $structure->input($in); + + $this->assertSame($len, $result); + $this->assertSame(0, $structure->Year); + $this->assertSame(10, $structure->Month); + $this->assertSame(0, $structure->DayOfWeek); + $this->assertSame(5, $structure->Day); + $this->assertSame(2, $structure->Hour); + $this->assertSame(0, $structure->Minute); + $this->assertSame(0, $structure->Second); + $this->assertSame(0, $structure->Milliseconds); } } diff --git a/tests/Unit/Filter/Mapistore/Structure/Timezonedefinition.php b/tests/Unit/Filter/Mapistore/Structure/Timezonedefinition.php new file mode 100644 index 0000000..d834283 --- /dev/null +++ b/tests/Unit/Filter/Mapistore/Structure/Timezonedefinition.php @@ -0,0 +1,57 @@ + '02', + 'MinorVersion' => '01', + 'cbHeader' => '3000', + 'Reserved' => '0200', + 'cchKeyName' => '1500', + 'KeyName' => '500061006300690066006900630020005300740061006E0064006100720064002000540069006D006500', + 'cRules' => '0200', + 'TZRule1' => '02013E000000D6070000000000000000000000000000E001000000000000C4FFFFFF00000A0000000500020000000000000000000400000001000200000000000000', + 'TZRule2' => '02013E000200D7070000000000000000000000000000E001000000000000C4FFFFFF00000B0000000100020000000000000000000300000002000200000000000000', + ); + + + /** + * Test output method + */ + function test_output() + { + // @TODO + $this->markTestIncomplete('TODO'); + } + + /** + * Test input method + */ + function test_input() + { + $structure = new kolab_api_filter_mapistore_structure_timezonedefinition; + + // convert input data into binary format + $in = pack("H*" , implode('', self::$sample)); + $len = strlen($in); + + $result = $structure->input($in); + + $this->assertSame($len, $result); + $this->assertSame(48, $structure->cbHeader); + $this->assertSame(21, $structure->cchKeyName); + $this->assertSame('Pacific Standard Time', $structure->KeyName); + $this->assertSame(2, $structure->cRules); + $this->assertCount(2, $structure->TZRules); + $this->assertInstanceOf('kolab_api_filter_mapistore_structure_tzrule', $structure->TZRules[0]); + $this->assertInstanceOf('kolab_api_filter_mapistore_structure_tzrule', $structure->TZRules[1]); + + // Note: TZrule contents is tested in another place + } +} diff --git a/tests/Unit/Filter/Mapistore/Structure/Timezonestruct.php b/tests/Unit/Filter/Mapistore/Structure/Timezonestruct.php index d1946f1..3361532 100644 --- a/tests/Unit/Filter/Mapistore/Structure/Timezonestruct.php +++ b/tests/Unit/Filter/Mapistore/Structure/Timezonestruct.php @@ -1,91 +1,125 @@ 'E0010000', - 'IStandardBias' => '00000000', - 'IDaylightBias' => 'C4FFFFFF', - 'WStandardYear' => '0000', - 'StStandardDate' => '00000B00000001000200000000000000', - 'WDaylightYear' => '0000', - 'StDaylightDate' => '00000300000002000200000000000000', + 'Bias' => 'E0010000', + 'StandardBias' => '00000000', + 'DaylightBias' => 'C4FFFFFF', + 'StandardYear' => '0000', + 'StandardDate' => '00000B00000001000200000000000000', + 'DaylightYear' => '0000', + 'DaylightDate' => '00000300000002000200000000000000', ); /** * Test output method */ function test_output() { $structure = new kolab_api_filter_mapistore_structure_timezonestruct; - $structure->IBias = 480; - $structure->IStandardBias = 0; - $structure->IDaylightBias = -60; - $structure->WStandardYear = 0; - $structure->WDaylightYear = 0; - $structure->StStandardDate = new kolab_api_filter_mapistore_structure_systemtime(array( - 'WYear' => 0, - 'WMonth' => 11, - 'WDayOfWeek' => 0, - 'WDay' => 1, - 'WHour' => 2, + $structure->Bias = 480; + $structure->StandardBias = 0; + $structure->DaylightBias = -60; + $structure->StandardYear = 0; + $structure->DaylightYear = 0; + $structure->StandardDate = new kolab_api_filter_mapistore_structure_systemtime(array( + 'Year' => 0, + 'Month' => 11, + 'DayOfWeek' => 0, + 'Day' => 1, + 'Hour' => 2, )); - $structure->StDaylightDate = new kolab_api_filter_mapistore_structure_systemtime(array( - 'WYear' => 0, - 'WMonth' => 3, - 'WDayOfWeek' => 0, - 'WDay' => 2, - 'WHour' => 2, + $structure->DaylightDate = new kolab_api_filter_mapistore_structure_systemtime(array( + 'Year' => 0, + 'Month' => 3, + 'DayOfWeek' => 0, + 'Day' => 2, + 'Hour' => 2, )); $result = $structure->output(); $this->assertSame(strtoupper(bin2hex($result)), implode('', self::$sample)); } /** * Test input method */ function test_input() { $structure = new kolab_api_filter_mapistore_structure_timezonestruct; // convert input data into binary format $in = pack("H*" , implode('', self::$sample)); $len = strlen($in); $result = $structure->input($in); $this->assertSame($len, $result); - $this->assertSame(480, $structure->IBias); - $this->assertSame(0, $structure->IStandardBias); - $this->assertSame(-60, $structure->IDaylightBias); - $this->assertSame(0, $structure->WStandardYear); - $this->assertSame(0, $structure->WDaylightYear); - $this->assertInstanceOf('kolab_api_filter_mapistore_structure_systemtime', $structure->StStandardDate); - $this->assertInstanceOf('kolab_api_filter_mapistore_structure_systemtime', $structure->StDaylightDate); - $this->assertSame(0, $structure->StStandardDate->WYear); - $this->assertSame(11, $structure->StStandardDate->WMonth); - $this->assertSame(0, $structure->StStandardDate->WDayOfWeek); - $this->assertSame(1, $structure->StStandardDate->WDay); - $this->assertSame(2, $structure->StStandardDate->WHour); - $this->assertSame(0, $structure->StStandardDate->WMinute); - $this->assertSame(0, $structure->StStandardDate->WSecond); - $this->assertSame(0, $structure->StStandardDate->WMilliseconds); - $this->assertSame(0, $structure->StDaylightDate->WYear); - $this->assertSame(3, $structure->StDaylightDate->WMonth); - $this->assertSame(0, $structure->StDaylightDate->WDayOfWeek); - $this->assertSame(2, $structure->StDaylightDate->WDay); - $this->assertSame(2, $structure->StDaylightDate->WHour); - $this->assertSame(0, $structure->StDaylightDate->WMinute); - $this->assertSame(0, $structure->StDaylightDate->WSecond); - $this->assertSame(0, $structure->StDaylightDate->WMilliseconds); + $this->assertSame(480, $structure->Bias); + $this->assertSame(0, $structure->StandardBias); + $this->assertSame(-60, $structure->DaylightBias); + $this->assertSame(0, $structure->StandardYear); + $this->assertSame(0, $structure->DaylightYear); + $this->assertInstanceOf('kolab_api_filter_mapistore_structure_systemtime', $structure->StandardDate); + $this->assertInstanceOf('kolab_api_filter_mapistore_structure_systemtime', $structure->DaylightDate); + $this->assertSame(0, $structure->StandardDate->Year); + $this->assertSame(11, $structure->StandardDate->Month); + $this->assertSame(0, $structure->StandardDate->DayOfWeek); + $this->assertSame(1, $structure->StandardDate->Day); + $this->assertSame(2, $structure->StandardDate->Hour); + $this->assertSame(0, $structure->StandardDate->Minute); + $this->assertSame(0, $structure->StandardDate->Second); + $this->assertSame(0, $structure->StandardDate->Milliseconds); + $this->assertSame(0, $structure->DaylightDate->Year); + $this->assertSame(3, $structure->DaylightDate->Month); + $this->assertSame(0, $structure->DaylightDate->DayOfWeek); + $this->assertSame(2, $structure->DaylightDate->Day); + $this->assertSame(2, $structure->DaylightDate->Hour); + $this->assertSame(0, $structure->DaylightDate->Minute); + $this->assertSame(0, $structure->DaylightDate->Second); + $this->assertSame(0, $structure->DaylightDate->Milliseconds); + } + + /** + * Test to_tzname() method + */ + function test_to_tzname() + { + $structure = new kolab_api_filter_mapistore_structure_timezonestruct; + + // convert input data into binary format + $in = pack("H*" , implode('', self::$sample)); + $len = strlen($in); + + $structure->input($in); + + $this->assertSame('America/Vancouver', $structure->to_tzname('America/Vancouver')); + } + + /** + * Test from_datetime() method + */ + function test_from_datetime() + { + $dt = new DateTime('2015-11-01 00:00:00', new DateTimeZone('Europe/Warsaw')); + $tzs = kolab_api_filter_mapistore_structure_timezonestruct::from_datetime($dt); + + $this->assertSame(-60, $tzs->Bias); + $this->assertSame(2015, $tzs->StandardYear); + $this->assertSame(10, $tzs->StandardDate->Month); + $this->assertSame(5, $tzs->StandardDate->Day); + $this->assertSame(3, $tzs->StandardDate->Hour); + $this->assertSame(3, $tzs->DaylightDate->Month); + $this->assertSame(5, $tzs->DaylightDate->Day); + $this->assertSame(2, $tzs->DaylightDate->Hour); } } diff --git a/tests/Unit/Filter/Mapistore/Structure/Tzrule.php b/tests/Unit/Filter/Mapistore/Structure/Tzrule.php index b8053b2..3100a64 100644 --- a/tests/Unit/Filter/Mapistore/Structure/Tzrule.php +++ b/tests/Unit/Filter/Mapistore/Structure/Tzrule.php @@ -1,31 +1,107 @@ '02', + 'MinorVersion' => '01', + 'Reserved' => '3E00', + 'Flags' => '0000', + 'Year' => 'D607', + 'X' => '0000000000000000000000000000', + 'Bias' => 'E0010000', + 'StandardBias' => '00000000', + 'DaylightBias' => 'C4FFFFFF', + 'StandardDate' => '00000A00000005000200000000000000', + 'DaylightDate' => '00000400000001000200000000000000', ); /** * Test output method */ function test_output() { - // @TODO - $this->markTestIncomplete('TODO'); + $structure = new kolab_api_filter_mapistore_structure_tzrule; + $structure->Bias = 480; + $structure->Year = 2006; + $structure->StandardBias = 0; + $structure->DaylightBias = -60; + $structure->StandardDate = new kolab_api_filter_mapistore_structure_systemtime(array( + 'Year' => 0, + 'Month' => 10, + 'DayOfWeek' => 0, + 'Day' => 5, + 'Hour' => 2, + )); + $structure->DaylightDate = new kolab_api_filter_mapistore_structure_systemtime(array( + 'Year' => 0, + 'Month' => 4, + 'DayOfWeek' => 0, + 'Day' => 1, + 'Hour' => 2, + )); + + $result = $structure->output(); + + $this->assertSame(strtoupper(bin2hex($result)), implode('', self::$sample)); } /** * Test input method */ function test_input() + { + $structure = new kolab_api_filter_mapistore_structure_tzrule; + + // convert input data into binary format + $in = pack("H*" , implode('', self::$sample)); + $len = strlen($in); + + $result = $structure->input($in); + + $this->assertSame($len, $result); + $this->assertSame(0, $structure->Flags); + $this->assertSame(2006, $structure->Year); + $this->assertSame(480, $structure->Bias); + $this->assertSame(0, $structure->StandardBias); + $this->assertSame(-60, $structure->DaylightBias); + $this->assertInstanceOf('kolab_api_filter_mapistore_structure_systemtime', $structure->StandardDate); + $this->assertInstanceOf('kolab_api_filter_mapistore_structure_systemtime', $structure->DaylightDate); + + // Note: systemtime structures are tested in a different place + } + + /** + * Test to_tzname() method + */ + function test_to_tzname() { // @TODO $this->markTestIncomplete('TODO'); } + + /** + * Test from_datetime() method + */ + function test_from_datetime() + { + $dt = new DateTime('2015-11-01 00:00:00', new DateTimeZone('Europe/Warsaw')); + $tzs = kolab_api_filter_mapistore_structure_tzrule::from_datetime($dt); + + $this->assertSame(-60, $tzs->Bias); + $this->assertSame(2015, $tzs->Year); + $this->assertSame(10, $tzs->StandardDate->Month); + $this->assertSame(5, $tzs->StandardDate->Day); + $this->assertSame(3, $tzs->StandardDate->Hour); + $this->assertSame(3, $tzs->DaylightDate->Month); + $this->assertSame(5, $tzs->DaylightDate->Day); + $this->assertSame(2, $tzs->DaylightDate->Hour); + } } diff --git a/tests/Unit/Filter/Mapistore/Timezone.php b/tests/Unit/Filter/Mapistore/Timezone.php new file mode 100644 index 0000000..6a9e333 --- /dev/null +++ b/tests/Unit/Filter/Mapistore/Timezone.php @@ -0,0 +1,81 @@ +assertSame(-60, $result->Bias); + $this->assertSame(0, $result->StandardBias); + $this->assertSame(2015, $result->StandardYear); + $this->assertSame(10, $result->StandardMonth); + $this->assertSame(0, $result->StandardDayOfWeek); + $this->assertSame(5, $result->StandardDay); + $this->assertSame(3, $result->StandardHour); + $this->assertSame(0, $result->StandardMinute); + $this->assertSame(-60, $result->DaylightBias); + $this->assertSame(2015, $result->DaylightYear); + $this->assertSame(3, $result->DaylightMonth); + $this->assertSame(0, $result->DaylightDayOfWeek); + $this->assertSame(5, $result->DaylightDay); + $this->assertSame(2, $result->DaylightHour); + $this->assertSame(0, $result->DaylightMinute); + } + + /** + * Test get_int_from_date() method + */ + function test_get_int_from_date() + { + $dt = new DateTime('2015-10-10 01:01:00', new DateTimeZone('Europe/Berlin')); + $result = kolab_api_filter_mapistore_timezone::get_int_from_date($dt); + + $this->assertSame(3, $result); + } + + /** + * Test get_timezone() method + */ + function test_get_timezone() + { + $tz = new kolab_api_filter_mapistore_timezone; + $tz->Bias = -60; + $tz->StandardYear = 2015; + $tz->StandardMonth = 10; + $tz->StandardDay = 5; + $tz->StandardHour = 3; + $tz->DaylightBias = -60; + $tz->DaylightYear = 2015; + $tz->DaylightMonth = 3; + $tz->DaylightDay = 5; + $tz->DaylightHour = 2; + + $result = $tz->get_timezone('Europe/Berlin'); + $this->assertSame('Europe/Berlin', $result); + + // invalid, expect 'UTC' + $tz->Bias = -6000; + $result = $tz->get_timezone('Europe/Berlin'); + $this->assertSame('UTC', $result); + } + + /** + * Test get_timezone_from_int() method + */ + function test_get_timezone_from_int() + { + $result = kolab_api_filter_mapistore_timezone::get_timezone_from_int(3, 'Europe/Berlin'); + + $this->assertSame('Europe/Berlin', $result); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 2387579..6a246ae 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,65 +1,67 @@ Unit/Output/Json.php Unit/Output/Json/Attachment.php Unit/Output/Json/Contact.php Unit/Output/Json/Event.php Unit/Output/Json/Folder.php Unit/Output/Json/Info.php Unit/Output/Json/Mail.php Unit/Output/Json/Note.php Unit/Output/Json/Task.php Unit/Input/Json.php Unit/Input/Json/Attachment.php Unit/Input/Json/Contact.php Unit/Input/Json/Event.php Unit/Input/Json/Folder.php Unit/Input/Json/Mail.php Unit/Input/Json/Note.php Unit/Input/Json/Folder.php Unit/Input/Json/Task.php Unit/Filter/Mapistore.php Unit/Filter/Mapistore/Structure/Appointmentrecurrencepattern.php Unit/Filter/Mapistore/Structure/Changehighlight.php Unit/Filter/Mapistore/Structure/Exceptioninfo.php Unit/Filter/Mapistore/Structure/Extendedexception.php Unit/Filter/Mapistore/Structure/Recipientrow.php Unit/Filter/Mapistore/Structure/Recurrencepattern.php Unit/Filter/Mapistore/Structure/Systemtime.php + Unit/Filter/Mapistore/Structure/Timezonedefinition.php Unit/Filter/Mapistore/Structure/Timezonestruct.php Unit/Filter/Mapistore/Structure/Tzrule.php Unit/Filter/Mapistore/Common.php Unit/Filter/Mapistore/Attachment.php Unit/Filter/Mapistore/Contact.php Unit/Filter/Mapistore/Event.php Unit/Filter/Mapistore/Folder.php Unit/Filter/Mapistore/Info.php Unit/Filter/Mapistore/Mail.php Unit/Filter/Mapistore/Note.php Unit/Filter/Mapistore/Task.php + Unit/Filter/Mapistore/Timezone.php API/Folders.php API/Attachments.php API/Contacts.php API/Events.php API/Info.php API/Mails.php API/Notes.php API/Tasks.php Mapistore/Folders.php Mapistore/Attachments.php Mapistore/Contacts.php Mapistore/Events.php Mapistore/Info.php Mapistore/Mails.php Mapistore/Notes.php Mapistore/Tasks.php