diff --git a/lib/filter/mapistore.php b/lib/filter/mapistore.php index 949368e..34e2955 100644 --- a/lib/filter/mapistore.php +++ b/lib/filter/mapistore.php @@ -1,642 +1,407 @@ | +--------------------------------------------------------------------------+ | 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' => '', - 'PidNameKeywords' => 'categories', - ); - /** * 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; $this->common_action = !in_array($input->action, array('folders', 'info')); // 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; } // convert / to // or /// if ($this->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' && $this->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); } } // convert parent_id into path on object update request else if ($input->method == 'PUT' && $folder && count($input->path) == 2) { $data = $input->input(null, true); if ($data['parent_id'] && $data['parent_id'] != $folder) { $this->parent_change_handler($data); } } } /** * 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 if ($this->input->method == 'PUT' && !in_array($input->action, array('info'))) { // Mapistore expects 204 on object updates // however, we'd like to send modified UID of the object sometimes // $status = kolab_api_output::STATUS_EMPTY; } } - /** - * 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 ($mapi_idx) { - case 'PidTagCreationTime': - case 'PidTagLastModificationTime': - $result[$mapi_idx] = self::date_php2mapi($value, true); - break; - - case 'PidNameKeywords': - $result[$mapi_idx] = self::parse_categories((array) $value); - break; - } - } - } - } - - /** - * Convert common properties into kolab format - */ - 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; - - default: - if ($value) { - $result[$kolab_idx] = $value; - } - break; - } - } - } - - // Handle x-custom fields - foreach ((array) $result as $key => $value) { - if (strpos($key, 'x-custom.') === 0) { - unset($result[$key]); - $key = substr($key, 9); - - foreach ((array) $original['x-custom'] as $idx => $custom) { - if ($custom['identifier'] == $key) { - if ($value) { - $original['x-custom'][$idx]['value'] = $value; - } - else { - unset($original['x-custom'][$idx]); - } - - $x_custom_update = true; - continue 2; - } - } - - if ($value) { - $original['x-custom'][] = array( - 'identifier' => $key, - 'value' => $value, - ); - } - - $x_custom_update = true; - } - } - - if ($x_custom_update) { - $result['x-custom'] = array_values($original['x-custom']); - } - } - /** * Filter property names */ - public function attributes_filter($attrs, $type = null, $reverse = false) + protected 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; + $model = $this->get_model_class($type); + return $model->attributes_filter($attrs, $reverse); } /** * 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. // Mapistore format for this type is a float number // seconds since 1601-01-01 00:00:00 $seconds = floatval($date->format('U')) + 11644473600; /* if ($microseconds = intval($date->format('u'))) { $seconds += $microseconds/1000000; } */ return $seconds; } /** * Convert date-time from MAPI format to DateTime */ public function date_mapi2php($date) { $seconds = floatval(sprintf('%.0f', $date)); // assumes we're working with dates after 1970-01-01 $dt = new DateTime('@' . intval($seconds - 11644473600)); /* if ($microseconds = intval(($date - $seconds) * 1000000)) { $dt = new DateTime($dt->format('Y-m-d H:i:s') . '.' . $microseconds, $dt->getTimezone()); } */ return $dt; } - /** - * 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); - } - /** * Handles object parent modification (move) */ protected function parent_change_handler($data) { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $target = $data['parent_id']; $api = kolab_api::get_instance(); // move the object $api->backend->objects_move($folder, $target, array($uid)); // replace folder uid in input arguments $this->input->path[0] = $target; // exit if the rest of input is empty if (count($data) < 2) { $api->output->send_status(kolab_api_output::STATUS_EMPTY); } } } diff --git a/lib/filter/mapistore/attachment.php b/lib/filter/mapistore/attachment.php index 52dfad7..da4c1dc 100644 --- a/lib/filter/mapistore/attachment.php +++ b/lib/filter/mapistore/attachment.php @@ -1,185 +1,185 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_filter_mapistore_attachment +class kolab_api_filter_mapistore_attachment extends kolab_api_filter_mapistore_common { protected $map = array( // general props (read-only) 'PidTagAccessLevel' => '', // 0 - read-only, 1 - modify 'PidTagObjectType' => '', 'PidTagRecordKey' => '', // PtypBinary // other props 'PidTagLastModificationTime' => '', 'PidTagCreationTime' => '', 'PidTagDisplayName' => 'filename', 'PidTagAttachSize' => 'size', 'PidTagAttachNumber' => '', // @TODO: unique attachment index within a message 'PidTagAttachDataBinary' => '', // PtypBinary 'PidTagAttachDataObject' => '', // PtypBinary 'PidTagAttachMethod' => '', 'PidTagAttachLongFilename' => 'filename', // filename with extension 'PidTagAttachFilename' => '', // filename in 8.3 form 'PidTagAttachExtension' => '', 'PidTagAttachLongPathname' => '', 'PidTagAttachPathname' => '', 'PidTagAttachTag' => '', // PtypBinary 'PidTagRenderingPosition' => '', 'PidTagAttachRendering' => '', // PtypBinary 'PidTagAttachFlags' => '', 'PidTagAttachTransportName' => '', 'PidTagAttachEncoding' => '', // PtypBinary 'PidTagAttachAdditionalInformation' => '', // PtypBinary 'PidTagAttachmentLinkId' => '', 'PidTagAttachmentFlags' => '', 'PidTagAttachmentHidden' => '', // PtypBoolean 'PidTagTextAttachmentChars' => 'charset', // MIME props 'PidTagAttachMimeTag' => 'mimetype', 'PidTagAttachContentId' => 'content-id', 'PidTagAttachContentLocation' => 'content-location', 'PidTagAttachContentBase' => '', 'PidTagAttachPayloadClass' => '', 'PidTagAttachPayloadProviderGuidString' => '', 'PidNameAttachmentMacContentType' => '', 'PidNameAttachmentMacInfo' => '', // PtypBinary ); /** * Methods for PidTagAttachMethod */ protected $methods = array( 'afNone' => 0x00000001, 'afByValue' => 0x00000001, 'afByReference' => 0x00000002, 'afByReferenceOnly' => 0x00000004, 'afEmbeddedMessage' => 0x00000005, 'afStorage' => 0x00000006, ); /** * 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( 'PidTagObjectType' => 0x00000007, // mapistore REST API specific properties 'collection' => 'attachments', ); // @TODO: Set PidTagAccessLevel depending if a message is in Drafts or not // or the attachment belongs to a kolab object in writeable folder? foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } - $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); + $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } $result[$mapi_idx] = $value; } if (($pos = strrpos($data['filename'], '.')) !== false) { $result['PidTagAttachExtension'] = substr($data['filename'], $pos + 1); } // Store attachment body in base64 // @TODO: shouldn't we do this only in attachment.info request? $result['PidTagAttachDataBinary'] = $this->attachment_body($context['object'], $data, true); $result['PidTagAttachMethod'] = $this->methods['afByValue']; - kolab_api_filter_mapistore::parse_common_props($result, $data, $context); + $this->parse_common_props($result, $data, $context); $result['id'] = kolab_api_filter_mapistore::uid_encode($context['folder_uid'], $context['object_uid'], $data['id']); 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]; $result[$kolab_idx] = $value; } return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidTagAttachExtension'] = 'filename'; return $map; } /** * Get attachment body */ protected function attachment_body($object, $attachment, $encoded = false) { if (empty($object)) { return; } $api = kolab_api::get_instance(); $body = $api->backend->attachment_get($object, $attachment['id']); return $encoded ? base64_encode($body) : $body; } } diff --git a/lib/filter/mapistore/event.php b/lib/filter/mapistore/common.php similarity index 57% copy from lib/filter/mapistore/event.php copy to lib/filter/mapistore/common.php index 1aa6133..23f6cd7 100644 --- a/lib/filter/mapistore/event.php +++ b/lib/filter/mapistore/common.php @@ -1,798 +1,755 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_filter_mapistore_event +class kolab_api_filter_mapistore_common { - protected $map = array( - // common properties [MS-OXOCAL] - 'PidLidAppointmentSequence' => 'sequence', // PtypInteger32 - 'PidLidBusyStatus' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-BUSYSTATUS - 'PidLidAppointmentAuxiliaryFlags' => '', // PtypInteger32 - 'PidLidLocation' => 'location', // PtypString - 'PidLidAppointmentStartWhole' => 'dtstart', // PtypTime, UTC - 'PidLidAppointmentEndWhole' => 'dtend', // PtypTime, UTC - 'PidLidAppointmentDuration' => '', // PtypInteger32, optional - 'PidLidAppointmentSubType' => '', // PtypBoolean - 'PidLidAppointmentStateFlags' => '', // PtypInteger32 - 'PidLidResponseStatus' => '', // PtypInteger32 - 'PidLidRecurring' => '', // PtypBoolean - 'PidLidIsRecurring' => '', // PtypBoolean - 'PidLidClipStart' => '', // PtypTime - 'PidLidClipEnd' => '', // PtypTime - 'PidLidAllAttendeesString' => '', // PtypString - 'PidLidToAttendeesString' => '', // PtypString - 'PidLidCCAttendeesString' => '', // PtypString - 'PidLidNonSendableTo' => '', // PtypString - 'PidLidNonSendableCc' => '', // PtypString - 'PidLidNonSendableBcc' => '', // PtypString - 'PidLidNonSendToTrackStatus' => '', // PtypMultipleInteger32 - 'PidLidNonSendCcTrackStatus' => '', // PtypMultipleInteger32 - 'PidLidNonSendBccTrackStatus' => '', // PtypMultipleInteger32 - 'PidLidAppointmentUnsendableRecipients' => '', // PtypBinary, optional - 'PidLidAppointmentNotAllowPropose' => '', // PtypBoolean, @TODO: X-MICROSOFT-CDO-DISALLOW-COUNTER - 'PidLidGlobalObjectId' => '', // PtypBinary - 'PidLidCleanGlobalObjectId' => '', // PtypBinary - 'PidTagOwnerAppointmentId' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-OWNERAPPTID - 'PidTagStartDate' => '', // PtypTime - 'PidTagEndDate' => '', // PtypTime - 'PidLidCommonStart' => '', // PtypTime - 'PidLidCommonEnd' => '', // PtypTime - 'PidLidOwnerCriticalChange' => '', // PtypTime, @TODO: X-MICROSOFT-CDO-CRITICAL-CHANGE - 'PidLidIsException' => '', // PtypBoolean - 'PidTagResponseRequested' => '', // PtypBoolean - 'PidTagReplyRequested' => '', // PtypBoolean - 'PidLidTimeZoneStruct' => '', // PtypBinary - 'PidLidTimeZoneDescription' => '', // PtypString - 'PidLidAppointmentTimeZoneDefinitionRecur' => '', // PtypBinary - 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => '', // PtypBinary - 'PidLidAppointmentTimeZoneDefinitionEndDisplay' => '', // PtypBinary - 'PidLidAppointmentRecur' => '', // PtypBinary - 'PidLidRecurrenceType' => '', // PtypInteger32 - 'PidLidRecurrencePattern' => '', // PtypString - 'PidLidLinkedTaskItems' => '', // PtypMultipleBinary - 'PidLidMeetingWorkspaceUrl' => '', // PtypString - 'PidTagIconIndex' => '', // PtypInteger32 - 'PidLidAppointmentColor' => '', // PtypInteger32 - 'PidLidAppointmentReplyTime' => '', // @TODO: X-MICROSOFT-CDO-REPLYTIME - 'PidLidIntendedBusyStatus' => '', // @TODO: X-MICROSOFT-CDO-INTENDEDSTATUS - // calendar object properties [MS-OXOCAL] - 'PidTagMessageClass' => '', - 'PidLidSideEffects' => '', // PtypInteger32 - 'PidLidFExceptionAttendees' => '', // PtypBoolean - 'PidLidClientIntent' => '', // PtypInteger32 - // common props [MS-OXCMSG] - 'PidTagSubject' => 'summary', - 'PidTagBody' => 'description', - 'PidTagHtml' => '', // @TODO: (?) - 'PidTagNativeBody' => '', - 'PidTagBodyHtml' => '', - 'PidTagRtfCompressed' => '', - 'PidTagInternetCodepage' => '', - 'PidTagContentId' => '', - 'PidTagBodyContentLocation' => '', - 'PidTagImportance' => 'priority', - 'PidTagSensitivity' => 'class', - 'PidLidPrivate' => '', - - 'PidTagCreationTime' => 'created', - 'PidTagLastModificationTime' => 'dtstamp', - // reminder properties [MS-OXORMDR] - 'PidLidReminderSet' => '', // PtypBoolean - 'PidLidReminderSignalTime' => '', // PtypTime - 'PidLidReminderDelta' => '', // PtypInteger32 - 'PidLidReminderTime' => '', // PtypTime - 'PidLidReminderOverride' => '', // PtypBoolean - 'PidLidReminderPlaySound' => '', // PtypBoolean - 'PidLidReminderFileParameter' => '', // PtypString - 'PidTagReplyTime' => '', // PtypTime - 'PidLidReminderType' => '', // PtypInteger32 + // Common properties [MS-OXCMSG] + protected static $common_map = array( +// 'PidTagAccess' => '', +// 'PidTagAccessLevel' => '', // 0 - read-only, 1 - modify +// 'PidTagChangeKey' => '', + 'PidTagCreationTime' => 'creation-date', // PtypTime, UTC + 'PidTagLastModificationTime' => 'last-modification-date', // PtypTime, UTC +// 'PidTagLastModifierName' => '', +// 'PidTagObjectType' => '', // @TODO +// 'PidTagHasAttachments' => '', // @TODO +// 'PidTagRecordKey' => '', +// 'PidTagSearchKey' => '', + 'PidNameKeywords' => 'categories', ); protected $recipient_track_status_map = array( 'TENTATIVE' => 0x00000002, 'ACCEPTED' => 0x00000003, 'DECLINED' => 0x00000004, ); protected $recipient_type_map = array( 'NON-PARTICIPANT' => 0x00000004, 'OPT-PARTICIPANT' => 0x00000002, - 'REQ-PARTICIPANT' => 0x00000002, + 'REQ-PARTICIPANT' => 0x00000001, '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 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 + * Extracts data from kolab data array */ - public function output($data, $context = null) + public static function get_kolab_value($data, $name) { - $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; + $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']; + } } - $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); + return null; + } - if ($value === null) { - continue; + for ($i = 1; $i < $count; $i++) { + if (!is_array($value)) { + return null; } - switch ($mapi_idx) { - case 'PidTagSensitivity': - $value = (int) $this->sensitivity[strtolower($value)]; - break; - - case 'PidTagCreationTime': - case 'PidTagLastModificationTime': - $value = kolab_api_filter_mapistore::date_php2mapi($value, true); - break; + list($key, $num) = explode(':', $name_items[$i]); + $value = $value[$key]; - case 'PidTagImportance': - $value = (int) $this->importance[(int) $value]; - break; + if ($num !== null && $value !== null) { + $value = is_array($value) ? $value[$num] : null; + } + } - case 'PidLidAppointmentStartWhole': - case 'PidLidAppointmentEndWhole': - $dt = kolab_api_input_json::to_datetime($value); - $value = kolab_api_filter_mapistore::date_php2mapi($dt, true); + return $value; + } - // PidLidAppointmentTimeZoneDefinitionStartDisplay - // PidLidAppointmentTimeZoneDefinitionEndDisplay + /** + * 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; + } - // this is all-day event - if ($dt->_dateonly) { - $result['PidLidAppointmentSubType'] = 0x00000001; + if ($count > 1) { + for ($i = 0; $i < $count - 1; $i++) { + $key = $name_items[$i]; + if (!array_key_exists($key, $element)) { + $element[$key] = array(); } - break; + $element = &$element[$key]; } - - $result[$mapi_idx] = $value; } - // Organizer - if (!empty($data['organizer'])) { - $this->add_attendee_to_result($data['organizer'], $result, true); + $element[$name_items[$count - 1]] = $value; + } + + /** + * Parse common properties in object data (convert into MAPI format) + */ + protected static function parse_common_props(&$result, $data, $context = array()) + { + if (empty($context)) { + // @TODO: throw exception? + return; } - // Attendees [MS-OXCICAL 2.1.3.1.1.20.2] - foreach ((array) $data['attendee'] as $attendee) { - $this->add_attendee_to_result($attendee, $result); + if ($data['uid'] && $context['folder_uid']) { + $result['id'] = kolab_api_filter_mapistore::uid_encode($context['folder_uid'], $data['uid']); } - // 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; - } + if ($context['folder_uid']) { + $result['parent_id'] = $context['folder_uid']; } - // @TODO: PidLidAppointmentDuration - // @TODO: exceptions, resources + foreach (self::$common_map as $mapi_idx => $kolab_idx) { + if (!isset($result[$mapi_idx]) && ($value = $data[$kolab_idx]) !== null) { + switch ($mapi_idx) { + case 'PidTagCreationTime': + case 'PidTagLastModificationTime': + $result[$mapi_idx] = kolab_api_filter_mapistore::date_php2mapi($value, true); + break; - // Recurrence rule - if (!empty($data['rrule']) && !empty($data['rrule']['recur'])) { - if ($rule = self::recurrence_from_kolab($data['rrule']['recur'], $result)) { - $result['PidLidAppointmentRecur'] = $rule; + case 'PidNameKeywords': + $result[$mapi_idx] = self::parse_categories((array) $value); + break; + } } } - - 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 + * Convert common properties into kolab format */ - public function input($data, $object = null) + protected static function convert_common_props(&$result, $data, $original) { - $result = array(); - - foreach ($this->map as $mapi_idx => $kolab_idx) { - if (empty($kolab_idx)) { - continue; - } - - if (!array_key_exists($mapi_idx, $data)) { - continue; + // @TODO: id, parent_id? + foreach (self::$common_map as $mapi_idx => $kolab_idx) { + if (array_key_exists($mapi_idx, $data) && !array_key_exists($kolab_idx, $result)) { + $value = $data[$mapi_idx]; + + switch ($mapi_idx) { + case 'PidTagCreationTime': + case 'PidTagLastModificationTime': + if ($value) { + $dt = kolab_api_filter_mapistore::date_mapi2php($value); + $result[$kolab_idx] = $dt->format('Y-m-d\TH:i:s\Z'); + } + break; + + default: + if ($value) { + $result[$kolab_idx] = $value; + } + break; + } } + } - $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'); + // 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; + } } - 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); + $original['x-custom'][] = array( + 'identifier' => $key, + 'value' => $value, + ); } - break; - } - $result[$kolab_idx] = $value; + $x_custom_update = true; + } } - // 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(); + if ($x_custom_update) { + $result['x-custom'] = array_values($original['x-custom']); } + } - // Recurrence - if (array_key_exists('PidLidAppointmentRecur', $data)) { - $result['rrule']['recur'] = $this->recurrence_to_kolab($data['PidLidAppointmentRecur']); - } + /** + * Filter property names with mapping (kolab <> MAPI) + * + * @param array $attrs Property names + * @param bool $reverse Reverse mapping + * + * @return array Property names + */ + public function attributes_filter($attrs, $reverse = false) + { + $map = array_merge(self::$common_map, $this->map()); + $result = array(); - // @TODO: PidLidAppointmentDuration (?) - // @TODO: exceptions, resources, attendees + // add some special common attributes + $map['PidTagMessageClass'] = 'PidTagMessageClass'; + $map['collection'] = 'collection'; + $map['id'] = 'uid'; - kolab_api_filter_mapistore::convert_common_props($result, $data, $object); + foreach ($attrs as $attr) { + if ($reverse) { + if ($name = array_search($attr, $map)) { + $result[] = $name; + } + } + else if ($name = $map[$attr]) { + $result[] = $name; + } + } return $result; } /** - * Returns the attributes names mapping + * Return properties map */ - public function map() + protected function map() { - $map = array_filter($this->map); - - // @TODO: add properties that are not in the map - $map['PidLidAppointmentRecur'] = 'rrule'; - - return $map; + return array(); } /** - * Setting PidTagRecipientType according to [MS-OXCICAL 2.1.3.1.1.20.2] + * Parse categories according to [MS-OXCICAL 2.1.3.1.1.20.3] + * + * @param array Categories + * + * @return array Categories */ - protected function to_recipient_type($cutype, $role) + public static function parse_categories($categories) { - if ($cutype && in_array($cutype, array('RESOURCE', 'ROOM'))) { - return 0x00000003; + if (!is_array($categories)) { + return; } - if ($role && ($type = $this->recipient_type_map[$role])) { - return $type; + $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 0x00000001; + return array_values($result); } /** * Convert Kolab 'attendee' specification into MAPI recipient * and add it to the result */ - protected function add_attendee_to_result($attendee, &$result, $is_organizer = false) + public function attendee_to_recipient($attendee, &$result, $is_organizer = false) { $email = $attendee['cal-address']; $params = (array) $attendee['parameters']; // parse mailto string if (strpos($email, 'mailto:') === 0) { $email = urldecode(substr($email, 7)); } $emails = rcube_mime::decode_address_list($email, 1); if (!empty($email)) { $email = $emails[key($emails)]; $recipient = array( 'PidTagAddressType' => 'SMTP', 'PidTagDisplayName' => $params['cn'] ?: $email['name'], 'PidTagDisplayType' => 0, 'PidTagEmailAddress' => $email['mailto'], ); if ($is_organizer) { $recipient['PidTagRecipientFlags'] = 0x00000003; $recipient['PidTagRecipientType'] = 0x00000001; } else { $recipient['PidTagRecipientFlags'] = 0x00000001; $recipient['PidTagRecipientTrackStatus'] = (int) $this->recipient_track_status_map[$params['partstat']]; $recipient['PidTagRecipientType'] = $this->to_recipient_type($params['cutype'], $params['role']); } - // 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 + * Convert MAPI recipient into Kolab attendee */ - protected static function reminder_delta_to_duration($delta) + public function recipient_to_attendee($recipient, &$result) { - if ($delta == 0x5AE980E1) { - $delta = 15; - } + if ($email = $recipient['PidTagEmailAddress']) { + $mailto = 'mailto:' . rawurlencode($email); + + $attendee = array( + 'cal-address' => $mailto, + 'parameters' => array( + 'cn' => $recipient['PidTagDisplayName'] ?: $recipient['PidTagRecipientDisplayName'], + ), + ); - $delta = (int) $delta; + if ($recipient['PidTagRecipientFlags'] == 0x00000003) { + $result['organizer'] = $attendee; + } + else { + switch ($recipient['PidTagRecipientType']) { + case 0x00000004: + $role = 'NON-PARTICIPANT'; + break; + + case 0x00000003: + $cutype = 'RESOURCE'; + break; + + case 0x00000002: + $role = 'OPT-PARTICIPANT'; + break; + + case 0x00000001: + $role = 'REQ-PARTICIPANT'; + break; + } - return "-PT{$delta}M"; - } + $map = array_flip($this->recipient_track_status_map); + $partstat = $map[$recipient['PidTagRecipientTrackStatus']] ?: 'NEEDS-ACTION'; - /** - * 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; - } + // @TODO: rsvp? + $attendee['parameters']['cutype'] = $cutype; + $attendee['parameters']['role'] = $role; + $attendee['parameters']['partstat'] = $partstat; - return $value; + $result['attendee'][] = $attendee; + } } } /** * 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 */ public static function recurrence_from_kolab($rule, $object = array(), $type = 'event') { $result = array( 'Period' => $rule['interval'] ? $rule['interval'] : 1, 'FirstDOW' => self::day2bitmask($rule['wkst'] ?: 'MO'), 'OccurrenceCount' => 0x0000000A, 'EndDate' => 0x5AE980DF, 'CalendarType' => kolab_api_filter_mapistore_structure_recurrencepattern::CALENDARTYPE_DEFAULT, // DeletedInstanceDates // ModifiedInstanceDates ); // Get event/task start date for FirstDateTime calculations if ($object['PidLidAppointmentStartWhole']) { $startdate = kolab_api_filter_mapistore::date_mapi2php($object['PidLidAppointmentStartWhole']); $result['StartDate'] = intval($object['PidLidAppointmentStartWhole'] / 10000000 / 60); } else if ($object['PidLidCommonStart']) { $startdate = kolab_api_filter_mapistore::date_mapi2php($object['PidLidCommonStart']); $result['StartDate'] = intval($object['PidLidCommonStart'] / 10000000 / 60); } else { rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Found recurring $type without start date, skipping recurrence", ), true, false); return; } // $startdate->setTime(0, 0, 0); // @TODO: // StartDate: Set to the date portion of DTSTART, in the time zone specified // by PidLidTimeZoneStruct. This date is stored in minutes after // midnight Jan 1, 1601. Note that this value MUST always be // evenly divisible by 1440. // EndDate: Set to the start date of the last instance of a recurrence, in the // time zone specified by PidLidTimeZoneStruct. This date is // stored in minutes after midnight January 1, 1601. If the // recurrence is infinite, set EndDate to 0x5AE980DF. Note that // this value MUST always be evenly divisible by 1440, except for // the special value 0x5AE980DF. // @TODO: get first occurrence of the event using libcalendaring_recurrence class ? switch ($rule['freq']) { case 'DAILY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY; $result['Period'] *= 1440; break; case 'WEEKLY': // if BYDAY does not exist use day from DTSTART if (empty($rule['byday'])) { $rule['byday'] = strtoupper($startdate->format('S')); } $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_WEEKLY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK; $result['PatternTypeSpecific'] = self::day2bitmask($rule['byday'], 'BYDAY-'); break; case 'MONTHLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY; if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH; $result['PatternTypeSpecific'] = $month_day == -1 ? 0x0000001F : $month_day; } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; case 'YEARLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY; $result['Period'] *= 12; // MAPI doesn't support multi-valued months if ($rule['bymonth']) { // @TODO: set $startdate } if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'] = array(0, $month_day == -1 ? 0x0000001F : $month_day); } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; } if (!empty($rule['until'])) { $result['EndDate'] = intval(kolab_api_filter_mapistore::date_php2mapi($rule['until']) / 10000000 / 60); // @TODO: calculate OccurrenceCount? $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER; } else if (!empty($rule['count'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurrenceCount'] = $rule['count']; // @TODO: set EndDate } else { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER; } $result['FirstDateTime'] = self::date_minutes_diff($startdate); $result = 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 static function day2bitmask($days, $prefix = '') { $days = explode(',', $days); $result = 0; foreach ($days as $day) { $result = $result + self::$recurrence_day_map[$prefix.$day]; } return $result; } /** * Convert bitmask used by MAPI to string of days (TU,TH) * * @param int $days * * @return string */ protected static function bitmask2day($days) { $days_arr = array(); foreach (self::$recurrence_day_map as $day => $bit) { if ($days & $bit) { $days_arr[] = preg_replace('/^[A-Z-]+/', '', $day); } } $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); } + + /** + * Setting PidTagRecipientType according to [MS-OXCICAL 2.1.3.1.1.20.2] + */ + protected function to_recipient_type($cutype, $role) + { + if ($cutype && in_array($cutype, array('RESOURCE', 'ROOM'))) { + return 0x00000003; + } + + if ($role && ($type = $this->recipient_type_map[$role])) { + return $type; + } + + return 0x00000001; + } } diff --git a/lib/filter/mapistore/contact.php b/lib/filter/mapistore/contact.php index 3457842..14152e6 100644 --- a/lib/filter/mapistore/contact.php +++ b/lib/filter/mapistore/contact.php @@ -1,580 +1,580 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_filter_mapistore_contact +class kolab_api_filter_mapistore_contact extends kolab_api_filter_mapistore_common { protected $map = array( // contact name properties [MS-OXOCNTC] 'PidTagNickname' => 'nickname', 'PidTagGeneration' => 'n.suffix', 'PidTagDisplayNamePrefix' => 'n.prefix', 'PidTagSurname' => 'n.surname', 'PidTagMiddleName' => 'n.additional', 'PidTagGivenName' => 'n.given', 'PidTagInitials' => 'x-custom.MAPI:PidTagInitials', 'PidTagDisplayName' => 'fn', 'PidLidYomiFirstName' => '', 'PidLidYomiLastName' => '', 'PidLidFileUnder' => '', 'PidLidFileUnderId' => '', // PtypInteger32 'PidLidFileUnderList' => '', // PtypMultipleInteger32 // electronic and phisical address properties 'PidTagPrimaryFaxNumber' => 'x-custom.MAPI:PidTagPrimaryFaxNumber', 'PidTagBusinessFaxNumber' => 'x-custom.MAPI:PidTagBusinessFaxNumber', 'PidTagHomeFaxNumber' => '', 'PidTagHomeAddressStreet' => '', 'PidTagHomeAddressCity' => '', 'PidTagHomeAddressStateOrProvince' => '', 'PidTagHomeAddressPostalCode' => '', 'PidTagHomeAddressCountry' => '', 'PidLidHomeAddressCountryCode' => '', 'PidTagHomeAddressPostOfficeBox' => '', 'PidLidHomeAddress' => '', // @TODO: ? 'PidLidWorkAddressStreet' => '', 'PidLidWorkAddressCity' => '', 'PidLidWorkAddressState' => '', 'PidLidWorkAddressPostalCode' => '', 'PidLidWorkAddressCountry' => '', 'PidLidWorkAddressCountryCode' => '', 'PidLidWorkAddressPostOfficeBox' => '', 'PidLidWorkAddress' => '', // @TODO: ? 'PidTagOtherAddressStreet' => '', 'PidTagOtherAddressCity' => '', 'PidTagOtherAddressStateOrProvince' => '', 'PidTagOtherAddressPostalCode' => '', 'PidTagOtherAddressCountry' => '', 'PidLidOtherAddressCountryCode' => '', 'PidTagOtherAddressPostOfficeBox' => '', 'PidLidOtherAddress' => '', // @TODO: ? 'PidTagStreetAddress' => '', // @TODO: ? 'PidTagLocality' => '', // @TODO: ? 'PidTagStateOrProvince' => '', // @TODO: ? 'PidTagPostalCode' => '', // @TODO: ? 'PidTagCountry' => '', // @TODO: ? 'PidLidAddressCountryCode' => '', // @TODO: ? 'PidTagPostOfficeBox' => '', // @TODO: ? 'PidTagPostalAddress' => '', // @TODO: ? 'PidTagPagerTelephoneNumber' => '', 'PidTagCallbackTelephoneNumber' => '', 'PidTagBusinessTelephoneNumber' => '', 'PidTagHomeTelephoneNumber' => '', 'PidTagPrimaryTelephoneNumber' => '', 'PidTagBusiness2TelephoneNumber' => '', 'PidTagMobileTelephoneNumber' => '', 'PidTagRadioTelephoneNumber' => '', 'PidTagCarTelephoneNumber' => '', 'PidTagOtherTelephoneNumber' => '', 'PidTagAssistantTelephoneNumber' => '', 'PidTagHome2TelephoneNumber' => 'x-custom.MAPI:PidTagHome2TelephoneNumber', 'PidTagTelecommunicationsDeviceForDeafTelephoneNumber' => 'x-custom.MAPI:PidTagTelecommunicationsDeviceForDeafTelephoneNumber', 'PidTagCompanyMainTelephoneNumber' => 'x-custom.MAPI:PidTagCompanyMainTelephoneNumber', 'PidTagTelexNumber' => '', 'PidTagIsdnNumber' => '', 'PidTagBirthday' => 'bday', // PtypTime, UTC 'PidLidBirthdayLocal' => '', // PtypTime 'PidTagWeddingAnniversary' => 'anniversary', // PtypTime, UTC 'PidLidWeddingAnniversaryLocal' => '', // PtypTime // professional properties 'PidTagTitle' => 'title', 'PidTagCompanyName' => '', 'PidLidYomiCompanyName' => '', 'PidTagDepartmentName' => '', 'PidTagOfficeLocation' => 'x-custom.MAPI:PidTagOfficeLocation', 'PidTagManagerName' => '', 'PidTagAssistant' => '', 'PidTagProfession' => 'group.role', 'PidLidHasPicture' => '', // PtypBoolean, more about photo attachments in MS-OXOCNTC // other properties 'PidTagHobbies' => 'x-custom.MAPI:PidTagHobbies', 'PidTagSpouseName' => '', 'PidTagLanguage' => 'lang', 'PidTagLocation' => 'x-custom.MAPI:PidTagLocation', 'PidLidInstantMessagingAddress' => 'impp', 'PidTagOrganizationalIdNumber' => 'x-custom.MAPI:PidTagOrganizationalIdNumber', 'PidTagCustomerId' => 'x-custom.MAPI:PidTagCustomerId', 'PidTagGovernmentIdNumber' => 'x-custom.MAPI:PidTagGovernmentIdNumber', 'PidTagPersonalHomePage' => 'url', 'PidTagBusinessHomePage' => 'x-custom.MAPI:PidTagBussinessHomePage', 'PidTagFtpSite' => 'x-custom.MAPI:PidTagFtpSite', 'PidLidFreeBusyLocation' => 'fburl', 'PidTagChildrenNames' => '', // PtypMultipleString 'PidTagGender' => 'gender', 'PidTagUserX509Certificate' => 'key', // PtypMultipleBinary 'PidTagMessageClass' => '', // IPM.Contact, IPM.DistList 'PidTagBody' => 'note', 'PidTagLastModificationTime' => 'rev', /* // distribution lists [MS-OXOCNTC] 'PidLidDistributionListName' => '', // PtypString = PidTagDisplayName 'PidLidDistributionListMembers' => '', // PtypMultipleBinary 'PidLidDistributionListOneOffMembers' => '', // PtypMultipleBinary 'PidLidDistributionListChecksum' => '', // PtypInteger32 'PidLidDistributionListStream' => '', // PtypBinary */ ); protected $gender_map = array( 0 => '', 1 => 'F', 2 => 'M', ); protected $phone_map = array( 'PidTagPagerTelephoneNumber' => 'pager', 'PidTagBusinessTelephoneNumber' => 'work', 'PidTagHomeTelephoneNumber' => 'home', 'PidTagMobileTelephoneNumber' => 'cell', 'PidTagCarTelephoneNumber' => 'x-car', 'PidTagOtherTelephoneNumber' => 'textphone', 'PidTagBusinessFaxNumber' => 'faxwork', 'PidTagHomeFaxNumber' => 'faxhome', ); protected $email_map = array( 'PidLidEmail1EmailAddress' => 'home', 'PidLidEmail2EmailAddress' => 'work', 'PidLidEmail3EmailAddress' => 'other', ); /** * 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( // @TODO: IPM.DistList for groups 'PidTagMessageClass' => 'IPM.Contact', // mapistore REST API specific properties 'collection' => 'contacts', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } - $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); + $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagGender': $value = (int) array_search($value, $this->gender_map); break; case 'PidTagPersonalHomePage': case 'PidLidInstantMessagingAddress': if (is_array($value)) { $value = $value[0]; } break; case 'PidTagBirthday': case 'PidTagWeddingAnniversary': case 'PidTagLastModificationTime': $value = kolab_api_filter_mapistore::date_php2mapi($value, false); break; case 'PidTagUserX509Certificate': foreach ((array) $value as $val) { if ($val && preg_match('|^data:application/pkcs7-mime;base64,|i', $val, $m)) { $result[$mapi_idx] = substr($val, strlen($m[0])); continue 3; } } $value = null; break; case 'PidTagTitle': if (is_array($value)) { $value = $value[0]; } break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // contact photo attachment [MS-OXVCARD 2.1.3.2.4] if (!empty($data['photo'])) { // @TODO: check if photo is one of .bmp, .gif, .jpeg, .png // Photo in MAPI is handled as attachment // Set PidTagAttachmentContactPhoto=true on attachment object $result['PidLidHasPicture'] = true; } // Organization/Department $organization = $data['group']['org']; if (is_array($organization)) { $result['PidTagCompanyName'] = $organization[0]; $result['PidTagDepartmentName'] = $organization[1]; } else if ($organization !== null) { $result['PidTagCompanyName'] = $organization; } // Manager/Assistant $related = $data['group']['related']; if ($related && $related['parameters']) { $related = array($related); } foreach ((array) $related as $rel) { $type = $rel['parameters']['type']; if ($type == 'x-manager') { $result['PidTagManagerName'] = $rel['text']; } else if ($type == 'x-assistant') { $result['PidTagAssistant'] = $rel['text']; } } // Children, Spouse foreach ((array) $data['related'] as $rel) { $type = $rel['parameters']['type']; if ($type == 'child') { $result['PidTagChildrensNames'][] = $rel['text']; } else if ($type == 'spouse') { $result['PidTagSpouseName'] = $rel['text']; } } // Emails $email_map = array_flip($this->email_map); foreach ((array) $data['email'] as $email) { $type = is_array($email) ? $email['parameters']['type'] : 'other'; $key = $email_map[$type] ?: $email_map['other']; $result[$key] = is_array($email) ? $email['text'] : $email; } // Phone(s) $phone_map = array_flip($this->phone_map); $phones = $data['tel']; if ($phones && $phones['parameters']) { $phones = array($phones); } foreach ((array) $phones as $phone) { $type = implode('', (array)$phone['parameters']['type']); if ($phone['text'] && ($idx = $phone_map[$type])) { $result[$idx] = $phone['text']; } } // Addresses(s) $addresses = $data['adr']; if ($addresses && $addresses['parameters']) { $addresses = array($addresses); } foreach ((array) $addresses as $addr) { $type = $addr['parameters']['type']; $address = null; if ($type == 'home') { $address = array( 'PidTagHomeAddressStreet' => $addr['street'], 'PidTagHomeAddressCity' => $addr['locality'], 'PidTagHomeAddressStateOrProvince' => $addr['region'], 'PidTagHomeAddressPostalCode' => $addr['code'], 'PidTagHomeAddressCountry' => $addr['country'], 'PidTagHomeAddressPostOfficeBox' => $addr['pobox'], ); } else if ($type == 'work') { $address = array( 'PidLidWorkAddressStreet' => $addr['street'], 'PidLidWorkAddressCity' => $addr['locality'], 'PidLidWorkAddressState' => $addr['region'], 'PidLidWorkAddressPostalCode' => $addr['code'], 'PidLidWorkAddressCountry' => $addr['country'], 'PidLidWorkAddressPostOfficeBox' => $addr['pobox'], ); } if (!empty($address)) { $result = array_merge($result, array_filter($address)); } } $other_adr_map = array( 'street' => 'PidTagOtherAddressStreet', 'locality' => 'PidTagOtherAddressCity', 'region' => 'PidTagOtherAddressStateOrProvince', 'code' => 'PidTagOtherAddressPostalCode', 'country' => 'PidTagOtherAddressCountry', 'pobox' => 'PidTagOtherAddressPostOfficeBox', ); foreach ((array) $data['group']['adr'] as $idx => $value) { if ($value && ($key = $other_adr_map[$idx])) { $result[$key] = $value; } } - kolab_api_filter_mapistore::parse_common_props($result, $data, $context); + $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagBirthday': case 'PidTagWeddingAnniversary': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidTagLastModificationTime': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidTagGender': $value = $this->gender_map[(int)$value]; break; case 'PidTagUserX509Certificate': if (!empty($value)) { $value = array('data:application/pkcs7-mime;base64,' . $value); } break; case 'PidTagPersonalHomePage': case 'PidLidInstantMessagingAddress': if (!empty($value)) { $value = array($value); } break; } - kolab_api_filter_mapistore::set_kolab_value($result, $kolab_idx, $value); + $this->set_kolab_value($result, $kolab_idx, $value); } // MS-OXVCARD 2.1.3.2.1 if (!empty($data['PidTagNormalizedSubject']) && empty($data['PidTagDisplayName'])) { $result['fn'] = $data['PidTagNormalizedSubject']; } // Organization/Department if ($data['PidTagCompanyName']) { $result['group']['org'][] = $data['PidTagCompanyName']; } if (!empty($data['PidTagDepartmentName'])) { $result['group']['org'][] = $data['PidTagDepartmentName']; } // Manager if ($data['PidTagManagerName']) { $result['group']['related'][] = array( 'parameters' => array('type' => 'x-manager'), 'text' => $data['PidTagManagerName'], ); } // Assistant if ($data['PidTagAssistant']) { $result['group']['related'][] = array( 'parameters' => array('type' => 'x-assistant'), 'text' => $data['PidTagAssistant'], ); } // Spouse if ($data['PidTagSpouseName']) { $result['related'][] = array( 'parameters' => array('type' => 'spouse'), 'text' => $data['PidTagSpouseName'], ); } // Children foreach ((array) $data['PidTagChildrensNames'] as $child) { $result['related'][] = array( 'parameters' => array('type' => 'child'), 'text' => $child, ); } // Emails foreach ($this->email_map as $mapi_idx => $type) { if ($email = $data[$mapi_idx]) { $result['email'][] = array( 'parameters' => array('type' => $type), 'text' => $email, ); } } // Phone(s) foreach ($this->phone_map as $mapi_idx => $type) { if (array_key_exists($mapi_idx, $data)) { // first remove the old phone... if (!empty($object['tel'])) { foreach ($object['tel'] as $idx => $phone) { $pt = implode('', (array) $phone['parameters']['type']); if ($pt == $type) { unset($object['tel'][$idx]); } } } if ($tel = $data[$mapi_idx]) { if (preg_match('/^fax(work|home)$/', $type, $m)) { $type = array('fax', $m[1]); } // and add it to the list $result['tel'][] = array( 'parameters' => array('type' => $type), 'text' => $tel, ); } } } if (!empty($object['tel'])) { $result['tel'] = array_merge((array) $result['tel'], (array) $object['tel']); } // Home address $address = array(); $adr_map = array( 'PidTagHomeAddressStreet' => 'street', 'PidTagHomeAddressCity' => 'locality', 'PidTagHomeAddressStateOrProvince' => 'region', 'PidTagHomeAddressPostalCode' => 'code', 'PidTagHomeAddressCountry' => 'country', 'PidTagHomeAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array('parameters' => array('type' => 'home')); $result['adr'][] = array_merge($address, $type); } // Work address $address = array(); $adr_map = array( 'PidLidWorkAddressStreet' => 'street', 'PidLidWorkAddressCity' => 'locality', 'PidLidWorkAddressState' => 'region', 'PidLidWorkAddressPostalCode' => 'code', 'PidLidWorkAddressCountry' => 'country', 'PidLidWorkAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array('parameters' => array('type' => 'work')); $result['adr'][] = array_merge($address, $type); } // Office address $address = array(); $adr_map = array( 'PidTagOtherAddressStreet' => 'street', 'PidTagOtherAddressCity' => 'locality', 'PidTagOtherAddressStateOrProvince' => 'region', 'PidTagOtherAddressPostalCode' => 'code', 'PidTagOtherAddressCountry' => 'country', 'PidTagOtherAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $result['group']['adr'] = array_merge($address, $type); } - kolab_api_filter_mapistore::convert_common_props($result, $data, $object); + $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } } diff --git a/lib/filter/mapistore/event.php b/lib/filter/mapistore/event.php index 1aa6133..fce154f 100644 --- a/lib/filter/mapistore/event.php +++ b/lib/filter/mapistore/event.php @@ -1,798 +1,380 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_filter_mapistore_event +class kolab_api_filter_mapistore_event extends kolab_api_filter_mapistore_common { protected $map = array( // common properties [MS-OXOCAL] 'PidLidAppointmentSequence' => 'sequence', // PtypInteger32 'PidLidBusyStatus' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-BUSYSTATUS 'PidLidAppointmentAuxiliaryFlags' => '', // PtypInteger32 'PidLidLocation' => 'location', // PtypString 'PidLidAppointmentStartWhole' => 'dtstart', // PtypTime, UTC 'PidLidAppointmentEndWhole' => 'dtend', // PtypTime, UTC 'PidLidAppointmentDuration' => '', // PtypInteger32, optional 'PidLidAppointmentSubType' => '', // PtypBoolean 'PidLidAppointmentStateFlags' => '', // PtypInteger32 'PidLidResponseStatus' => '', // PtypInteger32 'PidLidRecurring' => '', // PtypBoolean 'PidLidIsRecurring' => '', // PtypBoolean 'PidLidClipStart' => '', // PtypTime 'PidLidClipEnd' => '', // PtypTime 'PidLidAllAttendeesString' => '', // PtypString 'PidLidToAttendeesString' => '', // PtypString 'PidLidCCAttendeesString' => '', // PtypString 'PidLidNonSendableTo' => '', // PtypString 'PidLidNonSendableCc' => '', // PtypString 'PidLidNonSendableBcc' => '', // PtypString 'PidLidNonSendToTrackStatus' => '', // PtypMultipleInteger32 'PidLidNonSendCcTrackStatus' => '', // PtypMultipleInteger32 'PidLidNonSendBccTrackStatus' => '', // PtypMultipleInteger32 'PidLidAppointmentUnsendableRecipients' => '', // PtypBinary, optional 'PidLidAppointmentNotAllowPropose' => '', // PtypBoolean, @TODO: X-MICROSOFT-CDO-DISALLOW-COUNTER 'PidLidGlobalObjectId' => '', // PtypBinary 'PidLidCleanGlobalObjectId' => '', // PtypBinary 'PidTagOwnerAppointmentId' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-OWNERAPPTID 'PidTagStartDate' => '', // PtypTime 'PidTagEndDate' => '', // PtypTime 'PidLidCommonStart' => '', // PtypTime 'PidLidCommonEnd' => '', // PtypTime 'PidLidOwnerCriticalChange' => '', // PtypTime, @TODO: X-MICROSOFT-CDO-CRITICAL-CHANGE 'PidLidIsException' => '', // PtypBoolean 'PidTagResponseRequested' => '', // PtypBoolean 'PidTagReplyRequested' => '', // PtypBoolean 'PidLidTimeZoneStruct' => '', // PtypBinary 'PidLidTimeZoneDescription' => '', // PtypString 'PidLidAppointmentTimeZoneDefinitionRecur' => '', // PtypBinary 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => '', // PtypBinary 'PidLidAppointmentTimeZoneDefinitionEndDisplay' => '', // PtypBinary 'PidLidAppointmentRecur' => '', // PtypBinary 'PidLidRecurrenceType' => '', // PtypInteger32 'PidLidRecurrencePattern' => '', // PtypString 'PidLidLinkedTaskItems' => '', // PtypMultipleBinary 'PidLidMeetingWorkspaceUrl' => '', // PtypString 'PidTagIconIndex' => '', // PtypInteger32 'PidLidAppointmentColor' => '', // PtypInteger32 'PidLidAppointmentReplyTime' => '', // @TODO: X-MICROSOFT-CDO-REPLYTIME 'PidLidIntendedBusyStatus' => '', // @TODO: X-MICROSOFT-CDO-INTENDEDSTATUS // calendar object properties [MS-OXOCAL] 'PidTagMessageClass' => '', 'PidLidSideEffects' => '', // PtypInteger32 'PidLidFExceptionAttendees' => '', // PtypBoolean 'PidLidClientIntent' => '', // PtypInteger32 // common props [MS-OXCMSG] 'PidTagSubject' => 'summary', 'PidTagBody' => 'description', 'PidTagHtml' => '', // @TODO: (?) 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagContentId' => '', 'PidTagBodyContentLocation' => '', 'PidTagImportance' => 'priority', 'PidTagSensitivity' => 'class', 'PidLidPrivate' => '', 'PidTagCreationTime' => 'created', 'PidTagLastModificationTime' => 'dtstamp', // reminder properties [MS-OXORMDR] 'PidLidReminderSet' => '', // PtypBoolean 'PidLidReminderSignalTime' => '', // PtypTime 'PidLidReminderDelta' => '', // PtypInteger32 'PidLidReminderTime' => '', // PtypTime 'PidLidReminderOverride' => '', // PtypBoolean 'PidLidReminderPlaySound' => '', // PtypBoolean 'PidLidReminderFileParameter' => '', // PtypString 'PidTagReplyTime' => '', // PtypTime 'PidLidReminderType' => '', // PtypInteger32 ); - 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 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); + $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagSensitivity': $value = (int) $this->sensitivity[strtolower($value)]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = kolab_api_filter_mapistore::date_php2mapi($value, true); break; case 'PidTagImportance': $value = (int) $this->importance[(int) $value]; break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': $dt = kolab_api_input_json::to_datetime($value); $value = kolab_api_filter_mapistore::date_php2mapi($dt, true); // PidLidAppointmentTimeZoneDefinitionStartDisplay // PidLidAppointmentTimeZoneDefinitionEndDisplay // this is all-day event if ($dt->_dateonly) { $result['PidLidAppointmentSubType'] = 0x00000001; } break; } $result[$mapi_idx] = $value; } // Organizer if (!empty($data['organizer'])) { - $this->add_attendee_to_result($data['organizer'], $result, true); + $this->attendee_to_recipient($data['organizer'], $result, true); } // Attendees [MS-OXCICAL 2.1.3.1.1.20.2] foreach ((array) $data['attendee'] as $attendee) { - $this->add_attendee_to_result($attendee, $result); + $this->attendee_to_recipient($attendee, $result); } // Alarms (MAPI supports only one) foreach ((array) $data['valarm'] as $alarm) { if ($alarm['properties'] && $alarm['properties']['action'] == 'DISPLAY' && ($duration = $alarm['properties']['trigger']['duration']) && ($delta = self::reminder_duration_to_delta($duration)) ) { $result['PidLidReminderDelta'] = $delta; $result['PidLidReminderSet'] = true; // PidLidReminderTime // PidLidReminderSignalTime break; } } // @TODO: PidLidAppointmentDuration // @TODO: exceptions, resources // Recurrence rule if (!empty($data['rrule']) && !empty($data['rrule']['recur'])) { - if ($rule = self::recurrence_from_kolab($data['rrule']['recur'], $result)) { + if ($rule = $this->recurrence_from_kolab($data['rrule']['recur'], $result)) { $result['PidLidAppointmentRecur'] = $rule; } } - kolab_api_filter_mapistore::parse_common_props($result, $data, $context); + $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagImportance': $map = array( 0x00000002 => 1, 0x00000001 => 5, 0x00000000 => 9, ); $value = (int) $map[(int) $value]; break; case 'PidTagSensitivity': $map = array_flip($this->sensitivity); $value = $map[$value]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $format = $data['PidLidAppointmentSubType'] ? 'Y-m-d' : 'Y-m-d\TH:i:s\Z'; $value = $value->format($format); } break; } $result[$kolab_idx] = $value; } // Alarms (MAPI supports only one, DISPLAY) if ($data['PidLidReminderSet'] && ($delta = $data['PidLidReminderDelta'])) { $duration = self::reminder_delta_to_duration($delta); $alarm = array( 'action' => 'DISPLAY', 'trigger' => array('duration' => $duration), 'description' => 'Reminder', ); $result['valarm'] = array(array('properties' => $alarm)); } else if (array_key_exists('PidLidReminderSet', $data) || array_key_exists('PidLidReminderDelta', $data)) { $result['valarm'] = array(); } // Recurrence if (array_key_exists('PidLidAppointmentRecur', $data)) { $result['rrule']['recur'] = $this->recurrence_to_kolab($data['PidLidAppointmentRecur']); } + if (array_key_exists('recipients', $data)) { + $result['attendee'] = array(); + $result['organizer'] = array(); + + foreach ((array) $data['recipients'] as $recipient) { + $this->recipient_to_attendee($recipient, $result); + } + } + // @TODO: PidLidAppointmentDuration (?) - // @TODO: exceptions, resources, attendees + // @TODO: exceptions, resources - kolab_api_filter_mapistore::convert_common_props($result, $data, $object); + $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); // @TODO: add properties that are not in the map $map['PidLidAppointmentRecur'] = 'rrule'; return $map; } - /** - * 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 - */ - public static function recurrence_from_kolab($rule, $object = array(), $type = 'event') - { - $result = array( - 'Period' => $rule['interval'] ? $rule['interval'] : 1, - 'FirstDOW' => self::day2bitmask($rule['wkst'] ?: 'MO'), - 'OccurrenceCount' => 0x0000000A, - 'EndDate' => 0x5AE980DF, - 'CalendarType' => kolab_api_filter_mapistore_structure_recurrencepattern::CALENDARTYPE_DEFAULT, - // DeletedInstanceDates - // ModifiedInstanceDates - ); - - // Get event/task start date for FirstDateTime calculations - if ($object['PidLidAppointmentStartWhole']) { - $startdate = kolab_api_filter_mapistore::date_mapi2php($object['PidLidAppointmentStartWhole']); - $result['StartDate'] = intval($object['PidLidAppointmentStartWhole'] / 10000000 / 60); - } - else if ($object['PidLidCommonStart']) { - $startdate = kolab_api_filter_mapistore::date_mapi2php($object['PidLidCommonStart']); - $result['StartDate'] = intval($object['PidLidCommonStart'] / 10000000 / 60); - } - else { - rcube::raise_error(array( - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => "Found recurring $type without start date, skipping recurrence", - ), true, false); - - return; - } - -// $startdate->setTime(0, 0, 0); - - // @TODO: - // StartDate: Set to the date portion of DTSTART, in the time zone specified - // by PidLidTimeZoneStruct. This date is stored in minutes after - // midnight Jan 1, 1601. Note that this value MUST always be - // evenly divisible by 1440. - // EndDate: Set to the start date of the last instance of a recurrence, in the - // time zone specified by PidLidTimeZoneStruct. This date is - // stored in minutes after midnight January 1, 1601. If the - // recurrence is infinite, set EndDate to 0x5AE980DF. Note that - // this value MUST always be evenly divisible by 1440, except for - // the special value 0x5AE980DF. - - // @TODO: get first occurrence of the event using libcalendaring_recurrence class ? - - switch ($rule['freq']) { - case 'DAILY': - $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY; - $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY; - $result['Period'] *= 1440; - break; - - case 'WEEKLY': - // if BYDAY does not exist use day from DTSTART - if (empty($rule['byday'])) { - $rule['byday'] = strtoupper($startdate->format('S')); - } - - $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_WEEKLY; - $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK; - $result['PatternTypeSpecific'] = self::day2bitmask($rule['byday'], 'BYDAY-'); - break; - - case 'MONTHLY': - $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY; - - if (!empty($rule['bymonthday'])) { - // MAPI doesn't support multi-valued month days - $month_day = min(explode(',', $rule['bymonthday'])); - - $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH; - $result['PatternTypeSpecific'] = $month_day == -1 ? 0x0000001F : $month_day; - } - else { - $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; - $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); - - if (!empty($rule['bysetpos'])) { - $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; - } - } - - break; - - case 'YEARLY': - $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY; - $result['Period'] *= 12; - - // MAPI doesn't support multi-valued months - if ($rule['bymonth']) { - // @TODO: set $startdate - } - - if (!empty($rule['bymonthday'])) { - // MAPI doesn't support multi-valued month days - $month_day = min(explode(',', $rule['bymonthday'])); - - $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; - $result['PatternTypeSpecific'] = array(0, $month_day == -1 ? 0x0000001F : $month_day); - } - else { - $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; - $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); - - if (!empty($rule['bysetpos'])) { - $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; - } - } - - break; - } - - if (!empty($rule['until'])) { - $result['EndDate'] = intval(kolab_api_filter_mapistore::date_php2mapi($rule['until']) / 10000000 / 60); - // @TODO: calculate OccurrenceCount? - $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER; - } - else if (!empty($rule['count'])) { - $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; - $result['OccurrenceCount'] = $rule['count']; - // @TODO: set EndDate - } - else { - $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER; - } - - $result['FirstDateTime'] = self::date_minutes_diff($startdate); - - $result = 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 static function day2bitmask($days, $prefix = '') - { - $days = explode(',', $days); - $result = 0; - - foreach ($days as $day) { - $result = $result + self::$recurrence_day_map[$prefix.$day]; - } - - return $result; - } - - /** - * Convert bitmask used by MAPI to string of days (TU,TH) - * - * @param int $days - * - * @return string - */ - protected static function bitmask2day($days) - { - $days_arr = array(); - - foreach (self::$recurrence_day_map as $day => $bit) { - if ($days & $bit) { - $days_arr[] = preg_replace('/^[A-Z-]+/', '', $day); - } - } - - $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/folder.php b/lib/filter/mapistore/folder.php index 61fda5b..10816aa 100644 --- a/lib/filter/mapistore/folder.php +++ b/lib/filter/mapistore/folder.php @@ -1,168 +1,167 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_filter_mapistore_folder +class kolab_api_filter_mapistore_folder extends kolab_api_filter_mapistore_common { protected $map = array( // read-only properties 'PidTagAccess' => '', 'PidTagChangeKey' => '', 'PidTagCreationTime' => '', // PtypTime, @TODO: store in folder annotation? 'PidTagLastodificationTime' => '', // PtypTime 'PidTagContentCount' => '', // PtypInteger32 'PidTagContentUnreadCount' => '', // PtypInteger32 'PidTagDeletedOn' => '', // PtypTime // 'PidTagAddressbookEntryId' => '', // PtypBinary 'PidTagFolderId' => '', // PtypInteger64 'PidTagHierarchyChangeNumber' => '', // PtypInteger32, number of subfolders 'PidTagMessageSize' => '', // PtypInteger32, size of all messages 'PidTagMessageSizeExtended' => '', // PtypInteger64 'PidTagSubfolders' => '', // PtypBoolean 'PidTagLocalCommitTime' => '', // PtypTime, last change time in UTC 'PidTagLocalCommitTimeMax' => '', // PtypTime 'PidTagDeletedCountTotal' => '', // PtypInteger32 // read-write properties 'PidTagAttributeHidden' => '', // Ptypboolean 'PidTagComment' => '', // PtypString, @TODO: store in folder annotation? 'PidTagContainerClass' => 'type', // PtypString, IPF.* 'PidTagContainerHierarchy' => '', // PtypObject 'PidTagDisplayName' => 'name', // PtypString 'PidTagFolderAssociatedContents' => '', // PtypObject 'PidTagFolderType' => '', // PtypInteger32, 0 - namespace roots, 1 - other, 2 - virtual/search folders 'PidTagRights' => '', // PtypInteger32 'PidTagAccessControlListData' => '', // PtypBinary, see [MS-OXCPERM] ); protected $type_map = array( '' => 'IPF.Note', 'mail' => 'IPF.Note', 'task' => 'IPF.Task', 'note' => 'IPF.StickyNote', 'event' => 'IPF.Appointment', 'journal' => 'IPF.Journal', 'contact' => 'IPF.Contact', ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { list($type, ) = explode('.', $data['type']); $type = $this->type_map[(string)$type]; // skip folders of unsupported type if (empty($type)) { return; } // skip folders that are not subfolders of the specified folder, // in list-mode MAPI always requests for one-level of the hierarchy (?) if ($api->input->path[1] == 'folders') { $api = kolab_api::get_instance(); $parent = !empty($api->input->path) ? $api->input->path[0] : ''; if ($data['parent'] != $parent) { return; } } $result = array( // mapistore properties 'id' => $data['uid'], // MAPI properties 'PidTagFolderType' => 1, 'PidTagDisplayName' => $data['name'], 'PidTagContainerClass' => $type, ); if ($data['parent']) { $result['parent_id'] = $data['parent']; } $result = array_filter($result, function($v) { return $v !== null; }); 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(); // mapistore properties if ($data['id']) { $result['uid'] = $data['id']; } if ($data['parent_id']) { $result['parent'] = $data['parent_id']; } // MAPI properties if ($data['PidTagDisplayName']) { $result['name'] = $data['PidTagDisplayName']; } if ($data['PidTagContainerClass']) { // @TODO: what if folder is already a *.default or *.sentitems, etc. // we should keep the subtype intact $map = array_flip($this->type_map); $result['type'] = $map[$data['PidTagContainerClass']]; } return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['parent_id'] = 'parent'; $map['PidTagContainerClass'] = 'type'; $map['PidTagFolderType'] = 'PidTagFolderType'; return $map; } - } diff --git a/lib/filter/mapistore/mail.php b/lib/filter/mapistore/mail.php index b11fc21..f1bfdc3 100644 --- a/lib/filter/mapistore/mail.php +++ b/lib/filter/mapistore/mail.php @@ -1,451 +1,451 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_filter_mapistore_mail +class kolab_api_filter_mapistore_mail extends kolab_api_filter_mapistore_common { protected $map = array( // MS-OXCMSG properties 'PidTagMessageClass' => '', // PtypString 'PidTagHasAttachments' => '', // PtypBoolean 'PidTagMessageCodepage' => '', // PtypInteger32 'PidTagMessageLocaleId' => '', // PtypInteger32 'PidTagMessageFlags' => '', // PtypInteger32, @TODO 'PidTagMessageSize' => 'size', // PtypInteger32, size in bytes 'PidTagMessageStatus' => '', // PtypInteger32 'PidTagNormalizedSubject' => '', // PtypString 'PidTagImportance' => '', // PtypInteger32 'PidTagPriority' => 'priority', // PtypInteger32, 'PidTagSensitivity' => '', // PtypInteger32 'PidLidSmartNoAttach' => '', // PtypBoolean 'PidLidPrivate' => '', // PtypBoolean 'PidLidSideEffects' => '', // PtypInteger32 'PidLidCommonStart' => '', // PtypTime 'PidLidCommonEnd' => '', // PtypTime 'PidTagAutoForwarded' => '', // PtypBoolean 'PidTagAutoForwardComment' => '', // PtypString 'PidTagCategories' => '', // PtypMultipleString 'PidLidClassification' => '', // PtypString 'PidLidClassificationDescription' => '', // PtypString 'PidLidClassified' => '', // PtypBoolean 'PidTagInternetReferences' => '', // PtypString, @TODO 'PidLidInfoPathFormName' => '', // PtypString 'PidTagMimeSkeleton' => '', // PtypBinary 'PidTagTnefCorrelationKey' => '', // PtypBinary 'PidTagAddressBookDisplayNamePrintable' => '', // PtypString 'PidTagCreatorEntryId' => '', // PtypBinary 'PidTagLastModifierEntryId' => '', // PtypBinary 'PidLidAgingDontAgeMe' => '', // PtypBoolean 'PidLidCurrentVersion' => '', // PtypInteger32 'PidLidCurrentVersionName' => '', // PtypString, @TODO: User-Agent? 'PidTagAlternateRecipientAllowed' => '', // PtypBoolean 'PidTagResponsibility' => '', // PtypBoolean 'PidTagRowid' => '', // PtypInteger32 'PidTagHasNamedProperties' => '', // PtypBoolean 'PidTagRecipientOrder' => '', // PtypInteger32 'PidNameContentBase' => '', // PtypString, Content-Base header 'PidNameAcceptLanguage' => '', // PtypString, Accept-Language header 'PidTagPurportedSenderDomain' => '', // PtypString 'PidTagStoreEntryId' => '', // PtypBinary 'PidTagTrustSender' => '', // PtypInteger32 'PidTagSubject' => 'subject', // PtypString 'PidTagMessageRecipients' => '', // PtypObject 'PidNameContentClass' => '', // PtypString, @TODO 'PidTagLocalCommitTime' => '', // PtypTime 'PidNameContentType' => 'content-type', // PtypString, 'PidTagCreatorName' => '', // PtypString 'PidTagMessageAttachments' => '', // PtypObject 'PidTagRead' => '', // PtypBoolean, @TODO 'PidTagRecipientDisplayName' => '', // PtypString 'PidTagRecipientEntryId' => '', // PtypBinary // body properties 'PidTagBody' => 'text', // PtypString 'PidTagNativeBody' => '', // PtypInteger32 'PidTagBodyHtml' => '', // PtypString 'PidTagRtfCompressed' => '', // PtypBinary 'PidTagRtfInSync' => '', // PtypBoolean 'PidTagInternetCodepage' => '', // PtypInteger32 'PidTagBodyContentId' => '', // PtypString 'PidTagBodyContentLocation' => '', // PtypString 'PidTagHtml' => 'html', // PtypBinary // contact linking properties 'PidLidContactLinkEntry' => '', // PtypBinary 'PidLidContacts' => '', // PtypMultipleStrings 'PidLidContactLinkName' => '', // PtypString 'PidLidContactLinkSearchKey' => '', // PtypBinary // retention and archive properties 'PidTagArchiveTag' => '', // PtypBinary 'PidTagPolicyTag' => '', // PtypBinary 'PidTagRetentionPeriod' => '', // PtypInteger32 'PidTagStartDateEtc' => '', // PtypBinary 'PidTagRetentionDate' => '', // PtypTime 'PidTagRetentionFlags' => '', // PtypInteger32 'PidTagArchivePeriod' => '', // PtypInteger32 'PidTagArchiveDate' => '', // PtypTime // MS-OXOMSG properties 'PidTagBlockStatus' => '', // PtypInteger32 'PidTagConversationId' => '', // PtypBinary, @TODO 'PidTagConversationindex' => '', // PtypBinary 'PidTagConversationindexTracking' => '', // PtypBoolean 'PidTagConversationTopic' => '', // PtypString 'DeferredDeliveryTime' => '', // PtypTime 'PidTagDisplayBcc' => '', // PtypString 'PidTagDisplayCc' => '', // PtypString 'PidTagDisplayTo' => '', // PtypString 'PidTagIconIndex' => '', // PtypInteger32, @TODO 'PidTagInternetMailOverrideFormat' => '', // PtypInteger32 'PidTagInternetMessageId' => 'message-id', // PtypString 'PidTagInReplyToId' => 'in-reply-to', // PtypString, 'PidTagLastVerbExecuted' => '', // PtypInteger32 'PidTagLastVerbExecutionTime' => '', // PtypTime 'PidTagMessageToMe' => '', // PtypBoolean, @TODO 'PidTagMessageCcMe' => '', // PtypBoolean, @TODO 'PidTagMessageRecipientMe' => '', // PtypBoolean, @TODO 'PidTagOriginatorDeliveryReportRequested' => '', // PtypBoolean, @TODO 'PidTagOriginatorNonDeliveryReportRequested' => '', // PtypBoolean 'PidTagOriginalSensitivity' => '', // PtypInteger32 'PidTagReceivedRepresentingAddressType' => '', // PtypString 'PidTagReceivedRepresentingEmailAddress' => '', // PtypString 'PidTagReceivedRepresentingEntryId' => '', // PtypBinary 'PidTagReceivedRepresentingName' => '', // PtypString 'PidTagReceivedRepresentingSearchKey' => '', // PtypBinary 'PidTagReceivedRepresentingSmtpAddress' => '', // PtypString 'PidTagReadReceiptRequested' => '', // PtypBoolean, @TODO 'PidTagReadReceiptSmtpAddress' => '', // PtypString, @TODO 'PidTagNonReceiptNotificationRequested' => '', // PtypBoolean 'PidTagOriginalAuthorEntryId' => '', // PtypBinary 'PidTagOriginalAuthorName' => '', // PtypString 'PidTagReportDisposition' => '', // PtypString, @TODO 'PidTagReportDispositionMode' => '', // PtypString, @TODO 'PidTagReceivedByAddressType' => '', // PtypString 'PidTagReceivedByEmailAddress' => '', // PtypString 'PidTagReceivedByEntryId' => '', // PtypBinary 'PidTagReceivedBySearchKey' => '', // PtypBinary 'PidTagReceivedByName' => '', // PtypString 'PidTagReceivedBySmtpAddress' => '', // PtypString 'PidTagRecipientReassignmentProhibited' => '', // PtypBoolean 'PidTagReplyRecipientEntries' => '', // PtypBinary 'PidTagReplyRecipientNames' => '', // PtypString 'PidTagReplyRequested' => '', // PtypBoolean 'PidTagResponseRequested' => '', // PtypBoolean 'PidTagSendRichInfo' => '', // PtypBoolean 'PidTagSenderAddressType' => '', // PtypString, @TODO 'PidTagSenderEmailAddress' => '', // PtypString 'PidTagSenderEntryId' => '', // PtypBinary 'PidTagSenderSearchKey' => '', // PtypBinary 'PidTagSenderName' => '', // PtypString 'PidTagSenderSmtpAddress' => '', // PtypString 'PidTagSentRepresentingAddressType' => '', // PtypString, @TODO 'PidTagSentRepresentingEmailAddress' => '', // PtypString 'PidTagSentRepresentingEntryId' => '', // PtypBinary 'PidTagSentRepresentingSearchKey' => '', // PtypBinary 'PidTagSentRepresentingName' => '', // PtypString 'PidTagSentRepresentingSmtpAddress' => '', // PtypString 'PidTagSubjectPrefix' => '', // PtypString 'PidTagTransportMessageHeaders' => '', // PtypString 'PidLidInternetAccountName' => '', // PtypString 'PidLidInternetAccountStamp' => '', // PtypString 'PidTagPrimarySendAccount' => '', // PtypString 'PidTagNextSendAcct' => '', // PtypString 'PidLidUseTnef' => '', // PtypBoolean 'PidLidAutoProcessState' => '', // PtypInteger32 'PidLidVerbStream' => '', // PtypBinary 'PidLidVerbResponse' => '', // PtypString 'PidTagTargetEntryId' => '', // PtypBinary 'PidTagAutoResponseSuppress' => '', // PtypInteger32 'PidTagMessageEditorFormat' => '', // PtypInteger32 'PidTagMessageSubmissionId' => '', // PtypBinary 'PidTagSenderIdStatus' => '', // PtypInteger32 'PidTagListHelp' => '', // PtypString, List-Help header 'PidTagListSubscribe' => '', // PtypString, List-Subscribe header 'PidTagListUnsubscribe' => '', // PtypString, List-Unsubscribe header 'PidTagDelegatedByRule' => '', // PtypBoolean 'PidTagOriginalMessageId' => '', // PtypString, @TODO 'PidTagOriginalMessageClass' => '', // PtypString // @TODO MS-OXOMSG 2.2.2 Message Status Reports // @TODO MS-OXOFLAG ); /** * Message flags for PidTagMessageFlags as defined in [MS-OXCMSG] */ protected $flags = array( 'mfRead' => 0x00000001, 'mfUnsent' => 0x00000008, 'mfResend' => 0x00000080, 'mfUnmodified' => 0x00000002, 'mfSubmitted' => 0x00000004, 'mfHasAttach' => 0x00000010, 'mfFromMe' => 0x00000020, 'mfFAI' => 0x00000040, 'mfNotifyRead' => 0x00000100, 'mfNotifyUnread' => 0x00000200, 'mfEventRead' => 0x00000400, 'mfInternet' => 0x00002000, 'mfUntrusted' => 0x00008000, ); /** * Message status for PidTagMessageStatus as defined in [MS-OXCMSG] */ protected $status = array( 'msRemoteDownload' => 0x00001000, 'msInConflict' => 0x00000800, 'msRemoteDelete' => 0x00002000, ); /** * Message importance for PidTagImportance as defined in [MS-OXCMSG] */ protected $importance = array( 'low' => 0x00000000, 'normal' => 0x00000001, 'high' => 0x00000002, ); /** * Message priority for PidTagPriority as defined in [MS-OXCMSG] */ protected $priority = array( 'urgent' => 0x00000001, 'normal' => 0x00000000, 'not-urgent' => 0xFFFFFFFF, ); /** * Message sesnitivity for PidTagSensitivity as defined in [MS-OXCMSG] */ protected $sensitivity = array( 'normal' => 0x00000000, 'personal' => 0x00000001, 'private' => 0x00000002, 'confidential' => 0x00000003, ); /** * Recipient type for PidTagRecipientType as defined in [MS-OXOMSG] */ protected $recipient_types = array( 'to' => 0x00000001, 'cc' => 0x00000002, 'bcc' => 0x00000003, ); protected $body_types = array( 'plain' => 0x00000001, 'rtf' => 0x00000002, 'html' => 0x00000003, 'signed' => 0x00000004, ); /** * 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.Note', // mapistore REST API specific properties 'collection' => 'mails', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } - $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); + $value = $this->get_kolab_value($data, $kolab_idx); switch ($mapi_idx) { case 'PidTagInternetMessageId': case 'PidTagInReplyToId': if ($value) { $value = trim($value, '<>'); } break; case 'PidTagHtml': if ($value) { $value = base64_encode($value); } break; case 'PidTagPriority': if ($value == 1 || $value == 2) { $value = $this->priority['urgent']; } else if ($value > 3) { $value = $this->priority['not-urgent']; } else { $value = null; } break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // Recipients (To, Cc, Bcc) foreach (array('to', 'cc', 'bcc') as $idx) { foreach ((array) $data[$idx] as $address) { // @TODO: PidTagEntryId, PidTagEmailAddress if ($address['address']) { $recipient = array( 'PidTagSmtpAddress' => $address['address'], 'PidTagAddressType' => 'EX', 'PidTagRecipientType' => $this->recipient_types[$idx], ); if ($address['name']) { $recipient['PidTagDisplayName'] = $address['name']; } $result['recipients'][] = $recipient; } } } if ($data['html']) { $result['PidTagNativeBody'] = $this->body_types['html']; } else { $result['PidTagNativeBody'] = $this->body_types['plain']; } // @TODO: PidTagHasAttachments - kolab_api_filter_mapistore::parse_common_props($result, $data, $context); + $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagInternetMessageId': case 'PidTagInReplyToId': if ($value) { $value = '<' . trim($value, '<>') . '>'; } break; case 'PidTagHtml': if ($value) { $value = base64_decode($value); } break; case 'PidTagPriority': if ($value == $this->priority['urgent']) { $value = 1; } else if ($value == $this->priority['not-urgent']) { $value = 5; } else { $value = null; } break; } $result[$kolab_idx] = $value; } // API supports only html and text, we convert RTF to HTML if needed if ($data['PidTagRtfCompressed'] && empty($data['PidTagHtml']) && class_exists('rtf')) { // The same class is used in kolab-syncroton $rtf = new rtf(); $rtf->loadrtf($data['PidTagRtfCompressed']); // @TODO: Conversion to HTML is broken, convert to text $rtf->output('ascii'); $rtf->parse(); $result['text'] = trim($rtf->out); } // Recipients (To, Cc, Bcc) if (array_key_exists('recipients', $data)) { $types = array_flip($this->recipient_types); foreach ($data['recipients'] as $recip) { // @TODO: PidTagEntryId, PidTagEmailAddress $address = $recip['PidTagSmtpAddress']; $name = $recip['PidTagDisplayName']; if ($address && ($type = $types[$recip['PidTagRecipientType']])) { $result[$type][] = array( 'address' => $address, 'name' => $name, ); } } } - kolab_api_filter_mapistore::convert_common_props($result, $data, $object); + $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } } diff --git a/lib/filter/mapistore/note.php b/lib/filter/mapistore/note.php index 46ceb1e..b5a3214 100644 --- a/lib/filter/mapistore/note.php +++ b/lib/filter/mapistore/note.php @@ -1,140 +1,140 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_filter_mapistore_note +class kolab_api_filter_mapistore_note extends kolab_api_filter_mapistore_common { protected $map = array( // note specific props [MS-OXNOTE] 'PidLidNoteColor' => 'x-custom.MAPI:PidLidNoteColor', // PtypInteger32 'PidLidNoteHeight' => 'x-custom.MAPI:PidLidNoteHeight', // PtypInteger32 'PidLidNoteWidth' => 'x-custom.MAPI:PidLidNoteWidth', // PtypInteger32 'PidLidNoteX' => 'x-custom.MAPI:PidLidNoteX', // PtypInteger32 'PidLidNoteY' => 'x-custom.MAPI:PidLidNoteY', // PtypInteger32 // common props [MS-OXCMSG] 'PidTagBody' => 'description', 'PidTagHtml' => '', // @TODO: (?) 'PidTagMessageClass' => '', 'PidTagSubject' => 'summary', 'PidTagNormalizedSubject' => '', // @TODO: abbreviated note body 'PidTagIconIndex' => '', // @TODO: depends on PidLidNoteColor ); protected $color_map = array( '0000FF' => 0x00000000, // blue '008000' => 0x00000001, // green 'FFC0CB' => 0x00000002, // pink 'FFFF00' => 0x00000003, // yellow 'FFFFFF' => 0x00000004, // white ); /** * 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.StickyNote', // notes do not have attachments in MAPI // 'PidTagHasAttachments' => 0, // mapistore REST API specific properties 'collection' => 'notes', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } - $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); + $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidLidNoteColor': case 'PidLidNoteHeight': case 'PidLidNoteWidth': case 'PidLidNoteX': case 'PidLidNoteY': $value = (int) $value; break; } $result[$mapi_idx] = $value; } - kolab_api_filter_mapistore::parse_common_props($result, $data, $context); + $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; $result[$kolab_idx] = $value; } - kolab_api_filter_mapistore::convert_common_props($result, $data, $object); + $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } } diff --git a/lib/filter/mapistore/task.php b/lib/filter/mapistore/task.php index f61b81a..6f5dcea 100644 --- a/lib/filter/mapistore/task.php +++ b/lib/filter/mapistore/task.php @@ -1,273 +1,292 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_filter_mapistore_task +class kolab_api_filter_mapistore_task extends kolab_api_filter_mapistore_common { protected $map = array( // task specific props [MS-OXOTASK] 'PidTagProcessed' => '', // PtypBoolean 'PidLidTaskMode' => '', // ignored 'PidLidTaskStatus' => '', // PtypInteger32 'PidLidPercentComplete' => 'percent-complete', // PtypFloating64 'PidLidTaskStartDate' => 'dtstart', // PtypTime 'PidLidTaskDueDate' => 'due', // PtypTime 'PidLidTaskResetReminder' => '', // @TODO // PtypBoolean 'PidLidTaskAccepted' => '', // @TODO // PtypBoolean 'PidLidTaskDeadOccurrence' => '', // @TODO // PtypBoolean 'PidLidTaskDateCompleted' => 'x-custom.MAPI:PidLidTaskDateCompleted', // PtypTime 'PidLidTaskLastUpdate' => '', // PtypTime 'PidLidTaskActualEffort' => 'x-custom.MAPI:PidLidTaskActualEffort', // PtypInteger32 'PidLidTaskEstimatedEffort' => 'x-custom.MAPI:PidLidTaskEstimatedEffort', // PtypInteger32 'PidLidTaskVersion' => '', // PtypInteger32 'PidLidTaskState' => '', // PtypInteger32 'PidLidTaskRecurrence' => '', // PtypBinary 'PidLidTaskAssigners' => '', // PtypBinary 'PidLidTaskStatusOnComplete' => '', // PtypBoolean 'PidLidTaskHistory' => '', // @TODO: ? // PtypInteger32 'PidLidTaskUpdates' => '', // PtypBoolean 'PidLidTaskComplete' => '', // PtypBoolean 'PidLidTaskFCreator' => '', // PtypBoolean 'PidLidTaskOwner' => '', // @TODO // PtypString 'PidLidTaskMultipleRecipients' => '', // PtypBoolean 'PidLidTaskAssigner' => '', // PtypString 'PidLidTaskLastUser' => '', // PtypString 'PidLidTaskOrdinal' => '', // PtypInteger32 'PidLidTaskLastDelegate' => '', // PtypString 'PidLidTaskFRecurring' => '', // PtypBoolean 'PidLidTaskOwnership' => '', // @TODO // PtypInteger32 'PidLidTaskAcceptanceState' => '', // PtypInteger32 'PidLidTaskFFixOffline' => '', // PtypBoolean 'PidLidTaskGlobalId' => '', // @TODO // PtypBinary 'PidLidTaskCustomFlags' => '', // ignored 'PidLidTaskRole' => '', // ignored 'PidLidTaskNoCompute' => '', // ignored 'PidLidTeamTask' => '', // ignored // common props [MS-OXCMSG] 'PidTagSubject' => 'summary', 'PidTagBody' => 'description', 'PidTagHtml' => '', // @TODO: (?) 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagMessageClass' => '', 'PidLidCommonStart' => 'dtstart', 'PidLidCommonEnd' => 'due', 'PidTagIconIndex' => '', // @TODO 'PidTagCreationTime' => 'created', // PtypTime, UTC 'PidTagLastModificationTime' => 'dtstamp', // PtypTime, UTC ); /** * Values for PidLidTaskStatus property */ protected $status_map = array( 'none' => 0x00000000, // PidLidPercentComplete = 0 'in-progress' => 0x00000001, // PidLidPercentComplete > 0 and PidLidPercentComplete < 1 'complete' => 0x00000002, // PidLidPercentComplete = 1 'waiting' => 0x00000003, 'deferred' => 0x00000004, ); /** * Values for PidLidTaskHistory property */ protected $history_map = array( 'none' => 0x00000000, 'accepted' => 0x00000001, 'rejected' => 0x00000002, 'changed' => 0x00000003, 'due-changed' => 0x00000004, 'assigned' => 0x00000005, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => 'IPM.Task', // mapistore REST API specific properties 'collection' => 'tasks', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } - $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); + $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidLidPercentComplete': $value /= 100; break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': $value = kolab_api_filter_mapistore::date_php2mapi($value, false, array('hour' => 0)); break; case 'PidLidCommonStart': case 'PidLidCommonEnd': $value = kolab_api_filter_mapistore::date_php2mapi($value, true); break; // case 'PidLidTaskLastUpdate': case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = kolab_api_filter_mapistore::date_php2mapi($value, true); break; case 'PidLidTaskActualEffort': case 'PidLidTaskEstimatedEffort': $value = (int) $value; break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // set status $percent = $result['PidLidPercentComplete']; if ($precent == 1) { $result['PidLidTaskStatus'] = $this->status_map['complete']; // PidLidTaskDateCompleted (?) } else if ($precent > 0) { $result['PidLidTaskStatus'] = $this->status_map['in-progress']; } else { $result['PidLidTaskStatus'] = $this->status_map['none']; } + // Organizer + if (!empty($data['organizer'])) { + $this->attendee_to_recipient($data['organizer'], $result, true); + } + + // Attendees [MS-OXCICAL 2.1.3.1.1.20.2] + foreach ((array) $data['attendee'] as $attendee) { + $this->attendee_to_recipient($attendee, $result); + } + // Recurrence rule if (!empty($data['rrule']) && !empty($data['rrule']['recur'])) { - if ($rule = kolab_api_filter_mapistore_event::recurrence_from_kolab($data['rrule']['recur'], $result)) { + if ($rule = $this->recurrence_from_kolab($data['rrule']['recur'], $result)) { $result['PidLidTaskRecurrence'] = $rule; $result['PidLidTaskFRecurring'] = true; } } - kolab_api_filter_mapistore::parse_common_props($result, $data, $context); + $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidLidPercentComplete': $value = intval($value * 100); break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': if (intval($value) !== 0x5AE980E0) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidLidCommonStart': case 'PidLidCommonEnd': // $value = kolab_api_filter_mapistore::date_mapi2php($value, true); break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; } $result[$kolab_idx] = $value; } if ($data['PidLidTaskComplete']) { $result['status'] = 'COMPLETED'; } // Recurrences if (array_key_exists('PidLidTaskRecurrence', $data)) { - $result['rrule']['recur'] = kolab_api_filter_mapistore_event::recurrence_to_kolab($data['PidLidTaskRecurrence'], 'task'); + $result['rrule']['recur'] = $this->recurrence_to_kolab($data['PidLidTaskRecurrence'], 'task'); + } + + if (array_key_exists('recipients', $data)) { + $result['attendee'] = array(); + $result['organizer'] = array(); + + foreach ((array) $data['recipients'] as $recipient) { + $this->recipient_to_attendee($recipient, $result); + } } - kolab_api_filter_mapistore::convert_common_props($result, $data, $object); + $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidLidTaskRecurrence'] = 'rrule'; return $map; } } diff --git a/lib/input/json.php b/lib/input/json.php index f3579c5..614a2e3 100644 --- a/lib/input/json.php +++ b/lib/input/json.php @@ -1,149 +1,217 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json extends kolab_api_input { /** * Get request data (JSON) * * @param string Expected object type * @param bool Disable filters application * @param array Original object data (set on update requests) * * @return array Request data */ public function input($type = null, $disable_filters = false, $original = null) { if ($this->input_body === null) { $data = file_get_contents('php://input'); $data = trim($data); $data = json_decode($data, true); $this->input_body = $data; } if (!$disable_filters) { if ($this->filter) { if (!empty($original)) { // convert object data into API format $data = $this->api->get_object_data($original, $type); } $this->filter->input_body($this->input_body, $type, $data); } // convert input to internal kolab_storage format if ($type) { $class = "kolab_api_input_json_$type"; $model = new $class; $model->input($this->input_body, $original); } } return $this->input_body; } /** * Convert xCard/xCal date and date-time into internal DateTime * * @param array|string Date or Date-Time * * @return DateTime */ public static function to_datetime($input) { if (empty($input)) { return; } if (is_array($input)) { if ($input['date-time']) { if ($input['parameters']['tzid']) { $tzid = str_replace('/kolab.org/', '', $input['parameters']['tzid']); } else { $tzid = 'UTC'; } $datetime = $input['date-time']; try { $timezone = new DateTimeZone($tzid); } catch (Exception $e) {} } else if ($input['timestamp']) { $datetime = $input['timestamp']; } else if ($input['date']) { $datetime = $input['date']; $is_date = true; } else { return; } } else { $datetime = $input; $is_date = preg_match('/^[0-9]{4}-?[0-9]{2}-?[0-9]{2}$/', $input); } try { $dt = new DateTime($datetime, $timezone ?: new DateTimeZone('UTC')); } catch (Exception $e) { return; } if ($is_date) { $dt->_dateonly = true; $dt->setTime(0, 0, 0); } return $dt; } /** * Add x-custom fields to the result */ public static function add_x_custom($data, &$result) { if (array_key_exists('x-custom', (array) $data)) { $value = (array) $data['x-custom']; foreach ((array) $value as $idx => $v) { if ($v['identifier'] && $v['value'] !== null) { $value[$idx] = array($v['identifier'], $v['value']); } else { unset($value[$idx]); } } $result['x-custom'] = $value; } } + + /** + * Parse mailto URI, e.g. attendee/cal-address property + * + * @param string $uri Mailto: uri + * @param string $params Element parameters + * + * @return string E-mail address + */ + public static function parse_mailto_uri($uri, &$params = array()) + { + if (strpos($uri, 'mailto:') === 0) { + $uri = substr($uri, 7); + $uri = rawurldecode($uri); + $emails = rcube_mime::decode_address_list($uri, 1, true, null, false); + $email = $emails[1]; + + if (!empty($email['mailto'])) { + if (empty($params['cn']) && !empty($email['name'])) { + $params['cn'] = $email['name']; + } + + return $email['mailto']; + } + } + } + + /** + * Parse attendees property input + * + * @param array $attendees Attendees list + * + * @return array Attendees list in kolab_format_xcal format + */ + public static function parse_attendees($attendees) + { + foreach ((array) $attendees as $idx => $attendee) { + $params = $attendee['parameters']; + $email = kolab_api_input_json::parse_mailto_uri($attendee['cal-address'], $params); + + foreach (array('to', 'from') as $val) { + foreach ((array) $params['delegated-' . $val] as $del) { + if ($del_email = kolab_api_input_json::parse_mailto_uri($del, $params)) { + $delegated[$val][] = $del_email; + } + } + } + + if ($email) { + $attendees[$idx] = array_filter(array( + 'email' => $email, + 'name' => $params['cn'], + 'status' => $params['partstat'], + 'role' => $params['role'], + 'rsvp' => (bool) $params['rsvp'] || strtoupper($params['rsvp']) === 'TRUE', + 'cutype' => $params['cutype'], + 'dir' => $params['dir'], + 'delegated-to' => $delegated['to'], + 'delegated-from' => $delegated['from'], + )); + } + else { + unset($attendees[$idx]); + } + } + + return $attendees; + } } diff --git a/lib/input/json/event.php b/lib/input/json/event.php index 2790136..018e4b4 100644 --- a/lib/input/json/event.php +++ b/lib/input/json/event.php @@ -1,119 +1,128 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_event { // map xml/json attributes into internal (kolab_format) protected $field_map = array( 'description' => 'description', 'title' => 'summary', 'sensitivity' => 'class', 'sequence' => 'sequence', 'categories' => 'categories', 'created' => 'created', 'changed' => 'dtstamp', 'attendees' => 'attendee', 'organizer' => 'organizer', 'recurrence' => 'rrule', 'start' => 'dtstart', 'end' => 'dtend', 'valarms' => 'valarms', 'location' => 'location', 'priority' => 'priority', 'status' => 'status', 'url' => 'url', ); /** * Convert event input array into an array that can * be handled by kolab_storage_folder::save() * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } // require at least 'dtstart' property for new objects if (empty($original) && empty($data['dtstart'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'sensitivity': if ($value) { $value = strtolower($value); } break; case 'url': if (is_array($value)) { $value = $value[0]; } break; case 'created': case 'changed': case 'start': case 'end': $value = kolab_api_input_json::to_datetime($value); break; + + case 'attendees': + $value = kolab_api_input_json::parse_attendees($value); + break; + + case 'organizer': + if (!empty($value)) { + $value = kolab_api_input_json::parse_attendees(array($value)); + $value = $value[0]; + } + break; } $result[$kolab] = $value; } - // @TODO: attendees - // @TODO: organizer // @TODO: recurrence // @TODO: exceptions // @TOOD: alarms // x-custom fields kolab_api_input_json::add_x_custom($data, $result); // @TODO: should we require event summary/title? if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/input/json/task.php b/lib/input/json/task.php index 6600538..bc45401 100644 --- a/lib/input/json/task.php +++ b/lib/input/json/task.php @@ -1,112 +1,123 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_task { // map xml/json attributes into internal (kolab_format) protected $field_map = array( 'description' => 'description', 'title' => 'summary', 'sensitivity' => 'class', 'sequence' => 'sequence', 'categories' => 'categories', 'created' => 'created', 'changed' => 'dtstamp', 'complete' => 'percent-complete', 'status' => 'status', 'start' => 'dtstart', 'due' => 'due', 'parent_id' => 'related-to', 'location' => 'location', 'priority' => 'priority', 'url' => 'url', + 'attendees' => 'attendee', + 'organizer' => 'organizer', ); /** * Convert task input array into an array that can * be handled by kolab_storage_folder::save() * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'sensitivity': if ($value) { $value = strtolower($value); } break; case 'parent_id': // kolab_format_task supports only one parent if (is_array($value)) { $value = $value[0]; } break; case 'created': case 'changed': case 'start': case 'due': $value = kolab_api_input_json::to_datetime($value); break; + + case 'attendees': + $value = kolab_api_input_json::parse_attendees($value); + break; + + case 'organizer': + if (!empty($value)) { + $value = kolab_api_input_json::parse_attendees(array($value)); + $value = $value[0]; + } + break; } $result[$kolab] = $value; } // @TOOD: categories - // @TODO: attendees - // @TODO: organizer // @TODO: recurrence // @TOOD: alarms // x-custom fields kolab_api_input_json::add_x_custom($data, $result); if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/kolab_api.php b/lib/kolab_api.php index 5d1d3fe..917eb57 100644 --- a/lib/kolab_api.php +++ b/lib/kolab_api.php @@ -1,469 +1,468 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api extends rcube { const APP_NAME = 'Kolab REST API'; const VERSION = '0.1'; public $backend; public $filter; public $input; public $output; protected $model; /** * This implements the 'singleton' design pattern * * @return kolab_api The one and only instance */ public static function get_instance() { if (!self::$instance || !is_a(self::$instance, 'kolab_api')) { $path = kolab_api_input::request_path(); $request = array_shift($path); $class = 'kolab_api_' . $request; if (!$request || !class_exists($class)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Invalid request method: $request" )); } self::$instance = new $class(); self::$instance->startup(); } return self::$instance; } /** * Initial startup function * to register session, create database and imap connections */ protected function startup() { $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects or // doesn't throw errors when using them $plugins = (array) $this->config->get('kolab_api_plugins', array('kolab_auth')); $plugins = array_unique(array_merge($plugins, array('libkolab'))); // this way we're compatible with Roundcube Framework 1.2 // we can't use load_plugins() here foreach ($plugins as $plugin) { $this->plugins->load_plugin($plugin, true); } } /** * Exception handler * * @param kolab_api_exception Exception */ public static function exception_handler($exception) { $code = $exception->getCode(); $message = $exception->getMessage(); if ($code == 401) { header('WWW-Authenticate: Basic realm="' . self::APP_NAME .'"'); } if (!$exception instanceof kolab_api_exception) { rcube::raise_error($exception, true, false); } header("HTTP/1.1 $code $message"); exit; } /** * Program execution handler */ protected function initialize_handler() { // Handle request input $this->input = kolab_api_input::factory($this); // Get input/output filter $this->filter = $this->input->filter; // Start session, validate it and authenticate the user if needed if (!$this->session_validate()) { $this->authenticate(); $authenticated = true; } // Initialize backend $this->backend = kolab_api_backend::get_instance(); // Filter the input, we want this after authentication if ($this->filter) { $this->filter->input($this->input); } // set response output class $this->output = kolab_api_output::factory($this); if ($authenticated) { $this->output->headers(array('X-Session-Token' => session_id())); } } /** * Script shutdown handler */ public function shutdown() { parent::shutdown(); // write performance stats to logs/console if ($this->config->get('devel_mode')) { if (function_exists('memory_get_peak_usage')) $mem = memory_get_peak_usage(); else if (function_exists('memory_get_usage')) $mem = memory_get_usage(); $log = trim(kolab_api_input::request_uri() . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : '')); if (defined('KOLAB_API_START')) { rcube::print_timer(KOLAB_API_START, $log); } else { rcube::console($log); } } } /** * Validate the submitted session token */ protected function session_validate() { $sess_id = $this->input->request_header('X-Session-Token'); if (empty($sess_id)) { session_start(); return false; } session_id($sess_id); session_start(); - if (empty($_SESSION['user_id'])) { - return false; - } - // Session timeout $timeout = $this->config->get('kolab_api_session_timeout'); if ($timeout && $_SESSION['time'] && $_SESSION['time'] < time() - $timeout) { + $_SESSION = array(); return false; } // update session time $_SESSION['time'] = time(); return true; } /** * Authentication request handler (HTTP Auth) */ protected function authenticate() { if (!empty($_SERVER['PHP_AUTH_USER'])) { $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; } // when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule else if (!isset($_SERVER['PHP_AUTH_USER'])) { // "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..." if (isset($_SERVER['REMOTE_USER'])) { $basicAuthData = base64_decode(substr($_SERVER['REMOTE_USER'], 6)); } else if (isset($_SERVER['REDIRECT_REMOTE_USER'])) { $basicAuthData = base64_decode(substr($_SERVER['REDIRECT_REMOTE_USER'], 6)); } else if (isset($_SERVER['Authorization'])) { $basicAuthData = base64_decode(substr($_SERVER['Authorization'], 6)); } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $basicAuthData = base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)); } if (isset($basicAuthData) && !empty($basicAuthData)) { list($username, $password) = explode(':', $basicAuthData); } } if (!empty($username)) { $backend = kolab_api_backend::get_instance(); $result = $backend->authenticate($username, $password); } if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::UNAUTHORIZED); } + + $_SESSION['time'] = time(); } /** * Handle API request */ public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; if (!$path[1] && $path[0] && $method == 'POST') { $this->api_object_create(); } else if ($path[1]) { switch (strtolower($path[2])) { case 'attachments': if ($method == 'HEAD') { $this->api_object_count_attachments(); } else if ($method == 'GET') { $this->api_object_list_attachments(); } break; case '': if ($method == 'GET') { $this->api_object_info(); } else if ($method == 'PUT') { $this->api_object_update(); } else if ($method == 'HEAD') { $this->api_object_exists(); } else if ($method == 'DELETE') { $this->api_object_delete(); } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Fetch object info */ protected function api_object_info() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array('folder_uid' => $folder, 'object' => $object); $this->output->send($object, $this->model, $context); } /** * Create an object */ protected function api_object_create() { $folder = $this->input->path[0]; $input = $this->input->input($this->model); $context = array('folder_uid' => $folder); $uid = $this->backend->object_create($folder, $input, $this->model); $this->output->send(array('uid' => $uid), $this->model, $context, array('uid')); } /** * Update specified object */ protected function api_object_update() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); // parse input and merge with current data (result is in kolab_format/kolab_api_mail) $input = $this->input->input($this->model, false, $object); // update object on the backend $uid = $this->backend->object_update($folder, $input, $this->model); $this->output->send(array('uid' => $uid), $this->model, $context); } /** * Check if specified object exists */ protected function api_object_exists() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Remove specified object */ protected function api_object_delete() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $this->backend->objects_delete($folder, array($uid)); $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Count object attachments */ protected function api_object_count_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); $count = !empty($object['_attachments']) ? count($object['_attachments']) : 0; $this->output->headers(array('X-Count' => $count)); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * List object attachments */ protected function api_object_list_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); // @TODO: currently Kolab format (libkolabxml) allows attachments // in events, tasks and notes. We should support them also in contacts $list = $this->get_object_attachments($object); $this->output->send($list, 'attachment-list', $context, $props); } /** * Extract attachments from the object, depending if it's * Kolab object or email message */ protected function get_object_attachments($object) { // this is a kolab_format object data if (is_array($object)) { $list = (array) $object['_attachments']; foreach ($list as $idx => $att) { $attachment = new rcube_message_part; $attachment->mime_id = $att['id']; $attachment->filename = $att['name']; $attachment->mimetype = $att['mimetype']; $attachment->size = $att['size']; $attachment->disposition = 'attachment'; $list[$idx] = $attachment; } } // this is rcube_message(_header) else { $list = (array) $object->attachments; } return $list; } /** * Convert kolab_format object into API format * * @param array Object data in kolab_format * @param string Object type * * @return array Object data in API format */ public function get_object_data($object, $type) { $output = $this->output; if (!$this->output instanceof kolab_api_output_json) { $class = "kolab_api_output_json"; $output = new $class($this); } return $output->convert($object, $type); } /** * Returns RFC2822 formatted current date in user's timezone * * @return string Date */ public function user_date() { // get user's timezone try { $tz = new DateTimeZone($this->config->get('timezone')); $date = new DateTime('now', $tz); } catch (Exception $e) { $date = new DateTime(); } return $date->format('r'); } } diff --git a/lib/kolab_api_backend.php b/lib/kolab_api_backend.php index e90095c..459350a 100644 --- a/lib/kolab_api_backend.php +++ b/lib/kolab_api_backend.php @@ -1,1244 +1,1249 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $api; public $storage; public $username; public $password; public $user; public $delimiter; protected $icache = array(); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $this->api = kolab_api::get_instance(); $this->storage = $this->api->get_storage(); // @TODO: reset cache? if we do this for every request the cache would be useless // There's no session here //$this->storage->clear_cache('mailboxes.', true); // set additional header used by libkolab $this->storage->set_options(array( // @TODO: there can be Roundcube plugins defining additional headers, // we maybe would need to add them here 'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION', 'skip_deleted' => true, 'threading' => false, )); // Disable paging $this->storage->set_pagesize(999999); $this->delimiter = $this->storage->get_hierarchy_delimiter(); + + if ($_SESSION['user_id']) { + $this->user = new rcube_user($_SESSION['user_id']); + $this->api->config->set_user_prefs((array)$this->user->get_prefs()); + } } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { $host = $this->select_host($username); // use shared cache for kolab_auth plugin result (username canonification) $cache = $this->api->get_cache_shared('kolab_api_auth'); $cache_key = sha1($username . '::' . $host); if (!$cache || !($auth = $cache->get($cache_key))) { $auth = $this->api->plugins->exec_hook('authenticate', array( 'host' => $host, 'user' => $username, 'pass' => $password, )); if ($cache && !$auth['abort']) { $cache->set($cache_key, array( 'user' => $auth['user'], 'host' => $auth['host'], )); } // LDAP server failure... send 503 error if ($auth['kolab_ldap_error']) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } } else { $auth['pass'] = $password; } // authenticate user against the IMAP server $user_id = $auth['abort'] ? 0 : $this->login($auth['user'], $auth['pass'], $auth['host'], $error); if ($user_id) { $this->username = $auth['user']; $this->password = $auth['pass']; $this->delimiter = $this->storage->get_hierarchy_delimiter(); return true; } // IMAP server failure... send 503 error if ($error == rcube_imap_generic::ERROR_BAD) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } return false; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders, False on backend failure */ public function folders_list($type = null) { $type_keys = array( kolab_storage::CTYPE_KEY_PRIVATE, kolab_storage::CTYPE_KEY, ); // get folder unique identifiers and types $uid_data = $this->folder_uids(); $type_data = $this->storage->get_metadata('*', $type_keys); $folders = array(); if (!is_array($type_data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } foreach ($uid_data as $folder => $uid) { $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = null; if ($folders[$parent]) { $parent_id = $folders[$parent]['uid']; } // parent folder does not exist add it to the list else { for ($i=0; $idelimiter, $parent_arr); if ($folders[$parent]) { $parent_id = $folders[$parent]['uid']; } else { $fid = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $folders[$parent] = array( 'name' => array_pop($parent_arr), 'fullpath' => $parent, 'uid' => $fid, 'parent' => $parent_id, ); $parent_id = $fid; } } } } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); // folder type reset($type_keys); foreach ($type_keys as $key) { $data['type'] = $type_data[$folder][$key]; break; } if (empty($data['type'])) { $data['type'] = 'mail'; } $folders[$path] = $data; } // sort folders uksort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Returns folder type * * @param string $uid Folder unique identifier * @param string $with_suffix Enable to not remove the subtype * * @return string Folder type */ public function folder_type($uid, $with_suffix = false) { $folder = $this->folder_uid2name($uid); $type = kolab_storage::folder_type($folder); if ($type === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type rcube_message_header or kolab_format) * @throws kolab_api_exception */ public function objects_list($uid) { $type = $this->folder_type($uid); // use IMAP to fetch mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); $result = $this->storage->list_messages($folder, 1); foreach ($result as $idx => $mail) { $result[$idx] = new kolab_api_mail($mail); } } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->get_objects(); if ($result === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $type = $this->folder_type($uid); // use IMAP to count mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); // @TODO: error checking requires changes in rcube_imap $result = $this->storage->count($folder, 'ALL'); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->count(); } return $result; } /** * Delete objects in a folder * * @param string $uid Folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_delete($uid, $set) { $type = $this->folder_type($uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); } // delete all if ($set === "*") { if ($is_mail) { $result = $this->storage->clear_folder($folder); } else { $result = $folder->delete_all(); } } else { if ($is_mail) { $result = $this->storage->delete_message($set, $folder); } else { foreach ($set as $uid) { $result = $folder->delete($uid); if ($result === false) { break; } } } } if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Move objects into another folder * * @param string $uid Folder unique identifier * @param string $target_uid Target folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_move($uid, $target_uid, $set) { $type = $this->folder_type($uid); $target_type = $this->folder_type($target_uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); $target = $this->folder_uid2name($uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $target = $this->folder_get_by_uid($uid, $type); } if ($is_mail) { if ($set === "*") { $set = '1:*'; } $result = $this->storage->move_messages($set, $target, $folder); } else { if ($set === "*") { $set = $folder->get_uids(); } foreach ($set as $uid) { $result = $folder->move($uid, $target); if ($result === false) { break; } } } if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Get object data * * @param string $folder_uid Folder unique identifier * @param string $uid Object identifier * * @return kolab_api_mail|array Object data * @throws kolab_api_exception */ public function object_get($folder_uid, $uid) { $type = $this->folder_type($folder_uid); if ($type === 'mail') { $folder = $this->folder_uid2name($folder_uid); $object = new rcube_message($uid, $folder); if (!$object || empty($object->headers)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $object = new kolab_api_mail($object); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $object = $folder->get_object($uid); if (!$object) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $old_categories = $object['categories']; } // @TODO: Use relations also for events if ($type != 'configuration' && $type != 'event') { // get object categories (tag-relations) $categories = $this->get_tags($object, $old_categories); if ($type === 'mail') { $object->categories = $categories; } else { $object['categories'] = $categories; } } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param mixed $data Object data (an array or kolab_api_mail) * @param string $type Object type * * @return string Object UID * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype !== 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); // @TODO: categories return $data->save($folder); } // otherwise use kolab_storage else { // @TODO: Use relations also for events if (!preg_match('/^(event|configuration)/', $type)) { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$folder->save($data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // @TODO: Use relations also for events if (!empty($categories)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } return $data['uid']; } } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param mixed $data Object data (array or kolab_api_mail) * @param string $type Object type * * @return string Object UID (it can change) * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype != 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); return $data->save($folder); } // otherwise use kolab_storage else { // @TODO: Use relations also for events if (!preg_match('/^(event|configuration)/', $type)) { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$folder->save($data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // @TODO: Use relations also for events if (array_key_exists('categories', $data)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } return $data['uid']; } } /** * Get attachment body * * @param mixed $object Object data (from self::object_get()) * @param string $part_id Attachment part identifier * @param mixed $mode NULL to return a string, -1 to print body * or file pointer to save the body into * * @return string Attachment body if $fp=null * @throws kolab_api_exception */ public function attachment_get($object, $part_id, $mode = null) { // object is a mail message if ($object instanceof rcube_message) { return $object->get_part_body($part_id, false, 0, $mode); } // otherwise use kolab_storage else { return $this->storage->get_message_part($this->uid, $part_id, null, $mode === -1, is_resource($mode) ? $mode : null, true, 0, false); } } /** * Delete an attachment from the message * * @param mixed $object Object data (from self::object_get()) * @param string $id Attachment identifier * * @return boolean|string True or message UID (if changed) * @throws kolab_api_exception */ public function attachment_delete($object, $id) { // object is a mail message if ($object instanceof rcube_message) { // @TODO } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $found = false; // unset the attachment foreach ((array) $object['_attachments'] as $idx => $att) { if ($att['id'] == $id) { $object['_attachments'][$idx] = false; $found = true; } } if (!$found) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if (!$folder->save($data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return true; } } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $name = rcube_charset::convert($name, RCUBE_CHARSET, 'UTF7-IMAP'); if ($parent) { $parent = $this->folder_uid2name($parent); $name = $parent . $this->delimiter . $name; } if ($this->storage->folder_exists($name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $created = kolab_storage::folder_create($name, $type, false, false); if ($created) { $created = $this->folder_name2uid($name); } return $created; } /** * Subscribes a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folder_uid2name($uid); if (isset($updates['type'])) { $result = kolab_storage::set_folder_type($folder, $updates['type']); if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } if (isset($updates['subscribed'])) { if ($updates['subscribed']) { $result = $this->storage->subscribe($folder); } else { $result = $this->storage->unsubscribe($folder); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } // @TODO: active state } /** * Renames a folder * * @param string $old_name Folder name (UTF-8) * @param string $new_name New folder name (UTF-8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_name = rcube_charset::convert($old_name, RCUBE_CHARSET, 'UTF7-IMAP'); $new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP'); if (!strlen($old_name) || !strlen($new_name) || $old_name === $new_name) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if ($this->storage->folder_exists($new_name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // don't use kolab_storage for moving mail folders if (preg_match('/^mail/', $type)) { $result = $this->storage->rename_folder($old_name, $new_name); } else { $result = kolab_storage::folder_rename($old_name, $new_name); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folder_uid2name($uid); $type = $this->folder_type($uid); // don't use kolab_storage for mail folders if ($type === 'mail') { $status = $this->storage->delete_folder($folder); } else { $status = kolab_storage::folder_delete($folder); } if (!$status) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Folder info * * @param string $uid Folder UID * * @return array Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folder_uid2name($uid); // get IMAP folder info $info = $this->storage->folder_info($folder); // get IMAP folder data $data = $this->storage->folder_data($folder); $info['exists'] = $data['EXISTS']; $info['unseen'] = $data['UNSEEN']; $info['modseq'] = $data['HIGHESTMODSEQ']; // add some more parameters (used in folders list response) $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); $path = explode($this->delimiter, $path); $info['name'] = array_pop($path); $info['fullpath'] = implode($this->delimiter, $path); $info['uid'] = $uid; $info['type'] = kolab_storage::folder_type($folder, true) ?: 'mail'; if ($info['fullpath'] !== '') { $parent = $this->folder_name2uid(rcube_charset::convert($info['fullpath'], RCUBE_CHARSET, 'UTF7-IMAP')); $info['parent'] = $parent; } // convert some info to be more compact if (!empty($info['rights'])) { $info['rights'] = implode('', $info['rights']); } // @TODO: subscription status, active state // some info is not very interesting here ;) unset($info['attributes']); return $info; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { $folder = $this->folder_uid2name($uid); return strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); } /** * Returns IMAP folder name * * @param string $uid Folder identifier * * @return string Folder name (UTF7-IMAP) */ protected function folder_uid2name($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // we store last folder in-memory if (isset($this->icache["folder:$uid"])) { return $this->icache["folder:$uid"]; } $uids = $this->folder_uids(); foreach ($uids as $folder => $_uid) { if ($uid === $_uid) { return $this->icache["folder:$uid"] = $folder; } } // slowest method, but we need to try it, the full folders list // might contain non-existing folder (not in folder_uids() result) foreach ($this->folders_list() as $folder) { if ($folder['uid'] === $uid) { return rcube_charset::convert($folder['fullpath'], RCUBE_CHARSET, 'UTF7-IMAP'); } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Helper method to get folder UID * * @param string $folder Folder name (UTF7-IMAP) * * @return string Folder's UID */ protected function folder_name2uid($folder) { $uid_keys = array( kolab_storage::UID_KEY_CYRUS, ); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } /* // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array( kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_SHARED, ); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } */ if (!empty($metadata[$folder])) { foreach ($uid_keys as $key) { if ($uid = $metadata[$folder][$key]) { return $uid; } } } return md5($folder); /* // @TODO: // make sure folder exists // generate a folder UID and set it to IMAP $uid = rtrim(chunk_split(md5($folder . $this->get_owner() . uniqid('-', true)), 12, '-'), '-'); if (!$this->storage->set_metadata($folder, array(kolab_storage::UID_KEY_SHARED => $uid))) { if ($this->storage->set_metadata($folder, array(kolab_storage::UID_KEY_PRIVATE => $uid))) { return $uid; } } // create hash from folder name if we can't write the UID metadata return md5($folder . $this->get_owner()); */ } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1); $path2 = explode($this->delimiter, $str2); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Return UIDs of all folders * * @return array Folder name to UID map */ protected function folder_uids() { $uid_keys = array( kolab_storage::UID_KEY_CYRUS, ); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } /* // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array( kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_SHARED, ); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } */ $lambda = function(&$item, $key, $keys) { reset($keys); foreach ($keys as $key) { $item = $item[$key]; return; } }; array_walk($metadata, $lambda, $uid_keys); return $metadata; } /** * Get folder by UID (use only for non-mail folders) * * @param string $uid Folder UID * @param string $type Folder type * * @return kolab_storage_folder Folder object * @throws kolab_api_exception */ protected function folder_get_by_uid($uid, $type = null) { $folder = $this->folder_uid2name($uid); $folder = kolab_storage::get_folder($folder, $type); if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // Check the given storage folder instance for validity and throw // the right exceptions according to the error state. if (!$folder->valid || ($error = $folder->get_error())) { if ($error === kolab_storage::ERROR_IMAP_CONN) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_CACHE_DB) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_NO_PERMISSION) { throw new kolab_api_exception(kolab_api_exception::FORBIDDEN); } else if ($error === kolab_storage::ERROR_INVALID_FOLDER) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $folder; } /** * Storage host selection */ protected function select_host($username) { // Get IMAP host $host = $this->api->config->get('default_host', 'localhost'); if (is_array($host)) { list($user, $domain) = explode('@', $username); // try to select host by mail domain if (!empty($domain)) { foreach ($host as $storage_host => $mail_domains) { if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { $host = $storage_host; break; } else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { $host = is_numeric($storage_host) ? $mail_domains : $storage_host; break; } } } // take the first entry if $host is not found if (is_array($host)) { list($key, $val) = each($default_host); $host = is_numeric($key) ? $val : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP and returns Roundcube user ID. */ protected function login($username, $password, $host, &$error = null) { if (empty($username)) { return null; } $login_lc = $this->api->config->get('login_lc'); $default_port = $this->api->config->get('default_port', 143); // parse $host $a_host = parse_url($host); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP if (!$this->storage->connect($host, $username, $password, $port, $ssl)) { $error = $this->storage->get_error_code(); return null; } // No user in database, but IMAP auth works if (!is_object($user)) { if ($this->api->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ), true, false); return null; } } else { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", ), true, false); return null; } } // overwrite config with user preferences $this->user = $user; $this->api->config->set_user_prefs((array)$this->user->get_prefs()); $_SESSION['user_id'] = $this->user->ID; $_SESSION['username'] = $this->user->data['username']; $_SESSION['storage_host'] = $host; $_SESSION['storage_port'] = $port; $_SESSION['storage_ssl'] = $ssl; $_SESSION['password'] = $this->api->encrypt($password); $_SESSION['login_time'] = time(); setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); return $user->ID; } /** * Returns list of tag-relation names assigned to kolab object */ protected function get_tags($object, $categories = null) { // Kolab object if (is_array($object)) { $ident = $object['uid']; } // Mail message else if (is_object($object)) { // support only messages with message-id $ident = $object->headers->get('message-id', false); $folder = $message->folder; $uid = $message->uid; } if (empty($ident)) { return array(); } $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($ident); $delta = 300; // resolve members if it wasn't done recently if ($uid) { foreach ($tags as $idx => $tag) { $force = empty($this->tag_rts[$tag['uid']]) || $this->tag_rts[$tag['uid']] <= time() - $delta; $members = $config->resolve_members($tag, $force); if (empty($members[$folder]) || !in_array($uid, $members[$folder])) { unset($tags[$idx]); } if ($force) { $this->tag_rts[$tag['uid']] = time(); } } // make sure current folder is set correctly again $this->storage->set_folder($folder); } $tags = array_filter(array_map(function($v) { return $v['name']; }, $tags)); // merge result with old categories if (!empty($categories)) { $tags = array_unique(array_merge($tags, (array) $categories)); } return $tags; } /** * Set tag-relations to kolab object */ protected function set_tags($uid, $tags) { // @TODO: set_tags() for email $config = kolab_storage_config::get_instance(); $config->save_tags($uid, $tags); } } diff --git a/tests/Unit/Filter/Mapistore.php b/tests/Unit/Filter/Mapistore.php index 166870e..022cac1 100644 --- a/tests/Unit/Filter/Mapistore.php +++ b/tests/Unit/Filter/Mapistore.php @@ -1,197 +1,82 @@ array( - 'n2' => 'test2', - ), - 'n3' => 'test3', - 'x-custom' => array( - array('identifier' => 'i', value => 'val_i'), - ), - ); - - $value = kolab_api_filter_mapistore::get_kolab_value($data, 'n1.n2'); - $this->assertSame('test2', $value); - - $value = kolab_api_filter_mapistore::get_kolab_value($data, 'n3'); - $this->assertSame('test3', $value); - - $value = kolab_api_filter_mapistore::get_kolab_value($data, 'n30'); - $this->assertSame(null, $value); - - $value = kolab_api_filter_mapistore::get_kolab_value($data, 'x-custom.i'); - $this->assertSame('val_i', $value); - } - - /** - * Test set_kolab_value method - */ - function test_set_kolab_value() - { - $data = array(); - - kolab_api_filter_mapistore::set_kolab_value($data, 'n1.n2', 'test'); - $this->assertSame('test', $data['n1']['n2']); - - kolab_api_filter_mapistore::set_kolab_value($data, 'n1', 'test'); - $this->assertSame('test', $data['n1']); - - kolab_api_filter_mapistore::set_kolab_value($data, 'x-custom.i', 'test1'); - $this->assertSame('test1', $data['x-custom.i']); - } /** * Test uid_encode method */ function test_uid_encode() { $uid = kolab_api_filter_mapistore::uid_encode('folder', 'msg'); $this->assertSame('folder.msg', $uid); $uid = kolab_api_filter_mapistore::uid_encode('folder', 'msg', 'attach'); $this->assertSame('folder.msg.attach', $uid); $uid = kolab_api_filter_mapistore::uid_encode('f-ol.der', 'm-s.g', 'att.a-ch'); $this->assertSame('f-ol_46der.m-s_46g.att_46a-ch', $uid); } /** * Test uid_decode method */ function test_uid_decode() { $uid = kolab_api_filter_mapistore::uid_decode('f-ol_46der.m-s_46g.att_46a-ch'); $this->assertSame(array('f-ol.der', 'm-s.g', 'att.a-ch'), $uid); } - /** - * Test parse_common_props method - */ - function test_parse_common_props() - { - kolab_api_filter_mapistore::parse_common_props($result = array(), array(), array()); - $this->assertSame(array(), $result); - } - - /** - * Test attributes_filter method - */ - function test_attributes_filter() - { - $api = new kolab_api_filter_mapistore; - - $input = array( - 'creation-date', - 'uid', - 'unknown', - ); - - $expected = array( - 'PidTagCreationTime', - 'id', - ); - - $result = $api->attributes_filter($input, '', true); - $this->assertSame($expected, $result); - - $input = $expected; - $expected = array( - 'creation-date', - 'uid', - ); - - $result = $api->attributes_filter($input, ''); - $this->assertSame($expected, $result); - - $result = $api->attributes_filter(array(), ''); - $this->assertSame(array(), $result); - } - /** * Test date_php2mapi method */ function test_date_php2mapi() { $date = kolab_api_filter_mapistore::date_php2mapi('2014-01-01T00:00:00+00:00'); $this->assertSame(13033008000.0, $date); $date = kolab_api_filter_mapistore::date_php2mapi('2014-01-01'); $this->assertSame(13033008000.0, $date); $date = kolab_api_filter_mapistore::date_php2mapi('1970-01-01T00:00:00Z'); $this->assertSame(11644473600.0, $date); $date = kolab_api_filter_mapistore::date_php2mapi('1601-01-01T00:00:00Z'); $this->assertSame(0.0, $date); $date = new DateTime('1601-01-01T00:00:00Z'); $date = kolab_api_filter_mapistore::date_php2mapi($date); $this->assertSame(0.0, $date); /* $date = new DateTime('1970-01-01 00:00:00.1000 +0000'); $date = kolab_api_filter_mapistore::date_php2mapi($date); $this->assertSame(11644473600.0 + (1000/1000000), $date); */ $date = kolab_api_filter_mapistore::date_php2mapi(''); $this->assertSame(null, $date); } /** * Test date_mapi2php method */ function test_date_mapi2php() { $format = 'c'; $data = array( 13033008000 => '2014-01-01T00:00:00+00:00', 11644473600 => '1970-01-01T00:00:00+00:00', // 11644473600.00001 => '1970-01-01T00:00:00.10+00:00', 0 => '1601-01-01T00:00:00+00:00', ); foreach ($data as $mapi => $exp) { $date = kolab_api_filter_mapistore::date_mapi2php($mapi); $this->assertSame($exp, $date->format($format)); } } - - /** - * Test parse_categories method - */ - function test_parse_categories() - { - $categories = array( - "test\x3Btest", - "test\x2Ctest", - "a\x06\x1Ba", - "b\xFE\x54b", - "c\xFF\x1Bc", - "test ", - " test", - ); - - $expected = array( - "testtest", - "aa", - "bb", - "cc", - "test", - ); - - $result = kolab_api_filter_mapistore::parse_categories($categories); - - $this->assertSame($expected, $result); - } } diff --git a/tests/Unit/Filter/Mapistore/Common.php b/tests/Unit/Filter/Mapistore/Common.php new file mode 100644 index 0000000..77e9944 --- /dev/null +++ b/tests/Unit/Filter/Mapistore/Common.php @@ -0,0 +1,224 @@ + array( + 'n2' => 'test2', + ), + 'n3' => 'test3', + 'x-custom' => array( + array('identifier' => 'i', value => 'val_i'), + ), + ); + + $value = kolab_api_filter_mapistore_common::get_kolab_value($data, 'n1.n2'); + $this->assertSame('test2', $value); + + $value = kolab_api_filter_mapistore_common::get_kolab_value($data, 'n3'); + $this->assertSame('test3', $value); + + $value = kolab_api_filter_mapistore_common::get_kolab_value($data, 'n30'); + $this->assertSame(null, $value); + + $value = kolab_api_filter_mapistore_common::get_kolab_value($data, 'x-custom.i'); + $this->assertSame('val_i', $value); + } + + /** + * Test set_kolab_value method + */ + function test_set_kolab_value() + { + $data = array(); + + kolab_api_filter_mapistore_common::set_kolab_value($data, 'n1.n2', 'test'); + $this->assertSame('test', $data['n1']['n2']); + + kolab_api_filter_mapistore_common::set_kolab_value($data, 'n1', 'test'); + $this->assertSame('test', $data['n1']); + + kolab_api_filter_mapistore_common::set_kolab_value($data, 'x-custom.i', 'test1'); + $this->assertSame('test1', $data['x-custom.i']); + } + + /** + * Test attributes_filter method + */ + function test_attributes_filter() + { + $api = new kolab_api_filter_mapistore_common; + + $input = array( + 'creation-date', + 'uid', + 'unknown', + ); + + $expected = array( + 'PidTagCreationTime', + 'id', + ); + + $result = $api->attributes_filter($input, true); + $this->assertSame($expected, $result); + + $input = $expected; + $expected = array( + 'creation-date', + 'uid', + ); + + $result = $api->attributes_filter($input); + $this->assertSame($expected, $result); + + $result = $api->attributes_filter(array()); + $this->assertSame(array(), $result); + } + + /** + * Test parse_categories method + */ + function test_parse_categories() + { + $categories = array( + "test\x3Btest", + "test\x2Ctest", + "a\x06\x1Ba", + "b\xFE\x54b", + "c\xFF\x1Bc", + "test ", + " test", + ); + + $expected = array( + "testtest", + "aa", + "bb", + "cc", + "test", + ); + + $result = kolab_api_filter_mapistore_common::parse_categories($categories); + + $this->assertSame($expected, $result); + } + + /** + * Test recurrence_to_kolab + */ + function test_recurrence_to_kolab() + { + // empty result + $result = kolab_api_filter_mapistore_event::recurrence_to_kolab(''); + $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/Event.php b/tests/Unit/Filter/Mapistore/Event.php index fa19ac4..dd091d8 100644 --- a/tests/Unit/Filter/Mapistore/Event.php +++ b/tests/Unit/Filter/Mapistore/Event.php @@ -1,288 +1,204 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '100-100-100-100'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-05-14T13:03:33Z'), $result['PidTagCreationTime']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-05-14T13:50:18Z'), $result['PidTagLastModificationTime']); $this->assertSame(2, $result['PidLidAppointmentSequence']); $this->assertSame(3, $result['PidTagSensitivity']); $this->assertSame('Work', $result['PidNameKeywords'][0]); /* $this->assertSame('/kolab.org/Europe/Berlin', $result['dtstart']['parameters']['tzid']); $this->assertSame('2015-05-15T10:00:00', $result['dtstart']['date-time']); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtend']['parameters']['tzid']); $this->assertSame('2015-05-15T10:30:00', $result['dtend']['date-time']); $this->assertSame('https://some.url', $result['url']); */ $this->assertSame('Summary', $result['PidTagSubject']); $this->assertSame('Description', $result['PidTagBody']); $this->assertSame(2, $result['PidTagImportance']); $this->assertSame('Location', $result['PidLidLocation']); - $this->assertSame('German, Mark', $result['recipients'][0]['PidTagDisplayName']); - $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagEmailAddress']); - $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); - $this->assertSame('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('German, Mark', $result['recipients'][0]['PidTagDisplayName']); + $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagEmailAddress']); + $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); + $this->assertSame(3, $result['recipients'][0]['PidTagRecipientFlags']); + $this->assertSame('Manager, Jane', $result['recipients'][1]['PidTagDisplayName']); + $this->assertSame(1, $result['recipients'][1]['PidTagRecipientType']); + $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); + $this->assertSame(0, $result['recipients'][1]['PidTagRecipientTrackStatus']); + $this->assertSame(1, $result['recipients'][1]['PidTagRecipientFlags']); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); $data = kolab_api_tests::get_data('101-101-101-101', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '101-101-101-101'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(0, $result['PidTagSensitivity']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentStartWhole']); $this->assertSame(kolab_api_filter_mapistore::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentEndWhole']); $this->assertSame(1, $result['PidLidAppointmentSubType']); // recurrence $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(1, $arp->RecurrencePattern->Period); $this->assertSame(0x200B, $arp->RecurrencePattern->RecurFrequency); $this->assertSame(1, $arp->RecurrencePattern->PatternType); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_event; $data = array( 'PidTagCreationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T13:03:33Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 10, 'PidTagSensitivity' => 3, 'PidNameKeywords' => array('work'), 'PidTagSubject' => 'subject', 'PidTagBody' => 'body', 'PidTagImportance' => 2, 'PidLidLocation' => 'location', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T16:00:00Z'), 'PidLidReminderDelta' => 15, 'PidLidReminderSet' => true, + 'recipients' => array( + array( + 'PidTagDisplayName' => 'German, Mark', + 'PidTagEmailAddress' => 'mark.german@example.org', + 'PidTagRecipientType' => 1, + 'PidTagRecipientFlags' => 3, + ), + array( + 'PidTagDisplayName' => 'Manager, Jane', + 'PidTagEmailAddress' => 'manager@example.org', + 'PidTagRecipientType' => 1, + 'PidTagRecipientTrackStatus' => 2, + ), + ), ); $result = $api->input($data); $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame(10, $result['sequence']); $this->assertSame('confidential', $result['class']); $this->assertSame(array('work'), $result['categories']); $this->assertSame('location', $result['location']); $this->assertSame(1, $result['priority']); $this->assertSame('2015-05-14T13:03:33Z', $result['created']); $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame('2015-05-14T13:03:33Z', $result['dtstart']); $this->assertSame('2015-05-14T16:00:00Z', $result['dtend']); - $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); + $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Reminder', $result['valarm'][0]['properties']['description']); - $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); + $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); - self::$original = $result; + $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); + $this->assertSame('TENTATIVE', $result['attendee'][0]['parameters']['partstat']); + $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); +// $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); + $this->assertSame('mailto:manager%40example.org', $result['attendee'][0]['cal-address']); + $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); + $this->assertSame('mailto:mark.german%40example.org', $result['organizer']['cal-address']); + self::$original = $result; $data = array( // all-day event 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T00:00:00Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T00:00:00Z'), 'PidLidAppointmentSubType' => 1, 'PidLidReminderSet' => false, - // @TODO: recurrence, exceptions, alarms, attendees + // @TODO: recurrence, exceptions, alarms ); $result = $api->input($data); $this->assertSame('2015-05-14', $result['dtstart']); $this->assertSame('2015-05-14', $result['dtend']); $this->assertSame(array(), $result['valarm']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_event; $data = array( // 'PidTagCreationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T13:03:33Z'), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 20, 'PidTagSensitivity' => 2, 'PidNameKeywords' => array('work1'), 'PidTagSubject' => 'subject1', 'PidTagBody' => 'body1', 'PidTagImportance' => 1, 'PidLidLocation' => 'location1', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-15T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-05-15T16:00:00Z'), 'PidLidReminderDelta' => 25, 'PidLidReminderSet' => true, ); $result = $api->input($data, self::$original); $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame(20, $result['sequence']); $this->assertSame('private', $result['class']); $this->assertSame(array('work1'), $result['categories']); $this->assertSame('location1', $result['location']); $this->assertSame(5, $result['priority']); // $this->assertSame('2015-05-14T13:03:33Z', $result['created']); // $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame('2015-05-15T13:03:33Z', $result['dtstart']); $this->assertSame('2015-05-15T16:00:00Z', $result['dtend']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Reminder', $result['valarm'][0]['properties']['description']); $this->assertSame('-PT25M', $result['valarm'][0]['properties']['trigger']['duration']); // @TODO: recurrence, exceptions, attendees } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_event; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } - - /** - * 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 eead53e..1da578f 100644 --- a/tests/Unit/Filter/Mapistore/Task.php +++ b/tests/Unit/Filter/Mapistore/Task.php @@ -1,163 +1,182 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Tasks', false, '10-10-10-10'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Tasks', false), $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', 'Tasks', 'task', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Tasks', false, '20-20-20-20'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Tasks', false), $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('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('mailto:%3Cjane.manager%40example.org%3E', $result['attendee']['cal-address']); -*/ + // organizer/attendees + $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(1, $result['recipients'][1]['PidTagRecipientType']); + $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); + // 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_tests::mapi_uid('Tasks', false, '10-10-10-10'), 'parent_id' => kolab_api_tests::folder_uid('Tasks', false), '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, 'PidNameKeywords' => array('work1'), + 'recipients' => array( + array( + 'PidTagDisplayName' => 'German, Mark', + 'PidTagEmailAddress' => 'mark.german@example.org', + 'PidTagRecipientType' => 1, + 'PidTagRecipientFlags' => 3, + ), + array( + 'PidTagDisplayName' => 'Manager, Jane', + 'PidTagEmailAddress' => 'manager@example.org', + 'PidTagRecipientType' => 1, + 'PidTagRecipientTrackStatus' => 2, + ), + ), ); $result = $api->input($data); 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']); $this->assertSame(array('work1'), $result['categories']); + $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); + $this->assertSame('TENTATIVE', $result['attendee'][0]['parameters']['partstat']); + $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); +// $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); + $this->assertSame('mailto:manager%40example.org', $result['attendee'][0]['cal-address']); + $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); + $this->assertSame('mailto:mark.german%40example.org', $result['organizer']['cal-address']); + $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']); + $this->assertSame('MAPI:PidLidTaskDateCompleted', $result['x-custom'][0]['identifier']); + $this->assertSame(13073961600.0, $result['x-custom'][0]['value']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_task; $data = array( '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/Input/Json.php b/tests/Unit/Input/Json.php index 016c797..5ecc32b 100644 --- a/tests/Unit/Input/Json.php +++ b/tests/Unit/Input/Json.php @@ -1,98 +1,116 @@ markTestIncomplete('TODO'); } /** * Test to_datetime method */ function test_to_datetime() { $result = kolab_api_input_json::to_datetime(null); $this->assertNull($result); $date = '2014-01-01'; $result = kolab_api_input_json::to_datetime($date); $this->assertInstanceOf('DateTime', $result); $this->assertSame('2014-01-01T00:00:00+00:00', $result->format('c')); $this->assertTrue($result->_dateonly); $date = array('date' => '2014-01-01'); $result = kolab_api_input_json::to_datetime($date); $this->assertInstanceOf('DateTime', $result); $this->assertSame('2014-01-01T00:00:00+00:00', $result->format('c')); $this->assertTrue($result->_dateonly); $date = '2015-04-20T14:22:18Z'; $result = kolab_api_input_json::to_datetime($date); $this->assertInstanceOf('DateTime', $result); $this->assertSame('2015-04-20T14:22:18+00:00', $result->format('c')); $this->assertFalse((bool) $result->_dateonly); $date = array('date-time' => '2015-04-21T00:00:00Z'); $result = kolab_api_input_json::to_datetime($date); $this->assertInstanceOf('DateTime', $result); $this->assertSame('2015-04-21T00:00:00+00:00', $result->format('c')); $this->assertFalse((bool) $result->_dateonly); $date = array( 'date-time' => '2015-04-21T00:00:00', 'parameters' => array( 'tzid' => '/kolab.org/Europe/Zurich', ), ); $result = kolab_api_input_json::to_datetime($date); $this->assertInstanceOf('DateTime', $result); $this->assertSame('2015-04-21T00:00:00+02:00', $result->format('c')); $this->assertFalse((bool) $result->_dateonly); $this->assertSame('Europe/Zurich', $result->getTimezone()->getName()); } /** * Test add_x_custom */ function test_add_x_custom() { kolab_api_input_json::add_x_custom($data, $result); $this->assertNull($result); $data = array('x-custom' => null); kolab_api_input_json::add_x_custom($data, $result); $this->assertSame(array(), $result['x-custom']); $data = array('x-custom' => array()); kolab_api_input_json::add_x_custom($data, $result); $this->assertSame(array(), $result['x-custom']); $data = array('x-custom' => array( array('identifier' => 'i', 'value' => 'v'), )); kolab_api_input_json::add_x_custom($data, $result); $this->assertSame('i', $result['x-custom'][0][0]); $this->assertSame('v', $result['x-custom'][0][1]); } + + /** + * Test input parse_mailto_uri + */ + function test_parse_mailto_uri() + { + // @TODO + $this->markTestIncomplete('TODO'); + } + + /** + * Test input parse_attendees + */ + function test_parse_attendees() + { + // @TODO + $this->markTestIncomplete('TODO'); + } } diff --git a/tests/Unit/Input/Json/Event.php b/tests/Unit/Input/Json/Event.php index d29315c..8000035 100644 --- a/tests/Unit/Input/Json/Event.php +++ b/tests/Unit/Input/Json/Event.php @@ -1,132 +1,157 @@ input($data); } /** * Test expected exception in input method * * @expectedException kolab_api_exception * @expectedExceptionCode 422 */ function test_input_exception2() { $input = new kolab_api_input_json_event; $data = 'test'; $input->input($data); } /** * Test expected exception in input method * * @expectedException kolab_api_exception * @expectedExceptionCode 422 */ function test_input_exception3() { $input = new kolab_api_input_json_event; $data = array('test' => 'test'); // 'dtstamp' field is required $input->input($data); } /** * Test input method (convert JSON to internal format) */ function test_input() { $input = new kolab_api_input_json_event; $data = array( 'description' => 'description', 'summary' => 'summary', 'sequence' => 10, 'class' => 'PUBLIC', 'categories' => array('test'), 'created' => '2015-04-20T14:22:18Z', 'dtstamp' => '2015-04-21T00:00:00Z', 'status' => 'NEEDS-ACTION', 'dtstart' => '2014-01-01', 'dtend' => '2014-02-01', 'location' => null, 'priority' => 1, 'url' => 'url', + 'attendee' => array( + array( + 'parameters' => array( + 'cn' => 'Manager, Jane', + 'partstat' => 'NEEDS-ACTION', + 'role' => 'REQ-PARTICIPANT', + 'rsvp' => true, + ), + 'cal-address' => 'mailto:%3Cjane.manager%40example.org%3E', + ), + ), + 'organizer' => array( + 'parameters' => array( + 'cn' => 'Organizer', + ), + 'cal-address' => 'mailto:organizer%40example.org', + ), ); $input->input($data); $this->assertSame('description', $data['description']); $this->assertSame('summary', $data['title']); $this->assertSame('public', $data['sensitivity']); $this->assertSame(10, $data['sequence']); $this->assertSame(array('test'), $data['categories']); $this->assertSame(null, $data['location']); $this->assertSame(1, $data['priority']); $this->assertSame('url', $data['url']); $this->assertSame(kolab_api_input_json::to_datetime('2015-04-20T14:22:18Z')->format('c'), $data['created']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2015-04-21T00:00:00Z')->format('c'), $data['changed']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-01-01')->format('c'), $data['start']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-02-01')->format('c'), $data['end']->format('c')); + $this->assertSame('Manager, Jane', $data['attendees'][0]['name']); + $this->assertSame('NEEDS-ACTION', $data['attendees'][0]['status']); + $this->assertSame('REQ-PARTICIPANT', $data['attendees'][0]['role']); + $this->assertSame(true, $data['attendees'][0]['rsvp']); + $this->assertSame('jane.manager@example.org', $data['attendees'][0]['email']); + $this->assertSame('Organizer', $data['organizer']['name']); + $this->assertSame('organizer@example.org', $data['organizer']['email']); + self::$original = $data; } /** * Test input method with merging */ function test_input2() { $input = new kolab_api_input_json_event; $data = array( 'description' => 'description1', 'summary' => 'summary1', 'sequence' => 20, 'class' => 'PRIVATE', 'categories' => array('test1'), // 'created' => '2015-04-20T14:22:18Z', // 'dtstamp' => '2015-04-21T00:00:00Z', // 'status' => 'IN-PROCESS', 'dtstart' => '2014-01-11', 'dtend' => '2014-02-11', 'location' => 'location1', 'priority' => 2, 'url' => 'url1', ); $input->input($data, self::$original); $this->assertSame('description1', $data['description']); $this->assertSame('summary1', $data['title']); $this->assertSame('private', $data['sensitivity']); $this->assertSame(20, $data['sequence']); $this->assertSame(array('test1'), $data['categories']); $this->assertSame('location1', $data['location']); $this->assertSame(2, $data['priority']); $this->assertSame('url1', $data['url']); // $this->assertSame(kolab_api_input_json::to_datetime('2015-04-20T14:22:18Z')->format('c'), $data['created']->format('c')); // $this->assertSame(kolab_api_input_json::to_datetime('2015-04-21T00:00:00Z')->format('c'), $data['changed']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-01-11')->format('c'), $data['start']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-02-11')->format('c'), $data['end']->format('c')); } } diff --git a/tests/Unit/Input/Json/Task.php b/tests/Unit/Input/Json/Task.php index d1541fb..552274a 100644 --- a/tests/Unit/Input/Json/Task.php +++ b/tests/Unit/Input/Json/Task.php @@ -1,126 +1,149 @@ input($data); } /** * Test expected exception in input method * * @expectedException kolab_api_exception * @expectedExceptionCode 422 */ function test_input_exception2() { $input = new kolab_api_input_json_task; $data = 'test'; $input->input($data); } /** * Test input method (convert JSON to internal format) */ function test_input() { $input = new kolab_api_input_json_task; $data = array( 'description' => 'description', 'summary' => 'summary', 'sequence' => 10, 'class' => 'PUBLIC', 'categories' => array('test'), 'created' => '2015-04-20T14:22:18Z', 'dtstamp' => '2015-04-21T00:00:00Z', 'percent-complete' => 50, 'status' => 'NEEDS-ACTION', 'dtstart' => '2014-01-01', 'due' => '2014-02-01', 'related-to' => 'parent', 'location' => null, 'priority' => 1, 'url' => 'url', + 'attendee' => array( + array( + 'parameters' => array( + 'cn' => 'Manager, Jane', + 'partstat' => 'NEEDS-ACTION', + 'role' => 'OPT-PARTICIPANT', + ), + 'cal-address' => 'mailto:%3Cjane.manager%40example.org%3E', + ), + ), + 'organizer' => array( + 'parameters' => array( + 'cn' => 'Organizer', + ), + 'cal-address' => 'mailto:organizer%40example.org', + ), ); $input->input($data); $this->assertSame('description', $data['description']); $this->assertSame('summary', $data['title']); $this->assertSame('public', $data['sensitivity']); $this->assertSame(10, $data['sequence']); $this->assertSame(array('test'), $data['categories']); $this->assertSame(50, $data['complete']); $this->assertSame('parent', $data['parent_id']); $this->assertSame(null, $data['location']); $this->assertSame(1, $data['priority']); $this->assertSame('url', $data['url']); $this->assertSame(kolab_api_input_json::to_datetime('2015-04-20T14:22:18Z')->format('c'), $data['created']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2015-04-21T00:00:00Z')->format('c'), $data['changed']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-01-01')->format('c'), $data['start']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-02-01')->format('c'), $data['due']->format('c')); + $this->assertSame('Manager, Jane', $data['attendees'][0]['name']); + $this->assertSame('NEEDS-ACTION', $data['attendees'][0]['status']); + $this->assertSame('OPT-PARTICIPANT', $data['attendees'][0]['role']); + $this->assertSame('jane.manager@example.org', $data['attendees'][0]['email']); + $this->assertSame('Organizer', $data['organizer']['name']); + $this->assertSame('organizer@example.org', $data['organizer']['email']); + // for test_input2() below self::$original = $data; } /** * Test input method with merging */ function test_input2() { $input = new kolab_api_input_json_task; $data = array( 'description' => 'description1', 'summary' => 'summary1', 'sequence' => 20, 'class' => 'PRIVATE', 'categories' => array('test1'), // 'created' => '2015-04-20T14:22:18Z', // 'dtstamp' => '2015-04-21T00:00:00Z', 'percent-complete' => 55, 'status' => 'NEEDS-ACTION', 'dtstart' => '2014-01-11', 'due' => '2014-02-11', 'related-to' => 'parent1', 'location' => 'location1', 'priority' => 2, 'url' => 'url1', ); $input->input($data, self::$original); $this->assertSame('description1', $data['description']); $this->assertSame('summary1', $data['title']); $this->assertSame('private', $data['sensitivity']); $this->assertSame(20, $data['sequence']); $this->assertSame(array('test1'), $data['categories']); $this->assertSame(55, $data['complete']); $this->assertSame('parent1', $data['parent_id']); $this->assertSame('location1', $data['location']); $this->assertSame(2, $data['priority']); $this->assertSame('url1', $data['url']); // $this->assertSame(kolab_api_input_json::to_datetime('2015-04-20T14:22:18Z')->format('c'), $data['created']->format('c')); // $this->assertSame(kolab_api_input_json::to_datetime('2015-04-21T00:00:00Z')->format('c'), $data['changed']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-01-11')->format('c'), $data['start']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-02-11')->format('c'), $data['due']->format('c')); } } diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 292f3bd..2387579 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,64 +1,65 @@ Unit/Output/Json.php Unit/Output/Json/Attachment.php Unit/Output/Json/Contact.php Unit/Output/Json/Event.php Unit/Output/Json/Folder.php Unit/Output/Json/Info.php Unit/Output/Json/Mail.php Unit/Output/Json/Note.php Unit/Output/Json/Task.php Unit/Input/Json.php Unit/Input/Json/Attachment.php Unit/Input/Json/Contact.php Unit/Input/Json/Event.php Unit/Input/Json/Folder.php Unit/Input/Json/Mail.php Unit/Input/Json/Note.php Unit/Input/Json/Folder.php Unit/Input/Json/Task.php Unit/Filter/Mapistore.php Unit/Filter/Mapistore/Structure/Appointmentrecurrencepattern.php Unit/Filter/Mapistore/Structure/Changehighlight.php Unit/Filter/Mapistore/Structure/Exceptioninfo.php Unit/Filter/Mapistore/Structure/Extendedexception.php Unit/Filter/Mapistore/Structure/Recipientrow.php Unit/Filter/Mapistore/Structure/Recurrencepattern.php Unit/Filter/Mapistore/Structure/Systemtime.php Unit/Filter/Mapistore/Structure/Timezonestruct.php Unit/Filter/Mapistore/Structure/Tzrule.php + Unit/Filter/Mapistore/Common.php Unit/Filter/Mapistore/Attachment.php Unit/Filter/Mapistore/Contact.php Unit/Filter/Mapistore/Event.php Unit/Filter/Mapistore/Folder.php Unit/Filter/Mapistore/Info.php Unit/Filter/Mapistore/Mail.php Unit/Filter/Mapistore/Note.php Unit/Filter/Mapistore/Task.php API/Folders.php API/Attachments.php API/Contacts.php API/Events.php API/Info.php API/Mails.php API/Notes.php API/Tasks.php Mapistore/Folders.php Mapistore/Attachments.php Mapistore/Contacts.php Mapistore/Events.php Mapistore/Info.php Mapistore/Mails.php Mapistore/Notes.php Mapistore/Tasks.php