diff --git a/lib/filter/mapistore.php b/lib/filter/mapistore.php index 1d5262e..36333f7 100644 --- a/lib/filter/mapistore.php +++ b/lib/filter/mapistore.php @@ -1,646 +1,636 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore extends kolab_api_filter { protected $input; protected $api; protected $attrs_filter; /** * 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); } } // 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($this->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); } // 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; } } 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; } $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($this); } /** * 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() { 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') { unset($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? else if ($this->input->path[0] && $this->input->path[1] && $this->input->method == 'POST') { $data = $this->input->input(null, true); 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); } $object['photo'] = base64_decode($data['PidTagAttachDataBinary']); $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } } } /** * 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(); $type = 'folder'; } // 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(); + $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'; if ($input->args['properties'] && !$this->is_builtin_folder($this->path[0])) { $type = $this->api->backend->folder_type($input->path[0]); list($type, ) = explode('.', $type); } } else if ($input->path[1] == 'deletemessages') { $input->path[1] = 'deleteobjects'; } // properties filter, map MAPI attribute names to Kolab attributes if ($type && $input->args['properties']) { $this->attrs_filter = explode(',', $this->input->args['properties']); $properties = $this->attributes_filter($this->attrs_filter, $type); $input->args['properties'] = implode(',', $properties); } // 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() + protected function get_builtin_folder_list($parent = null) { $folders = kolab_api_filter_mapistore_folder::$builtin_folders; foreach ($folders as $idx => $folder) { - $folders[$idx] = array( - 'name' => $folder, - 'comment' => $folder, - 'parent' => 0, - 'uid' => $idx, - 'hidden' => true, - 'role' => 10, - 'system_idx' => $idx, - 'item_count' => 0, // @TODO - ); + 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, + 'item_count' => 0, // @TODO + ), $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); + } + if (strval($uid) === '1') { - $folders = $this->api->backend->folders_list(); - $type_rxp = '/^(mail|task|note|event|journal|contact)/'; - $item_count = 0; + $folders = $this->api->backend->folders_list(); + $type_rxp = '/^(mail|task|note|event|journal|contact)/'; - // count top-level folders + // count real top-level folders foreach ($folders as $folder) { if (!$folder['parent'] && (!$folder['type'] || preg_match($type_rxp, $folder['type']))) { - $item_count++; + $result['item_count']++; } } - return array( - 'name' => 'MsgRoot', - 'comment' => '/', - 'parent' => -1, - 'uid' => 1, - 'hidden' => false, - 'role' => -1, - 'system_idx' => true, - 'item_count' => $item_count, - ); + // @FIXME: should we add item_count+1 for Outbox + return $result; } - $list = $this->get_builtin_folder_list(); - - if (strval($uid) === '0') { - return array( - 'name' => 'Root', - 'comment' => '/', - 'parent' => -1, - 'uid' => 0, - 'hidden' => false, - 'role' => -1, - 'system_idx' => true, - 'item_count' => count($list), - ); - } - - if (empty($list[$uid])) { - throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + // count subfolders of the built-in folder + foreach ($list as $folder) { + if ($folder['parent'] == $uid) { + $result['item_count']++; + } } - return $list[$uid]; + 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/folder.php b/lib/filter/mapistore/folder.php index b1771bb..e5d7fc8 100644 --- a/lib/filter/mapistore/folder.php +++ b/lib/filter/mapistore/folder.php @@ -1,209 +1,328 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_folder extends kolab_api_filter_mapistore_common { const TYPE_ROOT = 0; const TYPE_GENERIC = 1; const TYPE_SEARCH = 2; + const FOLDER_ROOT = 0; + const FOLDER_MSGROOT = 1; + + const FOLDER_INBOX = 10; + const FOLDER_OUTBOX = 11; + const FOLDER_DRAFTS = 12; + const FOLDER_SENT = 13; + const FOLDER_DELETED_ITEMS = 14; + const FOLDER_CONTACTS = 15; + const FOLDER_CALENDAR = 16; + const FOLDER_TASKS = 17; + const FOLDER_NOTES = 18; + const FOLDER_JOURNAL = 19; +// const FOLDER_FALLBACK = -1; + const FOLDER_DEFERRED = 101; const FOLDER_SPOOLER = 102; const FOLDER_COMMON_VIEWS = 103; const FOLDER_SCHEDULE = 104; const FOLDER_FINDER = 105; const FOLDER_VIEW = 106; const FOLDER_SHORTCUTS = 107; protected $model = 'folder'; protected $map = array( // [MS-OXCFOLD] read-only properties 'PidTagAccess' => '', 'PidTagChangeKey' => '', 'PidTagCreationTime' => '', // PtypTime, @TODO: store in folder annotation? 'PidTagLastModificationTime' => '', // PtypTime 'PidTagContentCount' => '', // PtypInteger32 'PidTagContentUnreadCount' => '', // PtypInteger32 'PidTagDeletedOn' => '', // PtypTime // 'PidTagAddressbookEntryId' => '', // PtypBinary 'PidTagFolderId' => '', // PtypInteger64 'PidTagHierarchyChangeNumber' => '', // PtypInteger32, 'PidTagMessageSize' => '', // PtypInteger32, size of all messages 'PidTagMessageSizeExtended' => '', // PtypInteger64 'PidTagSubfolders' => '', // PtypBoolean 'PidTagLocalCommitTime' => '', // PtypTime, last change time in UTC 'PidTagLocalCommitTimeMax' => '', // PtypTime 'PidTagDeletedCountTotal' => '', // PtypInteger32 // read-write properties 'PidTagAttributeHidden' => '', // PtypBoolean 'PidTagComment' => 'comment', // PtypString, @TODO: store in folder annotation? 'PidTagContainerClass' => 'type', // PtypString, IPF.* 'PidTagContainerHierarchy' => '', // PtypObject 'PidTagDisplayName' => 'name', // PtypString 'PidTagFolderAssociatedContents' => '', // PtypObject 'PidTagFolderType' => '', // PtypInteger32, 0 - namespace roots, 1 - other, 2 - virtual/search folders 'PidTagRights' => '', // PtypInteger32 'PidTagAccessControlListData' => '', // PtypBinary, see [MS-OXCPERM] ); protected $type_map = array( '' => 'IPF.Note', 'mail' => 'IPF.Note', 'task' => 'IPF.Task', 'note' => 'IPF.StickyNote', 'event' => 'IPF.Appointment', 'journal' => 'IPF.Journal', 'contact' => 'IPF.Contact', ); public static $builtin_folders = array( - self::FOLDER_DEFERRED => 'Deferred Action', - self::FOLDER_SPOOLER => 'Spooler Queue', - self::FOLDER_COMMON_VIEWS => 'Common Views', - self::FOLDER_SCHEDULE => 'Schedule', - self::FOLDER_FINDER => 'Finder', - self::FOLDER_VIEW => 'View', - self::FOLDER_SHORTCUTS => 'Shortcuts', + // Roots + self::FOLDER_ROOT => array( + 'name' => 'Root', + 'comment' => '/', + 'parent' => -1, + 'hidden' => false, + 'role' => -1, + 'system_idx' => true, + ), + self::FOLDER_MSGROOT => array( + 'name' => 'MsgRoot', + 'comment' => '/', + 'parent' => -1, + 'hidden' => false, + 'role' => -1, + 'system_idx' => true, + ), + // IPM subtree (and openchange "contexts") + self::FOLDER_INBOX => array( + 'name' => 'INBOX', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 0, + 'type' => 'mail.inbox', + ), + self::FOLDER_OUTBOX => array( + 'name' => 'Outbox', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 3, + ), + self::FOLDER_DRAFTS => array( + 'name' => 'Drafts', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 1, + 'type' => 'mail.drafts', + ), + self::FOLDER_SENT => array( + 'name' => 'Sent Items', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 2, + 'type' => 'mail.sentitems', + ), + self::FOLDER_DELETED_ITEMS => array( + 'name' => 'Deleted Items', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 4, + 'type' => 'mail.wastebasket', + ), + self::FOLDER_CONTACTS => array( + 'name' => 'Contacts', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 6, + 'type' => 'contact.default', + ), + self::FOLDER_CALENDAR => array( + 'name' => 'Calendar', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 5, + 'type' => 'event.default', + ), + self::FOLDER_TASKS => array( + 'name' => 'Tasks', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 7, + 'type' => 'task.default', + ), + self::FOLDER_NOTES => array( + 'name' => 'Notes', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 8, + 'type' => 'note.default', + ), +/* + self::FOLDER_JOURNAL => array( + 'name' => 'Journal', + 'parent' => self::FOLDER_MSGROOT, + 'role' => 9, + 'type' => 'journal.default', + ), +*/ + // Non-IPM Subtree + self::FOLDER_DEFERRED => array( + 'name' => 'Deferred Action', + 'parent' => self::FOLDER_ROOT, + ), + self::FOLDER_SPOOLER => array( + 'name' => 'Spooler Queue', + 'parent' => self::FOLDER_ROOT, + ), + self::FOLDER_COMMON_VIEWS => array( + 'name' => 'Common Views', + 'parent' => self::FOLDER_ROOT, + ), + self::FOLDER_SCHEDULE => array( + 'name' => 'Schedule', + 'parent' => self::FOLDER_ROOT, + ), + self::FOLDER_FINDER => array( + 'name' => 'Finder', + 'parent' => self::FOLDER_ROOT, + ), + self::FOLDER_VIEW => array( + 'name' => 'View', + 'parent' => self::FOLDER_ROOT, + ), + self::FOLDER_SHORTCUTS => array( + 'name' => 'Shortcuts', + 'parent' => self::FOLDER_ROOT, + ), ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { list($type, ) = explode('.', $data['type']); $type = $this->type_map[(string)$type]; // skip folders of unsupported type if (empty($type)) { return; } if (!isset($data['parent'])) { $data['parent'] = 1; } // skip folders that are not subfolders of the specified folder, // in list-mode MAPI always requests for one-level of the hierarchy (?) if ($api->input->path[1] == 'folders') { $api = kolab_api::get_instance(); $parent = $api->input->path[0]; if ($data['parent'] != $parent) { return; } } $result = array( // mapistore properties 'id' => $data['uid'], + 'collection' => 'folders', // MAPI properties 'PidTagDisplayName' => $data['name'], 'PidTagContainerClass' => $type, ); if ($data['uid'] === 1 || $data['uid'] === 0) { $result['PidTagFolderType'] = self::TYPE_ROOT; } else { $result['PidTagFolderType'] = self::TYPE_GENERIC; } if (isset($data['parent'])) { $result['parent_id'] = $data['parent']; } - $result['PidTagComment'] = (string) $data['comment']; + if (isset($data['comment'])) { + $result['PidTagComment'] = $data['comment']; + } // special Mapistore props foreach (array('role', 'item_count', 'system_idx', 'hidden') as $prop) { if (isset($data[$prop])) { $result[$prop] = $data[$prop]; } } $result = array_filter($result, function($v) { return $v !== null; }); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); // mapistore properties if ($data['id']) { $result['uid'] = $data['id']; } if ($data['parent_id']) { $result['parent'] = $data['parent_id']; } // MAPI properties if ($data['PidTagDisplayName']) { $result['name'] = $data['PidTagDisplayName']; } if ($data['PidTagContainerClass']) { // @TODO: what if folder is already a *.default or *.sentitems, etc. // we should keep the subtype intact $map = array_flip($this->type_map); $result['type'] = $map[$data['PidTagContainerClass']]; } return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['parent_id'] = 'parent'; $map['PidTagContainerClass'] = 'type'; $map['PidTagFolderType'] = 'PidTagFolderType'; return $map; } } diff --git a/lib/filter/mapistore/info.php b/lib/filter/mapistore/info.php index 18c2fd1..b966e19 100644 --- a/lib/filter/mapistore/info.php +++ b/lib/filter/mapistore/info.php @@ -1,89 +1,138 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_info { protected $model = 'info'; protected $map = array( 'name' => 'name', 'version' => 'version', 'capabilities' => 'capabilities', // @TODO ); /** * 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(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $data[$kolab_idx]; if ($value === null) { continue; } $result[$mapi_idx] = $value; } + $result['contexts'] = $this->contexts_list(); + $result = array_filter($result, function($v) { return $v !== null; }); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { return null; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } + + /** + * Get 'contexts' property + */ + protected function contexts_list() + { + $api = kolab_api::get_instance(); + + if (!$api->backend) { + + } + + $builtins = kolab_api_filter_mapistore_folder::$builtin_folders; + $folders = $api->backend->folders_list(); + $contexts = array(); + + foreach ($builtins as $idx => $folder) { + if ($folder['parent'] == kolab_api_filter_mapistore_folder::FOLDER_MSGROOT) { + // find real folder + if ($folder['type']) { + reset($folders); + foreach ($folders as $f) { + if ($folder['name'] == 'INBOX' && $f['name'] == 'INBOX') { + $idx = $f['uid']; + break; + } + else if ($folder['type'] == $f['type']) { + $idx = $f['uid']; + $folder['name'] = $f['name']; + } + } + } + + $url = $folder['url'] ?: "/folders/$idx/"; + + $contexts[] = array( + 'main_folder' => true, + 'name' => $folder['name'], + 'role' => $folder['role'], + 'system_idx' => $idx, + 'url' => $url, + ); + } + } + + return $contexts; + } } diff --git a/tests/Mapistore/Info.php b/tests/Mapistore/Info.php index 178ff88..40e7f07 100644 --- a/tests/Mapistore/Info.php +++ b/tests/Mapistore/Info.php @@ -1,51 +1,57 @@ get('info'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api::APP_NAME, $body['name']); $this->assertSame(kolab_api::VERSION, $body['version']); + $this->assertTrue(count($body['contexts']) >= 5); + $this->assertSame('INBOX', $body['contexts'][0]['name']); + $this->assertSame(0, $body['contexts'][0]['role']); + $this->assertSame(true, $body['contexts'][0]['main_folder']); + $this->assertTrue(!empty($body['contexts'][0]['system_idx'])); + $this->assertSame('/folders/'.$body['contexts'][0]['system_idx'].'/', $body['contexts'][0]['url']); } /** * Test non-existing request */ function test_nonexisting() { self::$api->post('info'); $code = self::$api->response_code(); $this->assertEquals(404, $code); } } diff --git a/tests/Unit/Filter/Mapistore/Folder.php b/tests/Unit/Filter/Mapistore/Folder.php index 837c610..3d29394 100644 --- a/tests/Unit/Filter/Mapistore/Folder.php +++ b/tests/Unit/Filter/Mapistore/Folder.php @@ -1,103 +1,104 @@ 'test-uid', 'parent' => 'parent', 'name' => 'folder name', ); $result = $api->output($data, $context); $this->assertSame('test-uid', $result['id']); $this->assertSame('parent', $result['parent_id']); + $this->assertSame('folders', $result['collection']); $this->assertSame('folder name', $result['PidTagDisplayName']); $this->assertSame(1, $result['PidTagFolderType']); $types = array( '' => 'IPF.Note', 'mail.inbox' => 'IPF.Note', 'task.default' => 'IPF.Task', 'note.default' => 'IPF.StickyNote', 'event.default' => 'IPF.Appointment', 'journal.default' => 'IPF.Journal', 'contact.default' => 'IPF.Contact', ); foreach ($types as $type => $exp) { $data = array('uid' => 'test', 'type' => $type); $result = $api->output($data, $context); $this->assertSame($exp, $result['PidTagContainerClass']); } } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_folder; $data = array( 'id' => 'test-uid', 'parent_id' => 'parent', 'PidTagDisplayName' => 'folder name', 'PidTagContainerClass' => 'IPF.Contact', ); $result = $api->input($data); $this->assertSame('test-uid', $result['uid']); $this->assertSame('parent', $result['parent']); $this->assertSame('folder name', $result['name']); $this->assertSame('contact', $result['type']); self::$original = $result; } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_folder; $data = array( // 'id' => 'test-uid', // 'parent_id' => 'parent', 'PidTagDisplayName' => 'folder name1', 'PidTagContainerClass' => 'IPF.Appointment', ); $result = $api->input($data, self::$original); // $this->assertSame('test-uid', $result['uid']); // $this->assertSame('parent', $result['parent']); $this->assertSame('folder name1', $result['name']); $this->assertSame('event', $result['type']); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_folder; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Info.php b/tests/Unit/Filter/Mapistore/Info.php index 884513e..e6209c6 100644 --- a/tests/Unit/Filter/Mapistore/Info.php +++ b/tests/Unit/Filter/Mapistore/Info.php @@ -1,63 +1,66 @@ 'test1', 'version' => 'version1', ); $result = $api->output($data, $context); $this->assertSame('test1', $result['name']); $this->assertSame('version1', $result['version']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_info; $data = array( 'name' => 'test1', 'version' => 'version1', ); $result = $api->input($data); $this->assertSame(null, $result); } /** * Test input method with merge */ function test_input2() { // there's nothing to test here } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_info; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/lib/kolab_api_tests.php b/tests/lib/kolab_api_tests.php index 33775c2..8f608fe 100644 --- a/tests/lib/kolab_api_tests.php +++ b/tests/lib/kolab_api_tests.php @@ -1,393 +1,405 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_tests { static $items_map; static $folders_map; /** * Reset backend state */ public static function reset_backend() { $rcube = rcube::get_instance(); $temp_dir = $rcube->config->get('temp_dir'); $filename = $temp_dir . '/tests.db'; if (file_exists($filename)) { unlink($filename); } $username = $rcube->config->get('tests_username'); $password = $rcube->config->get('tests_password'); if (!$username) { return; } $authenticated = self::login($username, $password); if (!$authenticated) { throw new Exception("IMAP login failed for user $username"); } // get all existing folders $imap = $rcube->get_storage(); $old_folders = $imap->list_folders('', '*'); $old_subscribed = $imap->list_folders_subscribed('', '*'); // get configured folders $json = file_get_contents(__DIR__ . '/../data/data.json'); $data = json_decode($json, true); $items = array(); $uids = array(); // initialize/update content in existing folders // create configured folders if they do not exists foreach ($data['folders'] as $folder_name => $folder) { if (($idx = array_search($folder_name, $old_folders)) !== false) { // cleanup messages in the folder $imap->delete_message('*', $folder_name); unset($old_folders[$idx]); // make sure it's subscribed if (!in_array($folder_name, $old_subscribed)) { $imap->subscribe($folder_name); } } else { // create the folder $imap->create_folder($folder_name, true); } // set folder type kolab_storage::set_folder_type($folder_name, $folder['type']); list($type, ) = explode('.', $folder['type']); // append messages foreach ((array) $folder['items'] as $uid) { $file = file_get_contents(__DIR__ . "/../data/$type/$uid"); $res = $imap->save_message($folder_name, $file); if (is_numeric($uid)) { $items[$uid] = $res; } } } // remove extra folders $deleted = array(); foreach ($old_folders as $folder) { // ...but only personal if ($imap->folder_namespace($folder) == 'personal') { $path = explode('/', $folder); while (array_pop($path) !== null) { if (in_array(implode('/', $path), $deleted)) { $deleted[] = $folder; continue 2; } } if (!$imap->delete_folder($folder)) { throw new Exception("Failed removing '$folder'"); } $deleted[] = $folder; } else { } } // get folder UIDs map $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $imap->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new Exception("Failed to get folders metadata"); } foreach ($metadata as $folder => $meta) { $uids[$folder] = $meta[kolab_storage::UID_KEY_CYRUS]; } self::$items_map = $items; self::$folders_map = $uids; } /** * Initialize testing environment */ public static function init() { $rcube = rcube::get_instance(); // If tests_username is set we use real Kolab server // otherwise use dummy backend class which emulates a real server if (!$rcube->config->get('tests_username')) { // Load backend wrappers for tests // @TODO: maybe we could replace kolab_storage and rcube_imap instead? require_once __DIR__ . '/kolab_api_backend.php'; } // Message wrapper for unit tests require_once __DIR__ . '/kolab_api_message.php'; // load HTTP_Request2 wrapper for functional/integration tests require_once __DIR__ . '/kolab_api_request.php'; // extend include path with kolab_format/kolab_storage classes $include_path = __DIR__ . '/../../lib/ext/plugins/libkolab/lib' . PATH_SEPARATOR . ini_get('include_path'); set_include_path($include_path); } /** * Initializes kolab_api_request object * * @param string Accepted response type (xml|json) * * @return kolab_api_request Request object */ public static function get_request($type, $suffix = '') { $rcube = rcube::get_instance(); $base_uri = $rcube->config->get('tests_uri', 'http://localhost/copenhagen-tests'); $username = $rcube->config->get('tests_username', 'test@example.org'); $password = $rcube->config->get('tests_password', 'test@example.org'); if ($suffix) { $base_uri .= $suffix; } $request = new kolab_api_request($base_uri, $username, $password); // set expected response type $request->set_header('Accept', $type == 'xml' ? 'application/xml' : 'application/json'); return $request; } /** * Get data object */ public static function get_data($uid, $folder_name, $type, $format = '', &$context = null) { $file = file_get_contents(__DIR__ . "/../data/$type/$uid"); $folder_uid = self::folder_uid($folder_name, false); // get message content and parse it $file = str_replace("\r?\n", "\r\n", $file); $params = array('uid' => $uid, 'folder' => $folder_uid); $object = new kolab_api_message($file, $params); if ($type != 'mail') { $object = $object->to_array($type); } else { $object = new kolab_api_message($object); } $context = array( 'object' => $object, 'folder_uid' => $folder_uid, 'object_uid' => $uid, ); if ($format) { $model = self::get_output_class($format, $type); $object = $model->element($object); } return $object; } public static function get_output_class($format, $type) { // fake GET request to have proper API class in kolab_api::get_instance $_GET['request'] = "{$type}s"; $output = "kolab_api_output_{$format}"; $class = "{$output}_{$type}"; $output = new $output(kolab_api::get_instance()); $model = new $class($output); return $model; } /** * Get folder UID by name */ public static function folder_uid($name, $api_test = true) { if ($api_test && !empty(self::$folders_map)) { if (self::$folders_map[$name]) { return self::$folders_map[$name]; } // it maybe is a newly created folder? check the metadata again $rcube = rcube::get_instance(); $imap = $rcube->get_storage(); $uid_keys = array(kolab_storage::UID_KEY_CYRUS); $metadata = $imap->get_metadata($name, $uid_keys); if ($uid = $metadata[$name][kolab_storage::UID_KEY_CYRUS]) { return self::$folders_map[$name] = $uid; } } return md5($name); } /** * Get message UID */ public static function msg_uid($uid, $api_test = true) { if ($uid && $api_test && !empty(self::$items_map)) { if (self::$items_map[$uid]) { return self::$items_map[$uid]; } } return $uid; } /** * Build MAPI object identifier */ public static function mapi_uid($folder_name, $api_test, $msg_uid, $attachment_uid = null) { $folder_uid = self::folder_uid($folder_name, $api_test); $msg_uid = self::msg_uid($msg_uid, $api_test); return kolab_api_filter_mapistore::uid_encode($folder_uid, $msg_uid, $attachment_uid); } protected static function login($username, $password) { $rcube = rcube::get_instance(); $login_lc = $rcube->config->get('login_lc'); $host = $rcube->config->get('default_host'); $default_port = $rcube->config->get('default_port', 143); $rcube->storage = null; $storage = $rcube->get_storage(); // parse $host $a_host = parse_url($host); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP if (!$storage->connect($host, $username, $password, $port, $ssl)) { throw new Exception("Unable to connect to IMAP"); } // No user in database, but IMAP auth works if (!is_object($user)) { if ($rcube->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { throw new Exception("Failed to create a user record"); } } else { throw new Exception("Access denied for new user $username. 'auto_create_user' is disabled"); } } // overwrite config with user preferences $rcube->user = $user; $rcube->config->set_user_prefs((array)$user->get_prefs()); /* $_SESSION['user_id'] = $user->ID; $_SESSION['username'] = $user->data['username']; $_SESSION['storage_host'] = $host; $_SESSION['storage_port'] = $port; $_SESSION['storage_ssl'] = $ssl; $_SESSION['password'] = $rcube->encrypt($password); $_SESSION['login_time'] = time(); */ setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); // clear the cache $storage->clear_cache('mailboxes', true); // to clear correctly the cache index in testing environments // (where we call self::reset_backend() many times in one go) // we need to also close() the cache if ($ctype = $rcube->config->get('imap_cache')) { $cache = $rcube->get_cache('IMAP', $ctype, $rcube->config->get('imap_cache_ttl', '10d')); $cache->close(); } // clear also libkolab cache $db = $rcube->get_dbh(); $db->query('DELETE FROM `kolab_folders`'); return true; } + + /** + * Initialize backend class, some unit-tests require it + */ + public static function init_backend() + { + $api = kolab_api::get_instance(); + + if (!$api->backend) { + $api->backend = kolab_api_backend::get_instance(); + } + } }