diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php index a6fca9ce..ece0e484 100644 --- a/plugins/libcalendaring/lib/libcalendaring_itip.php +++ b/plugins/libcalendaring/lib/libcalendaring_itip.php @@ -213,6 +213,14 @@ class libcalendaring_itip $event['attendees'] = $reply_attendees; } } + // set RSVP=TRUE for every attendee if not set + else if ($method == 'REQUEST') { + foreach ($event['attendees'] as $i => $attendee) { + if (!isset($attendee['rsvp'])) { + $event['attendees'][$i]['rsvp']= true; + } + } + } // compose multipart message using PEAR:Mail_Mime $message = new Mail_mime("\r\n"); @@ -532,15 +540,21 @@ class libcalendaring_itip } /** - * Render event details in a table + * Render event/task details in a table */ function itip_object_details_table($event, $title) { $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails')); $table->add('ititle', $title); $table->add('title', Q($event['title'])); - $table->add('label', $this->plugin->gettext('date'), $this->domain); - $table->add('date', Q($this->lib->event_date_text($event))); + if ($event['start'] && $event['end']) { + $table->add('label', $this->plugin->gettext('date'), $this->domain); + $table->add('date', Q($this->lib->event_date_text($event))); + } + else if ($event['due'] && $event['_type'] == 'task') { + $table->add('label', $this->plugin->gettext('date'), $this->domain); + $table->add('date', Q($this->lib->event_date_text($event))); + } if ($event['location']) { $table->add('label', $this->plugin->gettext('location'), $this->domain); $table->add('location', Q($event['location'])); diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php index 5a1a8b0d..36fc287a 100644 --- a/plugins/libcalendaring/libcalendaring.php +++ b/plugins/libcalendaring/libcalendaring.php @@ -247,11 +247,25 @@ class libcalendaring extends rcube_plugin */ public function event_date_text($event, $tzinfo = false) { - $fromto = ''; + $fromto = '--'; + + // handle task objects + if ($event['_type'] == 'task' && is_object($event['due'])) { + $date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null; + $fromto = $this->rc->format_date($event['due'], $date_format, false); + + // add timezone information + if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) { + $fromto .= ' (' . strtr($tzname, '_', ' ') . ')'; + } + + return $fromto; + } // abort if no valid event dates are given - if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) + if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) { return $fromto; + } $duration = $event['start']->diff($event['end'])->format('s'); diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php index 855e0749..a89cec2b 100644 --- a/plugins/libcalendaring/libvcalendar.php +++ b/plugins/libcalendaring/libvcalendar.php @@ -567,7 +567,7 @@ class libvcalendar implements Iterator } // make organizer part of the attendees list for compatibility reasons - if (!empty($event['organizer']) && is_array($event['attendees'])) { + if (!empty($event['organizer']) && is_array($event['attendees']) && $event['_type'] == 'event') { array_unshift($event['attendees'], $event['organizer']); } diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index 624bdd56..68840838 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -774,6 +774,8 @@ class tasklist_kolab_driver extends tasklist_driver 'parent_id' => $record['parent_id'], 'recurrence' => $record['recurrence'], 'attendees' => $record['attendees'], + 'organizer' => $record['organizer'], + 'sequence' => $record['sequence'], ); // convert from DateTime to internal date format @@ -817,8 +819,8 @@ class tasklist_kolab_driver extends tasklist_driver } /** - * 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()) + * 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()) { @@ -826,14 +828,14 @@ class tasklist_kolab_driver extends tasklist_driver $object['categories'] = (array)$task['tags']; if (!empty($task['date'])) { - $object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone); + $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'] = new DateTime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone); + $object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone); if (empty($task['starttime'])) $object['start']->_dateonly = true; unset($object['startdate']); @@ -900,12 +902,6 @@ class tasklist_kolab_driver extends tasklist_driver unset($object['attachments']); } - // set current user as ORGANIZER - $identity = $this->rc->user->get_identity(); - if (empty($object['attendees']) && $identity['email']) { - $object['attendees'] = array(array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'])); - } - $object['_owner'] = $identity['email']; unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']); diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc index f43fbb93..b37faa88 100644 --- a/plugins/tasklist/localization/en_US.inc +++ b/plugins/tasklist/localization/en_US.inc @@ -33,6 +33,7 @@ $labels['status-needs-action'] = 'Needs action'; $labels['status-in-process'] = 'In process'; $labels['status-completed'] = 'Completed'; $labels['status-cancelled'] = 'Cancelled'; +$labels['assignedto'] = 'Assigned to'; $labels['all'] = 'All'; $labels['flagged'] = 'Flagged'; @@ -98,35 +99,35 @@ $labels['arialabeltaskselector'] = 'List mode'; $labels['arialabeltasklisting'] = 'Tasks listing'; // attendees -$labels['attendee'] = 'Participant'; +$labels['attendee'] = 'Assignee'; $labels['role'] = 'Role'; $labels['availability'] = 'Avail.'; $labels['confirmstate'] = 'Status'; -$labels['addattendee'] = 'Add participant'; +$labels['addattendee'] = 'Add assignee'; $labels['roleorganizer'] = 'Organizer'; $labels['rolerequired'] = 'Required'; $labels['roleoptional'] = 'Optional'; $labels['rolechair'] = 'Chair'; -$labels['rolenonparticipant'] = 'Absent'; +$labels['rolenonparticipant'] = 'Observer'; $labels['sendinvitations'] = 'Send invitations'; -$labels['sendnotifications'] = 'Notify participants about modifications'; -$labels['sendcancellation'] = 'Notify participants about task cancellation'; -$labels['invitationsubject'] = 'You\'ve been invited to "$title"'; -$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the task details which you can import to your tasks application."; -$labels['invitationattendlinks'] = "In case your email client doesn't support iTip requests you can use the following link to either accept or decline this invitation:\n\$url"; -$labels['eventupdatesubject'] = '"$title" has been updated'; -$labels['eventupdatesubjectempty'] = 'A task that concerns you has been updated'; -$labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated task details which you can import to your tasks application."; -$labels['eventcancelsubject'] = '"$title" has been canceled'; -$labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details."; +$labels['sendnotifications'] = 'Notify assignees about modifications'; +$labels['sendcancellation'] = 'Notify assignees about task cancellation'; +$labels['invitationsubject'] = 'You\'ve been assigned to "$title"'; +$labels['invitationmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nPlease find attached an iCalendar file with all the task details which you can import to your tasks application."; +$labels['itipupdatesubject'] = '"$title" has been updated'; +$labels['itipupdatesubjectempty'] = 'A task that concerns you has been updated'; +$labels['itipupdatemailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nPlease find attached an iCalendar file with the updated task details which you can import to your tasks application."; +$labels['itipcancelsubject'] = '"$title" has been canceled'; +$labels['itipcancelmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details."; +$labels['saveintasklist'] = 'save in '; // invitation handling (overrides labels from libcalendaring) $labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.'; -$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees"; -$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees"; -$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees"; -$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nWhen: \$date"; +$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees"; +$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees"; +$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees"; +$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nDue: \$date"; $labels['itipdeclineevent'] = 'Do you want to decline your assignment to this task?'; $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?'; diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css index bcdf29aa..1d891ed8 100644 --- a/plugins/tasklist/skins/larry/tasklist.css +++ b/plugins/tasklist/skins/larry/tasklist.css @@ -855,6 +855,14 @@ a.morelink:hover { margin-top: 0.5em; } +.edit-attendees-table tbody td { + padding: 4px 7px; +} + +.edit-attendees-table tbody tr:last-child td { + border-bottom: 0; +} + .edit-attendees-table th.role, .edit-attendees-table td.role { width: 9em; @@ -864,18 +872,19 @@ a.morelink:hover { .edit-attendees-table td.availability, .edit-attendees-table th.confirmstate, .edit-attendees-table td.confirmstate { - width: 4em; + width: 6em; } .edit-attendees-table th.options, .edit-attendees-table td.options { - width: 16px; + width: 24px; padding: 2px 4px; + text-align: right; } .edit-attendees-table th.sendmail, .edit-attendees-table td.sendmail { - width: 44px; + width: 48px; padding: 2px; } @@ -955,7 +964,7 @@ a.morelink:hover { div.form-section { position: relative; margin-top: 0.2em; - margin-bottom: 0.8em; + margin-bottom: 0.5em; } .form-section label { @@ -970,6 +979,10 @@ label.block { margin-bottom: 0.3em; } +#task-description { + margin-bottom: 1em; +} + #taskedit-completeness-slider { display: inline-block; margin-left: 2em; @@ -1047,7 +1060,7 @@ label.block { } .task-attendees span.organizer { - background-position: right -80px; + background-position: right 100px; } #all-task-attendees span.attendee { diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html index 1881c762..727d31fc 100644 --- a/plugins/tasklist/skins/larry/templates/mainview.html +++ b/plugins/tasklist/skins/larry/templates/mainview.html @@ -156,12 +156,16 @@ -
-
-
+
+ + +
+
+ +
+
+ + +

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 03aed934..1b107737 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -84,7 +84,6 @@ function rcube_tasklist_ui(settings) var focused_subclass; var task_attendees = []; var attendees_list; -// var resources_list; var me = this; // general datepicker settings @@ -1349,7 +1348,7 @@ function rcube_tasklist_ui(settings) }; // check if the current user is an attendee of this task - var is_attendee = function(task, role, email) + var is_attendee = function(task, email, role) { var i, attendee, emails = email ? ';' + email.toLowerCase() : settings.identity.emails; @@ -1366,7 +1365,10 @@ function rcube_tasklist_ui(settings) // check if the current user is the organizer var is_organizer = function(task, email) { - return is_attendee(task, 'ORGANIZER', email) || !task.id; + 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 @@ -1421,34 +1423,10 @@ function rcube_tasklist_ui(settings) if (exists) return false; -// var list = me.selected_task && me.tasklists[me.selected_task.list] ? me.tasklists[me.selected_task.list] : me.tasklists[me.selected_list]; - var dispname = Q(data.name || data.email); if (data.email) dispname = '' + dispname + ''; - // role selection - var opts = {}, organizer = data.role == 'ORGANIZER'; - if (organizer) - opts.ORGANIZER = rcmail.gettext('tasklist.roleorganizer'); - opts['REQ-PARTICIPANT'] = rcmail.gettext('tasklist.rolerequired'); - opts['OPT-PARTICIPANT'] = rcmail.gettext('tasklist.roleoptional'); - opts['NON-PARTICIPANT'] = rcmail.gettext('tasklist.rolenonparticipant'); - - if (data.cutype != 'RESOURCE') - opts['CHAIR'] = rcmail.gettext('tasklist.rolechair'); - - if (organizer && !readonly) - dispname = rcmail.env['identities-selector']; - - var select = ''; - - // availability - var avail = data.email ? 'loading' : 'unknown'; - // delete icon var icon = rcmail.env.deleteicon ? '' : rcmail.gettext('delete'); var dellink = '' + icon + ''; @@ -1463,18 +1441,15 @@ function rcube_tasklist_ui(settings) else if (data['delegated-from']) tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from']; - var html = '' + select + '' + - '' + dispname + '' + -// '' + + var html = '' + dispname + '' + '' + Q(data.status || '') + '' + - (data.cutype != 'RESOURCE' ? '' + (organizer || readonly || !invbox ? '' : invbox) + '' : '') + - '' + (organizer || readonly ? '' : dellink) + ''; + (data.cutype != 'RESOURCE' ? '' + (readonly || !invbox ? '' : invbox) + '' : '') + + '' + (readonly ? '' : dellink) + ''; - var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list; var tr = $('') .addClass(String(data.role).toLowerCase()) .html(html) - .appendTo(table); + .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); @@ -1483,15 +1458,6 @@ function rcube_tasklist_ui(settings) $('p.attendees-commentbox')[enabled ? 'show' : 'hide'](); }); - // select organizer identity - if (data.identity_id) - $('#edit-identities-list').val(data.identity_id); - - // check free-busy status -// if (avail == 'loading') { -// check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_task); -// } - task_attendees.push(data); return true; }; @@ -1499,15 +1465,8 @@ function rcube_tasklist_ui(settings) // event handler for clicks on an attendee link var task_attendee_click = function(e) { - var cutype = $(this).attr('data-cutype'), - mailto = this.href.substr(7); - - if (rcmail.env.tasklist_resources && cutype == 'RESOURCE') { - task_resources_dialog(mailto); - } - else { - rcmail.redirect(rcmail.url('mail/compose', {_to: mailto})); - } + var mailto = this.href.substr(7); + rcmail.command('compose', mailto); return false; }; @@ -1547,7 +1506,7 @@ function rcube_tasklist_ui(settings) $('#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').hide(); + $('#task-attendees, #task-organizer').hide(); var itags = get_inherited_tags(rec); var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty(); @@ -1587,6 +1546,7 @@ function rcube_tasklist_ui(settings) // list task attendees if (list.attendees && rec.attendees) { + console.log(rec.attendees) /* // sort resources to the end rec.attendees.sort(function(a,b) { @@ -1595,30 +1555,19 @@ function rcube_tasklist_ui(settings) return (j - k); }); */ - var j, data, dispname, tooltip, organizer = false, rsvp = false, mystatus = null, line, morelink, html = '', overflow = ''; + 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]; - dispname = Q(data.name || data.email); - tooltip = ''; - if (data.email) { - tooltip = data.email; - dispname = '' + dispname + ''; - if (data.role == 'ORGANIZER') - organizer = true; - else if (settings.identity.emails.indexOf(';'+data.email) >= 0) { - mystatus = data.status.toLowerCase(); - if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) - rsvp = mystatus; - } + 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; } - 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']; - - line = '' + dispname + ' '; + line = task_attendee_html(data); if (morelink) overflow += line; @@ -1631,7 +1580,7 @@ function rcube_tasklist_ui(settings) } } - if (html && (rec.attendees.length > 1 || !organizer)) { + if (html) { $('#task-attendees').show() .children('.task-text') .html(html) @@ -1667,6 +1616,10 @@ function rcube_tasklist_ui(settings) $('#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(task_attendee_html(rec.organizer)); + } } // define dialog buttons @@ -1697,7 +1650,7 @@ function rcube_tasklist_ui(settings) closeOnEscape: true, title: rcmail.gettext('taskdetails', 'tasklist'), open: function() { - $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); + $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); }, close: function() { $dialog.dialog('destroy').appendTo(document.body); @@ -1711,6 +1664,24 @@ function rcube_tasklist_ui(settings) me.dialog_resize($dialog.get(0), $dialog.height(), 580); } + // render HTML code for displaying an attendee record + function task_attendee_html(data) + { + var dispname = Q(data.name || data.email), tooltip = ''; + + if (data.email) { + tooltip = data.email; + dispname = '' + dispname + ''; + } + + 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']; + + return '' + dispname + ' '; + } + /** * Opens the dialog to edit a task */ @@ -1786,18 +1757,17 @@ function rcube_tasklist_ui(settings) task_attendees = []; attendees_list = $('#edit-attendees-table > tbody').html(''); - //resources_list = $('#edit-resources-table > tbody').html(''); $('#edit-attendees-notify')[(notify.checked && allow_invitations ? 'show' : 'hide')](); $('#edit-localchanges-warning')[(has_attendees(rec) && !(allow_invitations || (rec.owner && is_organizer(rec, rec.owner))) ? 'show' : 'hide')](); - var load_attendees_tab = function() - { + // 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.role != 'ORGANIZER' && !data.noreply) { + if (allow_invitations && !data.noreply) { reply_selected++; } } @@ -1812,16 +1782,16 @@ function rcube_tasklist_ui(settings) // select the correct organizer identity var identity_id = 0; $.each(settings.identities, function(i,v) { - if (organizer && v == organizer.email) { + if (rec.organizer && v == rec.organizer.email) { identity_id = i; return false; } }); - $('#edit-identities-list').val(identity_id); $('#edit-attendees-form')[(allow_invitations?'show':'hide')](); -// $('#edit-attendee-schedule')[(tasklist.freebusy?'show':'hide')](); - }; + $('#edit-identities-list').val(identity_id); + $('#taskedit-organizer')[(organizer ? 'show' : 'hide')](); + } // attachments rcmail.enable_command('remove-attachment', list.editable); @@ -1904,15 +1874,9 @@ function rcube_tasklist_ui(settings) if (!data.tags.length) data.tags = ''; - // read attendee roles - $('select.edit-attendee-role').each(function(i, elem) { - if (data.attendees[i]) { - data.attendees[i].role = $(elem).val(); - } - }); - if (organizer) { data._identity = $('#edit-identities-list option:selected').val(); + delete data.organizer; } // don't submit attendees if only myself is added as organizer @@ -1977,9 +1941,6 @@ function rcube_tasklist_ui(settings) // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 580); - - if (list.attendees) - window.setTimeout(load_attendees_tab, 1); } /** diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index 76ef2514..0757c637 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -334,7 +334,7 @@ class tasklist extends rcube_plugin $task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec); // only notify if data really changed (TODO: do diff check on client already) - if (!$oldrec || $action == 'delete' || self::task_diff($event, $old)) { + if (!$oldrec || $action == 'delete' || self::task_diff($task, $oldrec)) { $sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']); if ($sent > 0) $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation'); @@ -366,10 +366,7 @@ class tasklist extends rcube_plugin if (!$this->itip) { require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php'); $this->itip = new libcalendaring_itip($this, 'tasklist'); - -// if ($this->rc->config->get('kolab_invitation_tasklists')) { -// $this->itip->set_rsvp_actions(array('accepted','tentative','declined','needs-action')); -// } + $this->itip->set_rsvp_actions(array('accepted','declined')); } return $this->itip; @@ -520,6 +517,11 @@ class tasklist extends rcube_plugin $rec['attachments'] = $attachments; + // set organizer from identity selector + if (isset($rec['_identity']) && ($identity = $this->rc->user->get_identity($rec['_identity']))) { + $rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']); + } + if (is_numeric($rec['id']) && $rec['id'] < 0) unset($rec['id']); @@ -646,7 +648,8 @@ class tasklist extends rcube_plugin // compose multipart message using PEAR:Mail_Mime $method = $action == 'delete' ? 'CANCEL' : 'REQUEST'; - $message = $itip->compose_itip_message($task, $method); + $object = $this->to_libcal($task); + $message = $itip->compose_itip_message($object, $method); // list existing attendees from the $old task $old_attendees = array(); @@ -671,11 +674,11 @@ class tasklist extends rcube_plugin // which template to use for mail text $is_new = !in_array($attendee['email'], $old_attendees); - $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody'); - $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'eventupdatesubject' : 'eventupdatesubjectempty')); + $bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody'); + $subject = $is_cancelled ? 'itipcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty')); // finally send the message - if ($itip->send_itip_message($task, $method, $attendee, $subject, $bodytext, $message)) + if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message)) $sent++; else $sent = -100; @@ -683,16 +686,16 @@ class tasklist extends rcube_plugin // send CANCEL message to removed attendees foreach ((array)$old['attendees'] as $attendee) { - if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current)) { + if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) { continue; } - $vevent = $old; - $vevent['cancelled'] = $is_cancelled; - $vevent['attendees'] = array($attendee); - $vevent['comment'] = $comment; + $vtodo = $this->to_libcal($old); + $vtodo['cancelled'] = $is_cancelled; + $vtodo['attendees'] = array($attendee); + $vtodo['comment'] = $comment; - if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody')) + if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody')) $sent++; else $sent = -100; @@ -1393,6 +1396,8 @@ class tasklist extends rcube_plugin // successfully parsed events? if (!empty($tasks) && ($task = $tasks[$index])) { + $task = $this->from_ical($task); + // store the message's sender address for comparisons $task['_sender'] = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : ''; $askt['_sender_utf'] = rcube_idn_to_utf8($task['_sender']); @@ -1496,6 +1501,7 @@ class tasklist extends rcube_plugin foreach ($tasks as $task) { // save to tasklist if ($list && $list['editable'] && $task['_type'] == 'task') { + $task = $this->from_ical($task); $task['list'] = $list['id']; if (!$this->driver->get_task($task['uid'])) { @@ -1555,14 +1561,11 @@ class tasklist extends rcube_plugin // update my attendee status according to submitted method if (!empty($status)) { - $organizer = null; + $organizer = $task['organizer']; $emails = $this->lib->get_user_emails(); foreach ($task['attendees'] as $i => $attendee) { - if ($attendee['role'] == 'ORGANIZER') { - $organizer = $attendee; - } - else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT'; $reply_sender = $attendee['email']; @@ -1714,7 +1717,7 @@ class tasklist extends rcube_plugin $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); - if ($itip->send_itip_message($task, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) + if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); @@ -1729,7 +1732,7 @@ class tasklist extends rcube_plugin /** * Handler for calendar/itip-status requests */ - function task_itip_status() + public function task_itip_status() { $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); @@ -1768,7 +1771,7 @@ class tasklist extends rcube_plugin /** * Handler for calendar/itip-remove requests */ - function task_itip_remove() + public function task_itip_remove() { $success = false; $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST); @@ -1797,6 +1800,78 @@ class tasklist extends rcube_plugin return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); } + /** + * Map task properties for ical exprort using libcalendaring + */ + public function to_libcal($task) + { + $object = $task; + $object['categories'] = (array)$task['tags']; + + // convert to datetime objects + if (!empty($task['date'])) { + $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->timezone); + if (empty($task['time'])) + $object['due']->_dateonly = true; + unset($object['date']); + } + + if (!empty($task['startdate'])) { + $object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->timezone); + if (empty($task['starttime'])) + $object['start']->_dateonly = true; + unset($object['startdate']); + } + + $object['complete'] = $task['complete'] * 100; + if ($task['complete'] == 1.0 && empty($task['complete'])) { + $object['status'] = 'COMPLETED'; + } + + if ($task['flagged']) { + $object['priority'] = 1; + } + else if (!$task['priority']) { + $object['priority'] = 0; + } + + return $object; + } + + /** + * Convert task properties from ical parser to the internal format + */ + public function from_ical($vtodo) + { + $task = $vtodo; + + $task['tags'] = array_filter((array)$vtodo['categories']); + $task['flagged'] = $vtodo['priority'] == 1; + $task['complete'] = floatval($vtodo['complete'] / 100); + + // convert from DateTime to internal date format + if (is_a($vtodo['due'], 'DateTime')) { + $due = $this->lib->adjust_timezone($vtodo['due']); + $task['date'] = $due->format('Y-m-d'); + if (!$vtodo['due']->_dateonly) + $task['time'] = $due->format('H:i'); + } + // convert from DateTime to internal date format + if (is_a($vtodo['start'], 'DateTime')) { + $start = $this->lib->adjust_timezone($vtodo['start']); + $task['startdate'] = $start->format('Y-m-d'); + if (!$vtodo['start']->_dateonly) + $task['starttime'] = $start->format('H:i'); + } + if (is_a($vtodo['dtstamp'], 'DateTime')) { + $task['changed'] = $vtodo['dtstamp']; + } + + unset($task['categories'], $task['due'], $task['start'], $task['dtstamp']); + + return $task; + } + /** * Handler for user_delete plugin hook */ diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index 5fc0a207..21b322ec 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -56,7 +56,6 @@ class tasklist_ui // copy config to client $this->rc->output->set_env('tasklist_settings', $this->load_settings()); - $this->rc->output->set_env('identities-selector', $this->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->plugin->gettext('roleorganizer')))); // initialize attendees autocompletion $this->rc->autocomplete_init(); @@ -71,7 +70,7 @@ class tasklist_ui { $settings = array(); - //$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']); + $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0); // get user identity to create default attendee foreach ($this->rc->user->list_identities() as $rec) { @@ -128,6 +127,7 @@ class tasklist_ui $this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area')); $this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list')); $this->plugin->register_handler('plugin.attendees_form', array($this, 'attendees_form')); + $this->plugin->register_handler('plugin.identity_select', array($this, 'identity_select')); $this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify')); $this->plugin->include_script('jquery.tagedit.js'); @@ -438,9 +438,8 @@ class tasklist_ui $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite')); $table = new html_table(array('cols' => 4 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable')); - $table->add_header('role', $this->plugin->gettext('role')); +// $table->add_header('role', $this->plugin->gettext('role')); $table->add_header('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee')); -// $table->add_header('availability', $this->plugin->gettext('availability')); $table->add_header('confirmstate', $this->plugin->gettext('confirmstate')); if ($invitations) { $table->add_header(array('class' => 'sendmail', 'title' => $this->plugin->gettext('sendinvitations')),