diff --git a/lib/filter/mapistore/common.php b/lib/filter/mapistore/common.php index 23f6cd7..9777e9a 100644 --- a/lib/filter/mapistore/common.php +++ b/lib/filter/mapistore/common.php @@ -1,755 +1,808 @@ | +--------------------------------------------------------------------------+ | 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] = kolab_api_filter_mapistore::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 = kolab_api_filter_mapistore::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 $rule Recurrence rule in Kolab format + * @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($rule, $object = array(), $type = 'event') + public static function recurrence_from_kolab($data, $object = array(), $type = 'event') { - $result = array( + if (empty($data['rrule']) || empty($data['rrule']['recur'])) { + return null; + } + + $rule = $data['rrule']['recur']; + $startdate = kolab_api_input_json::to_datetime($data['dtstart']); + $result = array( 'Period' => $rule['interval'] ? $rule['interval'] : 1, 'FirstDOW' => self::day2bitmask($rule['wkst'] ?: 'MO'), 'OccurrenceCount' => 0x0000000A, 'EndDate' => 0x5AE980DF, 'CalendarType' => kolab_api_filter_mapistore_structure_recurrencepattern::CALENDARTYPE_DEFAULT, // DeletedInstanceDates // ModifiedInstanceDates ); // Get event/task start date for FirstDateTime calculations - if ($object['PidLidAppointmentStartWhole']) { - $startdate = kolab_api_filter_mapistore::date_mapi2php($object['PidLidAppointmentStartWhole']); - $result['StartDate'] = intval($object['PidLidAppointmentStartWhole'] / 10000000 / 60); - } - else if ($object['PidLidCommonStart']) { - $startdate = kolab_api_filter_mapistore::date_mapi2php($object['PidLidCommonStart']); - $result['StartDate'] = intval($object['PidLidCommonStart'] / 10000000 / 60); + if ($startdate) { + $mapi_dt = kolab_api_filter_mapistore::date_php2mapi($startdate, true); + $result['StartDate'] = intval($mapi_dt / 10000000 / 60); } else { rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Found recurring $type without start date, skipping recurrence", ), true, false); return; } // $startdate->setTime(0, 0, 0); // @TODO: // 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. // @TODO: get first occurrence of the event using libcalendaring_recurrence class ? 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; } if (!empty($rule['until'])) { $result['EndDate'] = intval(kolab_api_filter_mapistore::date_php2mapi($rule['until']) / 10000000 / 60); // @TODO: calculate OccurrenceCount? $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER; } else if (!empty($rule['count'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurrenceCount'] = $rule['count']; // @TODO: set EndDate } else { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER; } - $result['FirstDateTime'] = self::date_minutes_diff($startdate); + $result['FirstDateTime'] = self::date_php2minutes($startdate); + + // Deleted instances + if (!empty($data['exdate']) && $startdate) { + 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, $startdate->getTimezone()); + $dt->setTime(0, 0, 0); + $exceptions[$idx] = self::date_php2minutes($dt); + } + catch (Exception $e) { + } + } + + // [MS-OXCICAL] 2.1.3.1.1.20.13: Sort and make exceptions valid + sort($exceptions); + $exceptions = array_values(array_unique(array_filter($exceptions))); + + $result['DeletedInstanceDates'] = $exceptions; + } $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 // ExtendedExceptions ); $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 string $type Object type (task, event) - * - * @return array Recurrence rule in Kolab format + * @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, $type = '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']); } - return $result; + // Deleted exceptions + $object['exdate'] = array(); + foreach ((array) $rp->DeletedInstanceDates as $date) { + if ($dt = self::date_minutes2php($date)) { + $object['exdate']['date'][] = $dt->format('Y-m-d'); + } + } + + $object['rrule']['recur'] = $result; } /** * 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) { - $days_arr[] = preg_replace('/^[A-Z-]+/', '', $day); + if (($days & $bit) === $bit) { + $days_arr[] = preg_replace('/^BYDAY-/', '', $day); } } $result = implode(',', $days_arr); return $result; } /** * Returns number of minutes between midnight 1601-01-01 * and specified UTC DateTime */ - protected static function date_minutes_diff($date) + protected 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 round(($date->getTimestamp() - $start->getTimestamp()) * 60); + 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 + */ + protected 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); } /** * 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; } } diff --git a/lib/filter/mapistore/event.php b/lib/filter/mapistore/event.php index fce154f..5ed2128 100644 --- a/lib/filter/mapistore/event.php +++ b/lib/filter/mapistore/event.php @@ -1,380 +1,378 @@ | +--------------------------------------------------------------------------+ | 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 = kolab_api_filter_mapistore::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 = kolab_api_filter_mapistore::date_php2mapi($dt, true); // PidLidAppointmentTimeZoneDefinitionStartDisplay // PidLidAppointmentTimeZoneDefinitionEndDisplay // this is all-day event if ($dt->_dateonly) { $result['PidLidAppointmentSubType'] = 0x00000001; } 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 rule - if (!empty($data['rrule']) && !empty($data['rrule']['recur'])) { - if ($rule = $this->recurrence_from_kolab($data['rrule']['recur'], $result)) { - $result['PidLidAppointmentRecur'] = $rule; - } + // Recurrence + if ($rule = $this->recurrence_from_kolab($data, $result)) { + $result['PidLidAppointmentRecur'] = $rule; } $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(); 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 = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $format = $data['PidLidAppointmentSubType'] ? 'Y-m-d' : 'Y-m-d\TH:i:s\Z'; $value = $value->format($format); } 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)) { - $result['rrule']['recur'] = $this->recurrence_to_kolab($data['PidLidAppointmentRecur']); + $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: exceptions, resources + // @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; } } } diff --git a/lib/filter/mapistore/task.php b/lib/filter/mapistore/task.php index 6f5dcea..f3fb709 100644 --- a/lib/filter/mapistore/task.php +++ b/lib/filter/mapistore/task.php @@ -1,292 +1,290 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_task extends kolab_api_filter_mapistore_common { protected $map = array( // task specific props [MS-OXOTASK] 'PidTagProcessed' => '', // PtypBoolean 'PidLidTaskMode' => '', // ignored 'PidLidTaskStatus' => '', // PtypInteger32 'PidLidPercentComplete' => 'percent-complete', // PtypFloating64 'PidLidTaskStartDate' => 'dtstart', // PtypTime 'PidLidTaskDueDate' => 'due', // PtypTime 'PidLidTaskResetReminder' => '', // @TODO // PtypBoolean 'PidLidTaskAccepted' => '', // @TODO // PtypBoolean 'PidLidTaskDeadOccurrence' => '', // @TODO // PtypBoolean 'PidLidTaskDateCompleted' => 'x-custom.MAPI:PidLidTaskDateCompleted', // PtypTime 'PidLidTaskLastUpdate' => '', // PtypTime 'PidLidTaskActualEffort' => 'x-custom.MAPI:PidLidTaskActualEffort', // PtypInteger32 'PidLidTaskEstimatedEffort' => 'x-custom.MAPI:PidLidTaskEstimatedEffort', // PtypInteger32 'PidLidTaskVersion' => '', // PtypInteger32 'PidLidTaskState' => '', // PtypInteger32 'PidLidTaskRecurrence' => '', // PtypBinary 'PidLidTaskAssigners' => '', // PtypBinary 'PidLidTaskStatusOnComplete' => '', // PtypBoolean 'PidLidTaskHistory' => '', // @TODO: ? // PtypInteger32 'PidLidTaskUpdates' => '', // PtypBoolean 'PidLidTaskComplete' => '', // PtypBoolean 'PidLidTaskFCreator' => '', // PtypBoolean 'PidLidTaskOwner' => '', // @TODO // PtypString 'PidLidTaskMultipleRecipients' => '', // PtypBoolean 'PidLidTaskAssigner' => '', // PtypString 'PidLidTaskLastUser' => '', // PtypString 'PidLidTaskOrdinal' => '', // PtypInteger32 'PidLidTaskLastDelegate' => '', // PtypString 'PidLidTaskFRecurring' => '', // PtypBoolean 'PidLidTaskOwnership' => '', // @TODO // PtypInteger32 'PidLidTaskAcceptanceState' => '', // PtypInteger32 'PidLidTaskFFixOffline' => '', // PtypBoolean 'PidLidTaskGlobalId' => '', // @TODO // PtypBinary 'PidLidTaskCustomFlags' => '', // ignored 'PidLidTaskRole' => '', // ignored 'PidLidTaskNoCompute' => '', // ignored 'PidLidTeamTask' => '', // ignored // common props [MS-OXCMSG] 'PidTagSubject' => 'summary', 'PidTagBody' => 'description', 'PidTagHtml' => '', // @TODO: (?) 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagMessageClass' => '', 'PidLidCommonStart' => 'dtstart', 'PidLidCommonEnd' => 'due', 'PidTagIconIndex' => '', // @TODO 'PidTagCreationTime' => 'created', // PtypTime, UTC 'PidTagLastModificationTime' => 'dtstamp', // PtypTime, UTC ); /** * Values for PidLidTaskStatus property */ protected $status_map = array( 'none' => 0x00000000, // PidLidPercentComplete = 0 'in-progress' => 0x00000001, // PidLidPercentComplete > 0 and PidLidPercentComplete < 1 'complete' => 0x00000002, // PidLidPercentComplete = 1 'waiting' => 0x00000003, 'deferred' => 0x00000004, ); /** * Values for PidLidTaskHistory property */ protected $history_map = array( 'none' => 0x00000000, 'accepted' => 0x00000001, 'rejected' => 0x00000002, 'changed' => 0x00000003, 'due-changed' => 0x00000004, 'assigned' => 0x00000005, ); /** * 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.Task', // mapistore REST API specific properties 'collection' => 'tasks', ); 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 'PidLidPercentComplete': $value /= 100; break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': $value = kolab_api_filter_mapistore::date_php2mapi($value, false, array('hour' => 0)); break; case 'PidLidCommonStart': case 'PidLidCommonEnd': $value = kolab_api_filter_mapistore::date_php2mapi($value, true); break; // case 'PidLidTaskLastUpdate': case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = kolab_api_filter_mapistore::date_php2mapi($value, true); break; case 'PidLidTaskActualEffort': case 'PidLidTaskEstimatedEffort': $value = (int) $value; break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // set status $percent = $result['PidLidPercentComplete']; if ($precent == 1) { $result['PidLidTaskStatus'] = $this->status_map['complete']; // PidLidTaskDateCompleted (?) } else if ($precent > 0) { $result['PidLidTaskStatus'] = $this->status_map['in-progress']; } else { $result['PidLidTaskStatus'] = $this->status_map['none']; } // 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); } - // Recurrence rule - if (!empty($data['rrule']) && !empty($data['rrule']['recur'])) { - if ($rule = $this->recurrence_from_kolab($data['rrule']['recur'], $result)) { - $result['PidLidTaskRecurrence'] = $rule; - $result['PidLidTaskFRecurring'] = true; - } + // Recurrence + if ($rule = $this->recurrence_from_kolab($data, $result)) { + $result['PidLidTaskRecurrence'] = $rule; + $result['PidLidTaskFRecurring'] = true; } $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(); 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 'PidLidPercentComplete': $value = intval($value * 100); break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': if (intval($value) !== 0x5AE980E0) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidLidCommonStart': case 'PidLidCommonEnd': // $value = kolab_api_filter_mapistore::date_mapi2php($value, true); break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; } $result[$kolab_idx] = $value; } if ($data['PidLidTaskComplete']) { $result['status'] = 'COMPLETED'; } - // Recurrences + // Recurrence if (array_key_exists('PidLidTaskRecurrence', $data)) { - $result['rrule']['recur'] = $this->recurrence_to_kolab($data['PidLidTaskRecurrence'], 'task'); + $this->recurrence_to_kolab($data['PidLidTaskRecurrence'], $result, 'task'); } if (array_key_exists('recipients', $data)) { $result['attendee'] = array(); $result['organizer'] = array(); foreach ((array) $data['recipients'] as $recipient) { $this->recipient_to_attendee($recipient, $result); } } $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidLidTaskRecurrence'] = 'rrule'; return $map; } } diff --git a/lib/input/json/event.php b/lib/input/json/event.php index 018e4b4..70a0e61 100644 --- a/lib/input/json/event.php +++ b/lib/input/json/event.php @@ -1,128 +1,139 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_event { // map xml/json attributes into internal (kolab_format) protected $field_map = array( 'description' => 'description', 'title' => 'summary', 'sensitivity' => 'class', 'sequence' => 'sequence', 'categories' => 'categories', 'created' => 'created', 'changed' => 'dtstamp', 'attendees' => 'attendee', 'organizer' => 'organizer', 'recurrence' => 'rrule', 'start' => 'dtstart', 'end' => 'dtend', 'valarms' => 'valarms', 'location' => 'location', 'priority' => 'priority', 'status' => 'status', 'url' => 'url', ); /** * Convert event input array into an array that can * be handled by kolab_storage_folder::save() * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } // require at least 'dtstart' property for new objects if (empty($original) && empty($data['dtstart'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'sensitivity': if ($value) { $value = strtolower($value); } break; case 'url': if (is_array($value)) { $value = $value[0]; } break; case 'created': case 'changed': case 'start': case 'end': $value = kolab_api_input_json::to_datetime($value); break; case 'attendees': $value = kolab_api_input_json::parse_attendees($value); break; case 'organizer': if (!empty($value)) { $value = kolab_api_input_json::parse_attendees(array($value)); $value = $value[0]; } break; } $result[$kolab] = $value; } // @TODO: recurrence // @TODO: exceptions // @TOOD: alarms + // 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']; + } + } + // x-custom fields kolab_api_input_json::add_x_custom($data, $result); // @TODO: should we require event summary/title? if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/output/json.php b/lib/output/json.php index 331e52a..f4f0d95 100644 --- a/lib/output/json.php +++ b/lib/output/json.php @@ -1,250 +1,271 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json extends kolab_api_output { /** * Send successful response * * @param mixed Response data * @param string Data type * @param array Context (folder_uid, object_uid, object) * @param array Optional attributes filter */ public function send($data, $type, $context = null, $attrs_filter = array()) { // Set output type $this->headers(array('Content-Type' => "application/json; charset=utf-8")); list($type, $mode) = explode('-', $type); if ($mode != 'list') { $data = array($data); } $class = "kolab_api_output_json_$type"; $model = new $class($this); $result = array(); $debug = $this->api->config->get('kolab_api_debug'); foreach ($data as $idx => $item) { if ($element = $model->element($item, $attrs_filter)) { $result[] = $element; } else { unset($data[$idx]); } } // apply output filter if ($this->api->filter) { $this->api->filter->output($result, $type, $context, $attrs_filter); } // generate JSON output $opts = $debug && defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; $result = json_encode($result, $opts); if ($mode != 'list') { $result = trim($result, '[]'); } if ($debug) { rcube::console($result); } $this->send_status(kolab_api_output::STATUS_OK, false); // send JSON output echo $result; exit; } /** * Convert object data into JSON API format * * @param array Object data * @param string Object type * * @return array Object data in JSON API format */ public function convert($data, $type) { $class = "kolab_api_output_json_$type"; $model = new $class($this); return $model->element($data); } /** * Convert (part of) kolab_format object into an array * * @param array Kolab object * @param string Object type * @param string Data element name * @param array Optional list of return properties * * @return array Object data */ public function object_to_array($object, $type, $element, $properties = array(), $array_elements = array()) { // load old object to preserve data we don't understand/process if (is_object($object['_formatobj'])) { $format = $object['_formatobj']; } // create new kolab_format instance if (!$format) { $format = kolab_format::factory($type, kolab_storage::$version); if (PEAR::isError($format)) { return; } $format->set($object); } $xml = $format->write(kolab_storage::$version); if (empty($xml) || !$format->is_valid() || !$format->uid) { return; } // The simplest way of "normalizing object properties // is to use its XML representation $doc = new DOMDocument(); // LIBXML_NOBLANKS is required for xml_to_array() below $doc->loadXML($xml, LIBXML_NOBLANKS); $node = $doc->getElementsByTagName($element)->item(0); $node = $this->xml_to_array($node); $node = array_filter($node); unset($node['prodid']); // faked 'categories' property (we need this for unit-tests // @TODO: find a better way if (!empty($object['categories'])) { $node['categories'] = $object['categories']; } if (!empty($properties)) { $node = array_intersect_key($node, array_combine($properties, $properties)); } // force some elements to be arrays if (!empty($array_elements)) { self::parse_array_result($node, $array_elements); } return $node; } /** * Convert XML element into an array * This is intended to use with Kolab XML format * * @param DOMElement XML element * * @return mixed Conversion result */ public function xml_to_array($node) { $children = $node->childNodes; if (!$children->length) { return; } if ($children->length == 1) { if ($node->firstChild->nodeType == XML_TEXT_NODE || !$node->firstChild->childNodes->length ) { return (string) $node->textContent; } if ($node->firstChild->nodeType == XML_ELEMENT_NODE && $node->firstChild->childNodes->length == 1 && $node->firstChild->firstChild->nodeType == XML_TEXT_NODE ) { switch ($node->firstChild->nodeName) { case 'integer': return (int) $node->textContent; case 'boolean': return strtoupper($node->textContent) == 'TRUE'; case 'date-time': case 'timestamp': case 'date': case 'text': case 'uri': case 'sex': return (string) $node->textContent; } } } $result = array(); foreach ($children as $child) { $value = $child->nodeType == XML_TEXT_NODE ? $child->nodeValue : $this->xml_to_array($child); if (!isset($result[$child->nodeName])) { $result[$child->nodeName] = $value; } else { if (!is_array($result[$child->nodeName]) || !isset($result[$child->nodeName][0])) { $result[$child->nodeName] = array($result[$child->nodeName]); } $result[$child->nodeName][] = $value; } } if (is_array($result['text']) && count($result) == 1) { $result = $result['text']; } return $result; } public static function parse_array_result(&$data, $array_elements = array()) { foreach ($array_elements as $key) { $items = explode('/', $key); if (count($items) > 1 && !empty($data[$items[0]])) { $key = array_shift($items); self::parse_array_result($data[$key], array(implode('/', $items))); } else if (!empty($data[$key]) && (!is_array($data[$key]) || !array_key_exists(0, $data[$key]))) { $data[$key] = array($data[$key]); } } } + + /** + * Makes sure exdate/rdate output is consistent/unified + */ + public static function parse_recur_dates(&$data) + { + foreach (array('exdate', 'rdate') as $key) { + if ($data[$key]) { + if (is_string($data[$key])) { + $idx = strlen($data[$key]) > 10 ? 'date-time' : 'date'; + $data[$key] = array($idx => array($data[$key])); + } + else if (array_key_exists('date', $data[$key]) && !is_array($data[$key]['date'])) { + $data[$key]['date'] = (array) $data[$key]['date']; + } + else if (array_key_exists('date-time', $data[$key]) && !is_array($data[$key]['date-time'])) { + $data[$key]['date-time'] = (array) $data[$key]['date-time']; + } + } + } + } } diff --git a/lib/output/json/event.php b/lib/output/json/event.php index 575b764..45770fc 100644 --- a/lib/output/json/event.php +++ b/lib/output/json/event.php @@ -1,81 +1,84 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_event { protected $output; protected $array_elements = array( 'attach', 'attendee', 'categories', 'x-custom', 'valarm', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (is_array($data) && count($data) == 1) { return $data; } $result = $this->output->object_to_array($data, 'event', 'vevent'); if (!empty($attrs_filter)) { $result['properties'] = array_intersect_key($result['properties'], array_combine($attrs_filter, $attrs_filter)); } // add 'components' to the result if (!empty($result['components'])) { $result['properties'] += (array) $result['components']; } $result = $result['properties']; kolab_api_output_json::parse_array_result($result, $this->array_elements); + // make sure exdate/rdate format is unified + kolab_api_output_json::parse_recur_dates($result); + return $result; } } diff --git a/lib/output/json/task.php b/lib/output/json/task.php index f99814b..ff95fcc 100644 --- a/lib/output/json/task.php +++ b/lib/output/json/task.php @@ -1,82 +1,85 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_task { protected $output; protected $array_elements = array( 'attach', 'attendee', 'related-to', 'x-custom', 'categories', 'valarm', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (is_array($data) && count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'task', 'vtodo'); if (!empty($attrs_filter)) { $result['properties'] = array_intersect_key($result['properties'], array_combine($attrs_filter, $attrs_filter)); } // add 'components' to the result if (!empty($result['components'])) { $result['properties'] += (array) $result['components']; } $result = $result['properties']; kolab_api_output_json::parse_array_result($result, $this->array_elements); + // make sure exdate/rdate format is unified + kolab_api_output_json::parse_recur_dates($result); + return $result; } } diff --git a/tests/Unit/Filter/Mapistore.php b/tests/Unit/Filter/Mapistore.php index 022cac1..730f93a 100644 --- a/tests/Unit/Filter/Mapistore.php +++ b/tests/Unit/Filter/Mapistore.php @@ -1,82 +1,82 @@ assertSame('folder.msg', $uid); $uid = kolab_api_filter_mapistore::uid_encode('folder', 'msg', 'attach'); $this->assertSame('folder.msg.attach', $uid); $uid = kolab_api_filter_mapistore::uid_encode('f-ol.der', 'm-s.g', 'att.a-ch'); $this->assertSame('f-ol_46der.m-s_46g.att_46a-ch', $uid); } /** * Test uid_decode method */ function test_uid_decode() { $uid = kolab_api_filter_mapistore::uid_decode('f-ol_46der.m-s_46g.att_46a-ch'); $this->assertSame(array('f-ol.der', 'm-s.g', 'att.a-ch'), $uid); } /** * Test date_php2mapi method */ function test_date_php2mapi() { $date = kolab_api_filter_mapistore::date_php2mapi('2014-01-01T00:00:00+00:00'); $this->assertSame(13033008000.0, $date); $date = kolab_api_filter_mapistore::date_php2mapi('2014-01-01'); $this->assertSame(13033008000.0, $date); $date = kolab_api_filter_mapistore::date_php2mapi('1970-01-01T00:00:00Z'); $this->assertSame(11644473600.0, $date); $date = kolab_api_filter_mapistore::date_php2mapi('1601-01-01T00:00:00Z'); $this->assertSame(0.0, $date); $date = new DateTime('1601-01-01T00:00:00Z'); $date = kolab_api_filter_mapistore::date_php2mapi($date); $this->assertSame(0.0, $date); /* $date = new DateTime('1970-01-01 00:00:00.1000 +0000'); $date = kolab_api_filter_mapistore::date_php2mapi($date); - $this->assertSame(11644473600.0 + (1000/1000000), $date); + $this->assertSame(11644473600.1, $date); */ $date = kolab_api_filter_mapistore::date_php2mapi(''); $this->assertSame(null, $date); } /** * Test date_mapi2php method */ function test_date_mapi2php() { $format = 'c'; $data = array( 13033008000 => '2014-01-01T00:00:00+00:00', 11644473600 => '1970-01-01T00:00:00+00:00', // 11644473600.00001 => '1970-01-01T00:00:00.10+00:00', 0 => '1601-01-01T00:00:00+00:00', ); foreach ($data as $mapi => $exp) { $date = kolab_api_filter_mapistore::date_mapi2php($mapi); $this->assertSame($exp, $date->format($format)); } } } diff --git a/tests/Unit/Filter/Mapistore/Common.php b/tests/Unit/Filter/Mapistore/Common.php index 77e9944..a448231 100644 --- a/tests/Unit/Filter/Mapistore/Common.php +++ b/tests/Unit/Filter/Mapistore/Common.php @@ -1,224 +1,239 @@ array( 'n2' => 'test2', ), 'n3' => 'test3', 'x-custom' => array( array('identifier' => 'i', value => 'val_i'), ), ); $value = kolab_api_filter_mapistore_common::get_kolab_value($data, 'n1.n2'); $this->assertSame('test2', $value); $value = kolab_api_filter_mapistore_common::get_kolab_value($data, 'n3'); $this->assertSame('test3', $value); $value = kolab_api_filter_mapistore_common::get_kolab_value($data, 'n30'); $this->assertSame(null, $value); $value = kolab_api_filter_mapistore_common::get_kolab_value($data, 'x-custom.i'); $this->assertSame('val_i', $value); } /** * Test set_kolab_value method */ function test_set_kolab_value() { $data = array(); kolab_api_filter_mapistore_common::set_kolab_value($data, 'n1.n2', 'test'); $this->assertSame('test', $data['n1']['n2']); kolab_api_filter_mapistore_common::set_kolab_value($data, 'n1', 'test'); $this->assertSame('test', $data['n1']); kolab_api_filter_mapistore_common::set_kolab_value($data, 'x-custom.i', 'test1'); $this->assertSame('test1', $data['x-custom.i']); } /** * Test attributes_filter method */ function test_attributes_filter() { $api = new kolab_api_filter_mapistore_common; $input = array( 'creation-date', 'uid', 'unknown', ); $expected = array( 'PidTagCreationTime', 'id', ); $result = $api->attributes_filter($input, true); $this->assertSame($expected, $result); $input = $expected; $expected = array( 'creation-date', 'uid', ); $result = $api->attributes_filter($input); $this->assertSame($expected, $result); $result = $api->attributes_filter(array()); $this->assertSame(array(), $result); } /** * Test parse_categories method */ function test_parse_categories() { $categories = array( "test\x3Btest", "test\x2Ctest", "a\x06\x1Ba", "b\xFE\x54b", "c\xFF\x1Bc", "test ", " test", ); $expected = array( "testtest", "aa", "bb", "cc", "test", ); $result = kolab_api_filter_mapistore_common::parse_categories($categories); $this->assertSame($expected, $result); } /** * Test recurrence_to_kolab */ function test_recurrence_to_kolab() { // empty result - $result = kolab_api_filter_mapistore_event::recurrence_to_kolab(''); + kolab_api_filter_mapistore_event::recurrence_to_kolab('', $result = array()); $this->assertSame(array(), $result); // build complete AppointmentRecurrencePattern structure $structure = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $exceptioninfo = new kolab_api_filter_mapistore_structure_exceptioninfo; $recurrencepattern = new kolab_api_filter_mapistore_structure_recurrencepattern; $extendedexception = new kolab_api_filter_mapistore_structure_extendedexception; $highlight = new kolab_api_filter_mapistore_structure_changehighlight; $highlight->ChangeHighlightValue = 4; $extendedexception->ChangeHighlight = $highlight; $extendedexception->StartDateTime = 0x0CBC9934; $extendedexception->EndDateTime = 0x0CBC9952; $extendedexception->OriginalStartDate = 0x0CBC98F8; $extendedexception->WideCharSubject = 'Simple Recurrence with exceptions'; $extendedexception->WideCharLocation = '34/4141'; $recurrencepattern->RecurFrequency = 0x200b; $recurrencepattern->PatternType = 1; $recurrencepattern->CalendarType = 0; $recurrencepattern->FirstDateTime = 0x000021C0; $recurrencepattern->Period = 1; $recurrencepattern->SlidingFlag = 0; $recurrencepattern->PatternTypeSpecific = 0x00000032; $recurrencepattern->EndType = 0x00002022; $recurrencepattern->OccurrenceCount = 12; $recurrencepattern->FirstDOW = 0; - $recurrencepattern->DeletedInstanceDates = array(0x0CBC96A0); + $recurrencepattern->DeletedInstanceDates = array(217742400, 218268000); $recurrencepattern->ModifiedInstanceDates = array(0x0CBC96A0); $recurrencepattern->StartDate = 213655680; $recurrencepattern->EndDate = 0x0CBCAD20; $exceptioninfo->StartDateTime = 0x0CBC9934; $exceptioninfo->EndDateTime = 0x0CBC9952; $exceptioninfo->OriginalStartDate = 0x0CBC98F8; $exceptioninfo->Subject = 'Simple Recurrence with exceptions'; $exceptioninfo->Location = '34/4141'; $structure->StartTimeOffset = 600; $structure->EndTimeOffset = 630; $structure->ExceptionInfo = array($exceptioninfo); $structure->RecurrencePattern = $recurrencepattern; $structure->ExtendedException = array($extendedexception); - $rule = $structure->output(true); - $result = kolab_api_filter_mapistore_event::recurrence_to_kolab($rule); + $rule = $structure->output(true); + kolab_api_filter_mapistore_event::recurrence_to_kolab($rule, $result); - $this->assertSame('WEEKLY', $result['freq']); + $this->assertSame('WEEKLY', $result['rrule']['recur']['freq']); + $this->assertSame('SU', $result['rrule']['recur']['wkst']); + $this->assertSame('SU,TU,MO,TH,FR', $result['rrule']['recur']['byday']); + $this->assertSame(12, $result['rrule']['recur']['count']); + $this->assertSame('2015-01-01', $result['exdate']['date'][0]); + $this->assertSame('2016-01-01', $result['exdate']['date'][1]); } /** * Test recurrence_from_kolab */ function test_recurrence_from_kolab() { - $event = array( - 'PidLidAppointmentStartWhole' => 123456789, - ); - - $rule = array( - 'freq' => 'MONTHLY', - 'bymonthday' => 5, - 'count' => 10, - 'interval' => 2, + $data = array( + 'dtstart' => '2015-01-01T00:00:00Z', + 'rrule' => array( + 'recur' => array( + 'freq' => 'MONTHLY', + 'bymonthday' => 5, + 'count' => 10, + 'interval' => 2, + ), + ), + 'exdate' => array( + 'date' => array( + '2015-01-01', + '2016-01-01', + ), + ), ); - $result = kolab_api_filter_mapistore_event::recurrence_from_kolab($rule, $event); + $result = kolab_api_filter_mapistore_event::recurrence_from_kolab($data, $event = array()); $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result, true); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH, $arp->RecurrencePattern->PatternType); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY, $arp->RecurrencePattern->RecurFrequency); $this->assertSame(5, $arp->RecurrencePattern->PatternTypeSpecific); $this->assertSame(10, $arp->RecurrencePattern->OccurrenceCount); $this->assertSame(2, $arp->RecurrencePattern->Period); + $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); + $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); // test $type=task - $task = array( - 'PidLidCommonStart' => 123456789, - ); - - $rule = array( - 'freq' => 'YEARLY', - 'bymonth' => 5, - 'bymonthday' => 1, - 'count' => 10, + $data = array( + 'dtstart' => '2015-01-01T00:00:00Z', + 'rrule' => array( + 'recur' => array( + 'freq' => 'YEARLY', + 'bymonth' => 5, + 'bymonthday' => 1, + 'count' => 10, + ), + ), ); - $result = kolab_api_filter_mapistore_event::recurrence_from_kolab($rule, $task, 'task'); - $rp = new kolab_api_filter_mapistore_structure_recurrencepattern; + $result = kolab_api_filter_mapistore_event::recurrence_from_kolab($data, $task = array(), 'task'); + $rp = new kolab_api_filter_mapistore_structure_recurrencepattern; $rp->input($result, true); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH, $rp->PatternType); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY, $rp->RecurFrequency); $this->assertSame(1, $rp->PatternTypeSpecific[1]); $this->assertSame(10, $rp->OccurrenceCount); $this->assertSame(12, $rp->Period); // @TODO: test other $rp properties } } diff --git a/tests/Unit/Filter/Mapistore/Event.php b/tests/Unit/Filter/Mapistore/Event.php index dd091d8..e9bdee7 100644 --- a/tests/Unit/Filter/Mapistore/Event.php +++ b/tests/Unit/Filter/Mapistore/Event.php @@ -1,204 +1,206 @@ 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::date_php2mapi('2015-05-14T13:03:33Z'), $result['PidTagCreationTime']); $this->assertSame(kolab_api_filter_mapistore::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']); $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::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentStartWhole']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentEndWhole']); $this->assertSame(1, $result['PidLidAppointmentSubType']); // recurrence $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); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_event; $data = array( 'PidTagCreationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T13:03:33Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore::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::date_php2mapi('2015-05-14T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T16:00:00Z'), 'PidLidReminderDelta' => 15, 'PidLidReminderSet' => 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('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; $data = array( // all-day event 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T00:00:00Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T00:00:00Z'), 'PidLidAppointmentSubType' => 1, '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(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::date_php2mapi('2015-05-14T13:03:33Z'), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore::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::date_php2mapi('2015-05-15T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore::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('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/Input/Json/Event.php b/tests/Unit/Input/Json/Event.php index 8000035..93fde62 100644 --- a/tests/Unit/Input/Json/Event.php +++ b/tests/Unit/Input/Json/Event.php @@ -1,157 +1,166 @@ input($data); } /** * Test expected exception in input method * * @expectedException kolab_api_exception * @expectedExceptionCode 422 */ function test_input_exception2() { $input = new kolab_api_input_json_event; $data = 'test'; $input->input($data); } /** * Test expected exception in input method * * @expectedException kolab_api_exception * @expectedExceptionCode 422 */ function test_input_exception3() { $input = new kolab_api_input_json_event; $data = array('test' => 'test'); // 'dtstamp' field is required $input->input($data); } /** * Test input method (convert JSON to internal format) */ function test_input() { $input = new kolab_api_input_json_event; $data = array( 'description' => 'description', 'summary' => 'summary', 'sequence' => 10, 'class' => 'PUBLIC', 'categories' => array('test'), 'created' => '2015-04-20T14:22:18Z', 'dtstamp' => '2015-04-21T00:00:00Z', 'status' => 'NEEDS-ACTION', 'dtstart' => '2014-01-01', 'dtend' => '2014-02-01', 'location' => null, 'priority' => 1, 'url' => 'url', 'attendee' => array( array( 'parameters' => array( 'cn' => 'Manager, Jane', 'partstat' => 'NEEDS-ACTION', 'role' => 'REQ-PARTICIPANT', 'rsvp' => true, ), 'cal-address' => 'mailto:%3Cjane.manager%40example.org%3E', ), ), 'organizer' => array( 'parameters' => array( 'cn' => 'Organizer', ), 'cal-address' => 'mailto:organizer%40example.org', ), + 'exdate' => array( + 'date' => array( + '2015-06-05', + '2015-06-12', + ), + ), ); $input->input($data); $this->assertSame('description', $data['description']); $this->assertSame('summary', $data['title']); $this->assertSame('public', $data['sensitivity']); $this->assertSame(10, $data['sequence']); $this->assertSame(array('test'), $data['categories']); $this->assertSame(null, $data['location']); $this->assertSame(1, $data['priority']); $this->assertSame('url', $data['url']); $this->assertSame(kolab_api_input_json::to_datetime('2015-04-20T14:22:18Z')->format('c'), $data['created']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2015-04-21T00:00:00Z')->format('c'), $data['changed']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-01-01')->format('c'), $data['start']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-02-01')->format('c'), $data['end']->format('c')); $this->assertSame('Manager, Jane', $data['attendees'][0]['name']); $this->assertSame('NEEDS-ACTION', $data['attendees'][0]['status']); $this->assertSame('REQ-PARTICIPANT', $data['attendees'][0]['role']); $this->assertSame(true, $data['attendees'][0]['rsvp']); $this->assertSame('jane.manager@example.org', $data['attendees'][0]['email']); $this->assertSame('Organizer', $data['organizer']['name']); $this->assertSame('organizer@example.org', $data['organizer']['email']); + $this->assertSame('2015-06-05', $data['recurrence']['EXDATE'][0]); + $this->assertSame('2015-06-12', $data['recurrence']['EXDATE'][1]); + self::$original = $data; } /** * Test input method with merging */ function test_input2() { $input = new kolab_api_input_json_event; $data = array( 'description' => 'description1', 'summary' => 'summary1', 'sequence' => 20, 'class' => 'PRIVATE', 'categories' => array('test1'), // 'created' => '2015-04-20T14:22:18Z', // 'dtstamp' => '2015-04-21T00:00:00Z', // 'status' => 'IN-PROCESS', 'dtstart' => '2014-01-11', 'dtend' => '2014-02-11', 'location' => 'location1', 'priority' => 2, 'url' => 'url1', ); $input->input($data, self::$original); $this->assertSame('description1', $data['description']); $this->assertSame('summary1', $data['title']); $this->assertSame('private', $data['sensitivity']); $this->assertSame(20, $data['sequence']); $this->assertSame(array('test1'), $data['categories']); $this->assertSame('location1', $data['location']); $this->assertSame(2, $data['priority']); $this->assertSame('url1', $data['url']); // $this->assertSame(kolab_api_input_json::to_datetime('2015-04-20T14:22:18Z')->format('c'), $data['created']->format('c')); // $this->assertSame(kolab_api_input_json::to_datetime('2015-04-21T00:00:00Z')->format('c'), $data['changed']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-01-11')->format('c'), $data['start']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-02-11')->format('c'), $data['end']->format('c')); } } diff --git a/tests/Unit/Output/Json/Event.php b/tests/Unit/Output/Json/Event.php index c091cb8..723f550 100644 --- a/tests/Unit/Output/Json/Event.php +++ b/tests/Unit/Output/Json/Event.php @@ -1,60 +1,62 @@ element($object); $this->assertSame('100-100-100-100', $result['uid']); $this->assertSame('2015-05-14T13:03:33Z', $result['created']); $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame(2, $result['sequence']); $this->assertSame('CONFIDENTIAL', $result['class']); $this->assertSame('Work', $result['categories'][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('Summary', $result['summary']); $this->assertSame('Description', $result['description']); $this->assertSame(1, $result['priority']); $this->assertSame('Location', $result['location']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:%3Cmark.german%40example.org%3E', $result['organizer']['cal-address']); $this->assertSame('https://some.url', $result['url']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('NEEDS-ACTION', $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:%3Cjane.manager%40example.org%3E', $result['attendee'][0]['cal-address']); $this->assertSame('image/jpeg', $result['attach'][0]['parameters']['fmttype']); $this->assertSame('photo-mini.jpg', $result['attach'][0]['parameters']['x-label']); $this->assertSame('cid:photo-mini.1431611291.28810.jpg', $result['attach'][0]['uri']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Summary', $result['valarm'][0]['properties']['description']); $this->assertSame('START', $result['valarm'][0]['properties']['trigger']['parameters']['related']); $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); $object = kolab_api_tests::get_data('101-101-101-101', 'Calendar', 'event', null, $context); $result = $output->element($object); $this->assertSame('101-101-101-101', $result['uid']); $this->assertSame('PUBLIC', $result['class']); $this->assertSame('2015-05-15', $result['dtstart']); $this->assertSame('2015-05-15', $result['dtend']); $this->assertSame('WEEKLY', $result['rrule']['recur']['freq']); $this->assertSame('MO', $result['rrule']['recur']['byday']); + $this->assertSame('2015-06-05', $result['exdate']['date'][0]); + $this->assertSame('2015-06-12', $result['exdate']['date'][1]); } } diff --git a/tests/data/event/101-101-101-101 b/tests/data/event/101-101-101-101 index a028fad..1b4582b 100644 --- a/tests/data/event/101-101-101-101 +++ b/tests/data/event/101-101-101-101 @@ -1,92 +1,101 @@ MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=_becd0be121fff5bc88bd1bcfb6c0997f" From: mark.german@example.org To: mark.german@example.org Date: Thu, 14 May 2015 16:59:09 +0200 X-Kolab-Type: application/x-vnd.kolab.event X-Kolab-Mime-Version: 3.0 Subject: 101-101-101-101 User-Agent: Kolab 3.1/Roundcube 1.2-git --=_becd0be121fff5bc88bd1bcfb6c0997f Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=ISO-8859-1 This is a Kolab Groupware object. To view this object you will need an emai= l client that understands the Kolab Groupware format. For a list of such em= ail clients please visit http://www.kolab.org/ --=_becd0be121fff5bc88bd1bcfb6c0997f Content-Transfer-Encoding: 8bit Content-Type: application/calendar+xml; charset=UTF-8; name=kolab.xml Content-Disposition: attachment; filename=kolab.xml; size=2466 Roundcube-libkolab-1.1 Libkolabxml-1.1 2.0 3.1.0 101-101-101-101 2015-05-14T14:59:09Z 2015-05-14T14:59:09Z 0 PUBLIC 2015-05-15 2015-05-15 WEEKLY MO + + + + /kolab.org/Europe/Berlin + + + 2015-06-05 + 2015-06-12 + Summary German, Mark mailto:%3Cmark.german%40example.org%3E --=_becd0be121fff5bc88bd1bcfb6c0997f--