diff --git a/lib/filter/mapistore.php b/lib/filter/mapistore.php index 51aebb4..0458027 100644 --- a/lib/filter/mapistore.php +++ b/lib/filter/mapistore.php @@ -1,616 +1,799 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore extends kolab_api_filter { public $input; public $api; public $attrs_filter = array(); /** * 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) { // handle differences between OpenChange API and Kolab API $this->input = $input; $this->api = $input->api; $this->common_action = !in_array($input->action, array('folders', 'info')); // 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); } } // properties filter, map MAPI attribute names to Kolab attributes else if ($input->method == 'GET' && $input->args['properties']) { if ($input->action == 'folders' && $input->path[1] == 'messages') { if (!$this->is_builtin_folder($input->path[0])) { $type = $this->api->backend->folder_type($input->path[0]); list($type, ) = explode('.', $type); } } else { $type = $input->action[strlen($input->action)-1] == 's' ? substr($input->action, 0, -1) : $input->action; } $this->attrs_filter = explode(',', $input->args['properties']); $properties = $this->attributes_filter($this->attrs_filter, $type); $input->args['properties'] = implode(',', $properties); } // handle actions on contact photo attachments switch ($input->action) { case 'attachments': $this->attachment_actions_handler(); break; case 'folders': $this->folder_actions_handler(); break; case 'notes': // Notes do not have attachments in Exchange if ($input->path[1] === 'attachments' || count($input->path) > 2) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } break; } } /** * 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) { // handle differences between OpenChange API and Kolab API // Note: input->path is already modified by input() and path() above switch ($this->input->action) { case 'folders': // folders//deletemessages input if ($this->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); + + if (!empty($attrs_filter)) { + $attrs_filter = array_combine($attrs_filter, $attrs_filter); + } } // Add contact photo to attachments list if ($this->input->action == 'contacts' && $this->input->path[2] == 'attachments' && $context && !empty($context['object']) ) { if ($attachment = kolab_api_filter_mapistore_contact::photo_attachment($context['object'])) { $result[] = $attachment; } } + // Add event exceptions to attachments list + else if ($this->input->action == 'events' && $this->input->path[2] == 'attachments' + && $context && !empty($context['object']) + ) { + $event_model = $this->get_model_class('event'); + $event_output = new kolab_api_output_json_event($this->api->output); + $event = $event_output->element($context['object']); + $attachments = $event_model->exception_attachments($event); + + if (!empty($attachments)) { + $result = array_merge($result, $attachments); + } + } 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); } // cleanup unset($_SESSION['uploads']['MAPIATTACH']); } /** * Executed for response headers * * @param array Response headers * @param array Context (folder_uid, object_uid, object) */ public function headers(&$headers, $context = null) { // handle differences between OpenChange API and Kolab API foreach ($headers as $name => $value) { switch ($name) { case 'X-Count': // Add contact photo to attachments count if ($this->input->action == 'contacts' && $this->input->path[2] == 'attachments' && $context && !empty($context['object']) && kolab_api_filter_mapistore_contact::photo_attachment($context['object']) ) { $value += 1; } + // Add event exceptions to attachments count + if ($this->input->action == 'events' && $this->input->path[2] == 'attachments' + && $context && !empty($context['object']) + ) { + $event_model = $this->get_model_class('event'); + $event_output = new kolab_api_output_json_event($this->api->output); + $event = $event_output->element($context['object']); + $value += count($event_model->exception_attachments($event)); + } + $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; } } /** * 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; } /** * Filter property names */ protected function attributes_filter($attrs, $type = null, $reverse = false) { $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; } /** * 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); } } /** * Overwrite attachment actions for contact photos */ protected function attachment_actions_handler() { + // contact photo attachment if ($this->input->path[2] == kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID) { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $attach_uid = $this->input->path[2]; $object = $this->api->backend->object_get($folder, $object_uid); $attachment = kolab_api_filter_mapistore_contact::photo_attachment($object); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); if (!$attachment) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // fetch photo info/body if ($this->input->method == 'GET') { $this->api->output->send($attachment, 'attachment', $context); } // photo existence check else if ($this->input->method == 'HEAD') { $this->api->output->send_status(kolab_api_output::STATUS_OK); } // photo delete else if ($this->input->method == 'DELETE') { $object['photo'] = ''; $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } // photo update else if ($this->input->method == 'PUT') { $data = file_get_contents('php://input'); $data = trim($data); $data = json_decode($data, true); if (empty($data) || empty($data['PidTagAttachDataBinary'])) { $error = "Invalid input for contact photo update request"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } $object['photo'] = base64_decode($data['PidTagAttachDataBinary']); $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } } - // add photo to contact? + // event exception attachment + else if ($this->input->path[2] >= 1000000 && $this->api->backend->folder_type($this->input->path[0]) == 'event') { + $folder = $this->input->path[0]; + $object_uid = $this->input->path[1]; + $attach_uid = $this->input->path[2]; + $object = $this->api->backend->object_get($folder, $object_uid); + $context = array( + 'folder_uid' => $folder, + 'object_uid' => $object_uid, + 'object' => $object + ); + + $model = $this->get_model_class('event'); + $event_output = new kolab_api_output_json_event($this->api->output); + $event = $event_output->element($object); + $attachments = $model->exception_attachments($event); + + foreach ($attachments as $attach_idx => $att) { + if ($att['id'] == $attach_uid) { + $attachment = $att; + break; + } + } + + if (!$attachment) { + throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + } + + // fetch attachment info + if ($this->input->method == 'GET') { + $this->api->output->send($attachment, 'attachment', $context); + } + // attachment existence check + else if ($this->input->method == 'HEAD') { + $this->api->output->send_status(kolab_api_output::STATUS_OK); + } + // attachment delete + else if ($this->input->method == 'DELETE') { + unset($object['exceptions'][$attach_idx]); + $this->api->backend->object_update($folder, $object, 'event'); + $this->api->output->send_status(kolab_api_output::STATUS_OK); + } + // exception update + else if ($this->input->method == 'PUT') { + $data = file_get_contents('php://input'); + $data = trim($data); + $data = json_decode($data, true); + + if (empty($data['PidTagAttachDataObject'])) { + $error = "Invalid input for event exception update request"; + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); + } + + if ($data['PidTagAttachDataObject']['PidTagMessageClass'] != kolab_api_filter_mapistore_event::EXCEPTION_CLASS) { + // as of now we do not support other DataObject attachments + $error = "Unsupported input for DataObject attachment"; + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); + } + + // parse exception data + $exception = $model->input($data['PidTagAttachDataObject']); + + // Convert the exception into internal format + $input = new kolab_api_input_json_event; + $input->input($exception, $object['exceptions'][$attach_idx]); + + if ($exception['start']) { + $dt = $exception['start']; + } + else if ($data['PidTagExceptionReplaceTime']) { + $dt = $model->date_mapi2php($data['PidTagExceptionReplaceTime']); + } + + if (empty($dt)) { + $error = "Invalid input for event exception"; + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); + } + + $exception['recurrence_date'] = $dt; + + // Exception attachment id + $id = $model->recurrence_id($dt, $event['dtstart']); + + // Replace the exception and remove other exceptions with the same new recurrence-id + reset($attachments); + foreach ($attachments as $idx => $att) { + if ($idx == $attach_idx) { + $object['exceptions'][$attach_idx] = $exception; + } + else if ($att['id'] == $id) { + unset($object['exceptions'][$idx]); + } + } + + // Save the event + $this->api->backend->object_update($folder, $object, 'event'); + $this->api->output->send(array('id' => $id), 'attachment', $context, array('id')); + } + } + // new attachment... else if ($this->input->path[0] && $this->input->path[1] && $this->input->method == 'POST') { $data = $this->input->input(null, true); + // add photo to a contact? if ($data['PidTagAttachmentContactPhoto']) { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $object = $this->api->backend->object_get($folder, $object_uid); if (empty($data['PidTagAttachDataBinary'])) { $error = "Invalid input for contact photo create request"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } + $id = kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID; + $context = array( + 'folder_uid' => $folder, + 'object_uid' => $object_uid, + 'object' => $object + ); + $object['photo'] = base64_decode($data['PidTagAttachDataBinary']); + $this->api->backend->object_update($folder, $object, 'contact'); + $this->api->output->send(array('id' => $id), 'attachment', $context, array('id')); + } + // embedded message, e.g. calendar event exception + else if ($data['PidTagAttachMethod'] == 0x00000005) { + $folder = $this->input->path[0]; + $object_uid = $this->input->path[1]; + $object = $this->api->backend->object_get($folder, $object_uid); - $this->api->output->send_status(kolab_api_output::STATUS_OK); + if (empty($data['PidTagAttachDataObject'])) { + $error = "Invalid input for event exception create request"; + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); + } + + if ($data['PidTagAttachDataObject']['PidTagMessageClass'] != kolab_api_filter_mapistore_event::EXCEPTION_CLASS) { + // as of now we do not support other DataObject attachments + $error = "Unsupported input for DataObject attachment"; + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); + } + + $context = array( + 'folder_uid' => $folder, + 'object_uid' => $object_uid, + 'object' => $object + ); + + // parse exception data + $model = $this->get_model_class('event'); + $exception = $model->input($data['PidTagAttachDataObject']); + $dt = kolab_api_input_json::to_datetime($exception['recurrence-id'] ?: $exception['dtstart']); + + if (empty($dt) && $data['PidTagExceptionReplaceTime']) { + $dt = $model->date_mapi2php($data['PidTagExceptionReplaceTime']); + } + + if (empty($dt)) { + $error = "Invalid input for event exception"; + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); + } + + // Convert the exception into internal format + $input = new kolab_api_input_json_event; + $input->input($exception); + + // Exception attachment id + $id = $model->recurrence_id($dt, $object['start']); + + // Append the exception and save event + $object['exceptions'][] = $exception; + $this->api->backend->object_update($folder, $object, 'event'); + $this->api->output->send(array('id' => $id), 'attachment', $context, array('id')); } } } /** * Overwrite folders actions */ protected function folder_actions_handler() { $input = $this->input; // in OpenChange folders/1/folders means get folders of the IPM Subtree if ($input->method == 'GET' && $input->path[0] === '1' && $input->path[1] == 'folders') { $input->path = array(); } // in OpenChange folders/0/folders means get the hierarchy of the NON-IPM Subtree else if ($input->method == 'GET' && $input->path[0] === '0' && $input->path[1] == 'folders') { $list = $this->get_builtin_folder_list(0); $this->api->output->send($list, 'folder-list', null); } else if ($input->path[1] == 'messages') { $input->path[1] = 'objects'; } else if ($input->path[1] == 'deletemessages') { $input->path[1] = 'deleteobjects'; } // request for built-in folder if ($this->is_builtin_folder($input->path[0])) { $folder = $this->get_builtin_folder($input->path[0]); if (count($input->path) == 1) { // folder info if ($input->method == 'GET') { $this->api->output->send($folder, 'folder', null); } // folder exists else if ($input->method == 'HEAD') { $this->api->output->send_status(kolab_api_output::STATUS_OK); } throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } else { switch (strtolower((string) $input->path[1])) { case 'objects': if ($input->method == 'HEAD') { $this->builtin_folder_count_objects(); } else if ($input->method == 'GET') { $this->builtin_folder_list_objects(); } break; case 'folders': if ($input->method == 'HEAD') { $this->builtin_folder_count_folders(); } else if ($input->method == 'GET') { $this->builtin_folder_list_folders(); } break; case 'empty': if ($input->method == 'POST') { $this->builtin_folder_empty(); } break; case 'deleteobjects': if ($input->method == 'POST') { $this->builtin_folder_delete_objects(); } break; } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } } /** * Returns list of built-in folders (NON-IPM subtree) */ protected function get_builtin_folder_list($parent = null) { $folders = kolab_api_filter_mapistore_folder::$builtin_folders; foreach ($folders as $idx => $folder) { if ($parent !== null && $parent != $folder['parent']) { unset($folders[$idx]); continue; } $folders[$idx] = array_merge(array( 'comment' => $folder['name'], 'uid' => $idx, 'hidden' => true, 'role' => 10, 'system_idx' => $idx, ), $folder); } return $folders; } /** * Returns built-in folder information */ protected function get_builtin_folder($uid) { $list = $this->get_builtin_folder_list(); $result = $list[$uid]; if (!$result) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return $result; } /** * Check if specified uid is an uid of builtin folder */ protected function is_builtin_folder($uid) { return is_numeric($uid) && strlen($uid) < 4; } /** * Coun objects in built-in folder */ protected function builtin_folder_count_objects() { // @TODO $this->api->output->headers(array('X-Count' => 0)); $this->api->output->send_status(kolab_api_output::STATUS_OK); } /** * List objects in built-in folder */ protected function builtin_folder_list_objects() { // @TODO $this->api->output->send(array(), 'folder-list'); } /** * Count sub-folders of built-in folder */ protected function builtin_folder_count_folders() { // @TODO $this->api->output->headers(array('X-Count' => 0)); $this->api->output->send_status(kolab_api_output::STATUS_OK); } /** * List sub-folders in built-in folder */ protected function builtin_folder_list_folders() { // @TODO $this->api->output->send(array(), 'folder-list'); } /** * Delete all objects in built-in folder */ protected function builtin_folder_empty() { throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } /** * Delete objects in built0in folder */ protected function builtin_folder_delete_objects() { throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } } diff --git a/lib/filter/mapistore/attachment.php b/lib/filter/mapistore/attachment.php index fd18b13..2a95452 100644 --- a/lib/filter/mapistore/attachment.php +++ b/lib/filter/mapistore/attachment.php @@ -1,206 +1,221 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_attachment extends kolab_api_filter_mapistore_common { protected $model = 'attachment'; 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 + // attachment props [MS-OXCMSG] + 'PidTagLastModificationTime' => '', // PtypTime + 'PidTagCreationTime' => '', // PtypTime + 'PidTagDisplayName' => 'filename', // PtypString + 'PidTagAttachSize' => 'size', // PtypInteger32 + 'PidTagAttachNumber' => '', // PtypInteger32, @TODO: unique attachment index within a message + 'PidTagAttachDataBinary' => '', // PtypBinary + 'PidTagAttachDataObject' => 'data-object', // PtypBinary + 'PidTagAttachMethod' => '', // PtypInteger32 + 'PidTagAttachLongFilename' => 'filename', // PtypString, filename with extension + 'PidTagAttachFilename' => '', // PtypString, filename in 8.3 form + 'PidTagAttachExtension' => '', // PtypString + 'PidTagAttachLongPathname' => '', // PtypString + 'PidTagAttachPathname' => '', // PtypString + 'PidTagAttachTag' => '', // PtypBinary + 'PidTagRenderingPosition' => '', // PtypInteger32 + 'PidTagAttachRendering' => '', // PtypBinary + 'PidTagAttachFlags' => '', // PtypInteger32 + 'PidTagAttachTransportName' => '', // PtypString + 'PidTagAttachEncoding' => '', // PtypBinary + 'PidTagAttachAdditionalInformation' => '', // PtypBinary + 'PidTagAttachmentLinkId' => '', // PtypInteger32 + 'PidTagAttachmentFlags' => 'attachment-flags', // PtypInteger32 + 'PidTagAttachmentHidden' => 'is-hidden', // PtypBoolean + 'PidTagTextAttachmentCharset' => 'charset', // PtypString + // MIME props [MS-OXCMSG] + 'PidTagAttachMimeTag' => 'mimetype', // PtypString + 'PidTagAttachContentId' => 'content-id', // PtypString + 'PidTagAttachContentLocation' => 'content-location', // PtypString + 'PidTagAttachContentBase' => '', // PtypString + 'PidTagAttachPayloadClass' => '', // PtypString + 'PidTagAttachPayloadProviderGuidString' => '', // PtypString + 'PidNameAttachmentMacContentType' => '', // PtypString + 'PidNameAttachmentMacInfo' => '', // PtypBinary // Contact photo [MS-OXOCNTC] - 'PidTagAttachmentContactPhoto' => 'is_photo', // PtypBoolean + 'PidTagAttachmentContactPhoto' => 'is-photo', // PtypBoolean + // Event exceptions [MS-OXOCAL] + 'PidTagExceptionStartTime' => 'start-time', // PtypTime + 'PidTagExceptionEndTime' => 'end-time', // PtypTime + 'PidTagExceptionReplaceTime' => 'replace-time', // PtypTime ); /** * Methods for PidTagAttachMethod */ protected $methods = array( 'afNone' => 0x00000001, 'afByValue' => 0x00000001, 'afByReference' => 0x00000002, 'afByReferenceOnly' => 0x00000004, 'afEmbeddedMessage' => 0x00000005, 'afStorage' => 0x00000006, ); + /** + * Flags for PidTagAttachFlags + */ + protected $flags = array( + 'attInvisibleInHtml' => 0x00000001, + 'attInvisibleInRtf' => 0x00000002, + 'attRenderedInBody' => 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( - '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 = $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); } // Store attachment body in base64 // @TODO: shouldn't we do this only in attachment.info request? if ($data['content']) { $result['PidTagAttachDataBinary'] = base64_encode($data['content']); } - else { + else if (empty($result['PidTagAttachDataObject'])) { $result['PidTagAttachDataBinary'] = $this->attachment_body($context['object'], $data, true); } - $result['PidTagAttachMethod'] = $this->methods['afByValue']; + if ($result['PidTagAttachDataObject']) { + $result['PidTagAttachMethod'] = $this->methods['afEmbeddedMessage']; + } + else if (!isset($result['PidTagAttachMethod'])) { + $result['PidTagAttachMethod'] = $this->methods['afByValue']; + } $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; } if ($data['PidTagAttachDataBinary']) { $stream = base64_decode($data['PidTagAttachDataBinary']); $result['upload-id'] = $sess_key = 'MAPIATTACH'; $_SESSION['uploads'][$sess_key] = array( 'group' => 'kolab_upload', 'name' => $data['PidTagDisplayName'], 'mimetype' => rcube_mime::file_content_type($stream, $data['PidTagDisplayName'], 'application/octet-stream', true), 'data' => $stream, 'size' => strlen($stream), ); } return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidTagAttachExtension'] = 'filename'; + $map['id'] = 'id'; 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/common.php b/lib/filter/mapistore/common.php index 0429820..33f03b1 100644 --- a/lib/filter/mapistore/common.php +++ b/lib/filter/mapistore/common.php @@ -1,1136 +1,1290 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_common { + // PidTagObjectType property values + const OBJECT_STORE = 0x00000001; + const OBJECT_ABOOK = 0x00000002; + const OBJECT_ABOOK_CONTAINER = 0x00000004; + const OBJECT_MESSAGE = 0x00000005; + const OBJECT_USER = 0x00000006; + const OBJECT_ATTACHMENT = 0x00000007; + const OBJECT_DISTLIST = 0x00000008; + // Common properties [MS-OXCMSG] protected static $common_map = array( -// 'PidTagAccess' => '', -// 'PidTagAccessLevel' => '', // 0 - read-only, 1 - modify -// 'PidTagChangeKey' => '', - 'PidTagCreationTime' => 'creation-date', // PtypTime, UTC - 'PidTagLastModificationTime' => 'last-modification-date', // PtypTime, UTC -// 'PidTagLastModifierName' => '', -// 'PidTagObjectType' => '', // @TODO - 'PidTagHasAttachments' => 'attach', // PtypBoolean -// 'PidTagRecordKey' => '', -// 'PidTagSearchKey' => '', +// 'PidTagAccess' => '', // PtypInteger32 +// 'PidTagAccessLevel' => '', // PtypInteger32, 0 - read-only, 1 - modify +// 'PidTagChangeKey' => '', // PtypBinary + 'PidTagCreationTime' => 'creation-date', // PtypTime, UTC + 'PidTagLastModificationTime' => 'last-modification-date', // PtypTime, UTC +// 'PidTagLastModifierName' => '', // PtypString +// 'PidTagObjectType' => '', // PtypInteger32 + 'PidTagHasAttachments' => 'attach', // PtypBoolean +// 'PidTagRecordKey' => '', // PtypBinary +// 'PidTagSearchKey' => '', // PtypBinary 'PidNameKeywords' => 'categories', ); protected $recipient_track_status_map = array( 'TENTATIVE' => 0x00000002, 'ACCEPTED' => 0x00000003, 'DECLINED' => 0x00000004, ); protected $recipient_type_map = array( 'NON-PARTICIPANT' => 0x00000004, 'OPT-PARTICIPANT' => 0x00000002, 'REQ-PARTICIPANT' => 0x00000001, 'CHAIR' => 0x00000001, ); /** * Mapping of weekdays */ protected static $recurrence_day_map = array( 'SU' => 0x00000000, 'MO' => 0x00000001, 'TU' => 0x00000002, 'WE' => 0x00000003, 'TH' => 0x00000004, 'FR' => 0x00000005, 'SA' => 0x00000006, 'BYDAY-SU' => 0x00000001, 'BYDAY-MO' => 0x00000002, 'BYDAY-TU' => 0x00000004, 'BYDAY-WE' => 0x00000008, 'BYDAY-TH' => 0x00000010, 'BYDAY-FR' => 0x00000020, 'BYDAY-SA' => 0x00000040, ); /** * Extracts data from kolab data array */ public static function get_kolab_value($data, $name) { $name_items = explode('.', $name); $count = count($name_items); $value = $data[$name_items[0]]; // special handling of x-custom properties if ($name_items[0] === 'x-custom') { foreach ((array) $value as $custom) { if ($custom['identifier'] === $name_items[1]) { return $custom['value']; } } - return null; + return; } for ($i = 1; $i < $count; $i++) { if (!is_array($value)) { return null; } list($key, $num) = explode(':', $name_items[$i]); $value = $value[$key]; if ($num !== null && $value !== null) { $value = is_array($value) ? $value[$num] : null; } } return $value; } /** * Sets specified kolab data item */ public static function set_kolab_value(&$data, $name, $value) { $name_items = explode('.', $name); $count = count($name_items); $element = &$data; // x-custom properties if ($name_items[0] === 'x-custom') { // this is supposed to be converted later by parse_common_props() $data[$name] = $value; return; } if ($count > 1) { for ($i = 0; $i < $count - 1; $i++) { $key = $name_items[$i]; if (!array_key_exists($key, $element)) { $element[$key] = array(); } $element = &$element[$key]; } } $element[$name_items[$count - 1]] = $value; } /** * Parse common properties in object data (convert into MAPI format) */ protected function parse_common_props(&$result, $data, $context = array()) { if (empty($context)) { // @TODO: throw exception? return; } if ($data['uid'] && $context['folder_uid']) { $result['id'] = kolab_api_filter_mapistore::uid_encode($context['folder_uid'], $data['uid']); } if ($context['folder_uid']) { $result['parent_id'] = $context['folder_uid']; } foreach (self::$common_map as $mapi_idx => $kolab_idx) { if (!isset($result[$mapi_idx]) && ($value = $data[$kolab_idx]) !== null) { switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': $result[$mapi_idx] = self::date_php2mapi($value, true); break; case 'PidTagHasAttachments': if (!empty($value) && $this->model != 'note') { $result[$mapi_idx] = true; } break; case 'PidNameKeywords': $result[$mapi_idx] = self::parse_categories((array) $value); break; } } } + + // set object type + switch ($this->model) { + case 'attachment': + $result['PidTagObjectType'] = self::OBJECT_ATTACHMENT; + break; + + case 'folder': + case 'info': + // no object type for folders + break; + + default: + if ($result['PidTagMessageClass'] == 'IPM.DistList') { + $result['PidTagObjectType'] = self::OBJECT_DISTLIST; + } + else { + $result['PidTagObjectType'] = self::OBJECT_MESSAGE; + } + } } /** * Convert common properties into kolab format */ protected function convert_common_props(&$result, $data, $original) { // @TODO: id, parent_id? foreach (self::$common_map as $mapi_idx => $kolab_idx) { if (array_key_exists($mapi_idx, $data) && !array_key_exists($kolab_idx, $result)) { $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $dt = self::date_mapi2php($value); $result[$kolab_idx] = $dt->format('Y-m-d\TH:i:s\Z'); } break; default: if ($value) { $result[$kolab_idx] = $value; } break; } } } // Handle x-custom fields foreach ((array) $result as $key => $value) { if (strpos($key, 'x-custom.') === 0) { unset($result[$key]); $key = substr($key, 9); foreach ((array) $original['x-custom'] as $idx => $custom) { if ($custom['identifier'] == $key) { if ($value) { $original['x-custom'][$idx]['value'] = $value; } else { unset($original['x-custom'][$idx]); } $x_custom_update = true; continue 2; } } if ($value) { $original['x-custom'][] = array( 'identifier' => $key, 'value' => $value, ); } $x_custom_update = true; } } if ($x_custom_update) { $result['x-custom'] = array_values($original['x-custom']); } } /** * Filter property names with mapping (kolab <> MAPI) * * @param array $attrs Property names * @param bool $reverse Reverse mapping * * @return array Property names */ public function attributes_filter($attrs, $reverse = false) { $map = array_merge(self::$common_map, $this->map()); $result = array(); // add some special common attributes $map['PidTagMessageClass'] = 'PidTagMessageClass'; $map['collection'] = 'collection'; - $map['id'] = 'uid'; + if (!isset($map['id'])) { + $map['id'] = 'uid'; + } foreach ($attrs as $attr) { if ($reverse) { if ($name = array_search($attr, $map)) { $result[] = $name; } } else if ($name = $map[$attr]) { $result[] = $name; } } return $result; } /** * Return properties map */ protected function map() { return array(); } /** * Parse categories according to [MS-OXCICAL 2.1.3.1.1.20.3] * * @param array Categories * * @return array Categories */ public static function parse_categories($categories) { if (!is_array($categories)) { return; } $result = array(); foreach ($categories as $idx => $val) { $val = preg_replace('/(\x3B|\x2C|\x06\x1B|\xFE\x54|\xFF\x1B)/', '', $val); $val = preg_replace('/\s+/', ' ', $val); $val = trim($val); $len = mb_strlen($val); if ($len) { if ($len > 255) { $val = mb_substr($val, 0, 255); } $result[mb_strtolower($val)] = $val; } } return array_values($result); } /** * Convert Kolab 'attendee' specification into MAPI recipient * and add it to the result */ protected function attendee_to_recipient($attendee, &$result, $is_organizer = false) { $email = $attendee['cal-address']; $params = (array) $attendee['parameters']; // parse mailto string if (strpos($email, 'mailto:') === 0) { $email = urldecode(substr($email, 7)); } $emails = rcube_mime::decode_address_list($email, 1); if (!empty($email)) { $email = $emails[key($emails)]; $recipient = array( 'PidTagAddressType' => 'SMTP', 'PidTagDisplayName' => $params['cn'] ?: $email['name'], 'PidTagDisplayType' => 0, 'PidTagEmailAddress' => $email['mailto'], ); if ($is_organizer) { $recipient['PidTagRecipientFlags'] = 0x00000003; $recipient['PidTagRecipientType'] = 0x00000001; } else { $recipient['PidTagRecipientFlags'] = 0x00000001; $recipient['PidTagRecipientTrackStatus'] = (int) $this->recipient_track_status_map[$params['partstat']]; $recipient['PidTagRecipientType'] = $this->to_recipient_type($params['cutype'], $params['role']); } $recipient['PidTagRecipientDisplayName'] = $recipient['PidTagDisplayName']; $result['recipients'][] = $recipient; if (strtoupper($params['rsvp']) == 'TRUE') { $result['PidTagReplyRequested'] = true; $result['PidTagResponseRequested'] = true; } } } /** * Convert MAPI recipient into Kolab attendee */ protected function recipient_to_attendee($recipient, &$result) { if ($email = $recipient['PidTagEmailAddress']) { $mailto = 'mailto:' . rawurlencode($email); $attendee = array( 'cal-address' => $mailto, 'parameters' => array( 'cn' => $recipient['PidTagDisplayName'] ?: $recipient['PidTagRecipientDisplayName'], ), ); if ($recipient['PidTagRecipientFlags'] == 0x00000003) { $result['organizer'] = $attendee; } else { switch ($recipient['PidTagRecipientType']) { case 0x00000004: $role = 'NON-PARTICIPANT'; break; case 0x00000003: $cutype = 'RESOURCE'; break; case 0x00000002: $role = 'OPT-PARTICIPANT'; break; case 0x00000001: $role = 'REQ-PARTICIPANT'; break; } $map = array_flip($this->recipient_track_status_map); $partstat = $map[$recipient['PidTagRecipientTrackStatus']] ?: 'NEEDS-ACTION'; // @TODO: rsvp? $attendee['parameters']['cutype'] = $cutype; $attendee['parameters']['role'] = $role; $attendee['parameters']['partstat'] = $partstat; $result['attendee'][] = $attendee; } } } /** * Convert Kolab valarm specification into MAPI properties * * @param array $data Kolab object * @param array $result Object data (MAPI format) */ protected function alarm_from_kolab($data, &$result) { // [MS-OXCICAL] 2.1.3.1.1.20.62 foreach ((array) $data['valarm'] as $alarm) { if (!empty($alarm['properties']) && $alarm['properties']['action'] != 'DISPLAY') { continue; } // @TODO alarms with Date-Time instead of Duration $trigger = $alarm['properties']['trigger']; if ($trigger['duration'] && $trigger['parameters']['related'] != 'END' && ($delta = self::reminder_duration_to_delta($trigger['duration'])) ) { // Find next instance of the appointment (in UTC) $now = kolab_api::$now ?: new DateTime('now', new DateTimeZone('UTC')); if ($data['dtstart']) { $dtstart = kolab_api_input_json::to_datetime($data['dtstart']); // check if start date is from the future if ($dtstart > $now) { $reminder_time = $dtstart; } // find next occurence else { kolab_api_input_json::parse_recurrence($data, $res); if (!empty($res['recurrence'])) { $recurlib = libcalendaring::get_recurrence(); $recurlib->init($res['recurrence'], $now); $next = $recurlib->next(); if ($next) { $reminder_time = $next; } } } } $result['PidLidReminderDelta'] = $delta; // If all instances are in the past, don't set ReminderTime nor ReminderSet if ($reminder_time) { $signal_time = clone $reminder_time; $signal_time->sub(new DateInterval('PT' . $delta . 'M')); $result['PidLidReminderSet'] = true; $result['PidLidReminderTime'] = $this->date_php2mapi($reminder_time, true); $result['PidLidReminderSignalTime'] = $this->date_php2mapi($signal_time, true); } // MAPI supports only one alarm break; } } } /** * Convert MAPI recurrence into Kolab (MS-OXICAL: 2.1.3.2.2) * * @param string $data MAPI object * @param array $result Kolab object */ protected function alarm_to_kolab($data, &$result) { if ($data['PidLidReminderSet'] && ($delta = $data['PidLidReminderDelta'])) { $duration = self::reminder_delta_to_duration($delta); $alarm = array( 'action' => 'DISPLAY', 'trigger' => array('duration' => $duration), // 'description' => 'Reminder', ); $result['valarm'] = array(array('properties' => $alarm)); } else if (array_key_exists('PidLidReminderSet', $data) || array_key_exists('PidLidReminderDelta', $data)) { $result['valarm'] = array(); } } /** * Convert PidLidReminderDelta value into xCal duration */ protected static function reminder_delta_to_duration($delta) { if ($delta == 0x5AE980E1) { $delta = 15; } $delta = (int) $delta; return "-PT{$delta}M"; } /** * Convert Kolab alarm duration into PidLidReminderDelta */ protected static function reminder_duration_to_delta($duration) { if ($duration && preg_match('/^-[PT]*([0-9]+)([WDHMS])$/', $duration, $matches)) { $value = intval($matches[1]); switch ($matches[2]) { case 'S': $value = intval(round($value/60)); break; case 'H': $value *= 60; break; case 'D': $value *= 24 * 60; break; case 'W': $value *= 7 * 24 * 60; break; } return $value; } } /** * Convert Kolab recurrence specification into MAPI properties * * @param array $data Kolab object * @param array $object Object data (MAPI format) - * - * @return object MAPI recurrence in binary format */ - protected function recurrence_from_kolab($data, $object = array()) + protected function recurrence_from_kolab($data, &$object) { if ((empty($data['rrule']) || empty($data['rrule']['recur'])) && (empty($data['rdate']) || empty($data['rdate']['date'])) ) { - return null; + return false; } - $type = $this->model; - // Get event/task start date for FirstDateTime calculations if ($dtstart = kolab_api_input_json::to_datetime($data['dtstart'])) { // StartDate: Set to the date portion of DTSTART, in the time zone specified // by PidLidTimeZoneStruct. This date is stored in minutes after // midnight Jan 1, 1601. Note that this value MUST always be // evenly divisible by 1440. // EndDate: Set to the start date of the last instance of a recurrence, in the // time zone specified by PidLidTimeZoneStruct. This date is // stored in minutes after midnight January 1, 1601. If the // recurrence is infinite, set EndDate to 0x5AE980DF. Note that // this value MUST always be evenly divisible by 1440, except for // the special value 0x5AE980DF. $startdate = clone $dtstart; $startdate->setTime(0, 0, 0); $startdate = self::date_php2mapi($startdate, true); $startdate = intval($startdate / 60); if ($mod = ($startdate % 1440)) { $startdate -= $mod; } // @TODO: get first occurrence of the event using libcalendaring_recurrence class ? } else { rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, - 'message' => "Found recurring $type without start date, skipping recurrence", + 'message' => "Found recurring {$this->model} without start date, skipping recurrence", ), true, false); - return; + return false; } $rule = (array) ($data['rrule'] ? $data['rrule']['recur'] : null); $result = array( 'Period' => $rule && $rule['interval'] ? $rule['interval'] : 1, 'FirstDOW' => self::day2bitmask($rule['wkst'] ?: 'MO'), 'OccurrenceCount' => 0x0000000A, 'StartDate' => $startdate, 'EndDate' => 0x5AE980DF, 'FirstDateTime' => $startdate, 'CalendarType' => kolab_api_filter_mapistore_structure_recurrencepattern::CALENDARTYPE_DEFAULT, 'ModifiedInstanceDates' => array(), 'DeletedInstanceDates' => array(), ); switch ($rule['freq']) { case 'DAILY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY; $result['Period'] *= 1440; break; case 'WEEKLY': // if BYDAY does not exist use day from DTSTART if (empty($rule['byday'])) { $rule['byday'] = strtoupper($startdate->format('S')); } $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_WEEKLY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK; $result['PatternTypeSpecific'] = self::day2bitmask($rule['byday'], 'BYDAY-'); break; case 'MONTHLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY; if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH; $result['PatternTypeSpecific'] = $month_day == -1 ? 0x0000001F : $month_day; } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; case 'YEARLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY; $result['Period'] *= 12; // MAPI doesn't support multi-valued months if ($rule['bymonth']) { // @TODO: set $startdate } if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'] = array(0, $month_day == -1 ? 0x0000001F : $month_day); } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; } + // [MS-OXOTASK] 2.2.2.2.15 says that tasks do not have Deleted/Modified instances + // does that mean there's also no exceptions support for tasks? It looks so. + $exception_info = array(); $extended_exception = array(); // Custom occurrences (RDATE) - if (!empty($data['rdate'])) { + if ($this->model != 'task' && !empty($data['rdate'])) { foreach ((array) $data['rdate']['date'] as $dt) { try { $dt = new DateTime($dt, $dtstart->getTimezone()); $dt->setTime(0, 0, 0); $dt = self::date_php2minutes($dt); $result['ModifiedInstanceDates'][] = $dt; $result['DeletedInstanceDates'][] = $dt; $exception_info[] = new kolab_api_filter_mapistore_structure_exceptioninfo(array( 'StartDateTime' => $dt, 'EndDateTime' => $dt + $object['PidLidAppointmentDuration'], 'OriginalStartDate' => $dt, 'OverrideFlags' => 0, )); $extended_exception[] = kolab_api_filter_mapistore_structure_extendedexception::get_empty(); } catch (Exception $e) { } } $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurenceCount'] = count($result['ModifiedInstanceDates']); // @FIXME: Kolab format says there can be RDATE and/or RRULE // MAPI specification says there must be RRULE if RDATE is specified if (!$result['RecurFrequency']) { $result['RecurFrequency'] = 0; $result['PatternType'] = 0; } } if ($rule && !empty($rule['until'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER; } else if ($rule && !empty($rule['count'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurrenceCount'] = $rule['count']; } else if (!isset($result['EndType'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER; } // calculate EndDate if ($rule && $result['EndType'] != kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER) { kolab_api_input_json::parse_recurrence($data, $res); if (!empty($res['recurrence'])) { try { $recurlib = libcalendaring::get_recurrence(); $recurlib->init($res['recurrence'], $dtstart); $end = $recurlib->end(); if ($end) { $result['EndDate'] = intval(self::date_php2mapi($end) / 60); } } catch (Exception $e) { rcube::raise_error($e, true, false); } } } // Deleted instances (EXDATE) - if (!empty($data['exdate'])) { + if ($this->model != 'task' && !empty($data['exdate'])) { if (!empty($data['exdate']['date'])) { $exceptions = (array) $data['exdate']['date']; } else if (!empty($data['exdate']['date-time'])) { $exceptions = (array) $data['exdate']['date-time']; } else { $exceptions = array(); } // convert date(-time)s to numbers foreach ($exceptions as $idx => $dt) { try { $dt = new DateTime($dt, $dtstart->getTimezone()); $dt->setTime(0, 0, 0); - $result['DeletedInstanceDates'][] = self::date_php2minutes($dt); + $dt = self::date_php2minutes($dt); + +// $result['ModifiedInstanceDates'][] = $dt; + $result['DeletedInstanceDates'][] = $dt; + } + catch (Exception $e) { + rcube::raise_error($e, true, false); + } + } + } + + // Exceptions + // @TODO: Support range=THISANDFUTURE + foreach ((array) $data['exceptions'] as $exception) { + if (!empty($exception['recurrence-id'])) { + try { + $dt = new DateTime($exception['recurrence-id'], $dtstart->getTimezone()); + $dt->setTime(0, 0, 0); + + // exception_info [MS-OXCICAL] 2.1.3.1.1.20.18 (page 61) + $exception_data = $this->exception_from_kolab($exception, $data, $object); + $exception_info[] = $exception_data[0]; + $extended_exception[] = $exception_data[1]; + + $result['ModifiedInstanceDates'][] = self::date_php2minutes($dt); + $object['PidTagHasAttachments'] = true; } catch (Exception $e) { + rcube::raise_error($e, true, false); } } } // [MS-OXCICAL] 2.1.3.1.1.20.13: Sort and make exceptions valid foreach (array('DeletedInstanceDates', 'ModifiedInstanceDates') as $key) { if (!empty($result[$key])) { sort($result[$key]); $result[$key] = array_values(array_unique(array_filter($result[$key]))); } } - $result = new kolab_api_filter_mapistore_structure_recurrencepattern($result); + $rp = new kolab_api_filter_mapistore_structure_recurrencepattern($result); - if ($type == 'task') { - return $result->output(true); + if ($this->model == 'task') { + $object['PidLidTaskRecurrence'] = $rp->output(true); + $object['PidLidTaskFRecurring'] = true; + return 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, + 'RecurrencePattern' => $rp, 'StartTimeOffset' => $offset, 'EndTimeOffset' => $offset + $object['PidLidAppointmentDuration'], 'ExceptionInfo' => $exception_info, 'ExtendedException' => $extended_exception, ); - $result = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern($arp); + $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern($arp); + + $object['PidLidAppointmentRecur'] = $arp->output(true); - return $result->output(true); + return true; } /** * Convert MAPI recurrence into Kolab (MS-OXICAL: 2.1.3.2.2) * * @param string $rule MAPI binary representation of recurrence rule * @param array $object Kolab object */ protected function recurrence_to_kolab($rule, &$object) { if (empty($rule)) { return array(); } // parse binary (Appointment)RecurrencePattern if ($this->model == 'event') { $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern(); $arp->input($rule, true); $rp = $arp->RecurrencePattern; } else { $rp = new kolab_api_filter_mapistore_structure_recurrencepattern(); $rp->input($rule, true); } $result = array( 'interval' => $rp->Period, ); switch ($rp->PatternType) { case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY: $result['freq'] = 'DAILY'; $result['interval'] /= 1440; if ($arp) { $result['byhour'] = floor($arp->StartTimeOffset / 60); $result['byminute'] = $arp->StartTimeOffset - $result['byhour'] * 60; } break; case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK: $result['freq'] = 'WEEKLY'; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific); if ($rp->Period >= 1) { $result['wkst'] = self::bitmask2day($rp->FirstDOW); } break; default: // monthly/yearly $evenly_divisible = $rp->Period % 12 == 0; switch ($rp->PatternType) { case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH: case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHEND: $result['freq'] = $evenly_divisible ? 'YEARLY' : 'MONTHLY'; break; case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH: case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_HJMONTHNTH: $result['freq'] = $evenly_divisible ? 'YEARLY-NTH' : 'MONTHLY-NTH'; break; default: // not-supported return; } if ($result['freq'] = 'MONTHLY') { $rule['bymonthday'] = intval($rp->PatternTypeSpecific == 0x0000001F ? -1 : $rp->PatternTypeSpecific); } else if ($result['freq'] = 'MONTHLY-NTH') { $result['freq'] = 'MONTHLY'; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific[0]); if ($rp->PatternTypeSpecific[1]) { $result['bysetpos'] = intval($rp->PatternTypeSpecific[1] == 0x00000005 ? -1 : $rp->PatternTypeSpecific[1]); } } else if ($result['freq'] = 'YEARLY') { $result['interval'] /= 12; $rule['bymonthday'] = intval($rp->PatternTypeSpecific == 0x0000001F ? -1 : $rp->PatternTypeSpecific); $rule['bymonth'] = 0;// @TODO: month from FirstDateTime } else if ($result['freq'] = 'YEARLY-NTH') { $result['freq'] = 'YEARLY'; $result['interval'] /= 12; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific[0]); $result['bymonth'] = 0;// @TODO: month from FirstDateTime if ($rp->PatternTypeSpecific[1]) { $result['bysetpos'] = intval($rp->PatternTypeSpecific[1] == 0x00000005 ? -1 : $rp->PatternTypeSpecific[1]); } } } if ($rp->EndType == kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER) { // @TODO: set UNTIL to EndDate + StartTimeOffset, or the midnight of EndDate } else if ($rp->EndType == kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC) { $result['count'] = $rp->OccurrenceCount; } if ($result['interval'] == 1) { unset($result['interval']); } $object['rrule']['recur'] = $result; $object['exdate'] = array(); $object['rdate'] = array(); // $exception_info = (array) $rp->ExceptionInfo; // $extended_exception = (array) $rp->ExtendedException; $modified_dates = (array) $rp->ModifiedInstanceDates; $deleted_dates = (array) $rp->DeletedInstanceDates; // Deleted/Modified exceptions (EXDATE/RDATE) foreach ($deleted_dates as $date) { $idx = in_array($date, $modified_dates) ? 'rdate' : 'exdate'; $dt = self::date_minutes2php($date); if ($dt) { $object[$idx]['date'][] = $dt->format('Y-m-d'); } } } + /** + * Convert Kolab exception specification into MAPI exception + * + * @param array $exception Kolab event exception data + * @param array $event Kolab event data + * @param array $object Main event data (MAPI format) + * + * @return array Contains exceptioninfo and extendedexception structures + */ + protected function exception_from_kolab($exception, $event, &$object) + { + if ($this->model != 'event') { + throw new Exception("Exceptions are not supported on {$this->model} objects."); + } + + $ex = $this->output($exception); + + // exception_info [MS-OXCICAL] 2.1.3.1.1.20.18 (page 61) + $map = array( + 'Subject' => 'PidTagSubject', + 'ReminderDelta' => 'PidLidReminderDelta', + 'ReminderSet' => 'PidLidReminderSet', + 'Location' => 'PidLidLocation', + 'BusyStatus' => 'PidLidBusyStatus', + 'SubType' => 'PidLidAppointmentSubType', + // 'AppointmentColor', 'Attachment', 'MeetingType' + ); + + // OriginalStartDate is a date-time at which the instance would have occured + // if it were not an exception (in event timezone) + + $exception_date = kolab_api_input_json::to_datetime($exception['recurrence-id']); + if (!$exception_date) { + throw new Exception("Recurrence exception with no date?. Skipping."); + } + + $event_start = kolab_api_input_json::to_datetime($event['dtstart']); + $orig_start_dt = clone $event_start; + $orig_start_dt->setDate($exception_date->format('Y'), $exception_date->format('n'), $exception_date->format('j')); + + if (!empty($exception['dtstart'])) { + $start_dt = kolab_api_input_json::to_datetime($exception['dtstart']); + } + else { + $start_dt = $orig_start_dt; + } + + if (!empty($exception['dtend'])) { + $end_dt = kolab_api_input_json::to_datetime($exception['dtend']); + // for all-day events dtstart == dtend, move dtend to the next day + if ($end_dt <= $start_dt) { + $end_dt->add(new DateInterval("PT24H")); + } + } + else if ($minutes = $object['PidLidAppointmentDuration']) { + $end_dt = clone $start_dt; + $end_dt->add(new DateInterval("PT{$minutes}M")); + } + + $exception_info = array( + 'StartDateTime' => $this->date_php2minutes($start_dt), + 'EndDateTime' => $this->date_php2minutes($end_dt), + 'OriginalStartDate' => $this->date_php2minutes($orig_start_dt), + ); + + $extended_exception = array( + // @TODO: set ChangeHighligh appropriately + 'ChangeHighlight' => new kolab_api_filter_mapistore_structure_changehighlight(), + ); + + foreach ($map as $key => $prop) { + if (isset($ex[$prop]) && $ex[$prop] != $object[$prop]) { + $exception_info[$key] = $ex[$prop]; + + if ($key == 'Location' || $key == 'Subject') { + $extended_exception["WideChar$key"] = $ex[$prop]; + } + } + } + + // some properties are needed when subject or location changed + if (count($extended_exception) > 1) { + $extended_exception['StartDateTime'] = $exception_info['StartDateTime']; + $extended_exception['EndDateTime'] = $exception_info['EndDateTime']; + $extended_exception['OriginalStartDate'] = $exception_info['OriginalStartDate']; + } + + $exception_info = new kolab_api_filter_mapistore_structure_exceptioninfo($exception_info); + $extended_exception = new kolab_api_filter_mapistore_structure_extendedexception($extended_exception); + + return array($exception_info, $extended_exception); + } + /** * Convert Kolab description property into MAPI body properties * * @param array $data Kolab object * @param array $result Object data (MAPI format) * @param string $field Kolab object property name * @param string $force Force output format (plain|html) */ protected function body_from_kolab($data, &$result, $field = 'description', $force = null) { $text = $data[$field]; if (self::is_html($text)) { // some objects does not support HTML e.g. notes if ($force == 'plain') { $h2t = new rcube_html2text($text, false, false, 0); $result['PidTagBody'] = trim($h2t->get_text()); } else { $result['PidTagHtml'] = $text; } } else if ($text) { $result['PidTagBody'] = $text; } } /** * Convert MAPI body properties into Kolab * * @param string $data MAPI object * @param array $result Kolab object * @param string $field Kolab object property name * @param string $force Force output format (plain|html) */ protected function body_to_kolab($data, &$result, $field = 'description', $force = null) { // Kolab supports HTML and plain text but not RTF if (array_key_exists('PidTagRtfCompressed', $data)) { require_once 'rtf.php'; $rtf = new rtf(base64_decode($data['PidTagRtfCompressed'])); $text = $rtf->parse($force ?: 'html'); } else if (array_key_exists('PidTagHtml', $data)) { $text = $data['PidTagHtml']; // some objects does not support HTML e.g. contacts if ($force == 'plain') { $h2t = new rcube_html2text($text, false, false, 0); $text = trim($h2t->get_text()); } } else if (array_key_exists('PidTagBody', $data)) { $text = $data['PidTagBody']; } if (isset($text)) { $result[$field] = $text; } } /** * Returns number of minutes between midnight 1601-01-01 * and specified UTC DateTime */ public static function date_php2minutes($date) { $start = new DateTime('1601-01-01 00:00:00 UTC'); // make sure the specified date is in UTC $date->setTimezone(new DateTimeZone('UTC')); return (int) round(($date->getTimestamp() - $start->getTimestamp()) / 60); } /** * Convert number of minutes between midnight 1601-01-01 (UTC) into PHP DateTime * * @return DateTime|bool DateTime object or False on failure */ public static function date_minutes2php($minutes) { $datetime = new DateTime('1601-01-01 00:00:00 UTC'); $interval = new DateInterval(sprintf('PT%dM', $minutes)); return $datetime->add($interval); } /** * Convert DateTime object to MAPI date format */ public static function date_php2mapi($date, $utc = true, $time = null) { // convert string to DateTime if (!is_object($date) && !empty($date)) { // convert date to datetime on 00:00:00 if (preg_match('/^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/', $date, $m)) { $date = $m[1] . '-' . $m[2] . '-' . $m[3] . 'T00:00:00+00:00'; } $date = new DateTime($date); } - else if (is_object($date) && $utc) { - // clone the date object if we're going to change timezone - $date = clone $date; + else if (is_object($date)) { + if ($utc) { + // clone the date object if we're going to change timezone + $date = clone $date; + } } else { return; } if ($utc) { $date->setTimezone(new DateTimeZone('UTC')); } if (!empty($time)) { $date->setTime((int) $time['hour'], (int) $time['minute'], (int) $time['second']); } // MAPI PTypTime is 64-bit integer representing the number // of 100-nanosecond intervals since January 1, 1601. // Mapistore format for this type is a float number // seconds since 1601-01-01 00:00:00 $seconds = floatval($date->format('U')) + 11644473600; /* if ($microseconds = intval($date->format('u'))) { $seconds += $microseconds/1000000; } */ return $seconds; } /** * Convert date-time from MAPI format to DateTime */ public static function date_mapi2php($date) { $seconds = floatval(sprintf('%.0f', $date)); // assumes we're working with dates after 1970-01-01 $dt = new DateTime('@' . intval($seconds - 11644473600), new DateTimeZone('UTC')); /* if ($microseconds = intval(($date - $seconds) * 1000000)) { $dt = new DateTime($dt->format('Y-m-d H:i:s') . '.' . $microseconds, $dt->getTimezone()); } */ return $dt; } /** * Setting PidTagRecipientType according to [MS-OXCICAL 2.1.3.1.1.20.2] */ protected function to_recipient_type($cutype, $role) { if ($cutype && in_array($cutype, array('RESOURCE', 'ROOM'))) { return 0x00000003; } if ($role && ($type = $this->recipient_type_map[$role])) { return $type; } return 0x00000001; } /** * Converts string of days (TU,TH) to bitmask used by MAPI * * @param string $days * * @return int */ protected static function day2bitmask($days, $prefix = '') { $days = explode(',', $days); $result = 0; foreach ($days as $day) { $result = $result + self::$recurrence_day_map[$prefix.$day]; } return $result; } /** * Convert bitmask used by MAPI to string of days (TU,TH) * * @param int $days * * @return string */ protected static function bitmask2day($days) { $days_arr = array(); foreach (self::$recurrence_day_map as $day => $bit) { if (($days & $bit) === $bit) { $days_arr[] = preg_replace('/^BYDAY-/', '', $day); } } $result = implode(',', $days_arr); return $result; } /** * Determine whether the given event description is HTML formatted */ protected static function is_html($text) { // check for opening and closing or tags return preg_match('/<(html|body)(\s+[a-z]|>)/', $text, $m) && strpos($text, '') > 0; } } diff --git a/lib/filter/mapistore/contact.php b/lib/filter/mapistore/contact.php index 91fc0cf..c0aaff2 100644 --- a/lib/filter/mapistore/contact.php +++ b/lib/filter/mapistore/contact.php @@ -1,696 +1,696 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_contact extends kolab_api_filter_mapistore_common { const PHOTO_ATTACHMENT_ID = 9999; protected $model = 'contact'; protected $map = array( // contact name properties [MS-OXOCNTC] 'PidTagNickname' => 'nickname', // PtypString 'PidTagGeneration' => 'n.suffix', // PtypString 'PidTagDisplayNamePrefix' => 'n.prefix', // PtypString 'PidTagSurname' => 'n.surname', // PtypString 'PidTagMiddleName' => 'n.additional', // PtypString 'PidTagGivenName' => 'n.given', // PtypString 'PidTagInitials' => 'x-custom.MAPI:PidTagInitials', // PtypString 'PidTagDisplayName' => 'fn', // PtypString 'PidLidYomiFirstName' => '', // PtypString 'PidLidYomiLastName' => '', // PtypString 'PidLidFileUnder' => '', // PtypString 'PidLidFileUnderId' => '', // PtypInteger32 'PidLidFileUnderList' => '', // PtypMultipleInteger32 // electronic and phisical address properties 'PidTagPrimaryFaxNumber' => 'x-custom.MAPI:PidTagPrimaryFaxNumber', // PtypString 'PidTagBusinessFaxNumber' => 'x-custom.MAPI:PidTagBusinessFaxNumber', // PtypString 'PidTagHomeFaxNumber' => '', // PtypString 'PidTagHomeAddressStreet' => '', // PtypString 'PidTagHomeAddressCity' => '', // PtypString 'PidTagHomeAddressStateOrProvince' => '', // PtypString 'PidTagHomeAddressPostalCode' => '', // PtypString 'PidTagHomeAddressCountry' => '', // PtypString 'PidLidHomeAddressCountryCode' => '', // PtypString 'PidTagHomeAddressPostOfficeBox' => '', // PtypString 'PidLidHomeAddress' => '', // @TODO: ? 'PidLidWorkAddressStreet' => '', // PtypString 'PidLidWorkAddressCity' => '', // PtypString 'PidLidWorkAddressState' => '', // PtypString 'PidLidWorkAddressPostalCode' => '', // PtypString 'PidLidWorkAddressCountry' => '', // PtypString 'PidLidWorkAddressCountryCode' => '', // PtypString 'PidLidWorkAddressPostOfficeBox' => '', // PtypString 'PidLidWorkAddress' => '', // @TODO: ? 'PidTagOtherAddressStreet' => '', // PtypString 'PidTagOtherAddressCity' => '', // PtypString 'PidTagOtherAddressStateOrProvince' => '', // PtypString 'PidTagOtherAddressPostalCode' => '', // PtypString 'PidTagOtherAddressCountry' => '', // PtypString 'PidLidOtherAddressCountryCode' => '', // PtypString 'PidTagOtherAddressPostOfficeBox' => '', // PtypString 'PidLidOtherAddress' => '', // @TODO: ? // PtypString 'PidTagStreetAddress' => '', // @TODO: ? // PtypString 'PidTagLocality' => '', // @TODO: ? // PtypString 'PidTagStateOrProvince' => '', // @TODO: ? // PtypString 'PidTagPostalCode' => '', // @TODO: ? // PtypString 'PidTagCountry' => '', // @TODO: ? // PtypString 'PidLidAddressCountryCode' => '', // @TODO: ? // PtypString 'PidTagPostOfficeBox' => '', // @TODO: ? // PtypString 'PidTagPostalAddress' => '', // @TODO: ? // PtypString 'PidLidPostalAddressId' => '', // PtypInteger32 'PidTagPagerTelephoneNumber' => '', // PtypString 'PidTagCallbackTelephoneNumber' => '', // PtypString 'PidTagBusinessTelephoneNumber' => '', // PtypString 'PidTagHomeTelephoneNumber' => '', // PtypString 'PidTagPrimaryTelephoneNumber' => '', // PtypString 'PidTagBusiness2TelephoneNumber' => '', // PtypString 'PidTagMobileTelephoneNumber' => '', // PtypString 'PidTagRadioTelephoneNumber' => '', // PtypString 'PidTagCarTelephoneNumber' => '', // PtypString 'PidTagOtherTelephoneNumber' => '', // PtypString 'PidTagAssistantTelephoneNumber' => '', // PtypString 'PidTagHome2TelephoneNumber' => 'x-custom.MAPI:PidTagHome2TelephoneNumber', 'PidTagTelecommunicationsDeviceForDeafTelephoneNumber' => 'x-custom.MAPI:PidTagTelecommunicationsDeviceForDeafTelephoneNumber', 'PidTagCompanyMainTelephoneNumber' => 'x-custom.MAPI:PidTagCompanyMainTelephoneNumber', 'PidTagTelexNumber' => '', // PtypString 'PidTagIsdnNumber' => '', // PtypString 'PidLidAddressBookProviderEmailList' => '', // PtypMultipleInteger32, @TODO: ? 'PidLidAddressBookProviderArrayType' => '', // PtypInteger32, @TODO: ? // event properties 'PidTagBirthday' => 'bday', // PtypTime, UTC 'PidLidBirthdayLocal' => '', // PtypTime, @TODO 'PidLidBirthdayEventEntryId' => '', // PtypBinary 'PidTagWeddingAnniversary' => 'anniversary', // PtypTime, UTC 'PidLidWeddingAnniversaryLocal' => '', // PtypTime, @TODO 'PidLidAnniversaryEventEntryId' => '', // PtypBinary // professional properties 'PidTagTitle' => 'title', // PtypString 'PidTagCompanyName' => '', // PtypString 'PidLidYomiCompanyName' => '', // PtypString 'PidTagDepartmentName' => '', // PtypString 'PidTagOfficeLocation' => 'x-custom.MAPI:PidTagOfficeLocation', // PtypString 'PidTagManagerName' => '', // PtypString 'PidTagAssistant' => '', // PtypString 'PidTagProfession' => 'group.role', // PtypString 'PidLidHasPicture' => '', // PtypBoolean, more about photo attachments in MS-OXOCNTC // other properties 'PidTagHobbies' => 'x-custom.MAPI:PidTagHobbies', // PtypString 'PidTagSpouseName' => '', // PtypString 'PidTagLanguage' => 'lang', // PtypString 'PidTagLocation' => 'x-custom.MAPI:PidTagLocation', // PtypString 'PidLidInstantMessagingAddress' => 'impp', // PtypString 'PidTagOrganizationalIdNumber' => 'x-custom.MAPI:PidTagOrganizationalIdNumber',// PtypString 'PidTagCustomerId' => 'x-custom.MAPI:PidTagCustomerId', // PtypString 'PidTagGovernmentIdNumber' => 'x-custom.MAPI:PidTagGovernmentIdNumber',// PtypString 'PidTagPersonalHomePage' => 'url', // PtypString 'PidTagBusinessHomePage' => 'x-custom.MAPI:PidTagBussinessHomePage', // PtypString 'PidTagFtpSite' => 'x-custom.MAPI:PidTagFtpSite', // PtypString 'PidTagReferredByName' => 'x-custom.MAPI:PidTagReferredByName', // PtypString 'PidLidBilling' => 'x-custom.MAPI:PidLidBilling', // PtypString 'PidLidFreeBusyLocation' => 'fburl', // PtypString 'PidTagChildrenNames' => '', // PtypMultipleString 'PidTagGender' => 'gender', // PtypString 'PidTagUserX509Certificate' => 'key', // PtypMultipleBinary 'PidTagMessageClass' => '', // PtypString: IPM.Contact, IPM.DistList 'PidTagBody' => '', // 'note' // PtypString // contact aggregation properties - skipped 'PidTagLastModificationTime' => 'rev', // PtypTime // 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', ); protected $address_ids = array( 'home' => 0x00000001, 'work' => 0x00000002, 'other' => 0x00000003, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => $data['kind'] == 'group' ? 'IPM.DistList' : 'IPM.Contact', // mapistore REST API specific properties 'collection' => 'contacts', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case '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 = $this->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; } // notes can be in plain text format only? $this->body_from_kolab($data, $result, 'note'); // 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; // @FIXME: should we set PidTagHasAttachments? } // 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']; // @TODO: This may be addr-spec (RFC5322), we should parse it // and fill also *AddressType and *DisplayName $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']; $pref = $addr['parameters']['pref']; $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)); if (!empty($pref)) { $result['PidLidPostalAddressId'] = $this->address_ids[$type]; } } } $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; } } // Group members $this->members_from_kolab($data, $result); $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 = $this->date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidTagLastModificationTime': if ($value) { $value = $this->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; } $this->set_kolab_value($result, $kolab_idx, $value); } // notes $this->body_to_kolab($data, $result, 'note', 'plain'); if (!empty($data['PidTagMessageClass'])) { $result['kind'] = stripos($data['PidTagMessageClass'], 'IPM.DistList') === 0 ? 'group' : 'individual'; } // 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']); } // Preferred (mailing) address if ($data['PidLidPostalAddressId']) { $map = array_flip($this->address_ids); $pref = $map[$data['PidLidPostalAddressId']]; } // 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')); if ($pref == 'home') { $type['parameters']['pref'] = 1; } $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')); if ($pref == 'work') { $type['parameters']['pref'] = 1; } $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)) { $type = array(); if ($pref == 'other') { $type['parameters']['pref'] = 1; } $result['group']['adr'] = array_merge($address, $type); } // Group members $this->members_to_kolab($data, $result); $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidTagBody'] = 'note'; return $map; } /** * Return attachment data for photo of the contact * * @param array $contact Contact data * * @return array Attachment data */ public static function photo_attachment($contact) { if (!empty($contact['photo'])) { $bin_data = $contact['photo']; $mimetype = rcube_mime::file_content_type($bin_data, 'ContactPicture.jpg', 'image/jpeg', true); list(, $type) = explode('/', $mimetype); $ext = $type == 'jpeg' ? 'jpg' : $type; // @TODO: Do we need to convert to JPEG? // [MS-OXOCNTC]: The value of the PidTagAttachDataBinary property, // which is the contents of the attachment, SHOULD be in JPEG format. // Support for other formats is as determined by the implementer. $attachment = array( - 'is_photo' => true, + 'is-photo' => true, 'filename' => 'ContactPicture.' . $ext, 'size' => strlen($bin_data), 'content' => $bin_data, 'mimetype' => $mimetype, 'id' => self::PHOTO_ATTACHMENT_ID, ); if ($attachment['size']) { return $attachment; } } } /** * Convert Kolab members list into MAPI properties */ protected function members_from_kolab($data, &$result) { // @TODO foreach ((array) $data['member'] as $member) { } } /** * Convert MAPI properties into Kolab member array */ protected function members_to_kolab($data, &$result) { // @TODO } } diff --git a/lib/filter/mapistore/event.php b/lib/filter/mapistore/event.php index 2f8903e..2352f7b 100644 --- a/lib/filter/mapistore/event.php +++ b/lib/filter/mapistore/event.php @@ -1,467 +1,549 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_event extends kolab_api_filter_mapistore_common { + const EXCEPTION_CLASS = 'IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}'; + protected $model = 'event'; protected $map = array( // common properties [MS-OXOCAL] 'PidLidAppointmentSequence' => 'sequence', // PtypInteger32 'PidLidBusyStatus' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-BUSYSTATUS 'PidLidAppointmentAuxiliaryFlags' => '', // PtypInteger32 'PidLidLocation' => 'location', // PtypString 'PidLidAppointmentStartWhole' => 'dtstart', // PtypTime, UTC 'PidLidAppointmentEndWhole' => 'dtend', // PtypTime, UTC 'PidLidAppointmentDuration' => '', // PtypInteger32, optional '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' => '', 'PidTagHtml' => '', 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagContentId' => '', 'PidTagBodyContentLocation' => '', 'PidTagImportance' => 'priority', 'PidTagSensitivity' => 'class', 'PidLidPrivate' => '', - + 'PidTagHasAttachments' => 'attach', 'PidTagCreationTime' => 'created', 'PidTagLastModificationTime' => 'dtstamp', // reminder properties [MS-OXORMDR] 'PidLidReminderSet' => '', // PtypBoolean 'PidLidReminderSignalTime' => '', // PtypTime 'PidLidReminderDelta' => '', // PtypInteger32 'PidLidReminderTime' => '', // PtypTime 'PidLidReminderOverride' => '', // PtypBoolean 'PidLidReminderPlaySound' => '', // PtypBoolean 'PidLidReminderFileParameter' => '', // PtypString 'PidTagReplyTime' => '', // PtypTime 'PidLidReminderType' => '', // PtypInteger32 ); /** * Message importance for PidTagImportance as defined in [MS-OXCMSG] */ protected $importance = array( 0 => 0x00000000, 1 => 0x00000002, 2 => 0x00000002, 3 => 0x00000002, 4 => 0x00000002, 5 => 0x00000001, 6 => 0x00000000, 7 => 0x00000000, 8 => 0x00000000, 9 => 0x00000000, ); /** * Message sesnitivity for PidTagSensitivity as defined in [MS-OXCMSG] */ protected $sensitivity = array( 'public' => 0x00000000, 'personal' => 0x00000001, 'private' => 0x00000002, 'confidential' => 0x00000003, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( - 'PidTagMessageClass' => 'IPM.Appointment', + 'PidTagMessageClass' => !empty($data['recurrence-id']) ? self::EXCEPTION_CLASS : 'IPM.Appointment', // mapistore REST API specific properties 'collection' => 'calendars', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagSensitivity': $value = (int) $this->sensitivity[strtolower($value)]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = $this->date_php2mapi($value, true); break; case 'PidTagImportance': $value = (int) $this->importance[(int) $value]; break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': $dt = kolab_api_input_json::to_datetime($value); $value = $this->date_php2mapi($dt, true); // this is all-day event if ($dt->_dateonly) { $result['PidLidAppointmentSubType'] = 0x00000001; } else if (empty($data['rrule']) && $dt->getTimezone()->getName() != 'UTC') { $idx = sprintf('PidLidAppointmentTimeZoneDefinition%sDisplay', strpos($mapi_idx, 'Start') ? 'Start' : 'End'); $result[$idx] = $this->timezone_definition($dt); } break; + case 'PidTagHasAttachments': + $value = !empty($value); + break; } $result[$mapi_idx] = $value; } // event description $this->body_from_kolab($data, $result); - // fix end dat of all-day event + // fix end date-time of all-day event if ($result['PidLidAppointmentSubType'] && $result['PidLidAppointmentStartWhole'] && $result['PidLidAppointmentStartWhole'] == $result['PidLidAppointmentEndWhole'] ) { $result['PidLidAppointmentEndWhole'] += 24 * 60 * 60; } // 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); } // PidLidAppointmentDuration if ($result['PidLidAppointmentStartWhole'] && $result['PidLidAppointmentEndWhole']) { $result['PidLidAppointmentDuration'] = (int) round(($result['PidLidAppointmentEndWhole'] - $result['PidLidAppointmentStartWhole']) / 60); } else if ($result['PidLidAppointmentStartWhole'] && $data['duration']) { try { $interval = new DateInterval($data['duration']); $duration = min(24 * 60, $interval->i + $interval->h * 60 + $interval->d * 24 * 60); $result['PidLidAppointmentDuration'] = $duration; $result['PidLidAppointmentEndWhole'] = $result['PidLidAppointmentStartWhole'] + $duration * 60; } catch (Exception $e) { - rcube::raise_error(array( - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => $e->getMessage(), - ), true, false); + rcube::raise_error($e, true, false); } } - // @TODO: exceptions, resources + // @TODO: resources? // Recurrence - if ($rule = $this->recurrence_from_kolab($data, $result)) { - $result['PidLidAppointmentRecur'] = $rule; + if ($this->recurrence_from_kolab($data, $result)) { if ($dt && $dt->getTimezone()->getName() != 'UTC') { $result['PidLidTimeZoneStruct'] = $this->timezone_structure($dt); $result['PidLidTimeZoneDescription'] = $this->timezone_description($dt); } } // Alarms (MAPI supports only one) $this->alarm_from_kolab($data, $result); $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); if ($data['PidLidTimeZoneStruct']) { $timezone = $this->timezone_structure_to_tzname($data['PidLidTimeZoneStruct']); } foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagImportance': $map = array( 0x00000002 => 1, 0x00000001 => 5, 0x00000000 => 9, ); $value = (int) $map[(int) $value]; break; case 'PidTagSensitivity': $map = array_flip($this->sensitivity); $value = $map[$value]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': if ($value) { $datetime = $this->date_mapi2php($value); $datetime->_dateonly = !empty($data['PidLidAppointmentSubType']); $tz_idx = sprintf('PidLidAppointmentTimeZoneDefinition%sDisplay', strpos($mapi_idx, 'Start') ? 'Start' : 'End'); if ($data[$tz_idx]) { $tz = $this->timezone_definition_to_tzname($data[$tz_idx]); } else { $tz = $timezone; } $value = kolab_api_input_json::from_datetime($datetime, $tz); } break; } $result[$kolab_idx] = $value; } // event description $this->body_to_kolab($data, $result); // Recurrence if (array_key_exists('PidLidAppointmentRecur', $data)) { $this->recurrence_to_kolab($data['PidLidAppointmentRecur'], $result); } // Alarms (MAPI supports only one, DISPLAY) $this->alarm_to_kolab($data, $result); 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: exception, resources $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); // @TODO: add properties that are not in the map $map['PidLidAppointmentRecur'] = 'rrule'; $map['PidTagBody'] = 'description'; return $map; } /** * Generate PidLidTimeZoneDescription string for a timezone * specified in a DateTime object */ protected static function timezone_description($datetime) { $timezone = $datetime->getTimezone(); $location = $timezone->getLocation(); $description = $location['comments']; $offset = $timezone->getOffset($datetime); // some location descriptions are useful, but some are not really // replace with timezone name in such cases if (!$description || strpos($description, 'location') !== false) { $description = $timezone->getName(); } if ($description == 'Z') { $description = 'UTC'; } // convert seconds into hours offset format $hours = round(abs($offset)/3600); $minutes = round((abs($offset) - $hours * 3600) / 60); $offset = sprintf('%s%02d:%02d', $offset < 0 ? '-' : '+', $hours, $minutes); return sprintf('(GMT%s) %s', $offset, $description); } /** * Generate PidLidTimeZoneDefinitionRecur blob for a timezone * specified in a DateTime object */ protected static function timezone_definition($datetime) { $timezone = $datetime->getTimezone(); $tzrule = kolab_api_filter_mapistore_structure_tzrule::from_datetime($datetime); $tzrule->Flags = kolab_api_filter_mapistore_structure_tzrule::FLAG_EFFECTIVE; // @FIXME $tzdef = new kolab_api_filter_mapistore_structure_timezonedefinition(array( 'TZRules' => array($tzrule), 'KeyName' => $timezone->getName(), )); return $tzdef->output(true); } /** * Generate PidLidTimeZoneStruct blob for a timezone * specified in a DateTime object */ protected static function timezone_structure($datetime) { $tzs = kolab_api_filter_mapistore_structure_timezonestruct::from_datetime($datetime, true); return $tzs->output(true); } /** * Parse PidLidTimeZoneStruct blob and convert to timezone name */ protected static function timezone_structure_to_tzname($data) { $api = kolab_api::get_instance(); $tzs = new kolab_api_filter_mapistore_structure_timezonestruct; $tzs->input($data, true); return $tzs->to_tzname($api->config->get('timezone')); } /** * Parse PidLidTimeZoneDefinitionRecur blob and convert to timezone name */ protected static function timezone_definition_to_tzname($data) { $api = kolab_api::get_instance(); $tzdef = new kolab_api_filter_mapistore_structure_timezonedefinition; $tzdef->input($data, true); // Note: we ignore KeyName as it most likely will not contain Olson TZ name foreach ($tzdef->TZRules as $tzrule) { if ($tzname = $tzrule->to_tzname($api->config->get('timezone'))) { return $tzname; } } } + + /** + * Return attachments for event exceptions + * + * @param array $event Event object data (API format) + * + * @return array Attachments list + */ + public function exception_attachments($event) + { + $attachments = array(); + + foreach ((array) $event['exceptions'] as $exception) { + $rid = kolab_api_input_json::to_datetime($exception['recurrence-id']); + if (!$rid) { + // sanity check + continue; + } + + // @TODO: the object should be of type PtypObject encoded in a way + // described in https://msdn.microsoft.com/en-us/library/ee218131%28v=exchg.80%29.aspx + // Would be nice if Openchange would generate the blob on their side + $object = $this->output($exception); + + // remove internal properties + unset($object['collection']); + + $start_time = $end_time = null; + + if (!empty($exception['dtstart'])) { + $dt = kolab_api_input_json::to_datetime($exception['dtstart']); + $start_time = $this->date_php2mapi($dt, false); + } + + if (!empty($exception['dtend'])) { + $dt = kolab_api_input_json::to_datetime($exception['dtend']); + $end_time = $this->date_php2mapi($dt, false); + + if ($end_time == $start_time) { + $end_time += 24 * 60 * 60; + } + } + + // "replace-time" is a date-time at which the instance would have occured + // if it were not an exception (in UTC) + $dtstart = kolab_api_input_json::to_datetime($event['dtstart']); + $dtstart->setDate($rid->format('Y'), $rid->format('n'), $rid->format('j')); + $replace_time = $this->date_php2mapi($dtstart, true); + + $attachments[] = array( + 'id' => self::recurrence_id($rid, $event['dtstart']), + 'filename' => $object['PidTagSubject'] ?: $event['summary'], + 'data-object' => $object, + 'start-time' => $start_time, + 'end-time' => $end_time, + 'replace-time' => $replace_time, + // properties required for exception attachments + 'is-hidden' => true, + 'rendering-position' => 0xFFFFFFFF, + 'attachment-flags' => 0x00000002, + ); + } + + return $attachments; + } + + /** + * Generate recurrence-id identifier for exception attachment + * + * @param DateTime Recurrence date (recurrence-id) + * @param DateTime|array|string Event start date-time + * + * @return int Attachment identifier + */ + public static function recurrence_id($date, $start_date) + { + if (!$start_date instanceof DateTime) { + $start_date = kolab_api_input_json::to_datetime($start_date); + } + + return $date->format($start_date->_dateonly ? 'Ymd' : 'YmdHi'); + } } diff --git a/lib/filter/mapistore/task.php b/lib/filter/mapistore/task.php index cb27330..a0bf8c2 100644 --- a/lib/filter/mapistore/task.php +++ b/lib/filter/mapistore/task.php @@ -1,301 +1,298 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_task extends kolab_api_filter_mapistore_common { protected $model = 'task'; protected $map = array( // task specific props [MS-OXOTASK] 'PidTagProcessed' => '', // PtypBoolean 'PidLidTaskMode' => '', // ignored 'PidLidTaskStatus' => '', // PtypInteger32 'PidLidPercentComplete' => 'percent-complete', // PtypFloating64 'PidLidTaskStartDate' => 'dtstart', // PtypTime 'PidLidTaskDueDate' => 'due', // PtypTime 'PidLidTaskResetReminder' => '', // @TODO // PtypBoolean 'PidLidTaskAccepted' => '', // @TODO // PtypBoolean 'PidLidTaskDeadOccurrence' => '', // @TODO // PtypBoolean 'PidLidTaskDateCompleted' => 'x-custom.MAPI:PidLidTaskDateCompleted', // PtypTime 'PidLidTaskLastUpdate' => '', // PtypTime 'PidLidTaskActualEffort' => 'x-custom.MAPI:PidLidTaskActualEffort', // PtypInteger32 'PidLidTaskEstimatedEffort' => 'x-custom.MAPI:PidLidTaskEstimatedEffort', // PtypInteger32 'PidLidTaskVersion' => '', // PtypInteger32 'PidLidTaskState' => '', // PtypInteger32 'PidLidTaskRecurrence' => '', // 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' => '', 'PidTagHtml' => '', 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagMessageClass' => '', 'PidLidCommonStart' => 'dtstart', 'PidLidCommonEnd' => 'due', 'PidTagIconIndex' => '', // @TODO 'PidTagCreationTime' => 'created', // PtypTime, UTC 'PidTagLastModificationTime' => 'dtstamp', // PtypTime, UTC ); /** * Values for PidLidTaskStatus property */ protected $status_map = array( 'none' => 0x00000000, // PidLidPercentComplete = 0 'in-progress' => 0x00000001, // PidLidPercentComplete > 0 and PidLidPercentComplete < 1 'complete' => 0x00000002, // PidLidPercentComplete = 1 'waiting' => 0x00000003, 'deferred' => 0x00000004, ); /** * Values for PidLidTaskHistory property */ protected $history_map = array( 'none' => 0x00000000, 'accepted' => 0x00000001, 'rejected' => 0x00000002, 'changed' => 0x00000003, 'due-changed' => 0x00000004, 'assigned' => 0x00000005, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => 'IPM.Task', // mapistore REST API specific properties 'collection' => 'tasks', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidLidPercentComplete': $value /= 100; break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': $value = $this->date_php2mapi($value, false, array('hour' => 0)); break; case 'PidLidCommonStart': case 'PidLidCommonEnd': // case 'PidLidTaskLastUpdate': case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = $this->date_php2mapi($value, true); break; case 'PidLidTaskActualEffort': case 'PidLidTaskEstimatedEffort': $value = (int) $value; break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // task description $this->body_from_kolab($data, $result); // 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 - if ($rule = $this->recurrence_from_kolab($data, $result)) { - $result['PidLidTaskRecurrence'] = $rule; - $result['PidLidTaskFRecurring'] = true; - } + $this->recurrence_from_kolab($data, $result); // Alarms (MAPI supports only one) $this->alarm_from_kolab($data, $result); $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 = $this->date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidLidCommonStart': case 'PidLidCommonEnd': // $value = $this->date_mapi2php($value, true); break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; } $result[$kolab_idx] = $value; } // task description $this->body_to_kolab($data, $result); if ($data['PidLidTaskComplete']) { $result['status'] = 'COMPLETED'; } // Recurrence if (array_key_exists('PidLidTaskRecurrence', $data)) { $this->recurrence_to_kolab($data['PidLidTaskRecurrence'], $result, 'task'); } // Alarms (MAPI supports only one) $this->alarm_to_kolab($data, $result); if (array_key_exists('recipients', $data)) { $result['attendee'] = array(); $result['organizer'] = array(); foreach ((array) $data['recipients'] as $recipient) { $this->recipient_to_attendee($recipient, $result); } } $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidLidTaskRecurrence'] = 'rrule'; $map['PidTagBody'] = 'description'; return $map; } } diff --git a/lib/input/json/task.php b/lib/input/json/task.php index 6e45361..53d914f 100644 --- a/lib/input/json/task.php +++ b/lib/input/json/task.php @@ -1,125 +1,124 @@ | +--------------------------------------------------------------------------+ | 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', 'recurrence' => 'rrule', ); /** * 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 // @TOOD: alarms kolab_api_input_json::parse_recurrence($data, $result); // 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/tests/API/Folders.php b/tests/API/Folders.php index 828ae32..314b827 100644 --- a/tests/API/Folders.php +++ b/tests/API/Folders.php @@ -1,387 +1,387 @@ get('folders/test'); $code = self::$api->response_code(); $this->assertEquals(404, $code); // non-existing action self::$api->get('folders/' . kolab_api_tests::folder_uid('INBOX') . '/test'); $code = self::$api->response_code(); $this->assertEquals(404, $code); // existing action and folder, but wrong method self::$api->get('folders/' . kolab_api_tests::folder_uid('Mail-Test') . '/empty'); $code = self::$api->response_code(); $this->assertEquals(404, $code); } /** * Test listing all folders */ function test_folder_list_folders() { // get all folders self::$api->get('folders'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertTrue(count($body) >= 15); $this->assertSame('Calendar', $body[0]['fullpath']); $this->assertSame('event.default', $body[0]['type']); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['uid']); $this->assertNull($body[0]['parent']); // test listing subfolders of specified folder self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar') . '/folders'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame('Calendar/Personal Calendar', $body[0]['fullpath']); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['parent']); // get all folders with properties filter self::$api->get('folders', array('properties' => 'uid')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body[0]); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['uid']); } /** * Test folder delete */ function test_folder_delete() { // delete existing folder self::$api->delete('folders/' . kolab_api_tests::folder_uid('Mail-Test')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing folder self::$api->delete('folders/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test folder existence */ function test_folder_exists() { self::$api->head('folders/' . kolab_api_tests::folder_uid('INBOX')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing folder - deleted in test_folder_delete() self::$api->head('folders/' . kolab_api_tests::folder_uid('Mail-Test')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test folder update */ function test_folder_update() { $post = json_encode(array( 'name' => 'Mail-Test22', 'type' => 'mail' )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test2'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Mail-Test2'), $body['uid']); // move into an existing folder $post = json_encode(array( 'name' => 'Trash', )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test22'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(500, $code); // change parent to an existing folder $post = json_encode(array( 'parent' => kolab_api_tests::folder_uid('Trash'), )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test22'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(200, $code); } /** * Test folder create */ function test_folder_create() { $post = json_encode(array( 'name' => 'Test-create', 'type' => 'mail' )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Test-create'), $body['uid']); // folder already exists $post = json_encode(array( 'name' => 'Test-create', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $this->assertEquals(500, $code); // create a subfolder $post = json_encode(array( 'name' => 'Test', 'parent' => kolab_api_tests::folder_uid('Test-create'), 'type' => 'mail' )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Test-create/Test'), $body['uid']); // parent folder does not exists $post = json_encode(array( 'name' => 'Test-create-2', 'parent' => '123456789', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); } /** * Test folder info */ function test_folder_info() { self::$api->get('folders/' . kolab_api_tests::folder_uid('INBOX')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('INBOX', $body['name']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body['uid']); $args = array('properties' => 'name,exists,unread,size'); self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar'), $args); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('Calendar', $body['name']); $this->assertSame(null, $body['uid']); - $this->assertSame(8776, $body['size']); - $this->assertSame(2, $body['exists']); - $this->assertSame(2, $body['unread']); + $this->assertSame(16102, $body['size']); + $this->assertSame(3, $body['exists']); + $this->assertSame(3, $body['unread']); } /** * Test folder create */ function test_folder_empty() { self::$api->post('folders/' . kolab_api_tests::folder_uid('Trash') . '/empty'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } /** * Test listing folder content */ function test_folder_list_objects() { self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar/Personal Calendar') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array(), $body); // get all objects with properties filter self::$api->get('folders/' . kolab_api_tests::folder_uid('Notes') . '/objects', array('properties' => 'uid')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(2, $body); $this->assertCount(1, $body[0]); $this->assertSame('1-1-1-1', $body[0]['uid']); } /** * Test counting folder content */ function test_folder_count_objects() { self::$api->head('folders/' . kolab_api_tests::folder_uid('INBOX') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(5, (int) $count); // folder emptied in test_folder_empty() self::$api->head('folders/' . kolab_api_tests::folder_uid('Trash') . '/objects'); $count = self::$api->response_header('X-Count'); $this->assertSame(0, (int) $count); // one item removed in test_folder_delete_objects() self::$api->head('folders/' . kolab_api_tests::folder_uid('Notes') . '/objects'); $count = self::$api->response_header('X-Count'); $this->assertSame(2, (int) $count); } /** * Test moving objects from one folder to another */ function test_folder_move_objects() { $post = json_encode(array('100-100-100-100')); // invalid request: target == source self::$api->post('folders/' . kolab_api_tests::folder_uid('Calendar') . '/move/' . kolab_api_tests::folder_uid('Calendar'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); // move one object self::$api->post('folders/' . kolab_api_tests::folder_uid('Calendar') . '/move/' . kolab_api_tests::folder_uid('Calendar/Personal Calendar'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar/Personal Calendar') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame('100-100-100-100', $body[0]['uid']); self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); - $this->assertCount(1, $body); + $this->assertCount(2, $body); $this->assertSame('101-101-101-101', $body[0]['uid']); // @TODO: the same for mail } /** * Test deleting objects in a folder */ function test_folder_delete_objects() { // delete non-existing object $post = json_encode(array('1-1-1-1')); self::$api->post('folders/' . kolab_api_tests::folder_uid('Notes') . '/deleteobjects', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } } diff --git a/tests/API/Notes.php b/tests/API/Notes.php index c5b723d..9f76e9d 100644 --- a/tests/API/Notes.php +++ b/tests/API/Notes.php @@ -1,214 +1,214 @@ get('folders/' . kolab_api_tests::folder_uid('Notes') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(2, count($body)); $this->assertSame('1-1-1-1', $body[0]['uid']); $this->assertSame('test', $body[0]['summary']); $this->assertSame('2-2-2-2', $body[1]['uid']); $this->assertSame('wwww', $body[1]['summary']); } /** * Test note existence */ function test_note_exists() { self::$api->head('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing note self::$api->head('notes/' . kolab_api_tests::folder_uid('Notes') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test note info */ function test_note_info() { self::$api->get('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('1-1-1-1', $body['uid']); $this->assertSame('test', $body['summary']); $this->assertSame(array('tag1'), $body['categories']); } /** * Test note create */ function test_note_create() { $post = json_encode(array( 'summary' => 'Test summary', 'description' => 'Test description' )); self::$api->post('notes/' . kolab_api_tests::folder_uid('Notes'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); // folder does not exists $post = json_encode(array( 'summary' => 'Test summary 2', )); self::$api->post('notes/' . kolab_api_tests::folder_uid('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('notes/' . kolab_api_tests::folder_uid('Notes'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test note update */ function test_note_update() { $post = json_encode(array( 'summary' => 'Modified summary', 'description' => 'Modified description', 'classification' => 'PRIVATE', 'categories' => array('test'), 'unknown' => 'test' )); self::$api->put('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('1-1-1-1', $body['uid']); self::$api->get('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified summary', $body['summary']); $this->assertSame('Modified description', $body['description']); $this->assertSame('PRIVATE', $body['classification']); $this->assertSame(array('test'), $body['categories']); $this->assertSame(null, $body['unknown']); // test unsetting some data $post = json_encode(array( 'description' => null, 'categories' => null, )); self::$api->put('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); self::$api->get('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame(null, $body['description']); - $this->assertSame(null, $body['categories']); + $this->assertTrue(empty($body['categories'])); } /** * Test counting task attachments */ function test_count_attachments() { // @TODO } /** * Test listing task attachments */ function test_list_attachments() { // @TODO } /** * Test note delete */ function test_note_delete() { // delete existing note self::$api->delete('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing note self::$api->delete('notes/' . kolab_api_tests::folder_uid('Notes') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/Mapistore/Attachments.php b/tests/Mapistore/Attachments.php index 47e72ed..d0e3ead 100644 --- a/tests/Mapistore/Attachments.php +++ b/tests/Mapistore/Attachments.php @@ -1,275 +1,379 @@ head('attachments/' . kolab_api_tests::mapi_uid('INBOX', true, 6, 2)); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing attachment self::$api->head('attachments/' . kolab_api_tests::mapi_uid('INBOX', true, 6, 2345)); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); // test attachments of non-mail objects self::$api->head('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', 3)); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // test contact-photo attachment $id = kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID; self::$api->head('attachments/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d', $id)); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); + + // test event exception attachments + self::$api->head('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103', 20150622)); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(200, $code); + $this->assertSame('', $body); } /** * Test attachment info */ function test_attachment_info() { self::$api->get('attachments/' . kolab_api_tests::mapi_uid('INBOX', true, 6, 2)); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('INBOX', true, 6, 2), $body['id']); $this->assertSame('text/plain', $body['PidTagAttachMimeTag']); $this->assertSame('test.txt', $body['PidTagDisplayName']); $this->assertSame(4, $body['PidTagAttachSize']); $this->assertSame(1, $body['PidTagAttachMethod']); $this->assertSame('test', base64_decode($body['PidTagAttachDataBinary'])); // and non-existing attachment self::$api->get('attachments/' . kolab_api_tests::mapi_uid('INBOX', true, 6, 2345)); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); // test attachments of kolab objects self::$api->get('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', 3)); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', 3), $body['id']); $this->assertSame('image/jpeg', $body['PidTagAttachMimeTag']); $this->assertSame('photo-mini.jpg', $body['PidTagDisplayName']); $this->assertSame(793, $body['PidTagAttachSize']); $this->assertSame(1, $body['PidTagAttachMethod']); $this->assertSame('/9j/4AAQSkZJRgAB', substr($body['PidTagAttachDataBinary'], 0, 16)); // test contact photo attachment $id = kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID; self::$api->get('attachments/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d', $id)); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d', $id), $body['id']); // $this->assertSame('image/jpeg', $bod['PidTagAttachMimeTag']); $this->assertSame(true, $body['PidTagAttachmentContactPhoto']); $this->assertSame('ContactPicture.jpg', $body['PidTagDisplayName']); $this->assertSame('.jpg', $body['PidTagAttachExtension']); $this->assertSame(12797, $body['PidTagAttachSize']); $this->assertSame('/9j/4AAQ', substr($body['PidTagAttachDataBinary'], 0, 8)); + + // test event exception attachment + self::$api->get('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103', 20150622)); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103', 20150622), $body['id']); + $this->assertTrue(!empty($body['PidTagAttachDataObject'])); + $this->assertSame('Summary', $body['PidTagDisplayName']); + $this->assertSame(7, $body['PidTagObjectType']); } /** * Test attachment create */ function test_attachment_create() { $post = json_encode(array( 'PidTagAttachDataBinary' => 'R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', 'PidTagDisplayName' => 'image.gif', )); self::$api->post('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); self::$api->get('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', 4)); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', 4), $body['id']); $this->assertSame('image/gif', $body['PidTagAttachMimeTag']); $this->assertSame('image.gif', $body['PidTagDisplayName']); $this->assertSame(54, $body['PidTagAttachSize']); $this->assertSame(1, $body['PidTagAttachMethod']); $this->assertSame('R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', $body['PidTagAttachDataBinary']); // add contact photo $post = json_encode(array( 'PidTagAttachDataBinary' => 'R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', 'PidTagAttachmentContactPhoto' => true, )); self::$api->post('attachments/' . kolab_api_tests::mapi_uid('Contacts', true, 'e-f-g-h'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $id = kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID; self::$api->get('attachments/' . kolab_api_tests::mapi_uid('Contacts', true, 'e-f-g-h', $id)); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame(true, $body['PidTagAttachmentContactPhoto']); $this->assertSame(54, $body['PidTagAttachSize']); $this->assertSame('R0lGODlh', substr($body['PidTagAttachDataBinary'], 0, 8)); + + // add event exception + $post = json_encode(array( + 'PidTagAttachDataObject' => array( + 'PidTagMessageClass' => 'IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}', + 'PidLidAppointmentSequence' => 1, + 'PidLidLocation' => 'LocationX', + 'PidLidAppointmentSubType' => 1, + 'PidLidAppointmentStartWhole' => 13079404800, + 'PidLidAppointmentEndWhole' => 13079491200, + 'PidTagSubject' => 'SummaryX', + 'PidTagSensitivity' => 0, + 'PidLidAppointmentDuration' => 1440, + 'PidLidReminderDelta' => 35, + ), + 'PidTagObjectType' => 7, + 'PidTagAttachMethod' => 5, + )); + self::$api->post('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '101-101-101-101'), array(), $post); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + + self::$api->get('attachments/' . $body['id']); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertSame('LocationX', $body['PidTagAttachDataObject']['PidLidLocation']); + $this->assertSame('SummaryX', $body['PidTagAttachDataObject']['PidTagSubject']); } /** * Test attachment update */ function test_attachment_update() { $post = json_encode(array( 'PidTagAttachDataBinary' => base64_encode('test text file'), 'PidTagDisplayName' => 'test.txt', )); self::$api->put('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', 4), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); self::$api->get('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', 4)); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', 4), $body['id']); $this->assertSame('text/plain', $body['PidTagAttachMimeTag']); $this->assertSame('test.txt', $body['PidTagDisplayName']); $this->assertSame(14, $body['PidTagAttachSize']); $this->assertSame(1, $body['PidTagAttachMethod']); $this->assertSame('test text file', base64_decode($body['PidTagAttachDataBinary'])); // update contact photo $id = kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID; $post = json_encode(array( 'PidTagAttachDataBinary' => 'R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', 'PidTagAttachmentContactPhoto' => true, )); self::$api->put('attachments/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d', $id), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); self::$api->get('attachments/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d', $id)); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame(true, $body['PidTagAttachmentContactPhoto']); $this->assertSame('ContactPicture.gif', $body['PidTagDisplayName']); $this->assertSame('.gif', $body['PidTagAttachExtension']); $this->assertSame(54, $body['PidTagAttachSize']); $this->assertSame('R0lGODlh', substr($body['PidTagAttachDataBinary'], 0, 8)); + + // update event exception attachment + $post = json_encode(array( + 'PidTagAttachDataObject' => array( + 'PidTagMessageClass' => 'IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}', + 'PidLidAppointmentSequence' => 2, + 'PidLidLocation' => 'LocationY', + 'PidLidAppointmentSubType' => 1, + 'PidTagSubject' => 'SummaryY', + ), + 'PidTagObjectType' => 7, + 'PidTagAttachMethod' => 5, + )); + self::$api->put('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103', 20150622), array(), $post); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + + self::$api->get('attachments/' . $body['id']); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertSame('LocationY', $body['PidTagAttachDataObject']['PidLidLocation']); + $this->assertSame('SummaryY', $body['PidTagAttachDataObject']['PidTagSubject']); + $this->assertSame(35, $body['PidTagAttachDataObject']['PidLidReminderDelta']); } /** * Test attachment delete */ function test_attachment_delete() { // delete existing attachment self::$api->delete('attachments/' . kolab_api_tests::mapi_uid('Tasks', true, '10-10-10-10', '3')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); // delete non-existing attachment in an existing object self::$api->delete('attachments/' . kolab_api_tests::mapi_uid('Tasks', true, '10-10-10-10', '3')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); // delete contact photo $id = kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID; self::$api->delete('attachments/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d', $id)); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); self::$api->get('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d'), $body['id']); $this->assertSame(null, $body['PidLidHasPicture']); + + // delete event-exception + self::$api->delete('attachments/' . kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103', 20150629)); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(200, $code); + + self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103') . '/attachments'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertCount(1, $body); } } diff --git a/tests/Mapistore/Events.php b/tests/Mapistore/Events.php index 3a4bfd8..8c31d5e 100644 --- a/tests/Mapistore/Events.php +++ b/tests/Mapistore/Events.php @@ -1,252 +1,293 @@ get('folders/' . kolab_api_tests::folder_uid('Calendar') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), $body[0]['id']); $this->assertSame('Summary', $body[0]['PidTagSubject']); $this->assertSame('Description', $body[0]['PidTagBody']); $this->assertSame('calendars', $body[0]['collection']); $this->assertSame('IPM.Appointment', $body[0]['PidTagMessageClass']); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '101-101-101-101'), $body[1]['id']); $this->assertSame(0, $body[1]['PidTagSensitivity']); $this->assertSame('calendars', $body[1]['collection']); $this->assertSame('IPM.Appointment', $body[1]['PidTagMessageClass']); } /** * Test event existence */ function test_event_exists() { self::$api->head('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing event self::$api->head('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test event info */ function test_event_info() { self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), $body['id']); $this->assertSame('Summary', $body['PidTagSubject']); $this->assertSame('calendars', $body['collection']); $this->assertSame('IPM.Appointment', $body['PidTagMessageClass']); $this->assertSame(true, $body['PidTagHasAttachments']); } /** * Test event create */ function test_event_create() { $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Calendar'), 'PidTagSubject' => 'Test summary', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-01'), )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['id'])); // folder does not exists $post = json_encode(array( 'parent_id' => md5('non-existing'), 'PidTagSubject' => 'Test summary', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-01'), )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Calendar'), 'test' => 'Test summary 2', )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test event update */ function test_event_update() { // @TODO: test modification of all supported properties $post = json_encode(array( 'PidTagSubject' => 'Modified subject (1)', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-01'), )); self::$api->put('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100')); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified subject (1)', $body['PidTagSubject']); } /** * Test counting event attachments */ function test_count_attachments() { self::$api->head('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(1, (int) $count); self::$api->head('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '101-101-101-101') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); + + // Count event-exception attachments + self::$api->head('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103') . '/attachments'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $count = self::$api->response_header('X-mapistore-rowcount'); + + $this->assertEquals(200, $code); + $this->assertSame(2, (int) $count); } /** * Test listing event attachments */ function test_list_attachments() { self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', '3'), $body[0]['id']); $this->assertSame('image/jpeg', $body[0]['PidTagAttachMimeTag']); $this->assertSame('photo-mini.jpg', $body[0]['PidTagDisplayName']); $this->assertSame(793, $body[0]['PidTagAttachSize']); + + // List event-exception attachments + self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103') . '/attachments'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertCount(2, $body); + + $this->assertSame('attachments', $body[0]['collection']); + $this->assertSame('Summary', $body[0]['PidTagDisplayName']); + $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103', 20150622), $body[0]['id']); + $this->assertSame(2, $body[0]['PidTagAttachmentFlags']); + $this->assertSame(true, $body[0]['PidTagAttachmentHidden']); + $this->assertSame(5, $body[0]['PidTagAttachMethod']); + $this->assertSame(7, $body[0]['PidTagObjectType']); + $this->assertSame(13079404800, $body[0]['PidTagExceptionStartTime']); + $this->assertSame(13079491200, $body[0]['PidTagExceptionEndTime']); + $this->assertSame('IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}', $body[0]['PidTagAttachDataObject']['PidTagMessageClass']); + $this->assertSame('Location mod', $body[0]['PidTagAttachDataObject']['PidLidLocation']); + + $this->assertSame('Summary', $body[1]['PidTagDisplayName']); + $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '103-103-103-103', 20150629), $body[1]['id']); + $this->assertSame(2, $body[1]['PidTagAttachmentFlags']); + $this->assertSame(true, $body[1]['PidTagAttachmentHidden']); + $this->assertSame(13080009600, $body[1]['PidTagExceptionStartTime']); + $this->assertSame(13080096000, $body[1]['PidTagExceptionEndTime']); + $this->assertSame('IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}', $body[1]['PidTagAttachDataObject']['PidTagMessageClass']); + $this->assertSame(null, $body[1]['PidTagAttachDataObject']['PidLidLocation']); } /** * Test event delete */ function test_event_delete() { // delete existing event self::$api->delete('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '101-101-101-101')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing event self::$api->delete('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test event update with moving to another folder */ function test_event_update_and_move() { // test event moving to another folder (by parent_id change) $post = json_encode(array( 'PidTagSubject' => 'Modified subject (2)', 'parent_id' => kolab_api_tests::folder_uid('Calendar/Personal Calendar'), )); self::$api->put('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar/Personal Calendar', true, '100-100-100-100')); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified subject (2)', $body['PidTagSubject']); } } diff --git a/tests/Mapistore/Folders.php b/tests/Mapistore/Folders.php index 61958d9..76f1dd0 100644 --- a/tests/Mapistore/Folders.php +++ b/tests/Mapistore/Folders.php @@ -1,334 +1,334 @@ get('folders/1/folders'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertTrue(count($body) >= 12); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['id']); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[1]['parent_id']); $this->assertSame(1, $body[0]['parent_id']); $this->assertSame('IPF.Appointment', $body[0]['PidTagContainerClass']); $this->assertSame('IPF.Appointment', $body[1]['PidTagContainerClass']); // test listing subfolders of specified folder self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar') . '/folders'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['parent_id']); // get all folders with properties filter self::$api->get('folders/1/folders', array('properties' => 'id')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array('id' => kolab_api_tests::folder_uid('Calendar')), $body[0]); // get non-ipm folders self::$api->get('folders/0/folders'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertTrue(count($body) >= 7); $this->assertSame(0, $body[0]['parent_id']); } /** * Test folder delete */ function test_folder_delete() { // delete existing folder self::$api->delete('folders/' . kolab_api_tests::folder_uid('Mail-Test')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing folder self::$api->get('folders/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test folder existence */ function test_folder_exists() { self::$api->head('folders/' . kolab_api_tests::folder_uid('INBOX')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing folder - deleted in test_folder_delete() self::$api->get('folders/' . kolab_api_tests::folder_uid('Mail-Test')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); // special folder self::$api->head('folders/0'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } /** * Test folder update */ function test_folder_update() { $post = json_encode(array( 'PidTagDisplayName' => 'Mail-Test22', )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test2'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); // move into an existing folder $post = json_encode(array( 'PidTagDisplayName' => 'Trash', )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test22'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(500, $code); // change parent to an existing folder $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Trash'), )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test22'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(200, $code); } /** * Test folder create */ function test_folder_create() { $post = json_encode(array( 'PidTagDisplayName' => 'Test-create', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Test-create'), $body['id']); // folder already exists $post = json_encode(array( 'PidTagDisplayName' => 'Test-create', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $this->assertEquals(500, $code); // create a subfolder $post = json_encode(array( 'PidTagDisplayName' => 'Test', 'parent_id' => kolab_api_tests::folder_uid('Test-create'), )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Test-create/Test'), $body['id']); // parent folder does not exists $post = json_encode(array( 'PidTagDisplayName' => 'Test-create-2', 'parent_id' => '123456789', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); } /** * Test folder info */ function test_folder_info() { self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('Calendar', $body['PidTagDisplayName']); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body['id']); - $this->assertSame(2, $body['PidTagContentCount']); - $this->assertSame(2, $body['PidTagContentUnreadCount']); - $this->assertSame(8776, $body['PidTagMessageSize']); + $this->assertSame(3, $body['PidTagContentCount']); + $this->assertSame(3, $body['PidTagContentUnreadCount']); + $this->assertSame(16102, $body['PidTagMessageSize']); $this->assertSame(true, $body['PidTagSubfolders']); $this->assertSame(0, $body['PidTagDeletedCountTotal']); $this->assertSame(true, $body['PidTagCreationTime'] > 0); self::$api->get('folders/0'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('Root', $body['PidTagDisplayName']); $this->assertSame(0, $body['id']); $this->assertSame(0, $body['PidTagFolderType']); } /** * Test listing folder content */ function test_folder_list_objects() { self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar/Personal Calendar') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array(), $body); // get all objects with properties filter self::$api->get('folders/' . kolab_api_tests::folder_uid('Notes') . '/messages', array('properties' => 'id,collection')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(2, $body[0]); $this->assertTrue(!empty($body[0]['id'])); $this->assertSame('notes', $body[0]['collection']); } /** * Test counting folder content */ function test_folder_count_objects() { self::$api->head('folders/' . kolab_api_tests::folder_uid('INBOX') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(5, (int) $count); // folder emptied in test_folder_empty() self::$api->head('folders/' . kolab_api_tests::folder_uid('Trash') . '/mssages'); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertSame(0, (int) $count); // one item removed in test_folder_delete_objects() self::$api->head('folders/' . kolab_api_tests::folder_uid('Notes') . '/messages'); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertSame(2, (int) $count); } /** * Test folder create */ function test_folder_delete_objects() { $post = json_encode(array(array('id' => '1-1-1-1'))); self::$api->post('folders/' . kolab_api_tests::folder_uid('Notes') . '/deletemessages', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } /** * Test folder empty */ function test_folder_empty() { self::$api->post('folders/' . kolab_api_tests::folder_uid('Trash') . '/empty'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } } diff --git a/tests/Unit/Filter/Mapistore/Attachment.php b/tests/Unit/Filter/Mapistore/Attachment.php index e6257b9..a9d861b 100644 --- a/tests/Unit/Filter/Mapistore/Attachment.php +++ b/tests/Unit/Filter/Mapistore/Attachment.php @@ -1,92 +1,92 @@ 'folder', 'object_uid' => 'msg'); $attach = array( 'id' => 'id', 'mimetype' => 'mimetype', 'size' => 100, 'filename' => 'filename.txt', 'disposition' => 'disposition', 'content-id' => 'content-id', 'content-location' => 'content-location', ); $result = $api->output($attach, $context); $this->assertSame(kolab_api_filter_mapistore::uid_encode('folder', 'msg', 'id'), $result['id']); $this->assertSame('attachments', $result['collection']); $this->assertSame('mimetype', $result['PidTagAttachMimeTag']); $this->assertSame(100, $result['PidTagAttachSize']); $this->assertSame('filename.txt', $result['PidTagDisplayName']); $this->assertSame('.txt', $result['PidTagAttachExtension']); $this->assertSame('content-id', $result['PidTagAttachContentId']); $this->assertSame('content-location', $result['PidTagAttachContentLocation']); $this->assertSame(1, $result['PidTagAttachMethod']); $this->assertSame(7, $result['PidTagObjectType']); $this->assertSame(null, $result['PidTagAttachDataBinary']); // @TODO: test attachment body handling } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_attachment; $data = array( 'id' => 'id', 'PidTagAttachMimeTag' => 'mimetype', 'PidTagAttachSize' => 100, 'PidTagDisplayName' => 'filename.txt', 'PidTagAttachContentId' => 'content-id', 'PidTagAttachContentLocation' => 'content-location', 'PidTagAttachmentContactPhoto' => true, ); $result = $api->input($data); // $this->assertSame(kolab_api_filter_mapistore::uid_encode('folder', 'msg', 'id'), $result['id']); $this->assertSame('mimetype', $result['mimetype']); $this->assertSame(100, $result['size']); $this->assertSame('filename.txt', $result['filename']); $this->assertSame('content-id', $result['content-id']); $this->assertSame('content-location', $result['content-location']); - $this->assertSame(true, $result['is_photo']); + $this->assertSame(true, $result['is-photo']); } /** * Test input method with merge */ function test_input2() { // @TODO $this->markTestIncomplete('TODO'); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_attachment; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Contact.php b/tests/Unit/Filter/Mapistore/Contact.php index dcae74f..50732b5 100644 --- a/tests/Unit/Filter/Mapistore/Contact.php +++ b/tests/Unit/Filter/Mapistore/Contact.php @@ -1,445 +1,445 @@ output($data, $context); $this->assertSame('IPM.Contact', $result['PidTagMessageClass']); $this->assertSame('contacts', $result['collection']); $this->assertSame(kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Contacts', false), $result['parent_id']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150420T141533Z'), $result['PidTagLastModificationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150330', false), $result['PidTagBirthday']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150301', false), $result['PidTagWeddingAnniversary']); $this->assertSame(null, $result['PidTagHasAttachments']); $this->assertSame('displname', $result['PidTagDisplayName']); $this->assertSame('last', $result['PidTagSurname']); $this->assertSame('test', $result['PidTagGivenName']); $this->assertSame('middlename', $result['PidTagMiddleName']); $this->assertSame('prefx', $result['PidTagDisplayNamePrefix']); $this->assertSame('suff', $result['PidTagGeneration']); $this->assertSame('dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', $result['PidTagBody']); $this->assertSame('free-busy url', $result['PidLidFreeBusyLocation']); $this->assertSame('title', $result['PidTagTitle']); $this->assertSame('Org', $result['PidTagCompanyName']); $this->assertSame('dept', $result['PidTagDepartmentName']); $this->assertSame('profeion', $result['PidTagProfession']); $this->assertSame('manager name', $result['PidTagManagerName']); $this->assertSame('assist', $result['PidTagAssistant']); $this->assertSame('website', $result['PidTagPersonalHomePage']); $this->assertSame('office street', $result['PidTagOtherAddressStreet']); $this->assertSame('office city', $result['PidTagOtherAddressCity']); $this->assertSame('office state', $result['PidTagOtherAddressStateOrProvince']); $this->assertSame('office zip', $result['PidTagOtherAddressPostalCode']); $this->assertSame('office country', $result['PidTagOtherAddressCountry']); // $this->assertSame('office pobox', $result['PidTagOtherAddressPostOfficeBox']); $this->assertSame('home street', $result['PidTagHomeAddressStreet']); $this->assertSame('home city', $result['PidTagHomeAddressCity']); $this->assertSame('home state', $result['PidTagHomeAddressStateOrProvince']); $this->assertSame('home zip', $result['PidTagHomeAddressPostalCode']); $this->assertSame('home country', $result['PidTagHomeAddressCountry']); // $this->assertSame('home pobox', $result['PidTagHomeAddressPostOfficeBox']); $this->assertSame('work street', $result['PidLidWorkAddressStreet']); $this->assertSame('work city', $result['PidLidWorkAddressCity']); $this->assertSame('work state', $result['PidLidWorkAddressState']); $this->assertSame('work zip', $result['PidLidWorkAddressPostalCode']); $this->assertSame('work country', $result['PidLidWorkAddressCountry']); // $this->assertSame('work pobox', $result['PidLidWorkAddressPostOfficeBox']); $this->assertSame(2, $result['PidLidPostalAddressId']); $this->assertSame('nick', $result['PidTagNickname']); $this->assertSame(2, $result['PidTagGender']); $this->assertSame('spouse', $result['PidTagSpouseName']); $this->assertSame(array('children', 'children2'), $result['PidTagChildrensNames']); $this->assertSame('home phone', $result['PidTagHomeTelephoneNumber']); $this->assertSame('work phone', $result['PidTagBusinessTelephoneNumber']); $this->assertSame('home fax', $result['PidTagHomeFaxNumber']); $this->assertSame('work fax', $result['PidTagBusinessFaxNumber']); $this->assertSame('mobile', $result['PidTagMobileTelephoneNumber']); $this->assertSame('pager', $result['PidTagPagerTelephoneNumber']); $this->assertSame('car phone', $result['PidTagCarTelephoneNumber']); $this->assertSame('other phone', $result['PidTagOtherTelephoneNumber']); $this->assertSame('im gg', $result['PidLidInstantMessagingAddress']); $this->assertSame('test@mail.ru', $result['PidLidEmail1EmailAddress']); $this->assertSame('work@email.pl', $result['PidLidEmail2EmailAddress']); $this->assertSame('other@email.pl', $result['PidLidEmail3EmailAddress']); $this->assertRegExp('/^cy9.*/', $result['PidTagUserX509Certificate']); $this->assertSame(true, $result['PidLidHasPicture']); $this->assertSame(array('tag1'), $result['PidNameKeywords']); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); // $this->assertSame('individual', $result['kind']); // Distribution List $api = new kolab_api_filter_mapistore_contact; $data = kolab_api_tests::get_data('i-j-k-l', 'Contacts', 'contact', 'json', $context); $result = $api->output($data, $context); $this->assertSame('IPM.DistList', $result['PidTagMessageClass']); $this->assertSame('contacts', $result['collection']); $this->assertSame(kolab_api_tests::mapi_uid('Contacts', false, 'i-j-k-l'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Contacts', false), $result['parent_id']); $this->assertSame('test group', $result['PidTagDisplayName']); // @TODO: list members } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_contact; $data = array( 'id' => kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), 'parent_id' => kolab_api_tests::folder_uid('Contacts', false), 'PidTagMessageClass' => 'IPM.Contact', 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('20150421T145607Z'), 'PidTagBirthday' => kolab_api_filter_mapistore_common::date_php2mapi('20150330', true), 'PidTagWeddingAnniversary' => kolab_api_filter_mapistore_common::date_php2mapi('20150301', true), 'PidTagDisplayName' => 'displname', 'PidTagSurname' => 'last', 'PidTagGivenName' => 'test', 'PidTagMiddleName' => 'middlename', 'PidTagDisplayNamePrefix' => 'prefx', 'PidTagGeneration' => 'suff', 'PidTagBody' => 'dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', 'PidLidFreeBusyLocation' => 'free-busy url', 'PidTagTitle' => 'title', 'PidTagCompanyName' => 'Org', 'PidTagDepartmentName' => 'dept', 'PidTagProfession' => 'profeion', 'PidTagManagerName' => 'manager name', 'PidTagAssistant' => 'assist', 'PidTagPersonalHomePage' => 'website', 'PidTagOtherAddressStreet' => 'office street', 'PidTagOtherAddressCity' => 'office city', 'PidTagOtherAddressStateOrProvince' => 'office state', 'PidTagOtherAddressPostalCode' => 'office zip', 'PidTagOtherAddressCountry' => 'office country', // 'PidTagOtherAddressPostOfficeBox' => 'office pobox', 'PidTagHomeAddressStreet' => 'home street', 'PidTagHomeAddressCity' => 'home city', 'PidTagHomeAddressStateOrProvince' => 'home state', 'PidTagHomeAddressPostalCode' => 'home zip', 'PidTagHomeAddressCountry' => 'home country', // 'PidTagHomeAddressPostOfficeBox' => 'home pobox', 'PidLidWorkAddressStreet' => 'work street', 'PidLidWorkAddressCity' => 'work city', 'PidLidWorkAddressState' => 'work state', 'PidLidWorkAddressPostalCode' => 'work zip', 'PidLidWorkAddressCountry' => 'work country', // 'PidLidWorkAddressPostOfficeBox' => 'work pobox', 'PidLidPostalAddressId' => 1, 'PidTagNickname' => 'nick', 'PidTagGender' => 2, 'PidTagSpouseName' => 'spouse', 'PidTagChildrensNames' => array('children', 'children2'), 'PidTagHomeTelephoneNumber' => 'home phone', 'PidTagBusinessTelephoneNumber' => 'work phone', 'PidTagHomeFaxNumber' => 'home fax', 'PidTagBusinessFaxNumber' => 'work fax', 'PidTagMobileTelephoneNumber' => 'mobile', 'PidTagPagerTelephoneNumber' => 'pager', 'PidTagCarTelephoneNumber' => 'car phone', 'PidTagOtherTelephoneNumber' => 'other phone', 'PidLidInstantMessagingAddress' => 'im gg', 'PidLidEmail1EmailAddress' => 'test@mail.ru', 'PidLidEmail2EmailAddress' => 'work@email.pl', 'PidLidEmail3EmailAddress' => 'other@email.pl', 'PidTagUserX509Certificate' => '1234567890', 'PidTagInitials' => 'initials', ); $result = $api->input($data); // $this->assertSame('a-b-c-d', $result['uid']); $this->assertSame('individual', $result['kind']); $this->assertSame('displname', $result['fn']); $this->assertSame('last', $result['n']['surname']); $this->assertSame('test', $result['n']['given']); $this->assertSame('middlename', $result['n']['additional']); $this->assertSame('prefx', $result['n']['prefix']); $this->assertSame('suff', $result['n']['suffix']); $this->assertSame('dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', $result['note']); $this->assertSame('free-busy url', $result['fburl']); $this->assertSame('title', $result['title']); $this->assertSame('Org', $result['group']['org'][0]); $this->assertSame('dept', $result['group']['org'][1]); $this->assertSame('profeion', $result['group']['role']); $this->assertSame('x-manager', $result['group']['related'][0]['parameters']['type']); $this->assertSame('manager name', $result['group']['related'][0]['text']); $this->assertSame('x-assistant', $result['group']['related'][1]['parameters']['type']); $this->assertSame('assist', $result['group']['related'][1]['text']); // $this->assertSame('', $result['group']['adr']['pobox']); $this->assertSame('office street', $result['group']['adr']['street']); $this->assertSame('office city', $result['group']['adr']['locality']); $this->assertSame('office state', $result['group']['adr']['region']); $this->assertSame('office zip', $result['group']['adr']['code']); $this->assertSame('office country', $result['group']['adr']['country']); $this->assertSame(array('website'), $result['url']); $this->assertSame('home', $result['adr'][0]['parameters']['type']); $this->assertSame(1, $result['adr'][0]['parameters']['pref']); $this->assertSame('home street', $result['adr'][0]['street']); $this->assertSame('home city', $result['adr'][0]['locality']); $this->assertSame('home state', $result['adr'][0]['region']); $this->assertSame('home zip', $result['adr'][0]['code']); $this->assertSame('home country', $result['adr'][0]['country']); $this->assertSame('work', $result['adr'][1]['parameters']['type']); $this->assertSame('work street', $result['adr'][1]['street']); $this->assertSame('work city', $result['adr'][1]['locality']); $this->assertSame('work state', $result['adr'][1]['region']); $this->assertSame('work zip', $result['adr'][1]['code']); $this->assertSame('work country', $result['adr'][1]['country']); $this->assertSame('nick', $result['nickname']); $this->assertSame('spouse', $result['related'][0]['parameters']['type']); $this->assertSame('spouse', $result['related'][0]['text']); $this->assertSame('child', $result['related'][1]['parameters']['type']); $this->assertSame('children', $result['related'][1]['text']); $this->assertSame('child', $result['related'][2]['parameters']['type']); $this->assertSame('children2', $result['related'][2]['text']); $this->assertSame('2015-03-30', $result['bday']); // ? $this->assertSame('2015-03-01', $result['anniversary']); // ? $this->assertSame('M', $result['gender']); $this->assertSame(array('im gg'), $result['impp']); $this->assertSame('home', $result['email'][0]['parameters']['type']); $this->assertSame('test@mail.ru', $result['email'][0]['text']); $this->assertSame('work', $result['email'][1]['parameters']['type']); $this->assertSame('work@email.pl', $result['email'][1]['text']); $this->assertSame('other', $result['email'][2]['parameters']['type']); $this->assertSame('other@email.pl', $result['email'][2]['text']); $this->assertRegExp('|^data:application/pkcs7-mime;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][1]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); $this->assertSame('MAPI:PidTagInitials', $result['x-custom'][0]['identifier']); $this->assertSame('initials', $result['x-custom'][0]['value']); $phones = array( 'home' => 'home phone', 'work' => 'work phone', 'faxhome' => 'home fax', 'faxwork' => 'work fax', 'cell' => 'mobile', 'pager' => 'pager', 'x-car' => 'car phone', 'textphone' => 'other phone', ); foreach ($result['tel'] as $tel) { $type = implode('', (array)$tel['parameters']['type']); $text = $tel['text']; if (!empty($phones[$type]) && $phones[$type] == $text) { unset($phones[$type]); } } $this->assertCount(8, $result['tel']); $this->assertCount(0, $phones); self::$original = $result; } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_contact; $data = array( 'id' => kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), 'parent_id' => kolab_api_tests::folder_uid('Contacts', false), 'PidTagBirthday' => kolab_api_filter_mapistore_common::date_php2mapi('20150430', true), 'PidTagWeddingAnniversary' => kolab_api_filter_mapistore_common::date_php2mapi('20150401', true), 'PidTagDisplayName' => 'displname1', 'PidTagSurname' => 'last1', 'PidTagGivenName' => 'test1', 'PidTagMiddleName' => 'middlename1', 'PidTagDisplayNamePrefix' => 'prefx1', 'PidTagGeneration' => 'suff1', 'PidTagBody' => 'body1', 'PidLidFreeBusyLocation' => 'free-busy url1', 'PidTagTitle' => 'title1', 'PidTagCompanyName' => 'Org1', 'PidTagDepartmentName' => 'dept1', 'PidTagProfession' => 'profeion1', 'PidTagManagerName' => 'manager name1', 'PidTagAssistant' => 'assist1', 'PidTagPersonalHomePage' => 'website1', 'PidTagOtherAddressStreet' => 'office street1', 'PidTagOtherAddressCity' => 'office city1', 'PidTagOtherAddressStateOrProvince' => 'office state1', 'PidTagOtherAddressPostalCode' => 'office zip1', 'PidTagOtherAddressCountry' => 'office country1', 'PidTagHomeAddressStreet' => 'home street1', 'PidTagHomeAddressCity' => 'home city1', 'PidTagHomeAddressStateOrProvince' => 'home state1', 'PidTagHomeAddressPostalCode' => 'home zip1', 'PidTagHomeAddressCountry' => 'home country1', 'PidLidWorkAddressStreet' => 'work street1', 'PidLidWorkAddressCity' => 'work city1', 'PidLidWorkAddressState' => 'work state1', 'PidLidWorkAddressPostalCode' => 'work zip1', 'PidLidWorkAddressCountry' => 'work country1', 'PidTagNickname' => 'nick1', 'PidTagGender' => 1, 'PidTagSpouseName' => 'spouse1', 'PidTagChildrensNames' => array('children10', 'children20'), 'PidTagHomeTelephoneNumber' => 'home phone1', 'PidTagBusinessTelephoneNumber' => null, 'PidTagHomeFaxNumber' => 'home fax1', 'PidTagBusinessFaxNumber' => 'work fax1', 'PidTagMobileTelephoneNumber' => 'mobile1', 'PidTagPagerTelephoneNumber' => 'pager1', 'PidTagOtherTelephoneNumber' => 'other phone1', 'PidLidInstantMessagingAddress' => 'im gg1', 'PidLidEmail1EmailAddress' => 'test@mail.ru', 'PidLidEmail2EmailAddress' => 'work@email.pl', 'PidTagUserX509Certificate' => '12345678901', 'PidTagInitials' => 'initials1', 'PidNameKeywords' => array('work1'), ); $result = $api->input($data, self::$original); // $this->assertSame('a-b-c-d', $result['uid']); $this->assertSame('displname1', $result['fn']); $this->assertSame('last1', $result['n']['surname']); $this->assertSame('test1', $result['n']['given']); $this->assertSame('middlename1', $result['n']['additional']); $this->assertSame('prefx1', $result['n']['prefix']); $this->assertSame('suff1', $result['n']['suffix']); $this->assertSame('body1', $result['note']); $this->assertSame('free-busy url1', $result['fburl']); $this->assertSame('title1', $result['title']); $this->assertSame('Org1', $result['group']['org'][0]); $this->assertSame('dept1', $result['group']['org'][1]); $this->assertSame('profeion1', $result['group']['role']); $this->assertSame('x-manager', $result['group']['related'][0]['parameters']['type']); $this->assertSame('manager name1', $result['group']['related'][0]['text']); $this->assertSame('x-assistant', $result['group']['related'][1]['parameters']['type']); $this->assertSame('assist1', $result['group']['related'][1]['text']); $this->assertSame('office street1', $result['group']['adr']['street']); $this->assertSame('office city1', $result['group']['adr']['locality']); $this->assertSame('office state1', $result['group']['adr']['region']); $this->assertSame('office zip1', $result['group']['adr']['code']); $this->assertSame('office country1', $result['group']['adr']['country']); $this->assertSame(array('website1'), $result['url']); $this->assertSame('home', $result['adr'][0]['parameters']['type']); $this->assertSame('home street1', $result['adr'][0]['street']); $this->assertSame('home city1', $result['adr'][0]['locality']); $this->assertSame('home state1', $result['adr'][0]['region']); $this->assertSame('home zip1', $result['adr'][0]['code']); $this->assertSame('home country1', $result['adr'][0]['country']); $this->assertSame('work', $result['adr'][1]['parameters']['type']); $this->assertSame('work street1', $result['adr'][1]['street']); $this->assertSame('work city1', $result['adr'][1]['locality']); $this->assertSame('work state1', $result['adr'][1]['region']); $this->assertSame('work zip1', $result['adr'][1]['code']); $this->assertSame('work country1', $result['adr'][1]['country']); $this->assertSame('nick1', $result['nickname']); $this->assertSame('spouse', $result['related'][0]['parameters']['type']); $this->assertSame('spouse1', $result['related'][0]['text']); $this->assertSame('child', $result['related'][1]['parameters']['type']); $this->assertSame('children10', $result['related'][1]['text']); $this->assertSame('child', $result['related'][2]['parameters']['type']); $this->assertSame('children20', $result['related'][2]['text']); $this->assertSame('2015-04-30', $result['bday']); // ? $this->assertSame('2015-04-01', $result['anniversary']); // ? $this->assertSame('F', $result['gender']); $this->assertSame(array('im gg1'), $result['impp']); $this->assertSame('home', $result['email'][0]['parameters']['type']); $this->assertSame('test@mail.ru', $result['email'][0]['text']); $this->assertSame('work', $result['email'][1]['parameters']['type']); $this->assertSame('work@email.pl', $result['email'][1]['text']); $this->assertSame(null, $result['email'][2]); $this->assertRegExp('|^data:application/pkcs7-mime;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][1]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); $this->assertSame('MAPI:PidTagInitials', $result['x-custom'][0]['identifier']); $this->assertSame('initials1', $result['x-custom'][0]['value']); $this->assertSame(array('work1'), $result['categories']); $phones = array( 'home' => 'home phone1', 'faxhome' => 'home fax1', 'faxwork' => 'work fax1', 'cell' => 'mobile1', 'pager' => 'pager1', 'x-car' => 'car phone', 'textphone' => 'other phone1', ); foreach ($result['tel'] as $tel) { $type = implode('', (array)$tel['parameters']['type']); $text = $tel['text']; if (!empty($phones[$type]) && $phones[$type] == $text) { unset($phones[$type]); } } $this->assertCount(7, $result['tel']); $this->assertCount(0, $phones); // @TODO: updating some deep items (e.g. adr); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_contact; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } /** * Test photo_attachment method */ function test_photo_attachment() { $contact = array(); $result = kolab_api_filter_mapistore_contact::photo_attachment($contact); $this->assertSame(null, $result); $contact['photo'] = base64_decode('R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7'); $result = kolab_api_filter_mapistore_contact::photo_attachment($contact); - $this->assertSame(true, $result['is_photo']); + $this->assertSame(true, $result['is-photo']); $this->assertSame(54, $result['size']); $this->assertSame('ContactPicture.gif', $result['filename']); $this->assertSame('image/gif', $result['mimetype']); $this->assertSame($contact['photo'], $result['content']); $this->assertSame(kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID, $result['id']); } } diff --git a/tests/Unit/Filter/Mapistore/Event.php b/tests/Unit/Filter/Mapistore/Event.php index e4922d6..5f6cb86 100644 --- a/tests/Unit/Filter/Mapistore/Event.php +++ b/tests/Unit/Filter/Mapistore/Event.php @@ -1,443 +1,490 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '100-100-100-100'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), $result['PidTagCreationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), $result['PidTagLastModificationTime']); $this->assertSame(30, $result['PidLidAppointmentDuration']); $this->assertSame(2, $result['PidLidAppointmentSequence']); $this->assertSame(3, $result['PidTagSensitivity']); $this->assertSame(true, $result['PidTagHasAttachments']); $this->assertSame(array('tag1'), $result['PidNameKeywords']); /* $this->assertSame('/kolab.org/Europe/Berlin', $result['dtstart']['parameters']['tzid']); $this->assertSame('2015-05-15T10:00:00', $result['dtstart']['date-time']); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtend']['parameters']['tzid']); $this->assertSame('2015-05-15T10:30:00', $result['dtend']['date-time']); $this->assertSame('https://some.url', $result['url']); */ $this->assertSame('Summary', $result['PidTagSubject']); $this->assertSame('Description', $result['PidTagBody']); $this->assertSame(2, $result['PidTagImportance']); $this->assertSame('Location', $result['PidLidLocation']); $this->assertSame('German, Mark', $result['recipients'][0]['PidTagDisplayName']); $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagEmailAddress']); $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); $this->assertSame(3, $result['recipients'][0]['PidTagRecipientFlags']); $this->assertSame('Manager, Jane', $result['recipients'][1]['PidTagDisplayName']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientType']); $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); $this->assertSame(0, $result['recipients'][1]['PidTagRecipientTrackStatus']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientFlags']); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); $this->assertTrue($result['PidLidAppointmentTimeZoneDefinitionStartDisplay'] == $result['PidLidAppointmentTimeZoneDefinitionStartDisplay']); // PidLidTimeZoneDefinition $tzd = new kolab_api_filter_mapistore_structure_timezonedefinition; $tzd->input($result['PidLidAppointmentTimeZoneDefinitionStartDisplay'], true); $this->assertSame('Europe/Berlin', $tzd->KeyName); $this->assertCount(1, $tzd->TZRules); $this->assertSame(2015, $tzd->TZRules[0]->Year); $this->assertSame(-60, $tzd->TZRules[0]->Bias); $data = kolab_api_tests::get_data('101-101-101-101', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '101-101-101-101'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(0, $result['PidTagSensitivity']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentStartWhole']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-16T00:00:00Z'), $result['PidLidAppointmentEndWhole']); $this->assertSame(24 * 60, $result['PidLidAppointmentDuration']); $this->assertSame(1, $result['PidLidAppointmentSubType']); $this->assertSame(null, $result['PidTagHasAttachments']); // EXDATE $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(1, $arp->RecurrencePattern->Period); $this->assertSame(0x200B, $arp->RecurrencePattern->RecurFrequency); $this->assertSame(1, $arp->RecurrencePattern->PatternType); $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); // RDATE $data = kolab_api_tests::get_data('102-102-102-102', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); // recurrence $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); $this->assertSame(2, $arp->RecurrencePattern->ModifiedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->ModifiedInstanceDates); $this->assertSame(2, $arp->ExceptionCount); $this->assertCount(2, $arp->ExceptionInfo); $this->assertCount(2, $arp->ExtendedException); // PidLidTimeZoneStruct $tz = new kolab_api_filter_mapistore_structure_timezonestruct; $tz->input($result['PidLidTimeZoneStruct'], true); $this->assertSame(-60, $tz->Bias); $this->assertSame(0, $tz->StandardYear); $this->assertSame(10, $tz->StandardDate->Month); $this->assertSame('(GMT+01:00) Europe/Berlin', $result['PidLidTimeZoneDescription']); } /** * Test recurrences output */ function test_output_recurrence() { $data = array( 'dtstart' => '2015-01-01T00:00:00Z', 'rrule' => array( 'recur' => array( 'freq' => 'MONTHLY', 'bymonthday' => 5, 'count' => 10, 'interval' => 2, ), ), 'exdate' => array( 'date' => array( '2015-01-01', '2016-01-01', ), ), 'rdate' => array( 'date' => array( '2015-02-01', ), ), ); $api = new kolab_api_filter_mapistore_event; $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $result = $api->output($data, $context); $arp->input($result['PidLidAppointmentRecur'], 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); - // @TODO: test mode recurrence exception details $this->assertSame(5, $arp->RecurrencePattern->PatternTypeSpecific); $this->assertSame(10, $arp->RecurrencePattern->OccurrenceCount); $this->assertSame(2, $arp->RecurrencePattern->Period); $this->assertSame(3, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(3, $arp->RecurrencePattern->DeletedInstanceDates); $this->assertSame(1, $arp->RecurrencePattern->ModifiedInstanceCount); $this->assertCount(1, $arp->RecurrencePattern->ModifiedInstanceDates); $this->assertSame(1, $arp->ExceptionCount); $this->assertCount(1, $arp->ExceptionInfo); $this->assertCount(1, $arp->ExtendedException); $this->assertSame(null, $result['PidLidAppointmentDuration']); } /** * Test alarms output */ function test_output_alarms() { kolab_api::$now = new DateTime('2015-01-20 00:00:00 UTC'); $data = array( 'dtstart' => '2015-01-01T00:00:00Z', 'rrule' => array( 'recur' => array( 'freq' => 'MONTHLY', 'bymonthday' => 5, 'count' => 10, 'interval' => 1, ), ), 'valarm' => array( array( 'properties' => array( 'action' => 'DISPLAY', 'trigger' => array( 'duration' => '-PT15M', ), ), ), ), 'duration' => 'PT10M', ); $api = new kolab_api_filter_mapistore_event; $result = $api->output($data, $context); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); $this->assertSame('2015-02-20T00:00:00+00:00', kolab_api_filter_mapistore_common::date_mapi2php($result['PidLidReminderTime'])->format('c')); $this->assertSame('2015-02-19T23:45:00+00:00', kolab_api_filter_mapistore_common::date_mapi2php($result['PidLidReminderSignalTime'])->format('c')); $this->assertSame(10, $result['PidLidAppointmentDuration']); } + /** + * Test exceptions output + */ + function test_output_exceptions() + { + kolab_api::$now = new DateTime('2015-05-10 00:00:00 UTC'); + + $api = new kolab_api_filter_mapistore_event; + $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; + $exi = new kolab_api_filter_mapistore_structure_exceptioninfo; + $data = kolab_api_tests::get_data('103-103-103-103', 'Calendar', 'event', 'json', $context); + $result = $api->output($data, $context); + + $this->assertSame(true, $result['PidTagHasAttachments']); + + $arp->input($result['PidLidAppointmentRecur'], true); + + $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK, $arp->RecurrencePattern->PatternType); + $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_WEEKLY, $arp->RecurrencePattern->RecurFrequency); + + $this->assertSame(2, $arp->RecurrencePattern->ModifiedInstanceCount); + $this->assertCount(2, $arp->RecurrencePattern->ModifiedInstanceDates); + $this->assertSame(1, $arp->RecurrencePattern->DeletedInstanceCount); + $this->assertCount(1, $arp->RecurrencePattern->DeletedInstanceDates); + $this->assertCount(2, $arp->ExtendedException); + $this->assertCount(2, $arp->ExceptionInfo); + + $this->assertSame('Location mod', $arp->ExceptionInfo[0]->Location); + $this->assertSame(35, $arp->ExceptionInfo[0]->ReminderDelta); + $this->assertSame(1, $arp->ExceptionInfo[0]->ReminderSet); + $this->assertSame(kolab_api_filter_mapistore_common::date_php2minutes(new DateTime('2015-06-22T00:00:00+00:00')), $arp->ExceptionInfo[0]->StartDateTime); + $this->assertSame(kolab_api_filter_mapistore_common::date_php2minutes(new DateTime('2015-06-23T00:00:00+00:00')), $arp->ExceptionInfo[0]->EndDateTime); + $this->assertSame(kolab_api_filter_mapistore_common::date_php2minutes(new DateTime('2015-06-22T00:00:00+00:00')), $arp->ExceptionInfo[0]->OriginalStartDate); + + $this->assertSame(kolab_api_filter_mapistore_common::date_php2minutes(new DateTime('2015-06-29T00:00:00+00:00')), $arp->ExceptionInfo[1]->StartDateTime); + $this->assertSame(kolab_api_filter_mapistore_common::date_php2minutes(new DateTime('2015-06-30T00:00:00+00:00')), $arp->ExceptionInfo[1]->EndDateTime); + $this->assertSame(kolab_api_filter_mapistore_common::date_php2minutes(new DateTime('2015-06-29T00:00:00+00:00')), $arp->ExceptionInfo[1]->OriginalStartDate); + + $this->assertSame('Location mod', $arp->ExtendedException[0]->WideCharLocation); + $this->assertSame($arp->ExceptionInfo[0]->StartDateTime, $arp->ExtendedException[0]->StartDateTime); + $this->assertSame($arp->ExceptionInfo[0]->EndDateTime, $arp->ExtendedException[0]->EndDateTime); + $this->assertSame($arp->ExceptionInfo[0]->OriginalStartDate, $arp->ExtendedException[0]->OriginalStartDate); + + $this->assertSame(null, $arp->ExtendedException[1]->StartDateTime); + $this->assertSame(null, $arp->ExtendedException[1]->EndDateTime); + $this->assertSame(null, $arp->ExtendedException[1]->OriginalStartDate); + } + /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_event; $tzs = new kolab_api_filter_mapistore_structure_timezonestruct(array( 'Bias' => -60, 'StandardBias' => 0, 'DaylightBias' => -60, 'StandardDate' => new kolab_api_filter_mapistore_structure_systemtime(array( 'Month' => 10, 'DayOfWeek' => 0, 'Day' => 5, 'Hour' => 3, )), 'DaylightDate' => new kolab_api_filter_mapistore_structure_systemtime(array( 'Month' => 3, 'Day' => 5, 'DayOfWeek' => 0, 'Hour' => 2, )), )); $data = array( 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 10, 'PidTagSensitivity' => 3, 'PidNameKeywords' => array('work'), 'PidTagSubject' => 'subject', 'PidTagBody' => 'body', 'PidTagImportance' => 2, 'PidLidLocation' => 'location', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T16:00:00Z'), 'PidLidReminderDelta' => 15, 'PidLidReminderSet' => true, 'PidLidTimeZoneStruct' => $tzs->output(true), 'recipients' => array( array( 'PidTagDisplayName' => 'German, Mark', 'PidTagEmailAddress' => 'mark.german@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientFlags' => 3, ), array( 'PidTagDisplayName' => 'Manager, Jane', 'PidTagEmailAddress' => 'manager@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientTrackStatus' => 2, ), ), ); $result = $api->input($data); $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame(10, $result['sequence']); $this->assertSame('confidential', $result['class']); $this->assertSame(array('work'), $result['categories']); $this->assertSame('location', $result['location']); $this->assertSame(1, $result['priority']); $this->assertSame('2015-05-14T13:03:33Z', $result['created']); $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame('2015-05-14T15:03:33', $result['dtstart']['date-time']); $this->assertSame('2015-05-14T18:00:00', $result['dtend']['date-time']); $this->assertRegexp('/kolab.org/', $result['dtstart']['parameters']['tzid']); $this->assertRegexp('/kolab.org/', $result['dtend']['parameters']['tzid']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('TENTATIVE', $result['attendee'][0]['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); // $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:manager%40example.org', $result['attendee'][0]['cal-address']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:mark.german%40example.org', $result['organizer']['cal-address']); self::$original = $result; $tzdef = base64_encode(pack("H*", '0201300002001500' . '500061006300690066006900630020005300740061006E0064006100720064002000540069006D006500' . '0100' . '02013E000000D6070000000000000000000000000000E001000000000000C4FFFFFF00000A0000000500020000000000000000000400000001000200000000000000' )); $data = array( 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T05:00:00Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T05:00:00Z'), 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => $tzdef, 'PidLidReminderSet' => false, // @TODO: recurrence, exceptions, alarms ); $result = $api->input($data); $this->assertSame('2015-05-13T22:00:00', $result['dtstart']['date-time']); $this->assertSame('2015-05-14T05:00:00Z', $result['dtend']['date-time']); $this->assertSame(array(), $result['valarm']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_event; $data = array( // 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 20, 'PidTagSensitivity' => 2, 'PidNameKeywords' => array('work1'), 'PidTagSubject' => 'subject1', 'PidTagBody' => 'body1', 'PidTagImportance' => 1, 'PidLidLocation' => 'location1', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T16:00:00Z'), 'PidLidReminderDelta' => 25, 'PidLidReminderSet' => true, ); $result = $api->input($data, self::$original); $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame(20, $result['sequence']); $this->assertSame('private', $result['class']); $this->assertSame(array('work1'), $result['categories']); $this->assertSame('location1', $result['location']); $this->assertSame(5, $result['priority']); // $this->assertSame('2015-05-14T13:03:33Z', $result['created']); // $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame('2015-05-15T13:03:33Z', $result['dtstart']['date-time']); $this->assertSame('2015-05-15T16:00:00Z', $result['dtend']['date-time']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('-PT25M', $result['valarm'][0]['properties']['trigger']['duration']); // @TODO: exceptions, attendees } /** * Test input recurrence */ function test_input_recurrence() { $api = new kolab_api_filter_mapistore_event; // 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(217742400, 218268000, 217787040); $recurrencepattern->ModifiedInstanceDates = array(217787040); $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 = $api->input(array('PidLidAppointmentRecur' => $rule), $context); $this->assertSame('WEEKLY', $result['rrule']['recur']['freq']); $this->assertSame('SU', $result['rrule']['recur']['wkst']); $this->assertSame('SU,TU,MO,TH,FR', $result['rrule']['recur']['byday']); $this->assertSame(12, $result['rrule']['recur']['count']); $this->assertSame('2015-01-01', $result['exdate']['date'][0]); $this->assertSame('2016-01-01', $result['exdate']['date'][1]); $this->assertSame('2015-02-01', $result['rdate']['date'][0]); } /** * Test input body */ function test_input_body() { $api = new kolab_api_filter_mapistore_event; $body = '0QAAAB0CAABMWkZ1Pzsq5D8ACQMwAQMB9wKnAgBjaBEKwHNldALRcHJx4DAgVGFoA3ECgwBQ6wNUDzcyD9MyBgAGwwKDpxIBA+MReDA0EhUgAoArApEI5jsJbzAVwzEyvjgJtBdCCjIXQRb0ORIAHxeEGOEYExjgFcMyNTX/CbQaYgoyGmEaHBaKCaUa9v8c6woUG3YdTRt/Hwwabxbt/xyPF7gePxg4JY0YVyRMKR+dJfh9CoEBMAOyMTYDMUksgSc1FGAnNhqAJ1Q3My3BNAqFfS7A'; $result = $api->input(array('PidTagRtfCompressed' => $body), $context); $this->assertSame('Test
', $result['description']); $result = $api->input(array('PidTagBody' => 'test'), $context); $this->assertSame('test', $result['description']); $result = $api->input(array('PidTagHtml' => 'test'), $context); $this->assertSame('test', $result['description']); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_event; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/data/data.json b/tests/data/data.json index 62be4ee..bc2a2d6 100644 --- a/tests/data/data.json +++ b/tests/data/data.json @@ -1,78 +1,78 @@ { "folders": { "INBOX": { "type": "mail.inbox", "items": ["1","2","5","6","7"] }, "Trash": { "type": "mail.wastebasket", "items": [] }, "Drafts": { "type": "mail.drafts" }, "Sent": { "type": "mail.sentitems" }, "Junk": { "type": "mail.junkemail" }, "Calendar": { "type": "event.default", - "items": ["100-100-100-100","101-101-101-101"], + "items": ["100-100-100-100","101-101-101-101","103-103-103-103"], "children": 1, "deleted": 0, "uidvalidity": 123456789, - "unread": 2, - "exists": 2, - "size": 8776 + "unread": 3, + "exists": 3, + "size": 16102 }, "Calendar/Personal Calendar": { "type": "event" }, "Contacts": { "type": "contact.default", "items": ["a-b-c-d","e-f-g-h","i-j-k-l"] }, "Files": { "type": "file.default" }, "Files2": { "type": "file" }, "Notes": { "type": "note.default", "items": ["1-1-1-1","2-2-2-2"] }, "Tasks": { "type": "task.default", "items":["10-10-10-10","20-20-20-20"] }, "Configuration": { "type": "configuration.default", "items": ["98-98-98-98","99-99-99-99"] }, "Mail-Test": { "type": "mail" }, "Mail-Test2": { "type": "mail" } }, "tags": { "tag1": { "members": ["1", "10-10-10-10", "1-1-1-1", "a-b-c-d", "100-100-100-100"] }, "tag2": { } } } diff --git a/tests/data/event/103-103-103-103 b/tests/data/event/103-103-103-103 new file mode 100644 index 0000000..5216c65 --- /dev/null +++ b/tests/data/event/103-103-103-103 @@ -0,0 +1,259 @@ +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_8256fd02ca814c49ec3e2a7eed8c4917" +From: mark.german@example.org +To: mark.german@example.org +Date: Thu, 25 Jun 2015 10:24:25 +0200 +X-Kolab-Type: application/x-vnd.kolab.event +X-Kolab-Mime-Version: 3.0 +Subject: 103-103-103-103 +User-Agent: Kolab 3.1/Roundcube 1.2-git + +--=_8256fd02ca814c49ec3e2a7eed8c4917 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=ISO-8859-1 + +This is a Kolab Groupware object. To view this object you will need an emai= +l client that understands the Kolab Groupware format. For a list of such em= +ail clients please visit http://www.kolab.org/ + + +--=_8256fd02ca814c49ec3e2a7eed8c4917 +Content-Transfer-Encoding: 8bit +Content-Type: application/calendar+xml; charset=UTF-8; + name=kolab.xml +Content-Disposition: attachment; + filename=kolab.xml; + + + + + + + + Roundcube-libkolab-1.1 Libkolabxml-1.1 + + + 2.0 + + + 3.1.0 + + + + + + + 103-103-103-103 + + + 2015-05-14T14:59:09Z + + + 2015-06-25T08:24:25Z + + + 0 + + + PUBLIC + + + 2015-05-15 + + + 2015-05-15 + + + + WEEKLY + MO + + + + + + /kolab.org/Europe/Berlin + + + 2015-07-06 + + + Summary + + + + + German, Mark + + + mailto:%3Cmark.german%40example.org%3E + + + + + + + AUDIO + + + + + + + + unknown + + + null + + + + + START + + + -PT15M + + + + + + + + + 103-103-103-103 + + + 2015-05-14T14:59:09Z + + + 2015-06-25T08:24:25Z + + + 1 + + + PUBLIC + + + 2015-06-22 + + + 2015-06-22 + + + 2015-06-22 + + + Summary + + + Location mod + + + + + German, Mark + + + mailto:%3Cmark.german%40example.org%3E + + + + + + + DISPLAY + + + Summary + + + + + START + + + -PT35M + + + + + + + + + 103-103-103-103 + + + 2015-05-14T14:59:09Z + + + 2015-06-25T08:24:25Z + + + 0 + + + PUBLIC + + + 2015-06-29 + + + 2015-06-29 + + + 2015-06-29 + + + Summary + + + desc + + + + + German, Mark + + + mailto:%3Cmark.german%40example.org%3E + + + + + + + AUDIO + + + + + + + + unknown + + + null + + + + + START + + + -PT15M + + + + + + + + + + +--=_8256fd02ca814c49ec3e2a7eed8c4917--