diff --git a/lib/api/folders.php b/lib/api/folders.php index 5817c1f..374b7b3 100644 --- a/lib/api/folders.php +++ b/lib/api/folders.php @@ -1,347 +1,375 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_folders extends kolab_api { protected $model = 'folder'; public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; $request_length = count($path); if (!$request_length && $method == 'POST') { $this->api_folder_create(); } else if (!$request_length && $method == 'GET') { $this->api_folder_list_folders(); } else if ($request_length >= 1) { switch (strtolower((string) $path[1])) { case 'objects': if ($method == 'HEAD') { $this->api_folder_count_objects(); } else if ($method == 'GET') { $this->api_folder_list_objects(); } break; case 'folders': if ($method == 'HEAD') { $this->api_folder_count_folders(); } else if ($method == 'GET') { $this->api_folder_list_folders(); } break; case 'empty': if ($method == 'POST') { $this->api_folder_empty(); } break; case 'deleteobjects': if ($method == 'POST') { $this->api_folder_delete_objects(); } break; + case 'move': + if ($method == 'POST') { + $this->api_folder_move_objects(); + } + break; + case '': if ($request_length == 1) { if ($method == 'GET') { $this->api_folder_info(); } else if ($method == 'PUT') { $this->api_folder_update(); } else if ($method == 'HEAD') { $this->api_folder_exists(); } else if ($method == 'DELETE') { $this->api_folder_delete(); } } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Returns list of folders (all or subfolders of specified folder) */ protected function api_folder_list_folders() { $root = $this->input->path[0]; $list = $this->backend->folders_list(); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; // filter by parent if ($root) { $this->filter_folders($list, $root); } $list = array_map(array($this, 'folder_props_filter'), $list); $this->output->send($list, $this->model . '-list', null, $props); } /** * Returns count of folders (all or subfolders of specified folder) */ protected function api_folder_count_folders() { $root = $this->input->path[0]; $list = $this->backend->folders_list(); // filter by parent if ($root) { $this->filter_folders($list, $root); } $this->output->headers(array('X-Count' => count($list))); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Returns folders info */ protected function api_folder_info() { $folder = $this->input->path[0]; $info = $this->backend->folder_info($folder); $info = self::folder_props_filter($info); $this->output->send($info, $this->model); } /** * Checks if folder exists */ protected function api_folder_exists() { $folder = $this->input->path[0]; $folder = $this->backend->folder_uid2path($folder); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Delete a folder */ protected function api_folder_delete() { $folder = $this->input->path[0]; $this->backend->folder_delete($folder); $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Update specified folder (rename or set folder type, or state) */ protected function api_folder_update() { $folder = $this->input->path[0]; $request = $this->input->input($this->model); $info = $this->backend->folder_info($folder); // need rename? $parent_change = isset($request['parent']) && $request['parent'] != $info['parent']; $name_change = isset($request['name']) && $request['name'] !== $info['name']; $fullpath = $info['fullpath']; $updates = array(); // rename first if ($parent_change || $name_change) { if ($name_change) { if (!strlen($request['name'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $path = explode($this->backend->delimiter, $info['fullpath']); array_pop($path); $path = implode($this->backend->delimiter, $path); $info['name'] = $request['name']; $info['fullpath'] = (strlen($path) ? $path . $this->backend->delimiter : '') . $request['name']; } if ($parent_change) { if ($request['parent']) { $parent = $this->backend->folder_uid2path($request['parent']); $info['fullpath'] = $parent . $this->backend->delimiter . $info['name']; } else { $info['fullpath'] = $info['name']; } } $this->backend->folder_rename($fullpath, $info['fullpath']); } // type change? if (isset($request['type']) && $request['type'] != $info['type']) { // @TODO: don't allow type changes on subfolders or non-empty folders? // @TODO: check input validity $updates['type'] = $request['type']; } // subscription change? if (isset($request['subscribed']) && $request['subscribed'] != $info['subscribed']) { $updates['subscribed'] = (bool) $request['subscribed']; } if (!empty($updates)) { $this->backend->folder_update($folder, $updates); } $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Create a folder */ protected function api_folder_create() { $folder = $this->input->input($this->model); if (!isset($folder['name']) || !strlen($folder['name'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $uid = $this->backend->folder_create($folder['name'], $folder['parent'], $folder['type']); // @TODO: folder subscribe, activate $this->output->send(array('uid' => $uid), $this->model, null, array('uid')); } /** * Returns list of objects (not folders) in specified folder */ protected function api_folder_list_objects() { $folder = $this->input->path[0]; $type = $this->backend->folder_type($folder); $list = $this->backend->objects_list($folder); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $context = array('folder_uid' => $folder); $this->output->send($list, $type . '-list', $context, $props); } /** * Returns count of objects (not folders) in specified folder */ protected function api_folder_count_objects() { $folder = $this->input->path[0]; $count = $this->backend->objects_count($folder); $this->output->headers(array('X-Count' => $count)); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Delete objects in specified folder */ protected function api_folder_delete_objects() { $folder = $this->input->path[0]; $set = $this->input->input(); if (empty($set)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $this->backend->objects_delete($folder, $set); $this->output->send_status(kolab_api_output::STATUS_OK); } + /** + * Move objects into specified folder + */ + protected function api_folder_move_objects() + { + $folder = $this->input->path[0]; + $target = $this->input->path[2]; + $set = $this->input->input(); + + if (empty($set)) { + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); + } + + if ($target === null || $target === '' || $target === $folder) { + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); + } + + $this->backend->objects_move($folder, $target, $set); + + $this->output->send_status(kolab_api_output::STATUS_OK); + } + /** * Remove all objects in specified folder */ protected function api_folder_empty() { $folder = $this->input->path[0]; $this->backend->objects_delete($folder, '*'); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Filter folders by parent */ protected function filter_folders(&$list, $parent) { // filter by parent if ($parent && ($parent = $this->backend->folder_uid2path($parent))) { // we'll compare with 'fullpath' which is in UTF-8 $parent .= $this->backend->delimiter; foreach ($list as $idx => $folder) { if (strpos($folder['fullpath'], $parent) !== 0) { unset($list[$idx]); } } } $list = array_values($list); } /** * Filter folder data and return only supported properties */ public static function folder_props_filter($data) { static $props; if ($props === null) { $props = array( 'uid', 'parent', 'name', 'fullpath', 'type', 'rights', 'namespace', 'exists', 'unseen', 'modseq', ); $props = array_combine($props, $props); } return array_intersect_key($data, $props); } } diff --git a/lib/filter/mapistore.php b/lib/filter/mapistore.php index 39dae0d..ca0c73e 100644 --- a/lib/filter/mapistore.php +++ b/lib/filter/mapistore.php @@ -1,594 +1,624 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore extends kolab_api_filter { protected $input; protected $attrs_filter; // Common properties [MS-OXCMSG] protected static $map = array( // 'PidTagAccess' => '', // 'PidTagAccessLevel' => '', // 0 - read-only, 1 - modify // 'PidTagChangeKey' => '', 'PidTagCreationTime' => 'creation-date', // PtypTime, UTC 'PidTagLastModificationTime' => 'last-modification-date', // PtypTime, UTC // 'PidTagLastModifierName' => '', // 'PidTagObjectType' => '', // @TODO // 'PidTagHasAttachments' => '', // @TODO // 'PidTagRecordKey' => '', // 'PidTagSearchKey' => '', 'PidNameKeywords' => 'categories', ); /** * Modify request path * * @param array (Exploded) request path */ public function path(&$path) { // handle differences between OpenChange API and Kolab API // here we do only very basic modifications, just to be able // to select apprioprate api action class if ($path[0] == 'calendars') { $path[0] = 'events'; } } /** * Executed before every api action * * @param kolab_api_input Request */ public function input(&$input) { $this->input = $input; // handle differences between OpenChange API and Kolab API switch ($input->action) { case 'folders': // in OpenChange folders/1/folders means get all folders if ($input->method == 'GET' && $input->path[0] === '1' && $input->path[1] == 'folders') { $input->path = array(); $type = 'folder'; } // in OpenChange folders/0/folders means get the hierarchy of the NON IPM Subtree // we should ignore/send empty request else if ($input->method == 'GET' && $input->path[0] === '0' && $input->path[1] == 'folders') { // @TODO throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } else if ($input->path[1] == 'messages') { $input->path[1] = 'objects'; if ($input->args['properties']) { $type = $input->api->backend->folder_type($input->path[0]); list($type, ) = explode('.', $type); } } else if ($input->path[1] == 'deletemessages') { $input->path[1] = 'deleteobjects'; } // properties filter, map MAPI attribute names to Kolab attributes if ($type && $input->args['properties']) { $this->attrs_filter = explode(',', $this->input->args['properties']); $properties = $this->attributes_filter($this->attrs_filter, $type); $input->args['properties'] = implode(',', $properties); } break; case 'notes': // Notes do not have attachments in Exchange if ($input->path[1] === 'attachments' || count($this->path) > 2) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } break; } $common_action = !in_array($input->action, array('folders', 'info')); // convert / to // or /// if ($common_action && ($uid = $input->path[0])) { list($folder, $msg, $attach) = self::uid_decode($uid); $path = array($folder, $msg); if ($attach) { $path[] = $attach; } array_splice($input->path, 0, 1, $path); } // convert parent_id into path on object create request if ($input->method == 'POST' && $common_action && !count($input->path)) { $data = $input->input(null, true); if ($data['parent_id']) { $input->path[0] = $data['parent_id']; } else { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } } + // convert parent_id into path on object update request + else if ($input->method == 'PUT' && $folder && count($input->path) == 2) { + $data = $input->input(null, true); + + if ($data['parent_id'] && $data['parent_id'] != $folder) { + $this->parent_change_handler($data); + } + } } /** * Executed when parsing request body * * @param string Request data * @param string Expected object type * @param string Original object data (set on update requests) */ public function input_body(&$data, $type = null, $original_object = null) { $input = $this->input; // handle differences between OpenChange API and Kolab API // Note: input->path is already modified by input() and path() above switch ($input->action) { case 'folders': // folders//deletemessages input if ($input->path[1] == 'deleteobjects') { // Kolab API expects just a list of identifiers, I.e.: // [{"id": "1"}, {"id": "2"}] => ["1", "2"] foreach ((array) $data as $idx => $element) { $data[$idx] = $element['id']; } } break; } switch ($type) { case 'attachment': case 'event': case 'note': case 'task': case 'contact': case 'mail': case 'folder': $model = $this->get_model_class($type); $data = $model->input($data, $original_object); break; } } /** * Apply filter on output data * * @param array Result data * @param string Object type * @param array Context (folder_uid, object_uid, object) * @param array Optional attributes filter */ public function output(&$result, $type, $context = null, $attrs_filter = array()) { // handle differences between OpenChange API and Kolab API $model = $this->get_model_class($type); if (!empty($this->attrs_filter)) { $attrs_filter = array_combine($this->attrs_filter, $this->attrs_filter); } else if (!empty($attrs_filter)) { $attrs_filter = $this->attributes_filter($attrs_filter, $type, true); $attrs_filter = array_combine($attrs_filter, $attrs_filter); } foreach ($result as $idx => $data) { if ($filtered = $model->output($data, $context)) { // apply properties filter (again) if (!empty($attrs_filter)) { $filtered = array_intersect_key($filtered, $attrs_filter); } $result[$idx] = $filtered; } else { unset($result[$idx]); $unset = true; } } if ($unset) { $result = array_values($result); } } /** * Executed for response headers * * @param array Response headers */ public function headers(&$headers) { // handle differences between OpenChange API and Kolab API foreach ($headers as $name => $value) { switch ($name) { case 'X-Count': $headers['X-mapistore-rowcount'] = $value; unset($headers[$name]); break; } } } /** * Executed for empty response status * * @param int Status code */ public function send_status(&$status) { // handle differences between OpenChange API and Kolab API } /** * Extracts data from kolab data array */ public static function get_kolab_value($data, $name) { $name_items = explode('.', $name); $count = count($name_items); $value = $data[$name_items[0]]; // special handling of x-custom properties if ($name_items[0] === 'x-custom') { foreach ((array) $value as $custom) { if ($custom['identifier'] === $name_items[1]) { return $custom['value']; } } return null; } for ($i = 1; $i < $count; $i++) { if (!is_array($value)) { return null; } list($key, $num) = explode(':', $name_items[$i]); $value = $value[$key]; if ($num !== null && $value !== null) { $value = is_array($value) ? $value[$num] : null; } } return $value; } /** * Sets specified kolab data item */ public static function set_kolab_value(&$data, $name, $value) { $name_items = explode('.', $name); $count = count($name_items); $element = &$data; // x-custom properties if ($name_items[0] === 'x-custom') { // this is supposed to be converted later by parse_common_props() $data[$name] = $value; return; } if ($count > 1) { for ($i = 0; $i < $count - 1; $i++) { $key = $name_items[$i]; if (!array_key_exists($key, $element)) { $element[$key] = array(); } $element = &$element[$key]; } } $element[$name_items[$count - 1]] = $value; } /** * Converts kolab identifiers describind the object into * MAPI identifier that can be easily used in URL. * * @param string Folder UID * @param string Object UID * @param string Optional attachment identifier * * @return string Object identifier */ public static function uid_encode($folder_uid, $msg_uid, $attach_id = null) { $result = array($folder_uid, $msg_uid); if ($attach_id) { $result[] = $attach_id; } $result = array_map(array('kolab_api_filter_mapistore', 'uid_encode_item'), $result); return implode('.', $result); } /** * Converts back the MAPI identifier into kolab folder/object/attachment IDs * * @param string Object identifier * * @return array Object identifiers */ public static function uid_decode($uid) { $result = explode('.', $uid); $result = array_map(array('kolab_api_filter_mapistore', 'uid_decode_item'), $result); return $result; } /** * Encodes UID element */ protected static function uid_encode_item($str) { $fn = function($match) { return '_' . ord($match[1]); }; $str = preg_replace_callback('/([^0-9a-zA-Z-])/', $fn, $str); return $str; } /** * Decodes UID element */ protected static function uid_decode_item($str) { $fn = function($match) { return chr($match[1]); }; $str = preg_replace_callback('/_([0-9]{2})/', $fn, $str); return $str; } /** * Parse common properties in object data (convert into MAPI format) */ public static function parse_common_props(&$result, $data, $context = array()) { if (empty($context)) { // @TODO: throw exception? return; } if ($data['uid'] && $context['folder_uid']) { $result['id'] = self::uid_encode($context['folder_uid'], $data['uid']); } if ($context['folder_uid']) { $result['parent_id'] = $context['folder_uid']; } foreach (self::$map as $mapi_idx => $kolab_idx) { if (!isset($result[$mapi_idx]) && ($value = $data[$kolab_idx]) !== null) { switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': $result[$mapi_idx] = self::date_php2mapi($value, true); break; case 'PidNameKeywords': $result[$mapi_idx] = self::parse_categories((array) $value); break; } } } } /** * Convert common properties into kolab format */ public static function convert_common_props(&$result, $data, $original) { // @TODO: id, parent_id? foreach (self::$map as $mapi_idx => $kolab_idx) { if (array_key_exists($mapi_idx, $data) && !array_key_exists($kolab_idx, $result)) { $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $dt = self::date_mapi2php($value); $result[$kolab_idx] = $dt->format('Y-m-d\TH:i:s\Z'); } break; default: if ($value) { $result[$kolab_idx] = $value; } break; } } } // Handle x-custom fields foreach ((array) $result as $key => $value) { if (strpos($key, 'x-custom.') === 0) { unset($result[$key]); $key = substr($key, 9); foreach ((array) $original['x-custom'] as $idx => $custom) { if ($custom['identifier'] == $key) { if ($value) { $original['x-custom'][$idx]['value'] = $value; } else { unset($original['x-custom'][$idx]); } $x_custom_update = true; continue 2; } } if ($value) { $original['x-custom'][] = array( 'identifier' => $key, 'value' => $value, ); } $x_custom_update = true; } } if ($x_custom_update) { $result['x-custom'] = array_values($original['x-custom']); } } /** * Filter property names */ public function attributes_filter($attrs, $type = null, $reverse = false) { $map = self::$map; $result = array(); if ($type) { $model = $this->get_model_class($type); $map = array_merge($map, $model->map()); } // add some special common attributes $map['PidTagMessageClass'] = 'PidTagMessageClass'; $map['collection'] = 'collection'; $map['id'] = 'uid'; foreach ($attrs as $attr) { if ($reverse) { if ($name = array_search($attr, $map)) { $result[] = $name; } } else if ($name = $map[$attr]) { $result[] = $name; } } return $result; } /** * Return instance of model class object */ protected function get_model_class($type) { $class = "kolab_api_filter_mapistore_$type"; return new $class($this); } /** * Convert DateTime object to MAPI date format */ public function date_php2mapi($date, $utc = true, $time = null) { // convert string to DateTime if (!is_object($date) && !empty($date)) { // convert date to datetime on 00:00:00 if (preg_match('/^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/', $date, $m)) { $date = $m[1] . '-' . $m[2] . '-' . $m[3] . 'T00:00:00+00:00'; } $date = new DateTime($date); } if (!is_object($date)) { return; } if ($utc) { $date->setTimezone(new DateTimeZone('UTC')); } if (!empty($time)) { $date->setTime((int) $time['hour'], (int) $time['minute'], (int) $time['second']); } // MAPI PTypTime is 64-bit integer representing the number // of 100-nanosecond intervals since January 1, 1601. // Note: probably does not work on 32-bit systems return ($date->format('U') + 11644473600) * 10000000; } /** * Convert date-time from MAPI format to DateTime */ public function date_mapi2php($date) { // Note: probably does not work on 32-bit systems $seconds = intval($date / 10000000) - 11644473600; // assumes we're working with dates after 1970-01-01 return new DateTime('@' . $seconds); } /** * Parse categories according to [MS-OXCICAL 2.1.3.1.1.20.3] * * @param array Categories * * @return array Categories */ public static function parse_categories($categories) { if (!is_array($categories)) { return; } $result = array(); foreach ($categories as $idx => $val) { $val = preg_replace('/(\x3B|\x2C|\x06\x1B|\xFE\x54|\xFF\x1B)/', '', $val); $val = preg_replace('/\s+/', ' ', $val); $val = trim($val); $len = mb_strlen($val); if ($len) { if ($len > 255) { $val = mb_substr($val, 0, 255); } $result[mb_strtolower($val)] = $val; } } return array_values($result); } + + /** + * Handles object parent modification (move) + */ + protected function parent_change_handler($data) + { + $folder = $this->input->path[0]; + $uid = $this->input->path[1]; + $target = $data['parent_id']; + $api = kolab_api::get_instance(); + + // move the object + $api->backend->objects_move($folder, $target, array($uid)); + + // replace folder uid in input arguments + $this->input->path[0] = $target; + + // exit if the rest of input is empty + if (count($data) < 2) { + $api->output->send_status(kolab_api_output::STATUS_EMPTY); + } + } } diff --git a/lib/kolab_api_backend.php b/lib/kolab_api_backend.php index fee9bc1..23eb830 100644 --- a/lib/kolab_api_backend.php +++ b/lib/kolab_api_backend.php @@ -1,1177 +1,1227 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $api; public $storage; public $username; public $password; public $delimiter; protected $icache = array(); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $this->api = kolab_api::get_instance(); $this->storage = $this->api->get_storage(); // @TODO: reset cache? if we do this for every request the cache would be useless // There's no session here //$this->storage->clear_cache('mailboxes.', true); // set additional header used by libkolab $this->storage->set_options(array( // @TODO: there can be Roundcube plugins defining additional headers, // we maybe would need to add them here 'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION', 'skip_deleted' => true, 'threading' => false, )); // Disable paging $this->storage->set_pagesize(999999); $this->delimiter = $this->storage->get_hierarchy_delimiter(); } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { $host = $this->select_host($username); // use shared cache for kolab_auth plugin result (username canonification) $cache = $this->api->get_cache_shared('kolab_api_auth'); $cache_key = sha1($username . '::' . $host); if (!$cache || !($auth = $cache->get($cache_key))) { $auth = $this->api->plugins->exec_hook('authenticate', array( 'host' => $host, 'user' => $username, 'pass' => $password, )); if ($cache && !$auth['abort']) { $cache->set($cache_key, array( 'user' => $auth['user'], 'host' => $auth['host'], )); } // LDAP server failure... send 503 error if ($auth['kolab_ldap_error']) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } } else { $auth['pass'] = $password; } // authenticate user against the IMAP server $user_id = $auth['abort'] ? 0 : $this->login($auth['user'], $auth['pass'], $auth['host'], $error); if ($user_id) { $this->username = $auth['user']; $this->password = $auth['pass']; $this->delimiter = $this->storage->get_hierarchy_delimiter(); return true; } // IMAP server failure... send 503 error if ($error == rcube_imap_generic::ERROR_BAD) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } return false; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders, False on backend failure */ public function folders_list($type = null) { $type_keys = array( kolab_storage::CTYPE_KEY_PRIVATE, kolab_storage::CTYPE_KEY, ); // get folder unique identifiers and types $uid_data = $this->folder_uids(); $type_data = $this->storage->get_metadata('*', $type_keys); $folders = array(); if (!is_array($type_data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } foreach ($uid_data as $folder => $uid) { $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = null; if ($folders[$parent]) { $parent_id = $folders[$parent]['uid']; } // parent folder does not exist add it to the list else { for ($i=0; $idelimiter, $parent_arr); if ($folders[$parent]) { $parent_id = $folders[$parent]['uid']; } else { $fid = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $folders[$parent] = array( 'name' => array_pop($parent_arr), 'fullpath' => $parent, 'uid' => $fid, 'parent' => $parent_id, ); $parent_id = $fid; } } } } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); // folder type reset($type_keys); foreach ($type_keys as $key) { $data['type'] = $type_data[$folder][$key]; break; } if (empty($data['type'])) { $data['type'] = 'mail'; } $folders[$path] = $data; } // sort folders uksort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Returns folder type * * @param string $uid Folder unique identifier * @param string $with_suffix Enable to not remove the subtype * * @return string Folder type */ public function folder_type($uid, $with_suffix = false) { $folder = $this->folder_uid2name($uid); $type = kolab_storage::folder_type($folder); if ($type === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type rcube_message_header or kolab_format) * @throws kolab_api_exception */ public function objects_list($uid) { $type = $this->folder_type($uid); // use IMAP to fetch mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); $result = $this->storage->list_messages($folder, 1); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->get_objects(); if ($result === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $type = $this->folder_type($uid); // use IMAP to count mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); // @TODO: error checking requires changes in rcube_imap $result = $this->storage->count($folder, 'ALL'); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->count(); } return $result; } /** * Delete objects in a folder * * @param string $uid Folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_delete($uid, $set) { $type = $this->folder_type($uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); } // delete all if ($set === "*") { if ($is_mail) { $result = $this->storage->clear_folder($folder); } else { $result = $folder->delete_all(); } } else { if ($is_mail) { $result = $this->storage->delete_message($set, $folder); } else { foreach ($set as $uid) { $result = $folder->delete($uid); if ($result === false) { break; } } } } if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } + /** + * Move objects into another folder + * + * @param string $uid Folder unique identifier + * @param string $target_uid Target folder unique identifier + * @param string|array $set List of object IDs or "*" for all + * + * @throws kolab_api_exception + */ + public function objects_move($uid, $target_uid, $set) + { + $type = $this->folder_type($uid); + $target_type = $this->folder_type($target_uid); + + if ($type === 'mail') { + $is_mail = true; + $folder = $this->folder_uid2name($uid); + $target = $this->folder_uid2name($uid); + } + // otherwise use kolab_storage + else { + $folder = $this->folder_get_by_uid($uid, $type); + $target = $this->folder_get_by_uid($uid, $type); + } + + if ($is_mail) { + if ($set === "*") { + $set = '1:*'; + } + + $result = $this->storage->move_messages($set, $target, $folder); + } + else { + if ($set === "*") { + $set = $folder->get_uids(); + } + + foreach ($set as $uid) { + $result = $folder->move($uid, $target); + if ($result === false) { + break; + } + } + } + + if ($result === false) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + } + /** * Get object data * * @param string $folder_uid Folder unique identifier * @param string $uid Object identifier * * @return rcube_message|array Object data * @throws kolab_api_exception */ public function object_get($folder_uid, $uid) { $type = $this->folder_type($folder_uid); if ($type === 'mail') { $folder = $this->folder_uid2name($folder_uid); $object = new rcube_message($uid, $folder); if (!$object || empty($object->headers)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $object = $folder->get_object($uid); if (!$object) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $old_categories = $object['categories']; } // @TODO: Use relations also for events if ($type != 'configuration' && $type != 'event') { // get object categories (tag-relations) $categories = $this->get_tags($object, $old_categories); if ($type === 'mail') { $object->categories = $categories; } else { $object['categories'] = $categories; } } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param array $data Object data * @param string $type Object type * * @return string Object UID * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype !== 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); // @TODO } // otherwise use kolab_storage else { // @TODO: Use relations also for events if (!preg_match('/^(event|configuration)/', $type)) { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$folder->save($data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // @TODO: Use relations also for events if (!empty($categories)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } return $data['uid']; } } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param array $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype != 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); // @TODO } // otherwise use kolab_storage else { // @TODO: Use relations also for events if (!preg_match('/^(event|configuration)/', $type)) { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$folder->save($data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // @TODO: Use relations also for events if (array_key_exists('categories', $data)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } } } /** * Get attachment body * * @param mixed $object Object data (from self::object_get()) * @param string $part_id Attachment part identifier * @param mixed $mode NULL to return a string, -1 to print body * or file pointer to save the body into * * @return string Attachment body if $fp=null * @throws kolab_api_exception */ public function attachment_get($object, $part_id, $mode = null) { // object is a mail message if ($object instanceof rcube_message) { return $object->get_part_body($part_id, false, 0, $mode); } // otherwise use kolab_storage else { return $this->storage->get_message_part($this->uid, $part_id, null, $mode === -1, is_resource($mode) ? $mode : null, true, 0, false); } } /** * Delete an attachment from the message * * @param mixed $object Object data (from self::object_get()) * @param string $id Attachment identifier * * @return boolean|string True or message UID (if changed) * @throws kolab_api_exception */ public function attachment_delete($object, $id) { // object is a mail message if ($object instanceof rcube_message) { // @TODO } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $found = false; // unset the attachment foreach ((array) $object['_attachments'] as $idx => $att) { if ($att['id'] == $id) { $object['_attachments'][$idx] = false; $found = true; } } if (!$found) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if (!$folder->save($data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return true; } } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $name = rcube_charset::convert($name, RCUBE_CHARSET, 'UTF7-IMAP'); if ($parent) { $parent = $this->folder_uid2name($parent); $name = $parent . $this->delimiter . $name; } if ($this->storage->folder_exists($name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $created = kolab_storage::folder_create($name, $type, false, false); if ($created) { $created = $this->folder_name2uid($name); } return $created; } /** * Subscribes a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folder_uid2name($uid); if (isset($updates['type'])) { $result = kolab_storage::set_folder_type($folder, $updates['type']); if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } if (isset($updates['subscribed'])) { if ($updates['subscribed']) { $result = $this->storage->subscribe($folder); } else { $result = $this->storage->unsubscribe($folder); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } // @TODO: active state } /** * Renames a folder * * @param string $old_name Folder name (UTF-8) * @param string $new_name New folder name (UTF-8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_name = rcube_charset::convert($old_name, RCUBE_CHARSET, 'UTF7-IMAP'); $new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP'); if (!strlen($old_name) || !strlen($new_name) || $old_name === $new_name) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if ($this->storage->folder_exists($new_name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // don't use kolab_storage for moving mail folders if (preg_match('/^mail/', $type)) { $result = $this->storage->rename_folder($old_name, $new_name); } else { $result = kolab_storage::folder_rename($old_name, $new_name); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folder_uid2name($uid); $type = $this->folder_type($uid); // don't use kolab_storage for mail folders if ($type === 'mail') { $status = $this->storage->delete_folder($folder); } else { $status = kolab_storage::folder_delete($folder); } if (!$status) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Folder info * * @param string $uid Folder UID * * @return array Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folder_uid2name($uid); // get IMAP folder info $info = $this->storage->folder_info($folder); // get IMAP folder data $data = $this->storage->folder_data($folder); $info['exists'] = $data['EXISTS']; $info['unseen'] = $data['UNSEEN']; $info['modseq'] = $data['HIGHESTMODSEQ']; // add some more parameters (used in folders list response) $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); $path = explode($this->delimiter, $path); $info['name'] = array_pop($path); $info['fullpath'] = implode($this->delimiter, $path); $info['uid'] = $uid; $info['type'] = kolab_storage::folder_type($folder, true) ?: 'mail'; if ($info['fullpath'] !== '') { $parent = $this->folder_name2uid(rcube_charset::convert($info['fullpath'], RCUBE_CHARSET, 'UTF7-IMAP')); $info['parent'] = $parent; } // convert some info to be more compact if (!empty($info['rights'])) { $info['rights'] = implode('', $info['rights']); } // @TODO: subscription status, active state // some info is not very interesting here ;) unset($info['attributes']); return $info; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { $folder = $this->folder_uid2name($uid); return strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); } /** * Returns IMAP folder name * * @param string $uid Folder identifier * * @return string Folder name (UTF7-IMAP) */ protected function folder_uid2name($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // we store last folder in-memory if (isset($this->icache["folder:$uid"])) { return $this->icache["folder:$uid"]; } $uids = $this->folder_uids(); foreach ($uids as $folder => $_uid) { if ($uid === $_uid) { return $this->icache["folder:$uid"] = $folder; } } // slowest method, but we need to try it, the full folders list // might contain non-existing folder (not in folder_uids() result) foreach ($this->folders_list as $folder) { if ($folder['uid'] === $uid) { return rcube_charset::convert($folder['fullpath'], RCUBE_CHARSET, 'UTF7-IMAP'); } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Helper method to get folder UID * * @param string $folder Folder name (UTF7-IMAP) * * @return string Folder's UID */ protected function folder_name2uid($folder) { $uid_keys = array( kolab_storage::UID_KEY_CYRUS, ); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array( kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_SHARED, ); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } if (!empty($metadata[$folder])) { foreach ($uid_keys as $key) { if ($uid = $metadata[$folder][$key]) { return $uid; } } } return md5($folder); /* // @TODO: // make sure folder exists // generate a folder UID and set it to IMAP $uid = rtrim(chunk_split(md5($folder . $this->get_owner() . uniqid('-', true)), 12, '-'), '-'); if (!$this->storage->set_metadata($folder, array(kolab_storage::UID_KEY_SHARED => $uid))) { if ($this->storage->set_metadata($folder, array(kolab_storage::UID_KEY_PRIVATE => $uid))) { return $uid; } } // create hash from folder name if we can't write the UID metadata return md5($folder . $this->get_owner()); */ } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1); $path2 = explode($this->delimiter, $str2); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Return UIDs of all folders * * @return array Folder name to UID map */ protected function folder_uids() { $uid_keys = array( kolab_storage::UID_KEY_CYRUS, ); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array( kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_SHARED, ); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } $lambda = function(&$item, $key, $keys) { reset($keys); foreach ($keys as $key) { $item = $item[$key]; return; } }; array_walk($metadata, $lambda, $uid_keys); return $metadata; } /** * Get folder by UID (use only for non-mail folders) * * @param string $uid Folder UID * @param string $type Folder type * * @return kolab_storage_folder Folder object * @throws kolab_api_exception */ protected function folder_get_by_uid($uid, $type = null) { $folder = $this->folder_uid2name($uid); $folder = kolab_storage::get_folder($folder, $type); if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // Check the given storage folder instance for validity and throw // the right exceptions according to the error state. if (!$folder->valid || ($error = $folder->get_error())) { if ($error === kolab_storage::ERROR_IMAP_CONN) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_CACHE_DB) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_NO_PERMISSION) { throw new kolab_api_exception(kolab_api_exception::FORBIDDEN); } else if ($error === kolab_storage::ERROR_INVALID_FOLDER) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $folder; } /** * Storage host selection */ protected function select_host($username) { // Get IMAP host $host = $this->api->config->get('default_host', 'localhost'); if (is_array($host)) { list($user, $domain) = explode('@', $username); // try to select host by mail domain if (!empty($domain)) { foreach ($host as $storage_host => $mail_domains) { if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { $host = $storage_host; break; } else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { $host = is_numeric($storage_host) ? $mail_domains : $storage_host; break; } } } // take the first entry if $host is not found if (is_array($host)) { list($key, $val) = each($default_host); $host = is_numeric($key) ? $val : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP and returns Roundcube user ID. */ protected function login($username, $password, $host, &$error = null) { if (empty($username)) { return null; } $login_lc = $this->api->config->get('login_lc'); $default_port = $this->api->config->get('default_port', 143); // parse $host $a_host = parse_url($host); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP if (!$this->storage->connect($host, $username, $password, $port, $ssl)) { $error = $this->storage->get_error_code(); return null; } // No user in database, but IMAP auth works if (!is_object($user)) { if ($this->api->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ), true, false); return null; } } else { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", ), true, false); return null; } } // overwrite config with user preferences $this->user = $user; $this->api->config->set_user_prefs((array)$this->user->get_prefs()); setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); return $user->ID; } /** * Returns list of tag-relation names assigned to kolab object */ protected function get_tags($object, $categories = null) { // Kolab object if (is_array($object)) { $ident = $object['uid']; } // Mail message else if (is_object($object)) { // support only messages with message-id $ident = $object->headers->get('message-id', false); $folder = $message->folder; $uid = $message->uid; } if (empty($ident)) { return array(); } $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($ident); $delta = 300; // resolve members if it wasn't done recently if ($uid) { foreach ($tags as $idx => $tag) { $force = empty($this->tag_rts[$tag['uid']]) || $this->tag_rts[$tag['uid']] <= time() - $delta; $members = $config->resolve_members($tag, $force); if (empty($members[$folder]) || !in_array($uid, $members[$folder])) { unset($tags[$idx]); } if ($force) { $this->tag_rts[$tag['uid']] = time(); } } // make sure current folder is set correctly again $this->storage->set_folder($folder); } $tags = array_filter(array_map(function($v) { return $v['name']; }, $tags)); // merge result with old categories if (!empty($categories)) { $tags = array_unique(array_merge($tags, (array) $categories)); } return $tags; } /** * Set tag-relations to kolab object */ protected function set_tags($uid, $tags) { // @TODO: set_tags() for email $config = kolab_storage_config::get_instance(); $config->save_tags($uid, $tags); } } diff --git a/tests/API/Folders.php b/tests/API/Folders.php index 1ce4c63..57f1403 100644 --- a/tests/API/Folders.php +++ b/tests/API/Folders.php @@ -1,324 +1,371 @@ 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->assertCount(15, $body); $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(); $this->assertEquals(204, $code); $this->assertSame('', $body); // 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(204, $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']); } /** * Test folder create */ function test_folder_delete_objects() { $post = json_encode(array('10')); 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); } /** * 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(4, (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->assertSame('101-101-101-101', $body[0]['uid']); + + // @TODO: the same for mail + } } diff --git a/tests/Mapistore/Contacts.php b/tests/Mapistore/Contacts.php index 19658c2..b9e5eb1 100644 --- a/tests/Mapistore/Contacts.php +++ b/tests/Mapistore/Contacts.php @@ -1,231 +1,232 @@ get('folders/' . kolab_api_tests::folder_uid('Contacts') . '/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('Contacts', true, 'a-b-c-d'), $body[0]['id']); $this->assertSame(kolab_api_tests::folder_uid('Contacts'), $body[0]['parent_id']); $this->assertSame('contacts', $body[0]['collection']); $this->assertSame('IPM.Contact', $body[0]['PidTagMessageClass']); $this->assertSame('displname', $body[0]['PidTagDisplayName']); } /** * Test contact existence */ function test_contact_exists() { self::$api->head('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing contact self::$api->get('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test contact info */ function test_contact_info() { 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(kolab_api_tests::folder_uid('Contacts'), $body['parent_id']); $this->assertSame('contacts', $body['collection']); $this->assertSame('IPM.Contact', $body['PidTagMessageClass']); $this->assertSame('displname', $body['PidTagDisplayName']); } /** * Test contact create */ function test_contact_create() { $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Contacts'), 'PidTagSurname' => 'lastname', 'PidTagTitle' => 'Test title', )); self::$api->post('contacts', 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' => kolab_api_tests::folder_uid('non-existing'), 'PidTagSurname' => 'lastname', 'PidTagTitle' => 'Test title', )); self::$api->post('contacts', 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('Contacts'), 'test' => 'Test summary 2', )); self::$api->post('contacts', array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test contact update */ function test_contact_update() { // @TODO: most of this should probably be in unit-tests not functional-tests $post = array( 'PidTagTitle' => 'Title', 'PidTagNickname' => 'Nickname', 'PidTagDisplayName' => 'DisplayName', 'PidTagSurname' => 'Surname', 'PidTagGivenName' => 'GivenName', 'PidTagMiddleName' => 'MiddleName', 'PidTagDisplayNamePrefix' => 'Prefix', 'PidTagGeneration' => 'Generation', 'PidTagBody' => 'Body', 'PidLidFreeBusyLocation' => 'FreeBusyLocation', 'PidTagCompanyName' => 'CompanyName', 'PidTagDepartmentName' => 'Department', 'PidTagProfession' => 'Profession', 'PidTagManagerName' => 'Manager', 'PidTagAssistant' => 'Assistant', 'PidTagPersonalHomePage' => 'HomePage', 'PidTagOtherAddressStreet' => 'OtherStreet', 'PidTagOtherAddressCity' => 'OtherCity', 'PidTagOtherAddressStateOrProvince' => 'OtherState', 'PidTagOtherAddressPostalCode' => 'OtherCode', 'PidTagOtherAddressCountry' => 'OtherCountry', // 'PidTagOtherAddressPostOfficeBox' => 'OtherBox', 'PidTagHomeAddressStreet' => 'HomeStreet', 'PidTagHomeAddressCity' => 'HomeCity', 'PidTagHomeAddressStateOrProvince' => 'HomeState', 'PidTagHomeAddressPostalCode' => 'HomeCode', 'PidTagHomeAddressCountry' => 'HomeCountry', // 'PidTagHomeAddressPostOfficeBox' => 'HomeBox', 'PidLidWorkAddressStreet' => 'WorkStreet', 'PidLidWorkAddressCity' => 'WorkCity', 'PidLidWorkAddressState' => 'WorkState', 'PidLidWorkAddressPostalCode' => 'WorkCode', 'PidLidWorkAddressCountry' => 'WorkCountry', // 'PidLidWorkAddressPostOfficeBox' => 'WorkBox', 'PidTagGender' => 1, 'PidTagSpouseName' => 'Spouse', 'PidTagChildrensNames' => array('child'), 'PidTagHomeTelephoneNumber' => 'HomeNumber', 'PidTagBusinessTelephoneNumber' => 'BusinessNumber', 'PidTagHomeFaxNumber' => 'HomeFax', 'PidTagBusinessFaxNumber' => 'BusinessFax', 'PidTagMobileTelephoneNumber' => 'Mobile', 'PidTagPagerTelephoneNumber' => 'Pager', 'PidTagCarTelephoneNumber' => 'Car', 'PidTagOtherTelephoneNumber' => 'OtherNumber', 'PidLidInstantMessagingAddress' => 'IM', 'PidLidEmail1EmailAddress' => 'email1@domain.tld', 'PidLidEmail2EmailAddress' => 'email1@domain.tld', 'PidLidEmail3EmailAddress' => 'email1@domain.tld', 'PidTagUserX509Certificate' => base64_encode('Certificate'), ); + self::$api->put('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d'), array(), json_encode($post)); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); self::$api->get('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d')); $body = self::$api->response_body(); $body = json_decode($body, true); foreach ($post as $idx => $value) { $this->assertSame($value, $body[$idx], "Test for update of $idx"); } } /** * Test contact delete */ function test_contact_delete() { // delete existing contact self::$api->delete('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing contact self::$api->get('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/Mapistore/Events.php b/tests/Mapistore/Events.php index aef15a4..71b147a 100644 --- a/tests/Mapistore/Events.php +++ b/tests/Mapistore/Events.php @@ -1,226 +1,253 @@ 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']); } /** * 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::date_php2mapi('2015-01-01'), )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['id'])); // folder does not exists $post = json_encode(array( 'parent_id' => md5('non-existing'), 'PidTagSubject' => 'Test summary', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-01-01'), )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'parent_id' => 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::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(204, $code); $this->assertSame('', $body); 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); } /** * 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']); } /** * 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(204, $code); + $this->assertSame('', $body); + + 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/lib/kolab_api_backend.php b/tests/lib/kolab_api_backend.php index 9917c14..323c0f8 100644 --- a/tests/lib/kolab_api_backend.php +++ b/tests/lib/kolab_api_backend.php @@ -1,668 +1,714 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $delimiter = '/'; protected $db = array(); protected $folder = array(); protected $data = array(); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $api = kolab_api::get_instance(); $db_file = $api->config->get('temp_dir') . '/tests.db'; if (file_exists($db_file)) { $db = file_get_contents($db_file); $this->db = unserialize($db); } $json = file_get_contents(__DIR__ . '/../data/data.json'); $this->data = json_decode($json, true); $this->folders = $this->parse_folders_list($this->data['folders']); if (!array_key_exists('tags', $this->db)) { $this->db['tags'] = $this->data['tags']; } } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { return true; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders, False on backend failure */ public function folders_list($type = null) { return array_values($this->folders); } /** * Returns folder type * * @param string $uid Folder unique identifier * @param string $with_suffix Enable to not remove the subtype * * @return string Folder type */ public function folder_type($uid, $with_suffix = false) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $type = $folder['type'] ?: 'mail'; if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type rcube_message_header or kolab_format) * @throws kolab_api_exception */ public function objects_list($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $result = array(); $is_mail = empty($folder['type']) || preg_match('/^mail/', $folder['type']); foreach ((array) $folder['items'] as $id) { $object = $this->object_get($uid, $id); if ($is_mail) { $object = $object->headers; } $result[] = $object; } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return count($folder['items']); } /** * Delete objects in a folder * * @param string $uid Folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_delete($uid, $set) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($set === '*') { $this->folders[$uid]['items'] = array(); - $this->db['items'][$folder_uid] = array(); - $result = true; + $this->db['items'][$uid] = array(); } else { $this->folders[$uid]['items'] = array_values(array_diff($this->folders[$uid]['items'], $set)); foreach ($set as $i) { - unset($this->folders[$uid]['items'][$i]); + unset($this->db['items'][$uid][$i]); } } $this->db['folders'][$uid]['items'] = $this->folders[$uid]['items']; $this->save_db(); } + /** + * Move objects into another folder + * + * @param string $uid Folder unique identifier + * @param string $target_uid Target folder unique identifier + * @param string|array $set List of object IDs or "*" for all + * + * @throws kolab_api_exception + */ + public function objects_move($uid, $target_uid, $set) + { + $folder = $this->folders[$uid]; + + if (!$folder) { + throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + } + + $target = $this->folders[$target_uid]; + + if (!$target) { + throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + } + + if ($set === "*") { + $set = $this->folders[$uid]['items']; + } + + // @TODO: we should check if all objects from the set exist + + $diff = array_values(array_diff($this->folders[$uid]['items'], $set)); + $this->folders[$uid]['items'] = $diff; + $this->db['folders'][$uid]['items'] = $diff; + + $diff = array_values(array_merge((array) $this->folders[$target_uid]['items'], $set)); + $this->folders[$target_uid]['items'] = $diff; + $this->db['folders'][$target_uid]['items'] = $diff; + + foreach ($set as $i) { + if ($this->db['items'][$uid][$i]) { + $this->db['items'][$target_uid][$i] = $this->db['items'][$uid][$i]; + unset($this->db['items'][$uid][$i]); + } + } + + $this->save_db(); + } + /** * Get object data * * @param string $folder_uid Folder unique identifier * @param string $uid Object identifier * * @return rcube_message|array Object data * @throws kolab_api_exception */ public function object_get($folder_uid, $uid) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if (!in_array($uid, (array) $folder['items'])) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($data = $this->db['items'][$folder_uid][$uid]) { return $data; } list($type,) = explode('.', $folder['type']); $file = file_get_contents(__DIR__ . '/../data/' . $type . '/' . $uid); if (empty($file)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // 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); // get assigned tag-relations $tags = array(); foreach ($this->db['tags'] as $tag_name => $tag) { if (in_array($uid, (array) $tag['members'])) { $tags[] = $tag_name; } } if ($type != 'mail') { $object = $object->to_array($type); $object['categories'] = $tags; } else { $object->categories = $tags; } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param string $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /* if (strpos($folder['type'], $type) !== 0) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ $data['uid'] = microtime(true); if (!empty($data['categories'])) { foreach ($categories as $cat) { if (!$this->db['tags'][$cat]) { $this->db['tags'][$cat] = array(); } if (!in_array($uid, (array) $this->db['tags'][$cat]['members'])) { $this->db['tags'][$cat]['members'][] = $uid; } } } $this->folders[$folder_uid]['items'][] = $data['uid']; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->db['items'][$folder_uid][$data['uid']] = $data; $this->save_db(); return $data['uid']; } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param string $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /* if (strpos($folder['type'], $type) !== 0) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ if (array_key_exists('categories', $data)) { foreach ($this->db['tags'] as $tag_name => $tag) { if (($idx = array_search($tag_name, (array) $data['categories'])) !== false) { unset($data['categories'][$idx]); continue; } $this->db['tags'][$tag_name]['members'] = array_diff((array)$this->db['tags'][$tag_name]['members'], array($data['uid'])); } foreach ((array) $data['categories'] as $cat) { $this->db['tags'][$cat] = array('members' => array($data['uid'])); } } // remove _formatobj which is problematic in serialize/unserialize unset($data['_formatobj']); $this->db['items'][$folder_uid][$data['uid']] = $data; $this->save_db(); } /** * Get attachment body * * @param mixed $object Object data (from self::object_get()) * @param string $part_id Attachment part identifier * @param mixed $mode NULL to return a string, -1 to print body * or file pointer to save the body into * * @return string Attachment body if $mode=null * @throws kolab_api_exception */ public function attachment_get($object, $part_id, $mode = null) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } // check if it's not deleted if (in_array($msg_uid . ":" . $part_id, (array) $this->db['deleted_attachments'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $body = $object->get_part_body($part_id); if (!$mode) { return $body; } else if ($mode === -1) { echo $body; } } /** * Delete an attachment from the message * * @param mixed $object Object data (from self::object_get()) * @param string $id Attachment identifier * * @return boolean|string True or message UID (if changed) * @throws kolab_api_exception */ public function attachment_delete($object, $id) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; $key = $msg_uid . ":" . $part_id; // check if it's not deleted if (in_array($key, (array) $this->db['deleted_attachments'])) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } if ($object->get_part_body($id) === null) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $this->db['deleted_attachments'][] = $key; $this->save_db(); } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $folder = $name; if ($parent) { $parent_folder = $this->folders[$parent]; if (!$parent_folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $folder = $parent_folder['fullpath'] . $this->delimiter . $folder; } $uid = kolab_api_tests::folder_uid($folder, false); // check if folder exists if ($this->folders[$uid]) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $this->folders[$uid] = array( 'name' => $name, 'fullpath' => $folder, 'parent' => $parent ? kolab_api_tests::folder_uid($parent, false) : null, 'uid' => $uid, 'type' => $type ? $type : 'mail', ); $this->db['folders'][$uid] = $this->folders[$uid]; $this->save_db(); return $uid; } /** * Updates a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } foreach ($updates as $idx => $value) { $this->db['folders'][$uid][$idx] = $value; $this->folders[$uid][$idx] = $value; } $this->save_db(); } /** * Renames/moves a folder * * @param string $old_name Folder name (UTF8) * @param string $new_name New folder name (UTF8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_uid = kolab_api_tests::folder_uid($old_name, false); $new_uid = kolab_api_tests::folder_uid($new_name, false); $folder = $this->folders[$old_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($this->folders[$new_uid]) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $path = explode($this->delimiter, $new_name); $folder['fullpath'] = $new_name; $folder['name'] = array_pop($path); unset($this->folders[$old_uid]); $this->folders[$new_uid] = $folder; $this->db['folders'][$new_uid] = $folder; $this->db['deleted'][] = $old_uid; $this->save_db(); } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } unset($this->folders[$uid]); $this->db['deleted'][] = $uid; $this->save_db(); } /** * Folder info * * @param string $uid Folder UID * * @return array Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // some info is not very interesting here ;) unset($folder['items']); return $folder; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return $folder['fullpath']; } /** * Parse folders list into API format */ protected function parse_folders_list($list) { $folders = array(); foreach ($list as $path => $folder) { $uid = kolab_api_tests::folder_uid($path, false); if (!empty($this->db['deleted']) && in_array($uid, $this->db['deleted'])) { continue; } if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = kolab_api_tests::folder_uid($parent, false); } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); if (!empty($this->db['folders']) && !empty($this->db['folders'][$uid])) { $data = array_merge($data, $this->db['folders'][$uid]); } $folders[$uid] = array_merge($folder, $data); } foreach ((array) $this->db['folders'] as $uid => $folder) { if (!$folders[$uid]) { $folders[$uid] = $folder; } } // sort folders uasort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1['fullpath']); $path2 = explode($this->delimiter, $str2['fullpath']); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Save current database state */ protected function save_db() { $api = kolab_api::get_instance(); $db_file = $api->config->get('temp_dir') . '/tests.db'; $db = serialize($this->db); file_put_contents($db_file, $db); } }