* * 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; 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, ); public $task = '?(?!login|logout).*'; public $rc; public $driver; public $timezone; public $ui; public $defaults = array( 'date_format' => "Y-m-d", 'time_format' => "H:i", 'first_day' => 1, ); /** * Plugin initialization. */ function init() { $this->rc = rcmail::get_instance(); $this->register_task('tasks', 'tasklist'); // load plugin configuration $this->load_config(); // load localizations $this->add_texts('localization/', $this->rc->task == 'tasks' && (!$this->rc->action || $this->rc->action == 'print')); if ($this->rc->task == 'tasks' && $this->rc->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')); } if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) { require_once($this->home . '/tasklist_ui.php'); $this->ui = new tasklist_ui($this); $this->ui->init(); } } /** * Helper method to load the backend driver according to local config */ private function load_driver() { if (is_object($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'); switch ($driver_name) { case "kolab": $this->require_plugin('libkolab'); default: $this->driver = new $driver_class($this); break; } // get user's timezone $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT')); } /** * */ public function task_action() { $action = get_input_value('action', RCUBE_INPUT_GPC); $rec = get_input_value('t', RCUBE_INPUT_POST, true); $oldrec = $rec; $success = $refresh = false; switch ($action) { case 'new': $oldrec = null; $rec = $this->prepare_task($rec); $rec['uid'] = $this->generate_uid(); $temp_id = $rec['tempid']; if ($success = $this->driver->create_task($rec)) { $refresh = $this->driver->get_task($rec); if ($temp_id) $refresh['tempid'] = $temp_id; } break; case 'edit': $rec = $this->prepare_task($rec); if ($success = $this->driver->edit_task($rec)) $refresh = $this->driver->get_task($rec); break; case 'delete': if (!($success = $this->driver->delete_task($rec, false))) $this->rc->output->command('plugin.reload_data'); break; case 'undelete': if ($success = $this->driver->undelete_task($rec)) $refresh = $this->driver->get_task($rec); break; } if ($success) { $this->rc->output->show_message('successfullysaved', 'confirmation'); $this->update_counts($oldrec, $refresh); } else $this->rc->output->show_message('tasklist.errorsaving', 'error'); // unlock client $this->rc->output->command('plugin.unlock_saving'); if ($refresh) { $this->encode_task($refresh); $this->rc->output->command('plugin.refresh_task', $refresh); } } /** * 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']) { 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'])) { try { $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone); $rec['date'] = $date->format('Y-m-d'); if (!empty($rec['time'])) $rec['time'] = $date->format('H:i'); } catch (Exception $e) { $rec['date'] = $rec['time'] = null; } } if (!empty($rec['startdate'])) { try { $date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone); $rec['startdate'] = $date->format('Y-m-d'); if (!empty($rec['starttime'])) $rec['starttime'] = $date->format('H:i'); } catch (Exception $e) { $rec['startdate'] = $rec['starttime'] = null; } } return $rec; } /** * */ public function tasklist_action() { $action = get_input_value('action', RCUBE_INPUT_GPC); $list = get_input_value('l', RCUBE_INPUT_POST, true); $success = false; if (isset($list['showalarms'])) $list['showalarms'] = intval($list['showalarms']); switch ($action) { case 'new': $list += array('showalarms' => true, 'active' => true); if ($insert_id = $this->driver->create_list($list)) { $list['id'] = $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; } if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); else $this->rc->output->show_message('tasklist.errorsaving', 'error'); $this->rc->output->command('plugin.unlock_saving'); } /** * */ public function fetch_counts() { $lists = get_input_value('lists', RCUBE_INPUT_GPC);; $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() { $f = intval(get_input_value('filter', RCUBE_INPUT_GPC)); $search = get_input_value('q', RCUBE_INPUT_GPC); $filter = array('mask' => $f, 'search' => $search); $lists = get_input_value('lists', RCUBE_INPUT_GPC);; /* // convert magic date filters into a real date range switch ($f) { case self::FILTER_MASK_TODAY: $today = new DateTime('now', $this->timezone); $filter['from'] = $filter['to'] = $today->format('Y-m-d'); break; case self::FILTER_MASK_TOMORROW: $tomorrow = new DateTime('now + 1 day', $this->timezone); $filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d'); break; case self::FILTER_MASK_OVERDUE: $yesterday = new DateTime('yesterday', $this->timezone); $filter['to'] = $yesterday->format('Y-m-d'); break; case self::FILTER_MASK_WEEK: $today = new DateTime('now', $this->timezone); $filter['from'] = $today->format('Y-m-d'); $weekend = new DateTime('now + 7 days', $this->timezone); $filter['to'] = $weekend->format('Y-m-d'); break; case self::FILTER_MASK_LATER: $date = new DateTime('now + 8 days', $this->timezone); $filter['from'] = $date->format('Y-m-d'); break; } */ $data = $tags = $this->task_tree = $this->task_titles = array(); foreach ($this->driver->list_tasks($filter, $lists) as $rec) { if ($rec['parent_id']) { $this->task_tree[$rec['id']] = $rec['parent_id']; } $this->encode_task($rec); if (!empty($rec['tags'])) $tags = array_merge($tags, (array)$rec['tags']); // apply filter; don't trust the driver on this :-) if ((!$f && $rec['complete'] < 1.0) || ($rec['mask'] & $f)) $data[] = $rec; } // sort tasks according to their hierarchy level and due date usort($data, array($this, 'task_sort_cmp')); $this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => array_values(array_unique($tags)))); } /** * 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']); $rec['changed'] = is_object($rec['changed']) ? $rec['changed']->format('U') : 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 (!isset($rec['_depth'])) { $rec['_depth'] = 0; $parent_id = $this->task_tree[$rec['id']]; while ($parent_id) { $rec['_depth']++; $rec['parent_title'] = $this->task_titles[$parent_id]; $parent_id = $this->task_tree[$parent_id]; } } $this->task_titles[$rec['id']] = $rec['title']; } /** * Compare function for task list sorting. * Nested tasks need to be sorted to the end. */ private function task_sort_cmp($a, $b) { $d = $a['_depth'] - $b['_depth']; if (!$d) $d = $b['_hasdate'] - $a['_hasdate']; if (!$d) $d = $a['datetime'] - $b['datetime']; return $d; } /** * 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, $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'); $week_date = new DateTime('now + 7 days', $this->timezone); $weeklimit = $week_date->format('Y-m-d'); } $mask = 0; $start = $rec['startdate'] ?: '1900-00-00'; if ($rec['flagged']) $mask |= self::FILTER_MASK_FLAGGED; if ($rec['complete'] == 1.0) $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 ($rec['date'] >= $today && $start <= $today) $mask |= self::FILTER_MASK_TODAY; if ($rec['date'] >= $tomorrow && $start <= $tomorrow) $mask |= self::FILTER_MASK_TOMORROW; if (($start > $tomorrow || $rec['date'] > $tomorrow) && $rec['date'] <= $weeklimit) $mask |= self::FILTER_MASK_WEEK; if ($start > $weeklimit || $rec['date'] > $weeklimit) $mask |= self::FILTER_MASK_LATER; return $mask; } /******* UI functions ********/ /** * Render main view of the tasklist task */ public function tasklist_view() { $this->ui->init(); $this->ui->init_templates(); $this->rc->output->set_pagetitle($this->gettext('navtitle')); $this->rc->output->send('tasklist.mainview'); } /******* 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)); } }