diff --git a/plugins/kolab_delegation/kolab_delegation.js b/plugins/kolab_delegation/kolab_delegation.js index 56ca066c..cdde2836 100644 --- a/plugins/kolab_delegation/kolab_delegation.js +++ b/plugins/kolab_delegation/kolab_delegation.js @@ -1,323 +1,341 @@ /** * Client scripts for the Kolab Delegation configuration utitlity * * @author Aleksander Machniak * @author Thomas Bruederli * * @licstart The following is the entire license notice for the * JavaScript code in this file. * - * Copyright (C) 2011-2015, Kolab Systems AG + * Copyright (C) 2011-2016, 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 . * * @licend The above is the entire license notice * for the JavaScript code in this file. */ window.rcmail && rcmail.addEventListener('init', function(evt) { - if (rcmail.env.task == 'mail' || rcmail.env.task == 'calendar') { - // set delegator context for calendar requests on invitation message - rcmail.addEventListener('requestcalendar/event', function(o) { rcmail.event_delegator_request(o); }); - rcmail.addEventListener('requestcalendar/mailimportevent', function(o) { rcmail.event_delegator_request(o); }); - rcmail.addEventListener('requestcalendar/mailimportitip', function(o) { rcmail.event_delegator_request(o); }); - rcmail.addEventListener('requestcalendar/itip-status', function(o) { rcmail.event_delegator_request(o); }); - rcmail.addEventListener('requestcalendar/itip-remove', function(o) { rcmail.event_delegator_request(o); }); - + if (rcmail.env.task == 'mail' || rcmail.env.task == 'calendar' || rcmail.env.task == 'tasks') { + // set delegator context for calendar/tasklist requests on invitation message + rcmail.addEventListener('requestcalendar/event', function(o) { rcmail.event_delegator_request(o); }) + .addEventListener('requestcalendar/mailimportitip', function(o) { rcmail.event_delegator_request(o); }) + .addEventListener('requestcalendar/itip-status', function(o) { rcmail.event_delegator_request(o); }) + .addEventListener('requestcalendar/itip-remove', function(o) { rcmail.event_delegator_request(o); }) + .addEventListener('requesttasks/task', function(o) { rcmail.event_delegator_request(o); }) + .addEventListener('requesttasks/mailimportitip', function(o) { rcmail.event_delegator_request(o); }) + .addEventListener('requesttasks/itip-status', function(o) { rcmail.event_delegator_request(o); }) + .addEventListener('requesttasks/itip-remove', function(o) { rcmail.event_delegator_request(o); }); + + // Calendar UI if (rcmail.env.delegators && window.rcube_calendar_ui) { - rcmail.calendar_identity_init(); + rcmail.calendar_identity_init('calendar'); // delegator context for calendar event form - rcmail.addEventListener('calendar-event-init', function(o) { return rcmail.calendar_event_init(o); }); + rcmail.addEventListener('calendar-event-init', function(o) { return rcmail.calendar_event_init(o, 'calendar'); }); // change organizer identity on calendar folder change - $('#edit-calendar').change(function() { rcmail.calendar_change(); }); + $('#edit-calendar').change(function() { rcmail.calendar_folder_change(this); }); + } + // Tasks UI + else if (rcmail.env.delegators && window.rcube_tasklist_ui) { + rcmail.calendar_identity_init('tasklist'); + // delegator context for task form + rcmail.addEventListener('tasklist-task-init', function(o) { return rcmail.calendar_event_init(o, 'tasklist'); }); + // change organizer identity on tasks folder change + $('#taskedit-tasklist').change(function() { rcmail.calendar_folder_change(this); }); } } else if (rcmail.env.task != 'settings') return; if (/^plugin.delegation/.test(rcmail.env.action)) { rcmail.addEventListener('plugin.delegate_save_complete', function(e) { rcmail.delegate_save_complete(e); }); if (rcmail.gui_objects.delegatelist) { rcmail.delegatelist = new rcube_list_widget(rcmail.gui_objects.delegatelist, { multiselect:true, draggable:false, keyboard:true }); rcmail.delegatelist.addEventListener('select', function(o) { rcmail.select_delegate(o); }) .init(); rcmail.enable_command('delegate-add', true); } else { rcmail.enable_command('delegate-save', true); var input = $('#delegate'); // delegate autocompletion if (input.length) { rcmail.init_address_input_events(input, {action: 'settings/plugin.delegation-autocomplete'}); rcmail.env.recipients_delimiter = ''; input.focus(); } // folders list $('input.write').change(function(e) { if (this.checked) $('input.read', this.parentNode.parentNode).prop('checked', true); }); $('input.read').change(function(e) { if (!this.checked) $('input.write', this.parentNode.parentNode).prop('checked', false); }); var fn = function(elem) { var classname = elem.className, list = $(elem).closest('table').find('input.' + classname), check = list.not(':checked').length > 0; list.prop('checked', check).change(); }; $('th.read,th.write').click(function() { fn(this); }) .keydown(function(e) { if (e.which == 13 || e.which == 32) fn(this); }); } } }); // delegates list onclick even handler rcube_webmail.prototype.select_delegate = function(list) { this.env.active_delegate = list.get_single_selection(); if (this.env.active_delegate) this.delegate_select(this.env.active_delegate); else if (this.env.contentframe) this.show_contentframe(false); }; // select delegate rcube_webmail.prototype.delegate_select = function(id) { var win, target = window, url = '&_action=plugin.delegation'; if (id) url += '&_id='+urlencode(id); else { this.show_contentframe(false); return; } if (win = this.get_frame_window(this.env.contentframe)) { target = win; url += '&_framed=1'; } if (String(target.location.href).indexOf(url) >= 0) this.show_contentframe(true); else this.location_href(this.env.comm_path+url, target, true); }; // display new delegate form rcube_webmail.prototype.delegate_add = function() { var win, target = window, url = '&_action=plugin.delegation'; this.delegatelist.clear_selection(); this.env.active_delegate = null; this.show_contentframe(false); if (win = this.get_frame_window(this.env.contentframe)) { target = win; url += '&_framed=1'; } this.location_href(this.env.comm_path+url, target, true); }; // handler for delete commands rcube_webmail.prototype.delegate_delete = function() { if (!this.env.active_delegate) return; var $dialog = $("#delegate-delete-dialog").addClass('uidialog'), buttons = {}; buttons[this.gettext('no', 'kolab_delegation')] = function() { $dialog.dialog('close'); }; buttons[this.gettext('yes', 'kolab_delegation')] = function() { $dialog.dialog('close'); var lock = rcmail.set_busy(true, 'kolab_delegation.savingdata'); rcmail.http_post('plugin.delegation-delete', {id: rcmail.env.active_delegate, acl: $("#delegate-delete-dialog input:checked").length}, lock); } // open jquery UI dialog $dialog.dialog({ modal: true, resizable: false, closeOnEscape: true, title: this.gettext('deleteconfirm', 'kolab_delegation'), close: function() { $dialog.dialog('destroy').hide(); }, buttons: buttons, width: 400 }).show(); }; // submit delegate form to the server rcube_webmail.prototype.delegate_save = function() { var data = {id: this.env.active_delegate}, lock = this.set_busy(true, 'kolab_delegation.savingdata'); // new delegate if (!data.id) { data.newid = $('#delegate').val().replace(/(^\s+|[\s,]+$)/, ''); if (data.newid.match(/\s*\(([^)]+)\)$/)) data.newid = RegExp.$1; } data.folders = {}; $('input.read').each(function(i, elem) { data.folders[elem.value] = this.checked ? 1 : 0; }); $('input.write:checked').each(function(i, elem) { data.folders[elem.value] = 2; }); this.http_post('plugin.delegation-save', data, lock); }; // callback function when saving/deleting has completed successfully rcube_webmail.prototype.delegate_save_complete = function(p) { // delegate created if (p.created) { var input = $('#delegate'), row = $(''), rc = this.is_framed() ? parent.rcmail : this; // remove delegate input input.parent().append($('').text(p.name)); input.remove(); // add delegate row to the list row.attr('id', 'rcmrow'+p.created); $('td', row).text(p.name); rc.delegatelist.insert_row(row.get(0)); rc.delegatelist.highlight_row(p.created); this.env.active_delegate = p.created; rc.env.active_delegate = p.created; rc.enable_command('delegate-delete', true); } // delegate updated else if (p.updated) { // do nothing } // delegate deleted else if (p.deleted) { this.env.active_delegate = null; this.delegate_select(); this.delegatelist.remove_row(p.deleted); this.enable_command('delegate-delete', false); } }; rcube_webmail.prototype.event_delegator_request = function(data) { if (!this.env.delegator_context) return; if (typeof data === 'object') data._context = this.env.delegator_context; else data += '&_context=' + this.env.delegator_context; return data; }; -// callback for calendar event form initialization -rcube_webmail.prototype.calendar_event_init = function(data) +// callback for calendar event/task form initialization +rcube_webmail.prototype.calendar_event_init = function(data, type) { + var folder = data.o[type == 'calendar' ? 'calendar' : 'list'] + // set identity for delegator context - this.env.calendar_settings.identity = this.calendar_folder_delegator(data.o.calendar); + this.env[type + '_settings'].identity = this.calendar_folder_delegator(folder); }; -// returns delegator's identity data according to selected calendar folder -rcube_webmail.prototype.calendar_folder_delegator = function(calendar) +// returns delegator's identity data according to selected calendar/tasks folder +rcube_webmail.prototype.calendar_folder_delegator = function(folder, type) { - var d, delegator; + var d, delegator, + settings = this.env[type + '_settings'], + list = this.env[type == 'calendar' ? 'calendars' : 'tasklists']; // derive delegator from the calendar owner property - if (this.env.calendars[calendar] && this.env.calendars[calendar].owner) { - delegator = this.env.calendars[calendar].owner.replace(/@.+$/, ''); + if (list[folder] && list[folder].owner) { + delegator = list[folder].owner.replace(/@.+$/, ''); } if (delegator && (d = this.env.delegators[delegator])) { // find delegator's identity id if (!d.identity_id) - $.each(this.env.calendar_settings.identities, function(i, v) { + $.each(settings.identities, function(i, v) { if (d.email == v) { d.identity_id = i; return false; } }); d.uid = delegator; } else d = this.env.original_identity; this.env.delegator_context = d.uid; return d; }; -// handler for calendar folder change -rcube_webmail.prototype.calendar_change = function() +// handler for calendar/tasklist folder change +rcube_webmail.prototype.calendar_folder_change = function(element) { - var calendar = $('#edit-calendar').val(), + var folder = $(element).val(), + type = element.id.indexOf('task') > -1 ? 'tasklist' : 'calendar', + sname = type + '_settings', select = $('#edit-identities-list'), - old = this.env.calendar_settings.identity; + old = this.env[sname].identity; - this.env.calendar_settings.identity = this.calendar_folder_delegator(calendar); + this.env[sname].identity = this.calendar_folder_delegator(folder, type); // change organizer identity in identity selector - if (select.length && old != this.env.calendar_settings.identity) { - var id = this.env.calendar_settings.identity.identity_id; + if (select.length && old != this.env[sname].identity) { + var id = this.env[sname].identity.identity_id; select.val(id || select.find('option').first().val()).change(); } }; // modify default identity of the user -rcube_webmail.prototype.calendar_identity_init = function() +rcube_webmail.prototype.calendar_identity_init = function(type) { - var identity = this.env.calendar_settings.identity, + var identity = this.env[type + '_settings'].identity, emails = identity.emails.split(';'); // remove delegators' emails from list of emails of the current user emails = $.map(emails, function(v) { for (var n in rcmail.env.delegators) if (rcmail.env.delegators[n].emails.indexOf(';'+v) > -1) return null; return v; }); identity.emails = emails.join(';'); this.env.original_identity = identity; -} +}; diff --git a/plugins/kolab_delegation/kolab_delegation.php b/plugins/kolab_delegation/kolab_delegation.php index 31e55d34..c6dd34d9 100644 --- a/plugins/kolab_delegation/kolab_delegation.php +++ b/plugins/kolab_delegation/kolab_delegation.php @@ -1,547 +1,566 @@ * @author Thomas Bruederli * * Copyright (C) 2011-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 kolab_delegation extends rcube_plugin { - public $task = 'login|mail|settings|calendar'; + public $task = 'login|mail|settings|calendar|tasks'; private $rc; private $engine; /** * Plugin initialization. */ public function init() { $this->rc = rcube::get_instance(); $this->require_plugin('libkolab'); $this->require_plugin('kolab_auth'); // on-login delegation initialization $this->add_hook('login_after', array($this, 'login_hook')); // on-check-recent delegation support $this->add_hook('check_recent', array($this, 'check_recent_hook')); // on-message-send delegation support $this->add_hook('message_before_send', array($this, 'message_before_send')); - // delegation support in Calendar plugin + // delegation support in Calendar and Tasklist plugins $this->add_hook('message_load', array($this, 'message_load')); $this->add_hook('calendar_user_emails', array($this, 'calendar_user_emails')); $this->add_hook('calendar_list_filter', array($this, 'calendar_list_filter')); $this->add_hook('calendar_load_itip', array($this, 'calendar_load_itip')); + $this->add_hook('tasklist_list_filter', array($this, 'tasklist_list_filter')); // delegation support in kolab_auth plugin $this->add_hook('kolab_auth_emails', array($this, 'kolab_auth_emails')); if ($this->rc->task == 'settings') { // delegation management interface $this->register_action('plugin.delegation', array($this, 'controller_ui')); $this->register_action('plugin.delegation-delete', array($this, 'controller_action')); $this->register_action('plugin.delegation-save', array($this, 'controller_action')); $this->register_action('plugin.delegation-autocomplete', array($this, 'controller_action')); $this->add_hook('settings_actions', array($this, 'settings_actions')); if ($this->rc->action == 'plugin.delegation' || empty($_REQUEST['_framed'])) { $this->add_texts('localization/', array('deleteconfirm', 'savingdata', 'yes', 'no')); if ($this->rc->action == 'plugin.delegation') { $this->include_script('kolab_delegation.js'); } $this->skin_path = $this->local_skin_path(); $this->include_stylesheet($this->skin_path . '/style.css'); } } - // Calendar plugin UI bindings - else if ($this->rc->task == 'calendar' && empty($_REQUEST['_framed'])) { + // Calendar/Tasklist plugin UI bindings + else if (($this->rc->task == 'calendar' || $this->rc->task == 'tasks') + && empty($_REQUEST['_framed']) + ) { if ($this->rc->output->type == 'html') { $this->calendar_ui(); } } } /** * Adds Delegation section in Settings */ function settings_actions($args) { $args['actions'][] = array( 'action' => 'plugin.delegation', 'class' => 'delegation', 'label' => 'tabtitle', 'domain' => 'kolab_delegation', 'title' => 'delegationtitle', ); return $args; } /** * Engine object getter */ private function engine() { if (!$this->engine) { require_once $this->home . '/kolab_delegation_engine.php'; $this->load_config(); $this->engine = new kolab_delegation_engine(); } return $this->engine; } /** * On-login action */ public function login_hook($args) { // Manage (create) identities for delegator's email addresses // and subscribe to delegator's folders. Also remove identities // after delegation is removed $engine = $this->engine(); $engine->delegation_init(); return $args; } /** * Check-recent action */ public function check_recent_hook($args) { // Checking for new messages shall be extended to Inbox folders of all // delegators if 'check_all_folders' is set to false. if ($this->rc->task != 'mail') { return $args; } if (!empty($args['all'])) { return $args; } if (empty($_SESSION['delegators'])) { return $args; } $storage = $this->rc->get_storage(); $other_ns = $storage->get_namespace('other'); $folders = $storage->list_folders_subscribed('', '*', 'mail'); foreach (array_keys($_SESSION['delegators']) as $uid) { foreach ($other_ns as $ns) { $folder = $ns[0] . $uid; if (in_array($folder, $folders) && !in_array($folder, $args['folders'])) { $args['folders'][] = $folder; } } } return $args; } /** * Mail send action */ public function message_before_send($args) { // Checking headers of email being send, we'll add // Sender: header if mail is send on behalf of someone else if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $engine->delegator_delivery_filter($args); } return $args; } /** * E-mail message loading action */ public function message_load($args) { // This is a place where we detect delegate context // So we can handle event invitations on behalf of delegator // @TODO: should we do this only in delegators' folders? // skip invalid messages or Kolab objects (for better performance) if (empty($args['object']->headers) || $args['object']->headers->get('x-kolab-type', false)) { return $args; } - $engine = $this->engine(); + $engine = $this->engine(); $context = $engine->delegator_context_from_message($args['object']); if ($context) { $this->rc->output->set_env('delegator_context', $context); $this->include_script('kolab_delegation.js'); } return $args; } /** * calendar::get_user_emails() handler */ public function calendar_user_emails($args) { // In delegator context we'll use delegator's addresses // instead of current user addresses if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $engine->delegator_emails_filter($args); } return $args; } /** * calendar_driver::list_calendars() handler */ public function calendar_list_filter($args) { // In delegator context we'll use delegator's folders // instead of current user folders if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); - $engine->delegator_folder_filter($args); + $engine->delegator_folder_filter($args, 'calendars'); + } + + return $args; + } + + /** + * tasklist_driver::get_lists() handler + */ + public function tasklist_list_filter($args) + { + // In delegator context we'll use delegator's folders + // instead of current user folders + + if (!empty($_SESSION['delegators'])) { + $engine = $this->engine(); + $engine->delegator_folder_filter($args, 'tasklists'); } return $args; } /** * calendar::load_itip() handler */ public function calendar_load_itip($args) { // In delegator context we'll use delegator's address/name // for invitation responses if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $engine->delegator_identity_filter($args); } return $args; } /** - * Delegation support in Calendar plugin UI + * Delegation support in Calendar/Tasks plugin UI */ public function calendar_ui() { // Initialize handling of delegators' identities in event form if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $this->rc->output->set_env('namespace', $engine->namespace_js()); $this->rc->output->set_env('delegators', $engine->list_delegators_js()); $this->include_script('kolab_delegation.js'); } } /** * Delegation support in kolab_auth plugin */ public function kolab_auth_emails($args) { // Add delegators addresses to address selector in user identity form if (!empty($_SESSION['delegators'])) { // @TODO: Consider not adding all delegator addresses to the list. // Instead add only address of currently edited identity foreach ($_SESSION['delegators'] as $emails) { $args['emails'] = array_merge($args['emails'], $emails); } $args['emails'] = array_unique($args['emails']); sort($args['emails']); } return $args; } /** * Delegation UI handler */ public function controller_ui() { // main interface (delegates list) if (empty($_REQUEST['_framed'])) { $this->register_handler('plugin.delegatelist', array($this, 'delegate_list')); $this->rc->output->include_script('list.js'); $this->rc->output->send('kolab_delegation.settings'); } // delegate frame else { $this->register_handler('plugin.delegateform', array($this, 'delegate_form')); $this->register_handler('plugin.delegatefolders', array($this, 'delegate_folders')); $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'); $this->rc->output->send('kolab_delegation.editform'); } } /** * Delegation action handler */ public function controller_action() { $this->add_texts('localization/'); $engine = $this->engine(); // Delegate delete if ($this->rc->action == 'plugin.delegation-delete') { $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GPC); $success = $engine->delegate_delete($id, (bool) rcube_utils::get_input_value('acl', rcube_utils::INPUT_GPC)); if ($success) { $this->rc->output->show_message($this->gettext('deletesuccess'), 'confirmation'); $this->rc->output->command('plugin.delegate_save_complete', array('deleted' => $id)); } else { $this->rc->output->show_message($this->gettext('deleteerror'), 'error'); } } // Delegate add/update else if ($this->rc->action == 'plugin.delegation-save') { $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GPC); $acl = rcube_utils::get_input_value('folders', rcube_utils::INPUT_GPC); // update if ($id) { $delegate = $engine->delegate_get($id); $success = $engine->delegate_acl_update($delegate['uid'], $acl); if ($success) { $this->rc->output->show_message($this->gettext('updatesuccess'), 'confirmation'); $this->rc->output->command('plugin.delegate_save_complete', array('updated' => $id)); } else { $this->rc->output->show_message($this->gettext('updateerror'), 'error'); } } // new else { $login = rcube_utils::get_input_value('newid', rcube_utils::INPUT_GPC); $delegate = $engine->delegate_get_by_name($login); $success = $engine->delegate_add($delegate, $acl); if ($success) { $this->rc->output->show_message($this->gettext('createsuccess'), 'confirmation'); $this->rc->output->command('plugin.delegate_save_complete', array( 'created' => $delegate['ID'], 'name' => $delegate['name'], )); } else { $this->rc->output->show_message($this->gettext('createerror'), 'error'); } } } // Delegate autocompletion else if ($this->rc->action == 'plugin.delegation-autocomplete') { $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true); $reqid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC); $users = $engine->list_users($search); $this->rc->output->command('ksearch_query_results', $users, $search, $reqid); } $this->rc->output->send(); } /** * Template object of delegates list */ public function delegate_list($attrib = array()) { $attrib += array('id' => 'delegate-list'); $engine = $this->engine(); $list = $engine->list_delegates(); $table = new html_table(); // sort delegates list asort($list, SORT_LOCALE_STRING); foreach ($list as $id => $delegate) { $table->add_row(array('id' => 'rcmrow' . $id)); $table->add(null, rcube::Q($delegate)); } $this->rc->output->add_gui_object('delegatelist', $attrib['id']); $this->rc->output->set_env('delegatecount', count($list)); return $table->show($attrib); } /** * Template object of delegate form */ public function delegate_form($attrib = array()) { $engine = $this->engine(); $table = new html_table(array('cols' => 2)); $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $field_id = 'delegate'; if ($id) { $delegate = $engine->delegate_get($id); } if ($delegate) { $input = new html_hiddenfield(array('name' => $field_id, 'id' => $field_id, 'size' => 40)); $input = rcube::Q($delegate['name']) . $input->show($id); $this->rc->output->set_env('active_delegate', $id); $this->rc->output->command('parent.enable_command','delegate-delete', true); } else { $input = new html_inputfield(array('name' => $field_id, 'id' => $field_id, 'size' => 40)); $input = $input->show(); } $table->add('title', html::label($field_id, $this->gettext('delegate'))); $table->add(null, $input); if ($attrib['form']) { $this->rc->output->add_gui_object('editform', $attrib['form']); } return $table->show($attrib); } /** * Template object of folders list */ public function delegate_folders($attrib = array()) { if (!$attrib['id']) { $attrib['id'] = 'delegatefolders'; } $engine = $this->engine(); $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); if ($id) { $delegate = $engine->delegate_get($id); } $folder_data = $engine->list_folders($delegate['uid']); $rights = array(); $folder_groups = array(); foreach ($folder_data as $folder_name => $folder) { $folder_groups[$folder['type']][] = $folder_name; $rights[$folder_name] = $folder['rights']; } // build block for every folder type foreach ($folder_groups as $type => $group) { if (empty($group)) { continue; } $attrib['type'] = $type; $html .= html::div('foldersblock', html::tag('h3', $type, $this->gettext($type)) . $this->delegate_folders_block($group, $attrib, $rights)); } $this->rc->output->add_gui_object('folderslist', $attrib['id']); return html::div($attrib, $html); } /** * List of folders in specified group */ private function delegate_folders_block($a_folders, $attrib, $rights) { $path = 'plugins/kolab_delegation/' . $this->skin_path . '/'; $read_ico = $attrib['readicon'] ? html::img(array('src' => $path . $attrib['readicon'], 'title' => $this->gettext('read'))) : ''; $write_ico = $attrib['writeicon'] ? html::img(array('src' => $path . $attrib['writeicon'], 'title' => $this->gettext('write'))) : ''; $table = new html_table(array('cellspacing' => 0)); $table->add_header(array('class' => 'read', 'title' => $this->gettext('read'), 'tabindex' => 0), $read_ico); $table->add_header(array('class' => 'write', 'title' => $this->gettext('write'), 'tabindex' => 0), $write_ico); $table->add_header('foldername', $this->rc->gettext('folder')); $checkbox_read = new html_checkbox(array('name' => 'read[]', 'class' => 'read')); $checkbox_write = new html_checkbox(array('name' => 'write[]', 'class' => 'write')); $names = array(); foreach ($a_folders as $folder) { $foldername = $origname = preg_replace('/^INBOX »\s+/', '', kolab_storage::object_name($folder)); // find folder prefix to truncate (the same code as in kolab_addressbook plugin) for ($i = count($names)-1; $i >= 0; $i--) { if (strpos($foldername, $names[$i].' » ') === 0) { $length = strlen($names[$i].' » '); $prefix = substr($foldername, 0, $length); $count = count(explode(' » ', $prefix)); $foldername = str_repeat('  ', $count-1) . '» ' . substr($foldername, $length); break; } } $folder_id = 'rcmf' . rcube_utils::html_identifier($folder); $names[] = $origname; $classes = array('mailbox'); if ($folder_class = $this->rc->folder_classname($folder)) { $foldername = html::quote($this->rc->gettext($folder_class)); $classes[] = $folder_class; } $table->add_row(); $table->add('read', $checkbox_read->show( $rights[$folder] >= kolab_delegation_engine::ACL_READ ? $folder : null, array('value' => $folder))); $table->add('write', $checkbox_write->show( $rights[$folder] >= kolab_delegation_engine::ACL_WRITE ? $folder : null, array('value' => $folder, 'id' => $folder_id))); $table->add(join(' ', $classes), html::label($folder_id, $foldername)); } return $table->show(); } } diff --git a/plugins/kolab_delegation/kolab_delegation_engine.php b/plugins/kolab_delegation/kolab_delegation_engine.php index de1d9cf8..eae73477 100644 --- a/plugins/kolab_delegation/kolab_delegation_engine.php +++ b/plugins/kolab_delegation/kolab_delegation_engine.php @@ -1,943 +1,955 @@ * @author Aleksander Machniak * * Copyright (C) 2011-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 kolab_delegation_engine { public $context; private $rc; private $ldap_filter; private $ldap_delegate_field; private $ldap_login_field; private $ldap_name_field; private $ldap_email_field; private $ldap_org_field; private $ldap_dn; private $cache = array(); private $folder_types = array('mail', 'event', 'task'); const ACL_READ = 1; const ACL_WRITE = 2; /** * Class constructor */ public function __construct() { $this->rc = rcube::get_instance(); } /** * Add delegate * * @param string|array $delegate Delegate DN (encoded) or delegate data (result of delegate_get()) * @param array $acl List of folder->right map */ public function delegate_add($delegate, $acl) { if (!is_array($delegate)) { $delegate = $this->delegate_get($delegate); } $dn = $delegate['ID']; if (empty($delegate) || empty($dn)) { return false; } $list = $this->list_delegates(); // add delegate to the list $list = array_keys((array)$list); $list = array_filter($list); if (!in_array($dn, $list)) { $list[] = $dn; } $list = array_map(array('kolab_auth_ldap', 'dn_decode'), $list); // update user record $result = $this->user_update_delegates($list); // Set ACL on folders if ($result && !empty($acl)) { $this->delegate_acl_update($delegate['uid'], $acl); } return $result; } /** * Set/Update ACL on delegator's folders * * @param string $uid Delegate authentication identifier * @param array $acl List of folder->right map * @param bool $update Update (remove) old rights */ public function delegate_acl_update($uid, $acl, $update = false) { $storage = $this->rc->get_storage(); $right_types = $this->right_types(); $folders = $update ? $this->list_folders($uid) : array(); foreach ($acl as $folder_name => $rights) { $r = $right_types[$rights]; if ($r) { $storage->set_acl($folder_name, $uid, $r); } else { $storage->delete_acl($folder_name, $uid); } if (!empty($folders) && isset($folders[$folder_name])) { unset($folders[$folder_name]); } } foreach ($folders as $folder_name => $folder) { if ($folder['rights']) { $storage->delete_acl($folder_name, $uid); } } return true; } /** * Delete delgate * * @param string $dn Delegate DN (encoded) * @param bool $acl_del Enable ACL deletion on delegator folders */ public function delegate_delete($dn, $acl_del = false) { $delegate = $this->delegate_get($dn); $list = $this->list_delegates(); $user = $this->user(); if (empty($delegate) || !isset($list[$dn])) { return false; } // remove delegate from the list unset($list[$dn]); $list = array_keys($list); $list = array_map(array('kolab_auth_ldap', 'dn_decode'), $list); $user[$this->ldap_delegate_field] = $list; // update user record $result = $this->user_update_delegates($list); // remove ACL if ($result && $acl_del) { $this->delegate_acl_update($delegate['uid'], array(), true); } return $result; } /** * Return delegate data * * @param string $dn Delegate DN (encoded) * * @return array Delegate record (ID, name, uid, imap_uid) */ public function delegate_get($dn) { // use internal cache so we not query LDAP more than once per request if (!isset($this->cache[$dn])) { $ldap = $this->ldap(); if (!$ldap || empty($dn)) { return array(); } // Get delegate $user = $ldap->get_record(kolab_auth_ldap::dn_decode($dn)); if (empty($user)) { return array(); } $delegate = $this->parse_ldap_record($user); $delegate['ID'] = $dn; $this->cache[$dn] = $delegate; } return $this->cache[$dn]; } /** * Return delegate data * * @param string $login Delegate name (the 'uid' returned in get_users()) * * @return array Delegate record (ID, name, uid, imap_uid) */ public function delegate_get_by_name($login) { $ldap = $this->ldap(); if (!$ldap || empty($login)) { return array(); } $list = $ldap->dosearch($this->ldap_login_field, $login, 1); if (count($list) == 1) { $dn = key($list); $user = $list[$dn]; return $this->parse_ldap_record($user, $dn); } } /** * LDAP object getter */ private function ldap() { $ldap = kolab_auth::ldap(); if (!$ldap || !$ldap->ready) { return null; } // Default filter of LDAP queries $this->ldap_filter = $this->rc->config->get('kolab_delegation_filter', '(|(objectClass=kolabInetOrgPerson)(&(objectclass=kolabsharedfolder)(kolabFolderType=mail)))'); // Name of the LDAP field for delegates list $this->ldap_delegate_field = $this->rc->config->get('kolab_delegation_delegate_field', 'kolabDelegate'); // Encoded LDAP DN of current user, set on login by kolab_auth plugin $this->ldap_dn = $_SESSION['kolab_dn']; // Name of the LDAP field with authentication ID $this->ldap_login_field = $this->rc->config->get('kolab_auth_login'); // Name of the LDAP field with user name used for identities $this->ldap_name_field = $this->rc->config->get('kolab_auth_name'); // Name of the LDAP field with email addresses used for identities $this->ldap_email_field = $this->rc->config->get('kolab_auth_email'); // Name of the LDAP field with organization name for identities $this->ldap_org_field = $this->rc->config->get('kolab_auth_organization'); $ldap->set_filter($this->ldap_filter); $ldap->extend_fieldmap(array($this->ldap_delegate_field => $this->ldap_delegate_field)); return $ldap; } /** * List current user delegates */ public function list_delegates() { $result = array(); $ldap = $this->ldap(); $user = $this->user(); if (empty($ldap) || empty($user)) { return array(); } // Get delegates of current user $delegates = $user[$this->ldap_delegate_field]; if (!empty($delegates)) { foreach ((array)$delegates as $dn) { $delegate = $ldap->get_record($dn); $data = $this->parse_ldap_record($delegate, $dn); if (!empty($data) && !empty($data['name'])) { $result[$data['ID']] = $data['name']; } } } return $result; } /** * List current user delegators * * @return array List of delegators */ public function list_delegators() { $result = array(); $ldap = $this->ldap(); if (empty($ldap) || empty($this->ldap_dn)) { return array(); } $list = $ldap->dosearch($this->ldap_delegate_field, $this->ldap_dn, 1); foreach ($list as $dn => $delegator) { $delegator = $this->parse_ldap_record($delegator, $dn); $result[$delegator['ID']] = $delegator; } return $result; } /** * List current user delegators in format compatible with Calendar plugin * * @return array List of delegators */ public function list_delegators_js() { $list = $this->list_delegators(); $result = array(); foreach ($list as $delegator) { $name = $delegator['name']; if ($pos = strrpos($name, '(')) { $name = trim(substr($name, 0, $pos)); } $result[$delegator['imap_uid']] = array( 'emails' => ';' . implode(';', $delegator['email']), 'email' => $delegator['email'][0], 'name' => $name, ); } return $result; } /** * Prepare namespace prefixes for JS environment * * @return array List of prefixes */ public function namespace_js() { $storage = $this->rc->get_storage(); $ns = $storage->get_namespace('other'); if ($ns) { foreach ($ns as $idx => $nsval) { $ns[$idx] = kolab_storage::folder_id($nsval[0]); } } return $ns; } /** * Get all folders to which current user has admin access * * @param string $delegate IMAP user identifier * * @return array Folder type/rights */ public function list_folders($delegate = null) { $storage = $this->rc->get_storage(); $folders = $storage->list_folders(); $metadata = kolab_storage::folders_typedata(); $result = array(); if (!is_array($metadata)) { return $result; } // Definition of read and write ACL $right_types = $this->right_types(); foreach ($folders as $folder) { // get only folders in personal namespace if ($storage->folder_namespace($folder) != 'personal') { continue; } $rights = null; $type = $metadata[$folder] ?: 'mail'; list($class, $subclass) = explode('.', $type); if (!in_array($class, $this->folder_types)) { continue; } // in edit mode, get folder ACL if ($delegate) { // @TODO: cache ACL $acl = $storage->get_acl($folder); if ($acl = $acl[$delegate]) { if ($this->acl_compare($acl, $right_types[self::ACL_WRITE])) { $rights = self::ACL_WRITE; } else if ($this->acl_compare($acl, $right_types[self::ACL_READ])) { $rights = self::ACL_READ; } } } else if ($folder == 'INBOX' || $subclass == 'default' || $subclass == 'inbox') { $rights = self::ACL_WRITE; } $result[$folder] = array( 'type' => $class, 'rights' => $rights, ); } return $result; } /** * Returns list of users for autocompletion * * @param string $search Search string * * @return array Users list */ public function list_users($search) { $ldap = $this->ldap(); if (empty($ldap) || $search === '' || $search === null) { return array(); } $max = (int) $this->rc->config->get('autocomplete_max', 15); $mode = (int) $this->rc->config->get('addressbook_search_mode'); $fields = array_unique(array_filter(array_merge((array)$this->ldap_name_field, (array)$this->ldap_login_field))); $users = array(); $keys = array(); $result = $ldap->dosearch($fields, $search, $mode, (array)$this->ldap_login_field, $max); foreach ($result as $record) { // skip self if ($record['dn'] == $_SESSION['kolab_dn']) { continue; } $user = $this->parse_ldap_record($record); if ($user['uid']) { $display = rcube_addressbook::compose_search_name($record); $user = array('name' => $user['uid'], 'display' => $display); $users[] = $user; $keys[] = $display ?: $user['uid']; } } if (count($users)) { // sort users index asort($keys, SORT_LOCALE_STRING); // re-sort users according to index foreach (array_keys($keys) as $idx) { $keys[$idx] = $users[$idx]; } $users = array_values($keys); } return $users; } /** * Extract delegate identifiers and pretty name from LDAP record */ private function parse_ldap_record($data, $dn = null) { $email = array(); $uid = $data[$this->ldap_login_field]; if (is_array($uid)) { $uid = array_filter($uid); $uid = $uid[0]; } // User name for identity foreach ((array)$this->ldap_name_field as $field) { $name = is_array($data[$field]) ? $data[$field][0] : $data[$field]; if (!empty($name)) { break; } } // User email(s) for identity foreach ((array)$this->ldap_email_field as $field) { $user_email = is_array($data[$field]) ? array_filter($data[$field]) : $data[$field]; if (!empty($user_email)) { $email = array_merge((array)$email, (array)$user_email); } } // Organization for identity foreach ((array)$this->ldap_org_field as $field) { $organization = is_array($data[$field]) ? $data[$field][0] : $data[$field]; if (!empty($organization)) { break; } } $realname = $name; if ($uid && $name) { $name .= ' (' . $uid . ')'; } else { $name = $uid; } // get IMAP uid - identifier used in shared folder hierarchy $imap_uid = $uid; if ($pos = strpos($imap_uid, '@')) { $imap_uid = substr($imap_uid, 0, $pos); } return array( 'ID' => kolab_auth_ldap::dn_encode($dn), 'uid' => $uid, 'name' => $name, 'realname' => $realname, 'imap_uid' => $imap_uid, 'email' => $email, 'organization' => $organization, ); } /** * Returns LDAP record of current user * * @return array User data */ public function user($parsed = false) { if (!isset($this->cache['user'])) { $ldap = $this->ldap(); if (!$ldap) { return array(); } // Get current user record $this->cache['user'] = $ldap->get_record($this->ldap_dn); } return $parsed ? $this->parse_ldap_record($this->cache['user']) : $this->cache['user']; } /** * Update LDAP record of current user * * @param array List of delegates */ public function user_update_delegates($list) { $ldap = $this->ldap(); $pass = $this->rc->decrypt($_SESSION['password']); if (!$ldap) { return false; } // need to bind as self for sufficient privilages if (!$ldap->bind($this->ldap_dn, $pass)) { return false; } $user[$this->ldap_delegate_field] = $list; unset($this->cache['user']); // replace delegators list in user record return $ldap->replace($this->ldap_dn, $user); } /** * Manage delegation data on user login */ public function delegation_init() { // Fetch all delegators from LDAP who assigned the // current user as their delegate and create identities // a) if identity with delegator's email exists, continue // b) create identity ($delegate on behalf of $delegator // <$delegator-email>) for new delegators // c) remove all other identities which do not match the user's primary // or alias email if 'kolab_delegation_purge_identities' is set. $delegators = $this->list_delegators(); $use_subs = $this->rc->config->get('kolab_use_subscriptions'); $identities = $this->rc->user->list_emails(); $emails = array(); $uids = array(); if (!empty($delegators)) { $storage = $this->rc->get_storage(); $other_ns = $storage->get_namespace('other'); $folders = $storage->list_folders(); } // convert identities to simpler format for faster access foreach ($identities as $idx => $ident) { // get user name from default identity if (!$idx) { $default = array( 'name' => $ident['name'], ); } $emails[$ident['identity_id']] = $ident['email']; } // for every delegator... foreach ($delegators as $delegator) { $uids[$delegator['imap_uid']] = $email_arr = $delegator['email']; $diff = array_intersect($emails, $email_arr); // identity with delegator's email already exist, do nothing if (count($diff)) { $emails = array_diff($emails, $email_arr); continue; } // create identities for delegator emails foreach ($email_arr as $email) { // @TODO: "Delegatorname" or "Username on behalf of Delegatorname"? $default['name'] = $delegator['realname']; $default['email'] = $email; // Database field for organization is NOT NULL $default['organization'] = empty($delegator['organization']) ? '' : $delegator['organization']; $this->rc->user->insert_identity($default); } // IMAP folders shared by new delegators shall be subscribed on login, // as well as existing subscriptions of previously shared folders shall // be removed. I suppose the latter one is already done in Roundcube. // for every accessible folder... foreach ($folders as $folder) { // for every 'other' namespace root... foreach ($other_ns as $ns) { $prefix = $ns[0] . $delegator['imap_uid']; // subscribe delegator's folder if ($folder === $prefix || strpos($folder, $prefix . substr($ns[0], -1)) === 0) { // Event/Task folders need client-side activation $type = kolab_storage::folder_type($folder); if (preg_match('/^(event|task)/i', $type)) { kolab_storage::folder_activate($folder); } // Subscribe to mail folders and (if system is configured // to display only subscribed folders) to other if ($use_subs || preg_match('/^mail/i', $type)) { $storage->subscribe($folder); } } } } } // remove identities that "do not belong" to user nor delegators if ($this->rc->config->get('kolab_delegation_purge_identities')) { $user = $this->user(true); $emails = array_diff($emails, $user['email']); foreach (array_keys($emails) as $idx) { $this->rc->user->delete_identity($idx); } } $_SESSION['delegators'] = $uids; } /** * Sets delegator context according to email message recipient * * @param rcube_message $message Email message object */ public function delegator_context_from_message($message) { if (empty($_SESSION['delegators'])) { return; } // Match delegators' addresses with message To: address // @TODO: Is this reliable enough? // Roundcube sends invitations to every attendee separately, // but maybe there's a software which sends with CC header or many addresses in To: $emails = $message->get_header('to'); $emails = rcube_mime::decode_address_list($emails, null, false); foreach ($emails as $email) { foreach ($_SESSION['delegators'] as $uid => $addresses) { if (in_array($email['mailto'], $addresses)) { return $this->context = $uid; } } } } /** * Return (set) current delegator context * * @return string Delegator UID */ public function delegator_context() { if (!$this->context && !empty($_SESSION['delegators'])) { $context = rcube_utils::get_input_value('_context', rcube_utils::INPUT_GPC); if ($context && isset($_SESSION['delegators'][$context])) { $this->context = $context; } } return $this->context; } /** * Set user identity according to delegator delegator * * @param array $args Reference to plugin hook arguments */ public function delegator_identity_filter(&$args) { $context = $this->delegator_context(); if (!$context) { return; } $identities = $this->rc->user->list_emails(); $emails = $_SESSION['delegators'][$context]; foreach ($identities as $ident) { if (in_array($ident['email'], $emails)) { $args['identity'] = $ident; return; } } // fallback to default identity $args['identity'] = array_shift($identities); } /** * Filter user emails according to delegator context * * @param array $args Reference to plugin hook arguments */ public function delegator_emails_filter(&$args) { $context = $this->delegator_context(); // try to derive context from the given user email if (!$context && !empty($args['emails'])) { if (($user = preg_replace('/@.+$/', '', $args['emails'][0])) && isset($_SESSION['delegators'][$user])) { $context = $user; } } // return delegator's addresses if ($context) { $args['emails'] = $_SESSION['delegators'][$context]; $args['abort'] = true; } // return only user addresses (exclude all delegators addresses) else if (!empty($_SESSION['delegators'])) { $identities = $this->rc->user->list_emails(); $emails[] = $this->rc->user->get_username(); foreach ($identities as $identity) { $emails[] = $identity['email']; } foreach ($_SESSION['delegators'] as $delegator_emails) { $emails = array_diff($emails, $delegator_emails); } $args['emails'] = array_unique($emails); $args['abort'] = true; } } /** - * Filters list of calendars according to delegator context + * Filters list of calendar/task folders according to delegator context * * @param array $args Reference to plugin hook arguments */ - public function delegator_folder_filter(&$args) + public function delegator_folder_filter(&$args, $mode = 'calendars') { $context = $this->delegator_context(); if (empty($context)) { return $args; } - $storage = $this->rc->get_storage(); - $other_ns = $storage->get_namespace('other'); - $delim = $storage->get_hierarchy_delimiter(); - $editable = $args['filter'] & calendar_driver::FILTER_WRITEABLE; - $active = $args['filter'] & calendar_driver::FILTER_ACTIVE; - $personal = $args['filter'] & calendar_driver::FILTER_PERSONAL; - $shared = $args['filter'] & calendar_driver::FILTER_SHARED; - $calendars = array(); + $storage = $this->rc->get_storage(); + $other_ns = $storage->get_namespace('other'); + $delim = $storage->get_hierarchy_delimiter(); + + if ($mode == 'calendars') { + $editable = $args['filter'] & calendar_driver::FILTER_WRITEABLE; + $active = $args['filter'] & calendar_driver::FILTER_ACTIVE; + $personal = $args['filter'] & calendar_driver::FILTER_PERSONAL; + $shared = $args['filter'] & calendar_driver::FILTER_SHARED; + } + else { + $editable = $args['filter'] & tasklist_driver::FILTER_WRITEABLE; + $active = $args['filter'] & tasklist_driver::FILTER_ACTIVE; + $personal = $args['filter'] & tasklist_driver::FILTER_PERSONAL; + $shared = $args['filter'] & tasklist_driver::FILTER_SHARED; + } - // code parts derived from kolab_driver::filter_calendars() - foreach ($args['list'] as $cal) { - if (!$cal->ready) { + $folders = array(); + + foreach ($args['list'] as $folder) { + if (isset($folder->ready) && !$folder->ready) { continue; } - if ($editable && !$cal->editable) { + + if ($editable && !$folder->editable) { continue; } - if ($active && !$cal->storage->is_active()) { + + if ($active && !$folder->storage->is_active()) { continue; } + if ($personal || $shared) { - $ns = $cal->get_namespace(); + $ns = $folder->get_namespace(); if ($personal && $ns == 'personal') { continue; } else if ($personal && $ns == 'other') { $found = false; foreach ($other_ns as $ns) { - $folder = $ns[0] . $context . $delim; - if (strpos($cal->name, $folder) === 0) { + $c_folder = $ns[0] . $context . $delim; + if (strpos($folder->name, $c_folder) === 0) { $found = true; } } if (!$found) { continue; } } else if (!$shared || $ns != 'shared') { continue; } } - $calendars[$cal->id] = $cal; + $folders[$folder->id] = $folder; } - $args['calendars'] = $calendars; - $args['abort'] = true; + $args[$mode] = $folders; + $args['abort'] = true; } /** * Filters/updates message headers according to delegator context * * @param array $args Reference to plugin hook arguments */ public function delegator_delivery_filter(&$args) { // no context, but message still can be send on behalf of... if (!empty($_SESSION['delegators'])) { $message = $args['message']; $headers = $message->headers(); // get email address from From: header $from = rcube_mime::decode_address_list($headers['From']); $from = array_shift($from); $from = $from['mailto']; foreach ($_SESSION['delegators'] as $uid => $addresses) { if (in_array($from, $addresses)) { $context = $uid; break; } } // add Sender: header with current user default identity if ($context) { $identity = $this->rc->user->get_identity(); $sender = format_email_recipient($identity['email'], $identity['name']); $message->headers(array('Sender' => $sender), false, true); } } } /** * Compares two ACLs (according to supported rights) * * @param array $acl1 ACL rights array (or string) * @param array $acl2 ACL rights array (or string) * * @param bool True if $acl1 contains all rights from $acl2 */ function acl_compare($acl1, $acl2) { if (!is_array($acl1)) $acl1 = str_split($acl1); if (!is_array($acl2)) $acl2 = str_split($acl2); $rights = $this->rights_supported(); $acl1 = array_intersect($acl1, $rights); $acl2 = array_intersect($acl2, $rights); $res = array_intersect($acl1, $acl2); $cnt1 = count($res); $cnt2 = count($acl2); if ($cnt1 >= $cnt2) { return true; } } /** * Get list of supported access rights (according to RIGHTS capability) * * @todo: this is stolen from acl plugin, move to rcube_storage/rcube_imap * * @return array List of supported access rights abbreviations */ public function rights_supported() { if ($this->supported !== null) { return $this->supported; } $storage = $this->rc->get_storage(); $capa = $storage->get_capability('RIGHTS'); if (is_array($capa)) { $rights = strtolower($capa[0]); } else { $rights = 'cd'; } return $this->supported = str_split('lrswi' . $rights . 'pa'); } private function right_types() { // Get supported rights and build column names $supported = $this->rights_supported(); // depending on server capability either use 'te' or 'd' for deleting msgs $deleteright = implode(array_intersect(str_split('ted'), $supported)); return array( self::ACL_READ => 'lrs', self::ACL_WRITE => 'lrswi'.$deleteright, ); } } diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php index 9c92d288..af974e8f 100644 --- a/plugins/tasklist/drivers/database/tasklist_database_driver.php +++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php @@ -1,849 +1,852 @@ * * 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 $list_ids = ''; private $tags = array(); 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() + 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 ); 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::delete_list() */ public function delete_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; } /** * 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_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 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_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 ' . 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 ($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'])) . ')'); } $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 + * @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) + public function get_task($prop, $filter = 0) { 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 NOT " . self::IS_COMPLETE_SQL, 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); } /** * 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['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'], $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']); } if (is_array($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); $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, flagged, complete, status, alarms, recurrence, 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['flagged'] ? 1 : 0, intval($prop['complete']), strval($prop['status']), $prop['alarms'], $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'])) { $prop['alarms'] = $this->serialize_alarms($prop['valarms']); } if (is_array($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', 'status') 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'] && !$this->is_complete($task)) { $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_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; $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))); } } } } diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index b2b28eef..6cbdc717 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -1,1697 +1,1775 @@ * * 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_kolab_driver extends tasklist_driver { // features supported by the backend public $alarms = false; public $attachments = true; public $attendees = true; public $undelete = false; // task undelete action public $alarm_types = array('DISPLAY','AUDIO'); public $search_more_results; private $rc; private $plugin; private $lists; private $folders = array(); private $tasks = array(); private $tags = array(); private $bonnie_api = false; /** * Default constructor */ public function __construct($plugin) { $this->rc = $plugin->rc; $this->plugin = $plugin; if (kolab_storage::$version == '2.0') { $this->alarm_absolute = false; } // tasklist use fully encoded identifiers kolab_storage::$encode_ids = true; // get configuration for the Bonnie API $this->bonnie_api = libkolab::get_bonnie_api(); $this->_read_lists(); $this->plugin->register_action('folder-acl', array($this, 'folder_acl')); } /** * Read available calendars for the current user and store them internally */ private function _read_lists($force = false) { // already read sources if (isset($this->lists) && !$force) return $this->lists; // get all folders that have type "task" $folders = kolab_storage::sort_folders(kolab_storage::get_folders('task')); $this->lists = $this->folders = array(); $delim = $this->rc->get_storage()->get_hierarchy_delimiter(); // find default folder $default_index = 0; foreach ($folders as $i => $folder) { if ($folder->default && strpos($folder->name, $delim) === false) $default_index = $i; } // put default folder (aka INBOX) on top of the list if ($default_index > 0) { $default_folder = $folders[$default_index]; unset($folders[$default_index]); array_unshift($folders, $default_folder); } $prefs = $this->rc->config->get('kolab_tasklists', array()); foreach ($folders as $folder) { $tasklist = $this->folder_props($folder, $prefs); $this->lists[$tasklist['id']] = $tasklist; $this->folders[$tasklist['id']] = $folder; $this->folders[$folder->name] = $folder; } } /** * Derive list properties from the given kolab_storage_folder object */ protected function folder_props($folder, $prefs) { if ($folder->get_namespace() == 'personal') { $norename = false; $editable = true; $rights = 'lrswikxtea'; $alarms = true; } else { $alarms = false; $rights = 'lr'; $editable = false; - if (($myrights = $folder->get_myrights()) && !PEAR::isError($myrights)) { + if ($myrights = $folder->get_myrights()) { $rights = $myrights; if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) - $editable = strpos($rights, 'i'); + $editable = strpos($rights, 'i') !== false; } $info = $folder->get_folder_info(); $norename = $readonly || $info['norename'] || $info['protected']; } $list_id = $folder->id; #kolab_storage::folder_id($folder->name); $old_id = kolab_storage::folder_id($folder->name, false); if (!isset($prefs[$list_id]['showalarms']) && isset($prefs[$old_id]['showalarms'])) { $prefs[$list_id]['showalarms'] = $prefs[$old_id]['showalarms']; } return array( 'id' => $list_id, 'name' => $folder->get_name(), 'listname' => $folder->get_foldername(), 'editname' => $folder->get_foldername(), 'color' => $folder->get_color('0000CC'), 'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms, 'editable' => $editable, 'rights' => $rights, 'norename' => $norename, 'active' => $folder->is_active(), + 'owner' => $folder->get_owner(), 'parentfolder' => $folder->get_parent(), 'default' => $folder->default, 'virtual' => $folder->virtual, 'children' => true, // TODO: determine if that folder indeed has child folders 'subscribed' => (bool)$folder->is_subscribed(), 'removable' => !$folder->default, 'subtype' => $folder->subtype, 'group' => $folder->default ? 'default' : $folder->get_namespace(), 'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')), 'caldavuid' => $folder->get_uid(), 'history' => !empty($this->bonnie_api), ); } /** * Get a list of available task lists from this source + * + * @param integer Bitmask defining filter criterias. + * See FILTER_* constants for possible values. */ - public function get_lists(&$tree = null) + public function get_lists($filter = 0, &$tree = null) { // attempt to create a default list for this user if (empty($this->lists) && !isset($this->search_more_results)) { $prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true); if ($this->create_list($prop)) $this->_read_lists(true); } - $folders = array(); - foreach ($this->lists as $id => $list) { - if (!empty($this->folders[$id])) { - $folders[] = $this->folders[$id]; - } - } + $folders = $this->filter_folders($filter); // include virtual folders for a full folder tree if (!is_null($tree)) { $folders = kolab_storage::folder_hierarchy($folders, $tree); } $delim = $this->rc->get_storage()->get_hierarchy_delimiter(); $prefs = $this->rc->config->get('kolab_tasklists', array()); $lists = array(); foreach ($folders as $folder) { $list_id = $folder->id; // kolab_storage::folder_id($folder->name); $imap_path = explode($delim, $folder->name); // find parent do { array_pop($imap_path); $parent_id = kolab_storage::folder_id(join($delim, $imap_path)); } while (count($imap_path) > 1 && !$this->folders[$parent_id]); // restore "real" parent ID if ($parent_id && !$this->folders[$parent_id]) { $parent_id = kolab_storage::folder_id($folder->get_parent()); } $fullname = $folder->get_name(); $listname = $folder->get_foldername(); // special handling for virtual folders if ($folder instanceof kolab_storage_folder_user) { $lists[$list_id] = array( 'id' => $list_id, 'name' => $folder->get_name(), 'listname' => $listname, 'title' => $folder->get_title(), 'virtual' => true, 'editable' => false, 'rights' => 'l', 'group' => 'other virtual', 'class' => 'user', 'parent' => $parent_id, ); } else if ($folder->virtual) { $lists[$list_id] = array( 'id' => $list_id, 'name' => kolab_storage::object_name($fullname), 'listname' => $listname, 'virtual' => true, 'editable' => false, 'rights' => 'l', 'group' => $folder->get_namespace(), 'class' => 'folder', 'parent' => $parent_id, ); } else { if (!$this->lists[$list_id]) { $this->lists[$list_id] = $this->folder_props($folder, $prefs); $this->folders[$list_id] = $folder; } $this->lists[$list_id]['parent'] = $parent_id; $lists[$list_id] = $this->lists[$list_id]; } } return $lists; } + /** + * Get list of folders according to specified filters + * + * @param integer Bitmask defining restrictions. See FILTER_* constants for possible values. + * + * @return array List of task folders + */ + protected function filter_folders($filter) + { + $this->_read_lists(); + + $folders = array(); + foreach ($this->lists as $id => $list) { + if (!empty($this->folders[$id])) { + $folder = $this->folders[$id]; + + if ($folder->get_namespace() == 'personal') { + $folder->editable = true; + } + else if ($rights = $folder->get_myrights()) { + if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) { + $folder->editable = strpos($rights, 'i') !== false; + } + } + + $folders[] = $folder; + } + } + + $plugin = $this->rc->plugins->exec_hook('tasklist_list_filter', array( + 'list' => $folders, + 'filter' => $filter, + 'tasklists' => $folders, + )); + + if ($plugin['abort'] || !$filter) { + return $plugin['tasklists']; + } + + $personal = $filter & self::FILTER_PERSONAL; + $shared = $filter & self::FILTER_SHARED; + + $tasklists = array(); + foreach ($folders as $folder) { + if (($filter & self::FILTER_WRITEABLE) && !$folder->editable) { + continue; + } +/* + if (($filter & self::FILTER_INSERTABLE) && !$folder->insert) { + continue; + } + if (($filter & self::FILTER_ACTIVE) && !$folder->is_active()) { + continue; + } + if (($filter & self::FILTER_PRIVATE) && $folder->subtype != 'private') { + continue; + } + if (($filter & self::FILTER_CONFIDENTIAL) && $folder->subtype != 'confidential') { + continue; + } +*/ + if ($personal || $shared) { + $ns = $folder->get_namespace(); + if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) { + continue; + } + } + + $tasklists[$folder->id] = $folder; + } + + return $tasklists; + } + /** * Get the kolab_calendar instance for the given calendar ID * * @param string List identifier (encoded imap folder name) * @return object kolab_storage_folder Object nor null if list doesn't exist */ protected function get_folder($id) { // create list and folder instance if necesary if (!$this->lists[$id]) { $folder = kolab_storage::get_folder(kolab_storage::id_decode($id)); if ($folder->type) { $this->folders[$id] = $folder; $this->lists[$id] = $this->folder_props($folder, $this->rc->config->get('kolab_tasklists', array())); } } return $this->folders[$id]; } /** * Create a new list assigned to the current user * * @param array Hash array with list properties * name: List name * color: The color of the list * showalarms: True if alarms are enabled * @return mixed ID of the new list on success, False on error */ public function create_list(&$prop) { $prop['type'] = 'task' . ($prop['default'] ? '.default' : ''); $prop['active'] = true; // activate folder by default $prop['subscribed'] = true; $folder = kolab_storage::folder_update($prop); if ($folder === false) { $this->last_error = kolab_storage::$last_error; return false; } // create ID $id = kolab_storage::folder_id($folder); $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array()); if (isset($prop['showalarms'])) $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; if ($prefs['kolab_tasklists'][$id]) $this->rc->user->save_prefs($prefs); // force page reload to properly render folder hierarchy if (!empty($prop['parent'])) { $prop['_reload'] = true; } else { $folder = kolab_storage::get_folder($folder); $prop += $this->folder_props($folder, array()); } return $id; } /** * Update properties of an existing tasklist * * @param array Hash array with list properties * id: List Identifier * name: List name * color: The color of the list * showalarms: True if alarms are enabled (if supported) * @return boolean True on success, Fales on failure */ public function edit_list(&$prop) { if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) { $prop['oldname'] = $folder->name; $prop['type'] = 'task'; $newfolder = kolab_storage::folder_update($prop); if ($newfolder === false) { $this->last_error = kolab_storage::$last_error; return false; } // create ID $id = kolab_storage::folder_id($newfolder); // fallback to local prefs $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array()); unset($prefs['kolab_tasklists'][$prop['id']]); if (isset($prop['showalarms'])) $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; if ($prefs['kolab_tasklists'][$id]) $this->rc->user->save_prefs($prefs); // force page reload if folder name/hierarchy changed if ($newfolder != $prop['oldname']) $prop['_reload'] = true; return $id; } return false; } /** * Set active/subscribed state of a list * * @param array Hash array with list properties * id: List Identifier * active: True if list is active, false if not * permanent: True if list is to be subscribed permanently * @return boolean True on success, Fales on failure */ public function subscribe_list($prop) { if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) { $ret = false; if (isset($prop['permanent'])) $ret |= $folder->subscribe(intval($prop['permanent'])); if (isset($prop['active'])) $ret |= $folder->activate(intval($prop['active'])); // apply to child folders, too if ($prop['recursive']) { foreach ((array)kolab_storage::list_folders($folder->name, '*', 'task') as $subfolder) { if (isset($prop['permanent'])) ($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder)); if (isset($prop['active'])) ($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder)); } } return $ret; } return false; } /** * Delete the given list with all its contents * * @param array Hash array with list properties * id: list Identifier * @return boolean True on success, Fales on failure */ public function delete_list($prop) { if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) { if (kolab_storage::folder_delete($folder->name)) return true; else $this->last_error = kolab_storage::$last_error; } 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) { if (!kolab_storage::setup()) { return array(); } $this->search_more_results = false; $this->lists = $this->folders = array(); // find unsubscribed IMAP folders that have "event" type if ($source == 'folders') { foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) { $this->folders[$folder->id] = $folder; $this->lists[$folder->id] = $this->folder_props($folder, array()); } } // search other user's namespace via LDAP else if ($source == 'users') { $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) { $folders = array(); // search for tasks folders shared by this user foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) { $folders[] = new kolab_storage_folder($foldername, 'task'); } if (count($folders)) { $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user); $this->folders[$userfolder->id] = $userfolder; $this->lists[$userfolder->id] = $this->folder_props($userfolder, array()); foreach ($folders as $folder) { $this->folders[$folder->id] = $folder; $this->lists[$folder->id] = $this->folder_props($folder, array()); $count++; } } if ($count >= $limit) { $this->search_more_results = true; break; } } } return $this->get_lists(); } /** * Get a list of tags to assign tasks to * * @return array List of tags */ public function get_tags() { $config = kolab_storage_config::get_instance(); $tags = $config->get_tags(); $backend_tags = array_map(function($v) { return $v['name']; }, $tags); return array_values(array_unique(array_merge($this->tags, $backend_tags))); } /** * Get number of tasks matching the given filter * * @param array List of lists to count tasks of * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate) */ public function count_tasks($lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); $today_date = new DateTime('now', $this->plugin->timezone); $today = $today_date->format('Y-m-d'); $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone); $tomorrow = $tomorrow_date->format('Y-m-d'); $counts = array('all' => 0, 'today' => 0, 'tomorrow' => 0, 'later' => 0, 'overdue' => 0); foreach ($lists as $list_id) { if (!$folder = $this->get_folder($list_id)) { continue; } foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) { $rec = $this->_to_rcube_task($record, $list_id, false); if ($this->is_complete($rec)) // don't count complete tasks continue; $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']++; } } // avoid session race conditions that will loose temporary subscriptions $this->plugin->rc->session->nowrite = true; return $counts; } /** * Get all task records matching the given filter * * @param array Hash array with filter criterias: * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants) * - from: Date range start as string (Y-m-d) * - to: Date range end as string (Y-m-d) * - search: Search query string * - uid: Task UIDs * @param array List of lists to get tasks from * @return array List of tasks records matchin the criteria */ public function list_tasks($filter, $lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); $results = array(); // query Kolab storage $query = array(); if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) $query[] = array('tags','~','x-complete'); else if (empty($filter['since'])) $query[] = array('tags','!~','x-complete'); // full text search (only works with cache enabled) if ($filter['search']) { $search = mb_strtolower($filter['search']); foreach (rcube_utils::normalize_string($search, true) as $word) { $query[] = array('words', '~', $word); } } if ($filter['since']) { $query[] = array('changed', '>=', $filter['since']); } if ($filter['uid']) { $query[] = array('uid', '=', (array) $filter['uid']); } // load all tags into memory first kolab_storage_config::get_instance()->get_tags(); foreach ($lists as $list_id) { if (!$folder = $this->get_folder($list_id)) { continue; } foreach ($folder->select($query) as $record) { $this->load_tags($record); $task = $this->_to_rcube_task($record, $list_id); // TODO: post-filter tasks returned from storage $results[] = $task; } } // avoid session race conditions that will loose temporary subscriptions $this->plugin->rc->session->nowrite = true; return $results; } /** * Return data of a specific task * - * @param mixed Hash array with task properties or task UID + * @param mixed Hash array with task properties or task UID + * @param integer Bitmask defining filter criterias for folders. + * See FILTER_* constants for possible values. + * * @return array Hash array with task properties or false if not found */ - public function get_task($prop) + public function get_task($prop, $filter = 0) { $this->_parse_id($prop); $id = $prop['uid']; $list_id = $prop['list']; - $folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->folders; + $folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->get_lists($filter); // find task in the available folders foreach ($folders as $list_id => $folder) { + if (is_array($folder)) + $folder = $this->folders[$list_id]; if (is_numeric($list_id) || !$folder) continue; if (!$this->tasks[$id] && ($object = $folder->get_object($id))) { $this->load_tags($object); $this->tasks[$id] = $this->_to_rcube_task($object, $list_id); break; } } return $this->tasks[$id]; } /** * Get all decendents of the given task record * * @param mixed Hash array with task properties or task UID * @param boolean True if all childrens children should be fetched * @return array List of all child task IDs */ public function get_childs($prop, $recursive = false) { if (is_string($prop)) { $task = $this->get_task($prop); $prop = array('uid' => $task['uid'], 'list' => $task['list']); } else { $this->_parse_id($prop); } $childs = array(); $list_id = $prop['list']; $task_ids = array($prop['uid']); $folder = $this->get_folder($list_id); // query for childs (recursively) while ($folder && !empty($task_ids)) { $query_ids = array(); foreach ($task_ids as $task_id) { $query = array(array('tags','=','x-parent:' . $task_id)); foreach ($folder->select($query) as $record) { // don't rely on kolab_storage_folder filtering if ($record['parent_id'] == $task_id) { $childs[] = $list_id . ':' . $record['uid']; $query_ids[] = $record['uid']; } } } if (!$recursive) break; $task_ids = $query_ids; } return $childs; } /** * Provide a list of revisions for the given task * * @param array $task Hash array with task properties * @return array List of changes, each as a hash array * @see tasklist_driver::get_task_changelog() */ public function get_task_changelog($prop) { if (empty($this->bonnie_api)) { return false; } list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); $result = $uid && $mailbox ? $this->bonnie_api->changelog('task', $uid, $mailbox, $msguid) : null; if (is_array($result) && $result['uid'] == $uid) { return $result['changes']; } return false; } /** * Return full data of a specific revision of an event * * @param mixed $task UID string or hash array with task properties * @param mixed $rev Revision number * * @return array Task object as hash array * @see tasklist_driver::get_task_revision() */ public function get_task_revison($prop, $rev) { if (empty($this->bonnie_api)) { return false; } $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); // call Bonnie API $result = $this->bonnie_api->get('task', $uid, $rev, $mailbox, $msguid); if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) { $format = kolab_format::factory('task'); $format->load($result['xml']); $rec = $format->to_array(); $format->get_attachments($rec, true); if ($format->is_valid()) { $rec = self::_to_rcube_task($rec, $list_id, false); $rec['rev'] = $result['rev']; return $rec; } } return false; } /** * Command the backend to restore a certain revision of a task. * This shall replace the current object with an older version. * * @param mixed $task UID string or hash array with task properties * @param mixed $rev Revision number * * @return boolean True on success, False on failure * @see tasklist_driver::restore_task_revision() */ public function restore_task_revision($prop, $rev) { if (empty($this->bonnie_api)) { return false; } $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); $folder = $this->get_folder($list_id); $success = false; if ($folder && ($raw_msg = $this->bonnie_api->rawdata('task', $uid, $rev, $mailbox))) { $imap = $this->rc->get_storage(); // insert $raw_msg as new message if ($imap->save_message($folder->name, $raw_msg, null, false)) { $success = true; // delete old revision from imap and cache $imap->delete_message($msguid, $folder->name); $folder->cache->set($msguid, false); } } return $success; } /** * Get a list of property changes beteen two revisions of a task object * * @param array $task Hash array with task properties * @param mixed $rev Revisions: "from:to" * * @return array List of property changes, each as a hash array * @see tasklist_driver::get_task_diff() */ public function get_task_diff($prop, $rev1, $rev2) { $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); // call Bonnie API $result = $this->bonnie_api->diff('task', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id); if (is_array($result) && $result['uid'] == $uid) { $result['rev1'] = $rev1; $result['rev2'] = $rev2; $keymap = array( 'start' => 'start', 'due' => 'date', 'dstamp' => 'changed', 'summary' => 'title', 'alarm' => 'alarms', 'attendee' => 'attendees', 'attach' => 'attachments', 'rrule' => 'recurrence', 'related-to' => 'parent_id', 'percent-complete' => 'complete', 'lastmodified-date' => 'changed', ); $prop_keymaps = array( 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'), 'attendees' => array('partstat' => 'status'), ); $special_changes = array(); // map kolab event properties to keys the client expects array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) { if (array_key_exists($change['property'], $keymap)) { $change['property'] = $keymap[$change['property']]; } if ($change['property'] == 'priority') { $change['property'] = 'flagged'; $change['old'] = $change['old'] == 1 ? $this->plugin->gettext('yes') : null; $change['new'] = $change['new'] == 1 ? $this->plugin->gettext('yes') : null; } // map alarms trigger value if ($change['property'] == 'alarms') { if (is_array($change['old']) && is_array($change['old']['trigger'])) $change['old']['trigger'] = $change['old']['trigger']['value']; if (is_array($change['new']) && is_array($change['new']['trigger'])) $change['new']['trigger'] = $change['new']['trigger']['value']; } // make all property keys uppercase if ($change['property'] == 'recurrence') { $special_changes['recurrence'] = $i; foreach (array('old','new') as $m) { if (is_array($change[$m])) { $props = array(); foreach ($change[$m] as $k => $v) { $props[strtoupper($k)] = $v; } $change[$m] = $props; } } } // map property keys names if (is_array($prop_keymaps[$change['property']])) { foreach ($prop_keymaps[$change['property']] as $k => $dest) { if (is_array($change['old']) && array_key_exists($k, $change['old'])) { $change['old'][$dest] = $change['old'][$k]; unset($change['old'][$k]); } if (is_array($change['new']) && array_key_exists($k, $change['new'])) { $change['new'][$dest] = $change['new'][$k]; unset($change['new'][$k]); } } } if ($change['property'] == 'exdate') { $special_changes['exdate'] = $i; } else if ($change['property'] == 'rdate') { $special_changes['rdate'] = $i; } }); // merge some recurrence changes foreach (array('exdate','rdate') as $prop) { if (array_key_exists($prop, $special_changes)) { $exdate = $result['changes'][$special_changes[$prop]]; if (array_key_exists('recurrence', $special_changes)) { $recurrence = &$result['changes'][$special_changes['recurrence']]; } else { $i = count($result['changes']); $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array()); $recurrence = &$result['changes'][$i]['recurrence']; } $key = strtoupper($prop); $recurrence['old'][$key] = $exdate['old']; $recurrence['new'][$key] = $exdate['new']; unset($result['changes'][$special_changes[$prop]]); } } return $result; } return false; } /** * Helper method to resolved the given task identifier into uid and folder * * @return array (uid,folder,msguid) tuple */ private function _resolve_task_identity($prop) { $mailbox = $msguid = null; $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; if ($folder = $this->get_folder($list_id)) { $mailbox = $folder->get_mailbox_id(); // get task object from storage in order to get the real object uid an msguid if ($rec = $folder->get_object($uid)) { $msguid = $rec['_msguid']; $uid = $rec['uid']; } } return array($uid, $mailbox, $msguid); } /** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array with task properties * @see tasklist_driver::pending_alarms() */ public function pending_alarms($time, $lists = null) { $interval = 300; $time -= $time % 60; $slot = $time; $slot -= $slot % $interval; $last = $time - max(60, $this->rc->config->get('refresh_interval', 0)); $last -= $last % $interval; // only check for alerts once in 5 minutes if ($last == $slot) return array(); if ($lists && is_string($lists)) $lists = explode(',', $lists); $time = $slot + $interval; $candidates = array(); $query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete')); foreach ($this->lists as $lid => $list) { // skip lists with alarms disabled if (!$list['showalarms'] || ($lists && !in_array($lid, $lists))) continue; $folder = $this->get_folder($lid); foreach ($folder->select($query) as $record) { if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-) continue; $task = $this->_to_rcube_task($record, $lid, false); // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) { $id = $alarm['id']; // use alarm-id as primary identifier $candidates[$id] = array( 'id' => $id, 'title' => $task['title'], 'date' => $task['date'], 'time' => $task['time'], 'notifyat' => $alarm['time'], 'action' => $alarm['action'], ); } } } // get alarm information stored in local database if (!empty($candidates)) { $alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates)); $result = $this->rc->db->query("SELECT *" . " FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")" . " AND `user_id` = ?", $this->rc->user->ID ); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $dbdata[$rec['alarm_id']] = $rec; } } $alarms = array(); foreach ($candidates as $id => $task) { // skip dismissed if ($dbdata[$id]['dismissed']) continue; // snooze function may have shifted alarm time $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat']; if ($notifyat <= $time) $alarms[] = $task; } return $alarms; } /** * (User) feedback after showing an alarm notification * This should mark the alarm as 'shown' or snooze it for the given amount of time * * @param string Task identifier * @param integer Suspend the alarm for this number of seconds */ public function dismiss_alarm($id, $snooze = 0) { // delete old alarm entry $this->rc->db->query( "DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` = ? AND `user_id` = ?", $id, $this->rc->user->ID ); // set new notifyat time or unset if not snoozed $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; $query = $this->rc->db->query( "INSERT INTO " . $this->rc->db->table_name('kolab_alarms', true) . " (`alarm_id`, `user_id`, `dismissed`, `notifyat`) VALUES (?, ?, ?, ?)", $id, $this->rc->user->ID, $snooze > 0 ? 0 : 1, $notifyat ); return $this->rc->db->affected_rows($query); } /** * Remove alarm dismissal or snooze state * * @param string Task identifier */ public function clear_alarms($id) { // delete alarm entry $this->rc->db->query( "DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` = ? AND `user_id` = ?", $id, $this->rc->user->ID ); return true; } /** * Get task tags */ private function load_tags(&$object) { // this task hasn't been migrated yet if (!empty($object['categories'])) { // OPTIONAL: call kolab_storage_config::apply_tags() to migrate the object $object['tags'] = (array)$object['categories']; if (!empty($object['tags'])) { $this->tags = array_merge($this->tags, $object['tags']); } } else { $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($object['uid']); $object['tags'] = array_map(function($v) { return $v['name']; }, $tags); } } /** * Update task tags */ private function save_tags($uid, $tags) { $config = kolab_storage_config::get_instance(); $config->save_tags($uid, $tags); } /** * Find messages linked with a task record */ private function get_links($uid) { $config = kolab_storage_config::get_instance(); return $config->get_object_links($uid); } /** * */ private function save_links($uid, $links) { // make sure we have a valid array if (empty($links)) { $links = array(); } $config = kolab_storage_config::get_instance(); $remove = array_diff($config->get_object_links($uid), $links); return $config->save_object_links($uid, $links, $remove); } /** * Extract uid + list identifiers from the given input * * @param mixed array or string with task identifier(s) */ private function _parse_id(&$prop) { $id_ = null; if (is_array($prop)) { // 'uid' + 'list' available, nothing to be done if (!empty($prop['uid']) && !empty($prop['list'])) { return; } // 'id' is given if (!empty($prop['id'])) { if (!empty($prop['list'])) { $list_id = $prop['_fromlist'] ?: $prop['list']; if (strpos($prop['id'], $list_id.':') === 0) { $prop['uid'] = substr($prop['id'], strlen($list_id)+1); } else { $prop['uid'] = $prop['id']; } } else { $id_ = $prop['id']; } } } else { $id_ = strval($prop); $prop = array(); } // split 'id' into list + uid if (!empty($id_)) { list($list, $uid) = explode(':', $id_, 2); if (!empty($uid)) { $prop['uid'] = $uid; $prop['list'] = $list; } else { $prop['uid'] = $id_; } } } /** * Convert from Kolab_Format to internal representation */ private function _to_rcube_task($record, $list_id, $all = true) { $id_prefix = $list_id . ':'; $task = array( 'id' => $id_prefix . $record['uid'], 'uid' => $record['uid'], 'title' => $record['title'], // 'location' => $record['location'], 'description' => $record['description'], 'flagged' => $record['priority'] == 1, 'complete' => floatval($record['complete'] / 100), 'status' => $record['status'], 'parent_id' => $record['parent_id'] ? $id_prefix . $record['parent_id'] : null, 'recurrence' => $record['recurrence'], 'attendees' => $record['attendees'], 'organizer' => $record['organizer'], 'sequence' => $record['sequence'], 'tags' => $record['tags'], 'list' => $list_id, ); // we can sometimes skip this expensive operation if ($all) { $task['links'] = $this->get_links($task['uid']); } // convert from DateTime to internal date format if (is_a($record['due'], 'DateTime')) { $due = $this->plugin->lib->adjust_timezone($record['due']); $task['date'] = $due->format('Y-m-d'); if (!$record['due']->_dateonly) $task['time'] = $due->format('H:i'); } // convert from DateTime to internal date format if (is_a($record['start'], 'DateTime')) { $start = $this->plugin->lib->adjust_timezone($record['start']); $task['startdate'] = $start->format('Y-m-d'); if (!$record['start']->_dateonly) $task['starttime'] = $start->format('H:i'); } if (is_a($record['changed'], 'DateTime')) { $task['changed'] = $record['changed']; } if (is_a($record['created'], 'DateTime')) { $task['created'] = $record['created']; } if ($record['valarms']) { $task['valarms'] = $record['valarms']; } else if ($record['alarms']) { $task['alarms'] = $record['alarms']; } if (!empty($task['attendees'])) { foreach ((array)$task['attendees'] as $i => $attendee) { if (is_array($attendee['delegated-from'])) { $task['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']); } if (is_array($attendee['delegated-to'])) { $task['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']); } } } if (!empty($record['_attachments'])) { foreach ($record['_attachments'] as $key => $attachment) { if ($attachment !== false) { if (!$attachment['name']) $attachment['name'] = $key; $attachments[] = $attachment; } } $task['attachments'] = $attachments; } return $task; } /** * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving * (opposite of self::_to_rcube_event()) */ private function _from_rcube_task($task, $old = array()) { $object = $task; $id_prefix = $task['list'] . ':'; if (!empty($task['date'])) { $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->plugin->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->plugin->timezone); if (empty($task['starttime'])) $object['start']->_dateonly = true; unset($object['startdate']); } // as per RFC (and the Kolab schema validation), start and due dates need to be of the same type (#3614) // this should be catched in the client already but just make sure we don't write invalid objects if (!empty($object['start']) && !empty($object['due']) && $object['due']->_dateonly != $object['start']->_dateonly) { $object['start']->_dateonly = true; $object['due']->_dateonly = true; } $object['complete'] = $task['complete'] * 100; if ($task['complete'] == 1.0 && empty($task['complete'])) $object['status'] = 'COMPLETED'; if ($task['flagged']) $object['priority'] = 1; else $object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0; // remove list: prefix from parent_id if (!empty($task['parent_id']) && strpos($task['parent_id'], $id_prefix) === 0) { $object['parent_id'] = substr($task['parent_id'], strlen($id_prefix)); } // copy meta data (starting with _) from old object foreach ((array)$old as $key => $val) { if (!isset($object[$key]) && $key[0] == '_') $object[$key] = $val; } // copy recurrence rules if the client didn't submit it (#2713) if (!array_key_exists('recurrence', $object) && $old['recurrence']) { $object['recurrence'] = $old['recurrence']; } unset($task['attachments']); kolab_format::merge_attachments($object, $old); // allow sequence increments if I'm the organizer if ($this->plugin->is_organizer($object) && empty($object['_method'])) { unset($object['sequence']); } else if (isset($old['sequence']) && empty($object['_method'])) { $object['sequence'] = $old['sequence']; } unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['created']); return $object; } /** * Add a single task to the database * * @param array Hash array with task properties (see header of tasklist_driver.php) * @return mixed New task ID on success, False on error */ public function create_task($task) { return $this->edit_task($task); } /** * Update an task entry with the given data * * @param array Hash array with task properties (see header of tasklist_driver.php) * @return boolean True on success, False on error */ public function edit_task($task) { $this->_parse_id($task); $list_id = $task['list']; if (!$list_id || !($folder = $this->get_folder($list_id))) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Invalid list identifer to save task: " . print_r($list_id, true)), true, false); return false; } // email links and tags are stored separately $links = $task['links']; $tags = $task['tags']; unset($task['tags'], $task['links']); // moved from another folder if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) { if (!$fromfolder->move($task['uid'], $folder)) return false; unset($task['_fromlist']); } // load previous version of this task to merge if ($task['id']) { $old = $folder->get_object($task['uid']); if (!$old || PEAR::isError($old)) return false; // merge existing properties if the update isn't complete if (!isset($task['title']) || !isset($task['complete'])) $task += $this->_to_rcube_task($old, $list_id); } // generate new task object from RC input $object = $this->_from_rcube_task($task, $old); $saved = $folder->save($object, 'task', $task['uid']); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving task object to Kolab server"), true, false); $saved = false; } else { // save links in configuration.relation object $this->save_links($object['uid'], $links); // save tags in configuration.relation object $this->save_tags($object['uid'], $tags); $task = $this->_to_rcube_task($object, $list_id); $task['tags'] = (array) $tags; $this->tasks[$task['uid']] = $task; } return $saved; } /** * Move a single task to another list * * @param array Hash array with task properties: * @return boolean True on success, False on error * @see tasklist_driver::move_task() */ public function move_task($task) { $this->_parse_id($task); $list_id = $task['list']; if (!$list_id || !($folder = $this->get_folder($list_id))) return false; // execute move command if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) { return $fromfolder->move($task['uid'], $folder); } return false; } /** * Remove a single task from the database * * @param array Hash array with task properties: * id: Task identifier * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend) * @return boolean True on success, False on error */ public function delete_task($task, $force = true) { $this->_parse_id($task); $list_id = $task['list']; if (!$list_id || !($folder = $this->get_folder($list_id))) return false; $status = $folder->delete($task['uid']); if ($status) { // remove tag assignments // @TODO: don't do this when undelete feature will be implemented $this->save_tags($task['uid'], null); } return $status; } /** * Restores a single deleted task (if supported) * * @param array Hash array with task properties: * id: Task identifier * @return boolean True on success, False on error */ public function undelete_task($prop) { // TODO: implement this return false; } /** * Get attachment properties * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return array Hash array with attachment properties: * id: Attachment identifier * name: Attachment name * mimetype: MIME content type of the attachment * size: Attachment size */ public function get_attachment($id, $task) { // get old revision of the object if ($task['rev']) { $task = $this->get_task_revison($task, $task['rev']); } else { $task = $this->get_task($task); } if ($task && !empty($task['attachments'])) { foreach ($task['attachments'] as $att) { if ($att['id'] == $id) return $att; } } return null; } /** * Get attachment body * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return string Attachment body */ public function get_attachment_body($id, $task) { $this->_parse_id($task); // get old revision of event if ($task['rev']) { if (empty($this->bonnie_api)) { return false; } $cid = substr($id, 4); // call Bonnie API and get the raw mime message list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($task); if ($msg_raw = $this->bonnie_api->rawdata('task', $uid, $task['rev'], $mailbox, $msguid)) { // parse the message and find the part with the matching content-id $message = rcube_mime::parse_message($msg_raw); foreach ((array)$message->parts as $part) { if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) { return $part->body; } } } return false; } if ($storage = $this->get_folder($task['list'])) { return $storage->get_attachment($task['uid'], $id); } return false; } /** * Build a struct representing the given message reference * * @see tasklist_driver::get_message_reference() */ public function get_message_reference($uri_or_headers, $folder = null) { if (is_object($uri_or_headers)) { $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder); } if (is_string($uri_or_headers)) { return kolab_storage_config::get_message_reference($uri_or_headers, 'task'); } return false; } /** * Find tasks assigned to a specified message * * @see tasklist_driver::get_message_related_tasks() */ public function get_message_related_tasks($headers, $folder) { $config = kolab_storage_config::get_instance(); $result = $config->get_message_relations($headers, $folder, 'task'); foreach ($result as $idx => $rec) { $result[$idx] = $this->_to_rcube_task($rec, kolab_storage::folder_id($rec['_mailbox'])); } return $result; } /** * */ public function tasklist_edit_form($action, $list, $fieldprop) { if ($list['id'] && ($list = $this->lists[$list['id']])) { $folder_name = $this->get_folder($list['id'])->name; // UTF7 } else { $folder_name = ''; } $storage = $this->rc->get_storage(); $delim = $storage->get_hierarchy_delimiter(); $form = array(); if (strlen($folder_name)) { $path_imap = explode($delim, $folder_name); array_pop($path_imap); // pop off name part $path_imap = implode($path_imap, $delim); $options = $storage->folder_info($folder_name); } else { $path_imap = ''; } $hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name); // folder name (default field) $input_name = new html_inputfield(array('name' => 'name', 'id' => 'taskedit-tasklistame', 'size' => 20)); $fieldprop['name']['value'] = $input_name->show($list['editname'], array('disabled' => ($options['norename'] || $options['protected']))); // prevent user from moving folder if (!empty($options) && ($options['norename'] || $options['protected'])) { $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap); } else { $select = kolab_storage::folder_selector('task', array('name' => 'parent', 'id' => 'taskedit-parentfolder'), $folder_name); $fieldprop['parent'] = array( 'id' => 'taskedit-parentfolder', 'label' => $this->plugin->gettext('parentfolder'), 'value' => $select->show($path_imap), ); } // General tab $form['properties'] = array( 'name' => $this->rc->gettext('properties'), 'fields' => array(), ); foreach (array('name','parent','showalarms') as $f) { $form['properties']['fields'][$f] = $fieldprop[$f]; } // add folder ACL tab if ($action != 'form-new') { $form['sharing'] = array( 'name' => rcube::Q($this->plugin->gettext('tabsharing')), 'content' => html::tag('iframe', array( 'src' => $this->rc->url(array('_action' => 'folder-acl', '_folder' => $folder_name, 'framed' => 1)), 'width' => '100%', 'height' => 280, 'border' => 0, 'style' => 'border:0'), '') ); } $form_html = ''; if (is_array($hidden_fields)) { foreach ($hidden_fields as $field) { $hiddenfield = new html_hiddenfield($field); $form_html .= $hiddenfield->show() . "\n"; } } // create form output foreach ($form as $tab) { if (is_array($tab['fields']) && empty($tab['content'])) { $table = new html_table(array('cols' => 2)); foreach ($tab['fields'] as $col => $colprop) { $label = !empty($colprop['label']) ? $colprop['label'] : $this->plugin->gettext($col); $table->add('title', html::label($colprop['id'], rcube::Q($label))); $table->add(null, $colprop['value']); } $content = $table->show(); } else { $content = $tab['content']; } if (!empty($content)) { $form_html .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($tab['name'])) . $content) . "\n"; } } return $form_html; } /** * Handler to render ACL form for a notes folder */ public function folder_acl() { $this->plugin->require_plugin('acl'); $this->rc->output->add_handler('folderacl', array($this, 'folder_acl_form')); $this->rc->output->send('tasklist.kolabacl'); } /** * Handler for ACL form template object */ public function folder_acl_form() { $folder = rcube_utils::get_input_value('_folder', rcube_utils::INPUT_GPC); if (strlen($folder)) { $storage = $this->rc->get_storage(); $options = $storage->folder_info($folder); // get sharing UI from acl plugin $acl = $this->rc->plugins->exec_hook('folder_form', array('form' => array(), 'options' => $options, 'name' => $folder)); } return $acl['form']['sharing']['content'] ?: html::div('hint', $this->plugin->gettext('aclnorights')); } } diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php index a0df271a..df68d005 100644 --- a/plugins/tasklist/drivers/tasklist_driver.php +++ b/plugins/tasklist/drivers/tasklist_driver.php @@ -1,454 +1,469 @@ * * Copyright (C) 2012, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ /** * Struct of an internal task object how it is passed from/to the driver classes: * * $task = array( * 'id' => 'Task ID used for editing', // must be unique for the current user * 'parent_id' => 'ID of parent task', // null if top-level task * 'uid' => 'Unique identifier of this task', * 'list' => 'Task list identifier to add the task to or where the task is stored', * 'changed' => , // Last modification date/time of the record * 'title' => 'Event title/summary', * 'description' => 'Event description', * 'tags' => array(), // List of tags for this task * 'date' => 'Due date', // as string of format YYYY-MM-DD or null if no date is set * 'time' => 'Due time', // as string of format hh::ii or null if no due time is set * 'startdate' => 'Start date' // Delay start of the task until that date * 'starttime' => 'Start time' // ...and time * 'categories' => 'Task category', * 'flagged' => 'Boolean value whether this record is flagged', * 'complete' => 'Float value representing the completeness state (range 0..1)', * 'status' => 'Task status string according to (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED) RFC 2445', * 'valarms' => array( // List of reminders (new format), each represented as a hash array: * array( * 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object * 'action' => 'DISPLAY|EMAIL|AUDIO', * 'duration' => 'PT15M', // ISO 8601 period string * 'repeat' => 0, // number of repetitions * 'description' => '', // text to display for DISPLAY actions * 'summary' => '', // message text for EMAIL actions * 'attendees' => array(), // list of email addresses to receive alarm messages * ), * ), * 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs * 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY', * 'INTERVAL' => 1...n, * 'UNTIL' => DateTime, * 'COUNT' => 1..n, // number of times * 'RDATE' => array(), // complete list of DateTime objects denoting individual repeat dates * ), * '_fromlist' => 'List identifier where the task was stored before', * ); */ /** * Driver interface for the Tasklist plugin */ abstract class tasklist_driver { // features supported by the backend public $alarms = false; public $attachments = false; public $attendees = false; public $undelete = false; // task undelete action public $sortable = false; public $alarm_types = array('DISPLAY'); public $alarm_absolute = true; public $last_error; + const FILTER_ALL = 0; + const FILTER_WRITEABLE = 1; + const FILTER_INSERTABLE = 2; + const FILTER_ACTIVE = 4; + const FILTER_PERSONAL = 8; + const FILTER_PRIVATE = 16; + const FILTER_CONFIDENTIAL = 32; + const FILTER_SHARED = 64; + + /** * Get a list of available task lists from this source + * @param integer Bitmask defining filter criterias. + * See FILTER_* constants for possible values. */ - abstract function get_lists(); + abstract function get_lists($filter = 0); /** * Create a new list assigned to the current user * * @param array Hash array with list properties * name: List name * color: The color of the list * showalarms: True if alarms are enabled * @return mixed ID of the new list on success, False on error */ abstract function create_list(&$prop); /** * Update properties of an existing tasklist * * @param array Hash array with list properties * id: List Identifier * name: List name * color: The color of the list * showalarms: True if alarms are enabled (if supported) * @return boolean True on success, Fales on failure */ abstract function edit_list(&$prop); /** * Set active/subscribed state of a list * * @param array Hash array with list properties * id: List Identifier * active: True if list is active, false if not * @return boolean True on success, Fales on failure */ abstract function subscribe_list($prop); /** * Delete the given list with all its contents * * @param array Hash array with list properties * id: list Identifier * @return boolean True on success, Fales on failure */ abstract function delete_list($prop); /** * 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 */ abstract function search_lists($query, $source); /** * Get number of tasks matching the given filter * * @param array List of lists to count tasks of * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate) */ abstract function count_tasks($lists = null); /** * Get all task records matching the given filter * * @param array Hash array with filter criterias: * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants) * - from: Date range start as string (Y-m-d) * - to: Date range end as string (Y-m-d) * - search: Search query string * @param array List of lists to get tasks from * @return array List of tasks records matchin the criteria */ abstract function list_tasks($filter, $lists = null); /** * Get a list of tags to assign tasks to * * @return array List of tags */ abstract function get_tags(); /** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array with task properties * id: Task identifier * uid: Unique identifier of this task * date: Task due date * time: Task due time * title: Task title/summary */ abstract function pending_alarms($time, $lists = null); /** * (User) feedback after showing an alarm notification * This should mark the alarm as 'shown' or snooze it for the given amount of time * * @param string Task identifier * @param integer Suspend the alarm for this number of seconds */ abstract function dismiss_alarm($id, $snooze = 0); /** * Remove alarm dismissal or snooze state * * @param string Task identifier */ abstract public function clear_alarms($id); /** * Return data of a specific task * - * @param mixed Hash array with task properties or task UID + * @param mixed Hash array with task properties or task UID + * @param integer Bitmask defining filter criterias for folders. + * See FILTER_* constants for possible values. + * * @return array Hash array with task properties or false if not found */ - abstract public function get_task($prop); + abstract public function get_task($prop, $filter = 0); /** * Get decendents of the given task record * * @param mixed Hash array with task properties or task UID * @param boolean True if all childrens children should be fetched * @return array List of all child task IDs */ abstract public function get_childs($prop, $recursive = false); /** * Add a single task to the database * * @param array Hash array with task properties (see header of this file) * @return mixed New event ID on success, False on error */ abstract function create_task($prop); /** * Update an task entry with the given data * * @param array Hash array with task properties (see header of this file) * @return boolean True on success, False on error */ abstract function edit_task($prop); /** * Move a single task to another list * * @param array Hash array with task properties: * id: Task identifier * list: New list identifier to move to * _fromlist: Previous list identifier * @return boolean True on success, False on error */ abstract function move_task($prop); /** * Remove a single task from the database * * @param array Hash array with task properties: * id: Task identifier * list: Tasklist identifer * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend) * @return boolean True on success, False on error */ abstract function delete_task($prop, $force = true); /** * Restores a single deleted task (if supported) * * @param array Hash array with task properties: * id: Task identifier * @return boolean True on success, False on error */ public function undelete_task($prop) { return false; } /** * Get attachment properties * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return array Hash array with attachment properties: * id: Attachment identifier * name: Attachment name * mimetype: MIME content type of the attachment * size: Attachment size */ public function get_attachment($id, $task) { } /** * Get attachment body * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return string Attachment body */ public function get_attachment_body($id, $task) { } /** * Build a struct representing the given message reference * * @param object|string $uri_or_headers rcube_message_header instance holding the message headers * or an URI from a stored link referencing a mail message. * @param string $folder IMAP folder the message resides in * * @return array An struct referencing the given IMAP message */ public function get_message_reference($uri_or_headers, $folder = null) { // to be implemented by the derived classes return false; } /** * Find tasks assigned to a specified message * * @param object $message rcube_message_header instance * @param string $folder IMAP folder the message resides in * * @param array List of linked task objects */ public function get_message_related_tasks($headers, $folder) { // to be implemented by the derived classes return array(); } /** * Helper method to determine whether the given task is considered "complete" * * @param array $task Hash array with event properties * @return boolean True if complete, False otherwiese */ public function is_complete($task) { return ($task['complete'] >= 1.0 && empty($task['status'])) || $task['status'] === 'COMPLETED'; } /** * Provide a list of revisions for the given task * * @param array $task Hash array with task properties: * id: Task identifier * list: List identifier * * @return array List of changes, each as a hash array: * rev: Revision number * type: Type of the change (create, update, move, delete) * date: Change date * user: The user who executed the change * ip: Client IP * mailbox: Destination list for 'move' type */ public function get_task_changelog($task) { return false; } /** * Get a list of property changes beteen two revisions of a task object * * @param array $task Hash array with task properties: * id: Task identifier * list: List identifier * @param mixed $rev1 Old Revision * @param mixed $rev2 New Revision * * @return array List of property changes, each as a hash array: * property: Revision number * old: Old property value * new: Updated property value */ public function get_task_diff($task, $rev1, $rev2) { return false; } /** * Return full data of a specific revision of an event * * @param mixed $task UID string or hash array with task properties: * id: Task identifier * list: List identifier * @param mixed $rev Revision number * * @return array Task object as hash array * @see self::get_task() */ public function get_task_revison($task, $rev) { return false; } /** * Command the backend to restore a certain revision of a task. * This shall replace the current object with an older version. * * @param mixed $task UID string or hash array with task properties: * id: Task identifier * list: List identifier * @param mixed $rev Revision number * * @return boolean True on success, False on failure */ public function restore_task_revision($task, $rev) { return false; } /** * Build the edit/create form for lists. * This gives the drivers the opportunity to add more list properties * * @param string The action called this form * @param array Tasklist properties * @param array List with form fields to be rendered * @return string HTML content of the form */ public function tasklist_edit_form($action, $list, $formfields) { $html = ''; foreach ($formfields as $field) { $html .= html::div('form-section', html::label($field['id'], $field['label']) . $field['value']); } return $html; } /** * Compose an URL for CalDAV access to the given list (if configured) */ public function tasklist_caldav_url($list) { $rcmail = rcube::get_instance(); if (!empty($list['caldavuid']) && ($template = $rcmail->config->get('calendar_caldav_url', null))) { return strtr($template, array( '%h' => $_SERVER['HTTP_HOST'], '%u' => urlencode($rcmail->get_user_name()), '%i' => urlencode($list['caldavuid']), '%n' => urlencode($list['editname']), )); } return null; } /** * Handler for user_delete plugin hook * * @param array Hash array with hook arguments * @return array Return arguments for plugin hooks */ public function user_delete($args) { // TO BE OVERRIDDEN return $args; } } diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 3298626d..5997e21c 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -1,3451 +1,3454 @@ /** * Client scripts for the Tasklist plugin * * @author Thomas Bruederli * * @licstart The following is the entire license notice for the * JavaScript code in this file. * * 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 . * * @licend The above is the entire license notice * for the JavaScript code in this file. */ function rcube_tasklist_ui(settings) { // extend base class rcube_libcalendaring.call(this, settings); /* constants */ var FILTER_MASK_ALL = 0; var FILTER_MASK_TODAY = 1; var FILTER_MASK_TOMORROW = 2; var FILTER_MASK_WEEK = 4; var FILTER_MASK_LATER = 8; var FILTER_MASK_NODATE = 16; var FILTER_MASK_OVERDUE = 32; var FILTER_MASK_FLAGGED = 64; var FILTER_MASK_COMPLETE = 128; var FILTER_MASK_ASSIGNED = 256; var FILTER_MASK_MYTASKS = 512; var filter_masks = { all: FILTER_MASK_ALL, today: FILTER_MASK_TODAY, tomorrow: FILTER_MASK_TOMORROW, week: FILTER_MASK_WEEK, later: FILTER_MASK_LATER, nodate: FILTER_MASK_NODATE, overdue: FILTER_MASK_OVERDUE, flagged: FILTER_MASK_FLAGGED, complete: FILTER_MASK_COMPLETE, assigned: FILTER_MASK_ASSIGNED, mytasks: FILTER_MASK_MYTASKS }; /* private vars */ var tagsfilter = []; var filtermask = FILTER_MASK_ALL; var loadstate = { filter:-1, lists:'', search:null }; var idcount = 0; var focusview = false; var focusview_lists = []; var saving_lock; var ui_loading; var taskcounts = {}; var listindex = []; var listdata = {}; var tags = []; var draghelper; var search_request; var search_query; var completeness_slider; var task_draghelper; var tag_draghelper; var task_drag_active = false; var list_scroll_top = 0; var scroll_delay = 400; var scroll_step = 5; var scroll_speed = 20; var scroll_sensitivity = 40; var scroll_timer; var tasklists_widget; var focused_task; var focused_subclass; var task_attendees = []; var attendees_list; var me = this; // general datepicker settings var datepicker_settings = { // translate from PHP format to datepicker format dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'), firstDay : settings['first_day'], // dayNamesMin: settings['days_short'], // monthNames: settings['months'], // monthNamesShort: settings['months'], changeMonth: false, showOtherMonths: true, selectOtherMonths: true }; var extended_datepicker_settings; /* public members */ this.tasklists = rcmail.env.tasklists; this.selected_task; this.selected_list; /* public methods */ this.init = init; this.edit_task = task_edit_dialog; this.print_tasks = print_tasks; this.delete_task = delete_task; this.add_childtask = add_childtask; this.quicksearch = quicksearch; this.reset_search = reset_search; this.expand_collapse = expand_collapse; this.list_delete = list_delete; this.list_remove = list_remove; this.list_showurl = list_showurl; this.list_edit_dialog = list_edit_dialog; this.unlock_saving = unlock_saving; /* imports */ var Q = this.quote_html; var text2html = this.text2html; var event_date_text = this.event_date_text; var parse_datetime = this.parse_datetime; var date2unixtime = this.date2unixtime; var fromunixtime = this.fromunixtime; var render_message_links = this.render_message_links; /** * initialize the tasks UI */ function init() { if (rcmail.env.action == 'print' && rcmail.task == 'tasks') { filtermask = rcmail.env.filtermask; data_ready({data: rcmail.env.tasks}); return; } // initialize task list selectors for (var id in me.tasklists) { if (settings.selected_list && me.tasklists[settings.selected_list] && !me.tasklists[settings.selected_list].active) { me.tasklists[settings.selected_list].active = true; me.selected_list = settings.selected_list; $(rcmail.gui_objects.tasklistslist).find("input[value='"+settings.selected_list+"']").prop('checked', true); } if (me.tasklists[id].editable && (!me.selected_list || me.tasklists[id].default || (me.tasklists[id].active && !me.tasklists[me.selected_list].active))) { me.selected_list = id; } } // initialize treelist widget that controls the tasklists list var widget_class = window.kolab_folderlist || rcube_treelist_widget; tasklists_widget = new widget_class(rcmail.gui_objects.tasklistslist, { id_prefix: 'rcmlitasklist', selectable: true, save_state: true, keyboard: false, searchbox: '#tasklistsearch', search_action: 'tasks/tasklist', search_sources: [ 'folders', 'users' ], search_title: rcmail.gettext('listsearchresults','tasklist') }); tasklists_widget.addEventListener('select', function(node) { var id = $(this).data('id'); rcmail.enable_command('list-edit', has_permission(me.tasklists[node.id], 'wa')); rcmail.enable_command('list-delete', has_permission(me.tasklists[node.id], 'xa')); rcmail.enable_command('list-import', has_permission(me.tasklists[node.id], 'i')); rcmail.enable_command('list-remove', me.tasklists[node.id] && me.tasklists[node.id].removable); rcmail.enable_command('list-showurl', me.tasklists[node.id] && !!me.tasklists[node.id].caldavurl); me.selected_list = node.id; }); tasklists_widget.addEventListener('subscribe', function(p) { var list; if ((list = me.tasklists[p.id])) { list.subscribed = p.subscribed || false; rcmail.http_post('tasklist', { action:'subscribe', l:{ id:p.id, active:list.active?1:0, permanent:list.subscribed?1:0 } }); } }); tasklists_widget.addEventListener('remove', function(p) { if (me.tasklists[p.id] && me.tasklists[p.id].removable) { list_remove(p.id); } }); tasklists_widget.addEventListener('insert-item', function(p) { var list = p.data; if (list && list.id && !list.virtual) { me.tasklists[list.id] = list; var prop = { id:p.id, active:list.active?1:0 }; if (list.subscribed) prop.permanent = 1; rcmail.http_post('tasklist', { action:'subscribe', l:prop }); list_tasks(); $(p.item).data('type', 'tasklist'); } }); tasklists_widget.addEventListener('search-complete', function(data) { if (data.length) rcmail.display_message(rcmail.gettext('nrtasklistsfound','tasklist').replace('$nr', data.length), 'voice'); else rcmail.display_message(rcmail.gettext('notasklistsfound','tasklist'), 'info'); }); // init (delegate) event handler on tasklist checkboxes tasklists_widget.container.on('click', 'input[type=checkbox]', function(e) { var list, id = this.value; if ((list = me.tasklists[id])) { list.active = this.checked; fetch_counts(); if (!this.checked) remove_tasks(id); else list_tasks(null); rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:list.active?1:0 } }); // disable focusview if (!this.checked && focusview && $.inArray(id, focusview_lists) >= 0) { set_focusview(null); } // adjust checked state of original list item if (tasklists_widget.is_search()) { tasklists_widget.container.find('input[value="'+id+'"]').prop('checked', this.checked); } } e.stopPropagation(); }) .on('keypress', 'input[type=checkbox]', function(e) { // select tasklist on if (e.keyCode == 13) { tasklists_widget.select(this.value); return rcube_event.cancel(e); } }) .find('li:not(.virtual)').data('type', 'tasklist'); // handler for clicks on quickview buttons tasklists_widget.container.on('click', '.quickview', function(e){ var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, ''); if (tasklists_widget.is_search()) id = id.replace(/--xsR$/, ''); if (!rcube_event.is_keyboard(e) && this.blur) this.blur(); set_focusview(id, e.shiftKey || e.metaKey || e.ctrlKey); e.stopPropagation(); return false; }); // register dbl-click handler to open calendar edit dialog tasklists_widget.container.on('dblclick', ':not(.virtual) > .tasklist', function(e){ var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, ''); if (tasklists_widget.is_search()) id = id.replace(/--xsR$/, ''); list_edit_dialog(id); }); if (me.selected_list) { rcmail.enable_command('addtask', true); tasklists_widget.select(me.selected_list); } // register server callbacks rcmail.addEventListener('plugin.data_ready', data_ready); rcmail.addEventListener('plugin.update_task', update_taskitem); rcmail.addEventListener('plugin.refresh_tasks', function(p) { update_taskitem(p, true); }); rcmail.addEventListener('plugin.update_counts', update_counts); rcmail.addEventListener('plugin.insert_tasklist', insert_list); rcmail.addEventListener('plugin.update_tasklist', update_list); rcmail.addEventListener('plugin.destroy_tasklist', destroy_list); rcmail.addEventListener('plugin.unlock_saving', unlock_saving); rcmail.addEventListener('requestrefresh', before_refresh); rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null, true); setTimeout(fetch_counts, 200); }); rcmail.addEventListener('plugin.task_render_changelog', task_render_changelog); rcmail.addEventListener('plugin.task_show_diff', task_show_diff); rcmail.addEventListener('plugin.task_show_revision', function(data){ task_show_dialog(null, data, true); }); rcmail.addEventListener('plugin.close_history_dialog', close_history_dialog); rcmail.register_command('list-sort', list_set_sort, true); rcmail.register_command('list-order', list_set_order, (settings.sort_col || 'auto') != 'auto'); rcmail.register_command('task-history', task_history_dialog, false); $('#taskviewsortmenu .by-' + (settings.sort_col || 'auto')).attr('aria-checked', 'true').addClass('selected'); $('#taskviewsortmenu .sortorder.' + (settings.sort_order || 'asc')).attr('aria-checked', 'true').addClass('selected'); // start loading tasks fetch_counts(); list_tasks(settings.selected_filter); // register event handlers for UI elements $('#taskselector a').click(function(e) { if (!$(this).parent().hasClass('inactive')) { var selector = this.href.replace(/^.*#/, ''), mask = filter_masks[selector], shift = e.shiftKey || e.ctrlKey || e.metaKey; if (!shift) filtermask = mask; // reset selection on regular clicks else if (filtermask & mask) filtermask -= mask; else filtermask |= mask; list_tasks(); } return false; }); // quick-add a task $(rcmail.gui_objects.quickaddform).submit(function(e){ var tasktext = this.elements.text.value, rec = { id:-(++idcount), title:tasktext, readonly:true, mask:0, complete:0 }; if (tasktext && tasktext.length) { save_task({ tempid:rec.id, raw:tasktext, list:me.selected_list }, 'new'); render_task(rec); $('#listmessagebox').hide(); } // clear form this.reset(); return false; }).find('input[type=text]').placeholder(rcmail.gettext('createnewtask','tasklist')); // click-handler on tags list $(rcmail.gui_objects.tagslist).on('click', 'li', function(e){ var item = e.target.nodeName == 'LI' ? $(e.target) : $(e.target).closest('li'), tag = item.data('value'); if (!tag) return false; // reset selection on regular clicks var index = $.inArray(tag, tagsfilter); var shift = e.shiftKey || e.ctrlKey || e.metaKey; if (!shift) { if (tagsfilter.length > 1) index = -1; $('li', rcmail.gui_objects.tagslist).removeClass('selected').attr('aria-checked', 'false'); tagsfilter = []; } // add tag to filter if (index < 0) { item.addClass('selected').attr('aria-checked', 'true'); tagsfilter.push(tag); } else if (shift) { item.removeClass('selected').attr('aria-checked', 'false'); var a = tagsfilter.slice(0,index); tagsfilter = a.concat(tagsfilter.slice(index+1)); } list_tasks(); // clear text selection in IE after shift+click if (shift && document.selection) document.selection.empty(); e.preventDefault(); return false; }) .on('keypress', 'li', function(e) { if (e.keyCode == 13) { $(this).trigger('click', { pointerType:'keyboard' }); } }) .mousedown(function(e){ // disable content selection with the mouse e.preventDefault(); return false; }); // click-handler on task list items (delegate) $(rcmail.gui_objects.resultlist).on('click', function(e){ var item = $(e.target); var className = e.target.className; if (item.hasClass('childtoggle')) { item = item.parent().find('.taskhead'); className = 'childtoggle'; } else if (!item.hasClass('taskhead')) item = item.closest('div.taskhead'); // ignore if (!item.length) return false; var id = item.data('id'), li = item.parent(), rec = listdata[id]; switch (className) { case 'childtoggle': rec.collapsed = !rec.collapsed; li.children('.childtasks:first').toggle().attr('aria-hidden', rec.collapsed ? 'true' : 'false'); $(e.target).toggleClass('collapsed').html(rec.collapsed ? '▶' : '▼'); rcmail.http_post('tasks/task', { action:'collapse', t:{ id:rec.id, list:rec.list }, collapsed:rec.collapsed?1:0 }); if (e.shiftKey) // expand/collapse all childs li.children('.childtasks:first .childtoggle.'+(rec.collapsed?'expanded':'collapsed')).click(); break; case 'complete': if (rcmail.busy) return false; save_task_confirm(rec, 'edit', { _status_before:rec.status + '', status:e.target.checked ? 'COMPLETED' : (rec.complete > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION') }); item.toggleClass('complete'); return true; case 'flagged': if (rcmail.busy) return false; rec.flagged = rec.flagged ? 0 : 1; item.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false')); save_task(rec, 'edit'); break; case 'date': if (rcmail.busy) return false; var link = $(e.target).html(''), input = $('').appendTo(link).val(rec.date || '') input.datepicker($.extend({ onClose: function(dateText, inst) { if (dateText != (rec.date || '')) { save_task_confirm(rec, 'edit', { date:dateText }); } input.datepicker('destroy').remove(); link.html(dateText || rcmail.gettext('nodate','tasklist')); } }, extended_datepicker_settings) ) .datepicker('setDate', rec.date) .datepicker('show'); break; case 'delete': delete_task(id); break; case 'actions': var pos, ref = $(e.target), menu = $('#taskitemmenu'); if (menu.is(':visible') && menu.data('refid') == id) { rcmail.command('menu-close', 'taskitemmenu'); } else { rcmail.enable_command('task-history', me.tasklists[rec.list] && !!me.tasklists[rec.list].history); rcmail.command('menu-open', { menu: 'taskitemmenu', show: true }, e.target, e); menu.data('refid', id); me.selected_task = rec; } e.bubble = false; break; case 'extlink': return true; default: if (e.target.nodeName != 'INPUT') task_show_dialog(id); break; } return false; }) .on('dblclick', '.taskhead, .childtoggle', function(e){ var id, rec, item = $(e.target); if (!item.hasClass('taskhead')) item = item.closest('div.taskhead'); if (!rcmail.busy && item.length && (id = item.data('id')) && (rec = listdata[id])) { var list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {}; if (rec.readonly || !list.editable) task_show_dialog(id); else task_edit_dialog(id, 'edit'); clearSelection(); } }) .on('keydown', '.taskhead', function(e) { if (e.target.nodeName == 'INPUT' && e.target.type == 'text') return true; var inc = 1; switch (e.keyCode) { case 13: // Enter $(e.target).trigger('click', { pointerType:'keyboard' }); return rcube_event.cancel(e); case 38: // Up arrow key inc = -1; case 40: // Down arrow key if ($(e.target).hasClass('actions')) { // unfold actions menu $(e.target).trigger('click', { pointerType:'keyboard' }); return rcube_event.cancel(e); } // focus next/prev task item var x = 0, target = this, items = $(rcmail.gui_objects.resultlist).find('.taskhead:visible'); items.each(function(i, item) { if (item === target) { x = i; return false; } }); items.get(x + inc).focus(); return rcube_event.cancel(e); case 37: // Left arrow key case 39: // Right arrow key $(this).parent().children('.childtoggle:visible').first().trigger('click', { pointerType:'keyboard' }); break; } }) .on('focusin', '.taskhead', function(e){ if (rcube_event.is_keyboard(e)) { var item = $(e.target); if (!item.hasClass('taskhead')) item = item.closest('div.taskhead'); var id = item.data('id'); if (id && listdata[id]) { focused_task = id; focused_subclass = item.get(0) !== e.target ? e.target.className : null; } } }) .on('focusout', '.taskhead', function(e){ var item = $(e.target); if (focused_task && item.data('id') == focused_task) { focused_task = focused_subclass = null; } }); /** * */ function task_rsvp(response, delegate) { if (me.selected_task && me.selected_task.attendees && response) { // bring up delegation dialog if (response == 'delegated' && !delegate) { rcube_libcalendaring.itip_delegate_dialog(function(data) { $('#reply-comment-task-rsvp').val(data.comment); data.rsvp = data.rsvp ? 1 : ''; task_rsvp('delegated', data); }); return; } // update attendee status for (var data, i=0; i < me.selected_task.attendees.length; i++) { data = me.selected_task.attendees[i]; if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) { data.status = response.toUpperCase(); if (data.status == 'DELEGATED') { data['delegated-to'] = delegate.to; } else { delete data.rsvp; // unset RSVP flag if (data['delegated-to']) { delete data['delegated-to']; if (data.role == 'NON-PARTICIPANT' && data.status != 'DECLINED') { data.role = 'REQ-PARTICIPANT'; } } } } } // submit status change to server saving_lock = rcmail.set_busy(true, 'tasklist.savingdata'); rcmail.http_post('tasks/task', { action: 'rsvp', t: $.extend({}, me.selected_task, (delegate || {})), filter: filtermask, status: response, noreply: $('#noreply-task-rsvp:checked').length ? 1 : 0, comment: $('#reply-comment-task-rsvp').val() }); task_show_dialog(me.selected_task.id); } } // init RSVP widget $('#task-rsvp input.button').click(function(e) { task_rsvp($(this).attr('rel')) }); // register click handler for message links $('#task-links, #taskedit-links').on('click', 'li a.messagelink', function(e) { rcmail.open_window(this.href); return false; }); // register click handler for message delete buttons $('#taskedit-links').on('click', 'li a.delete', function(e) { remove_link(e.target); return false; }); // extended datepicker settings var extended_datepicker_settings = $.extend({ showButtonPanel: true, beforeShow: function(input, inst) { setTimeout(function(){ $(input).datepicker('widget').find('button.ui-datepicker-close') .html(rcmail.gettext('nodate','tasklist')) .attr('onclick', '') .unbind('click') .bind('click', function(e){ $(input).datepicker('setDate', null).datepicker('hide'); }); }, 1); } }, datepicker_settings); } /** * initialize task edit form elements */ function init_taskedit() { $('#taskedit').tabs({ activate: function(event, ui) { // reset autocompletion on tab change (#3389) if (ui.oldPanel.selector == '#taskedit-panel-attendees') { rcmail.ksearch_blur(); } } }); var completeness_slider_change = function(e, ui){ var v = completeness_slider.slider('value'); if (v >= 98) v = 100; if (v <= 2) v = 0; $('#taskedit-completeness').val(v); }; completeness_slider = $('#taskedit-completeness-slider').slider({ range: 'min', animate: 'fast', slide: completeness_slider_change, change: completeness_slider_change }); $('#taskedit-completeness').change(function(e){ completeness_slider.slider('value', parseInt(this.value)) }); // register events on alarms and recurrence fields me.init_alarms_edit('#taskedit-alarms'); me.init_recurrence_edit('#eventedit'); $('#taskedit-date, #taskedit-startdate').datepicker(datepicker_settings); $('a.edit-nodate').click(function(){ var sel = $(this).attr('rel'); if (sel) $(sel).val(''); return false; }); // init attendees autocompletion var ac_props; // parallel autocompletion if (rcmail.env.autocomplete_threads > 0) { ac_props = { threads: rcmail.env.autocomplete_threads, sources: rcmail.env.autocomplete_sources }; } rcmail.init_address_input_events($('#edit-attendee-name'), ac_props); rcmail.addEventListener('autocomplete_insert', function(e) { var success = false; if (e.field.name == 'participant') { success = add_attendees(e.insert, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:(e.data && e.data.type == 'group' ? 'GROUP' : 'INDIVIDUAL') }); } if (e.field && success) { e.field.value = ''; } }); $('#edit-attendee-add').click(function() { var input = $('#edit-attendee-name'); rcmail.ksearch_blur(); if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) { input.val(''); } }); // handle change of "send invitations" checkbox $('#edit-attendees-invite').change(function() { $('#edit-attendees-donotify,input.edit-attendee-reply').prop('checked', this.checked); // hide/show comment field $('#taskeditform .attendees-commentbox')[this.checked ? 'show' : 'hide'](); }); // delegate change task to "send invitations" checkbox $('#edit-attendees-donotify').change(function() { $('#edit-attendees-invite').click(); return false; }); // configure drop-down menu on time input fields based on jquery UI autocomplete $('#taskedit-starttime, #taskedit-time, #taskedit input.edit-alarm-time').each(function() { me.init_time_autocomplete(this, {container: '#taskedit'}); }); } /** * Check permissions on the given list object */ function has_permission(list, perm) { // multiple chars means "either of" if (String(perm).length > 1) { for (var i=0; i < perm.length; i++) { if (has_permission(list, perm[i])) { return true; } } } if (list.rights && String(list.rights).indexOf(perm) >= 0) { return true; } return (perm == 'i' && list.editable); } /** * Request counts from the server */ function fetch_counts() { var active = active_lists(); if (active.length) rcmail.http_request('counts', { lists:active.join(',') }); else update_counts({}); } /** * List tasks matching the given selector */ function list_tasks(sel, force) { if (rcmail.busy) return; if (sel && filter_masks[sel] !== undefined) { filtermask = filter_masks[sel]; } var active = active_lists(), basefilter = filtermask & FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL, reload = force || active.join(',') != loadstate.lists || basefilter != loadstate.filter || loadstate.search != search_query; if (active.length && reload) { ui_loading = rcmail.set_busy(true, 'loading'); rcmail.http_request('fetch', { filter:basefilter, lists:active.join(','), q:search_query }, true); } else if (reload) data_ready({ data:[], lists:'', filter:basefilter, search:search_query }); else render_tasklist(); $('#taskselector li.selected').removeClass('selected').attr('aria-checked', 'false'); // select all active selectors if (filtermask > 0) { $.each(filter_masks, function(sel, mask) { if (filtermask & mask) $('#taskselector li.'+sel).addClass('selected').attr('aria-checked', 'true'); }); } else $('#taskselector li.all').addClass('selected').attr('aria-checked', 'true'); } /** * Remove all tasks of the given list from the UI */ function remove_tasks(list_id) { // remove all tasks of the given list from index var newindex = $.grep(listindex, function(id, i){ return listdata[id] && listdata[id].list != list_id; }); listindex = newindex; render_tasklist(); // avoid reloading me.tasklists[list_id].active = false; loadstate.lists = active_lists(); } // open a tasks export dialog this.export_tasks = function() { // close show dialog first var $dialog = $("#tasksexport"), form = rcmail.gui_objects.exportform, buttons = {}; if (!form) return; if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); $("#task-export-list").val(''); buttons[rcmail.gettext('export', 'tasklist')] = function() { var data = {}, source = $('#task-export-list option:selected').val(), form_elements = $('select, input', form); // "current view" export, use hidden form to POST task IDs if (source === '') { var cache = {}, tasks = [], inputs = [], postform = $('#tasks-export-form-post'); $.each(listindex || [], function() { var rec = listdata[this]; if (match_filter(rec, cache)) { tasks.push(rec.id); } }); // copy form inputs, there may be controls added by other plugins form_elements.each(function() { if (this.type != 'checkbox' || this.checked) inputs.push($('').attr({type: 'hidden', name: this.name, value: this.value})); }); inputs.push($('').attr({type: 'hidden', name: '_token', value: rcmail.env.request_token})); inputs.push($('').attr({type: 'hidden', name: 'id', value: tasks.join(',')})); if (!postform.length) postform = $('
') .attr({style: 'display: none', method: 'POST', action: '?_task=tasks&_action=export'}) .appendTo('body'); postform.html('').append(inputs).submit(); } // otherwise we can use simple GET else { form_elements.each(function() { if (this.type != 'checkbox' || this.checked) data[this.name] = $(this).val(); }); rcmail.goto_url('export', data); } $dialog.dialog("close"); }; buttons[rcmail.gettext('cancel', 'tasklist')] = function() { $dialog.dialog("close"); }; // open jquery UI dialog $dialog.dialog({ modal: true, resizable: false, closeOnEscape: false, title: rcmail.gettext('exporttitle', 'tasklist'), open: function() { $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction'); }, close: function() { $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable'); $dialog.dialog("destroy").hide(); }, buttons: buttons, width: 520 }).show(); }; /* // download the selected task as iCal this.task_download = function(task) { if (task && task.id) { rcmail.goto_url('export', {source: task.list, id: task.id, attachments: 1}); } }; */ /** * Modify query parameters for refresh requests */ function before_refresh(query) { query.filter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL; query.lists = active_lists().join(','); if (search_query) query.q = search_query; return query; } /** * Callback if task data from server is ready */ function data_ready(response) { listdata = {}; listindex = []; loadstate.lists = response.lists; loadstate.filter = response.filter; loadstate.search = response.search; for (var id, i=0; i < response.data.length; i++) { id = response.data[i].id; listindex.push(id); listdata[id] = response.data[i]; listdata[id].children = []; // register a forward-pointer to child tasks if (listdata[id].parent_id && listdata[listdata[id].parent_id]) listdata[listdata[id].parent_id].children.push(id); } // sort index before rendering listindex.sort(function(a, b) { return task_cmp(listdata[a], listdata[b]); }); append_tags(response.tags || []); render_tasklist(); // show selected task dialog if (settings.selected_id) { if (listdata[settings.selected_id]) { task_show_dialog(settings.selected_id); delete settings.selected_id; } // remove _id from window location if (window.history.replaceState) { window.history.replaceState({}, document.title, rcmail.url('', { _list: me.selected_list })); } } rcmail.set_busy(false, 'loading', ui_loading); } /** * */ function render_tasklist() { // clear display var id, rec, count = 0, cache = {}, activetags = {}, msgbox = $('#listmessagebox').hide(), list = $(rcmail.gui_objects.resultlist).html(''); for (var i=0; i < listindex.length; i++) { id = listindex[i]; rec = listdata[id]; if (match_filter(rec, cache)) { if (rcmail.env.action == 'print') { render_task_printmode(rec); continue; } render_task(rec); count++; // keep a list of tags from all visible tasks for (var t, j=0; rec.tags && j < rec.tags.length; j++) { t = rec.tags[j]; if (typeof activetags[t] == 'undefined') activetags[t] = 0; activetags[t]++; } } } if (rcmail.env.action == 'print') return; fix_tree_toggles(); update_tagcloud(activetags); if (!count) { msgbox.html(rcmail.gettext('notasksfound','tasklist')).show(); rcmail.display_message(rcmail.gettext('notasksfound','tasklist'), 'voice'); } } /** * Show/hide child toggle buttons on all visible task items */ function fix_tree_toggles() { $('.taskitem', rcmail.gui_objects.resultlist).each(function(i,elem){ var li = $(elem), rec = listdata[li.attr('rel')], childs = $('.childtasks li', li); $('.childtoggle', li)[(childs.length ? 'show' : 'hide')](); }) } /** * Expand/collapse all task items with childs */ function expand_collapse(expand) { var collapsed = !expand; $('.taskitem .childtasks')[(collapsed ? 'hide' : 'show')](); $('.taskitem .childtoggle') .removeClass(collapsed ? 'expanded' : 'collapsed') .addClass(collapsed ? 'collapsed' : 'expanded') .html(collapsed ? '▶' : '▼'); // store new toggle collapse states var ids = []; for (var id in listdata) { if (listdata[id].children && listdata[id].children.length) ids.push(id); } if (ids.length) { rcmail.http_post('tasks/task', { action:'collapse', t:{ id:ids.join(',') }, collapsed:collapsed?1:0 }); } } /** * */ function append_tags(taglist) { // find new tags var newtags = []; for (var i=0; i < taglist.length; i++) { if ($.inArray(taglist[i], tags) < 0) newtags.push(taglist[i]); } tags = tags.concat(newtags); // append new tags to tag cloud $.each(newtags, function(i, tag){ $('
  • ') .attr('rel', tag) .data('value', tag) .html(Q(tag) + '') .appendTo(rcmail.gui_objects.tagslist) .draggable({ addClasses: false, revert: 'invalid', revertDuration: 300, helper: tag_draggable_helper, start: tag_draggable_start, appendTo: 'body', cursor: 'pointer' }); }); // re-sort tags list $(rcmail.gui_objects.tagslist).children('li').sortElements(function(a,b){ return $.text([a]).toLowerCase() > $.text([b]).toLowerCase() ? 1 : -1; }); } /** * Display the given counts to each tag and set those inactive which don't * have any matching tasks in the current view. */ function update_tagcloud(counts) { // compute counts first by iterating over all visible task items if (typeof counts == 'undefined') { counts = {}; $('li.taskitem', rcmail.gui_objects.resultlist).each(function(i,li){ var t, id = $(li).attr('rel'), rec = listdata[id]; for (var j=0; rec && rec.tags && j < rec.tags.length; j++) { t = rec.tags[j]; if (typeof counts[t] == 'undefined') counts[t] = 0; counts[t]++; } }); } $(rcmail.gui_objects.tagslist).children('li').each(function(i,li){ var elem = $(li), tag = elem.attr('rel'), count = counts[tag] || 0; elem.children('.count').html(count+''); if (count == 0) elem.addClass('inactive'); else elem.removeClass('inactive'); }); } /* Helper functions for drag & drop functionality of tags */ function tag_draggable_helper() { if (!tag_draghelper) tag_draghelper = $('
    '); else tag_draghelper.html(''); $(this).clone().addClass('tag').appendTo(tag_draghelper); return tag_draghelper; } function tag_draggable_start(event, ui) { $('.taskhead').droppable({ hoverClass: 'droptarget', accept: tag_droppable_accept, drop: tag_draggable_dropped, addClasses: false }); } function tag_droppable_accept(draggable) { if (rcmail.busy) return false; var tag = draggable.data('value'), drop_id = $(this).data('id'), drop_rec = listdata[drop_id], list = drop_rec && me.tasklists[drop_rec.list] ? me.tasklists[drop_rec.list] : { editable:true }; // target is not editable or already has this tag assigned if (!drop_rec || drop_rec.readonly || !has_permission(list, 'i') || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) { return false; } return true; } function tag_draggable_dropped(event, ui) { var drop_id = $(this).data('id'), tag = ui.draggable.data('value'), rec = listdata[drop_id]; if (rec && rec.id) { if (!rec.tags) rec.tags = []; rec.tags.push(tag); save_task(rec, 'edit'); } } /** * */ function update_counts(counts) { // got new data if (counts) taskcounts = counts; // iterate over all selector links and update counts $('#taskselector a').each(function(i, elem){ var link = $(elem), f = link.parent().attr('class').replace(/\s\w+/g, ''); if (f != 'all') link.children('span').html('+' + (taskcounts[f] || ''))[(taskcounts[f] ? 'show' : 'hide')](); }); // spacial case: overdue $('#taskselector li.overdue')[(taskcounts.overdue ? 'removeClass' : 'addClass')]('inactive'); } /** * Callback from server to update a single task item */ function update_taskitem(rec, filter) { // handle a list of task records if ($.isArray(rec)) { $.each(rec, function(i,r){ update_taskitem(r, filter); }); return; } var id = rec.id, oldid = rec.tempid || id, oldrec = listdata[oldid], oldindex = $.inArray(oldid, listindex), oldparent = oldrec ? (oldrec._old_parent_id || oldrec.parent_id) : null, list = me.tasklists[rec.list]; if (oldindex >= 0) listindex[oldindex] = id; else listindex.push(id); listdata[id] = rec; // remove child-pointer from old parent if (oldparent && listdata[oldparent] && oldparent != rec.parent_id) { var oldchilds = listdata[oldparent].children, i = $.inArray(oldid, oldchilds); if (i >= 0) { listdata[oldparent].children = oldchilds.slice(0,i).concat(oldchilds.slice(i+1)); } } // register a forward-pointer to child tasks if (rec.parent_id && listdata[rec.parent_id] && listdata[rec.parent_id].children && $.inArray(id, listdata[rec.parent_id].children) < 0) listdata[rec.parent_id].children.push(id); // restore pointers to my children if (!listdata[id].children) { listdata[id].children = []; for (var pid in listdata) { if (listdata[pid].parent_id == id) listdata[id].children.push(pid); } } // copy _depth property from old rec or derive from parent if (rec.parent_id && listdata[rec.parent_id]) { rec._depth = (listdata[rec.parent_id]._depth || 0) + 1; } else if (oldrec) { rec._depth = oldrec._depth || 0; } if (list.active || rec.tempid) { if (!filter || match_filter(rec, {})) render_task(rec, oldid); } else { $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).remove(); } append_tags(rec.tags || []); update_tagcloud(); fix_tree_toggles(); // refresh currently displayed task details dialog if ($('#taskshow').is(':visible') && me.selected_task && me.selected_task.id == rec.id) { task_show_dialog(rec.id); } } /** * Submit the given (changed) task record to the server */ function save_task(rec, action) { // show confirmation dialog when status of an assigned task has changed if (rec._status_before !== undefined && is_attendee(rec)) return save_task_confirm(rec, action); if (!rcmail.busy) { saving_lock = rcmail.set_busy(true, 'tasklist.savingdata'); rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask }); $('button.ui-button:ui-button').button('option', 'disabled', rcmail.busy); return true; } return false; } /** * Display confirm dialog when modifying/deleting a task record */ var save_task_confirm = function(rec, action, updates) { var data = $.extend({}, rec, updates || {}), notify = false, partstat = false, html = '', do_confirm = settings.itip_notify & 2; // task has attendees, ask whether to notify them if (has_attendees(rec) && is_organizer(rec)) { notify = true; if (do_confirm) { html = rcmail.gettext('changeconfirmnotifications', 'tasklist'); } else { data._notify = settings.itip_notify; } } // ask whether to change my partstat and notify organizer else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) { partstat = true; if (do_confirm) { html = rcmail.gettext('partstatupdatenotification', 'tasklist'); } else if (settings.itip_notify & 1) { data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status; } } // remove to avoid endless recursion delete data._status_before; // show dialog if (html) { var $dialog = $('
    ').html(html); var buttons = []; buttons.push({ text: rcmail.gettext('saveandnotify', 'tasklist'), click: function() { if (notify) data._notify = 1; if (partstat) data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status; save_task(data, action); $(this).dialog('close'); } }); buttons.push({ text: rcmail.gettext('save', 'tasklist'), click: function() { save_task(data, action); $(this).dialog('close'); } }); buttons.push({ text: rcmail.gettext('cancel', 'tasklist'), click: function() { $(this).dialog('close'); if (updates) render_task(rec, rec.id); // restore previous state } }); $dialog.dialog({ modal: true, width: 460, closeOnEscapeType: false, dialogClass: 'warning no-close', title: rcmail.gettext('changetaskconfirm', 'tasklist'), buttons: buttons, open: function() { setTimeout(function(){ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); }, 5); }, close: function(){ $dialog.dialog('destroy').remove(); } }).addClass('task-update-confirm').show(); return true; } // do update return save_task(data, action); } /** * Remove saving lock and free the UI for new input */ function unlock_saving() { if (saving_lock) { rcmail.set_busy(false, null, saving_lock); $('button.ui-button:ui-button').button('option', 'disabled', false); } } /** * Render the given task into the tasks list */ function render_task(rec, replace) { var tags_html = ''; for (var j=0; rec.tags && j < rec.tags.length; j++) tags_html += '' + Q(rec.tags[j]) + ''; var label_id = rcmail.html_identifier(rec.id) + '-title'; var div = $('
    ').addClass('taskhead').html( '
    ' + '' + '' + '' + text2html(Q(rec.title)) + '' + '' + tags_html + '' + '' + Q(rec.date || rcmail.gettext('nodate','tasklist')) + '' + '' ) .attr('tabindex', '0') .attr('aria-labelledby', label_id) .data('id', rec.id) .draggable({ revert: 'invalid', addClasses: false, cursorAt: { left:-10, top:12 }, helper: task_draggable_helper, appendTo: 'body', start: task_draggable_start, stop: task_draggable_stop, drag: task_draggable_move, revertDuration: 300 }); if (is_complete(rec)) div.addClass('complete'); if (rec.flagged) div.addClass('flagged'); if (!rec.date) div.addClass('nodate'); if ((rec.mask & FILTER_MASK_OVERDUE)) div.addClass('overdue'); var li, inplace = false, parent = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : null; if (replace && (li = $('li[rel="'+replace+'"]', rcmail.gui_objects.resultlist)) && li.length) { li.children('div.taskhead').first().replaceWith(div); li.attr('rel', rec.id); inplace = true; } else { li = $('
  • ') .attr('rel', rec.id) .addClass('taskitem') .append((rec.collapsed ? '
  • ').attr('rel', rec.id).addClass('taskitem') .append(div) .append('
      '); if (rec.description) div.append($('').text(rec.description)); /* if (is_complete(rec)) div.addClass('complete'); if (rec.flagged) div.addClass('flagged'); if (!rec.date) div.addClass('nodate'); if (rec.mask & FILTER_MASK_OVERDUE) div.addClass('overdue'); */ if (!parent || !parent.length) li.appendTo(rcmail.gui_objects.resultlist); else li.appendTo(parent); } /** * Move the given task item to the right place in the list */ function resort_task(rec, li, animated) { var dir = 0, index, slice, cmp, next_li, next_id, next_rec, insert_after, past_myself; // animated moving var insert_animated = function(li, before, after) { if (before && li.next().get(0) == before.get(0)) return; // nothing to do else if (after && li.prev().get(0) == after.get(0)) return; // nothing to do var speed = 300; li.slideUp(speed, function(){ if (before) li.insertBefore(before); else if (after) li.insertAfter(after); li.slideDown(speed, function(){ if (focused_task == rec.id) { focus_task(li); } }); }); } // remove from list index var oldlist = listindex.join('%%%'); var oldindex = $.inArray(rec.id, listindex); if (oldindex >= 0) { slice = listindex.slice(0,oldindex); listindex = slice.concat(listindex.slice(oldindex+1)); } // find the right place to insert the task item li.parent().children('.taskitem').each(function(i, elem){ next_li = $(elem); next_id = next_li.attr('rel'); next_rec = listdata[next_id]; if (next_id == rec.id) { past_myself = true; return 1; // continue } cmp = next_rec ? task_cmp(rec, next_rec) : 0; if (cmp > 0 || (cmp == 0 && !past_myself)) { insert_after = next_li; return 1; // continue; } else if (next_li && cmp < 0) { if (animated) insert_animated(li, next_li); else li.insertBefore(next_li); index = $.inArray(next_id, listindex); return false; // break } }); if (insert_after) { if (animated) insert_animated(li, null, insert_after); else li.insertAfter(insert_after); next_id = insert_after.attr('rel'); index = $.inArray(next_id, listindex); } // insert into list index if (next_id && index >= 0) { slice = listindex.slice(0,index); slice.push(rec.id); listindex = slice.concat(listindex.slice(index)); } else { // restore old list index listindex = oldlist.split('%%%'); } } /** * Compare function of two task records. * (used for sorting) */ function task_cmp(a, b) { // sort by hierarchy level first if ((a._depth || 0) != (b._depth || 0)) return a._depth - b._depth; var p, alt, inv = 1, c = is_complete(a) - is_complete(b), d = c; // completed tasks always move to the end if (c != 0) return c; // custom sorting if (settings.sort_col && settings.sort_col != 'auto') { alt = settings.sort_col == 'datetime' || settings.sort_col == 'startdatetime' ? 99999999999 : 0 d = (a[settings.sort_col]||alt) - (b[settings.sort_col]||alt); inv = settings.sort_order == 'desc' ? -1 : 1; } // default sorting (auto) else { if (!d) d = (b._hasdate-0) - (a._hasdate-0); if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999); } // fall-back to created/changed date if (!d) d = (a.created||0) - (b.created||0); if (!d) d = (a.changed||0) - (b.changed||0); return d * inv; } /** * Set focus on the given task item after DOM update */ function focus_task(li) { var selector = '.taskhead'; if (focused_subclass) selector += ' .' + focused_subclass li.find(selector).focus(); } /** * Determine whether the given task should be displayed as "complete" */ function is_complete(rec) { return ((rec.complete == 1.0 && !rec.status) || rec.status === 'COMPLETED') ? 1 : 0; } /** * */ function get_all_childs(id) { var cid, childs = []; for (var i=0; listdata[id].children && i < listdata[id].children.length; i++) { cid = listdata[id].children[i]; childs.push(cid); childs = childs.concat(get_all_childs(cid)); } return childs; } /* Helper functions for drag & drop functionality */ function task_draggable_helper() { if (!task_draghelper) task_draghelper = $('
      '); return task_draghelper; } function task_draggable_start(event, ui) { var opts = { hoverClass: 'droptarget', accept: task_droppable_accept, drop: task_draggable_dropped, addClasses: false }; $('.taskhead, #rootdroppable').droppable(opts); tasklists_widget.droppable(opts); $(this).parent().addClass('dragging'); $('#rootdroppable').show(); // enable auto-scrolling of list container var container = $(rcmail.gui_objects.resultlist); if (container.height() > container.parent().height()) { task_drag_active = true; list_scroll_top = container.parent().scrollTop(); } } function task_draggable_move(event, ui) { var scroll = 0, mouse = rcube_event.get_mouse_pos(event), container = $(rcmail.gui_objects.resultlist); mouse.y -= container.parent().offset().top; if (mouse.y < scroll_sensitivity && list_scroll_top > 0) { scroll = -1; // up } else if (mouse.y > container.parent().height() - scroll_sensitivity) { scroll = 1; // down } if (task_drag_active && scroll != 0) { if (!scroll_timer) scroll_timer = window.setTimeout(function(){ tasklist_drag_scroll(container, scroll); }, scroll_delay); } else if (scroll_timer) { window.clearTimeout(scroll_timer); scroll_timer = null; } } function task_draggable_stop(event, ui) { $(this).parent().removeClass('dragging'); $('#rootdroppable').hide(); task_drag_active = false; } function task_droppable_accept(draggable) { if (rcmail.busy) return false; var drag_id = draggable.data('id'), drop_id = $(this).data('id'), drag_rec = listdata[drag_id] || {}, drop_rec = listdata[drop_id]; // drop target is another list if (drag_rec && $(this).data('type') == 'tasklist') { var drop_list = me.tasklists[drop_id], from_list = me.tasklists[drag_rec.list]; return !drag_rec.parent_id && drop_id != drag_rec.list && drop_list && drop_list.editable && from_list && from_list.editable; } if (drop_rec && drop_rec.list != drag_rec.list) return false; if (drop_id == drag_rec.parent_id) return false; while (drop_rec && drop_rec.parent_id) { if (drop_rec.parent_id == drag_id) return false; drop_rec = listdata[drop_rec.parent_id]; } return true; } function task_draggable_dropped(event, ui) { var drop_id = $(this).data('id'), task_id = ui.draggable.data('id'), rec = listdata[task_id], parent, li; // dropped on another list -> move if ($(this).data('type') == 'tasklist') { if (rec) { save_task({ id:rec.id, list:drop_id, _fromlist:rec.list }, 'move'); rec.list = drop_id; } } // dropped on a new parent task or root else { parent = drop_id ? $('li[rel="'+drop_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : $(rcmail.gui_objects.resultlist) if (rec && parent.length) { // submit changes to server rec._old_parent_id = rec.parent_id; rec.parent_id = drop_id || 0; save_task(rec, 'edit'); li = ui.draggable.parent(); li.slideUp(300, function(){ li.appendTo(parent); resort_task(rec, li); li.slideDown(300); fix_tree_toggles(); }); } } } /** * Scroll list container in the given direction */ function tasklist_drag_scroll(container, dir) { if (!task_drag_active) return; var old_top = list_scroll_top; container.parent().get(0).scrollTop += scroll_step * dir; list_scroll_top = container.parent().scrollTop(); scroll_timer = null; if (list_scroll_top != old_top) scroll_timer = window.setTimeout(function(){ tasklist_drag_scroll(container, dir); }, scroll_speed); } // check if the task has 'real' attendees, excluding the current user var has_attendees = function(task) { return !!(task.attendees && task.attendees.length && (task.attendees.length > 1 || String(task.attendees[0].email).toLowerCase() != settings.identity.email)); }; // check if the current user is an attendee of this task var is_attendee = function(task, email, role) { var i, attendee, emails = email ? ';' + email.toLowerCase() : settings.identity.emails; for (i=0; task.attendees && i < task.attendees.length; i++) { attendee = task.attendees[i]; if ((!role || attendee.role == role) && attendee.email && emails.indexOf(';'+attendee.email.toLowerCase()) >= 0) { return attendee; } } return false; }; // check if the current user is the organizer var is_organizer = function(task, email) { if (!email) email = task.organizer ? task.organizer.email : null; if (email) return settings.identity.emails.indexOf(';'+email) >= 0; return true; }; // add the given list of participants var add_attendees = function(names, params) { names = explode_quoted_string(names.replace(/,\s*$/, ''), ','); // parse name/email pairs var i, item, email, name, success = false; for (i=0; i < names.length; i++) { email = name = ''; item = $.trim(names[i]); if (!item.length) { continue; } // address in brackets without name (do nothing) else if (item.match(/^<[^@]+@[^>]+>$/)) { email = item.replace(/[<>]/g, ''); } // address without brackets and without name (add brackets) else if (rcube_check_email(item)) { email = item; } // address with name else if (item.match(/([^\s<@]+@[^>]+)>*$/)) { email = RegExp.$1; name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, ''); } if (email) { add_attendee($.extend({ email:email, name:name }, params)); success = true; } else { alert(rcmail.gettext('noemailwarning')); } } return success; }; // add the given attendee to the list var add_attendee = function(data, readonly, before) { if (!me.selected_task) return false; // check for dupes... var exists = false; $.each(task_attendees, function(i, v) { exists |= (v.email == data.email); }); if (exists) return false; var dispname = Q(data.name || data.email); if (data.email) dispname = '' + dispname + ''; // delete icon var icon = rcmail.env.deleteicon ? '' : rcmail.gettext('delete'); var dellink = '' + icon + ''; var tooltip, status = (data.status || '').toLowerCase(), status_label = rcmail.gettext('status' + status, 'libcalendaring'); // send invitation checkbox var invbox = ''; if (data['delegated-to']) tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to']; else if (data['delegated-from']) tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from']; else if (status) tooltip = status_label; // add expand button for groups if (data.cutype == 'GROUP') { dispname += ' ' + rcmail.gettext('expandattendeegroup','libcalendaring') + ''; } var html = '' + dispname + '' + '' + Q(status ? status_label : '') + '' + (data.cutype != 'RESOURCE' ? '' + (readonly || !invbox ? '' : invbox) + '' : '') + '' + (readonly ? '' : dellink) + ''; var tr = $('') .addClass(String(data.role).toLowerCase()) .html(html); if (before) tr.insertBefore(before) else tr.appendTo(attendees_list); tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; }); tr.find('a.mailtolink').click(task_attendee_click); tr.find('a.expandlink').click(data, function(e) { me.expand_attendee_group(e, add_attendee, remove_attendee); return false; }); tr.find('input.edit-attendee-reply').click(function() { var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length; $('#taskeditform .attendees-commentbox')[enabled ? 'show' : 'hide'](); }); task_attendees.push(data); return true; }; // event handler for clicks on an attendee link var task_attendee_click = function(e) { var mailto = this.href.substr(7); rcmail.command('compose', mailto); return false; }; // remove an attendee from the list var remove_attendee = function(elem, id) { $(elem).closest('tr').remove(); task_attendees = $.grep(task_attendees, function(data) { return (data.name != id && data.email != id) }); }; /** * Show task details in a dialog */ function task_show_dialog(id, data, temp) { var $dialog = $('#taskshow'), rec, list; if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); // remove status-* classes $dialog.removeClass(function(i, oldclass) { var oldies = String(oldclass).split(' '); return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' '); }); if (!(rec = (data || listdata[id])) || (rcmail.menu_stack && rcmail.menu_stack.length > 0)) return; me.selected_task = rec; list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {}; // fill dialog data $('#task-parent-title').html(Q(rec.parent_title || '')+' »').css('display', rec.parent_title ? 'block' : 'none'); $('#task-title').html(text2html(Q(rec.title || ''))); $('#task-description').html(text2html(rec.description || '', 300, 6))[(rec.description ? 'show' : 'hide')](); $('#task-date')[(rec.date ? 'show' : 'hide')]().children('.task-text').html(Q(rec.date || rcmail.gettext('nodate','tasklist'))); $('#task-time').html(Q(rec.time || '')); $('#task-start')[(rec.startdate ? 'show' : 'hide')]().children('.task-text').html(Q(rec.startdate || '')); $('#task-starttime').html(Q(rec.starttime || '')); $('#task-alarm')[(rec.alarms_text ? 'show' : 'hide')]().children('.task-text').html(Q(rec.alarms_text)); $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%'); $('#task-status')[(rec.status ? 'show' : 'hide')]().children('.task-text').html(rcmail.gettext('status-'+String(rec.status).toLowerCase(),'tasklist')); $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : '')); $('#task-attendees, #task-organizer, #task-created-changed, #task-rsvp, #task-rsvp-comment').hide(); var itags = get_inherited_tags(rec); var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty(); if (rec.tags && rec.tags.length) { $.each(rec.tags, function(i,val){ $('').addClass('tag-element').html(Q(val)).appendTo(taglist); }); } // append inherited tags if (itags.length) { $.each(itags, function(i,val){ if (!rec.tags || $.inArray(val, rec.tags) < 0) $('').addClass('tag-element inherit').html(Q(val)).appendTo(taglist); }); // re-sort tags list $(taglist).children().sortElements(function(a,b){ return $.text([a]).toLowerCase() > $.text([b]).toLowerCase() ? 1 : -1; }); } if (rec.status) { $dialog.addClass('status-' + String(rec.status).toLowerCase()); } if (rec.flagged) { $dialog.addClass('status-flagged'); } if (rec.recurrence && rec.recurrence_text) { $('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text)); } else { $('#task-recurrence').hide(); } if (rec.created || rec.changed) { $('#task-created-changed .task-created').html(Q(rec.created_ || rcmail.gettext('unknown','tasklist'))) $('#task-created-changed .task-changed').html(Q(rec.changed_ || rcmail.gettext('unknown','tasklist'))) $('#task-created-changed').show() } // build attachments list $('#task-attachments').hide(); if ($.isArray(rec.attachments)) { task_show_attachments(rec.attachments || [], $('#task-attachments').children('.task-text'), rec); if (rec.attachments.length > 0) { $('#task-attachments').show(); } } // build attachments list $('#task-links').hide(); if ($.isArray(rec.links) && rec.links.length) { render_message_links(rec.links || [], $('#task-links').children('.task-text'), false, 'tasklist'); $('#task-links').show(); } // list task attendees if (list.attendees && rec.attendees) { /* // sort resources to the end rec.attendees.sort(function(a,b) { var j = a.cutype == 'RESOURCE' ? 1 : 0, k = b.cutype == 'RESOURCE' ? 1 : 0; return (j - k); }); */ var j, data, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '', organizer = is_organizer(rec); for (j=0; j < rec.attendees.length; j++) { data = rec.attendees[j]; if (data.email && settings.identity.emails.indexOf(';'+data.email) >= 0) { mystatus = data.status.toLowerCase(); if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) rsvp = mystatus; } line = rcube_libcalendaring.attendee_html(data); if (morelink) overflow += line; else html += line; // stop listing attendees if (j == 7 && rec.attendees.length >= 7) { morelink = $('').html(rcmail.gettext('andnmore', 'tasklist').replace('$nr', rec.attendees.length - j - 1)); } } if (html) { $('#task-attendees').show() .children('.task-text') .html(html) .find('a.mailtolink').click(task_attendee_click); // display all attendees in a popup when clicking the "more" link if (morelink) { $('#task-attendees .task-text').append(morelink); morelink.click(function(e) { rcmail.show_popup_dialog( '
      ' + html + overflow + '
      ', rcmail.gettext('tabattendees', 'tasklist'), null, {width: 450, modal: false} ); $('#all-task-attendees a.mailtolink').click(task_attendee_click); return false; }); } } /* if (mystatus && !rsvp) { $('#task-partstat').show().children('.changersvp') .removeClass('accepted tentative declined delegated needs-action') .addClass(mystatus) .children('.task-text') .html(rcmail.gettext('status' + mystatus, 'libcalendaring')); } */ var show_rsvp = !temp && rsvp && list.editable && !is_organizer(rec) && rec.status != 'CANCELLED'; $('#task-rsvp')[(show_rsvp ? 'show' : 'hide')](); $('#task-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true); if (show_rsvp && rec.comment) { $('#task-rsvp-comment').show().children('.task-text').html(Q(rec.comment)); } $('#task-rsvp a.reply-comment-toggle').show(); $('#task-rsvp .itip-reply-comment textarea').hide().val(''); if (rec.organizer && !organizer) { $('#task-organizer').show().children('.task-text').html(rcube_libcalendaring.attendee_html($.extend(rec.organizer, { role:'ORGANIZER' }))); } } // define dialog buttons var buttons = []; if (list.editable && !rec.readonly) { buttons.push({ text: rcmail.gettext('edit','tasklist'), click: function() { task_edit_dialog(me.selected_task.id, 'edit'); }, disabled: rcmail.busy }); } if (has_permission(list, 'td') && !rec.readonly) { buttons.push({ text: rcmail.gettext('delete','tasklist'), 'class': 'delete', click: function() { if (delete_task(me.selected_task.id)) $dialog.dialog('close'); }, disabled: rcmail.busy }); } // open jquery UI dialog $dialog.dialog({ modal: false, resizable: true, closeOnEscape: true, title: rcmail.gettext('taskdetails', 'tasklist'), open: function() { $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); }, close: function() { $dialog.dialog('destroy').appendTo(document.body); $('.libcal-rsvp-replymode').hide(); }, dragStart: function() { $('.libcal-rsvp-replymode').hide(); }, resizeStart: function() { $('.libcal-rsvp-replymode').hide(); }, buttons: buttons, minWidth: 500, width: 580 }).show(); // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 580); } /** * */ function task_history_dialog() { var dialog, rec = me.selected_task; if (!rec || !rec.id || !window.libkolab_audittrail) { return false; } // render dialog $dialog = libkolab_audittrail.object_history_dialog({ module: 'tasklist', container: '#taskhistory', title: rcmail.gettext('objectchangelog','tasklist') + ' - ' + rec.title, // callback function for list actions listfunc: function(action, rev) { var rec = $dialog.data('rec'); saving_lock = rcmail.set_busy(true, 'loading', saving_lock); rcmail.http_post('task', { action: action, t: { id: rec.id, list:rec.list, rev: rev } }, saving_lock); }, // callback function for comparing two object revisions comparefunc: function(rev1, rev2) { var rec = $dialog.data('rec'); saving_lock = rcmail.set_busy(true, 'loading', saving_lock); rcmail.http_post('task', { action:'diff', t: { id: rec.id, list: rec.list, rev1: rev1, rev2: rev2 } }, saving_lock); } }); $dialog.data('rec', rec); // fetch changelog data saving_lock = rcmail.set_busy(true, 'loading', saving_lock); rcmail.http_post('task', { action: 'changelog', t: { id: rec.id, list: rec.list } }, saving_lock); } /** * */ function task_render_changelog(data) { var $dialog = $('#taskhistory'), rec = $dialog.data('rec'); if (data === false || !data.length || !rec) { // display 'unavailable' message $('
      ' + rcmail.gettext('objectchangelognotavailable','tasklist') + '
      ') .insertBefore($dialog.find('.changelog-table').hide()); return; } data.module = 'tasklist'; libkolab_audittrail.render_changelog(data, rec, me.tasklists[rec.list]); // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 600); } /** * */ function task_show_diff(data) { var rec = me.selected_task, $dialog = $("#taskdiff"); $dialog.find('div.form-section, h2.task-title-new').hide().data('set', false).find('.index').html(''); $dialog.find('div.form-section.clone').remove(); // always show event title and date $('.task-title', $dialog).text(rec.title).removeClass('task-text-old').show(); // show each property change $.each(data.changes, function(i, change) { var prop = change.property, r2, html = false, row = $('div.task-' + prop, $dialog).first(); // special case: title if (prop == 'title') { $('.task-title', $dialog).addClass('task-text-old').text(change['old'] || '--'); $('.task-title-new', $dialog).text(change['new'] || '--').show(); } // no display container for this property if (!row.length) { return true; } // clone row if already exists if (row.data('set')) { r2 = row.clone().addClass('clone').insertAfter(row); row = r2; } // render description text if (prop == 'description') { if (!change.diff_ && change['old']) change.old_ = text2html(change['old']); if (!change.diff_ && change['new']) change.new_ = text2html(change['new']); html = true; } // format attendees struct else if (prop == 'attendees') { if (change['old']) change.old_ = rcube_libcalendaring.attendee_html(change['old']); if (change['new']) change.new_ = rcube_libcalendaring.attendee_html($.extend({}, change['old'] || {}, change['new'])); html = true; } // localize status else if (prop == 'status') { if (change['old']) change.old_ = rcmail.gettext('status-'+String(change['old']).toLowerCase(), 'tasklist'); if (change['new']) change.new_ = rcmail.gettext('status-'+String(change['new']).toLowerCase(), 'tasklist'); } // format attachments struct if (prop == 'attachments') { if (change['old']) task_show_attachments([change['old']], row.children('.task-text-old'), rec, false); else row.children('.task-text-old').text('--'); if (change['new']) task_show_attachments([$.extend({}, change['old'] || {}, change['new'])], row.children('.task-text-new'), rec, false); else row.children('.task-text-new').text('--'); // remove click handler in diff view $('.attachmentslist li a', row).unbind('click').removeAttr('href'); } else if (change.diff_) { row.children('.task-text-diff').html(change.diff_); row.children('.task-text-old, .task-text-new').hide(); } else { if (!html) { // escape HTML characters change.old_ = Q(change.old_ || change['old'] || '--') change.new_ = Q(change.new_ || change['new'] || '--') } row.children('.task-text-old').html(change.old_ || change['old'] || '--').show(); row.children('.task-text-new').html(change.new_ || change['new'] || '--').show(); } // display index number if (typeof change.index != 'undefined') { row.find('.index').html('(' + change.index + ')'); } row.show().data('set', true); }); // open jquery UI dialog $dialog.dialog({ modal: false, resizable: true, closeOnEscape: true, title: rcmail.gettext('objectdiff','tasklist').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + rec.title, open: function() { $dialog.attr('aria-hidden', 'false'); }, close: function() { $dialog.dialog('destroy').attr('aria-hidden', 'true').hide(); }, buttons: [ { text: rcmail.gettext('close'), click: function() { $dialog.dialog('close'); }, autofocus: true } ], minWidth: 320, width: 450 }).show(); // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 400); } // close the event history dialog function close_history_dialog() { $('#taskhistory, #taskdiff').each(function(i, elem) { var $dialog = $(elem); if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); }); }; /** * Opens the dialog to edit a task */ function task_edit_dialog(id, action, presets) { $('#taskshow:ui-dialog').dialog('close'); var selected_list, rec = listdata[id] || presets, $dialog = $('
      '), editform = $('#taskedit'), list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : (me.selected_list ? me.tasklists[me.selected_list] : { editable: action == 'new', rights: action == 'new' ? 'rwitd' : 'r' }); if (rcmail.busy || !has_permission(list, 'i') || (action == 'edit' && (!rec || rec.readonly))) return false; me.selected_task = $.extend({ valarms:[] }, rec); // clone task object rec = me.selected_task; // assign temporary id if (!me.selected_task.id) me.selected_task.id = -(++idcount); // reset dialog first $('#taskeditform').get(0).reset(); + // allow other plugins to do actions when task form is opened + rcmail.triggerEvent('tasklist-task-init', {o: rec}); + // fill form data var title = $('#taskedit-title').val(rec.title || ''); var description = $('#taskedit-description').val(rec.description || ''); var recdate = $('#taskedit-date').val(rec.date || ''); var rectime = $('#taskedit-time').val(rec.time || ''); var recstartdate = $('#taskedit-startdate').val(rec.startdate || ''); var recstarttime = $('#taskedit-starttime').val(rec.starttime || ''); var complete = $('#taskedit-completeness').val((rec.complete || 0) * 100); completeness_slider.slider('value', complete.val()); var taskstatus = $('#taskedit-status').val(rec.status || ''); var tasklist = $('#taskedit-tasklist').prop('disabled', rec.parent_id ? true : false); var notify = $('#edit-attendees-donotify').get(0); var invite = $('#edit-attendees-invite').get(0); var comment = $('#edit-attendees-comment'); invite.checked = settings.itip_notify & 1 > 0; notify.checked = has_attendees(rec) && invite.checked; // set tasklist selection according to permissions tasklist.find('option').each(function(i, opt) { var l = me.tasklists[opt.value] || {}, writable = l.editable || (action == 'new' && has_permission(l, 'i')); $(opt).prop('disabled', !writable); if (!selected_list && writable) selected_list = opt.value; }); tasklist.val(rec.list || me.selected_list || selected_list); // tag-edit line var tagline = $(rcmail.gui_objects.edittagline).empty(); $.each(typeof rec.tags == 'object' && rec.tags.length ? rec.tags : [''], function(i,val){ $('') .attr('name', 'tags[]') .attr('tabindex', '0') .addClass('tag') .val(val) .appendTo(tagline); }); $('input.tag', rcmail.gui_objects.edittagline).tagedit({ animSpeed: 100, allowEdit: false, checkNewEntriesCaseSensitive: false, autocompleteOptions: { source: tags, minLength: 0, noCheck: true, appendTo:'#taskedit' }, texts: { removeLinkTitle: rcmail.gettext('removetag', 'tasklist') } }); // set alarm(s) me.set_alarms_edit('#taskedit-alarms', action != 'new' && rec.valarms ? rec.valarms : []); if ($.isArray(rec.links) && rec.links.length) { render_message_links(rec.links, $('#taskedit-links .task-text'), true, 'tasklist'); $('#taskedit-links').show(); } else { $('#taskedit-links').hide(); } // set recurrence me.set_recurrence_edit(rec); // init attendees tab var organizer = !rec.attendees || is_organizer(rec), allow_invitations = organizer || (rec.owner && rec.owner == 'anonymous') || settings.invite_shared; task_attendees = []; attendees_list = $('#edit-attendees-table > tbody').html(''); $('#edit-attendees-notify')[(allow_invitations && has_attendees(rec) && (settings.itip_notify & 2) ? 'show' : 'hide')](); $('#edit-localchanges-warning')[(has_attendees(rec) && !(allow_invitations || (rec.owner && is_organizer(rec, rec.owner))) ? 'show' : 'hide')](); // attendees (aka assignees) if (list.attendees) { var j, data, reply_selected = 0; if (rec.attendees) { for (j=0; j < rec.attendees.length; j++) { data = rec.attendees[j]; add_attendee(data, !allow_invitations); if (allow_invitations && !data.noreply) { reply_selected++; } } } // make sure comment box is visible if at least one attendee has reply enabled // or global "send invitations" checkbox is checked $('#taskeditform .attendees-commentbox')[(reply_selected || invite.checked ? 'show' : 'hide')](); // select the correct organizer identity var identity_id = 0; $.each(settings.identities, function(i,v) { if (!rec.organizer || v == rec.organizer.email) { identity_id = i; return false; } }); $('#edit-tab-attendees').show(); $('#edit-attendees-form')[(allow_invitations?'show':'hide')](); $('#edit-identities-list').val(identity_id); $('#taskedit-organizer')[(organizer ? 'show' : 'hide')](); } else { $('#edit-tab-attendees').hide(); } // attachments rcmail.enable_command('remove-attachment', list.editable); me.selected_task.deleted_attachments = []; // we're sharing some code for uploads handling with app.js rcmail.env.attachments = []; rcmail.env.compose_id = me.selected_task.id; // for rcmail.async_upload_form() if ($.isArray(rec.attachments)) { task_show_attachments(rec.attachments, $('#taskedit-attachments'), rec, true); } else { $('#taskedit-attachments > ul').empty(); } // show/hide tabs according to calendar's feature support $('#taskedit-tab-attachments')[(list.attachments||rec.attachments?'show':'hide')](); // activate the first tab $('#taskedit').tabs('option', 'active', 0); // define dialog buttons var buttons = []; buttons.push({ text: rcmail.gettext('save', 'tasklist'), 'class': 'mainaction', click: function() { var data = me.selected_task; data._status_before = me.selected_task.status + ''; // copy form field contents into task object to save $.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus }, function(key,input){ data[key] = input.val(); }); data.list = tasklist.find('option:selected').val(); data.tags = []; data.attachments = []; data.attendees = task_attendees; data.valarms = me.serialize_alarms('#taskedit-alarms'); data.recurrence = me.serialize_recurrence(rectime.val()); // do some basic input validation if (!data.title || !data.title.length) { title.focus(); return false; } else if (data.startdate && data.date) { var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, data.startdate, datepicker_settings); var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, data.date, datepicker_settings); if (startdate > duedate) { alert(rcmail.gettext('invalidstartduedates', 'tasklist')); return false; } else if ((data.time == '') != (data.starttime == '')) { alert(rcmail.gettext('invalidstartduetimes', 'tasklist')); return false; } } else if (data.recurrence && !data.startdate && !data.date) { alert(rcmail.gettext('recurrencerequiresdate', 'tasklist')); return false; } // collect tags $('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem) { if (elem.value) data.tags.push(elem.value); }); // including the "pending" one in the text box var newtag = $('#tagedit-input').val(); if (newtag != '') { data.tags.push(newtag); } // uploaded attachments list for (var i in rcmail.env.attachments) { if (i.match(/^rcmfile(.+)/)) data.attachments.push(RegExp.$1); } // task assigned to a new list if (data.list && listdata[id] && data.list != listdata[id].list) { data._fromlist = list.id; } data.complete = complete.val() / 100; if (isNaN(data.complete)) data.complete = null; else if (data.complete == 1.0 && rec.status === '') data.status = 'COMPLETED'; if (!data.list && list.id) data.list = list.id; if (!data.tags.length) data.tags = ''; if (organizer) { data._identity = $('#edit-identities-list option:selected').val(); delete data.organizer; } // per-attendee notification suppression var need_invitation = false; if (allow_invitations) { $.each(data.attendees, function (i, v) { if (v.role != 'ORGANIZER') { if ($('input.edit-attendee-reply[value="' + v.email + '"]').prop('checked')) { need_invitation = true; delete data.attendees[i]['noreply']; } else if (settings.itip_notify > 0) { data.attendees[i].noreply = 1; } } }); } // tell server to send notifications if ((has_attendees(data) || (rec.id && has_attendees(rec))) && allow_invitations && (notify.checked || invite.checked || need_invitation)) { data._notify = settings.itip_notify; data._comment = comment.val(); } else if (data._notify) { delete data._notify; } if (save_task(data, action)) $dialog.dialog('close'); } // end click: }); if (action != 'new') { buttons.push({ text: rcmail.gettext('delete', 'tasklist'), 'class': 'delete', click: function() { if (delete_task(rec.id)) $dialog.dialog('close'); } }); } buttons.push({ text: rcmail.gettext('cancel', 'tasklist'), click: function() { $dialog.dialog('close'); } }); // open jquery UI dialog $dialog.dialog({ modal: true, resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons closeOnEscape: false, title: rcmail.gettext((action == 'edit' ? 'edittask' : 'newtask'), 'tasklist'), close: function() { rcmail.ksearch_blur(); editform.hide().appendTo(document.body); $dialog.dialog('destroy').remove(); }, buttons: buttons, minHeight: 460, minWidth: 500, width: 580 }).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE title.select(); // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 580); } /** * Open a task attachment either in a browser window for inline view or download it */ function load_attachment(rec, att) { // can't open temp attachments if (!rec.id || rec.id < 0) return false; var query = { _id: att.id, _t: rec.recurrence_id||rec.id, _list:rec.list, _frame: 1 }; if (rec.rev) query._rev = rec.rev; // open attachment in frame if it's of a supported mimetype // similar as in app.js and calendar_ui.js if (att.id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) { if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) { return; } } query._frame = null; query._download = 1; rcmail.goto_url('get-attachment', query, false); }; /** * Build task attachments list */ function task_show_attachments(list, container, rec, edit) { var i, id, len, content, li, elem, ul = $('