roundcubemail-plugins-kolab/plugins/tasklist/drivers/database/tasklist_database_driver.php
2021-03-21 11:14:57 +01:00

848 lines
28 KiB
PHP

<?php
/**
* Database driver for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-2015, 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
{
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']),
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']),
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 (!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) {
if (!empty($filter['from']) || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) {
$sql_add .= " AND (`date` IS NULL OR `date` >= " . $this->rc->db->quote($filter['from']) . ")";
}
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_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";
}
// 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 (!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 (!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"
);
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 = !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",
!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 = !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 (!empty($prop['valarms'])) {
$prop['alarms'] = $this->serialize_alarms($prop['valarms']);
}
if (!empty($prop['recurrence'])) {
$prop['recurrence'] = $this->serialize_recurrence($prop['recurrence']);
}
if (array_key_exists('complete', $prop) && !empty($prop['complete'])) {
$prop['complete'] = number_format($prop['complete'], 2, '.', '');
}
foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence', 'status', 'complete') 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'],
isset($prop['description']) ? strval($prop['description']) : '',
!empty($prop['tags']) ? join(',', (array)$prop['tags']) : '',
!empty($prop['flagged']) ? 1 : 0,
$prop['complete'] ?: 0,
strval($prop['status']),
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 (isset($prop['valarms'])) {
$prop['alarms'] = $this->serialize_alarms($prop['valarms']);
}
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 (!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 (!empty($task['valarms']) && !$this->is_complete($task)) {
$alarm = libcalendaring::get_next_alarm($task, 'task');
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)));
}
}
}
}