diff --git a/lib/filter/mapistore.php b/lib/filter/mapistore.php index c772b01..500fe7c 100644 --- a/lib/filter/mapistore.php +++ b/lib/filter/mapistore.php @@ -1,585 +1,585 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore extends kolab_api_filter { protected $input; protected $attrs_filter; // Common properties [MS-OXCMSG] protected static $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' => '', // 'PidLidCategories' => 'categories', // @TODO ); /** * Modify request path * * @param array (Exploded) request path */ public function path(&$path) { // handle differences between OpenChange API and Kolab API // here we do only very basic modifications, just to be able // to select apprioprate api action class if ($path[0] == 'calendars') { $path[0] = 'events'; } } /** * Executed before every api action * * @param kolab_api_input Request */ public function input(&$input) { $this->input = $input; // handle differences between OpenChange API and Kolab API switch ($input->action) { case 'folders': // in OpenChange folders/1/folders means get all folders if ($input->method == 'GET' && $input->path[0] === '1' && $input->path[1] == 'folders') { $input->path = array(); $type = 'folder'; } // in OpenChange folders/0/folders means get the hierarchy of the NON IPM Subtree // we should ignore/send empty request else if ($input->method == 'GET' && $input->path[0] === '0' && $input->path[1] == 'folders') { // @TODO throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } else if ($input->path[1] == 'messages') { $input->path[1] = 'objects'; if ($input->args['properties']) { $type = $input->api->backend->folder_type($input->path[0]); list($type, ) = explode('.', $type); } } else if ($input->path[1] == 'deletemessages') { $input->path[1] = 'deleteobjects'; } // properties filter, map MAPI attribute names to Kolab attributes if ($type && $input->args['properties']) { $this->attrs_filter = explode(',', $this->input->args['properties']); $properties = $this->attributes_filter($this->attrs_filter, $type); $input->args['properties'] = implode(',', $properties); } break; case 'notes': // Notes do not have attachments in Exchange if ($input->path[1] === 'attachments' || count($this->path) > 2) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } break; } $common_action = !in_array($input->action, array('folders', 'info')); // convert / to // or /// if ($common_action && ($uid = $input->path[0])) { list($folder, $msg, $attach) = self::uid_decode($uid); $path = array($folder, $msg); if ($attach) { $path[] = $attach; } array_splice($input->path, 0, 1, $path); } // convert parent_id into path on object create request if ($input->method == 'POST' && $common_action && !count($input->path)) { $data = $input->input(null, true); if ($data['parent_id']) { $input->path[0] = $data['parent_id']; } else { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } } } /** * Executed when parsing request body * * @param string Request data * @param string Expected object type * @param string Original object data (set on update requests) */ public function input_body(&$data, $type = null, $original_object = null) { $input = $this->input; // handle differences between OpenChange API and Kolab API // Note: input->path is already modified by input() and path() above switch ($input->action) { case 'folders': // folders//deletemessages input if ($input->path[1] == 'deleteobjects') { // Kolab API expects just a list of identifiers, I.e.: // [{"id": "1"}, {"id": "2"}] => ["1", "2"] foreach ((array) $data as $idx => $element) { $data[$idx] = $element['id']; } } break; } switch ($type) { case 'attachment': case 'event': case 'note': case 'task': case 'contact': case 'mail': case 'folder': $model = $this->get_model_class($type); $data = $model->input($data, $original_object); break; } } /** * Apply filter on output data * * @param array Result data * @param string Object type * @param array Context (folder_uid, object_uid, object) * @param array Optional attributes filter */ public function output(&$result, $type, $context = null, $attrs_filter = array()) { // handle differences between OpenChange API and Kolab API $model = $this->get_model_class($type); if (!empty($this->attrs_filter)) { $attrs_filter = array_combine($this->attrs_filter, $this->attrs_filter); } else if (!empty($attrs_filter)) { $attrs_filter = $this->attributes_filter($attrs_filter, $type, true); $attrs_filter = array_combine($attrs_filter, $attrs_filter); } foreach ($result as $idx => $data) { if ($filtered = $model->output($data, $context)) { // apply properties filter (again) if (!empty($attrs_filter)) { $filtered = array_intersect_key($filtered, $attrs_filter); } $result[$idx] = $filtered; } else { unset($result[$idx]); $unset = true; } } if ($unset) { $result = array_values($result); } } /** * Executed for response headers * * @param array Response headers */ public function headers(&$headers) { // handle differences between OpenChange API and Kolab API foreach ($headers as $name => $value) { switch ($name) { case 'X-Count': $headers['X-mapistore-rowcount'] = $value; unset($headers[$name]); break; } } } /** * Executed for empty response status * * @param int Status code */ public function send_status(&$status) { // handle differences between OpenChange API and Kolab API } /** * 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; } /** * Converts kolab identifiers describind the object into * MAPI identifier that can be easily used in URL. * * @param string Folder UID * @param string Object UID * @param string Optional attachment identifier * * @return string Object identifier */ public static function uid_encode($folder_uid, $msg_uid, $attach_id = null) { $result = array($folder_uid, $msg_uid); if ($attach_id) { $result[] = $attach_id; } $result = array_map(array('kolab_api_filter_mapistore', 'uid_encode_item'), $result); return implode('.', $result); } /** * Converts back the MAPI identifier into kolab folder/object/attachment IDs * * @param string Object identifier * * @return array Object identifiers */ public static function uid_decode($uid) { $result = explode('.', $uid); $result = array_map(array('kolab_api_filter_mapistore', 'uid_decode_item'), $result); return $result; } /** * Encodes UID element */ protected static function uid_encode_item($str) { $fn = function($match) { return '_' . ord($match[1]); }; $str = preg_replace_callback('/([^0-9a-zA-Z-])/', $fn, $str); return $str; } /** * Decodes UID element */ protected static function uid_decode_item($str) { $fn = function($match) { return chr($match[1]); }; $str = preg_replace_callback('/_([0-9]{2})/', $fn, $str); return $str; } /** * Parse common properties in object data (convert into MAPI format) */ public static function parse_common_props(&$result, $data, $context = array()) { if (empty($context)) { // @TODO: throw exception? return; } if ($data['uid'] && $context['folder_uid']) { $result['id'] = self::uid_encode($context['folder_uid'], $data['uid']); } if ($context['folder_uid']) { $result['parent_id'] = $context['folder_uid']; } foreach (self::$map as $mapi_idx => $kolab_idx) { if (!isset($result[$mapi_idx]) && ($value = $data[$kolab_idx]) !== null) { switch ($kolab_idx) { case 'creation-date': case 'last-modification-date': $result[$mapi_idx] = self::date_php2mapi($value, true); break; } } } } /** * Convert common properties into kolab format */ public static function convert_common_props(&$result, $data, $original) { // @TODO: id, parent_id? foreach (self::$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; } } } // 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 */ public function attributes_filter($attrs, $type = null, $reverse = false) { $map = self::$map; $result = array(); if ($type) { $model = $this->get_model_class($type); $map = array_merge($map, $model->map()); } // 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 instance of model class object */ protected function get_model_class($type) { $class = "kolab_api_filter_mapistore_$type"; return new $class($this); } /** * Convert DateTime object to MAPI date format */ public 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)) { 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. // Note: probably does not work on 32-bit systems return ($date->format('U') + 11644473600) * 10000000; } /** * Convert date-time from MAPI format to DateTime */ - public function date_mapi2php($date, $utc = true) + public function date_mapi2php($date) { // Note: probably does not work on 32-bit systems $seconds = intval($date / 10000000) - 11644473600; // assumes we're working with dates after 1970-01-01 return new DateTime('@' . $seconds); } /** * 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); } } diff --git a/lib/filter/mapistore/event.php b/lib/filter/mapistore/event.php index 8bd9d53..0497267 100644 --- a/lib/filter/mapistore/event.php +++ b/lib/filter/mapistore/event.php @@ -1,602 +1,802 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_event { 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 'PidNameKeywords' => 'categories', // PtypMultipleString '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 ); 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' => 0x00000002, 'CHAIR' => 0x00000001, ); /** * 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, ); /** * Mapping of weekdays */ - protected $recurrence_day_map = array( + 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, ); /** * 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 = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidNameKeywords': $value = kolab_api_filter_mapistore::parse_categories((array) $value); break; 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->add_attendee_to_result($data['organizer'], $result, true); } // Attendees [MS-OXCICAL 2.1.3.1.1.20.2] foreach ((array) $data['attendee'] as $attendee) { $this->add_attendee_to_result($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'])) { + if ($rule = self::recurrence_from_kolab($data['rrule']['recur'], $result)) { $result['PidLidAppointmentRecur'] = $rule; } } - // @TODO: PidLidAppointmentDuration - // @TODO: exceptions, resources - kolab_api_filter_mapistore::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']); + } + // @TODO: PidLidAppointmentDuration (?) - // @TODO: exceptions, resources, recurrence, attendees + // @TODO: exceptions, resources, attendees kolab_api_filter_mapistore::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; } /** * 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; } /** * Convert Kolab 'attendee' specification into MAPI recipient * and add it to the result */ protected function add_attendee_to_result($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']); } // PidTagEntryId // PidTagRecipientEntryId $recipient['PidTagRecipientDisplayName'] = $recipient['PidTagDisplayName']; $result['recipients'][] = $recipient; if (strtoupper($params['rsvp']) == 'TRUE') { $result['PidTagReplyRequested'] = true; $result['PidTagResponseRequested'] = true; } } } /** * Convert PidLidReminderDelta value into xCal duration */ protected static function reminder_delta_to_duration($delta) { if ($delta == 0x5AE980E1) { $delta = 15; } $delta = (int) $delta; return "-PT{$delta}M"; } /** * Convert Kolab alarm duration into PidLidReminderDelta */ protected static function reminder_duration_to_delta($duration) { if ($duration && preg_match('/^-[PT]*([0-9]+)([WDHMS])$/', $duration, $matches)) { $value = intval($matches[1]); switch ($matches[2]) { case 'S': $value = intval(round($value/60)); break; case 'H': $value *= 60; break; case 'D': $value *= 24 * 60; break; case 'W': $value *= 7 * 24 * 60; break; } return $value; } } /** * Convert Kolab recurrence specification into MAPI properties + * + * @param array $rule Recurrence rule in Kolab format + * @param array $type Object data (MAPI format) + * @param string $type Object type (event, task) + * + * @return object MAPI recurrence in binary format */ - protected function recurrence_from_kolab($rule) + public static function recurrence_from_kolab($rule, $object = array(), $type = 'event') { $result = array( - 'ReaderVersion' => 0x3004, - 'WriterVersion' => 0x3004, - 'ReaderVersion2' => 0x00003006, - 'WriterVersion2' => 0x00003009, 'Period' => $rule['interval'] ? $rule['interval'] : 1, - 'SlidingFlag' => 0, - 'FirstDOW' => $this->day2bitmask($rule['wkst']), - 'StartTimeOffset' => 60 * intval($rule['byhour']) + intval($rule['byminute']), - 'EndType' => 0x00002021, - 'OccurenceCount' => 0x0000000A, - 'EndDate' => 0x5AE980DF, // @TODO - 'CalendarType' => 0x0001, - // EndTimeOffset - // FirstDateTime - // ExceptionCount, ExceptionInfo - // ReservedBlocks, ExtendedExceptions - // DeletedInstanceCount, DeletedInstanceDates - // ModifiedInstanceCount, ModifiedInstanceDates + '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); + } + 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'] = 0x200A; - $result['PatternType'] = 0x000; - $result['Period'] *= 1440; + $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': - $result['RecurFrequency'] = 0x200B; - $result['PatternType'] = 0x0001; - $result['PatternTypeSpecific.Week.Sa-Su'] = $this->day2bitmask($rule['byday'], 'BYDAY-'); + // 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'] = 0x200C; - $result['PatternType'] = 0x0002; + $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY; if (!empty($rule['bymonthday'])) { - // @TODO: MAPI doesn't support multi-valued month days - $month_day = array_shift(explode(',', $rule['bymonthday'])); - $result['PatternTypeSpecific.Month.Day'] = $month_day == -1 ? 0x0000001F : $month_day; + // 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['PatternTypeSpecific.MonthNth.Sa-Su'] = $this->day2bitmask($rule['byday'], 'BYDAY-');; + $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; + $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); + if (!empty($rule['bysetpos'])) { - $result['PatternTypeSpecific.MonthNth.N'] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; + $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; case 'YEARLY': - $result['RecurFrequency'] = 0x200D; - $result['PatternType'] = 0x0002; + $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY; $result['Period'] *= 12; - // @TODO: MAPI doesn't support multi-valued months - $month = array_shift(explode(',', $rule['bymonth'])); - - // @TODO: StartDate/EndDate + // MAPI doesn't support multi-valued months + if ($rule['bymonth']) { + // @TODO: set $startdate + } if (!empty($rule['bymonthday'])) { - // @TODO: MAPI doesn't support multi-valued month days - $month_day = array_shift(explode(',', $rule['bymonthday'])); - $result['PatternTypeSpecific.Month.Day'] = $month_day == -1 ? 0x0000001F : $month_day; + // 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'] = 0x0003; - $result['PatternTypeSpecific.MonthNth.Sa-Su'] = $this->day2bitmask($rule['byday'], 'BYDAY-');; + $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; + $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); + if (!empty($rule['bysetpos'])) { - $result['PatternTypeSpecific.MonthNth.N'] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; + $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; } if (!empty($rule['until'])) { -// $recurrence['until'] = self::date_from_kolab($rule['until']); - // @TODO: calculate OccurenceCount? - $result['EndType'] = 0x00002023; + $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'] = 0x00002022; - $result['OccurenceCount'] = $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 = 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 + */ + public static function recurrence_to_kolab($rule, $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; } /** * Converts string of days (TU,TH) to bitmask used by MAPI * * @param string $days * * @return int */ - protected function day2bitmask($days, $prefix = '') + protected static function day2bitmask($days, $prefix = '') { $days = explode(',', $days); $result = 0; foreach ($days as $day) { - $result = $result + $this->recurrence_day_map[$prefix.$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 function bitmask2day($days) + protected static function bitmask2day($days) { $days_arr = array(); - foreach ($this->recurrence_day_map as $day => $bit) { + foreach (self::$recurrence_day_map as $day => $bit) { if ($days & $bit) { $days_arr[] = preg_replace('/^[A-Z-]+/', '', $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) + { + $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); + } } diff --git a/lib/filter/mapistore/structure.php b/lib/filter/mapistore/structure.php index b3b4faa..bad99ca 100644 --- a/lib/filter/mapistore/structure.php +++ b/lib/filter/mapistore/structure.php @@ -1,315 +1,332 @@ | +--------------------------------------------------------------------------+ | 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, ); /** * 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); // 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) { - $value = array_key_exists($idx, $this->data) ? $this->data[$idx] : $struct['default']; + 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'); // 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', $value); + $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 = 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]; } } diff --git a/lib/filter/mapistore/structure/recurrencepattern.php b/lib/filter/mapistore/structure/recurrencepattern.php index 463d925..d533d86 100644 --- a/lib/filter/mapistore/structure/recurrencepattern.php +++ b/lib/filter/mapistore/structure/recurrencepattern.php @@ -1,167 +1,170 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * RecurrencePattern structure definition according to MS-OXOCAL 2.2.1.44.1 */ class kolab_api_filter_mapistore_structure_recurrencepattern extends kolab_api_filter_mapistore_structure { protected $structure = array( 'ReaderVersion' => array('type' => 'WORD', 'default' => 0x3004), 'WriterVersion' => array('type' => 'WORD', 'default' => 0x3004), 'RecurFrequency' => array('type' => 'WORD'), 'PatternType' => array('type' => 'WORD'), - 'CalendarType' => array('type' => 'WORD'), + 'CalendarType' => array('type' => 'WORD', 'default' => 0x0000), 'FirstDateTime' => array('type' => 'ULONG'), 'Period' => array('type' => 'ULONG'), - 'SlidingFlag' => array('type' => 'ULONG', 'default' => 0), + 'SlidingFlag' => array('type' => 'ULONG', 'default' => 0x00000000), 'PatternTypeSpecific' => array('type' => 'EMPTY'), // default for PatternType=0 'EndType' => array('type' => 'ULONG'), 'OccurrenceCount' => array('type' => 'ULONG', 'default' => 0x0000000A), - 'FirstDOW' => array('type' => 'ULONG', 'default' => 0), - 'DeletedInstanceCount' => array('type' => 'ULONG', 'default' => 0), + 'FirstDOW' => array('type' => 'ULONG', 'default' => 0x00000000), + 'DeletedInstanceCount' => array('type' => 'ULONG', 'default' => 0x00000000), 'DeletedInstanceDates' => array('type' => 'ULONG[]', 'counter' => 'DeletedInstanceCount'), - 'ModifiedInstanceCount' => array('type' => 'ULONG', 'default' => 0), + 'ModifiedInstanceCount' => array('type' => 'ULONG', 'default' => 0x00000000), 'ModifiedInstanceDates' => array('type' => 'ULONG[]', 'counter' => 'ModifiedInstanceCount'), 'StartDate' => array('type' => 'ULONG'), 'EndDate' => array('type' => 'ULONG'), ); const RECURFREQUENCY_DAILY = 0x200A; const RECURFREQUENCY_WEEKLY = 0x200B; const RECURFREQUENCY_MONTHLY = 0x200C; const RECURFREQUENCY_YEARLY = 0x200D; const PATTERNTYPE_DAY = 0x0000; const PATTERNTYPE_WEEK = 0x0001; const PATTERNTYPE_MONTH = 0x0002; const PATTERNTYPE_MONTHEND = 0x0004; const PATTERNTYPE_MONTHNTH = 0x0003; const PATTERNTYPE_HJMONTH = 0x000A; const PATTERNTYPE_HJMONTHNTH = 0x000B; const PATTERNTYPE_HJMONTHEND = 0x000C; + const CALENDARTYPE_DEFAULT = 0x0000; const CALENDARTYPE_GREGORIAN = 0x0001; const CALENDARTYPE_GREGORIAN_US = 0x0002; const CALENDARTYPE_JAPAN = 0x0003; const CALENDARTYPE_TAIWAN = 0x0004; const CALENDARTYPE_KOREA = 0x0005; const CALENDARTYPE_HIJRI = 0x0006; const CALENDARTYPE_THAI = 0x0007; const CALENDARTYPE_HEBREW = 0x0008; const CALENDARTYPE_GREGORIAN_ME_FRENCH = 0x0009; const CALENDARTYPE_GREGORIAN_ARABIC = 0x000A; const CALENDARTYPE_GREGORIAN_XLIT_ENGLISH = 0x000B; const CALENDARTYPE_GREGORIAN_XLIT_FRENCH = 0x000C; const CALENDARTYPE_LUNAR_JAPANESE = 0x000E; const CALENDARTYPE_CHINESE_LUNAR = 0x000F; const CALENDARTYPE_SAKA = 0x0010; const CALENDARTYPE_LUNAR_ETO_CHN = 0x0011; const CALENDARTYPE_LUNAR_ETO_KOR = 0x0012; const CALENDARTYPE_LUNAR_ROKUYOU = 0x0013; const CALENDARTYPE_LUNAR_KOREAN = 0x0014; const CALENDARTYPE_UMALQURA = 0x0017; const ENDTYPE_AFTER = 0x00002021; const ENDTYPE_NOCC = 0x00002022; const ENDTYPE_NEVER = 0x00002023; // can be 0xffffffff const FIRSTDOW_SUNDAY = 0x00000000; const FIRSTDOW_MONDAY = 0x00000001; const FIRSTDOW_TUESDAY = 0x00000002; const FIRSTDOW_WEDNESDAY = 0x00000003; const FIRSTDOW_THURSDAY = 0x00000004; const FIRSTDOW_FRIDAY = 0x00000005; const FIRSTDOW_SATURDAY = 0x00000006; /** * 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 PatternType $unpack = unpack('v', substr($input, 6, 2)); $value = $unpack[1]; $this->data['PatternType'] = $value; // modify structure according to PatternType $this->set_structure(); 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->set_structure(); + $this->data['DeletedInstanceDates'] = (array) $this->data['DeletedInstanceDates']; + $this->data['ModifiedInstanceDates'] = (array) $this->data['ModifiedInstanceDates']; $this->data['DeletedInstanceCount'] = count($this->data['DeletedInstanceDates']); $this->data['ModifiedInstanceCount'] = count($this->data['ModifiedInstanceDates']); return parent::output($base64); } /** * Modify the structure according to PatternType */ protected function set_structure() { // Set PatternTypeSpecific field type according to PatternType switch ($this->data['PatternType']) { case self::PATTERNTYPE_WEEK: case self::PATTERNTYPE_MONTH: case self::PATTERNTYPE_MONTHEND: case self::PATTERNTYPE_HJMONTH: case self::PATTERNTYPE_HJMONTHEND: $this->structure['PatternTypeSpecific']['type'] = 'ULONG'; break; case self::PATTERNTYPE_MONTHNTH: case self::PATTERNTYPE_HJMONTHNTH: $this->structure['PatternTypeSpecific']['type'] = 'ULONG[2]'; break; case self::PATTERNTYPE_DAY: default: $this->structure['PatternTypeSpecific']['type'] = 'EMPTY'; break; } } } diff --git a/lib/filter/mapistore/structure/systemtime.php b/lib/filter/mapistore/structure/systemtime.php index f53107d..b03f4e7 100644 --- a/lib/filter/mapistore/structure/systemtime.php +++ b/lib/filter/mapistore/structure/systemtime.php @@ -1,40 +1,40 @@ | +--------------------------------------------------------------------------+ | 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'), - 'WSecond' => array('type' => 'WORD'), - 'WMilliseconds' => array('type' => 'WORD'), + 'WMinute' => array('type' => 'WORD', 'default' => 0), + 'WSecond' => array('type' => 'WORD', 'default' => 0), + 'WMilliseconds' => array('type' => 'WORD', 'default' => 0), ); } diff --git a/lib/filter/mapistore/task.php b/lib/filter/mapistore/task.php index 5613845..1f4e42b 100644 --- a/lib/filter/mapistore/task.php +++ b/lib/filter/mapistore/task.php @@ -1,259 +1,272 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_task { 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' => '', // @TODO // PtypBinary + '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 = kolab_api_filter_mapistore::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 + // PidLidTaskDateCompleted (?) } else if ($precent > 0) { $result['PidLidTaskStatus'] = $this->status_map['in-progress']; } else { $result['PidLidTaskStatus'] = $this->status_map['none']; } - // @TODO: recurrence + // Recurrence rule + if (!empty($data['rrule']) && !empty($data['rrule']['recur'])) { + if ($rule = kolab_api_filter_mapistore_event::recurrence_from_kolab($data['rrule']['recur'], $result)) { + $result['PidLidTaskRecurrence'] = $rule; + $result['PidLidTaskFRecurring'] = true; + } + } kolab_api_filter_mapistore::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 + if (array_key_exists('PidLidTaskRecurrence', $data)) { + $result['rrule']['recur'] = kolab_api_filter_mapistore_event::recurrence_to_kolab($data['PidLidTaskRecurrence'], 'task'); + } + kolab_api_filter_mapistore::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/output/json.php b/lib/output/json.php index 1c2f9e1..01d31d0 100644 --- a/lib/output/json.php +++ b/lib/output/json.php @@ -1,237 +1,241 @@ | +--------------------------------------------------------------------------+ | 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); } // 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']); 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': - default: + 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]); } } } } diff --git a/tests/API/Events.php b/tests/API/Events.php index a5b19a7..af8573f 100644 --- a/tests/API/Events.php +++ b/tests/API/Events.php @@ -1,217 +1,217 @@ get('folders/' . md5('Calendar') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('100-100-100-100', $body[0]['uid']); $this->assertSame('Summary', $body[0]['summary']); $this->assertSame('101-101-101-101', $body[1]['uid']); $this->assertSame('PUBLIC', $body[1]['class']); } /** * Test event existence */ function test_event_exists() { self::$api->head('events/' . md5('Calendar') . '/100-100-100-100'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing event self::$api->head('events/' . md5('Calendar') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test event info */ function test_event_info() { self::$api->get('events/' . md5('Calendar') . '/100-100-100-100'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('100-100-100-100', $body['uid']); $this->assertSame('Summary', $body['summary']); } /** * Test event create */ function test_event_create() { $post = json_encode(array( 'summary' => 'Test description', 'dtstart' => '2015-01-01', )); self::$api->post('events/' . md5('Calendar'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); // folder does not exists $post = json_encode(array( 'summary' => 'Test description', 'dtstart' => '2015-01-01', )); self::$api->post('events/' . md5('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('events/' . md5('Calendar'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test event update */ function test_event_update() { // @TODO: test modification of all supported properties $post = json_encode(array( 'summary' => 'Modified summary (1)', 'dtstart' => '2015-01-01', )); self::$api->put('events/' . md5('Calendar') . '/100-100-100-100', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); self::$api->get('events/' . md5('Calendar') . '/100-100-100-100'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified summary (1)', $body['summary']); } - /** - * Test event delete - */ - function test_event_delete() - { - // delete existing event - self::$api->delete('events/' . md5('Calendar') . '/101-101-101-101'); - - $code = self::$api->response_code(); - $body = self::$api->response_body(); - - $this->assertEquals(204, $code); - $this->assertSame('', $body); - - // and non-existing event - self::$api->delete('events/' . md5('Calendar') . '/12345'); - - $code = self::$api->response_code(); - $body = self::$api->response_body(); - - $this->assertEquals(404, $code); - $this->assertSame('', $body); - } /** * Test counting event attachments */ function test_count_attachments() { self::$api->head('events/' . md5('Calendar') . '/100-100-100-100/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(1, (int) $count); -/* + self::$api->head('events/' . md5('Calendar') . '/101-101-101-101/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); -*/ } /** * Test listing event attachments */ function test_list_attachments() { self::$api->get('events/' . md5('Calendar') . '/100-100-100-100/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame('3', $body[0]['id']); $this->assertSame('image/jpeg', $body[0]['mimetype']); $this->assertSame('photo-mini.jpg', $body[0]['filename']); $this->assertSame('attachment', $body[0]['disposition']); $this->assertSame(793, $body[0]['size']); } + + /** + * Test event delete + */ + function test_event_delete() + { + // delete existing event + self::$api->delete('events/' . md5('Calendar') . '/101-101-101-101'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(204, $code); + $this->assertSame('', $body); + + // and non-existing event + self::$api->delete('events/' . md5('Calendar') . '/12345'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(404, $code); + $this->assertSame('', $body); + } } diff --git a/tests/Mapistore/Events.php b/tests/Mapistore/Events.php index 7f0c03c..00c7d73 100644 --- a/tests/Mapistore/Events.php +++ b/tests/Mapistore/Events.php @@ -1,227 +1,227 @@ get('folders/' . md5('Calendar') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100'), $body[0]['id']); $this->assertSame('Summary', $body[0]['PidTagSubject']); $this->assertSame('Description', $body[0]['PidTagBody']); $this->assertSame('calendars', $body[0]['collection']); $this->assertSame('IPM.Appointment', $body[0]['PidTagMessageClass']); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '101-101-101-101'), $body[1]['id']); $this->assertSame(0, $body[1]['PidTagSensitivity']); $this->assertSame('calendars', $body[1]['collection']); $this->assertSame('IPM.Appointment', $body[1]['PidTagMessageClass']); } /** * Test event existence */ function test_event_exists() { self::$api->head('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing event self::$api->head('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test event info */ function test_event_info() { self::$api->get('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100'), $body['id']); $this->assertSame('Summary', $body['PidTagSubject']); $this->assertSame('calendars', $body['collection']); $this->assertSame('IPM.Appointment', $body['PidTagMessageClass']); } /** * Test event create */ function test_event_create() { $post = json_encode(array( 'parent_id' => md5('Calendar'), 'PidTagSubject' => 'Test summary', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-01-01'), )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['id'])); // folder does not exists $post = json_encode(array( 'parent_id' => md5('non-existing'), 'PidTagSubject' => 'Test summary', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-01-01'), )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'parent_id' => md5('Calendar'), 'test' => 'Test summary 2', )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test event update */ function test_event_update() { -return; // @TODO // @TODO: test modification of all supported properties $post = json_encode(array( 'PidTagSubject' => 'Modified subject (1)', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-01-01'), )); + self::$api->put('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); self::$api->get('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100')); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified subject (1)', $body['PidTagSubject']); } /** * Test event delete */ function test_event_delete() { // delete existing event self::$api->delete('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '101-101-101-101')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing event self::$api->delete('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test counting event attachments */ function test_count_attachments() { self::$api->head('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(1, (int) $count); /* self::$api->head('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '101-101-101-101') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); */ } /** * Test listing event attachments */ function test_list_attachments() { self::$api->get('calendars/' . kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '100-100-100-100', '3'), $body[0]['id']); $this->assertSame('image/jpeg', $body[0]['PidTagAttachMimeTag']); $this->assertSame('photo-mini.jpg', $body[0]['PidTagDisplayName']); $this->assertSame(793, $body[0]['PidTagAttachSize']); } } diff --git a/tests/Mapistore/Tasks.php b/tests/Mapistore/Tasks.php index 07cc49f..765333d 100644 --- a/tests/Mapistore/Tasks.php +++ b/tests/Mapistore/Tasks.php @@ -1,220 +1,238 @@ get('folders/' . md5('Tasks') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(2, count($body)); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10'), $body[0]['id']); $this->assertSame(md5('Tasks'), $body[0]['parent_id']); $this->assertSame('IPM.Task', $body[0]['PidTagMessageClass']); $this->assertSame('tasks', $body[0]['collection']); $this->assertSame('task title', $body[0]['PidTagSubject']); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '20-20-20-20'), $body[1]['id']); $this->assertSame(md5('Tasks'), $body[1]['parent_id']); $this->assertSame('IPM.Task', $body[1]['PidTagMessageClass']); $this->assertSame('tasks', $body[1]['collection']); $this->assertSame('task', $body[1]['PidTagSubject']); } /** * Test task existence */ function test_task_exists() { self::$api->head('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing task self::$api->head('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test task info */ function test_task_info() { self::$api->get('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10'), $body['id']); $this->assertSame('task title', $body['PidTagSubject']); $this->assertSame("task description\nsecond line", $body['PidTagBody']); } /** * Test task create */ function test_task_create() { $post = json_encode(array( 'parent_id' => md5('Tasks'), 'PidTagSubject' => 'Test summary', 'PidTagBody' => 'Test description', )); self::$api->post('tasks', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['id'])); // folder does not exists $post = json_encode(array( 'parent_id' => md5('non-existing'), 'PidTagSubject' => 'Test summary 2', )); self::$api->post('tasks', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'parent_id' => md5('Tasks'), 'test' => 'Test summary 2', )); self::$api->post('tasks', array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test task update */ function test_task_update() { $post = json_encode(array( 'PidTagSubject' => 'Modified summary', 'PidTagBody' => 'Modified description' )); self::$api->put('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); self::$api->get('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10')); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified summary', $body['PidTagSubject']); $this->assertSame('Modified description', $body['PidTagBody']); } - /** - * Test task delete - */ - function test_task_delete() - { - // delete existing task - self::$api->delete('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '20-20-20-20')); - - $code = self::$api->response_code(); - $body = self::$api->response_body(); - - $this->assertEquals(204, $code); - $this->assertSame('', $body); - - // and non-existing task - self::$api->delete('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '12345')); - - $code = self::$api->response_code(); - $body = self::$api->response_body(); - - $this->assertEquals(404, $code); - $this->assertSame('', $body); - } /** * Test counting task attachments */ function test_count_attachments() { + // task with an attachment self::$api->head('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'),'10-10-10-10') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(1, (int) $count); - // @TODO: test task with no attachments + // task with no attachments + self::$api->head('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '20-20-20-20') . '/attachments'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $count = self::$api->response_header('X-mapistore-rowcount'); + + $this->assertEquals(200, $code); + $this->assertSame('', $body); + $this->assertSame(0, (int) $count); } /** * Test listing task attachments */ function test_list_attachments() { self::$api->get('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); - $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10', 3), $body[0]['id']); $this->assertSame('text/plain', $body[0]['PidTagAttachMimeTag']); $this->assertSame('test.txt', $body[0]['PidTagDisplayName']); $this->assertSame(4, $body[0]['PidTagAttachSize']); - // @TODO: test task with no attachments + // task with no attachments + self::$api->get('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '20-20-20-20') . '/attachments'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertCount(0, $body); + } + + /** + * Test task delete + */ + function test_task_delete() + { + // delete existing task + self::$api->delete('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '20-20-20-20')); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(204, $code); + $this->assertSame('', $body); + + // and non-existing task + self::$api->delete('tasks/' . kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '12345')); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(404, $code); + $this->assertSame('', $body); } } diff --git a/tests/Unit/Filter/Mapistore/Event.php b/tests/Unit/Filter/Mapistore/Event.php index ef3f922..ebe6684 100644 --- a/tests/Unit/Filter/Mapistore/Event.php +++ b/tests/Unit/Filter/Mapistore/Event.php @@ -1,176 +1,288 @@ output($data, $context); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '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('Manager, Jane', $result['recipients'][1]['PidTagDisplayName']); $this->assertSame(2, $result['recipients'][1]['PidTagRecipientType']); $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); $data = kolab_api_tests::get_data('101-101-101-101', md5('Calendar'), 'event', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Calendar'), '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']); - $this->assertSame(1, $result['PidLidAppointmentRecur']['Period']); - $this->assertSame(0x200B, $result['PidLidAppointmentRecur']['RecurFrequency']); - $this->assertSame(1, $result['PidLidAppointmentRecur']['PatternType']); - $this->assertSame(2, $result['PidLidAppointmentRecur']['PatternTypeSpecific.Week.Sa-Su']); + + // 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); } /** * 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, ); $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']); self::$original = $result; - // @TODO: recurrence, alarms, attendees $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, attendees ); $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, alarms, attendees + // @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)); } + + /** + * Test recurrence_to_kolab + */ + function test_recurrence_to_kolab() + { + // empty result + $result = kolab_api_filter_mapistore_event::recurrence_to_kolab(''); + $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->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); + + $this->assertSame('WEEKLY', $result['freq']); + } + + /** + * Test recurrence_from_kolab + */ + function test_recurrence_from_kolab() + { + $event = array( + 'PidLidAppointmentStartWhole' => 123456789, + ); + + $rule = array( + 'freq' => 'MONTHLY', + 'bymonthday' => 5, + 'count' => 10, + 'interval' => 2, + ); + + $result = kolab_api_filter_mapistore_event::recurrence_from_kolab($rule, $event); + $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); + + // test $type=task + $task = array( + 'PidLidCommonStart' => 123456789, + ); + + $rule = 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; + $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/Task.php b/tests/Unit/Filter/Mapistore/Task.php index b0d400d..3430ac7 100644 --- a/tests/Unit/Filter/Mapistore/Task.php +++ b/tests/Unit/Filter/Mapistore/Task.php @@ -1,154 +1,162 @@ output($data, $context); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10'), $result['id']); $this->assertSame(md5('Tasks'), $result['parent_id']); $this->assertSame('IPM.Task', $result['PidTagMessageClass']); $this->assertSame('tasks', $result['collection']); $this->assertSame('task title', $result['PidTagSubject']); $this->assertSame("task description\nsecond line", $result['PidTagBody']); $this->assertSame(0.56, $result['PidLidPercentComplete']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-04-20T14:22:18Z', true), $result['PidTagLastModificationTime']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-04-20T14:22:18Z', true), $result['PidTagCreationTime']); $this->assertSame(8, $result['PidLidTaskActualEffort']); /* $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:%3Cmark.german%40example.org%3E', $result['organizer']['cal-address']); */ $data = kolab_api_tests::get_data('20-20-20-20', md5('Tasks'), 'task', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '20-20-20-20'), $result['id']); $this->assertSame(md5('Tasks'), $result['parent_id']); $this->assertSame('IPM.Task', $result['PidTagMessageClass']); $this->assertSame('tasks', $result['collection']); $this->assertSame('task', $result['PidTagSubject']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-04-20', true), $result['PidLidTaskStartDate']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-04-27', true), $result['PidLidTaskDueDate']); + /* - $this->assertSame('DAILY', $result['rrule']['recur']); $this->assertSame('NEEDS-ACTION', $result['status']); $this->assertSame('Manager, Jane', $result['attendee']['parameters']['cn']); $this->assertSame('NEEDS-ACTION', $result['attendee']['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee']['parameters']['role']); - $this->assertSame('true', $result['attendee']['parameters']['rsvp']); + $this->assertSame(true, $result['attendee']['parameters']['rsvp']); $this->assertSame('mailto:%3Cjane.manager%40example.org%3E', $result['attendee']['cal-address']); */ + // recurrence + $rp = new kolab_api_filter_mapistore_structure_recurrencepattern; + $rp->input($result['PidLidTaskRecurrence'], true); + + $this->assertSame(true, $result['PidLidTaskFRecurring']); + $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY, $rp->PatternType); + $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY, $rp->RecurFrequency); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_task; $data = array( 'id' => kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10'), 'parent_id' => md5('Tasks'), 'PidTagCreationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-01-20T11:44:59Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-01-22T11:30:17Z'), 'PidTagMessageClass' => 'IPM.Task', 'PidTagSubject' => 'subject', 'PidLidPercentComplete' => 0.56, 'PidTagBody' => 'body', 'PidLidTaskStartDate' => kolab_api_filter_mapistore::date_php2mapi('2015-04-20', true), 'PidLidTaskDueDate' => kolab_api_filter_mapistore::date_php2mapi('2015-04-27', true), 'PidLidTaskActualEffort' => 16, 'PidLidTaskEstimatedEffort' => 20, ); $result = $api->input($data); self::$original = $result; $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame(56, $result['percent-complete']); $this->assertSame('2015-01-20T11:44:59Z', $result['created']); $this->assertSame('2015-01-22T11:30:17Z', $result['dtstamp']); $this->assertSame('2015-04-20', $result['dtstart']); $this->assertSame('2015-04-27', $result['due']); $this->assertSame('MAPI:PidLidTaskActualEffort', $result['x-custom'][0]['identifier']); $this->assertSame(16, $result['x-custom'][0]['value']); $this->assertSame('MAPI:PidLidTaskEstimatedEffort', $result['x-custom'][1]['identifier']); $this->assertSame(20, $result['x-custom'][1]['value']); $data = array( 'PidLidTaskComplete' => true, 'PidLidTaskDateCompleted' => kolab_api_filter_mapistore::date_php2mapi('2015-04-20', true), 'PidLidTaskActualEffort' => 100, 'PidLidTaskEstimatedEffort' => 100, + // @TODO: recurrence ); $result = $api->input($data); $this->assertSame('COMPLETED', $result['status']); // $this->assertSame(100, $result['x-custom']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_task; $data = array( // 'id' => kolab_api_filter_mapistore::uid_encode(md5('Tasks'), '10-10-10-10'), // 'parent_id' => md5('Tasks'), 'PidTagCreationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-01-20T12:44:59Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-01-22T12:30:17Z'), // 'PidTagMessageClass' => 'IPM.Task', 'PidTagSubject' => 'subject1', 'PidLidPercentComplete' => 0.66, 'PidTagBody' => 'body1', 'PidLidTaskStartDate' => kolab_api_filter_mapistore::date_php2mapi('2015-04-21', true), 'PidLidTaskDueDate' => kolab_api_filter_mapistore::date_php2mapi('2015-04-28', true), 'PidLidTaskActualEffort' => 21, 'PidLidTaskEstimatedEffort' => null, ); $result = $api->input($data, self::$original); self::$original = $result; $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame(66, $result['percent-complete']); $this->assertSame('2015-01-20T12:44:59Z', $result['created']); $this->assertSame('2015-01-22T12:30:17Z', $result['dtstamp']); $this->assertSame('2015-04-21', $result['dtstart']); $this->assertSame('2015-04-28', $result['due']); $this->assertSame('MAPI:PidLidTaskActualEffort', $result['x-custom'][0]['identifier']); $this->assertSame(21, $result['x-custom'][0]['value']); $this->assertCount(1, $result['x-custom']); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_task; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Output/Json/Event.php b/tests/Unit/Output/Json/Event.php index f42c25c..fa65f47 100644 --- a/tests/Unit/Output/Json/Event.php +++ b/tests/Unit/Output/Json/Event.php @@ -1,60 +1,60 @@ 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(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', md5('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']); } } diff --git a/tests/Unit/Output/Json/Task.php b/tests/Unit/Output/Json/Task.php index e08d4d7..7a5d1dc 100644 --- a/tests/Unit/Output/Json/Task.php +++ b/tests/Unit/Output/Json/Task.php @@ -1,53 +1,53 @@ element($object); $this->assertSame('10-10-10-10', $result['uid']); $this->assertSame('task title', $result['summary']); $this->assertSame('2015-04-20T14:22:18Z', $result['created']); $this->assertSame('2015-04-20T14:22:18Z', $result['dtstamp']); $this->assertSame(0, $result['sequence']); $this->assertSame('PUBLIC', $result['class']); $this->assertSame("task description\nsecond line", $result['description']); $this->assertSame(56, $result['percent-complete']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:%3Cmark.german%40example.org%3E', $result['organizer']['cal-address']); $this->assertSame('text/plain', $result['attach'][0]['parameters']['fmttype']); $this->assertSame('test.txt', $result['attach'][0]['parameters']['x-label']); $this->assertSame('cid:test.1429539738.5833.txt', $result['attach'][0]['uri']); $this->assertSame('MAPI:PidLidTaskActualEffort', $result['x-custom'][0]['identifier']); $this->assertSame('8', $result['x-custom'][0]['value']); $object = kolab_api_tests::get_data('20-20-20-20', md5('Tasks'), 'task', null, $context); $result = $output->element($object); $this->assertSame('20-20-20-20', $result['uid']); $this->assertSame('task', $result['summary']); $this->assertSame('2015-04-20', $result['dtstart']); $this->assertSame('2015-04-27', $result['due']); - $this->assertSame('DAILY', $result['rrule']['recur']); + $this->assertSame(array('freq' => 'DAILY'), $result['rrule']['recur']); $this->assertSame('NEEDS-ACTION', $result['status']); $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(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:%3Cjane.manager%40example.org%3E', $result['attendee'][0]['cal-address']); } }