diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php index 4d61e1d3..628b7c52 100644 --- a/plugins/tasklist/drivers/database/tasklist_database_driver.php +++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php @@ -1,850 +1,846 @@ * * Copyright (C) 2012-2015, 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 { const IS_COMPLETE_SQL = "(`status` = 'COMPLETED' OR (`complete` = 1 AND `status` = ''))"; public $undelete = true; // yes, we can public $sortable = false; public $alarm_types = array('DISPLAY'); private $rc; private $plugin; private $lists = array(); private $tags = array(); private $list_ids = ''; private $db_tasks = 'tasks'; private $db_lists = 'tasklists'; /** * Default constructor */ public function __construct($plugin) { $this->rc = $plugin->rc; $this->plugin = $plugin; // read database config $db = $this->rc->get_dbh(); $this->db_lists = $this->rc->config->get('db_table_lists', $db->table_name($this->db_lists)); $this->db_tasks = $this->rc->config->get('db_table_tasks', $db->table_name($this->db_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['listname'] = html::quote($arr['name']); $arr['editable'] = true; $arr['rights'] = 'lrswikxtea'; $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($filter = 0) { // attempt to create a default list for this user if (empty($this->lists)) { $prop = array('name' => 'Default', 'color' => '000000'); if ($this->create_list($prop)) { $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, strval($prop['name']), - strval($prop['color']), - $prop['showalarms'] ? 1 : 0 + isset($prop['color']) ? strval($prop['color']) : '', + !empty($prop['showalarms']) ? 1 : 0 ); if ($result) { $prop['rights'] = 'lrswikxtea'; return $this->rc->db->insert_id($this->db_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` = ?", strval($prop['name']), - strval($prop['color']), - $prop['showalarms'] ? 1 : 0, + isset($prop['color']) ? strval($prop['color']) : '', + !empty($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']) { + if (!empty($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::delete_list() */ public function delete_list($prop) { $list_id = $prop['id']; if ($this->lists[$list_id]) { $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; } /** * Search for shared or otherwise not listed tasklists the user has access * * @param string Search string * @param string Section/source to search * * @return array List of tasklists */ public function search_lists($query, $source) { return array(); } /** * Get a list of tags to assign tasks to * * @return array List of tags */ public function get_tags() { return array_values(array_unique($this->tags, SORT_STRING)); } /** * 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_array($lists)) { $lists = explode(',', (string) $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 NOT " . self::IS_COMPLETE_SQL, join(',', $list_ids) )); $counts = array('all' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'later' => 0); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $counts['all']++; if (empty($rec['date'])) $counts['later']++; else if ($rec['date'] == $today) $counts['today']++; else if ($rec['date'] == $tomorrow) $counts['tomorrow']++; else if ($rec['date'] < $today) $counts['overdue']++; else if ($rec['date'] > $tomorrow) $counts['later']++; } return $counts; } /** * Get all task 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_array($lists)) { $lists = explode(',', (string) $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']) . ")"; + if ($filter) { + if (!empty($filter['from']) || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) { + $sql_add .= " AND (`date` IS NULL OR `date` >= " . $this->rc->db->quote($filter['from']) . ")"; } - } - // 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 (!empty($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']) . ")"; + } + } - if ($filter['mask'] & tasklist::FILTER_MASK_NODATE) { - $sql_add = " AND `date` IS NULL"; - } + if ($filter['mask'] & tasklist::FILTER_MASK_NODATE) { + $sql_add = " AND `date` IS NULL"; + } - if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) { - $sql_add .= " AND " . self::IS_COMPLETE_SQL; - } - else if (empty($filter['since'])) { - // don't show complete tasks by default - $sql_add .= " AND NOT " . self::IS_COMPLETE_SQL; - } + if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) { + $sql_add .= " AND " . self::IS_COMPLETE_SQL; + } + else if (empty($filter['since'])) { + // don't show complete tasks by default + $sql_add .= " AND NOT " . self::IS_COMPLETE_SQL; + } - if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED) { - $sql_add .= " AND `flagged` = 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'] . '%'); + // 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) . ")"; } - $sql_add = " AND (" . join(" OR ", $sql_query) . ")"; - } - if ($filter['since'] && is_numeric($filter['since'])) { - $sql_add .= " AND `changed` >= " . $this->rc->db->quote(date('Y-m-d H:i:s', $filter['since'])); - } + if (!empty($filter['since']) && is_numeric($filter['since'])) { + $sql_add .= " AND `changed` >= " . $this->rc->db->quote(date('Y-m-d H:i:s', $filter['since'])); + } - if ($filter['uid']) { - $sql_add .= " AND `uid` IN (" . implode(',', array_map(array($this->rc->db, 'quote'), $filter['uid'])) . ")"; + if (!empty($filter['uid'])) { + $sql_add .= " AND `uid` IN (" . implode(',', array_map(array($this->rc->db, 'quote'), $filter['uid'])) . ")"; + } } $tasks = array(); if (!empty($list_ids)) { $result = $this->rc->db->query("SELECT * FROM " . $this->db_tasks . " WHERE `tasklist_id` IN (" . join(',', $list_ids) . ")" . " AND `del` = 0" . $sql_add - . " ORDER BY `parent_id`, `task_id` ASC", - $datefrom + . " ORDER BY `parent_id`, `task_id` ASC" ); 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 * @param integer Bitmask defining filter criterias. * See FILTER_* constants for possible values. * * @return array Hash array with task properties or false if not found */ public function get_task($prop, $filter = 0) { if (is_string($prop)) { $prop['uid'] = $prop; } - $query_col = $prop['id'] ? 'task_id' : 'uid'; + $query_col = !empty($prop['id']) ? 'task_id' : 'uid'; $result = $this->rc->db->query("SELECT * FROM " . $this->db_tasks . " WHERE `tasklist_id` IN (" . $this->list_ids . ")" . " AND `$query_col` = ? AND `del` = 0", - $prop['id'] ? $prop['id'] : $prop['uid'] + !empty($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( "SELECT `task_id` AS id, `tasklist_id` AS list FROM " . $this->db_tasks . " WHERE `tasklist_id` IN (" . $this->list_ids . ")" . " AND `uid` = ?", $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( "SELECT `task_id` AS id FROM " . $this->db_tasks . " WHERE `tasklist_id` IN (" . $this->list_ids . ")" . " AND `parent_id` IN (" . join(',', array_map(array($this->rc->db, 'quote'), $task_ids)) . ")" . " AND `del` = 0" ); $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_array($lists)) { $lists = explode(',', (string) $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("SELECT * FROM " . $this->db_tasks . " WHERE `tasklist_id` IN (" . join(',', $list_ids) . ")" . " AND `notify` <= " . $this->rc->db->fromunixtime($time) . " AND NOT " . self::IS_COMPLETE_SQL ); 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("UPDATE " . $this->db_tasks . " SET `changed` = " . $this->rc->db->now() . ", `notify` = ?" . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", $notify_at, $task_id ); return $this->rc->db->affected_rows($query); } /** * Remove alarm dismissal or snooze state * * @param string Task identifier */ public function clear_alarms($id) { // Nothing to do here. Alarms are reset in edit_task() } /** * 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['created'] = new DateTime($rec['created']); $rec['tags'] = array_filter(explode(',', $rec['tags'])); if (!$rec['parent_id']) { unset($rec['parent_id']); } // decode serialized alarms if ($rec['alarms']) { $rec['valarms'] = $this->unserialize_alarms($rec['alarms']); unset($rec['alarms']); } // decode serialze recurrence rules if ($rec['recurrence']) { $rec['recurrence'] = $this->unserialize_recurrence($rec['recurrence']); } if (!empty($rec['tags'])) { $this->tags = array_merge($this->tags, (array)$rec['tags']); } unset($rec['task_id'], $rec['tasklist_id']); 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']) { + $list_id = !empty($prop['list']) ? $prop['list'] : reset(array_keys($this->lists)); + if (empty($this->lists[$list_id]) || !empty($this->lists[$list_id]['readonly'])) { return false; } - if (is_array($prop['valarms'])) { + if (!empty($prop['valarms'])) { $prop['alarms'] = $this->serialize_alarms($prop['valarms']); } - if (is_array($prop['recurrence'])) { + if (!empty($prop['recurrence'])) { $prop['recurrence'] = $this->serialize_recurrence($prop['recurrence']); } if (array_key_exists('complete', $prop)) { $prop['complete'] = number_format($prop['complete'], 2, '.', ''); } foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence', 'status') as $col) { if (empty($prop[$col])) { $prop[$col] = null; } } $notify_at = $this->_get_notification($prop); $now = $this->rc->db->now(); $result = $this->rc->db->query("INSERT INTO " . $this->db_tasks . " (`tasklist_id`, `uid`, `parent_id`, `created`, `changed`, `title`, `date`, `time`," . " `startdate`, `starttime`, `description`, `tags`, `flagged`, `complete`, `status`," . " `alarms`, `recurrence`, `notify`)" . " VALUES (?, ?, ?, $now, $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['flagged'] ? 1 : 0, - $prop['complete'] ?: 0, + isset($prop['description']) ? strval($prop['description']) : '', + !empty($prop['tags']) ? join(',', (array)$prop['tags']) : '', + !empty($prop['flagged']) ? 1 : 0, + !empty($prop['complete']) ?: 0, strval($prop['status']), - $prop['alarms'], - $prop['recurrence'], + isset($prop['alarms']) ? $prop['alarms'] : '', + isset($prop['recurrence']) ? $prop['recurrence'] : '', $notify_at ); if ($result) { return $this->rc->db->insert_id($this->db_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) { - if (is_array($prop['valarms'])) { + if (isset($prop['valarms'])) { $prop['alarms'] = $this->serialize_alarms($prop['valarms']); } - if (is_array($prop['recurrence'])) { + if (isset($prop['recurrence'])) { $prop['recurrence'] = $this->serialize_recurrence($prop['recurrence']); } if (array_key_exists('complete', $prop)) { $prop['complete'] = number_format($prop['complete'], 2, '.', ''); } $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', 'recurrence') 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['status'])) { $sql_set[] = $this->rc->db->quote_identifier('status') . '=' . $this->rc->db->quote($prop['status']); } 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'])) { + if (!empty($prop['_fromlist']) && ($newlist = $prop['list'])) { $sql_set[] = $this->rc->db->quote_identifier('tasklist_id') . '=' . $this->rc->db->quote($newlist); } $result = $this->rc->db->query("UPDATE " . $this->db_tasks . " SET `changed` = " . $this->rc->db->now() . ($sql_set ? ', ' . join(', ', $sql_set) : '') . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", $prop['id'] ); return $this->rc->db->affected_rows($result); } /** * 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) { $result = $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) { $result = $this->rc->db->query("UPDATE " . $this->db_tasks . " SET `changed` = " . $this->rc->db->now() . ", `del` = 1" . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", $task_id ); } return $this->rc->db->affected_rows($result); } /** * 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) { $result = $this->rc->db->query("UPDATE " . $this->db_tasks . " SET `changed` = " . $this->rc->db->now() . ", `del` = 0" . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", $prop['id'] ); return $this->rc->db->affected_rows($result); } /** * Compute absolute time to notify the user */ private function _get_notification($task) { - if ($task['valarms'] && !$this->is_complete($task)) { + if (!empty($task['valarms']) && !$this->is_complete($task)) { $alarm = libcalendaring::get_next_alarm($task, 'task'); - if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types)) { + if (!empty($alarm['time']) && in_array($alarm['action'], $this->alarm_types)) { return date('Y-m-d H:i:s', $alarm['time']); } } } /** * Helper method to serialize the list of alarms into a string */ private function serialize_alarms($valarms) { foreach ((array)$valarms as $i => $alarm) { if ($alarm['trigger'] instanceof DateTime) { $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c'); } } return $valarms ? json_encode($valarms) : null; } /** * Helper method to decode a serialized list of alarms */ private function unserialize_alarms($alarms) { // decode json serialized alarms if ($alarms && $alarms[0] == '[') { $valarms = json_decode($alarms, true); foreach ($valarms as $i => $alarm) { if ($alarm['trigger'][0] == '@') { try { $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1)); } catch (Exception $e) { unset($valarms[$i]); } } } } // convert legacy alarms data else if (strlen($alarms)) { list($trigger, $action) = explode(':', $alarms, 2); if ($trigger = libcalendaring::parse_alarm_value($trigger)) { $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0])); } } return $valarms; } /** * Helper method to serialize task recurrence properties */ private function serialize_recurrence($recurrence) { foreach ((array)$recurrence as $k => $val) { if ($val instanceof DateTime) { $recurrence[$k] = '@' . $val->format('c'); } } return $recurrence ? json_encode($recurrence) : null; } /** * Helper method to decode a serialized task recurrence struct */ private function unserialize_recurrence($ser) { if (strlen($ser)) { $recurrence = json_decode($ser, true); foreach ((array)$recurrence as $k => $val) { if ($val[0] == '@') { try { $recurrence[$k] = new DateTime(substr($val, 1)); } catch (Exception $e) { unset($recurrence[$k]); } } } } else { $recurrence = ''; } return $recurrence; } /** * Handler for user_delete plugin hook */ public function user_delete($args) { $db = $this->rc->db; $lists = $db->query("SELECT `tasklist_id` FROM " . $this->db_lists . " WHERE `user_id` = ?", $args['user']->ID); $list_ids = array(); while ($row = $db->fetch_assoc($lists)) { $list_ids[] = $row['tasklist_id']; } if (!empty($list_ids)) { foreach (array($this->db_tasks, $this->db_lists) as $table) { $db->query(sprintf("DELETE FROM $table WHERE `tasklist_id` IN (%s)", join(',', $list_ids))); } } } } diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index a5344dd9..2d991535 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -1,2515 +1,2527 @@ * * 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 extends rcube_plugin { const FILTER_MASK_TODAY = 1; const FILTER_MASK_TOMORROW = 2; const FILTER_MASK_WEEK = 4; const FILTER_MASK_LATER = 8; const FILTER_MASK_NODATE = 16; const FILTER_MASK_OVERDUE = 32; const FILTER_MASK_FLAGGED = 64; const FILTER_MASK_COMPLETE = 128; const FILTER_MASK_ASSIGNED = 256; const FILTER_MASK_MYTASKS = 512; const SESSION_KEY = 'tasklist_temp'; public static $filter_masks = array( 'today' => self::FILTER_MASK_TODAY, 'tomorrow' => self::FILTER_MASK_TOMORROW, 'week' => self::FILTER_MASK_WEEK, 'later' => self::FILTER_MASK_LATER, 'nodate' => self::FILTER_MASK_NODATE, 'overdue' => self::FILTER_MASK_OVERDUE, 'flagged' => self::FILTER_MASK_FLAGGED, 'complete' => self::FILTER_MASK_COMPLETE, 'assigned' => self::FILTER_MASK_ASSIGNED, 'mytasks' => self::FILTER_MASK_MYTASKS, ); public $task = '?(?!login|logout).*'; public $allowed_prefs = array('tasklist_sort_col','tasklist_sort_order'); public $rc; public $lib; public $timezone; public $ui; public $home; // declare public to be used in other classes // These are handled by __get() // public $driver; // public $itip; // public $ical; private $collapsed_tasks = array(); private $message_tasks = array(); + private $task_titles = array(); /** * Plugin initialization. */ function init() { $this->require_plugin('libcalendaring'); $this->require_plugin('libkolab'); $this->rc = rcube::get_instance(); $this->lib = libcalendaring::get_instance(); $this->register_task('tasks', 'tasklist'); // load plugin configuration $this->load_config(); $this->timezone = $this->lib->timezone; // proceed initialization in startup hook $this->add_hook('startup', array($this, 'startup')); $this->add_hook('user_delete', array($this, 'user_delete')); } /** * Startup hook */ public function startup($args) { // the tasks module can be enabled/disabled by the kolab_auth plugin if ($this->rc->config->get('tasklist_disabled', false) || !$this->rc->config->get('tasklist_enabled', true)) return; // load localizations $this->add_texts('localization/', $args['task'] == 'tasks' && (!$args['action'] || $args['action'] == 'print')); $this->rc->load_language($_SESSION['language'], array('tasks.tasks' => $this->gettext('navtitle'))); // add label for task title if ($args['task'] == 'tasks' && $args['action'] != 'save-pref') { $this->load_driver(); // register calendar actions $this->register_action('index', array($this, 'tasklist_view')); $this->register_action('task', array($this, 'task_action')); $this->register_action('tasklist', array($this, 'tasklist_action')); $this->register_action('counts', array($this, 'fetch_counts')); $this->register_action('fetch', array($this, 'fetch_tasks')); $this->register_action('print', array($this, 'print_tasks')); $this->register_action('dialog-ui', array($this, 'mail_message2task')); $this->register_action('get-attachment', array($this, 'attachment_get')); $this->register_action('upload', array($this, 'attachment_upload')); $this->register_action('import', array($this, 'import_tasks')); $this->register_action('export', array($this, 'export_tasks')); $this->register_action('mailimportitip', array($this, 'mail_import_itip')); $this->register_action('mailimportattach', array($this, 'mail_import_attachment')); $this->register_action('itip-status', array($this, 'task_itip_status')); $this->register_action('itip-remove', array($this, 'task_itip_remove')); $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply')); $this->register_action('itip-delegate', array($this, 'mail_itip_delegate')); $this->add_hook('refresh', array($this, 'refresh')); $this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', ''))); } else if ($args['task'] == 'mail') { if ($args['action'] == 'show' || $args['action'] == 'preview') { if ($this->rc->config->get('tasklist_mail_embed', true)) { $this->add_hook('message_load', array($this, 'mail_message_load')); } $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); } // add 'Create event' item to message menu - if ($this->api->output->type == 'html' && $_GET['_rel'] != 'task') { + if ($this->api->output->type == 'html' && (empty($_GET['_rel']) || $_GET['_rel'] != 'task')) { $this->api->add_content(html::tag('li', array('role' => 'menuitem'), $this->api->output->button(array( 'command' => 'tasklist-create-from-mail', 'label' => 'tasklist.createfrommail', 'type' => 'link', 'classact' => 'icon taskaddlink active', 'class' => 'icon taskaddlink disabled', 'innerclass' => 'icon taskadd', ))), 'messagemenu'); $this->api->output->add_label('tasklist.createfrommail'); } } - if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) { + if (!$this->rc->output->ajax_call && empty($this->rc->output->env['framed'])) { $this->load_ui(); $this->ui->init(); } // add hooks for alarms handling $this->add_hook('pending_alarms', array($this, 'pending_alarms')); $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms')); } /** * */ private function load_ui() { if (!$this->ui) { require_once($this->home . '/tasklist_ui.php'); $this->ui = new tasklist_ui($this); } } /** * Helper method to load the backend driver according to local config */ private function load_driver() { - if (is_object($this->driver)) { + if (!empty($this->driver)) { return; } $driver_name = $this->rc->config->get('tasklist_driver', 'database'); $driver_class = 'tasklist_' . $driver_name . '_driver'; require_once($this->home . '/drivers/tasklist_driver.php'); require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php'); $this->driver = new $driver_class($this); $this->rc->output->set_env('tasklist_driver', $driver_name); } /** * Dispatcher for task-related actions initiated by the client */ public function task_action() { $filter = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)); $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $rec = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true); $oldrec = $rec; $success = $refresh = $got_msg = false; // force notify if hidden + active $itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3); - if ($itip_send_option === 1 && empty($rec['_reportpartstat'])) + if ($itip_send_option === 1 && empty($rec['_reportpartstat'])) { $rec['_notify'] = 1; + } switch ($action) { case 'new': $oldrec = null; $rec = $this->prepare_task($rec); $rec['uid'] = $this->generate_uid(); - $temp_id = $rec['tempid']; + $temp_id = !empty($rec['tempid']) ? $rec['tempid'] : null; if ($success = $this->driver->create_task($rec)) { $refresh = $this->driver->get_task($rec); if ($temp_id) $refresh['tempid'] = $temp_id; $this->cleanup_task($rec); } break; case 'complete': $complete = intval(rcube_utils::get_input_value('complete', rcube_utils::INPUT_POST)); if (!($rec = $this->driver->get_task($rec))) { break; } $oldrec = $rec; $rec['status'] = $complete ? 'COMPLETED' : ($rec['complete'] > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION'); // sent itip notifications if enabled (no user interaction here) if (($itip_send_option & 1)) { if ($this->is_attendee($rec)) { $rec['_reportpartstat'] = $rec['status']; } else if ($this->is_organizer($rec)) { $rec['_notify'] = 1; } } case 'edit': $oldrec = $this->driver->get_task($rec); $rec = $this->prepare_task($rec); $clone = $this->handle_recurrence($rec, $this->driver->get_task($rec)); if ($success = $this->driver->edit_task($rec)) { $new_task = $this->driver->get_task($rec); $new_task['tempid'] = $rec['id']; $refresh[] = $new_task; $this->cleanup_task($rec); // add clone from recurring task if ($clone && $this->driver->create_task($clone)) { $new_clone = $this->driver->get_task($clone); $new_clone['tempid'] = $clone['id']; $refresh[] = $new_clone; $this->driver->clear_alarms($rec['id']); } // move all childs if list assignment was changed if (!empty($rec['_fromlist']) && !empty($rec['list']) && $rec['_fromlist'] != $rec['list']) { foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist']), true) as $cid) { $child = array('id' => $cid, 'list' => $rec['list'], '_fromlist' => $rec['_fromlist']); if ($this->driver->move_task($child)) { $r = $this->driver->get_task($child); if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) { $r['tempid'] = $cid; $refresh[] = $r; } } } } } break; case 'move': foreach ((array)$rec['id'] as $id) { $r = $rec; $r['id'] = $id; if ($this->driver->move_task($r)) { $new_task = $this->driver->get_task($r); $new_task['tempid'] = $id; $refresh[] = $new_task; $success = true; // move all childs, too foreach ($this->driver->get_childs(array('id' => $id, 'list' => $rec['_fromlist']), true) as $cid) { $child = $rec; $child['id'] = $cid; if ($this->driver->move_task($child)) { $r = $this->driver->get_task($child); if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) { $r['tempid'] = $cid; $refresh[] = $r; } } } } } break; case 'delete': $mode = intval(rcube_utils::get_input_value('mode', rcube_utils::INPUT_POST)); $oldrec = $this->driver->get_task($rec); if ($success = $this->driver->delete_task($rec, false)) { // delete/modify all childs foreach ($this->driver->get_childs($rec, $mode) as $cid) { $child = array('id' => $cid, 'list' => $rec['list']); if ($mode == 1) { // delete all childs if ($this->driver->delete_task($child, false)) { if ($this->driver->undelete) $_SESSION['tasklist_undelete'][$rec['id']][] = $cid; } else $success = false; } else { $child['parent_id'] = strval($oldrec['parent_id']); $this->driver->edit_task($child); } } // update parent task to adjust list of children if (!empty($oldrec['parent_id'])) { $parent = array('id' => $oldrec['parent_id'], 'list' => $rec['list']); if ($parent = $this->driver->get_task()) { $refresh[] = $parent; } } } if (!$success) $this->rc->output->command('plugin.reload_data'); break; case 'undelete': if ($success = $this->driver->undelete_task($rec)) { $refresh[] = $this->driver->get_task($rec); foreach ((array)$_SESSION['tasklist_undelete'][$rec['id']] as $cid) { if ($this->driver->undelete_task($rec)) { $refresh[] = $this->driver->get_task($rec); } } } break; case 'collapse': foreach (explode(',', $rec['id']) as $rec_id) { if (intval(rcube_utils::get_input_value('collapsed', rcube_utils::INPUT_GPC))) { $this->collapsed_tasks[] = $rec_id; } else { $i = array_search($rec_id, $this->collapsed_tasks); if ($i !== false) unset($this->collapsed_tasks[$i]); } } $this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks)))); return; // avoid further actions case 'rsvp': $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC); $noreply = intval(rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC)) || $status == 'needs-action'; $task = $this->driver->get_task($rec); $task['attendees'] = $rec['attendees']; $task['_type'] = 'task'; // send invitation to delegatee + add it as attendee if ($status == 'delegated' && $rec['to']) { $itip = $this->load_itip(); if ($itip->delegate_to($task, $rec['to'], (bool)$rec['rsvp'])) { $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation'); $refresh[] = $task; $noreply = false; } } $rec = $task; if ($success = $this->driver->edit_task($rec)) { if (!$noreply) { // let the reply clause further down send the iTip message $rec['_reportpartstat'] = $status; } } break; case 'changelog': $data = $this->driver->get_task_changelog($rec); if (is_array($data) && !empty($data)) { $lib = $this->lib; $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format'); array_walk($data, function(&$change) use ($lib, $dtformat) { if ($change['date']) { $dt = $lib->adjust_timezone($change['date']); if ($dt instanceof DateTime) { $change['date'] = $this->rc->format_date($dt, $dtformat, false); } } }); $this->rc->output->command('plugin.task_render_changelog', $data); } else { $this->rc->output->command('plugin.task_render_changelog', false); } $got_msg = true; break; case 'diff': $data = $this->driver->get_task_diff($rec, $rec['rev1'], $rec['rev2']); if (is_array($data)) { // convert some properties, similar to self::_client_event() $lib = $this->lib; $date_format = $this->rc->config->get('date_format', 'Y-m-d'); $time_format = $this->rc->config->get('time_format', 'H:i'); array_walk($data['changes'], function(&$change, $i) use ($lib, $date_format, $time_format) { // convert date cols if (in_array($change['property'], array('date','start','created','changed'))) { if (!empty($change['old'])) { $dtformat = strlen($change['old']) == 10 ? $date_format : $date_format . ' ' . $time_format; $change['old_'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format($dtformat); } if (!empty($change['new'])) { $dtformat = strlen($change['new']) == 10 ? $date_format : $date_format . ' ' . $time_format; $change['new_'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format($dtformat); } } // create textual representation for alarms and recurrence if ($change['property'] == 'alarms') { if (is_array($change['old'])) $change['old_'] = libcalendaring::alarm_text($change['old']); if (is_array($change['new'])) $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new'])); } if ($change['property'] == 'recurrence') { if (is_array($change['old'])) $change['old_'] = $lib->recurrence_text($change['old']); if (is_array($change['new'])) $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new'])); } if ($change['property'] == 'complete') { $change['old_'] = intval($change['old']) . '%'; $change['new_'] = intval($change['new']) . '%'; } if ($change['property'] == 'attachments') { if (is_array($change['old'])) $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']); if (is_array($change['new'])) { $change['new'] = array_merge((array)$change['old'], $change['new']); $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']); } } // resolve parent_id to the refered task title for display if ($change['property'] == 'parent_id') { $change['property'] = 'parent-title'; if (!empty($change['old']) && ($old_parent = $this->driver->get_task(array('id' => $change['old'], 'list' => $rec['list'])))) { $change['old_'] = $old_parent['title']; } if (!empty($change['new']) && ($new_parent = $this->driver->get_task(array('id' => $change['new'], 'list' => $rec['list'])))) { $change['new_'] = $new_parent['title']; } } // compute a nice diff of description texts if ($change['property'] == 'description') { $change['diff_'] = libkolab::html_diff($change['old'], $change['new']); } }); $this->rc->output->command('plugin.task_show_diff', $data); } else { $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error'); } $got_msg = true; break; case 'show': if ($rec = $this->driver->get_task_revison($rec, $rec['rev'])) { $this->encode_task($rec); $rec['readonly'] = 1; $this->rc->output->command('plugin.task_show_revision', $rec); } else { $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error'); } $got_msg = true; break; case 'restore': if ($success = $this->driver->restore_task_revision($rec, $rec['rev'])) { $refresh = $this->driver->get_task($rec); $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rec['rev']))), 'confirmation'); $this->rc->output->command('plugin.close_history_dialog'); } else { $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error'); } $got_msg = true; break; } if ($success) { $this->rc->output->show_message('successfullysaved', 'confirmation'); $this->update_counts($oldrec, $refresh); } else if (!$got_msg) { $this->rc->output->show_message('tasklist.errorsaving', 'error'); } // send out notifications - if ($success && $rec['_notify'] && ($rec['attendees'] || $oldrec['attendees'])) { + if ($success && !empty($rec['_notify']) && ($rec['attendees'] || $oldrec['attendees'])) { // make sure we have the complete record $task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec); // only notify if data really changed (TODO: do diff check on client already) if (!$oldrec || $action == 'delete' || self::task_diff($task, $oldrec)) { $sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']); if ($sent > 0) $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation'); else if ($sent < 0) $this->rc->output->show_message('tasklist.errornotifying', 'error'); } } - if ($success && $rec['_reportpartstat'] && $rec['_reportpartstat'] != 'NEEDS-ACTION') { + if ($success && !empty($rec['_reportpartstat']) && $rec['_reportpartstat'] != 'NEEDS-ACTION') { // get the full record after update if (!$task) { $task = $this->driver->get_task($rec); } // send iTip REPLY with the updated partstat if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) { $sender = $task['attendees'][$idx]; $status = strtolower($sender['status']); if (!empty($_POST['comment'])) $task['comment'] = rcube_utils::get_input_value('comment', rcube_utils::INPUT_POST); $itip = $this->load_itip(); $itip->set_sender_email($sender['email']); if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } } // unlock client $this->rc->output->command('plugin.unlock_saving', $success); if ($refresh) { - if ($refresh['id']) { + if (!empty($refresh['id'])) { $this->encode_task($refresh); } else if (is_array($refresh)) { foreach ($refresh as $i => $r) $this->encode_task($refresh[$i]); } $this->rc->output->command('plugin.update_task', $refresh); } else if ($success && ($action == 'delete' || $action == 'undelete')) { $this->rc->output->command('plugin.refresh_tagcloud'); } } /** * Load iTIP functions */ private function load_itip() { - if (!$this->itip) { - require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php'); + if (empty($this->itip)) { + require_once __DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php'; $this->itip = new libcalendaring_itip($this, 'tasklist'); $this->itip->set_rsvp_actions(array('accepted','declined','delegated')); $this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed')); } return $this->itip; } /** * repares new/edited task properties before save */ private function prepare_task($rec) { // try to be smart and extract date from raw input - if ($rec['raw']) { + if (!empty($rec['raw'])) { foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) { $locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i'; $normwords[] = $word; $datewords[] = $word; } foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) { $locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i'; $normwords[] = $month; $datewords[] = $month; } foreach (array('on','this','next','at') as $word) { $fillwords[] = preg_quote(mb_strtolower($this->gettext($word))); $fillwords[] = $word; } $raw = trim($rec['raw']); $date_str = ''; // translate localized keywords $raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw); $raw = preg_replace($locwords, $normwords, $raw); // find date pattern $date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i'; if (preg_match($date_pattern, $raw, $m)) { $date_str .= $m[1] . $m[2] . $m[3]; $raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw); // add year to date string if ($m[1] && !$m[3]) $date_str .= date('Y'); } // find time pattern $time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i'; if (preg_match($time_pattern, $raw, $m)) { $has_time = true; $date_str .= ($date_str ? ' ' : 'today ') . $m[1]; $raw = preg_replace($time_pattern, '', $raw); } // yes, raw input matched a (valid) date if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) { $rec['date'] = $date->format('Y-m-d'); if ($has_time) $rec['time'] = $date->format('H:i'); $rec['title'] = $raw; } else $rec['title'] = $rec['raw']; } // normalize input from client if (isset($rec['complete'])) { $rec['complete'] = floatval($rec['complete']); if ($rec['complete'] > 1) $rec['complete'] /= 100; } if (isset($rec['flagged'])) $rec['flagged'] = intval($rec['flagged']); // fix for garbage input if ($rec['description'] == 'null') $rec['description'] = ''; foreach ($rec as $key => $val) { if ($val === 'null') $rec[$key] = null; } if (!empty($rec['date'])) { $this->normalize_dates($rec, 'date', 'time'); } if (!empty($rec['startdate'])) { $this->normalize_dates($rec, 'startdate', 'starttime'); } // convert tags to array, filter out empty entries if (isset($rec['tags']) && !is_array($rec['tags'])) { $rec['tags'] = array_filter((array)$rec['tags']); } // convert the submitted alarm values - if ($rec['valarms']) { + if (!empty($rec['valarms'])) { $valarms = array(); foreach (libcalendaring::from_client_alarms($rec['valarms']) as $alarm) { // alarms can only work with a date (either task start, due or absolute alarm date) if (is_a($alarm['trigger'], 'DateTime') || $rec['date'] || $rec['startdate']) $valarms[] = $alarm; } $rec['valarms'] = $valarms; } // convert the submitted recurrence settings if (is_array($rec['recurrence'])) { $refdate = null; if (!empty($rec['date'])) { $refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone); } else if (!empty($rec['startdate'])) { $refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone); } if ($refdate) { $rec['recurrence'] = $this->lib->from_client_recurrence($rec['recurrence'], $refdate); // translate count into an absolute end date. // why? because when shifting completed tasks to the next recurrence, // the initial start date to count from gets lost. - if ($rec['recurrence']['COUNT']) { + if (!empty($rec['recurrence']['COUNT'])) { $engine = libcalendaring::get_recurrence(); $engine->init($rec['recurrence'], $refdate); if ($until = $engine->end()) { $rec['recurrence']['UNTIL'] = $until; unset($rec['recurrence']['COUNT']); } } } else { // recurrence requires a reference date $rec['recurrence'] = ''; } } $attachments = array(); $taskid = $rec['id']; - if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) { + if (!empty($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) { if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) { foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) { if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) { $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment); unset($attachments[$id]['abort'], $attachments[$id]['group']); } } } } $rec['attachments'] = $attachments; // convert link references into simple URIs if (array_key_exists('links', $rec)) { $rec['links'] = array_map(function($link) { return is_array($link) ? $link['uri'] : strval($link); }, (array)$rec['links']); } // convert invalid data - if (isset($rec['attendees']) && !is_array($rec['attendees'])) + if (isset($rec['attendees']) && !is_array($rec['attendees'])) { $rec['attendees'] = array(); + } - foreach ((array)$rec['attendees'] as $i => $attendee) { - if (is_string($attendee['rsvp'])) { - $rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1'; + if (!empty($rec['attendees'])) { + foreach ((array) $rec['attendees'] as $i => $attendee) { + if (is_string($attendee['rsvp'])) { + $rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1'; + } } } // copy the task status to my attendee partstat if (!empty($rec['_reportpartstat'])) { if (($idx = $this->is_attendee($rec)) !== false) { if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED')) $rec['attendees'][$idx]['status'] = $rec['_reportpartstat']; else unset($rec['_reportpartstat']); } } // set organizer from identity selector if ((isset($rec['_identity']) || (!empty($rec['attendees']) && empty($rec['organizer']))) && ($identity = $this->rc->user->get_identity($rec['_identity']))) { $rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']); } if (is_numeric($rec['id']) && $rec['id'] < 0) unset($rec['id']); return $rec; } /** * Utility method to convert a tasks date/time values into a normalized format */ private function normalize_dates(&$rec, $date_key, $time_key) { try { // parse date from user format (#2801) $date_format = $this->rc->config->get(empty($rec[$time_key]) ? 'date_format' : 'date_long', 'Y-m-d'); $date = DateTime::createFromFormat($date_format, trim($rec[$date_key] . ' ' . $rec[$time_key]), $this->timezone); // fall back to default strtotime logic if (empty($date)) { $date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone); } $rec[$date_key] = $date->format('Y-m-d'); if (!empty($rec[$time_key])) $rec[$time_key] = $date->format('H:i'); return true; } catch (Exception $e) { $rec[$date_key] = $rec[$time_key] = null; } return false; } /** * Releases some resources after successful save */ private function cleanup_task(&$rec) { // remove temp. attachment files if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) { $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid)); $this->rc->session->remove(self::SESSION_KEY); } } /** * When flagging a recurring task as complete, * clone it and shift dates to the next occurrence */ private function handle_recurrence(&$rec, $old) { $clone = null; if ($this->driver->is_complete($rec) && $old && !$this->driver->is_complete($old) && is_array($rec['recurrence'])) { $engine = libcalendaring::get_recurrence(); $rrule = $rec['recurrence']; $updates = array(); // compute the next occurrence of date attributes foreach (array('date'=>'time', 'startdate'=>'starttime') as $date_key => $time_key) { if (empty($rec[$date_key])) continue; $date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone); $engine->init($rrule, $date); if ($next = $engine->next()) { $updates[$date_key] = $next->format('Y-m-d'); if (!empty($rec[$time_key])) $updates[$time_key] = $next->format('H:i'); } } // shift absolute alarm dates if (!empty($updates) && is_array($rec['valarms'])) { $updates['valarms'] = array(); unset($rrule['UNTIL'], $rrule['COUNT']); // make recurrence rule unlimited foreach ($rec['valarms'] as $i => $alarm) { if ($alarm['trigger'] instanceof DateTime) { $engine->init($rrule, $alarm['trigger']); if ($next = $engine->next()) { $alarm['trigger'] = $next; } } $updates['valarms'][$i] = $alarm; } } if (!empty($updates)) { // clone task to save a completed copy $clone = $rec; $clone['uid'] = $this->generate_uid(); $clone['parent_id'] = $rec['id']; unset($clone['id'], $clone['recurrence'], $clone['attachments']); // update the task but unset completed flag $rec = array_merge($rec, $updates); $rec['complete'] = $old['complete']; $rec['status'] = $old['status']; } } return $clone; } /** * Send out an invitation/notification to all task attendees */ private function notify_attendees($task, $old, $action = 'edit', $comment = null) { if ($action == 'delete' || ($task['status'] == 'CANCELLED' && $old['status'] != $task['status'])) { $task['cancelled'] = true; $is_cancelled = true; } $itip = $this->load_itip(); $emails = $this->lib->get_user_emails(); $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3); // add comment to the iTip attachment $task['comment'] = $comment; // needed to generate VTODO instead of VEVENT entry $task['_type'] = 'task'; // compose multipart message using PEAR:Mail_Mime $method = $action == 'delete' ? 'CANCEL' : 'REQUEST'; $object = $this->to_libcal($task); $message = $itip->compose_itip_message($object, $method, $task['sequence'] > $old['sequence']); // list existing attendees from the $old task $old_attendees = array(); foreach ((array)$old['attendees'] as $attendee) { $old_attendees[] = $attendee['email']; } // send to every attendee $sent = 0; $current = array(); foreach ((array)$task['attendees'] as $attendee) { $current[] = strtolower($attendee['email']); // skip myself for obvious reasons if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) { continue; } // skip if notification is disabled for this attendee if ($attendee['noreply'] && $itip_notify & 2) { continue; } // skip if this attendee has delegated and set RSVP=FALSE if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) { continue; } // which template to use for mail text $is_new = !in_array($attendee['email'], $old_attendees); $is_rsvp = $is_new || $task['sequence'] > $old['sequence']; $bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody'); $subject = $is_cancelled ? 'itipcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty')); // finally send the message if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message, $is_rsvp)) $sent++; else $sent = -100; } // send CANCEL message to removed attendees foreach ((array)$old['attendees'] as $attendee) { if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) { continue; } $vtodo = $this->to_libcal($old); $vtodo['cancelled'] = $is_cancelled; $vtodo['attendees'] = array($attendee); $vtodo['comment'] = $comment; if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody')) $sent++; else $sent = -100; } return $sent; } /** * Compare two task objects and return differing properties * * @param array Event A * @param array Event B * @return array List of differing task properties */ public static function task_diff($a, $b) { $diff = array(); $ignore = array('changed' => 1, 'attachments' => 1); foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) { if (!$ignore[$key] && $a[$key] != $b[$key]) $diff[] = $key; } // only compare number of attachments if (count($a['attachments']) != count($b['attachments'])) $diff[] = 'attachments'; return $diff; } /** * Dispatcher for tasklist actions initiated by the client */ public function tasklist_action() { $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $list = rcube_utils::get_input_value('l', rcube_utils::INPUT_GPC, true); $success = false; unset($list['_token']); if (isset($list['showalarms'])) { $list['showalarms'] = intval($list['showalarms']); } switch ($action) { case 'form-new': case 'form-edit': $this->load_ui(); echo $this->ui->tasklist_editform($action, $list); exit; case 'new': $list += array('showalarms' => true, 'active' => true, 'editable' => true); if ($insert_id = $this->driver->create_list($list)) { $list['id'] = $insert_id; - if (!$list['_reload']) { + if (empty($list['_reload'])) { $this->load_ui(); $list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv); $list += (array)$jsenv[$insert_id]; } $this->rc->output->command('plugin.insert_tasklist', $list); $success = true; } break; case 'edit': if ($newid = $this->driver->edit_list($list)) { $list['oldid'] = $list['id']; $list['id'] = $newid; $this->rc->output->command('plugin.update_tasklist', $list); $success = true; } break; case 'subscribe': $success = $this->driver->subscribe_list($list); break; case 'delete': if (($success = $this->driver->delete_list($list))) $this->rc->output->command('plugin.destroy_tasklist', $list); break; case 'search': $this->load_ui(); $results = array(); $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); foreach ((array)$this->driver->search_lists($query, $source) as $id => $prop) { $editname = $prop['editname']; unset($prop['editname']); // force full name to be displayed $prop['active'] = false; // let the UI generate HTML and CSS representation for this calendar $html = $this->ui->tasklist_list_item($id, $prop, $jsenv); $prop += (array)$jsenv[$id]; $prop['editname'] = $editname; $prop['html'] = $html; $results[] = $prop; } // report more results available - if ($this->driver->search_more_results) { + if (!empty($this->driver->search_more_results)) { $this->rc->output->show_message('autocompletemore', 'notice'); } $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC)); return; } - if ($success) + if ($success) { $this->rc->output->show_message('successfullysaved', 'confirmation'); - else + } + else { $this->rc->output->show_message('tasklist.errorsaving', 'error'); + } $this->rc->output->command('plugin.unlock_saving'); } /** * Get counts for active tasks divided into different selectors */ public function fetch_counts() { if (isset($_REQUEST['lists'])) { $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC); } else { foreach ($this->driver->get_lists() as $list) { - if ($list['active']) + if (!empty($list['active'])) { $lists[] = $list['id']; + } } } $counts = $this->driver->count_tasks($lists); $this->rc->output->command('plugin.update_counts', $counts); } /** * Adjust the cached counts after changing a task */ public function update_counts($oldrec, $newrec) { // rebuild counts until this function is finally implemented $this->fetch_counts(); // $this->rc->output->command('plugin.update_counts', $counts); } /** * */ public function fetch_tasks() { $mask = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)); $search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC); $filter = array('mask' => $mask, 'search' => $search); $data = $this->tasks_data($this->driver->list_tasks($filter, $lists)); $this->rc->output->command('plugin.data_ready', array( 'filter' => $mask, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => $this->driver->get_tags(), )); } /** * Handler for printing calendars */ public function print_tasks() { // Add CSS stylesheets to the page header $skin_path = $this->local_skin_path(); $this->include_stylesheet($skin_path . '/print.css'); $this->include_script('tasklist.js'); $this->rc->output->add_handlers(array( 'plugin.tasklist_print' => array($this, 'print_tasks_list'), )); $this->rc->output->set_pagetitle($this->gettext('print')); $this->rc->output->send('tasklist.print'); } /** * Handler for printing calendars */ public function print_tasks_list($attrib) { $mask = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)); $search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC); $filter = array('mask' => $mask, 'search' => $search); $data = $this->tasks_data($this->driver->list_tasks($filter, $lists)); // we'll build the tasks table in javascript on page load // where we have sorting methods, etc. $this->rc->output->set_env('tasks', $data); $this->rc->output->set_env('filtermask', $mask); return $this->ui->tasks_resultview($attrib); } /** * Prepare and sort the given task records to be sent to the client */ private function tasks_data($records) { $data = $this->task_tree = $this->task_titles = array(); foreach ($records as $rec) { - if ($rec['parent_id']) { + if (!empty($rec['parent_id'])) { $this->task_tree[$rec['id']] = $rec['parent_id']; } $this->encode_task($rec); $data[] = $rec; } // assign hierarchy level indicators for later sorting array_walk($data, array($this, 'task_walk_tree')); return $data; } /** * Prepare the given task record before sending it to the client */ private function encode_task(&$rec) { $rec['mask'] = $this->filter_mask($rec); $rec['flagged'] = intval($rec['flagged']); $rec['complete'] = floatval($rec['complete']); if (is_object($rec['created'])) { $rec['created_'] = $this->rc->format_date($rec['created']); $rec['created'] = $rec['created']->format('U'); } if (is_object($rec['changed'])) { $rec['changed_'] = $this->rc->format_date($rec['changed']); $rec['changed'] = $rec['changed']->format('U'); } else { $rec['changed'] = null; } if ($rec['date']) { try { $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone); $rec['datetime'] = intval($date->format('U')); $rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d')); $rec['_hasdate'] = 1; } catch (Exception $e) { $rec['date'] = $rec['datetime'] = null; } } else { $rec['date'] = $rec['datetime'] = null; $rec['_hasdate'] = 0; } if ($rec['startdate']) { try { $date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone); $rec['startdatetime'] = intval($date->format('U')); $rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d')); } catch (Exception $e) { $rec['startdate'] = $rec['startdatetime'] = null; } } - if ($rec['valarms']) { + if (!empty($rec['valarms'])) { $rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']); $rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']); } - if ($rec['recurrence']) { + if (!empty($rec['recurrence'])) { $rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']); $rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']); } - foreach ((array)$rec['attachments'] as $k => $attachment) { - $rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); + if (!empty($rec['attachments'])) { + foreach ((array) $rec['attachments'] as $k => $attachment) { + $rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); + } } // convert link URIs references into structs if (array_key_exists('links', $rec)) { foreach ((array) $rec['links'] as $i => $link) { if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link, 'task'))) { $rec['links'][$i] = $msgref; } } } // Convert HTML description into plain text if ($this->is_html($rec)) { $h2t = new rcube_html2text($rec['description'], false, true, 0); $rec['description'] = $h2t->get_text(); } if (!is_array($rec['tags'])) $rec['tags'] = (array)$rec['tags']; sort($rec['tags'], SORT_LOCALE_STRING); if (in_array($rec['id'], $this->collapsed_tasks)) $rec['collapsed'] = true; if (empty($rec['parent_id'])) $rec['parent_id'] = null; $this->task_titles[$rec['id']] = $rec['title']; } /** * Determine whether the given task description is HTML formatted */ private function is_html($task) { // check for opening and closing or tags return (preg_match('/<(html|body)(\s+[a-z]|>)/', $task['description'], $m) && strpos($task['description'], '') > 0); } /** * Callback function for array_walk over all tasks. * Sets tree depth and parent titles */ private function task_walk_tree(&$rec) { $rec['_depth'] = 0; $parent_titles = array(); - $parent_id = $this->task_tree[$rec['id']]; + $parent_id = isset($this->task_tree[$rec['id']]) ? $this->task_tree[$rec['id']] : null; while ($parent_id) { $rec['_depth']++; - array_unshift($parent_titles, $this->task_titles[$parent_id]); - $parent_id = $this->task_tree[$parent_id]; + if (isset($this->task_titles[$parent_id])) { + array_unshift($parent_titles, $this->task_titles[$parent_id]); + } + $parent_id = isset($this->task_tree[$parent_id]) ? $this->task_tree[$parent_id] : null; } if (count($parent_titles)) { $rec['parent_title'] = join(' » ', array_filter($parent_titles)); } } /** * Compute the filter mask of the given task * * @param array Hash array with Task record properties * @return int Filter mask */ public function filter_mask($rec) { static $today, $today_date, $tomorrow, $weeklimit; if (!$today) { $today_date = new DateTime('now', $this->timezone); $today = $today_date->format('Y-m-d'); $tomorrow_date = new DateTime('now + 1 day', $this->timezone); $tomorrow = $tomorrow_date->format('Y-m-d'); // In Kolab-mode we hide "Next 7 days" filter, which means // "Later" should catch tasks with date after tomorrow (#5353) if ($this->rc->output->get_env('tasklist_driver') == 'kolab') { $weeklimit = $tomorrow; } else { $week_date = new DateTime('now + 7 days', $this->timezone); $weeklimit = $week_date->format('Y-m-d'); } } $mask = 0; $start = $rec['startdate'] ?: '1900-00-00'; $duedate = $rec['date'] ?: '3000-00-00'; if ($rec['flagged']) $mask |= self::FILTER_MASK_FLAGGED; if ($this->driver->is_complete($rec)) $mask |= self::FILTER_MASK_COMPLETE; if (empty($rec['date'])) $mask |= self::FILTER_MASK_NODATE; else if ($rec['date'] < $today) $mask |= self::FILTER_MASK_OVERDUE; if (empty($rec['recurrence']) || $duedate < $today || $start > $weeklimit) { if ($duedate <= $today || ($rec['startdate'] && $start <= $today)) $mask |= self::FILTER_MASK_TODAY; else if (($start > $today && $start <= $tomorrow) || ($duedate > $today && $duedate <= $tomorrow)) $mask |= self::FILTER_MASK_TOMORROW; else if (($start > $tomorrow && $start <= $weeklimit) || ($duedate > $tomorrow && $duedate <= $weeklimit)) $mask |= self::FILTER_MASK_WEEK; else if ($start > $weeklimit || $duedate > $weeklimit) $mask |= self::FILTER_MASK_LATER; } else if ($rec['startdate'] || $rec['date']) { $date = new DateTime($rec['startdate'] ?: $rec['date'], $this->timezone); // set safe recurrence start while ($date->format('Y-m-d') >= $today) { switch ($rec['recurrence']['FREQ']) { case 'DAILY': $date = clone $today_date; $date->sub(new DateInterval('P1D')); break; case 'WEEKLY': $date->sub(new DateInterval('P7D')); break; case 'MONTHLY': $date->sub(new DateInterval('P1M')); break; case 'YEARLY': $date->sub(new DateInterval('P1Y')); break; default; break 2; } } $date->_dateonly = true; $engine = libcalendaring::get_recurrence(); $engine->init($rec['recurrence'], $date); // check task occurrences (stop next week) // FIXME: is there a faster way of doing this? while ($date = $engine->next()) { $date = $date->format('Y-m-d'); // break iteration asap if ($date > $duedate || ($mask & self::FILTER_MASK_LATER)) { break; } if ($date == $today) { $mask |= self::FILTER_MASK_TODAY; } else if ($date == $tomorrow) { $mask |= self::FILTER_MASK_TOMORROW; } else if ($date > $tomorrow && $date <= $weeklimit) { $mask |= self::FILTER_MASK_WEEK; } else if ($date > $weeklimit) { $mask |= self::FILTER_MASK_LATER; break; } } } // add masks for assigned tasks if ($this->is_organizer($rec) && !empty($rec['attendees']) && $this->is_attendee($rec) === false) $mask |= self::FILTER_MASK_ASSIGNED; else if (/*empty($rec['attendees']) ||*/ $this->is_attendee($rec) !== false) $mask |= self::FILTER_MASK_MYTASKS; return $mask; } /** * Determine whether the current user is an attendee of the given task */ public function is_attendee($task) { $emails = $this->lib->get_user_emails(); foreach ((array)$task['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { return $i; } } return false; } /** * Determine whether the current user is the organizer of the given task */ public function is_organizer($task) { $emails = $this->lib->get_user_emails(); return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails)); } /******* UI functions ********/ /** * Render main view of the tasklist task */ public function tasklist_view() { $this->ui->init(); $this->ui->init_templates(); // set autocompletion env $this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0)); $this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15)); $this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length')); $this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'close'); $this->rc->output->set_pagetitle($this->gettext('navtitle')); $this->rc->output->send('tasklist.mainview'); } /** * Handler for keep-alive requests * This will check for updated data in active lists and sync them to the client */ public function refresh($attr) { // refresh the entire list every 10th time to also sync deleted items if (rand(0,10) == 10) { $this->rc->output->command('plugin.reload_data'); return; } $filter = array( 'since' => $attr['last'], 'search' => rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC), 'mask' => intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)) & self::FILTER_MASK_COMPLETE, ); $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);; $updates = $this->driver->list_tasks($filter, $lists); if (!empty($updates)) { $this->rc->output->command('plugin.refresh_tasks', $this->tasks_data($updates), true); // update counts $counts = $this->driver->count_tasks($lists); $this->rc->output->command('plugin.update_counts', $counts); } } /** * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. * This will check for pending notifications and pass them to the client */ public function pending_alarms($p) { $this->load_driver(); if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) { foreach ($alarms as $alarm) { // encode alarm object to suit the expectations of the calendaring code if ($alarm['date']) $alarm['start'] = new DateTime($alarm['date'].' '.$alarm['time'], $this->timezone); $alarm['id'] = 'task:' . $alarm['id']; // prefix ID with task: $alarm['allday'] = empty($alarm['time']) ? 1 : 0; $p['alarms'][] = $alarm; } } return $p; } /** * Handler for alarm dismiss hook triggered by the calendar module */ public function dismiss_alarms($p) { $this->load_driver(); foreach ((array)$p['ids'] as $id) { if (strpos($id, 'task:') === 0) $p['success'] |= $this->driver->dismiss_alarm(substr($id, 5), $p['snooze']); } return $p; } /** * Handler for importing .ics files */ function import_tasks() { // Upload progress update if (!empty($_GET['_progress'])) { $this->rc->upload_progress(); } @set_time_limit(0); // process uploaded file if there is no error $err = $_FILES['_data']['error']; if (!$err && $_FILES['_data']['tmp_name']) { $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); $lists = $this->driver->get_lists(); $list = $lists[$source] ?: $this->get_default_tasklist(); $source = $list['id']; // extract zip file if ($_FILES['_data']['type'] == 'application/zip') { $count = 0; if (class_exists('ZipArchive', false)) { $zip = new ZipArchive(); if ($zip->open($_FILES['_data']['tmp_name'])) { $randname = uniqid('zip-' . session_id(), true); $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname; mkdir($tmpdir, 0700); // extract each ical file from the archive and import it for ($i = 0; $i < $zip->numFiles; $i++) { $filename = $zip->getNameIndex($i); if (preg_match('/\.ics$/i', $filename)) { $tmpfile = $tmpdir . '/' . basename($filename); if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) { $count += $this->import_from_file($tmpfile, $source, $errors); unlink($tmpfile); } } } rmdir($tmpdir); $zip->close(); } else { $errors = 1; $msg = 'Failed to open zip file.'; } } else { $errors = 1; $msg = 'Zip files are not supported for import.'; } } else { // attempt to import the uploaded file directly $count = $this->import_from_file($_FILES['_data']['tmp_name'], $source, $errors); } if ($count) { $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation'); $this->rc->output->command('plugin.import_success', array('source' => $source, 'refetch' => true)); } else if (!$errors) { $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice'); $this->rc->output->command('plugin.import_success', array('source' => $source)); } else { $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : ''))); } } else { if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array( 'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); } else { $msg = $this->rc->gettext('fileuploaderror'); } $this->rc->output->command('plugin.import_error', array('message' => $msg)); } $this->rc->output->send('iframe'); } /** * Helper function to parse and import a single .ics file */ private function import_from_file($filepath, $source, &$errors) { $user_email = $this->rc->user->get_username(); $ical = $this->get_ical(); $errors = !$ical->fopen($filepath); $count = $i = 0; foreach ($ical as $task) { // keep the browser connection alive on long import jobs if (++$i > 100 && $i % 100 == 0) { echo ""; ob_flush(); } if ($task['_type'] == 'task') { $task['list'] = $source; if ($this->driver->create_task($task)) { $count++; } else { $errors++; } } } return $count; } /** * Construct the ics file for exporting tasks to iCalendar format */ function export_tasks() { $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); $task_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GPC); $attachments = (bool) rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GPC); $this->load_driver(); $browser = new rcube_browser; $lists = $this->driver->get_lists(); $tasks = array(); $filter = array(); // get message UIDs for filter if ($source && ($list = $lists[$source])) { $filename = html_entity_decode($list['name']) ?: $sorce; $filter = array($source => true); } else if ($task_id) { $filename = 'tasks'; foreach (explode(',', $task_id) as $id) { list($list_id, $task_id) = explode(':', $id, 2); if ($list_id && $task_id) { $filter[$list_id][] = $task_id; } } } // Get tasks foreach ($filter as $list_id => $uids) { $_filter = is_array($uids) ? array('uid' => $uids) : null; $_tasks = $this->driver->list_tasks($_filter, $list_id); if (!empty($_tasks)) { $tasks = array_merge($tasks, $_tasks); } } // Set file name if ($source && count($tasks) == 1) { $filename = $tasks[0]['title'] ?: 'task'; } $filename .= '.ics'; $filename = $browser->ie ? rawurlencode($filename) : addcslashes($filename, '"'); $tasks = array_map(array($this, 'to_libcal'), $tasks); // Give plugins a possibility to implement other output formats or modify the result $plugin = $this->rc->plugins->exec_hook('tasks_export', array( 'result' => $tasks, 'attachments' => $attachments, 'filename' => $filename, 'plugin' => $this, )); if ($plugin['abort']) { exit; } $this->rc->output->nocacheing_headers(); // don't kill the connection if download takes more than 30 sec. @set_time_limit(0); header("Content-Type: text/calendar"); header("Content-Disposition: inline; filename=\"". $plugin['filename'] ."\""); $this->get_ical()->export($plugin['result'], '', true, - $plugins['attachments'] ? array($this->driver, 'get_attachment_body') : null); + !empty($plugin['attachments']) ? array($this->driver, 'get_attachment_body') : null); exit; } /******* Attachment handling *******/ /** * Handler for attachments upload */ public function attachment_upload() { $handler = new kolab_attachments_handler(); $handler->attachment_upload(self::SESSION_KEY); } /** * Handler for attachments download/displaying */ public function attachment_get() { $handler = new kolab_attachments_handler(); // show loading page if (!empty($_GET['_preload'])) { return $handler->attachment_loading_page(); } $task = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC); $list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC); $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC); $task = array('id' => $task, 'list' => $list, 'rev' => $rev); $attachment = $this->driver->get_attachment($id, $task); // show part page if (!empty($_GET['_frame'])) { $handler->attachment_page($attachment); } // deliver attachment content else if ($attachment) { $attachment['body'] = $this->driver->get_attachment_body($id, $task); $handler->attachment_get($attachment); } // if we arrive here, the requested part was not found header('HTTP/1.1 404 Not Found'); exit; } /******* Email related function *******/ public function mail_message2task() { $this->load_ui(); $this->ui->init(); $this->ui->init_templates(); $this->ui->tasklists(); $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET); $task = array(); $imap = $this->rc->get_storage(); $message = new rcube_message($uid, $mbox); if ($message->headers) { $task['title'] = trim($message->subject); $task['description'] = trim($message->first_text_part()); $task['id'] = -$uid; $this->load_driver(); // add a reference to the email message if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) { $task['links'] = array($msgref); } // copy mail attachments to task else if ($message->attachments && $this->driver->attachments) { if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) { $_SESSION[self::SESSION_KEY] = array( 'id' => $task['id'], 'attachments' => array(), ); } foreach ((array)$message->attachments as $part) { $attachment = array( 'data' => $imap->get_message_part($uid, $part->mime_id, $part), 'size' => $part->size, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'group' => $task['id'], ); $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment); if ($attachment['status'] && !$attachment['abort']) { $id = $attachment['id']; $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); // store new attachment in session unset($attachment['status'], $attachment['abort'], $attachment['data']); $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment; $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new' $task['attachments'][] = $attachment; } } } $this->rc->output->set_env('task_prop', $task); } else { $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error'); } $this->rc->output->send('tasklist.dialog'); } /** * Add UI element to copy task invitations or updates to the tasklist */ public function mail_messagebody_html($p) { // load iCalendar functions (if necessary) if (!empty($this->lib->ical_parts)) { $this->get_ical(); $this->load_itip(); } $html = ''; $has_tasks = false; $ical_objects = $this->lib->get_mail_ical_objects(); // show a box for every task in the file foreach ($ical_objects as $idx => $task) { if ($task['_type'] != 'task') { continue; } $has_tasks = true; // get prepared inline UI for this event object if ($ical_objects->method) { $html .= html::div('tasklist-invitebox invitebox boxinformation', $this->itip->mail_itip_inline_ui( $task, $ical_objects->method, $ical_objects->mime_id . ':' . $idx, 'tasks', rcube_utils::anytodatetime($ical_objects->message_date) ) ); } // limit listing if ($idx >= 3) { break; } } // list linked tasks $links = array(); foreach ($this->message_tasks as $task) { $checkbox = new html_checkbox(array( 'name' => 'completed', 'class' => 'complete pretty-checkbox', 'title' => $this->gettext('complete'), 'data-list' => $task['list'], )); $complete = $this->driver->is_complete($task); $links[] = html::tag('li', 'messagetaskref' . ($complete ? ' complete' : ''), $checkbox->show($complete ? $task['id'] : null, array('value' => $task['id'])) . ' ' . html::a(array( 'href' => $this->rc->url(array( 'task' => 'tasks', 'list' => $task['list'], 'id' => $task['id'], )), 'class' => 'messagetasklink', 'rel' => $task['id'] . '@' . $task['list'], 'target' => '_blank', ), rcube::Q($task['title'])) ); } if (count($links)) { $html .= html::div('messagetasklinks boxinformation', html::tag('ul', 'tasklist', join("\n", $links))); } // prepend iTip/relation boxes to message body if ($html) { $this->load_ui(); $this->ui->init(); $p['content'] = $html . $p['content']; $this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm'); } // add "Save to tasks" button into attachment menu if ($has_tasks) { $this->add_button(array( 'id' => 'attachmentsavetask', 'name' => 'attachmentsavetask', 'type' => 'link', 'wrapper' => 'li', 'command' => 'attachment-save-task', 'class' => 'icon tasklistlink disabled', 'classact' => 'icon tasklistlink active', 'innerclass' => 'icon taskadd', 'label' => 'tasklist.savetotasklist', ), 'attachmentmenu'); } return $p; } /** * Lookup backend storage and find notes associated with the given message */ public function mail_message_load($p) { - if (!$p['object']->headers->others['x-kolab-type']) { + if (empty($p['object']->headers->others['x-kolab-type'])) { $this->load_driver(); $this->message_tasks = $this->driver->get_message_related_tasks($p['object']->headers, $p['object']->folder); // sort message tasks by completeness and due date $driver = $this->driver; array_walk($this->message_tasks, array($this, 'encode_task')); usort($this->message_tasks, function($a, $b) use ($driver) { $a_complete = intval($driver->is_complete($a)); $b_complete = intval($driver->is_complete($b)); $d = $a_complete - $b_complete; if (!$d) $d = $b['_hasdate'] - $a['_hasdate']; if (!$d) $d = $a['datetime'] - $b['datetime']; return $d; }); } } /** * Load iCalendar functions */ public function get_ical() { - if (!$this->ical) { + if (empty($this->ical)) { $this->ical = libcalendaring::get_ical(); } return $this->ical; } /** * Get properties of the tasklist this user has specified as default */ public function get_default_tasklist($sensitivity = null, $lists = null) { if ($lists === null) { $lists = $this->driver->get_lists(tasklist_driver::FILTER_PERSONAL | tasklist_driver::FILTER_WRITEABLE); } $list = null; foreach ($lists as $l) { if ($sensitivity && $l['subtype'] == $sensitivity) { $list = $l; break; } if ($l['default']) { $list = $l; } if ($l['editable']) { $first = $l; } } return $list ?: $first; } /** * Import the full payload from a mail message attachment */ public function mail_import_attachment() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); $charset = RCUBE_CHARSET; // establish imap connection $imap = $this->rc->get_storage(); $imap->set_folder($mbox); if ($uid && $mime_id) { $part = $imap->get_message_part($uid, $mime_id); // $headers = $imap->get_message_headers($uid); if ($part->ctype_parameters['charset']) { $charset = $part->ctype_parameters['charset']; } if ($part) { $tasks = $this->get_ical()->import($part, $charset); } } $success = $existing = 0; if (!empty($tasks)) { // find writeable tasklist to store task $cal_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null; $lists = $this->driver->get_lists(); foreach ($tasks as $task) { // save to tasklist $list = $lists[$cal_id] ?: $this->get_default_tasklist($task['sensitivity']); if ($list && $list['editable'] && $task['_type'] == 'task') { $task = $this->from_ical($task); $task['list'] = $list['id']; if (!$this->driver->get_task($task['uid'])) { $success += (bool) $this->driver->create_task($task); } else { $existing++; } } } } if ($success) { $this->rc->output->command('display_message', $this->gettext(array( 'name' => 'importsuccess', 'vars' => array('nr' => $success), )), 'confirmation'); } else if ($existing) { $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning'); } else { $this->rc->output->command('display_message', $this->gettext('errorimportingtask'), 'error'); } } /** * Handler for POST request to import an event attached to a mail message */ public function mail_import_itip() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); $status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST); $comment = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); $delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST)); $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)) || $status == 'needs-action'; $error_msg = $this->gettext('errorimportingtask'); $success = false; if ($status == 'delegated') { $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false); $delegate = reset($delegates); if (empty($delegate) || empty($delegate['mailto'])) { $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error'); return; } } // successfully parsed tasks? if ($task = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'task')) { $task = $this->from_ical($task); // forward iTip request to delegatee if ($delegate) { $rsvpme = rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST); $itip = $this->load_itip(); $task['comment'] = $comment; if ($itip->delegate_to($task, $delegate, !empty($rsvpme))) { $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation'); } else { $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } unset($task['comment']); } $mode = tasklist_driver::FILTER_PERSONAL | tasklist_driver::FILTER_SHARED | tasklist_driver::FILTER_WRITEABLE; // find writeable list to store the task $list_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null; $lists = $this->driver->get_lists($mode); $list = $lists[$list_id]; $dontsave = ($_REQUEST['_folder'] === '' && $task['_method'] == 'REQUEST'); // select default list except user explicitly selected 'none' if (!$list && !$dontsave) { $list = $this->get_default_tasklist($task['sensitivity'], $lists); } $metadata = array( 'uid' => $task['uid'], 'changed' => is_object($task['changed']) ? $task['changed']->format('U') : 0, 'sequence' => intval($task['sequence']), 'fallback' => strtoupper($status), 'method' => $task['_method'], 'task' => 'tasks', ); // update my attendee status according to submitted method if (!empty($status)) { $organizer = $task['organizer']; $emails = $this->lib->get_user_emails(); foreach ($task['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT'; $reply_sender = $attendee['email']; $task['attendees'][$i]['status'] = strtoupper($status); if (!in_array($task['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) { $task['attendees'][$i]['rsvp'] = false; // unset RSVP attribute } } } // add attendee with this user's default identity if not listed if (!$reply_sender) { $sender_identity = $this->rc->user->list_emails(true); $task['attendees'][] = array( 'name' => $sender_identity['name'], 'email' => $sender_identity['email'], 'role' => 'OPT-PARTICIPANT', 'status' => strtoupper($status), ); $metadata['attendee'] = $sender_identity['email']; } } // save to tasklist if ($list && $list['editable']) { $task['list'] = $list['id']; // check for existing task with the same UID $existing = $this->find_task($task['uid'], $mode); if ($existing) { // only update attendee status if ($task['_method'] == 'REPLY') { // try to identify the attendee using the email sender address $existing_attendee = -1; $existing_attendee_emails = array(); foreach ($existing['attendees'] as $i => $attendee) { $existing_attendee_emails[] = $attendee['email']; if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) { $existing_attendee = $i; } } $task_attendee = null; foreach ($task['attendees'] as $attendee) { if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) { $task_attendee = $attendee; $metadata['fallback'] = $attendee['status']; $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; if ($attendee['status'] != 'DELEGATED') { break; } } // also copy delegate attendee else if (!empty($attendee['delegated-from']) && (stripos($attendee['delegated-from'], $task['_sender']) !== false || stripos($attendee['delegated-from'], $task['_sender_utf']) !== false) && (!in_array($attendee['email'], $existing_attendee_emails))) { $existing['attendees'][] = $attendee; } } // if delegatee has declined, set delegator's RSVP=True if ($task_attendee && $task_attendee['status'] == 'DECLINED' && $task_attendee['delegated-from']) { foreach ($existing['attendees'] as $i => $attendee) { if ($attendee['email'] == $task_attendee['delegated-from']) { $existing['attendees'][$i]['rsvp'] = true; break; } } } // found matching attendee entry in both existing and new events if ($existing_attendee >= 0 && $task_attendee) { $existing['attendees'][$existing_attendee] = $task_attendee; $success = $this->driver->edit_task($existing); } // update the entire attendees block else if (($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) && $task_attendee) { $existing['attendees'][] = $task_attendee; $success = $this->driver->edit_task($existing); } else { $error_msg = $this->gettext('newerversionexists'); } } // delete the task when declined else if ($status == 'declined' && $delete) { $deleted = $this->driver->delete_task($existing, true); $success = true; } // import the (newer) task else if ($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) { $task['id'] = $existing['id']; $task['list'] = $existing['list']; // preserve my participant status for regular updates if (empty($status)) { $this->lib->merge_attendees($task, $existing); } // set status=CANCELLED on CANCEL messages if ($task['_method'] == 'CANCEL') { $task['status'] = 'CANCELLED'; } // update attachments list, allow attachments update only on REQUEST (#5342) if ($task['_method'] == 'REQUEST') { $task['deleted_attachments'] = true; } else { unset($task['attachments']); } // show me as free when declined (#1670) if ($status == 'declined' || $task['status'] == 'CANCELLED') { $task['free_busy'] = 'free'; } $success = $this->driver->edit_task($task); } else if (!empty($status)) { $existing['attendees'] = $task['attendees']; if ($status == 'declined') { // show me as free when declined (#1670) $existing['free_busy'] = 'free'; } $success = $this->driver->edit_event($existing); } else { $error_msg = $this->gettext('newerversionexists'); } } else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_tasklists'))) { $success = $this->driver->create_task($task); } else if ($status == 'declined') { $error_msg = null; } } else if ($status == 'declined' || $dontsave) { $error_msg = null; } else { $error_msg = $this->gettext('nowritetasklistfound'); } } if ($success || $dontsave) { if ($success) { $message = $task['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully')); $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation'); } $metadata['rsvp'] = intval($metadata['rsvp']); $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0); $this->rc->output->command('plugin.itip_message_processed', $metadata); $error_msg = null; } else if ($error_msg) { $this->rc->output->command('display_message', $error_msg, 'error'); } // send iTip reply if ($task['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) { $task['comment'] = $comment; $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } $this->rc->output->send(); } /**** Task invitation plugin hooks ****/ /** * Handler for task/itip-delegate requests */ function mail_itip_delegate() { // forward request to mail_import_itip() with the right status $_POST['_status'] = $_REQUEST['_status'] = 'delegated'; $this->mail_import_itip(); } /** * Find a task in user tasklists */ protected function find_task($task, &$mode) { $this->load_driver(); // We search for writeable folders in personal namespace by default $mode = tasklist_driver::FILTER_WRITEABLE | tasklist_driver::FILTER_PERSONAL; $result = $this->driver->get_task($task, $mode); // ... now check shared folders if not found if (!$result) { $result = $this->driver->get_task($task, tasklist_driver::FILTER_WRITEABLE | tasklist_driver::FILTER_SHARED); if ($result) { $mode |= tasklist_driver::FILTER_SHARED; } } return $result; } /** * Handler for task/itip-status requests */ public function task_itip_status() { $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); // find local copy of the referenced task $existing = $this->find_task($data, $mode); $is_shared = $mode & tasklist_driver::FILTER_SHARED; $itip = $this->load_itip(); $response = $itip->get_itip_status($data, $existing); // get a list of writeable lists to save new tasks to if ((!$existing || $is_shared) && $response['action'] == 'rsvp' || $response['action'] == 'import') { $lists = $this->driver->get_lists($mode); $select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true, 'class' => 'form-control')); $select->add('--', ''); foreach ($lists as $list) { if ($list['editable']) { $select->add($list['name'], $list['id']); } } } if ($select) { $default_list = $this->get_default_tasklist($data['sensitivity'], $lists); $response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . ' ' . $select->show($is_shared ? $existing['list'] : $default_list['id'])); } $this->rc->output->command('plugin.update_itip_object_status', $response); } /** * Handler for task/itip-remove requests */ public function task_itip_remove() { $success = false; $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST); // search for event if only UID is given if ($task = $this->driver->get_task($uid)) { $success = $this->driver->delete_task($task, true); } if ($success) { $this->rc->output->show_message('tasklist.successremoval', 'confirmation'); } else { $this->rc->output->show_message('tasklist.errorsaving', 'error'); } } /******* Utility functions *******/ /** * Generate a unique identifier for an event */ public function generate_uid() { return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); } /** * Map task properties for ical exprort using libcalendaring */ public function to_libcal($task) { $object = $task; $object['_type'] = 'task'; $object['categories'] = (array)$task['tags']; // convert to datetime objects if (!empty($task['date'])) { $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->timezone); if (empty($task['time'])) $object['due']->_dateonly = true; unset($object['date']); } if (!empty($task['startdate'])) { $object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->timezone); if (empty($task['starttime'])) $object['start']->_dateonly = true; unset($object['startdate']); } $object['complete'] = $task['complete'] * 100; if ($task['complete'] == 1.0 && empty($task['complete'])) { $object['status'] = 'COMPLETED'; } if ($task['flagged']) { $object['priority'] = 1; } - else if (!$task['priority']) { + else if (empty($task['priority'])) { $object['priority'] = 0; } return $object; } /** * Convert task properties from ical parser to the internal format */ public function from_ical($vtodo) { $task = $vtodo; $task['tags'] = array_filter((array)$vtodo['categories']); $task['flagged'] = $vtodo['priority'] == 1; $task['complete'] = floatval($vtodo['complete'] / 100); // convert from DateTime to internal date format if (is_a($vtodo['due'], 'DateTime')) { $due = $this->lib->adjust_timezone($vtodo['due']); $task['date'] = $due->format('Y-m-d'); if (!$vtodo['due']->_dateonly) $task['time'] = $due->format('H:i'); } // convert from DateTime to internal date format if (is_a($vtodo['start'], 'DateTime')) { $start = $this->lib->adjust_timezone($vtodo['start']); $task['startdate'] = $start->format('Y-m-d'); if (!$vtodo['start']->_dateonly) $task['starttime'] = $start->format('H:i'); } if (is_a($vtodo['dtstamp'], 'DateTime')) { $task['changed'] = $vtodo['dtstamp']; } unset($task['categories'], $task['due'], $task['start'], $task['dtstamp']); return $task; } /** * Handler for user_delete plugin hook */ public function user_delete($args) { $this->load_driver(); return $this->driver->user_delete($args); } /** * Magic getter for public access to protected members */ public function __get($name) { switch ($name) { case 'ical': return $this->get_ical(); case 'itip': return $this->load_itip(); case 'driver': $this->load_driver(); return $this->driver; } return null; } } diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index f4c262ec..249cfe7c 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -1,586 +1,619 @@ * * Copyright (C) 2012-2015, 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_ui { private $rc; private $plugin; private $ready = false; private $gui_objects = array(); function __construct($plugin) { $this->plugin = $plugin; $this->rc = $plugin->rc; } /** * Calendar UI initialization and requests handlers */ public function init() { if ($this->ready) { return; } // add taskbar button $this->plugin->add_button(array( 'command' => 'tasks', 'class' => 'button-tasklist', 'classsel' => 'button-tasklist button-selected', 'innerclass' => 'button-inner', 'label' => 'tasklist.navtitle', 'type' => 'link' ), 'taskbar'); $this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tasklist.css'); if ($this->rc->task == 'mail' || $this->rc->task == 'tasks') { $this->plugin->include_script('tasklist_base.js'); // copy config to client $this->rc->output->set_env('tasklist_settings', $this->load_settings()); // initialize attendees autocompletion $this->rc->autocomplete_init(); } $this->ready = true; } /** * */ function load_settings() { $settings = array(); $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0); $settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', 3); $settings['sort_col'] = $this->rc->config->get('tasklist_sort_col', ''); $settings['sort_order'] = $this->rc->config->get('tasklist_sort_order', 'asc'); // get user identity to create default attendee foreach ($this->rc->user->list_emails() as $rec) { - if (!$identity) + if (empty($identity)) { $identity = $rec; + } $identity['emails'][] = $rec['email']; $settings['identities'][$rec['identity_id']] = $rec['email']; } $identity['emails'][] = $this->rc->user->get_username(); $settings['identity'] = array( 'name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails'])) ); if ($list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC)) { $settings['selected_list'] = $list; } if ($list && ($id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC))) { $settings['selected_id'] = $id; // check if the referenced task is completed $task = $this->plugin->driver->get_task(array('id' => $id, 'list' => $list)); if ($task && $this->plugin->driver->is_complete($task)) { $settings['selected_filter'] = 'complete'; } } else if ($filter = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GPC)) { $settings['selected_filter'] = $filter; } return $settings; } /** * Render a HTML select box for user identity selection */ function identity_select($attrib = array()) { $attrib['name'] = 'identity'; $select = new html_select($attrib); $identities = $this->rc->user->list_emails(); foreach ($identities as $ident) { $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']); } return $select->show(null); } /** * Register handler methods for the template engine */ public function init_templates() { $this->plugin->register_handler('plugin.tasklists', array($this, 'tasklists')); $this->plugin->register_handler('plugin.tasklist_select', array($this, 'tasklist_select')); $this->plugin->register_handler('plugin.status_select', array($this, 'status_select')); $this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); $this->plugin->register_handler('plugin.quickaddform', array($this, 'quickadd_form')); $this->plugin->register_handler('plugin.tasks', array($this, 'tasks_resultview')); $this->plugin->register_handler('plugin.tags_editline', array($this, 'tags_editline')); $this->plugin->register_handler('plugin.alarm_select', array($this, 'alarm_select')); $this->plugin->register_handler('plugin.recurrence_form', array($this->plugin->lib, 'recurrence_form')); $this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list')); $this->plugin->register_handler('plugin.attendees_form', array($this, 'attendees_form')); $this->plugin->register_handler('plugin.identity_select', array($this, 'identity_select')); $this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify')); $this->plugin->register_handler('plugin.task_rsvp_buttons', array($this->plugin->itip, 'itip_rsvp_buttons')); $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table')); $this->plugin->register_handler('plugin.tasks_export_form', array($this, 'tasks_export_form')); $this->plugin->register_handler('plugin.tasks_import_form', array($this, 'tasks_import_form')); kolab_attachments_handler::ui(); $this->plugin->include_script('tasklist.js'); $this->plugin->api->include_script('libkolab/libkolab.js'); } /** * */ public function tasklists($attrib = array()) { $tree = true; $jsenv = array(); $lists = $this->plugin->driver->get_lists(0, $tree); if (empty($attrib['id'])) { $attrib['id'] = 'rcmtasklistslist'; } // walk folder tree if (is_object($tree)) { $html = $this->list_tree_html($tree, $lists, $jsenv, $attrib); } else { // fall-back to flat folder listing $attrib['class'] .= ' flat'; $html = ''; foreach ((array)$lists as $id => $prop) { - if ($attrib['activeonly'] && !$prop['active']) - continue; + if (!empty($attrib['activeonly']) && empty($prop['active'])) { + continue; + } $html .= html::tag('li', array( 'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id), - 'class' => $prop['group'], + 'class' => isset($prop['group']) ? $prop['group'] : null, ), - $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly']) + $this->tasklist_list_item($id, $prop, $jsenv, !empty($attrib['activeonly'])) ); } } $this->rc->output->include_script('treelist.js'); $this->rc->output->set_env('source', rcube_utils::get_input_value('source', rcube_utils::INPUT_GET)); $this->rc->output->set_env('tasklists', $jsenv); $this->register_gui_object('tasklistslist', $attrib['id']); return html::tag('ul', $attrib, $html, html::$common_attrib); } /** * Return html for a structured list
    for the folder tree */ public function list_tree_html($node, $data, &$jsenv, $attrib) { $out = ''; foreach ($node->children as $folder) { $id = $folder->id; $prop = $data[$id]; $is_collapsed = false; // TODO: determine this somehow? $content = $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly']); if (!empty($folder->children)) { $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), $this->list_tree_html($folder, $data, $jsenv, $attrib)); } if (strlen($content)) { $out .= html::tag('li', array( 'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id), 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''), ), $content); } } return $out; } /** * Helper method to build a tasklist item (HTML content and js data) */ public function tasklist_list_item($id, $prop, &$jsenv, $activeonly = false) { // enrich list properties with settings from the driver - if (!$prop['virtual']) { + if (empty($prop['virtual'])) { unset($prop['user_id']); $prop['alarms'] = $this->plugin->driver->alarms; $prop['undelete'] = $this->plugin->driver->undelete; $prop['sortable'] = $this->plugin->driver->sortable; $prop['attachments'] = $this->plugin->driver->attachments; $prop['attendees'] = $this->plugin->driver->attendees; $prop['caldavurl'] = $this->plugin->driver->tasklist_caldav_url($prop); $jsenv[$id] = $prop; } $classes = array('tasklist'); - $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ? - html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : ''); + $title = ''; - if ($prop['virtual']) + if (!empty($prop['title'])) { + $title = $prop['title']; + } + else if (empty($prop['listname']) || $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25) { + html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET); + } + + if (!empty($prop['virtual'])) { $classes[] = 'virtual'; - else if (!$prop['editable']) + } + else if (empty($prop['editable'])) { $classes[] = 'readonly'; - if ($prop['subscribed']) + } + if (!empty($prop['subscribed'])) { $classes[] = 'subscribed'; - if ($prop['class']) + } + if (!empty($prop['class'])) { $classes[] = $prop['class']; + } if (!$activeonly || $prop['active']) { $label_id = 'tl:' . $id; $chbox = html::tag('input', array( 'type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'title' => $this->plugin->gettext('activate'), 'aria-labelledby' => $label_id )); return html::div(join(' ', $classes), - html::a(array('class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id), $prop['listname'] ?: $prop['name']) . - ($prop['virtual'] ? '' : $chbox . html::span('actions', - ($prop['removable'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') : '') + html::a(array('class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id), + !empty($prop['listname']) ? $prop['listname'] : $prop['name']) . + (!empty($prop['virtual']) ? '' : $chbox . html::span('actions', + (!empty($prop['removable']) ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') : '') . html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->plugin->gettext('focusview'), 'role' => 'checkbox', 'aria-checked' => 'false'), ' ') . (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '') ) ) ); } return ''; } /** * Render HTML form for task status selector */ function status_select($attrib = array()) { $attrib['name'] = 'status'; $select = new html_select($attrib); $select->add('---', ''); $select->add($this->plugin->gettext('status-needs-action'), 'NEEDS-ACTION'); $select->add($this->plugin->gettext('status-in-process'), 'IN-PROCESS'); $select->add($this->plugin->gettext('status-completed'), 'COMPLETED'); $select->add($this->plugin->gettext('status-cancelled'), 'CANCELLED'); return $select->show(null); } /** * Render a HTML select box for list selection */ function tasklist_select($attrib = array()) { if (empty($attrib['name'])) { $attrib['name'] = 'list'; } $attrib['is_escaped'] = true; $select = new html_select($attrib); $default = null; - foreach ((array) $attrib['extra'] as $id => $name) { - $select->add($name, $id); + if (!empty($attrib['extra'])) { + foreach ((array) $attrib['extra'] as $id => $name) { + $select->add($name, $id); + } } - foreach ((array)$this->plugin->driver->get_lists() as $id => $prop) { - if ($prop['editable'] || strpos($prop['rights'], 'i') !== false) { + foreach ((array) $this->plugin->driver->get_lists() as $id => $prop) { + if (!empty($prop['editable']) || strpos($prop['rights'], 'i') !== false) { $select->add($prop['name'], $id); - if (!$default || $prop['default']) + if (!$default || !empty($prop['default'])) { $default = $id; + } } } return $select->show($default); } function tasklist_editform($action, $list = array()) { $this->action = $action; $this->list = $list; $this->rc->output->set_env('pagetitle', $this->plugin->gettext('arialabeltasklistform')); $this->rc->output->add_handler('folderform', array($this, 'tasklistform')); $this->rc->output->send('libkolab.folderform'); } function tasklistform($attrib) { $fields = array( 'name' => array( 'id' => 'taskedit-tasklistname', 'label' => $this->plugin->gettext('listname'), 'value' => html::tag('input', array('id' => 'taskedit-tasklistname', 'name' => 'name', 'type' => 'text', 'class' => 'text', 'size' => 40)), ), /* 'color' => array( 'id' => 'taskedit-color', 'label' => $this->plugin->gettext('color'), 'value' => html::tag('input', array('id' => 'taskedit-color', 'name' => 'color', 'type' => 'text', 'class' => 'text colorpicker', 'size' => 6)), ), */ 'showalarms' => array( 'id' => 'taskedit-showalarms', 'label' => $this->plugin->gettext('showalarms'), 'value' => html::tag('input', array('id' => 'taskedit-showalarms', 'name' => 'showalarms', 'type' => 'checkbox', 'value' => 1)), ), ); return html::tag('form', $attrib + array('action' => "#", 'method' => "post", 'id' => 'tasklisteditform'), $this->plugin->driver->tasklist_edit_form($this->action, $this->list, $fields) ); } /** * Render HTML form for alarm configuration */ function alarm_select($attrib = array()) { $attrib['_type'] = 'task'; return $this->plugin->lib->alarm_select($attrib, $this->plugin->driver->alarm_types, $this->plugin->driver->alarm_absolute); } /** * */ function quickadd_form($attrib) { $attrib += array('action' => $this->rc->url('add'), 'method' => 'post', 'id' => 'quickaddform'); $label = html::label(array('for' => 'quickaddinput', 'class' => 'voice'), $this->plugin->gettext('quickaddinput')); $input = new html_inputfield(array('name' => 'text', 'id' => 'quickaddinput')); $button = html::tag('input', array( 'type' => 'submit', 'value' => '+', 'title' => $this->plugin->gettext('createtask'), 'class' => 'button mainaction' )); $this->register_gui_object('quickaddform', $attrib['id']); return html::tag('form', $attrib, $label . $input->show() . $button); } /** * The result view */ function tasks_resultview($attrib) { $attrib += array('id' => 'rcmtaskslist'); $this->register_gui_object('resultlist', $attrib['id']); unset($attrib['name']); return html::tag('ul', $attrib, ''); } /** * Interactive UI element to add/remove tags */ function tags_editline($attrib) { $attrib += array('id' => 'rcmtasktagsedit'); $this->register_gui_object('edittagline', $attrib['id']); - $input = new html_inputfield(array('name' => 'tags[]', 'class' => 'tag', 'size' => $attrib['size'], 'tabindex' => $attrib['tabindex'])); + $input = new html_inputfield(array( + 'name' => 'tags[]', + 'class' => 'tag', + 'size' => !empty($attrib['size']) ? $attrib['size'] : null, + 'tabindex' => isset($attrib['tabindex']) ? $attrib['tabindex'] : null, + )); unset($attrib['tabindex']); return html::div($attrib, $input->show('')); } /** * */ function attendees_list($attrib = array()) { // add "noreply" checkbox to attendees table only $invitations = strpos($attrib['id'], 'attend') !== false; $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite')); $table = new html_table(array('cols' => 4 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable')); // $table->add_header('role', $this->plugin->gettext('role')); $table->add_header('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee')); $table->add_header('confirmstate', $this->plugin->gettext('confirmstate')); if ($invitations) { $table->add_header(array('class' => 'invite', 'title' => $this->plugin->gettext('sendinvitations')), $invite->show(1) . html::label('edit-attendees-invite', html::span('inner', $this->plugin->gettext('sendinvitations')))); } $table->add_header('options', ''); // hide invite column if disabled by config $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3); if ($invitations && !($itip_notify & 2)) { $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']); $this->rc->output->add_footer(html::tag('style', array('type' => 'text/css'), $css)); } return $table->show($attrib); } /** * */ function attendees_form($attrib = array()) { - $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => $attrib['size'], 'class' => 'form-control')); - $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment', - 'rows' => 4, 'cols' => 55, 'title' => $this->plugin->gettext('itipcommenttitle'), 'class' => 'form-control')); + $input = new html_inputfield(array( + 'name' => 'participant', + 'id' => 'edit-attendee-name', + 'size' => !empty($attrib['size']) ? $attrib['size'] : null, + 'class' => 'form-control' + )); + + $textarea = new html_textarea(array( + 'name' => 'comment', + 'id' => 'edit-attendees-comment', + 'rows' => 4, + 'cols' => 55, + 'title' => $this->plugin->gettext('itipcommenttitle'), + 'class' => 'form-control' + )); return html::div($attrib, html::div('form-searchbar', $input->show() . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->plugin->gettext('addattendee'))) // . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->plugin->gettext('scheduletime').'...')) ) . html::p('attendees-commentbox', html::label('edit-attendees-comment', $this->plugin->gettext('itipcomment')) . $textarea->show()) ); } /** * */ function edit_attendees_notify($attrib = array()) { $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1, 'class' => 'pretty-checkbox')); return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->plugin->gettext('sendnotifications'))); } /** * Form for uploading and importing tasks */ function tasks_import_form($attrib = array()) { - if (!$attrib['id']) { + if (empty($attrib['id'])) { $attrib['id'] = 'rcmImportForm'; } // Get max filesize, enable upload progress bar $max_filesize = $this->rc->upload_init(); $accept = '.ics, text/calendar, text/x-vcalendar, application/ics'; if (class_exists('ZipArchive', false)) { $accept .= ', .zip, application/zip'; } $input = new html_inputfield(array( 'id' => 'importfile', 'type' => 'file', 'name' => '_data', - 'size' => $attrib['uploadfieldsize'], + 'size' => !empty($attrib['uploadfieldsize']) ? $attrib['uploadfieldsize'] : null, 'accept' => $accept )); $html = html::div('form-section form-group row', html::label(array('class' => 'col-sm-4 col-form-label', 'for' => 'importfile'), rcube::Q($this->rc->gettext('importfromfile'))) . html::div('col-sm-8', $input->show() . html::div('hint', $this->rc->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))) ); $html .= html::div('form-section form-group row', html::label(array('for' => 'task-import-list', 'class' => 'col-sm-4 col-form-label'), $this->plugin->gettext('list')) . html::div('col-sm-8', $this->tasklist_select(array('name' => 'source', 'id' => 'task-import-list', 'editable' => true))) ); $this->rc->output->add_gui_object('importform', $attrib['id']); $this->rc->output->add_label('import', 'importerror'); return html::tag('p', null, $this->plugin->gettext('importtext')) .html::tag('form', array( 'action' => $this->rc->url(array('task' => 'tasklist', 'action' => 'import')), 'method' => 'post', 'enctype' => 'multipart/form-data', 'id' => $attrib['id'], ), $html ); } /** * Form to select options for exporting tasks */ function tasks_export_form($attrib = array()) { - if (!$attrib['id']) { + if (empty($attrib['id'])) { $attrib['id'] = 'rcmTaskExportForm'; } - $html .= html::div('form-section form-group row', + $html = html::div('form-section form-group row', html::label(array('for' => 'task-export-list', 'class' => 'col-sm-4 col-form-label'), $this->plugin->gettext('list')) . html::div('col-sm-8', $this->tasklist_select(array( 'name' => 'source', 'id' => 'task-export-list', 'extra' => array('' => '- ' . $this->plugin->gettext('currentview') . ' -'), ))) ); $checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'task-export-attachments', 'value' => 1, 'class' => 'form-check-input pretty-checkbox')); $html .= html::div('form-section row form-check', html::label(array('for' => 'task-export-attachments', 'class' => 'col-sm-4 col-form-label'), $this->plugin->gettext('exportattachments')) . html::div('col-sm-8', $checkbox->show(1)) ); $this->register_gui_object('exportform', $attrib['id']); return html::tag('form', array( 'action' => $this->rc->url(array('task' => 'tasklist', 'action' => 'export')), 'method' => 'post', 'id' => $attrib['id'] ), $html ); } /** * Wrapper for rcube_output_html::add_gui_object() */ function register_gui_object($name, $id) { $this->gui_objects[$name] = $id; $this->rc->output->add_gui_object($name, $id); } /** * Getter for registered gui objects. * (for manual registration when loading the inline UI) */ function get_gui_objects() { return $this->gui_objects; } }