roundcubemail-plugins-kolab/plugins/tasklist/drivers/database/tasklist_database_driver.php

745 lines
24 KiB
PHP

<?php
/**
* Database driver for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class tasklist_database_driver extends tasklist_driver
{
public $undelete = true; // yes, we can
public $sortable = false;
public $alarm_types = array('DISPLAY');
private $rc;
private $plugin;
private $lists = 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;
$this->lists[$arr['id']] = $arr;
$list_ids[] = $this->rc->db->quote($arr['id']);
}
$this->list_ids = join(',', $list_ids);
}
}
/**
* Get a list of available tasks lists from this source
*/
public function get_lists()
{
// attempt to create a default list for this user
if (empty($this->lists)) {
$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
);
if ($result)
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=?",
$prop['name'],
$prop['color'],
$prop['showalarms']?1:0,
$prop['id'],
$this->rc->user->ID
);
return $this->rc->db->affected_rows($query);
}
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* @return boolean True on success, Fales on failure
* @see tasklist_driver::subscribe_list()
*/
public function subscribe_list($prop)
{
$hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', '')));
if ($prop['active'])
unset($hidden[$prop['id']]);
else
$hidden[$prop['id']] = 1;
return $this->rc->user->save_prefs(array('hidden_tasklists' => join(',', array_keys($hidden))));
}
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* @return boolean True on success, Fales on failure
* @see tasklist_driver::remove_list()
*/
public function remove_list($prop)
{
$list_id = $prop['id'];
if ($this->lists[$list_id]) {
// delete all tasks linked with this list
$this->rc->db->query(
"DELETE FROM " . $this->db_tasks . "
WHERE tasklist_id=?",
$list_id
);
// delete list record
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_lists . "
WHERE tasklist_id=?
AND user_id=?",
$list_id,
$this->rc->user->ID
);
return $this->rc->db->affected_rows($query);
}
return false;
}
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|today|tomorrow|overdue|nodate)
* @see tasklist_driver::count_tasks()
*/
function count_tasks($lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
// only allow to select from lists of this user
$list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
$today_date = new DateTime('now', $this->plugin->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$result = $this->rc->db->query(sprintf(
"SELECT task_id, flagged, date FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND del=0 AND complete<1",
join(',', $list_ids)
));
$counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$counts['all']++;
if ($rec['flagged'])
$counts['flagged']++;
if (empty($rec['date']))
$counts['nodate']++;
else if ($rec['date'] == $today)
$counts['today']++;
else if ($rec['date'] == $tomorrow)
$counts['tomorrow']++;
else if ($rec['date'] < $today)
$counts['overdue']++;
}
return $counts;
}
/**
* Get all taks records matching the given filter
*
* @param array Hash array wiht filter criterias
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
* @see tasklist_driver::list_tasks()
*/
function list_tasks($filter, $lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
// only allow to select from lists of this user
$list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
$sql_add = '';
// add filter criteria
if ($filter['from'] || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) {
$sql_add .= ' AND (date IS NULL OR date >= ?)';
$datefrom = $filter['from'];
}
if ($filter['to']) {
if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE)
$sql_add .= ' AND (date IS NOT NULL AND date <= ' . $this->rc->db->quote($filter['to']) . ')';
else
$sql_add .= ' AND (date IS NULL OR date <= ' . $this->rc->db->quote($filter['to']) . ')';
}
// special case 'today': also show all events with date before today
if ($filter['mask'] & tasklist::FILTER_MASK_TODAY) {
$datefrom = date('Y-m-d', 0);
}
if ($filter['mask'] & tasklist::FILTER_MASK_NODATE)
$sql_add = ' AND date IS NULL';
if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
$sql_add .= ' AND complete=1';
else if (empty($filter['since'])) // don't show complete tasks by default
$sql_add .= ' AND complete<1';
if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED)
$sql_add .= ' AND flagged=1';
// compose (slow) SQL query for searching
// FIXME: improve searching using a dedicated col and normalized values
if ($filter['search']) {
$sql_query = array();
foreach (array('title','description','organizer','attendees') as $col)
$sql_query[] = $this->rc->db->ilike($col, '%'.$filter['search'].'%');
$sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
}
if ($filter['since'] && is_numeric($filter['since'])) {
$sql_add .= ' AND changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $filter['since']));
}
$tasks = array();
if (!empty($list_ids)) {
$result = $this->rc->db->query(sprintf(
"SELECT * FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND del=0
%s
ORDER BY parent_id, task_id ASC",
join(',', $list_ids),
$sql_add
),
$datefrom
);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$tasks[] = $this->_read_postprocess($rec);
}
}
return $tasks;
}
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @return array Hash array with task properties or false if not found
*/
public function get_task($prop)
{
if (is_string($prop))
$prop['uid'] = $prop;
$query_col = $prop['id'] ? 'task_id' : 'uid';
$result = $this->rc->db->query(sprintf(
"SELECT * FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND %s=?
AND del=0",
$this->list_ids,
$query_col
),
$prop['id'] ? $prop['id'] : $prop['uid']
);
if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
return $this->_read_postprocess($rec);
}
return false;
}
/**
* Get all decendents of the given task record
*
* @param mixed Hash array with task properties or task UID
* @param boolean True if all childrens children should be fetched
* @return array List of all child task IDs
*/
public function get_childs($prop, $recursive = false)
{
// resolve UID first
if (is_string($prop)) {
$result = $this->rc->db->query(sprintf(
"SELECT task_id AS id, tasklist_id AS list FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND uid=?",
$this->list_ids
),
$prop);
$prop = $this->rc->db->fetch_assoc($result);
}
$childs = array();
$task_ids = array($prop['id']);
// query for childs (recursively)
while (!empty($task_ids)) {
$result = $this->rc->db->query(sprintf(
"SELECT task_id AS id FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND parent_id IN (%s)
AND del=0",
$this->list_ids,
join(',', array_map(array($this->rc->db, 'quote'), $task_ids))
));
$task_ids = array();
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$childs[] = $rec['id'];
$task_ids[] = $rec['id'];
}
if (!$recursive)
break;
}
return $childs;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
* @param mixed List of list IDs to show alarms for (either as array or comma-separated string)
* @return array A list of alarms, each encoded as hash array with task properties
* @see tasklist_driver::pending_alarms()
*/
public function pending_alarms($time, $lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
// only allow to select from calendars with activated alarms
$list_ids = array();
foreach ($lists as $lid) {
if ($this->lists[$lid] && $this->lists[$lid]['showalarms'])
$list_ids[] = $lid;
}
$list_ids = array_map(array($this->rc->db, 'quote'), $list_ids);
$alarms = array();
if (!empty($list_ids)) {
$result = $this->rc->db->query(sprintf(
"SELECT * FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND notify <= %s AND complete < 1",
join(',', $list_ids),
$this->rc->db->fromunixtime($time)
));
while ($result && ($rec = $this->rc->db->fetch_assoc($result)))
$alarms[] = $this->_read_postprocess($rec);
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see tasklist_driver::dismiss_alarm()
*/
public function dismiss_alarm($task_id, $snooze = 0)
{
// set new notifyat time or unset if not snoozed
$notify_at = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_tasks . "
SET changed=%s, notify=?
WHERE task_id=?
AND tasklist_id IN (" . $this->list_ids . ")",
$this->rc->db->now()),
$notify_at,
$task_id
);
return $this->rc->db->affected_rows($query);
}
/**
* Map some internal database values to match the generic "API"
*/
private function _read_postprocess($rec)
{
$rec['id'] = $rec['task_id'];
$rec['list'] = $rec['tasklist_id'];
$rec['changed'] = new DateTime($rec['changed']);
$rec['tags'] = array_filter(explode(',', $rec['tags']));
if (!$rec['parent_id'])
unset($rec['parent_id']);
// decode serialized alarms
if ($rec['alarms']) {
$rec['valarms'] = $this->unserialize_alarms($rec['alarms']);
unset($rec['alarms']);
}
unset($rec['task_id'], $rec['tasklist_id'], $rec['created']);
return $rec;
}
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of this file)
* @return mixed New event ID on success, False on error
* @see tasklist_driver::create_task()
*/
public function create_task($prop)
{
// check list permissions
$list_id = $prop['list'] ? $prop['list'] : reset(array_keys($this->lists));
if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly'])
return false;
if (is_array($prop['valarms'])) {
$prop['alarms'] = $this->serialize_alarms($prop['valarms']);
}
foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) {
if (empty($prop[$col]))
$prop[$col] = null;
}
$notify_at = $this->_get_notification($prop);
$result = $this->rc->db->query(sprintf(
"INSERT INTO " . $this->db_tasks . "
(tasklist_id, uid, parent_id, created, changed, title, date, time, startdate, starttime, description, tags, alarms, notify)
VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
$this->rc->db->now(),
$this->rc->db->now()
),
$list_id,
$prop['uid'],
$prop['parent_id'],
$prop['title'],
$prop['date'],
$prop['time'],
$prop['startdate'],
$prop['starttime'],
strval($prop['description']),
join(',', (array)$prop['tags']),
$prop['alarms'],
$notify_at
);
if ($result)
return $this->rc->db->insert_id($this->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'])) {
$prop['alarms'] = $this->serialize_alarms($prop['valarms']);
}
$sql_set = array();
foreach (array('title', 'description', 'flagged', 'complete') as $col) {
if (isset($prop[$col]))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop[$col]);
}
foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) {
if (isset($prop[$col]))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col]));
}
if (isset($prop['tags']))
$sql_set[] = $this->rc->db->quote_identifier('tags') . '=' . $this->rc->db->quote(join(',', (array)$prop['tags']));
if (isset($prop['date']) || isset($prop['time']) || isset($prop['alarms'])) {
$notify_at = $this->_get_notification($prop);
$sql_set[] = $this->rc->db->quote_identifier('notify') . '=' . (empty($notify_at) ? 'NULL' : $this->rc->db->quote($notify_at));
}
// moved from another list
if ($prop['_fromlist'] && ($newlist = $prop['list'])) {
$sql_set[] = 'tasklist_id=' . $this->rc->db->quote($newlist);
}
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_tasks . "
SET changed=%s %s
WHERE task_id=?
AND tasklist_id IN (%s)",
$this->rc->db->now(),
($sql_set ? ', ' . join(', ', $sql_set) : ''),
$this->list_ids
),
$prop['id']
);
return $this->rc->db->affected_rows($query);
}
/**
* Move a single task to another list
*
* @param array Hash array with task properties:
* @return boolean True on success, False on error
* @see tasklist_driver::move_task()
*/
public function move_task($prop)
{
return $this->edit_task($prop);
}
/**
* Remove a single task from the database
*
* @param array Hash array with task properties
* @param boolean Remove record irreversible
* @return boolean True on success, False on error
* @see tasklist_driver::delete_task()
*/
public function delete_task($prop, $force = true)
{
$task_id = $prop['id'];
if ($task_id && $force) {
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_tasks . "
WHERE task_id=?
AND tasklist_id IN (" . $this->list_ids . ")",
$task_id
);
}
else if ($task_id) {
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_tasks . "
SET changed=%s, del=1
WHERE task_id=?
AND tasklist_id IN (%s)",
$this->rc->db->now(),
$this->list_ids
),
$task_id
);
}
return $this->rc->db->affected_rows($query);
}
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties
* @return boolean True on success, False on error
* @see tasklist_driver::undelete_task()
*/
public function undelete_task($prop)
{
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_tasks . "
SET changed=%s, del=0
WHERE task_id=?
AND tasklist_id IN (%s)",
$this->rc->db->now(),
$this->list_ids
),
$prop['id']
);
return $this->rc->db->affected_rows($query);
}
/**
* Compute absolute time to notify the user
*/
private function _get_notification($task)
{
if ($task['valarms'] && $task['complete'] < 1) {
$alarm = libcalendaring::get_next_alarm($task, 'task');
if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types))
return date('Y-m-d H:i:s', $alarm['time']);
}
return null;
}
/**
* 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_alaram_value($trigger)) {
$valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0]));
}
}
return $valarms;
}
/**
* Handler for user_delete plugin hook
*/
public function user_delete($args)
{
$db = $this->rc->db;
$list_ids = array();
$lists = $db->query("SELECT tasklist_id FROM " . $this->db_lists . " WHERE user_id=?", $args['user']->ID);
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)));
}
}
}
}