diff --git a/plugins/libkolab/lib/kolab_storage_config.php b/plugins/libkolab/lib/kolab_storage_config.php index b82a4a96..79b6f998 100644 --- a/plugins/libkolab/lib/kolab_storage_config.php +++ b/plugins/libkolab/lib/kolab_storage_config.php @@ -1,871 +1,902 @@ * @author Aleksander Machniak * * Copyright (C) 2012-2014, Kolab Systems AG * * 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 . */ class kolab_storage_config { const FOLDER_TYPE = 'configuration'; /** * Singleton instace of kolab_storage_config * * @var kolab_storage_config */ static protected $instance; private $folders; private $default; private $enabled; + private $tags; /** * This implements the 'singleton' design pattern * * @return kolab_storage_config The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_storage_config(); } return self::$instance; } /** * Private constructor (finds default configuration folder as a config source) */ private function __construct() { // get all configuration folders $this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false); foreach ($this->folders as $folder) { if ($folder->default) { $this->default = $folder; break; } } // if no folder is set as default, choose the first one if (!$this->default) { $this->default = reset($this->folders); } // attempt to create a default folder if it does not exist if (!$this->default) { $folder_name = 'Configuration'; $folder_type = self::FOLDER_TYPE . '.default'; if (kolab_storage::folder_create($folder_name, $folder_type, true)) { $this->default = new kolab_storage_folder($folder_name, $folder_type); } } // check if configuration folder exist if ($this->default && $this->default->name) { $this->enabled = true; } } /** * Check wether any configuration storage (folder) exists * * @return bool */ public function is_enabled() { return $this->enabled; } /** * Get configuration objects * * @param array $filter Search filter * @param bool $default Enable to get objects only from default folder * @param int $limit Max. number of records (per-folder) * * @return array List of objects */ public function get_objects($filter = array(), $default = false, $limit = 0) { $list = array(); foreach ($this->folders as $folder) { // we only want to read from default folder if ($default && !$folder->default) { continue; } // for better performance it's good to assume max. number of records if ($limit) { $folder->set_order_and_limit(null, $limit); } foreach ($folder->select($filter) as $object) { unset($object['_formatobj']); $list[] = $object; } } return $list; } /** * Get configuration object * * @param string $uid Object UID * @param bool $default Enable to get objects only from default folder * * @return array Object data */ public function get_object($uid, $default = false) { foreach ($this->folders as $folder) { // we only want to read from default folder if ($default && !$folder->default) { continue; } if ($object = $folder->get_object($uid)) { return $object; } } } /** * Create/update configuration object * * @param array $object Object data * @param string $type Object type * * @return bool True on success, False on failure */ public function save(&$object, $type) { if (!$this->enabled) { return false; } $folder = $this->find_folder($object); if ($type) { $object['type'] = $type; } - return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']); + $status = $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']); + + // on success, update cached tags list + if ($status && is_array($this->tags)) { + $found = false; + unset($object['_formatobj']); // we don't need it anymore + + foreach ($this->tags as $idx => $tag) { + if ($tag['uid'] == $object['uid']) { + $found = true; + $this->tags[$idx] = $object; + } + } + + if (!$found) { + $this->tags[] = $object; + } + } + + return !empty($status); } /** * Remove configuration object * * @param string $uid Object UID * * @return bool True on success, False on failure */ public function delete($uid) { if (!$this->enabled) { return false; } // fetch the object to find folder $object = $this->get_object($uid); if (!$object) { return false; } $folder = $this->find_folder($object); + $status = $folder->delete($uid); + + // on success, update cached tags list + if ($status && is_array($this->tags)) { + foreach ($this->tags as $idx => $tag) { + if ($tag['uid'] == $uid) { + unset($this->tags[$idx]); + break; + } + } + } - return $folder->delete($uid); + return $status; } /** * Find folder */ public function find_folder($object = array()) { // find folder object if ($object['_mailbox']) { foreach ($this->folders as $folder) { if ($folder->name == $object['_mailbox']) { break; } } } else { $folder = $this->default; } return $folder; } /** * Builds relation member URI * * @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date) * * @return string $url Member URI */ public static function build_member_url($params) { // param is object UUID if (is_string($params) && !empty($params)) { return 'urn:uuid:' . $params; } if (empty($params) || !strlen($params['folder'])) { return null; } $rcube = rcube::get_instance(); $storage = $rcube->get_storage(); list($username, $domain) = explode('@', $rcube->get_user_name()); if (strlen($domain)) { $domain = '@' . $domain; } // modify folder spec. according to namespace $folder = $params['folder']; $ns = $storage->folder_namespace($folder); if ($ns == 'shared') { // Note: this assumes there's only one shared namespace root if ($ns = $storage->get_namespace('shared')) { if ($prefix = $ns[0][0]) { $folder = substr($folder, strlen($prefix)); } } } else { if ($ns == 'other') { // Note: this assumes there's only one other users namespace root if ($ns = $storage->get_namespace('other')) { if ($prefix = $ns[0][0]) { list($otheruser, $path) = explode('/', substr($folder, strlen($prefix)), 2); $folder = 'user/' . $otheruser . $domain . '/' . $path; } } } else { $folder = 'user/' . $username . $domain . '/' . $folder; } } $folder = implode('/', array_map('rawurlencode', explode('/', $folder))); // build URI $url = 'imap:///' . $folder; // UID is optional here because sometimes we want // to build just a member uri prefix if ($params['uid']) { $url .= '/' . $params['uid']; } unset($params['folder']); unset($params['uid']); if (!empty($params)) { $url .= '?' . http_build_query($params, '', '&'); } return $url; } /** * Parses relation member string * * @param string $url Member URI * * @return array Message folder, UID, Search headers (Message-Id, Date) */ public static function parse_member_url($url) { // Look for IMAP URI: // imap:///(user/username@domain|shared)//? if (strpos($url, 'imap:///') === 0) { $rcube = rcube::get_instance(); $storage = $rcube->get_storage(); // parse_url does not work with imap:/// prefix $url = parse_url(substr($url, 8)); $path = explode('/', $url['path']); parse_str($url['query'], $params); $uid = array_pop($path); $ns = array_shift($path); $path = array_map('rawurldecode', $path); // resolve folder name if ($ns == 'user') { $username = array_shift($path); $folder = implode('/', $path); if ($username != $rcube->get_user_name()) { list($user, $domain) = explode('@', $username); // Note: this assumes there's only one other users namespace root if ($ns = $storage->get_namespace('other')) { if ($prefix = $ns[0][0]) { $folder = $prefix . $user . '/' . $folder; } } } else if (!strlen($folder)) { $folder = 'INBOX'; } } else { $folder = $ns . '/' . implode('/', $path); // Note: this assumes there's only one shared namespace root if ($ns = $storage->get_namespace('shared')) { if ($prefix = $ns[0][0]) { $folder = $prefix . $folder; } } } return array( 'folder' => $folder, 'uid' => $uid, 'params' => $params, ); } return false; } /** * Build array of member URIs from set of messages * * @param string $folder Folder name * @param array $messages Array of rcube_message objects * * @return array List of members (IMAP URIs) */ public static function build_members($folder, $messages) { $members = array(); foreach ((array) $messages as $msg) { $params = array( 'folder' => $folder, 'uid' => $msg->uid, ); // add search parameters: // we don't want to build "invalid" searches e.g. that // will return false positives (more or wrong messages) if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) { $params['message-id'] = $messageid; $params['date'] = $date; if ($subject = $msg->get('subject', false)) { $params['subject'] = substr($subject, 0, 256); } } $members[] = self::build_member_url($params); } return $members; } /** * Resolve/validate/update members (which are IMAP URIs) of relation object. * * @param array $tag Tag object * @param bool $force Force members list update * * @return array Folder/UIDs list */ public static function resolve_members(&$tag, $force = true) { $result = array(); foreach ((array) $tag['members'] as $member) { // IMAP URI members if ($url = self::parse_member_url($member)) { $folder = $url['folder']; if (!$force) { $result[$folder][] = $url['uid']; } else { $result[$folder]['uid'][] = $url['uid']; $result[$folder]['params'][] = $url['params']; $result[$folder]['member'][] = $member; } } } if (empty($result) || !$force) { return $result; } $rcube = rcube::get_instance(); $storage = $rcube->get_storage(); $search = array(); $missing = array(); // first we search messages by Folder+UID foreach ($result as $folder => $data) { // @FIXME: maybe better use index() which is cached? // @TODO: consider skip_deleted option $index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid'])); $uids = $index->get(); // messages that were not found need to be searched by search parameters $not_found = array_diff($data['uid'], $uids); if (!empty($not_found)) { foreach ($not_found as $uid) { $idx = array_search($uid, $data['uid']); if ($p = $data['params'][$idx]) { $search[] = $p; } $missing[] = $result[$folder]['member'][$idx]; unset($result[$folder]['uid'][$idx]); unset($result[$folder]['params'][$idx]); unset($result[$folder]['member'][$idx]); } } $result[$folder] = $uids; } // search in all subscribed mail folders using search parameters if (!empty($search)) { // remove not found members from the members list $tag['members'] = array_diff($tag['members'], $missing); // get subscribed folders $folders = $storage->list_folders_subscribed('', '*', 'mail', null, true); // @TODO: do this search in chunks (for e.g. 10 messages)? $search_str = ''; foreach ($search as $p) { $search_params = array(); foreach ($p as $key => $val) { $key = strtoupper($key); // don't search by subject, we don't want false-positives if ($key != 'SUBJECT') { $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val); } } $search_str .= ' (' . implode(' ', $search_params) . ')'; } $search_str = trim(str_repeat(' OR', count($search)-1) . $search_str); // search $search = $storage->search_once($folders, $search_str); // handle search result $folders = (array) $search->get_parameters('MAILBOX'); foreach ($folders as $folder) { $set = $search->get_set($folder); $uids = $set->get(); if (!empty($uids)) { $msgs = $storage->fetch_headers($folder, $uids, false); $members = self::build_members($folder, $msgs); // merge new members into the tag members list $tag['members'] = array_merge($tag['members'], $members); // add UIDs into the result $result[$folder] = array_unique(array_merge((array)$result[$folder], $uids)); } } // update tag object with new members list $tag['members'] = array_unique($tag['members']); kolab_storage_config::get_instance()->save($tag, 'relation', false); } return $result; } /** * Assign tags to kolab objects * * @param array $records List of kolab objects * * @return array List of tags */ public function apply_tags(&$records) { // first convert categories into tags foreach ($records as $i => $rec) { if (!empty($rec['categories'])) { $folder = new kolab_storage_folder($rec['_mailbox']); if ($object = $folder->get_object($rec['uid'])) { $tags = $rec['categories']; unset($object['categories']); unset($records[$i]['categories']); $this->save_tags($rec['uid'], $tags); $folder->save($object, $rec['_type'], $rec['uid']); } } } $tags = array(); // assign tags to objects foreach ($this->get_tags() as $tag) { foreach ($records as $idx => $rec) { $uid = self::build_member_url($rec['uid']); if (in_array($uid, (array) $tag['members'])) { $records[$idx]['tags'][] = $tag['name']; } } $tags[] = $tag['name']; } $tags = array_unique($tags); return $tags; } /** * Update object tags * * @param string $uid Kolab object UID * @param array $tags List of tag names */ public function save_tags($uid, $tags) { $url = self::build_member_url($uid); $relations = $this->get_tags(); foreach ($relations as $idx => $relation) { $selected = !empty($tags) && in_array($relation['name'], $tags); $found = !empty($relation['members']) && in_array($url, $relation['members']); $update = false; // remove member from the relation if ($found && !$selected) { $relation['members'] = array_diff($relation['members'], (array) $url); $update = true; } // add member to the relation else if (!$found && $selected) { $relation['members'][] = $url; $update = true; } if ($update) { if ($this->save($relation, 'relation')) { $this->tags[$idx] = $relation; // update in-memory cache } } if ($selected) { $tags = array_diff($tags, (array)$relation['name']); } } // create new relations if (!empty($tags)) { foreach ($tags as $tag) { $relation = array( 'name' => $tag, 'members' => (array) $url, 'category' => 'tag', ); if ($this->save($relation, 'relation')) { $this->tags[] = $relation; // update in-memory cache } } } } /** * Get tags (all or referring to specified object) * * @param string $uid Optional object UID * * @return array List of Relation objects */ public function get_tags($uid = '*') { if (!isset($this->tags)) { $default = true; $filter = array( array('type', '=', 'relation'), array('category', '=', 'tag') ); // use faster method if ($uid && $uid != '*') { $filter[] = array('member', '=', $uid); $tags = $this->get_objects($filter, $default); } else { $this->tags = $tags = $this->get_objects($filter, $default); } } else { $tags = $this->tags; } if ($uid === '*') { return $tags; } $result = array(); $search = self::build_member_url($uid); foreach ($tags as $tag) { if (in_array($search, (array) $tag['members'])) { $result[] = $tag; } } return $result; } /** * Find objects linked with the given groupware object through a relation * * @param string Object UUID * @param array List of related URIs */ public function get_object_links($uid) { $links = array(); $object_uri = self::build_member_url($uid); foreach ($this->get_relations_for_member($uid) as $relation) { if (in_array($object_uri, (array) $relation['members'])) { // make relation members up-to-date kolab_storage_config::resolve_members($relation); foreach ($relation['members'] as $member) { if ($member != $object_uri) { $links[] = $member; } } } } return array_unique($links); } /** * */ public function save_object_links($uid, $links, $remove = array()) { $object_uri = self::build_member_url($uid); $relations = $this->get_relations_for_member($uid); $done = false; foreach ($relations as $relation) { // make relation members up-to-date kolab_storage_config::resolve_members($relation); // remove and add links $members = array_diff($relation['members'], (array)$remove); $members = array_unique(array_merge($members, $links)); // make sure the object_uri is still a member if (!in_array($object_uri, $members)) { $members[$object_uri]; } // remove relation if no other members remain if (count($members) <= 1) { $done = $this->delete($relation['uid']); } // update relation object if members changed else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) { $relation['members'] = $members; $done = $this->save($relation, 'relation'); $links = array(); } // no changes, we're happy else { $done = true; $links = array(); } } // create a new relation if (!$done && !empty($links)) { $relation = array( 'members' => array_merge($links, array($object_uri)), 'category' => 'generic', ); $ret = $this->save($relation, 'relation'); } return $ret; } /** * Find relation objects referring to specified note */ public function get_relations_for_member($uid, $reltype = 'generic') { $default = true; $filter = array( array('type', '=', 'relation'), array('category', '=', $reltype), array('member', '=', $uid), ); return $this->get_objects($filter, $default, 100); } /** * Find kolab objects assigned to specified e-mail message * * @param rcube_message $message E-mail message * @param string $folder Folder name * @param string $type Result objects type * * @return array List of kolab objects */ public function get_message_relations($message, $folder, $type) { static $_cache = array(); $result = array(); $uids = array(); $default = true; $uri = self::get_message_uri($message, $folder); $filter = array( array('type', '=', 'relation'), array('category', '=', 'generic'), ); // query by message-id $member_id = $message->get('message-id', false); if (empty($member_id)) { // derive message identifier from URI $member_id = md5($uri); } $filter[] = array('member', '=', $member_id); if (!isset($_cache[$uri])) { // get UIDs of related groupware objects foreach ($this->get_objects($filter, $default) as $relation) { // we don't need to update members if the URI is found if (!in_array($uri, $relation['members'])) { // update members... $messages = kolab_storage_config::resolve_members($relation); // ...and check again if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) { continue; } } // find groupware object UID(s) foreach ($relation['members'] as $member) { if (strpos($member, 'urn:uuid:') === 0) { $uids[] = substr($member, 9); } } } // remember this lookup $_cache[$uri] = $uids; } else { $uids = $_cache[$uri]; } // get kolab objects of specified type if (!empty($uids)) { $query = array(array('uid', '=', array_unique($uids))); $result = kolab_storage::select($query, $type); } return $result; } /** * Build a URI representing the given message reference */ public static function get_message_uri($headers, $folder) { $params = array( 'folder' => $headers->folder ?: $folder, 'uid' => $headers->uid, ); if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) { $params['message-id'] = $messageid; $params['date'] = $date; if ($subject = $headers->get('subject')) { $params['subject'] = $subject; } } return self::build_member_url($params); } /** * Resolve the email message reference from the given URI */ public function get_message_reference($uri, $rel = null) { if ($linkref = self::parse_member_url($uri)) { $linkref['subject'] = $linkref['params']['subject']; $linkref['uri'] = $uri; $rcmail = rcube::get_instance(); if (method_exists($rcmail, 'url')) { $linkref['mailurl'] = $rcmail->url(array( 'task' => 'mail', 'action' => 'show', 'mbox' => $linkref['folder'], 'uid' => $linkref['uid'], 'rel' => $rel, )); } unset($linkref['params']); } return $linkref; } } diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php index cab3d199..56925447 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -1,1167 +1,1170 @@ * @author Aleksander Machniak * * Copyright (C) 2012-2013, Kolab Systems AG * * 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 . */ class kolab_storage_folder extends kolab_storage_folder_api { /** * The kolab_storage_cache instance for caching operations * @var object */ public $cache; /** * Indicate validity status * @var boolean */ public $valid = false; protected $error = 0; protected $resource_uri; /** * Default constructor * * @param string The folder name/path * @param string Expected folder type */ function __construct($name, $type = null, $type_annotation = null) { parent::__construct($name); $this->imap->set_options(array('skip_deleted' => true)); $this->set_folder($name, $type, $type_annotation); } /** * Set the IMAP folder this instance connects to * * @param string The folder name/path * @param string Expected folder type * @param string Optional folder type if known */ public function set_folder($name, $type = null, $type_annotation = null) { if (empty($type_annotation)) { $type_annotation = kolab_storage::folder_type($name); } $oldtype = $this->type; list($this->type, $suffix) = explode('.', $type_annotation); $this->default = $suffix == 'default'; $this->subtype = $this->default ? '' : $suffix; $this->name = $name; $this->id = kolab_storage::folder_id($name); $this->valid = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type); if (!$this->valid) { $this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER; } // reset cached object properties $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null; // get a new cache instance if folder type changed if (!$this->cache || $this->type != $oldtype) $this->cache = kolab_storage_cache::factory($this); else $this->cache->set_folder($this); $this->imap->set_folder($this->name); } /** * Returns code of last error * * @return int Error code */ public function get_error() { return $this->error ?: $this->cache->get_error(); } /** * Check IMAP connection error state */ public function check_error() { if (($err_code = $this->imap->get_error_code()) < 0) { $this->error = kolab_storage::ERROR_IMAP_CONN; if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) { $this->error = kolab_storage::ERROR_NO_PERMISSION; } } return $this->error; } /** * Compose a unique resource URI for this IMAP folder */ public function get_resource_uri() { - if (!empty($this->resource_uri)) + if (!empty($this->resource_uri)) { return $this->resource_uri; + } // strip namespace prefix from folder name - $ns = $this->get_namespace(); + $ns = $this->get_namespace(); $nsdata = $this->imap->get_namespace($ns); + if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) { $subpath = substr($this->name, strlen($nsdata[0][0])); if ($ns == 'other') { list($user, $suffix) = explode($nsdata[0][1], $subpath, 2); $subpath = $suffix; } } else { $subpath = $this->name; } // compose fully qualified ressource uri for this instance $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath; return $this->resource_uri; } /** * Helper method to extract folder UID metadata * * @return string Folder's UID */ public function get_uid() { // UID is defined in folder METADATA $metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_CYRUS); $metadata = $this->get_metadata($metakeys); foreach ($metakeys as $key) { if (($uid = $metadata[$key])) { return $uid; } } // generate a folder UID and set it to IMAP $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-'); if ($this->set_uid($uid)) { return $uid; } // create hash from folder name if we can't write the UID metadata return md5($this->name . $this->get_owner()); } /** * Helper method to set an UID value to the given IMAP folder instance * * @param string Folder's UID * @return boolean True on succes, False on failure */ public function set_uid($uid) { if (!($success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)))) { $success = $this->set_metadata(array(kolab_storage::UID_KEY_PRIVATE => $uid)); } $this->check_error(); return $success; } /** * Compose a folder Etag identifier */ public function get_ctag() { $fdata = $this->get_imap_data(); $this->check_error(); return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']); } /** * Check activation status of this folder * * @return boolean True if enabled, false if not */ public function is_active() { return kolab_storage::folder_is_active($this->name); } /** * Change activation status of this folder * * @param boolean The desired subscription status: true = active, false = not active * * @return True on success, false on error */ public function activate($active) { return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name); } /** * Check subscription status of this folder * * @return boolean True if subscribed, false if not */ public function is_subscribed() { return kolab_storage::folder_is_subscribed($this->name); } /** * Change subscription status of this folder * * @param boolean The desired subscription status: true = subscribed, false = not subscribed * * @return True on success, false on error */ public function subscribe($subscribed) { return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name); } /** * Get number of objects stored in this folder * * @param mixed Pseudo-SQL query as list of filter parameter triplets * or string with object type (e.g. contact, event, todo, journal, note, configuration) * @return integer The number of objects of the given type * @see self::select() */ public function count($query = null) { if (!$this->valid) { return 0; } // synchronize cache first $this->cache->synchronize(); return $this->cache->count($this->_prepare_query($query)); } /** * List all Kolab objects of the given type * * @param string $type Object type (e.g. contact, event, todo, journal, note, configuration) * @return array List of Kolab data objects (each represented as hash array) */ public function get_objects($type = null) { if (!$type) $type = $this->type; if (!$this->valid) { return array(); } // synchronize caches $this->cache->synchronize(); // fetch objects from cache return $this->cache->select($this->_prepare_query($type)); } /** * Select *some* Kolab objects matching the given query * * @param array Pseudo-SQL query as list of filter parameter triplets * triplet: array('', '', '') * @return array List of Kolab data objects (each represented as hash array) */ public function select($query = array()) { if (!$this->valid) { return array(); } // check query argument if (empty($query)) { return $this->get_objects(); } // synchronize caches $this->cache->synchronize(); // fetch objects from cache return $this->cache->select($this->_prepare_query($query)); } /** * Getter for object UIDs only * * @param array Pseudo-SQL query as list of filter parameter triplets * @return array List of Kolab object UIDs */ public function get_uids($query = array()) { if (!$this->valid) { return array(); } // synchronize caches $this->cache->synchronize(); // fetch UIDs from cache return $this->cache->select($this->_prepare_query($query), true); } /** * Setter for ORDER BY and LIMIT parameters for cache queries * * @param array List of columns to order by * @param integer Limit result set to this length * @param integer Offset row */ public function set_order_and_limit($sortcols, $length = null, $offset = 0) { $this->cache->set_order_by($sortcols); if ($length !== null) { $this->cache->set_limit($length, $offset); } } /** * Helper method to sanitize query arguments */ private function _prepare_query($query) { // string equals type query // FIXME: should not be called this way! if (is_string($query)) { return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array(); } foreach ((array)$query as $i => $param) { if ($param[0] == 'type' && !$this->cache->has_type_col()) { unset($query[$i]); } else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) { if (is_object($param[2]) && is_a($param[2], 'DateTime')) $param[2] = $param[2]->format('U'); if (is_numeric($param[2])) $query[$i][2] = date('Y-m-d H:i:s', $param[2]); } } return $query; } /** * Getter for a single Kolab object, identified by its UID * * @param string $uid Object UID * @param string $type Object type (e.g. contact, event, todo, journal, note, configuration) * Defaults to folder type * * @return array The Kolab object represented as hash array */ public function get_object($uid, $type = null) { if (!$this->valid) { return false; } // synchronize caches $this->cache->synchronize(); $msguid = $this->cache->uid2msguid($uid); if ($msguid && ($object = $this->cache->get($msguid, $type))) { return $object; } return false; } /** * Fetch a Kolab object attachment which is stored in a separate part * of the mail MIME message that represents the Kolab record. * * @param string Object's UID * @param string The attachment's mime number * @param string IMAP folder where message is stored; * If set, that also implies that the given UID is an IMAP UID * @param bool True to print the part content * @param resource File pointer to save the message part * @param boolean Disables charset conversion * * @return mixed The attachment content as binary string */ public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false) { if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) { $this->imap->set_folder($mailbox ? $mailbox : $this->name); if (substr($part, 0, 2) == 'i:') { // attachment data is stored in XML if ($object = $this->cache->get($msguid)) { // load data from XML (attachment content is not stored in cache) if ($object['_formatobj'] && isset($object['_size'])) { $object['_attachments'] = array(); $object['_formatobj']->get_attachments($object); } foreach ($object['_attachments'] as $attach) { if ($attach['id'] == $part) { if ($print) echo $attach['content']; else if ($fp) fwrite($fp, $attach['content']); else return $attach['content']; return true; } } } } else { // return message part from IMAP directly return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv); } } return null; } /** * Fetch the mime message from the storage server and extract * the Kolab groupware object from it * * @param string The IMAP message UID to fetch * @param string The object type expected (use wildcard '*' to accept all types) * @param string The folder name where the message is stored * * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found */ public function read_object($msguid, $type = null, $folder = null) { if (!$this->valid) { return false; } if (!$type) $type = $this->type; if (!$folder) $folder = $this->name; $this->imap->set_folder($folder); $this->cache->bypass(true); $message = new rcube_message($msguid); $this->cache->bypass(false); // Message doesn't exist? if (empty($message->headers)) { return false; } // extract the X-Kolab-Type header from the XML attachment part if missing if (empty($message->headers->others['x-kolab-type'])) { foreach ((array)$message->attachments as $part) { if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) { $message->headers->others['x-kolab-type'] = $part->mimetype; break; } } } // fix buggy messages stating the X-Kolab-Type header twice else if (is_array($message->headers->others['x-kolab-type'])) { $message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']); } // no object type header found: abort if (empty($message->headers->others['x-kolab-type'])) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "No X-Kolab-Type information found in message $msguid ($this->name).", ), true); return false; } $object_type = kolab_format::mime2object_type($message->headers->others['x-kolab-type']); $content_type = kolab_format::KTYPE_PREFIX . $object_type; // check object type header and abort on mismatch if ($type != '*' && $object_type != $type) return false; $attachments = array(); // get XML part foreach ((array)$message->attachments as $part) { if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) { $xml = $message->get_part_body($part->mime_id, true); } else if ($part->filename || $part->content_id) { $key = $part->content_id ? trim($part->content_id, '<>') : $part->filename; $size = null; // Use Content-Disposition 'size' as for the Kolab Format spec. if (isset($part->d_parameters['size'])) { $size = $part->d_parameters['size']; } // we can trust part size only if it's not encoded else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') { $size = $part->size; } $attachments[$key] = array( 'id' => $part->mime_id, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'size' => $size, ); } } if (!$xml) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not find Kolab data part in message $msguid ($this->name).", ), true); return false; } // check kolab format version $format_version = $message->headers->others['x-kolab-mime-version']; if (empty($format_version)) { list($xmltype, $subtype) = explode('.', $object_type); $xmlhead = substr($xml, 0, 512); // detect old Kolab 2.0 format if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false) $format_version = '2.0'; else $format_version = '3.0'; // assume 3.0 } // get Kolab format handler for the given type $format = kolab_format::factory($object_type, $format_version); if (is_a($format, 'PEAR_Error')) return false; // load Kolab object from XML part $format->load($xml); if ($format->is_valid()) { $object = $format->to_array(array('_attachments' => $attachments)); $object['_type'] = $object_type; $object['_msguid'] = $msguid; $object['_mailbox'] = $this->name; $object['_formatobj'] = $format; return $object; } else { // try to extract object UID from XML block if (preg_match('!(.+)!Uims', $xml, $m)) $msgadd = " UID = " . trim(strip_tags($m[1])); rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd, ), true); } return false; } /** * Save an object in this folder. * - * @param array $object The array that holds the data of the object. - * @param string $type The type of the kolab object. - * @param string $uid The UID of the old object if it existed before - * @return boolean True on success, false on error + * @param array $object The array that holds the data of the object. + * @param string $type The type of the kolab object. + * @param string $uid The UID of the old object if it existed before + * + * @return mixed False on error or IMAP message UID on success */ public function save(&$object, $type = null, $uid = null) { if (!$this->valid) { return false; } if (!$type) $type = $this->type; // copy attachments from old message $copyfrom = $object['_copyfrom'] ?: $object['_msguid']; if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) { foreach ((array)$old['_attachments'] as $key => $att) { if (!isset($object['_attachments'][$key])) { $object['_attachments'][$key] = $old['_attachments'][$key]; } // unset deleted attachment entries if ($object['_attachments'][$key] == false) { unset($object['_attachments'][$key]); } // load photo.attachment from old Kolab2 format to be directly embedded in xcard block else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) { if (!isset($object['photo'])) $object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']); unset($object['_attachments'][$key]); } } } // save contact photo to attachment for Kolab2 format if (kolab_storage::$version == '2.0' && $object['photo']) { $attkey = 'kolab-picture.png'; // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp $object['_attachments'][$attkey] = array( 'mimetype'=> rcube_mime::image_content_type($object['photo']), 'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']), ); } // process attachments if (is_array($object['_attachments'])) { $numatt = count($object['_attachments']); foreach ($object['_attachments'] as $key => $attachment) { // FIXME: kolab_storage and Roundcube attachment hooks use different fields! if (empty($attachment['content']) && !empty($attachment['data'])) { $attachment['content'] = $attachment['data']; unset($attachment['data'], $object['_attachments'][$key]['data']); } // make sure size is set, so object saved in cache contains this info if (!isset($attachment['size'])) { if (!empty($attachment['content'])) { if (is_resource($attachment['content'])) { // this need to be a seekable resource, otherwise // fstat() failes and we're unable to determine size // here nor in rcube_imap_generic before IMAP APPEND $stat = fstat($attachment['content']); $attachment['size'] = $stat ? $stat['size'] : 0; } else { $attachment['size'] = strlen($attachment['content']); } } else if (!empty($attachment['path'])) { $attachment['size'] = filesize($attachment['path']); } $object['_attachments'][$key] = $attachment; } // generate unique keys (used as content-id) for attachments if (is_numeric($key) && $key < $numatt) { // derrive content-id from attachment file name $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null; $basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii if (!$basename) $basename = 'noname'; $cid = $basename . '.' . microtime(true) . $key . $ext; $object['_attachments'][$cid] = $attachment; unset($object['_attachments'][$key]); } } } // save recurrence exceptions as individual objects due to lack of support in Kolab v2 format if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) { $this->save_recurrence_exceptions($object, $type); } // check IMAP BINARY extension support for 'file' objects // allow configuration to workaround bug in Cyrus < 2.4.17 $rcmail = rcube::get_instance(); $binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY'); // generate and save object message if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) { // resolve old msguid before saving if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) { $object['_msguid'] = $msguid; $object['_mailbox'] = $this->name; } $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary); // update cache with new UID if ($result) { $old_uid = $object['_msguid']; $object['_msguid'] = $result; $object['_mailbox'] = $this->name; if ($old_uid) { // delete old message $this->cache->bypass(true); $this->imap->delete_message($old_uid, $object['_mailbox']); $this->cache->bypass(false); } // insert/update message in cache $this->cache->save($result, $object, $old_uid); } // remove temp file if ($body_file) { @unlink($body_file); } } return $result; } /** * Save recurrence exceptions as individual objects. * The Kolab v2 format doesn't allow us to save fully embedded exception objects. * * @param array Hash array with event properties * @param string Object type */ private function save_recurrence_exceptions(&$object, $type = null) { if ($object['recurrence']['EXCEPTIONS']) { $exdates = array(); foreach ((array)$object['recurrence']['EXDATE'] as $exdate) { $key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate); $exdates[$key] = 1; } // save every exception as individual object foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) { $exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd')); $exception['sequence'] = $object['sequence'] + 1; if ($exception['thisandfuture']) { $exception['recurrence'] = $object['recurrence']; // adjust the recurrence duration of the exception if ($object['recurrence']['COUNT']) { $recurrence = new kolab_date_recurrence($object['_formatobj']); if ($end = $recurrence->end()) { unset($exception['recurrence']['COUNT']); $exception['recurrence']['UNTIL'] = $end; } } // set UNTIL date if we have a thisandfuture exception $untildate = clone $exception['start']; $untildate->sub(new DateInterval('P1D')); $object['recurrence']['UNTIL'] = $untildate; unset($object['recurrence']['COUNT']); } else { if (!$exdates[$exception['start']->format('Y-m-d')]) $object['recurrence']['EXDATE'][] = clone $exception['start']; unset($exception['recurrence']); } unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']); $this->save($exception, $type, $exception['uid']); } unset($object['recurrence']['EXCEPTIONS']); } } /** * Generate an object UID with the given recurrence-ID in a way that it is * unique (the original UID is not a substring) but still recoverable. */ private static function recurrence_exception_uid($uid, $recurrence_id) { $offset = -2; return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset); } /** * Delete the specified object from this folder. * * @param mixed $object The Kolab object to delete or object UID * @param boolean $expunge Should the folder be expunged? * * @return boolean True if successful, false on error */ public function delete($object, $expunge = true) { if (!$this->valid) { return false; } $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object); $success = false; $this->cache->bypass(true); if ($msguid && $expunge) { $success = $this->imap->delete_message($msguid, $this->name); } else if ($msguid) { $success = $this->imap->set_flag($msguid, 'DELETED', $this->name); } $this->cache->bypass(false); if ($success) { $this->cache->set($msguid, false); } return $success; } /** * */ public function delete_all() { if (!$this->valid) { return false; } $this->cache->purge(); $this->cache->bypass(true); $result = $this->imap->clear_folder($this->name); $this->cache->bypass(false); return $result; } /** * Restore a previously deleted object * * @param string Object UID * @return mixed Message UID on success, false on error */ public function undelete($uid) { if (!$this->valid) { return false; } if ($msguid = $this->cache->uid2msguid($uid, true)) { $this->cache->bypass(true); $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name); $this->cache->bypass(false); if ($result) { return $msguid; } } return false; } /** * Move a Kolab object message to another IMAP folder * * @param string Object UID * @param string IMAP folder to move object to * @return boolean True on success, false on failure */ public function move($uid, $target_folder) { if (!$this->valid) { return false; } if (is_string($target_folder)) $target_folder = kolab_storage::get_folder($target_folder); if ($msguid = $this->cache->uid2msguid($uid)) { $this->cache->bypass(true); $result = $this->imap->move_message($msguid, $target_folder->name, $this->name); $this->cache->bypass(false); if ($result) { $this->cache->move($msguid, $uid, $target_folder); return true; } else { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(), ), true); } } return false; } /** * Creates source of the configuration object message * * @param array $object The array that holds the data of the object. * @param string $type The type of the kolab object. * @param bool $binary Enables use of binary encoding of attachment(s) * @param string $body_file Reference to filename of message body * * @return mixed Message as string or array with two elements * (one for message file path, second for message headers) */ private function build_message(&$object, $type, $binary, &$body_file) { // load old object to preserve data we don't understand/process if (is_object($object['_formatobj'])) $format = $object['_formatobj']; else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) $format = $old['_formatobj']; // create new kolab_format instance if (!$format) $format = kolab_format::factory($type, kolab_storage::$version); if (PEAR::isError($format)) return false; $format->set($object); $xml = $format->write(kolab_storage::$version); $object['uid'] = $format->uid; // read UID from format $object['_formatobj'] = $format; if (empty($xml) || !$format->is_valid() || empty($object['uid'])) { return false; } $mime = new Mail_mime("\r\n"); $rcmail = rcube::get_instance(); $headers = array(); $files = array(); $part_id = 1; $encoding = $binary ? 'binary' : 'base64'; if ($user_email = $rcmail->get_user_email()) { $headers['From'] = $user_email; $headers['To'] = $user_email; } $headers['Date'] = date('r'); $headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type; $headers['X-Kolab-Mime-Version'] = kolab_storage::$version; $headers['Subject'] = $object['uid']; // $headers['Message-ID'] = $rcmail->gen_message_id(); $headers['User-Agent'] = $rcmail->config->get('useragent'); // Check if we have enough memory to handle the message in it // It's faster than using files, so we'll do this if we only can if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) { $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB foreach ($object['_attachments'] as $attachment) { $memory += $attachment['size']; } // 1.33 is for base64, we need at least 4x more memory than the message size if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) { $marker = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%'; $is_file = true; $temp_dir = unslashify($rcmail->config->get('temp_dir')); $mime->setParam('delay_file_io', true); } } $mime->headers($headers); $mime->setTXTBody("This is a Kolab Groupware object. " . "To view this object you will need an email client that understands the Kolab Groupware format. " . "For a list of such email clients please visit http://www.kolab.org/\n\n"); $ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE; // Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines" // when APPENDing from temp file $xml = preg_replace('/\r?\n/', "\r\n", $xml); $mime->addAttachment($xml, // file $ctype, // content-type 'kolab.xml', // filename false, // is_file '8bit', // encoding 'attachment', // disposition RCUBE_CHARSET // charset ); $part_id++; // save object attachments as separate parts foreach ((array)$object['_attachments'] as $key => $att) { if (empty($att['content']) && !empty($att['id'])) { // @TODO: use IMAP CATENATE to skip attachment fetch+push operation $msguid = $object['_copyfrom'] ?: ($object['_msguid'] ?: $object['uid']); if ($is_file) { $att['path'] = tempnam($temp_dir, 'rcmAttmnt'); if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) { fclose($fp); } else { return false; } } else { $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true); } } $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable')); $name = !empty($att['name']) ? $att['name'] : $key; // To store binary files we can use faster method // without writting full message content to a temporary file but // directly to IMAP, see rcube_imap_generic::append(). // I.e. use file handles where possible if (!empty($att['path'])) { if ($is_file && $binary) { $files[] = fopen($att['path'], 'r'); $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } else { $mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } } else { if (is_resource($att['content']) && $is_file && $binary) { $files[] = $att['content']; $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } else { if (is_resource($att['content'])) { @rewind($att['content']); $att['content'] = stream_get_contents($att['content']); } $mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } } $object['_attachments'][$key]['id'] = ++$part_id; } if (!$is_file || !empty($files)) { $message = $mime->getMessage(); } // parse message and build message array with // attachment file pointers in place of file markers if (!empty($files)) { $message = explode($marker, $message); $tmp = array(); foreach ($message as $msg_part) { $tmp[] = $msg_part; if ($file = array_shift($files)) { $tmp[] = $file; } } $message = $tmp; } // write complete message body into temp file else if ($is_file) { // use common temp dir $body_file = tempnam($temp_dir, 'rcmMsg'); if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) { rcube::raise_error(array('code' => 650, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not create message: ".$mime_result->getMessage()), true, false); return false; } $message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r')); } return $message; } /** * Triggers any required updates after changes within the * folder. This is currently only required for handling free/busy * information with Kolab. * * @return boolean|PEAR_Error True if successfull. */ public function trigger() { $owner = $this->get_owner(); $result = false; switch($this->type) { case 'event': if ($this->get_namespace() == 'personal') { $result = $this->trigger_url( sprintf('%s/trigger/%s/%s.pfb', kolab_storage::get_freebusy_server(), urlencode($owner), urlencode($this->imap->mod_folder($this->name)) ), $this->imap->options['user'], $this->imap->options['password'] ); } break; default: return true; } if ($result && is_object($result) && is_a($result, 'PEAR_Error')) { return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s", $this->name, $result->getMessage())); } return $result; } /** * Triggers a URL. * * @param string $url The URL to be triggered. * @param string $auth_user Username to authenticate with * @param string $auth_passwd Password for basic auth * @return boolean|PEAR_Error True if successfull. */ private function trigger_url($url, $auth_user = null, $auth_passwd = null) { try { $request = libkolab::http_request($url); // set authentication credentials if ($auth_user && $auth_passwd) $request->setAuth($auth_user, $auth_passwd); $result = $request->send(); // rcube::write_log('trigger', $result->getBody()); } catch (Exception $e) { return PEAR::raiseError($e->getMessage()); } return true; } } diff --git a/plugins/libkolab/tests/kolab_storage_config.php b/plugins/libkolab/tests/kolab_storage_config.php index 65cbdfd5..c4ab3a6c 100644 --- a/plugins/libkolab/tests/kolab_storage_config.php +++ b/plugins/libkolab/tests/kolab_storage_config.php @@ -1,114 +1,210 @@ 'Archive', 'uid' => '9', 'message-id' => '<1225270@example.org>', 'date' => 'Mon, 20 Apr 2015 15:30:30 UTC', 'subject' => 'Archived', ); - private $url_personal = 'imap:///user/john.doe%40example.org/Archive/9?message-id=%3C1225270%40example.org%3E&date=Mon%2C+20+Apr+2015+15%3A30%3A30+UTC&subject=Archived'; + private $url_personal = 'imap:///user/$user/Archive/9?message-id=%3C1225270%40example.org%3E&date=Mon%2C+20+Apr+2015+15%3A30%3A30+UTC&subject=Archived'; private $params_shared = array( 'folder' => 'Shared Folders/shared/Collected', 'uid' => '4', 'message-id' => '<5270122@example.org>', 'date' => 'Mon, 20 Apr 2015 16:33:03 +0200', 'subject' => 'Shared', ); private $url_shared = 'imap:///shared/Collected/4?message-id=%3C5270122%40example.org%3E&date=Mon%2C+20+Apr+2015+16%3A33%3A03+%2B0200&subject=Shared'; private $params_other = array( 'folder' => 'Other Users/lucy.white/Mailings', 'uid' => '378', 'message-id' => '<22448899@example.org>', 'date' => 'Tue, 14 Apr 2015 14:14:30 +0200', 'subject' => 'Happy Holidays', ); private $url_other = 'imap:///user/lucy.white%40example.org/Mailings/378?message-id=%3C22448899%40example.org%3E&date=Tue%2C+14+Apr+2015+14%3A14%3A30+%2B0200&subject=Happy+Holidays'; - public static function setUpBeforeClass() { - require_once __DIR__ . '/../../libkolab/libkolab.php'; - - $rcube = rcube::get_instance(); - $rcube->user = null; - - $lib = new libkolab($rcube->plugins); - $lib->init(); - - // fake some session data to make storage work without an actual IMAP connection - $_SESSION['username'] = 'john.doe@example.org'; - $_SESSION['imap_delimiter'] = '/'; - $_SESSION['imap_namespace'] = array( - 'personal' => array(array('','/')), - 'other' => array(array('Other Users/','/')), - 'shared' => array(array('Shared Folders/','/')), - 'prefix' => '', - ); + $rcube = rcmail::get_instance(); + $rcube->plugins->load_plugin('libkolab', true, true); + + if ($rcube->config->get('tests_username')) { + $authenticated = $rcube->login( + $rcube->config->get('tests_username'), + $rcube->config->get('tests_password'), + $rcube->config->get('default_host'), + false + ); + + if (!$authenticated) { + throw new Exception('IMAP login failed for user ' . $rcube->config->get('tests_username')); + } + + // check for defult groupware folders and clear them + $imap = $rcube->get_storage(); + $folders = $imap->list_folders('', '*'); + + foreach (array('Configuration') as $folder) { + if (in_array($folder, $folders)) { + if (!$imap->clear_folder($folder)) { + throw new Exception("Failed to clear folder '$folder'"); + } + } + else { + throw new Exception("Default folder '$folder' doesn't exits in test user account"); + } + } + } + else { + throw new Exception('Missing test account username/password in config-test.inc.php'); + } + + kolab_storage::setup(); } function test_001_build_member_url() { - $rcube = rcube::get_instance(); - $this->assertEquals('john.doe@example.org', $rcube->get_user_name()); + $rcube = rcube::get_instance(); + $email = $rcube->get_user_email(); + $personal = str_replace('$user', urlencode($email), $this->url_personal); // personal namespace $url = kolab_storage_config::build_member_url($this->params_personal); - $this->assertEquals($this->url_personal, $url); + $this->assertEquals($personal, $url); // shared namespace $url = kolab_storage_config::build_member_url($this->params_shared); $this->assertEquals($this->url_shared, $url); // other users namespace $url = kolab_storage_config::build_member_url($this->params_other); $this->assertEquals($this->url_other, $url); } function test_002_parse_member_url() { + $rcube = rcube::get_instance(); + $email = $rcube->get_user_email(); + $personal = str_replace('$user', urlencode($email), $this->url_personal); + // personal namespace - $params = kolab_storage_config::parse_member_url($this->url_personal); + $params = kolab_storage_config::parse_member_url($personal); $this->assertEquals($this->params_personal['uid'], $params['uid']); $this->assertEquals($this->params_personal['folder'], $params['folder']); $this->assertEquals($this->params_personal['subject'], $params['params']['subject']); $this->assertEquals($this->params_personal['message-id'], $params['params']['message-id']); // shared namespace $params = kolab_storage_config::parse_member_url($this->url_shared); $this->assertEquals($this->params_shared['uid'], $params['uid']); $this->assertEquals($this->params_shared['folder'], $params['folder']); // other users namespace $params = kolab_storage_config::parse_member_url($this->url_other); $this->assertEquals($this->params_other['uid'], $params['uid']); $this->assertEquals($this->params_other['folder'], $params['folder']); } function test_003_build_parse_member_url() { // personal namespace $params = $this->params_personal; $params_ = kolab_storage_config::parse_member_url(kolab_storage_config::build_member_url($params)); $this->assertEquals($params['uid'], $params_['uid']); $this->assertEquals($params['folder'], $params_['folder']); // shared namespace $params = $this->params_shared; $params_ = kolab_storage_config::parse_member_url(kolab_storage_config::build_member_url($params)); $this->assertEquals($params['uid'], $params_['uid']); $this->assertEquals($params['folder'], $params_['folder']); // other users namespace $params = $this->params_other; $params_ = kolab_storage_config::parse_member_url(kolab_storage_config::build_member_url($params)); $this->assertEquals($params['uid'], $params_['uid']); $this->assertEquals($params['folder'], $params_['folder']); } -} - \ No newline at end of file + /** + * Test relation/tag objects creation + * These objects will be used by following tests + */ + function test_save() + { + $config = kolab_storage_config::get_instance(); + $tags = array( + array( + 'category' => 'tag', + 'name' => 'test1', + ), + array( + 'category' => 'tag', + 'name' => 'test2', + ), + array( + 'category' => 'tag', + 'name' => 'test3', + ), + array( + 'category' => 'tag', + 'name' => 'test4', + ), + ); + + foreach ($tags as $tag) { + $result = $config->save($tag, 'relation'); + + $this->assertTrue(!empty($result)); + $this->assertTrue(!empty($tag['uid'])); + } + } + + /** + * Tests "race condition" in tags handling (T133) + */ + function test_T133() + { + $config = kolab_storage_config::get_instance(); + + // get tags + $tags = $config->get_tags(); + $this->assertCount(4, $tags); + + // create a tag + $tag = array( + 'category' => 'tag', + 'name' => 'new', + ); + $result = $config->save($tag, 'relation'); + $this->assertTrue(!empty($result)); + + // get tags again, make sure it contains the new tag + $tags = $config->get_tags(); + $this->assertCount(5, $tags); + + // update a tag + $tag['name'] = 'new-tag'; + $result = $config->save($tag, 'relation'); + $this->assertTrue(!empty($result)); + + // get tags again, make sure it contains the new tag + $tags = $config->get_tags(); + $this->assertCount(5, $tags); + $this->assertSame('new-tag', $tags[4]['name']); + + // remove a tag + $result = $config->delete($tag['uid']); + $this->assertTrue(!empty($result)); + + // get tags again, make sure it contains the new tag + $tags = $config->get_tags(); + $this->assertCount(4, $tags); + } +}