diff -Nur roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/config.inc.php.dist roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/config.inc.php.dist --- roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/config.inc.php.dist 1970-01-01 01:00:00.000000000 +0100 +++ roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/config.inc.php.dist 2013-11-24 00:38:51.000000000 +0100 @@ -0,0 +1,4 @@ + + * + * Copyright (C) 2012, 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 tasklist_database_driver extends tasklist_driver +{ + public $undelete = true; // yes, we can + public $sortable = false; + public $alarm_types = array('DISPLAY','EMAIL'); + + private $rc; + private $plugin; + private $lists = array(); + private $list_ids = ''; + + private $db_tasks = 'tasks'; + private $db_lists = 'tasklists'; + private $sequence_tasks = 'task_ids'; + private $sequence_lists = 'tasklist_ids'; + + + /** + * Default constructor + */ + public function __construct($plugin) + { + $this->rc = $plugin->rc; + $this->plugin = $plugin; + + // read database config + $this->db_lists = $this->rc->config->get('db_table_lists', $this->db_lists); + $this->db_tasks = $this->rc->config->get('db_table_tasks', $this->db_tasks); + $this->sequence_lists = $this->rc->config->get('db_sequence_lists', $this->sequence_lists); + $this->sequence_tasks = $this->rc->config->get('db_sequence_tasks', $this->sequence_tasks); + + $this->_read_lists(); + } + + /** + * Read available calendars for the current user and store them internally + */ + private function _read_lists() + { + $hidden = array_filter(explode(',', $this->rc->config->get('hidden_tasklists', ''))); + + if (!empty($this->rc->user->ID)) { + $list_ids = array(); + $result = $this->rc->db->query( + "SELECT *, tasklist_id AS id FROM " . $this->db_lists . " + WHERE user_id=? + ORDER BY CASE WHEN name='INBOX' THEN 0 ELSE 1 END, name", + $this->rc->user->ID + ); + + while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { + $arr['showalarms'] = intval($arr['showalarms']); + $arr['active'] = !in_array($arr['id'], $hidden); + $arr['name'] = html::quote($arr['name']); + $arr['editable'] = true; + $this->lists[$arr['id']] = $arr; + $list_ids[] = $this->rc->db->quote($arr['id']); + } + $this->list_ids = join(',', $list_ids); + } + } + + /** + * Get a list of available tasks lists from this source + */ + public function get_lists() + { + // attempt to create a default list for this user + if (empty($this->lists)) { + if ($this->create_list(array('name' => 'Default', 'color' => '000000'))) + $this->_read_lists(); + } + + return $this->lists; + } + + /** + * Create a new list assigned to the current user + * + * @param array Hash array with list properties + * @return mixed ID of the new list on success, False on error + * @see tasklist_driver::create_list() + */ + public function create_list($prop) + { + $result = $this->rc->db->query( + "INSERT INTO " . $this->db_lists . " + (user_id, name, color, showalarms) + VALUES (?, ?, ?, ?)", + $this->rc->user->ID, + $prop['name'], + $prop['color'], + $prop['showalarms']?1:0 + ); + + if ($result) + return $this->rc->db->insert_id($this->sequence_lists); + + return false; + } + + /** + * Update properties of an existing tasklist + * + * @param array Hash array with list properties + * @return boolean True on success, Fales on failure + * @see tasklist_driver::edit_list() + */ + public function edit_list($prop) + { + $query = $this->rc->db->query( + "UPDATE " . $this->db_lists . " + SET name=?, color=?, showalarms=? + WHERE tasklist_id=? + AND user_id=?", + $prop['name'], + $prop['color'], + $prop['showalarms']?1:0, + $prop['id'], + $this->rc->user->ID + ); + + return $this->rc->db->affected_rows($query); + } + + /** + * Set active/subscribed state of a list + * + * @param array Hash array with list properties + * @return boolean True on success, Fales on failure + * @see tasklist_driver::subscribe_list() + */ + public function subscribe_list($prop) + { + $hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', ''))); + + if ($prop['active']) + unset($hidden[$prop['id']]); + else + $hidden[$prop['id']] = 1; + + return $this->rc->user->save_prefs(array('hidden_tasklists' => join(',', array_keys($hidden)))); + } + + /** + * Delete the given list with all its contents + * + * @param array Hash array with list properties + * @return boolean True on success, Fales on failure + * @see tasklist_driver::remove_list() + */ + public function remove_list($prop) + { + $list_id = $prop['id']; + + if ($this->lists[$list_id]) { + // delete all tasks linked with this list + $this->rc->db->query( + "DELETE FROM " . $this->db_tasks . " + WHERE tasklist_id=?", + $list_id + ); + + // delete list record + $query = $this->rc->db->query( + "DELETE FROM " . $this->db_lists . " + WHERE tasklist_id=? + AND user_id=?", + $list_id, + $this->rc->user->ID + ); + + return $this->rc->db->affected_rows($query); + } + + return false; + } + + /** + * Get number of tasks matching the given filter + * + * @param array List of lists to count tasks of + * @return array Hash array with counts grouped by status (all|flagged|today|tomorrow|overdue|nodate) + * @see tasklist_driver::count_tasks() + */ + function count_tasks($lists = null) + { + if (empty($lists)) + $lists = array_keys($this->lists); + else if (is_string($lists)) + $lists = explode(',', $lists); + + // only allow to select from lists of this user + $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists))); + + $today_date = new DateTime('now', $this->plugin->timezone); + $today = $today_date->format('Y-m-d'); + $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone); + $tomorrow = $tomorrow_date->format('Y-m-d'); + + $result = $this->rc->db->query(sprintf( + "SELECT task_id, flagged, date FROM " . $this->db_tasks . " + WHERE tasklist_id IN (%s) + AND del=0 AND complete<1", + join(',', $list_ids) + )); + + $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0); + while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { + $counts['all']++; + if ($rec['flagged']) + $counts['flagged']++; + if (empty($rec['date'])) + $counts['nodate']++; + else if ($rec['date'] == $today) + $counts['today']++; + else if ($rec['date'] == $tomorrow) + $counts['tomorrow']++; + else if ($rec['date'] < $today) + $counts['overdue']++; + } + + return $counts; + } + + /** + * Get all taks records matching the given filter + * + * @param array Hash array wiht filter criterias + * @param array List of lists to get tasks from + * @return array List of tasks records matchin the criteria + * @see tasklist_driver::list_tasks() + */ + function list_tasks($filter, $lists = null) + { + if (empty($lists)) + $lists = array_keys($this->lists); + else if (is_string($lists)) + $lists = explode(',', $lists); + + // only allow to select from lists of this user + $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists))); + $sql_add = ''; + + // add filter criteria + if ($filter['from'] || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) { + $sql_add .= ' AND (date IS NULL OR date >= ?)'; + $datefrom = $filter['from']; + } + if ($filter['to']) { + if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE) + $sql_add .= ' AND (date IS NOT NULL AND date <= ' . $this->rc->db->quote($filter['to']) . ')'; + else + $sql_add .= ' AND (date IS NULL OR date <= ' . $this->rc->db->quote($filter['to']) . ')'; + } + + // special case 'today': also show all events with date before today + if ($filter['mask'] & tasklist::FILTER_MASK_TODAY) { + $datefrom = date('Y-m-d', 0); + } + + if ($filter['mask'] & tasklist::FILTER_MASK_NODATE) + $sql_add = ' AND date IS NULL'; + + if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) + $sql_add .= ' AND complete=1'; + else // don't show complete tasks by default + $sql_add .= ' AND complete<1'; + + if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED) + $sql_add .= ' AND flagged=1'; + + // compose (slow) SQL query for searching + // FIXME: improve searching using a dedicated col and normalized values + if ($filter['search']) { + $sql_query = array(); + foreach (array('title','description','organizer','attendees') as $col) + $sql_query[] = $this->rc->db->ilike($col, '%'.$filter['search'].'%'); + $sql_add = 'AND (' . join(' OR ', $sql_query) . ')'; + } + + $tasks = array(); + if (!empty($list_ids)) { + $result = $this->rc->db->query(sprintf( + "SELECT * FROM " . $this->db_tasks . " + WHERE tasklist_id IN (%s) + AND del=0 + %s + ORDER BY parent_id, task_id ASC", + join(',', $list_ids), + $sql_add + ), + $datefrom + ); + + while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { + $tasks[] = $this->_read_postprocess($rec); + } + } + + return $tasks; + } + + /** + * Return data of a specific task + * + * @param mixed Hash array with task properties or task UID + * @return array Hash array with task properties or false if not found + */ + public function get_task($prop) + { + if (is_string($prop)) + $prop['uid'] = $prop; + + $query_col = $prop['id'] ? 'task_id' : 'uid'; + + $result = $this->rc->db->query(sprintf( + "SELECT * FROM " . $this->db_tasks . " + WHERE tasklist_id IN (%s) + AND %s=? + AND del=0", + $this->list_ids, + $query_col + ), + $prop['id'] ? $prop['id'] : $prop['uid'] + ); + + if ($result && ($rec = $this->rc->db->fetch_assoc($result))) { + return $this->_read_postprocess($rec); + } + + return false; + } + + /** + * Get all decendents of the given task record + * + * @param mixed Hash array with task properties or task UID + * @param boolean True if all childrens children should be fetched + * @return array List of all child task IDs + */ + public function get_childs($prop, $recursive = false) + { + // resolve UID first + if (is_string($prop)) { + $result = $this->rc->db->query(sprintf( + "SELECT task_id AS id, tasklist_id AS list FROM " . $this->db_tasks . " + WHERE tasklist_id IN (%s) + AND uid=?", + $this->list_ids + ), + $prop); + $prop = $this->rc->db->fetch_assoc($result); + } + + $childs = array(); + $task_ids = array($prop['id']); + + // query for childs (recursively) + while (!empty($task_ids)) { + $result = $this->rc->db->query(sprintf( + "SELECT task_id AS id FROM " . $this->db_tasks . " + WHERE tasklist_id IN (%s) + AND parent_id IN (%s) + AND del=0", + $this->list_ids, + join(',', array_map(array($this->rc->db, 'quote'), $task_ids)) + )); + + $task_ids = array(); + while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { + $childs[] = $rec['id']; + $task_ids[] = $rec['id']; + } + + if (!$recursive) + break; + } + + return $childs; + } + + /** + * Get a list of pending alarms to be displayed to the user + * + * @param integer Current time (unix timestamp) + * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) + * @return array A list of alarms, each encoded as hash array with task properties + * @see tasklist_driver::pending_alarms() + */ + public function pending_alarms($time, $lists = null) + { + if (empty($lists)) + $lists = array_keys($this->lists); + else if (is_string($lists)) + $lists = explode(',', $lists); + + // only allow to select from calendars with activated alarms + $list_ids = array(); + foreach ($lists as $lid) { + if ($this->lists[$lid] && $this->lists[$lid]['showalarms']) + $list_ids[] = $lid; + } + $list_ids = array_map(array($this->rc->db, 'quote'), $list_ids); + + $alarms = array(); + if (!empty($list_ids)) { + $result = $this->rc->db->query(sprintf( + "SELECT * FROM " . $this->db_tasks . " + WHERE tasklist_id IN (%s) + AND notify <= %s AND complete < 1", + join(',', $list_ids), + $this->rc->db->fromunixtime($time) + )); + + while ($result && ($rec = $this->rc->db->fetch_assoc($result))) + $alarms[] = $this->_read_postprocess($rec); + } + + return $alarms; + } + + /** + * Feedback after showing/sending an alarm notification + * + * @see tasklist_driver::dismiss_alarm() + */ + public function dismiss_alarm($task_id, $snooze = 0) + { + // set new notifyat time or unset if not snoozed + $notify_at = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; + + $query = $this->rc->db->query(sprintf( + "UPDATE " . $this->db_tasks . " + SET changed=%s, notify=? + WHERE task_id=? + AND tasklist_id IN (" . $this->list_ids . ")", + $this->rc->db->now()), + $notify_at, + $task_id + ); + + return $this->rc->db->affected_rows($query); + } + + /** + * Map some internal database values to match the generic "API" + */ + private function _read_postprocess($rec) + { + $rec['id'] = $rec['task_id']; + $rec['list'] = $rec['tasklist_id']; + $rec['changed'] = new DateTime($rec['changed']); + $rec['tags'] = array_filter(explode(',', $rec['tags'])); + + if (!$rec['parent_id']) + unset($rec['parent_id']); + + unset($rec['task_id'], $rec['tasklist_id'], $rec['created']); + return $rec; + } + + /** + * Add a single task to the database + * + * @param array Hash array with task properties (see header of this file) + * @return mixed New event ID on success, False on error + * @see tasklist_driver::create_task() + */ + public function create_task($prop) + { + // check list permissions + $list_id = $prop['list'] ? $prop['list'] : reset(array_keys($this->lists)); + if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly']) + return false; + + foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) { + if (empty($prop[$col])) + $prop[$col] = null; + } + + $notify_at = $this->_get_notification($prop); + $result = $this->rc->db->query(sprintf( + "INSERT INTO " . $this->db_tasks . " + (tasklist_id, uid, parent_id, created, changed, title, date, time, startdate, starttime, description, tags, alarms, notify) + VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + $this->rc->db->now(), + $this->rc->db->now() + ), + $list_id, + $prop['uid'], + $prop['parent_id'], + $prop['title'], + $prop['date'], + $prop['time'], + $prop['startdate'], + $prop['starttime'], + strval($prop['description']), + join(',', (array)$prop['tags']), + $prop['alarms'], + $notify_at + ); + + if ($result) + return $this->rc->db->insert_id($this->sequence_tasks); + + return false; + } + + /** + * Update an task entry with the given data + * + * @param array Hash array with task properties + * @return boolean True on success, False on error + * @see tasklist_driver::edit_task() + */ + public function edit_task($prop) + { + $sql_set = array(); + foreach (array('title', 'description', 'flagged', 'complete') as $col) { + if (isset($prop[$col])) + $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop[$col]); + } + foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) { + if (isset($prop[$col])) + $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col])); + } + if (isset($prop['tags'])) + $sql_set[] = $this->rc->db->quote_identifier('tags') . '=' . $this->rc->db->quote(join(',', (array)$prop['tags'])); + + if (isset($prop['date']) || isset($prop['time']) || isset($prop['alarms'])) { + $notify_at = $this->_get_notification($prop); + $sql_set[] = $this->rc->db->quote_identifier('notify') . '=' . (empty($notify_at) ? 'NULL' : $this->rc->db->quote($notify_at)); + } + + // moved from another list + if ($prop['_fromlist'] && ($newlist = $prop['list'])) { + $sql_set[] = 'tasklist_id=' . $this->rc->db->quote($newlist); + } + + $query = $this->rc->db->query(sprintf( + "UPDATE " . $this->db_tasks . " + SET changed=%s %s + WHERE task_id=? + AND tasklist_id IN (%s)", + $this->rc->db->now(), + ($sql_set ? ', ' . join(', ', $sql_set) : ''), + $this->list_ids + ), + $prop['id'] + ); + + return $this->rc->db->affected_rows($query); + } + + /** + * Move a single task to another list + * + * @param array Hash array with task properties: + * @return boolean True on success, False on error + * @see tasklist_driver::move_task() + */ + public function move_task($prop) + { + return $this->edit_task($prop); + } + + /** + * Remove a single task from the database + * + * @param array Hash array with task properties + * @param boolean Remove record irreversible + * @return boolean True on success, False on error + * @see tasklist_driver::delete_task() + */ + public function delete_task($prop, $force = true) + { + $task_id = $prop['id']; + + if ($task_id && $force) { + $query = $this->rc->db->query( + "DELETE FROM " . $this->db_tasks . " + WHERE task_id=? + AND tasklist_id IN (" . $this->list_ids . ")", + $task_id + ); + } + else if ($task_id) { + $query = $this->rc->db->query(sprintf( + "UPDATE " . $this->db_tasks . " + SET changed=%s, del=1 + WHERE task_id=? + AND tasklist_id IN (%s)", + $this->rc->db->now(), + $this->list_ids + ), + $task_id + ); + } + + return $this->rc->db->affected_rows($query); + } + + /** + * Restores a single deleted task (if supported) + * + * @param array Hash array with task properties + * @return boolean True on success, False on error + * @see tasklist_driver::undelete_task() + */ + public function undelete_task($prop) + { + $query = $this->rc->db->query(sprintf( + "UPDATE " . $this->db_tasks . " + SET changed=%s, del=0 + WHERE task_id=? + AND tasklist_id IN (%s)", + $this->rc->db->now(), + $this->list_ids + ), + $prop['id'] + ); + + return $this->rc->db->affected_rows($query); + } + + /** + * Compute absolute time to notify the user + */ + private function _get_notification($task) + { + if ($task['alarms'] && $task['complete'] < 1 || strpos($task['alarms'], '@') !== false) { + $alarm = libcalendaring::get_next_alarm($task, 'task'); + + if ($alarm['time'] && $alarm['action'] == 'DISPLAY') + return date('Y-m-d H:i:s', $alarm['time']); + } + + return null; + } + +} diff -Nur roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php --- roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php 1970-01-01 01:00:00.000000000 +0100 +++ roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php 2013-11-24 00:38:51.000000000 +0100 @@ -0,0 +1,853 @@ + + * + * Copyright (C) 2012, 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 tasklist_kolab_driver extends tasklist_driver +{ + // features supported by the backend + public $alarms = false; + public $attachments = true; + public $undelete = false; // task undelete action + public $alarm_types = array('DISPLAY'); + + private $rc; + private $plugin; + private $lists; + private $folders = array(); + private $tasks = array(); + + + /** + * Default constructor + */ + public function __construct($plugin) + { + $this->rc = $plugin->rc; + $this->plugin = $plugin; + + $this->_read_lists(); + + if (kolab_storage::$version == '2.0') { + $this->alarm_absolute = false; + } + } + + /** + * Read available calendars for the current user and store them internally + */ + private function _read_lists($force = false) + { + // already read sources + if (isset($this->lists) && !$force) + return $this->lists; + + // get all folders that have type "task" + $this->folders = kolab_storage::get_folders('task'); + $this->lists = array(); + + // convert to UTF8 and sort + $names = array(); + $default_folder = null; + foreach ($this->folders as $folder) { + $names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP'); + $this->folders[$folder->name] = $folder; + if ($folder->default) + $default_folder = $folder->name; + } + + asort($names, SORT_LOCALE_STRING); + + // put default folder (aka INBOX) on top of the list + if ($default_folder) { + $default_name = $names[$default_folder]; + unset($names[$default_folder]); + $names = array_merge(array($default_folder => $default_name), $names); + } + + $delim = $this->rc->get_storage()->get_hierarchy_delimiter(); + $listnames = array(); + + $prefs = $this->rc->config->get('kolab_tasklists', array()); + + foreach ($names as $utf7name => $name) { + $folder = $this->folders[$utf7name]; + + $path_imap = explode($delim, $name); + $editname = array_pop($path_imap); // pop off raw name part + $path_imap = join($delim, $path_imap); + + $name = kolab_storage::folder_displayname(kolab_storage::object_name($utf7name), $listnames); + + if ($folder->get_namespace() == 'personal') { + $readonly = false; + $alarms = true; + } + else { + $alarms = false; + $readonly = true; + if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) { + if (strpos($rights, 'i') !== false) + $readonly = false; + } + } + + $list_id = kolab_storage::folder_id($utf7name); + $tasklist = array( + 'id' => $list_id, + 'name' => $name, + 'editname' => $editname, + 'color' => $folder->get_color('0000CC'), + 'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms, + 'editable' => !$readonly, + 'active' => $folder->is_active(), + 'parentfolder' => $path_imap, + 'default' => $folder->default, + 'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')), + ); + $this->lists[$tasklist['id']] = $tasklist; + $this->folders[$tasklist['id']] = $folder; + } + } + + /** + * Get a list of available task lists from this source + */ + public function get_lists() + { + // attempt to create a default list for this user + if (empty($this->lists)) { + if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true))) + $this->_read_lists(true); + } + + return $this->lists; + } + + /** + * Create a new list assigned to the current user + * + * @param array Hash array with list properties + * name: List name + * color: The color of the list + * showalarms: True if alarms are enabled + * @return mixed ID of the new list on success, False on error + */ + public function create_list($prop) + { + $prop['type'] = 'task' . ($prop['default'] ? '.default' : ''); + $prop['active'] = true; // activate folder by default + $folder = kolab_storage::folder_update($prop); + + if ($folder === false) { + $this->last_error = kolab_storage::$last_error; + return false; + } + + // create ID + $id = kolab_storage::folder_id($folder); + + $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array()); + + if (isset($prop['showalarms'])) + $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; + + if ($prefs['kolab_tasklists'][$id]) + $this->rc->user->save_prefs($prefs); + + return $id; + } + + /** + * Update properties of an existing tasklist + * + * @param array Hash array with list properties + * id: List Identifier + * name: List name + * color: The color of the list + * showalarms: True if alarms are enabled (if supported) + * @return boolean True on success, Fales on failure + */ + public function edit_list($prop) + { + if ($prop['id'] && ($folder = $this->folders[$prop['id']])) { + $prop['oldname'] = $folder->name; + $prop['type'] = 'task'; + $newfolder = kolab_storage::folder_update($prop); + + if ($newfolder === false) { + $this->last_error = kolab_storage::$last_error; + return false; + } + + // create ID + $id = kolab_storage::folder_id($newfolder); + + // fallback to local prefs + $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array()); + unset($prefs['kolab_tasklists'][$prop['id']]); + + if (isset($prop['showalarms'])) + $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; + + if ($prefs['kolab_tasklists'][$id]) + $this->rc->user->save_prefs($prefs); + + return $id; + } + + return false; + } + + /** + * Set active/subscribed state of a list + * + * @param array Hash array with list properties + * id: List Identifier + * active: True if list is active, false if not + * @return boolean True on success, Fales on failure + */ + public function subscribe_list($prop) + { + if ($prop['id'] && ($folder = $this->folders[$prop['id']])) { + return $folder->activate($prop['active']); + } + return false; + } + + /** + * Delete the given list with all its contents + * + * @param array Hash array with list properties + * id: list Identifier + * @return boolean True on success, Fales on failure + */ + public function remove_list($prop) + { + if ($prop['id'] && ($folder = $this->folders[$prop['id']])) { + if (kolab_storage::folder_delete($folder->name)) + return true; + else + $this->last_error = kolab_storage::$last_error; + } + + return false; + } + + /** + * Get number of tasks matching the given filter + * + * @param array List of lists to count tasks of + * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate) + */ + public function count_tasks($lists = null) + { + if (empty($lists)) + $lists = array_keys($this->lists); + else if (is_string($lists)) + $lists = explode(',', $lists); + + $today_date = new DateTime('now', $this->plugin->timezone); + $today = $today_date->format('Y-m-d'); + $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone); + $tomorrow = $tomorrow_date->format('Y-m-d'); + + $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0); + foreach ($lists as $list_id) { + $folder = $this->folders[$list_id]; + foreach ((array)$folder->select(array(array('tags','!~','x-complete'))) as $record) { + $rec = $this->_to_rcube_task($record); + + if ($rec['complete'] >= 1.0) // don't count complete tasks + continue; + + $counts['all']++; + if ($rec['flagged']) + $counts['flagged']++; + if (empty($rec['date'])) + $counts['nodate']++; + else if ($rec['date'] == $today) + $counts['today']++; + else if ($rec['date'] == $tomorrow) + $counts['tomorrow']++; + else if ($rec['date'] < $today) + $counts['overdue']++; + } + } + + return $counts; + } + + /** + * Get all taks records matching the given filter + * + * @param array Hash array with filter criterias: + * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants) + * - from: Date range start as string (Y-m-d) + * - to: Date range end as string (Y-m-d) + * - search: Search query string + * @param array List of lists to get tasks from + * @return array List of tasks records matchin the criteria + */ + public function list_tasks($filter, $lists = null) + { + if (empty($lists)) + $lists = array_keys($this->lists); + else if (is_string($lists)) + $lists = explode(',', $lists); + + $results = array(); + + // query Kolab storage + $query = array(); + if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) + $query[] = array('tags','~','x-complete'); + else + $query[] = array('tags','!~','x-complete'); + + // full text search (only works with cache enabled) + if ($filter['search']) { + $search = mb_strtolower($filter['search']); + foreach (rcube_utils::normalize_string($search, true) as $word) { + $query[] = array('words', '~', $word); + } + } + + foreach ($lists as $list_id) { + $folder = $this->folders[$list_id]; + foreach ((array)$folder->select($query) as $record) { + $task = $this->_to_rcube_task($record); + $task['list'] = $list_id; + + // TODO: post-filter tasks returned from storage + + $results[] = $task; + } + } + + return $results; + } + + /** + * Return data of a specific task + * + * @param mixed Hash array with task properties or task UID + * @return array Hash array with task properties or false if not found + */ + public function get_task($prop) + { + $id = is_array($prop) ? ($prop['uid'] ?: $prop['id']) : $prop; + $list_id = is_array($prop) ? $prop['list'] : null; + $folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders; + + // find task in the available folders + foreach ($folders as $list_id => $folder) { + if (is_numeric($list_id)) + continue; + if (!$this->tasks[$id] && ($object = $folder->get_object($id))) { + $this->tasks[$id] = $this->_to_rcube_task($object); + $this->tasks[$id]['list'] = $list_id; + break; + } + } + + return $this->tasks[$id]; + } + + /** + * Get all decendents of the given task record + * + * @param mixed Hash array with task properties or task UID + * @param boolean True if all childrens children should be fetched + * @return array List of all child task IDs + */ + public function get_childs($prop, $recursive = false) + { + if (is_string($prop)) { + $task = $this->get_task($prop); + $prop = array('id' => $task['id'], 'list' => $task['list']); + } + + $childs = array(); + $list_id = $prop['list']; + $task_ids = array($prop['id']); + $folder = $this->folders[$list_id]; + + // query for childs (recursively) + while ($folder && !empty($task_ids)) { + $query_ids = array(); + foreach ($task_ids as $task_id) { + $query = array(array('tags','=','x-parent:' . $task_id)); + foreach ((array)$folder->select($query) as $record) { + // don't rely on kolab_storage_folder filtering + if ($record['parent_id'] == $task_id) { + $childs[] = $record['uid']; + $query_ids[] = $record['uid']; + } + } + } + + if (!$recursive) + break; + + $task_ids = $query_ids; + } + + return $childs; + } + + /** + * Get a list of pending alarms to be displayed to the user + * + * @param integer Current time (unix timestamp) + * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) + * @return array A list of alarms, each encoded as hash array with task properties + * @see tasklist_driver::pending_alarms() + */ + public function pending_alarms($time, $lists = null) + { + $interval = 300; + $time -= $time % 60; + + $slot = $time; + $slot -= $slot % $interval; + + $last = $time - max(60, $this->rc->config->get('refresh_interval', 0)); + $last -= $last % $interval; + + // only check for alerts once in 5 minutes + if ($last == $slot) + return array(); + + if ($lists && is_string($lists)) + $lists = explode(',', $lists); + + $time = $slot + $interval; + + $tasks = array(); + $query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete')); + foreach ($this->lists as $lid => $list) { + // skip lists with alarms disabled + if (!$list['showalarms'] || ($lists && !in_array($lid, $lists))) + continue; + + $folder = $this->folders[$lid]; + foreach ((array)$folder->select($query) as $record) { + if (!$record['alarms']) // don't trust query :-) + continue; + + $task = $this->_to_rcube_task($record); + + // add to list if alarm is set + $alarm = libcalendaring::get_next_alarm($task, 'task'); + if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') { + $id = $task['id']; + $tasks[$id] = $task; + $tasks[$id]['notifyat'] = $alarm['time']; + } + } + } + + // get alarm information stored in local database + if (!empty($tasks)) { + $task_ids = array_map(array($this->rc->db, 'quote'), array_keys($tasks)); + $result = $this->rc->db->query(sprintf( + "SELECT * FROM kolab_alarms + WHERE event_id IN (%s) AND user_id=?", + join(',', $task_ids), + $this->rc->db->now() + ), + $this->rc->user->ID + ); + + while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { + $dbdata[$rec['event_id']] = $rec; + } + } + + $alarms = array(); + foreach ($tasks as $id => $task) { + // skip dismissed + if ($dbdata[$id]['dismissed']) + continue; + + // snooze function may have shifted alarm time + $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat']; + if ($notifyat <= $time) + $alarms[] = $task; + } + + return $alarms; + } + + /** + * (User) feedback after showing an alarm notification + * This should mark the alarm as 'shown' or snooze it for the given amount of time + * + * @param string Task identifier + * @param integer Suspend the alarm for this number of seconds + */ + public function dismiss_alarm($id, $snooze = 0) + { + // delete old alarm entry + $this->rc->db->query( + "DELETE FROM kolab_alarms + WHERE event_id=? AND user_id=?", + $id, + $this->rc->user->ID + ); + + // set new notifyat time or unset if not snoozed + $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; + + $query = $this->rc->db->query( + "INSERT INTO kolab_alarms + (event_id, user_id, dismissed, notifyat) + VALUES(?, ?, ?, ?)", + $id, + $this->rc->user->ID, + $snooze > 0 ? 0 : 1, + $notifyat + ); + + return $this->rc->db->affected_rows($query); + } + + /** + * Convert from Kolab_Format to internal representation + */ + private function _to_rcube_task($record) + { + $task = array( + 'id' => $record['uid'], + 'uid' => $record['uid'], + 'title' => $record['title'], +# 'location' => $record['location'], + 'description' => $record['description'], + 'tags' => (array)$record['categories'], + 'flagged' => $record['priority'] == 1, + 'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100), + 'parent_id' => $record['parent_id'], + ); + + // convert from DateTime to internal date format + if (is_a($record['due'], 'DateTime')) { + $task['date'] = $record['due']->format('Y-m-d'); + if (!$record['due']->_dateonly) + $task['time'] = $record['due']->format('H:i'); + } + // convert from DateTime to internal date format + if (is_a($record['start'], 'DateTime')) { + $task['startdate'] = $record['start']->format('Y-m-d'); + if (!$record['start']->_dateonly) + $task['starttime'] = $record['start']->format('H:i'); + } + if (is_a($record['dtstamp'], 'DateTime')) { + $task['changed'] = $record['dtstamp']; + } + + if ($record['alarms']) { + $task['alarms'] = $record['alarms']; + } + + if (!empty($record['_attachments'])) { + foreach ($record['_attachments'] as $key => $attachment) { + if ($attachment !== false) { + if (!$attachment['name']) + $attachment['name'] = $key; + $attachments[] = $attachment; + } + } + + $task['attachments'] = $attachments; + } + + return $task; + } + + /** + * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving + * (opposite of self::_to_rcube_event()) + */ + private function _from_rcube_task($task, $old = array()) + { + $object = $task; + $object['categories'] = (array)$task['tags']; + + if (!empty($task['date'])) { + $object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone); + if (empty($task['time'])) + $object['due']->_dateonly = true; + unset($object['date']); + } + + if (!empty($task['startdate'])) { + $object['start'] = new DateTime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone); + if (empty($task['starttime'])) + $object['start']->_dateonly = true; + unset($object['startdate']); + } + + $object['complete'] = $task['complete'] * 100; + if ($task['complete'] == 1.0) + $object['status'] = 'COMPLETED'; + + if ($task['flagged']) + $object['priority'] = 1; + else + $object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0; + + // copy meta data (starting with _) from old object + foreach ((array)$old as $key => $val) { + if (!isset($object[$key]) && $key[0] == '_') + $object[$key] = $val; + } + + // delete existing attachment(s) + if (!empty($task['deleted_attachments'])) { + foreach ($task['deleted_attachments'] as $attachment) { + if (is_array($object['_attachments'])) { + foreach ($object['_attachments'] as $idx => $att) { + if ($att['id'] == $attachment) + $object['_attachments'][$idx] = false; + } + } + } + unset($task['deleted_attachments']); + } + + // in kolab_storage attachments are indexed by content-id + if (is_array($task['attachments'])) { + foreach ($task['attachments'] as $idx => $attachment) { + $key = null; + // Roundcube ID has nothing to do with the storage ID, remove it + if ($attachment['content']) { + unset($attachment['id']); + } + else { + foreach ((array)$old['_attachments'] as $cid => $oldatt) { + if ($oldatt && $attachment['id'] == $oldatt['id']) + $key = $cid; + } + } + + // replace existing entry + if ($key) { + $object['_attachments'][$key] = $attachment; + } + // append as new attachment + else { + $object['_attachments'][] = $attachment; + } + } + + unset($object['attachments']); + } + + unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']); + return $object; + } + + /** + * Add a single task to the database + * + * @param array Hash array with task properties (see header of tasklist_driver.php) + * @return mixed New task ID on success, False on error + */ + public function create_task($task) + { + return $this->edit_task($task); + } + + /** + * Update an task entry with the given data + * + * @param array Hash array with task properties (see header of tasklist_driver.php) + * @return boolean True on success, False on error + */ + public function edit_task($task) + { + $list_id = $task['list']; + if (!$list_id || !($folder = $this->folders[$list_id])) + return false; + + // moved from another folder + if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) { + if (!$fromfolder->move($task['id'], $folder->name)) + return false; + + unset($task['_fromlist']); + } + + // load previous version of this task to merge + if ($task['id']) { + $old = $folder->get_object($task['id']); + if (!$old || PEAR::isError($old)) + return false; + + // merge existing properties if the update isn't complete + if (!isset($task['title']) || !isset($task['complete'])) + $task += $this->_to_rcube_task($old); + } + + // generate new task object from RC input + $object = $this->_from_rcube_task($task, $old); + $saved = $folder->save($object, 'task', $task['id']); + + if (!$saved) { + raise_error(array( + 'code' => 600, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Error saving task object to Kolab server"), + true, false); + $saved = false; + } + else { + $task = $this->_to_rcube_task($object); + $task['list'] = $list_id; + $this->tasks[$task['id']] = $task; + } + + return $saved; + } + + /** + * Move a single task to another list + * + * @param array Hash array with task properties: + * @return boolean True on success, False on error + * @see tasklist_driver::move_task() + */ + public function move_task($task) + { + $list_id = $task['list']; + if (!$list_id || !($folder = $this->folders[$list_id])) + return false; + + // execute move command + if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) { + return $fromfolder->move($task['id'], $folder->name); + } + + return false; + } + + /** + * Remove a single task from the database + * + * @param array Hash array with task properties: + * id: Task identifier + * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend) + * @return boolean True on success, False on error + */ + public function delete_task($task, $force = true) + { + $list_id = $task['list']; + if (!$list_id || !($folder = $this->folders[$list_id])) + return false; + + return $folder->delete($task['id']); + } + + /** + * Restores a single deleted task (if supported) + * + * @param array Hash array with task properties: + * id: Task identifier + * @return boolean True on success, False on error + */ + public function undelete_task($prop) + { + // TODO: implement this + return false; + } + + + /** + * Get attachment properties + * + * @param string $id Attachment identifier + * @param array $task Hash array with event properties: + * id: Task identifier + * list: List identifier + * + * @return array Hash array with attachment properties: + * id: Attachment identifier + * name: Attachment name + * mimetype: MIME content type of the attachment + * size: Attachment size + */ + public function get_attachment($id, $task) + { + $task['uid'] = $task['id']; + $task = $this->get_task($task); + + if ($task && !empty($task['attachments'])) { + foreach ($task['attachments'] as $att) { + if ($att['id'] == $id) + return $att; + } + } + + return null; + } + + /** + * Get attachment body + * + * @param string $id Attachment identifier + * @param array $task Hash array with event properties: + * id: Task identifier + * list: List identifier + * + * @return string Attachment body + */ + public function get_attachment_body($id, $task) + { + if ($storage = $this->folders[$task['list']]) { + return $storage->get_attachment($task['id'], $id); + } + + return false; + } + + /** + * + */ + public function tasklist_edit_form($fieldprop) + { + $select = kolab_storage::folder_selector('task', array('name' => 'parent', 'id' => 'taskedit-parentfolder'), null); + $fieldprop['parent'] = array( + 'id' => 'taskedit-parentfolder', + 'label' => $this->plugin->gettext('parentfolder'), + 'value' => $select->show(''), + ); + + $formfields = array(); + foreach (array('name','parent','showalarms') as $f) { + $formfields[$f] = $fieldprop[$f]; + } + + return parent::tasklist_edit_form($formfields); + } + +} diff -Nur roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/drivers/tasklist_driver.php roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/drivers/tasklist_driver.php --- roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/drivers/tasklist_driver.php 1970-01-01 01:00:00.000000000 +0100 +++ roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/drivers/tasklist_driver.php 2013-11-24 00:38:51.000000000 +0100 @@ -0,0 +1,278 @@ + + * + * Copyright (C) 2012, 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 . + */ + + /** + * Struct of an internal task object how it is passed from/to the driver classes: + * + * $task = array( + * 'id' => 'Task ID used for editing', // must be unique for the current user + * 'parent_id' => 'ID of parent task', // null if top-level task + * 'uid' => 'Unique identifier of this task', + * 'list' => 'Task list identifier to add the task to or where the task is stored', + * 'changed' => , // Last modification date/time of the record + * 'title' => 'Event title/summary', + * 'description' => 'Event description', + * 'tags' => array(), // List of tags for this task + * 'date' => 'Due date', // as string of format YYYY-MM-DD or null if no date is set + * 'time' => 'Due time', // as string of format hh::ii or null if no due time is set + * 'startdate' => 'Start date' // Delay start of the task until that date + * 'starttime' => 'Start time' // ...and time + * 'categories' => 'Task category', + * 'flagged' => 'Boolean value whether this record is flagged', + * 'complete' => 'Float value representing the completeness state (range 0..1)', + * 'sensitivity' => 0|1|2, // Event sensitivity (0=public, 1=private, 2=confidential) + * 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before due time) + * '_fromlist' => 'List identifier where the task was stored before', + * ); + */ + +/** + * Driver interface for the Tasklist plugin + */ +abstract class tasklist_driver +{ + // features supported by the backend + public $alarms = false; + public $attachments = false; + public $undelete = false; // task undelete action + public $sortable = false; + public $alarm_types = array('DISPLAY'); + public $alarm_absolute = true; + public $last_error; + + /** + * Get a list of available task lists from this source + */ + abstract function get_lists(); + + /** + * Create a new list assigned to the current user + * + * @param array Hash array with list properties + * name: List name + * color: The color of the list + * showalarms: True if alarms are enabled + * @return mixed ID of the new list on success, False on error + */ + abstract function create_list($prop); + + /** + * Update properties of an existing tasklist + * + * @param array Hash array with list properties + * id: List Identifier + * name: List name + * color: The color of the list + * showalarms: True if alarms are enabled (if supported) + * @return boolean True on success, Fales on failure + */ + abstract function edit_list($prop); + + /** + * Set active/subscribed state of a list + * + * @param array Hash array with list properties + * id: List Identifier + * active: True if list is active, false if not + * @return boolean True on success, Fales on failure + */ + abstract function subscribe_list($prop); + + /** + * Delete the given list with all its contents + * + * @param array Hash array with list properties + * id: list Identifier + * @return boolean True on success, Fales on failure + */ + abstract function remove_list($prop); + + /** + * Get number of tasks matching the given filter + * + * @param array List of lists to count tasks of + * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate) + */ + abstract function count_tasks($lists = null); + + /** + * Get all taks records matching the given filter + * + * @param array Hash array with filter criterias: + * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants) + * - from: Date range start as string (Y-m-d) + * - to: Date range end as string (Y-m-d) + * - search: Search query string + * @param array List of lists to get tasks from + * @return array List of tasks records matchin the criteria + */ + abstract function list_tasks($filter, $lists = null); + + /** + * Get a list of pending alarms to be displayed to the user + * + * @param integer Current time (unix timestamp) + * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) + * @return array A list of alarms, each encoded as hash array with task properties + * id: Task identifier + * uid: Unique identifier of this task + * date: Task due date + * time: Task due time + * title: Task title/summary + */ + abstract function pending_alarms($time, $lists = null); + + /** + * (User) feedback after showing an alarm notification + * This should mark the alarm as 'shown' or snooze it for the given amount of time + * + * @param string Task identifier + * @param integer Suspend the alarm for this number of seconds + */ + abstract function dismiss_alarm($id, $snooze = 0); + + /** + * Return data of a specific task + * + * @param mixed Hash array with task properties or task UID + * @return array Hash array with task properties or false if not found + */ + abstract public function get_task($prop); + + /** + * Get decendents of the given task record + * + * @param mixed Hash array with task properties or task UID + * @param boolean True if all childrens children should be fetched + * @return array List of all child task IDs + */ + abstract public function get_childs($prop, $recursive = false); + + /** + * Add a single task to the database + * + * @param array Hash array with task properties (see header of this file) + * @return mixed New event ID on success, False on error + */ + abstract function create_task($prop); + + /** + * Update an task entry with the given data + * + * @param array Hash array with task properties (see header of this file) + * @return boolean True on success, False on error + */ + abstract function edit_task($prop); + + /** + * Move a single task to another list + * + * @param array Hash array with task properties: + * id: Task identifier + * list: New list identifier to move to + * _fromlist: Previous list identifier + * @return boolean True on success, False on error + */ + abstract function move_task($prop); + + /** + * Remove a single task from the database + * + * @param array Hash array with task properties: + * id: Task identifier + * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend) + * @return boolean True on success, False on error + */ + abstract function delete_task($prop, $force = true); + + /** + * Restores a single deleted task (if supported) + * + * @param array Hash array with task properties: + * id: Task identifier + * @return boolean True on success, False on error + */ + public function undelete_task($prop) + { + return false; + } + + /** + * Get attachment properties + * + * @param string $id Attachment identifier + * @param array $task Hash array with event properties: + * id: Task identifier + * list: List identifier + * + * @return array Hash array with attachment properties: + * id: Attachment identifier + * name: Attachment name + * mimetype: MIME content type of the attachment + * size: Attachment size + */ + public function get_attachment($id, $task) { } + + /** + * Get attachment body + * + * @param string $id Attachment identifier + * @param array $task Hash array with event properties: + * id: Task identifier + * list: List identifier + * + * @return string Attachment body + */ + public function get_attachment_body($id, $task) { } + + /** + * List availabale categories + * The default implementation reads them from config/user prefs + */ + public function list_categories() + { + $rcmail = rcube::get_instance(); + return $rcmail->config->get('tasklist_categories', array()); + } + + /** + * Build the edit/create form for lists. + * This gives the drivers the opportunity to add more list properties + * + * @param array List with form fields to be rendered + * @return string HTML content of the form + */ + public function tasklist_edit_form($formfields) + { + $html = ''; + foreach ($formfields as $field) { + $html .= html::div('form-section', + html::label($field['id'], $field['label']) . + $field['value']); + } + + return $html; + } + +} diff -Nur roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/.gitignore roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/.gitignore --- roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/.gitignore 2013-11-24 00:38:51.000000000 +0100 @@ -0,0 +1 @@ +config.inc.php \ Pas de fin de ligne à la fin du fichier. diff -Nur roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/jquery.tagedit.js roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/jquery.tagedit.js --- roundcube-0.9.5-old/root/opt/roundcube/plugins/tasklist/jquery.tagedit.js 1970-01-01 01:00:00.000000000 +0100 +++ roundcube-0.9.5/root/opt/roundcube/plugins/tasklist/jquery.tagedit.js 2013-11-24 00:38:51.000000000 +0100 @@ -0,0 +1,535 @@ +/* +* Tagedit - jQuery Plugin +* The Plugin can be used to edit tags from a database the easy way +* +* Examples and documentation at: tagedit.webwork-albrecht.de +* +* Copyright (c) 2010 Oliver Albrecht +* +* License: +* This work is licensed under a MIT License +* http://www.opensource.org/licenses/mit-license.php +* +* @author Oliver Albrecht Mial: info@webwork-albrecht.de Twitter: @webworka +* @version 1.2.1 (11/2011) +* Requires: jQuery v1.4+, jQueryUI v1.8+, jQuerry.autoGrowInput +* +* Example of usage: +* +* $( "input.tag" ).tagedit(); +* +* Possible options: +* +* autocompleteURL: '', // url for a autocompletion +* deleteEmptyItems: true, // Deletes items with empty value +* deletedPostfix: '-d', // will be put to the Items that are marked as delete +* addedPostfix: '-a', // will be put to the Items that are choosem from the database +* additionalListClass: '', // put a classname here if the wrapper ul shoud receive a special class +* allowEdit: true, // Switch on/off edit entries +* allowDelete: true, // Switch on/off deletion of entries. Will be ignored if allowEdit = false +* allowAdd: true, // switch on/off the creation of new entries +* direction: 'ltr' // Sets the writing direction for Outputs and Inputs +* animSpeed: 500 // Sets the animation speed for effects +* autocompleteOptions: {}, // Setting Options for the jquery UI Autocomplete (http://jqueryui.com/demos/autocomplete/) +* breakKeyCodes: [ 13, 44 ], // Sets the characters to break on to parse the tags (defaults: return, comma) +* checkNewEntriesCaseSensitive: false, // If there is a new Entry, it is checked against the autocompletion list. This Flag controlls if the check is (in-)casesensitive +* texts: { // some texts +* removeLinkTitle: 'Remove from list.', +* saveEditLinkTitle: 'Save changes.', +* deleteLinkTitle: 'Delete this tag from database.', +* deleteConfirmation: 'Are you sure to delete this entry?', +* deletedElementTitle: 'This Element will be deleted.', +* breakEditLinkTitle: 'Cancel' +* } +*/ + +(function($) { + + $.fn.tagedit = function(options) { + /** + * Merge Options with defaults + */ + options = $.extend(true, { + // default options here + autocompleteURL: null, + deletedPostfix: '-d', + addedPostfix: '-a', + additionalListClass: '', + allowEdit: true, + allowDelete: true, + allowAdd: true, + direction: 'ltr', + animSpeed: 500, + autocompleteOptions: { + select: function( event, ui ) { + $(this).val(ui.item.value).trigger('transformToTag', [ui.item.id]); + return false; + } + }, + breakKeyCodes: [ 13, 44 ], + checkNewEntriesCaseSensitive: false, + texts: { + removeLinkTitle: 'Remove from list.', + saveEditLinkTitle: 'Save changes.', + deleteLinkTitle: 'Delete this tag from database.', + deleteConfirmation: 'Are you sure to delete this entry?', + deletedElementTitle: 'This Element will be deleted.', + breakEditLinkTitle: 'Cancel' + }, + tabindex: false + }, options || {}); + + // no action if there are no elements + if(this.length == 0) { + return; + } + + // set the autocompleteOptions source + if(options.autocompleteURL) { + options.autocompleteOptions.source = options.autocompleteURL; + } + + // Set the direction of the inputs + var direction= this.attr('dir'); + if(direction && direction.length > 0) { + options.direction = this.attr('dir'); + } + + var elements = this; + + var baseNameRegexp = new RegExp("^(.*)\\[([0-9]*?("+options.deletedPostfix+"|"+options.addedPostfix+")?)?\]$", "i"); + + var baseName = elements.eq(0).attr('name').match(baseNameRegexp); + if(baseName && baseName.length == 4) { + baseName = baseName[1]; + } + else { + // Elementname does not match the expected format, exit + alert('elementname dows not match the expected format (regexp: '+baseNameRegexp+')') + return; + } + + // read tabindex from source element + var ti; + if (!options.tabindex && (ti = elements.eq(0).attr('tabindex'))) + options.tabindex = ti; + + // init elements + inputsToList(); + + /** + * Creates the tageditinput from a list of textinputs + * + */ + function inputsToList() { + var html = '
    '; + + elements.each(function() { + var element_name = $(this).attr('name').match(baseNameRegexp); + if(element_name && element_name.length == 4 && (options.deleteEmptyItems == false || $(this).val().length > 0)) { + if(element_name[1].length > 0) { + var elementId = typeof element_name[2] != 'undefined'? element_name[2]: ''; + + html += '
  • '; + html += '' + $(this).val() + ''; + html += ''; + html += 'x'; + html += '
  • '; + } + } + }); + + // replace Elements with the list and save the list in the local variable elements + elements.last().after(html) + var newList = elements.last().next(); + elements.remove(); + elements = newList; + + // Check if some of the elementshav to be marked as deleted + if(options.deletedPostfix.length > 0) { + elements.find('input[name$="'+options.deletedPostfix+'\]"]').each(function() { + markAsDeleted($(this).parent()); + }); + } + + // put an input field at the End + // Put an empty element at the end + html = '
  • '; + html += ''; + html += '
  • '; + html += '
'; + + elements + .append(html) + .attr('tabindex', options.tabindex) // set tabindex to