').attr({'class': 'attendee ' + status, title: tooltip}).append(name.text(dispname))
).html();
};
/**
*
*/
rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id)
{
// ask user to delete the declined event from the local calendar (#1670)
var del = false;
if (rcmail.env.rsvp_saved && status == 'declined') {
del = confirm(rcmail.gettext('itip.declinedeleteconfirm'));
}
// open dialog for iTip delegation
if (status == 'delegated') {
rcube_libcalendaring.itip_delegate_dialog(function(data) {
rcmail.http_post(task + '/itip-delegate', {
_uid: rcmail.env.uid,
_mbox: rcmail.env.mailbox,
_part: mime_id,
_to: data.to,
_rsvp: data.rsvp ? 1 : 0,
_comment: data.comment,
_folder: data.target
}, rcmail.set_busy(true, 'itip.savingdata'));
}, $('#rsvp-'+dom_id+' .folder-select'));
return false;
}
var noreply = 0, comment = '';
if (dom_id) {
noreply = $('#noreply-'+dom_id+':checked').length ? 1 : 0;
if (!noreply)
comment = $('#reply-comment-'+dom_id).val();
}
rcmail.http_post(task + '/mailimportitip', {
_uid: rcmail.env.uid,
_mbox: rcmail.env.mailbox,
_part: mime_id,
_folder: $('#itip-saveto').val(),
_status: status,
_del: del?1:0,
_noreply: noreply,
_comment: comment
}, rcmail.set_busy(true, 'itip.savingdata'));
return false;
};
/**
* Helper function to render the iTip delegation dialog
* and trigger a callback function when submitted.
*/
rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
{
// show dialog for entering the delegatee address and comment
var html = '';
var dialog, buttons = [];
buttons.push({
text: rcmail.gettext('itipdelegated', 'itip'),
click: function() {
var doc = window.parent.document,
delegatee = String($('#itip-delegate-to', doc).val()).replace(/(^\s+)|(\s+$)/, '');
if (delegatee != '' && rcube_check_email(delegatee, true)) {
callback({
to: delegatee,
rsvp: $('#itip-delegate-rsvp', doc).prop('checked'),
comment: $('#itip-delegate-comment', doc).val(),
target: $('#itip-saveto', doc).val()
});
setTimeout(function() { dialog.dialog("close"); }, 500);
}
else {
alert(rcmail.gettext('itip.delegateinvalidaddress'));
$('#itip-delegate-to', doc).focus();
}
}
});
buttons.push({
text: rcmail.gettext('cancel', 'itip'),
click: function() {
dialog.dialog('close');
}
});
dialog = rcmail.show_popup_dialog(html, rcmail.gettext('delegateinvitation', 'itip'), buttons, {
width: 460,
open: function(event, ui) {
$(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
$(this).find('#itip-saveto').val('');
// initialize autocompletion
var ac_props, rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
rcm.init_address_input_events($(this).find('#itip-delegate-to').focus(), ac_props);
rcm.env.recipients_delimiter = '';
},
close: function(event, ui) {
rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
rcm.ksearch_blur();
$(this).remove();
}
});
return dialog;
};
/**
* Show a menu for selecting the RSVP reply mode
*/
rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback)
{
var mnu = $('').addClass('popupmenu libcal-rsvp-replymode');
$.each(['all','current'/*,'future'*/], function(i, mode) {
$('' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '')
.addClass('ui-menu-item')
.attr('rel', mode)
.appendTo(mnu);
});
var action = btn.attr('rel');
// open the mennu
mnu.menu({
select: function(event, ui) {
callback(action, ui.item.attr('rel'));
}
})
.appendTo(document.body)
.position({ my: 'left top', at: 'left bottom+2', of: btn })
.data('action', action);
setTimeout(function() {
$(document).one('click', function() {
mnu.menu('destroy');
mnu.remove();
});
}, 100);
};
/**
*
*/
rcube_libcalendaring.remove_from_itip = function(event, task, title)
{
if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
rcmail.http_post(task + '/itip-remove',
event,
rcmail.set_busy(true, 'itip.savingdata')
);
}
};
/**
*
*/
rcube_libcalendaring.decline_attendee_reply = function(mime_id, task)
{
// show dialog for entering a comment and send to server
var html = '' + rcmail.gettext('itip.declineattendeeconfirm') + '
' +
'';
var dialog, buttons = [];
buttons.push({
text: rcmail.gettext('declineattendee', 'itip'),
click: function() {
rcmail.http_post(task + '/itip-decline-reply', {
_uid: rcmail.env.uid,
_mbox: rcmail.env.mailbox,
_part: mime_id,
_comment: $('#itip-decline-comment', window.parent.document).val()
}, rcmail.set_busy(true, 'itip.savingdata'));
dialog.dialog("close");
}
});
buttons.push({
text: rcmail.gettext('cancel', 'itip'),
click: function() {
dialog.dialog('close');
}
});
dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, {
width: 460,
open: function() {
$(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
$('#itip-decline-comment').focus();
}
});
return false;
};
/**
*
*/
rcube_libcalendaring.fetch_itip_object_status = function(p)
{
p.mbox = rcmail.env.mailbox;
p.message_uid = rcmail.env.uid;
rcmail.http_post(p.task + '/itip-status', { data: p });
};
/**
*
*/
rcube_libcalendaring.update_itip_object_status = function(p)
{
rcmail.env.rsvp_saved = p.saved;
rcmail.env.itip_existing = p.existing;
// hide all elements first
$('#itip-buttons-'+p.id+' > div').hide();
$('#rsvp-'+p.id+' .folder-select').remove();
if (p.html) {
// append/replace rsvp status display
$('#loading-'+p.id).next('.rsvp-status').remove();
$('#loading-'+p.id).hide().after(p.html);
}
// enable/disable rsvp buttons
if (p.action == 'rsvp') {
$('#rsvp-'+p.id+' input.button').prop('disabled', false)
.filter('.'+String(p.status||'unknown').toLowerCase()).prop('disabled', p.latest);
}
// show rsvp/import buttons (with calendar selector)
$('#'+p.action+'-'+p.id).show().find('input.button').last().after(p.select);
// highlight date if date change detected
if (p.rescheduled)
$('.calendar-eventdetails td.date').addClass('modified');
// show itip box appendix after replacing the given placeholders
if (p.append && p.append.selector) {
var elem = $(p.append.selector);
if (p.append.replacements) {
$.each(p.append.replacements, function(k, html) {
elem.html(elem.html().replace(k, html));
});
}
else if (p.append.html) {
elem.html(p.append.html)
}
elem.show();
}
};
/**
* Callback from server after an iTip message has been processed
*/
rcube_libcalendaring.itip_message_processed = function(metadata)
{
if (metadata.after_action) {
setTimeout(function(){ rcube_libcalendaring.itip_after_action(metadata.after_action); }, 1200);
}
else {
rcube_libcalendaring.fetch_itip_object_status(metadata);
}
};
/**
* After-action on iTip request message. Action types:
* 0 - no action
* 1 - move to Trash
* 2 - delete the message
* 3 - flag as deleted
* folder_name - move the message to the specified folder
*/
rcube_libcalendaring.itip_after_action = function(action)
{
if (!action) {
return;
}
var rc = rcmail.is_framed() ? parent.rcmail : rcmail;
if (action === 2) {
rc.permanently_remove_messages();
}
else if (action === 3) {
rc.mark_message('delete');
}
else {
rc.move_messages(action === 1 ? rc.env.trash_mailbox : action);
}
};
/**
* Open the calendar preview for the current iTip event
*/
rcube_libcalendaring.open_itip_preview = function(url, msgref)
{
if (!rcmail.env.itip_existing)
url += '&itip=' + escape(msgref);
var win = rcmail.open_window(url);
};
// extend jQuery
(function($){
$.fn.serializeJSON = function(){
var json = {};
jQuery.map($(this).serializeArray(), function(n, i) {
json[n['name']] = n['value'];
});
return json;
};
})(jQuery);
/* libcalendaring plugin initialization */
window.rcmail && rcmail.addEventListener('init', function(evt) {
if (rcmail.env.libcal_settings) {
var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings);
rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); });
}
rcmail.addEventListener('plugin.update_itip_object_status', rcube_libcalendaring.update_itip_object_status)
.addEventListener('plugin.fetch_itip_object_status', rcube_libcalendaring.fetch_itip_object_status)
.addEventListener('plugin.itip_message_processed', rcube_libcalendaring.itip_message_processed);
-
- if (rcmail.env.action == 'get-attachment' && rcmail.gui_objects.attachmentframe) {
- rcmail.gui_objects.messagepartframe = rcmail.gui_objects.attachmentframe;
- rcmail.enable_command('image-scale', 'image-rotate', !!/^image\//.test(rcmail.env.mimetype));
- rcmail.register_command('print-attachment', function() {
- var frame = rcmail.get_frame_window(rcmail.gui_objects.attachmentframe.id);
- if (frame) frame.print();
- }, true);
- }
-
- if (rcmail.env.action == 'get-attachment' && rcmail.env.attachment_download_url) {
- rcmail.register_command('download-attachment', function() {
- rcmail.location_href(rcmail.env.attachment_download_url, window);
- }, true);
- }
});
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 21aeaf1f..4ae37166 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1,1790 +1,1523 @@
*
* 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 libcalendaring extends rcube_plugin
{
public $rc;
public $timezone;
public $gmt_offset;
public $dst_active;
public $timezone_offset;
public $ical_parts = array();
public $ical_message;
public $defaults = array(
'calendar_date_format' => "yyyy-MM-dd",
'calendar_date_short' => "M-d",
'calendar_date_long' => "MMM d yyyy",
'calendar_date_agenda' => "ddd MM-dd",
'calendar_time_format' => "HH:mm",
'calendar_first_day' => 1,
'calendar_first_hour' => 6,
'calendar_date_format_sets' => array(
'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
),
);
private static $instance;
private $mail_ical_parser;
/**
* Singleton getter to allow direct access from other plugins
*/
public static function get_instance()
{
if (!self::$instance) {
self::$instance = new libcalendaring(rcube::get_instance()->plugins);
self::$instance->init_instance();
}
return self::$instance;
}
/**
* Initializes class properties
*/
public function init_instance()
{
$this->rc = rcube::get_instance();
// set user's timezone
try {
$this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
$this->timezone = new DateTimeZone('GMT');
}
$now = new DateTime('now', $this->timezone);
$this->gmt_offset = $now->getOffset();
$this->dst_active = $now->format('I');
$this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
$this->add_texts('localization/', false);
}
/**
* Required plugin startup method
*/
public function init()
{
self::$instance = $this;
$this->rc = rcube::get_instance();
$this->init_instance();
// include client scripts and styles
if ($this->rc->output) {
// add hook to display alarms
$this->add_hook('refresh', array($this, 'refresh'));
$this->register_action('plugin.alarms', array($this, 'alarms_action'));
$this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group'));
}
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
}
/**
* Startup hook
*/
public function startup($args)
{
if ($this->rc->output && $this->rc->output->type == 'html') {
$this->rc->output->set_env('libcal_settings', $this->load_settings());
$this->include_script('libcalendaring.js');
$this->include_stylesheet($this->local_skin_path() . '/libcal.css');
$this->add_label(
'itipaccepted', 'itiptentative', 'itipdeclined',
'itipdelegated', 'expandattendeegroup', 'expandattendeegroupnodata',
'statusorganizer', 'statusaccepted', 'statusdeclined',
'statusdelegated', 'statusunknown', 'statusneeds-action',
'statustentative', 'statuscompleted', 'statusin-process',
'delegatedto', 'delegatedfrom', 'showmore'
);
}
if ($args['task'] == 'mail') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
$this->add_hook('message_load', array($this, 'mail_message_load'));
}
}
}
/**
* Load iCalendar functions
*/
public static function get_ical()
{
$self = self::get_instance();
require_once __DIR__ . '/libvcalendar.php';
return new libvcalendar();
}
/**
* Load iTip functions
*/
public static function get_itip($domain = 'libcalendaring')
{
$self = self::get_instance();
require_once __DIR__ . '/lib/libcalendaring_itip.php';
return new libcalendaring_itip($self, $domain);
}
/**
* Load recurrence computation engine
*/
public static function get_recurrence()
{
$self = self::get_instance();
require_once __DIR__ . '/lib/libcalendaring_recurrence.php';
return new libcalendaring_recurrence($self);
}
/**
* Shift dates into user's current timezone
*
* @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
* @return object DateTime object in user's timezone
*/
public function adjust_timezone($dt, $dateonly = false)
{
if (is_numeric($dt))
$dt = new DateTime('@'.$dt);
else if (is_string($dt))
$dt = rcube_utils::anytodatetime($dt);
if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) {
$dt->setTimezone($this->timezone);
}
return $dt;
}
/**
*
*/
public function load_settings()
{
$this->date_format_defaults();
$settings = array();
$keys = array('date_format', 'time_format', 'date_short', 'date_long');
foreach ($keys as $key) {
$settings[$key] = (string)$this->rc->config->get('calendar_' . $key, $this->defaults['calendar_' . $key]);
$settings[$key] = str_replace('Y', 'y', $settings[$key]);
}
$settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '—' " . $settings['date_long'] . '}';
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['timezone'] = $this->timezone_offset;
$settings['dst'] = $this->dst_active;
// localization
$settings['days'] = array(
$this->rc->gettext('sunday'), $this->rc->gettext('monday'),
$this->rc->gettext('tuesday'), $this->rc->gettext('wednesday'),
$this->rc->gettext('thursday'), $this->rc->gettext('friday'),
$this->rc->gettext('saturday')
);
$settings['days_short'] = array(
$this->rc->gettext('sun'), $this->rc->gettext('mon'),
$this->rc->gettext('tue'), $this->rc->gettext('wed'),
$this->rc->gettext('thu'), $this->rc->gettext('fri'),
$this->rc->gettext('sat')
);
$settings['months'] = array(
$this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
$this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
$this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
$this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
$this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
$this->rc->gettext('longnov'), $this->rc->gettext('longdec')
);
$settings['months_short'] = array(
$this->rc->gettext('jan'), $this->rc->gettext('feb'),
$this->rc->gettext('mar'), $this->rc->gettext('apr'),
$this->rc->gettext('may'), $this->rc->gettext('jun'),
$this->rc->gettext('jul'), $this->rc->gettext('aug'),
$this->rc->gettext('sep'), $this->rc->gettext('oct'),
$this->rc->gettext('nov'), $this->rc->gettext('dec')
);
$settings['today'] = $this->rc->gettext('today');
// define list of file types which can be displayed inline
// same as in program/steps/mail/show.inc
$settings['mimetypes'] = (array)$this->rc->config->get('client_mimetypes');
return $settings;
}
/**
* Helper function to set date/time format according to config and user preferences
*/
private function date_format_defaults()
{
static $defaults = array();
// nothing to be done
if (isset($defaults['date_format']))
return;
$defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
$defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
// override defaults
if ($defaults['date_format'])
$this->defaults['calendar_date_format'] = $defaults['date_format'];
if ($defaults['time_format'])
$this->defaults['calendar_time_format'] = $defaults['time_format'];
// derive format variants from basic date format
$format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
$this->defaults['calendar_date_long'] = $format_set[0];
$this->defaults['calendar_date_short'] = $format_set[1];
$this->defaults['calendar_date_agenda'] = $format_set[2];
}
}
/**
* Compose a date string for the given event
*/
public function event_date_text($event, $tzinfo = false)
{
$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')) {
return $fromto;
}
$duration = $event['start']->diff($event['end'])->format('s');
$this->date_format_defaults();
$date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
if ($event['allday']) {
$fromto = $this->rc->format_date($event['start'], $date_format);
if (($todate = $this->rc->format_date($event['end'], $date_format)) != $fromto)
$fromto .= ' - ' . $todate;
}
else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
$fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) .
' - ' . $this->rc->format_date($event['end'], $time_format);
}
else {
$fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) .
' - ' . $this->rc->format_date($event['end'], $date_format) . ' ' . $this->rc->format_date($event['end'], $time_format);
}
// add timezone information
if ($tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
/**
* Render HTML form for alarm configuration
*/
public function alarm_select($attrib, $alarm_types, $absolute_time = true)
{
unset($attrib['name']);
$input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value form-control', 'size' => 3));
$input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date form-control', 'size' => 10));
$input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time form-control', 'size' => 6));
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type form-control', 'id' => $attrib['id']));
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset form-control'));
$select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related form-control'));
$object_type = $attrib['_type'] ?: 'event';
$select_type->add($this->gettext('none'), '');
foreach ($alarm_types as $type)
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add($this->gettext('trigger' . $trigger), $trigger);
$select_offset->add($this->gettext('trigger0'), '0');
if ($absolute_time)
$select_offset->add($this->gettext('trigger@'), '@');
$select_related->add($this->gettext('relatedstart'), 'start');
$select_related->add($this->gettext('relatedend' . $object_type), 'end');
// pre-set with default values from user settings
$preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$hidden = array('style' => 'display:none');
$html = html::span('edit-alarm-set',
$select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
$input_value->show($preset[0]) . ' ' .
$select_offset->show($preset[1]) . ' ' .
$select_related->show() . ' ' .
$input_date->show('', $hidden) . ' ' .
$input_time->show('', $hidden)
)
);
// TODO: support adding more alarms
#$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')),
# $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
return $html;
}
/**
* Get a list of email addresses of the given user (from login and identities)
*
* @param string User Email (default to current user)
* @return array Email addresses related to the user
*/
public function get_user_emails($user = null)
{
static $_emails = array();
if (empty($user)) {
$user = $this->rc->user->get_username();
}
// return cached result
if (is_array($_emails[$user])) {
return $_emails[$user];
}
$emails = array($user);
$plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
$emails = array_map('strtolower', $plugin['emails']);
// add all emails from the current user's identities
if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) {
foreach ($this->rc->user->list_emails() as $identity) {
$emails[] = strtolower($identity['email']);
}
}
$_emails[$user] = array_unique($emails);
return $_emails[$user];
}
/**
* Set the given participant status to the attendee matching the current user's identities
*
* @param array Hash array with event struct
* @param string The PARTSTAT value to set
* @return mixed Email address of the updated attendee or False if none matching found
*/
public function set_partstat(&$event, $status, $recursive = true)
{
$success = false;
$emails = $this->get_user_emails();
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
$success = $attendee['email'];
}
}
// apply partstat update to each existing exception
if ($event['recurrence'] && is_array($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
$this->set_partstat($event['recurrence']['EXCEPTIONS'][$i], $status, false);
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
return $success;
}
/********* Alarms handling *********/
/**
* Helper function to convert alarm trigger strings
* into two-field values (e.g. "-45M" => 45, "-M")
*/
public static function parse_alarm_value($val)
{
if ($val[0] == '@') {
return array(new DateTime($val));
}
else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
if ($m[1] == '')
$m[1] = '+';
foreach ($m2 as $seg) {
$prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT';
if ($seg[1] > 0) { // ignore zero values
// convert seconds to minutes
if ($seg[2] == 'S') {
$seg[2] = 'M';
$seg[1] = max(1, round($seg[1]/60));
}
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
}
// return zero value nevertheless
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
return false;
}
/**
* Convert the alarms list items to be processed on the client
*/
public static function to_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'] instanceof DateTime) {
$alarm['trigger'] = '@' . $alarm['trigger']->format('U');
}
else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[2];
}
return $alarm;
}, (array)$valarms);
}
/**
* Process the alarms values submitted by the client
*/
public static function from_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'][0] == '@') {
try {
$alarm['trigger'] = new DateTime($alarm['trigger']);
$alarm['trigger']->setTimezone(new DateTimeZone('UTC'));
}
catch (Exception $e) { /* handle this ? */ }
}
else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[3];
}
return $alarm;
}, (array)$valarms);
}
/**
* Render localized text for alarm settings
*/
public static function alarms_text($alarms)
{
if (is_array($alarms) && is_array($alarms[0])) {
$texts = array();
foreach ($alarms as $alarm) {
if ($text = self::alarm_text($alarm))
$texts[] = $text;
}
return join(', ', $texts);
}
else {
return self::alarm_text($alarms);
}
}
/**
* Render localized text for a single alarm property
*/
public static function alarm_text($alarm)
{
if (is_string($alarm)) {
list($trigger, $action) = explode(':', $alarm);
}
else {
$trigger = $alarm['trigger'];
$action = $alarm['action'];
$related = $alarm['related'];
}
$text = '';
$rcube = rcube::get_instance();
switch ($action) {
case 'EMAIL':
$text = $rcube->gettext('libcalendaring.alarmemail');
break;
case 'DISPLAY':
$text = $rcube->gettext('libcalendaring.alarmdisplay');
break;
case 'AUDIO':
$text = $rcube->gettext('libcalendaring.alarmaudio');
break;
}
if ($trigger instanceof DateTime) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($trigger))
));
}
else if (preg_match('/@(\d+)/', $trigger, $m)) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($m[1]))
));
}
else if ($val = self::parse_alarm_value($trigger)) {
$r = strtoupper($related ?: 'start') == 'END' ? 'end' : '';
// TODO: for all-day events say 'on date of event at XX' ?
if ($val[0] == 0) {
$text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r);
}
else {
$label = 'libcalendaring.trigger' . $r . $val[1];
$text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label);
}
}
else {
return false;
}
return $text;
}
/**
* Get the next alarm (time & action) for the given event
*
* @param array Record data
* @return array Hash array with alarm time/type or null if no alarms are configured
*/
public static function get_next_alarm($rec, $type = 'event')
{
if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED')
return null;
if ($type == 'task') {
$timezone = self::get_instance()->timezone;
if ($rec['startdate'])
$rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
if ($rec['date'])
$rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
}
if (!$rec['end'])
$rec['end'] = $rec['start'];
// support legacy format
if (!$rec['valarms']) {
list($trigger, $action) = explode(':', $rec['alarms'], 2);
if ($alarm = self::parse_alarm_value($trigger)) {
$rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0]));
}
}
$expires = new DateTime('now - 12 hours');
$alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility
// handle multiple alarms
$notify_at = null;
foreach ($rec['valarms'] as $alarm) {
$notify_time = null;
if ($alarm['trigger'] instanceof DateTime) {
$notify_time = $alarm['trigger'];
}
else if (is_string($alarm['trigger'])) {
$refdate = $alarm['related'] == 'END' ? $rec['end'] : $rec['start'];
// abort if no reference date is available to compute notification time
if (!is_a($refdate, 'DateTime'))
continue;
// TODO: for all-day events, take start @ 00:00 as reference date ?
try {
$interval = new DateInterval(trim($alarm['trigger'], '+-'));
$interval->invert = $alarm['trigger'][0] == '-';
$notify_time = clone $refdate;
$notify_time->add($interval);
}
catch (Exception $e) {
rcube::raise_error($e, true);
continue;
}
}
if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) {
$notify_at = $notify_time;
$action = $alarm['action'];
$alarm_prop = $alarm;
// generate a unique alarm ID if multiple alarms are set
if (count($rec['valarms']) > 1) {
$alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis');
}
}
}
return !$notify_at ? null : array(
'time' => $notify_at->format('U'),
'action' => $action ? strtoupper($action) : 'DISPLAY',
'id' => $alarm_id,
'prop' => $alarm_prop,
);
}
/**
* Handler for keep-alive requests
* This will check for pending notifications and pass them to the client
*/
public function refresh($attr)
{
// collect pending alarms from all providers (e.g. calendar, tasks)
$plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
'time' => time(),
'alarms' => array(),
));
if (!$plugin['abort'] && !empty($plugin['alarms'])) {
// make sure texts and env vars are available on client
$this->add_texts('localization/', true);
$this->rc->output->add_label('close');
$this->rc->output->set_env('snooze_select', $this->snooze_select());
$this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
}
}
/**
* Handler for alarm dismiss/snooze requests
*/
public function alarms_action()
{
// $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
$data['ids'] = explode(',', $data['id']);
$plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data);
if ($plugin['success'])
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
/**
* Generate reduced and streamlined output for pending alarms
*/
private function _alarms_output($alarms)
{
$out = array();
foreach ($alarms as $alarm) {
$out[] = array(
'id' => $alarm['id'],
'start' => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '',
'end' => $alarm['end'] ? $this->adjust_timezone($alarm['end'])->format('c') : '',
'allDay' => $alarm['allday'] == 1,
'action' => $alarm['action'],
'title' => $alarm['title'],
'location' => $alarm['location'],
'calendar' => $alarm['calendar'],
);
}
return $out;
}
/**
* Render a dropdown menu to choose snooze time
*/
private function snooze_select($attrib = array())
{
$steps = array(
5 => 'repeatinmin',
10 => 'repeatinmin',
15 => 'repeatinmin',
20 => 'repeatinmin',
30 => 'repeatinmin',
60 => 'repeatinhr',
120 => 'repeatinhrs',
1440 => 'repeattomorrow',
10080 => 'repeatinweek',
);
$items = array();
foreach ($steps as $n => $label) {
$items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
$this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
}
return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
}
/********* Recurrence rules handling ********/
/**
* Render localized text describing the recurrence rule of an event
*/
public function recurrence_text($rrule)
{
$limit = 10;
$exdates = array();
$format = $this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
$format = self::to_php_date_format($format);
$format_fn = function($dt) use ($format) {
return rcmail::get_instance()->format_date($dt, $format);
};
if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
$exdates = array_map($format_fn, $rrule['EXDATE']);
}
if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
$rdates = array_map($format_fn, $rrule['RDATE']);
if (!empty($exdates)) {
$rdates = array_diff($rdates, $exdates);
}
if (count($rdates) > $limit) {
$rdates = array_slice($rdates, 0, $limit);
$more = true;
}
return $this->gettext('ondate') . ' ' . join(', ', $rdates)
. ($more ? '...' : '');
}
$output = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1);
switch ($rrule['FREQ']) {
case 'DAILY':
$output .= $this->gettext('days');
break;
case 'WEEKLY':
$output .= $this->gettext('weeks');
break;
case 'MONTHLY':
$output .= $this->gettext('months');
break;
case 'YEARLY':
$output .= $this->gettext('years');
break;
}
if ($rrule['COUNT']) {
$until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
}
else if ($rrule['UNTIL']) {
$until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format);
}
else {
$until = $this->gettext('forever');
}
$output .= ', ' . $until;
if (!empty($exdates)) {
if (count($exdates) > $limit) {
$exdates = array_slice($exdates, 0, $limit);
$more = true;
}
$output = '; ' . $this->gettext('except') . ' ' . join(', ', $exdates)
. ($more ? '...' : '');
}
return $output;
}
/**
* Generate the form for recurrence settings
*/
public function recurrence_form($attrib = array())
{
switch ($attrib['part']) {
// frequency selector
case 'frequency':
$select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency', 'class' => 'form-control'));
$select->add($this->gettext('never'), '');
$select->add($this->gettext('daily'), 'DAILY');
$select->add($this->gettext('weekly'), 'WEEKLY');
$select->add($this->gettext('monthly'), 'MONTHLY');
$select->add($this->gettext('yearly'), 'YEARLY');
$select->add($this->gettext('rdate'), 'RDATE');
$html = html::label(array('for' => 'edit-recurrence-frequency', 'class' => 'col-form-label col-sm-2'), $this->gettext('frequency'))
. html::div('col-sm-10', $select->show(''));
break;
// daily recurrence
case 'daily':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-daily'));
$html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-daily', 'class' => 'col-form-label col-sm-2'),
$this->gettext('every')) . html::div('col-sm-10', $select->show(1) . html::span('label-after', $this->gettext('days'))));
break;
// weekly recurrence form
case 'weekly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-weekly'));
$html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-weekly', 'class' => 'col-form-label col-sm-2'),
$this->gettext('every')) . html::div('col-sm-10', $select->show(1) . html::span('label-after', $this->gettext('weeks'))));
// weekday selection
$daymap = array('sun','mon','tue','wed','thu','fri','sat');
$checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
$first = $this->rc->config->get('calendar_first_day', 1);
for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$weekdays .= html::label(array('class' => 'weekday'),
$checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) .
$this->gettext($daymap[$d])
) . ' ';
}
$html .= html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), $this->gettext('bydays'))
. html::div('col-sm-10', $weekdays));
break;
// monthly recurrence form
case 'monthly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-monthly'));
$html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-monthly', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
. html::div('col-sm-10', $select->show(1) . html::span('label-after', $this->gettext('months'))));
$checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
for ($monthdays = '', $d = 1; $d <= 31; $d++) {
$monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
$monthdays .= $d % 7 ? ' ' : html::br();
}
// rule selectors
$radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each')));
$table->add(null, $monthdays);
$table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('onevery')));
$table->add(null, $this->rrule_selectors($attrib['part']));
$html .= html::div($attrib, $table->show());
break;
// annually recurrence form
case 'yearly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-yearly'));
$html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-yearly', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
. html::div('col-sm-10', $select->show(1) . html::span('label-after', $this->gettext('years'))));
// month selector
$monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
$checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
for ($months = '', $m = 1; $m <= 12; $m++) {
$months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m]));
$months .= $m % 4 ? ' ' : html::br();
}
$html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months);
// day rule selection
$html .= html::div($attrib, html::label(array('col-form-label col-sm-2'), $this->gettext('onevery'))
. html::div('col-sm-10', $this->rrule_selectors($attrib['part'], '---')));
break;
// end of recurrence form
case 'until':
$radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
$select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times', 'class' => 'form-control'));
$input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10", 'class' => 'form-control'));
$html = html::div('line first',
html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' .
$this->gettext('forever'))
);
$forntimes = $this->gettext(array(
'name' => 'forntimes',
'vars' => array('nr' => '%s'))
);
$html .= html::div('line',
$radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count', 'aria-label' => sprintf($forntimes, 'N'))) . ' ' .
sprintf($forntimes, $select->show(1))
);
$html .= html::div('line',
$radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate'))) . ' ' .
$this->gettext('untildate') . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate')))
);
$html = html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), ucfirst($this->gettext('recurrencend'))) . $html);
break;
case 'rdate':
$ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), '');
$input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10", 'class' => 'form-control'));
$button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate')));
$html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show()));
break;
}
return $html;
}
/**
* Input field for interval selection
*/
private function interval_selector($attrib)
{
$select = new html_select($attrib);
$select->add(range(1,30), range(1,30));
return $select;
}
/**
* Drop-down menus for recurrence rules like "each last sunday of"
*/
private function rrule_selectors($part, $noselect = null)
{
// rule selectors
$select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix", 'class' => 'form-control'));
if ($noselect) $select_prefix->add($noselect, '');
$select_prefix->add(array(
$this->gettext('first'),
$this->gettext('second'),
$this->gettext('third'),
$this->gettext('fourth'),
$this->gettext('last')
),
array(1, 2, 3, 4, -1));
$select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday", 'class' => 'form-control'));
if ($noselect) $select_wday->add($noselect, '');
$daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
$first = $this->rc->config->get('calendar_first_day', 1);
for ($j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
}
return $select_prefix->show() . ' ' . $select_wday->show();
}
/**
* Convert the recurrence settings to be processed on the client
*/
public function to_client_recurrence($recurrence, $allday = false)
{
if ($recurrence['UNTIL']) {
$recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c');
}
// format RDATE values
if (is_array($recurrence['RDATE'])) {
$libcal = $this;
$recurrence['RDATE'] = array_map(function($rdate) use ($libcal) {
return $libcal->adjust_timezone($rdate, true)->format('c');
}, $recurrence['RDATE']);
}
unset($recurrence['EXCEPTIONS']);
return $recurrence;
}
/**
* Process the alarms values submitted by the client
*/
public function from_client_recurrence($recurrence, $start = null)
{
if (is_array($recurrence) && !empty($recurrence['UNTIL'])) {
$recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone);
}
if (is_array($recurrence) && is_array($recurrence['RDATE'])) {
$tz = $this->timezone;
$recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) {
try {
$dt = new DateTime($rdate, $tz);
if (is_a($start, 'DateTime'))
$dt->setTime($start->format('G'), $start->format('i'));
return $dt;
}
catch (Exception $e) {
return null;
}
}, $recurrence['RDATE']);
}
return $recurrence;
}
- /********* Attachments handling *********/
-
- /**
- * Handler for attachment uploads
- */
- public function attachment_upload($session_key, $id_prefix = '')
- {
- // Upload progress update
- if (!empty($_GET['_progress'])) {
- $this->rc->upload_progress();
- }
-
- $recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
- $uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
-
- if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
- $_SESSION[$session_key] = array();
- $_SESSION[$session_key]['id'] = $recid;
- $_SESSION[$session_key]['attachments'] = array();
- }
-
- // clear all stored output properties (like scripts and env vars)
- $this->rc->output->reset();
-
- if (is_array($_FILES['_attachments']['tmp_name'])) {
- foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
- // Process uploaded attachment if there is no error
- $err = $_FILES['_attachments']['error'][$i];
-
- if (!$err) {
- $filename = $_FILES['_attachments']['name'][$i];
- $attachment = array(
- 'path' => $filepath,
- 'size' => $_FILES['_attachments']['size'][$i],
- 'name' => $filename,
- 'mimetype' => rcube_mime::file_content_type($filepath, $filename, $_FILES['_attachments']['type'][$i]),
- 'group' => $recid,
- );
-
- $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
- }
-
- if (!$err && $attachment['status'] && !$attachment['abort']) {
- $id = $attachment['id'];
-
- // store new attachment in session
- unset($attachment['status'], $attachment['abort']);
- $this->rc->session->append($session_key . '.attachments', $id, $attachment);
-
- if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) {
- $button = html::img(array(
- 'src' => $icon,
- 'alt' => $this->rc->gettext('delete')
- ));
- }
- else if ($_SESSION[$session_key . '_textbuttons']) {
- $button = rcube::Q($this->rc->gettext('delete'));
- }
- else {
- $button = '';
- }
-
- $link_content = sprintf('%s(%s)',
- rcube::Q($attachment['name']), $this->rc->show_bytes($attachment['size']));
-
- $delete_link = html::a(array(
- 'href' => "#delete",
- 'class' => 'delete',
- 'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", rcmail_output::JS_OBJECT_NAME, $id),
- 'title' => $this->rc->gettext('delete'),
- 'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
- ), $button);
-
- $content_link = html::a(array(
- 'href' => "#load",
- 'class' => 'filename',
- 'onclick' => 'return false', // sprintf("return %s.command('load-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
- ), $link_content);
-
- $content .= $_SESSION[$session_key . '_icon_pos'] == 'left' ? $delete_link.$content_link : $content_link.$delete_link;
-
- $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
- 'html' => $content,
- 'name' => $attachment['name'],
- 'mimetype' => $attachment['mimetype'],
- 'classname' => 'no-menu ' . rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
- 'complete' => true
- ), $uploadid);
- }
- else { // upload failed
- if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
- $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
- 'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
- }
- else if ($attachment['error']) {
- $msg = $attachment['error'];
- }
- else {
- $msg = $this->rc->gettext('fileuploaderror');
- }
-
- $this->rc->output->command('display_message', $msg, 'error');
- $this->rc->output->command('remove_from_attachment_list', $uploadid);
- }
- }
- }
- else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- // if filesize exceeds post_max_size then $_FILES array is empty,
- // show filesizeerror instead of fileuploaderror
- if ($maxsize = ini_get('post_max_size'))
- $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
- 'size' => $this->rc->show_bytes(parse_bytes($maxsize)))));
- else
- $msg = $this->rc->gettext('fileuploaderror');
-
- $this->rc->output->command('display_message', $msg, 'error');
- $this->rc->output->command('remove_from_attachment_list', $uploadid);
- }
-
- $this->rc->output->send('iframe');
- }
-
-
- /**
- * Deliver an event/task attachment to the client
- * (similar as in Roundcube core program/steps/mail/get.inc)
- */
- public function attachment_get($attachment)
- {
- ob_end_clean();
-
- if ($attachment && $attachment['body']) {
- // allow post-processing of the attachment body
- $part = new rcube_message_part;
- $part->filename = $attachment['name'];
- $part->size = $attachment['size'];
- $part->mimetype = $attachment['mimetype'];
-
- $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
- 'body' => $attachment['body'],
- 'mimetype' => strtolower($attachment['mimetype']),
- 'download' => !empty($_GET['_download']),
- 'part' => $part,
- ));
-
- if ($plugin['abort'])
- exit;
-
- $mimetype = $plugin['mimetype'];
- list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
-
- $browser = $this->rc->output->browser;
-
- // send download headers
- if ($plugin['download']) {
- header("Content-Type: application/octet-stream");
- if ($browser->ie)
- header("Content-Type: application/force-download");
- }
- else if ($ctype_primary == 'text') {
- header("Content-Type: text/$ctype_secondary");
- }
- else {
- header("Content-Type: $mimetype");
- header("Content-Transfer-Encoding: binary");
- }
-
- // display page, @TODO: support text/plain (and maybe some other text formats)
- if ($mimetype == 'text/html' && empty($_GET['_download'])) {
- $OUTPUT = new rcmail_html_page();
- // @TODO: use washtml on $body
- $OUTPUT->write($plugin['body']);
- }
- else {
- // don't kill the connection if download takes more than 30 sec.
- @set_time_limit(0);
-
- $filename = $attachment['name'];
- $filename = preg_replace('[\r\n]', '', $filename);
-
- if ($browser->ie && $browser->ver < 7)
- $filename = rawurlencode(abbreviate_string($filename, 55));
- else if ($browser->ie)
- $filename = rawurlencode($filename);
- else
- $filename = addcslashes($filename, '"');
-
- $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
- header("Content-Disposition: $disposition; filename=\"$filename\"");
-
- echo $plugin['body'];
- }
-
- exit;
- }
-
- // if we arrive here, the requested part was not found
- header('HTTP/1.1 404 Not Found');
- exit;
- }
-
- /**
- * Show "loading..." page in attachment iframe
- */
- public function attachment_loading_page()
- {
- $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
- $message = $this->rc->gettext('loadingdata');
-
- header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
- print "\n\n"
- . '' . "\n"
- . '' . "\n"
- . "\n\n$message\n\n";
- exit;
- }
-
- /**
- * Template object for attachment display frame
- */
- public function attachment_frame($attrib = array())
- {
- $mimetype = strtolower($this->attachment['mimetype']);
- list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
-
- $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
-
- $this->rc->output->add_gui_object('attachmentframe', $attrib['id']);
-
- return html::iframe($attrib);
- }
-
- /**
- *
- */
- public function attachment_header($attrib = array())
- {
- $rcmail = rcmail::get_instance();
- $dl_link = strtolower($attrib['downloadlink']) == 'true';
- $dl_url = $this->rc->url(array('_frame' => null, '_download' => 1) + $_GET);
-
- $table = new html_table(array('cols' => $dl_link ? 3 : 2));
-
- if (!empty($this->attachment['name'])) {
- $table->add('title', rcube::Q($this->rc->gettext('filename')));
- $table->add('header', rcube::Q($this->attachment['name']));
- if ($dl_link) {
- $table->add('download-link', html::a($dl_url, rcube::Q($this->rc->gettext('download'))));
- }
- }
-
- if (!empty($this->attachment['mimetype'])) {
- $table->add('title', rcube::Q($this->rc->gettext('type')));
- $table->add('header', rcube::Q($this->attachment['mimetype']));
- }
-
- if (!empty($this->attachment['size'])) {
- $table->add('title', rcube::Q($this->rc->gettext('filesize')));
- $table->add('header', rcube::Q($this->rc->show_bytes($this->attachment['size'])));
- }
-
- $this->rc->output->set_env('attachment_download_url', $dl_url);
-
- return $table->show($attrib);
- }
-
-
/********* iTip message detection *********/
/**
* Check mail message structure of there are .ics files attached
*/
public function mail_message_load($p)
{
$this->ical_message = $p['object'];
$itip_part = null;
// check all message parts for .ics files
foreach ((array)$this->ical_message->mime_parts as $part) {
if (self::part_is_vcalendar($part, $this->ical_message)) {
if ($part->ctype_parameters['method'])
$itip_part = $part->mime_id;
else
$this->ical_parts[] = $part->mime_id;
}
}
// priorize part with method parameter
if ($itip_part) {
$this->ical_parts = array($itip_part);
}
}
/**
* Getter for the parsed iCal objects attached to the current email message
*
* @return object libvcalendar parser instance with the parsed objects
*/
public function get_mail_ical_objects()
{
// create parser and load ical objects
if (!$this->mail_ical_parser) {
$this->mail_ical_parser = $this->get_ical();
foreach ($this->ical_parts as $mime_id) {
$part = $this->ical_message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ?: RCUBE_CHARSET;
$this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
// check if the parsed object is an instance of a recurring event/task
array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
// stop on the part that has an iTip method specified
if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
$this->mail_ical_parser->message_date = $this->ical_message->headers->date;
$this->mail_ical_parser->mime_id = $mime_id;
// store the message's sender address for comparisons
$from = rcube_mime::decode_address_list($this->ical_message->headers->from, 1, true, null, true);
$this->mail_ical_parser->sender = !empty($from) ? $from[1] : '';
if (!empty($this->mail_ical_parser->sender)) {
foreach ($this->mail_ical_parser->objects as $i => $object) {
$this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender;
$this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender);
}
}
break;
}
}
}
return $this->mail_ical_parser;
}
/**
* Read the given mime message from IMAP and parse ical data
*
* @param string Mailbox name
* @param string Message UID
* @param string Message part ID and object index (e.g. '1.2:0')
* @param string Object type filter (optional)
*
* @return array Hash array with the parsed iCal
*/
public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null)
{
$charset = RCUBE_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_folder($mbox);
if ($uid && $mime_id) {
list($mime_id, $index) = explode(':', $mime_id);
$part = $imap->get_message_part($uid, $mime_id);
$headers = $imap->get_message_headers($uid);
$parser = $this->get_ical();
if ($part->ctype_parameters['charset']) {
$charset = $part->ctype_parameters['charset'];
}
if ($part) {
$objects = $parser->import($part, $charset);
}
}
// successfully parsed events/tasks?
if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) {
if ($parser->method)
$object['_method'] = $parser->method;
// store the message's sender address for comparisons
$from = rcube_mime::decode_address_list($headers->from, 1, true, null, true);
$object['_sender'] = !empty($from) ? $from[1] : '';
$object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
// check if this is an instance of a recurring event/task
self::identify_recurrence_instance($object);
return $object;
}
return null;
}
/**
* Checks if specified message part is a vcalendar data
*
* @param rcube_message_part Part object
* @param rcube_message Message object
*
* @return boolean True if part is of type vcard
*/
public static function part_is_vcalendar($part, $message = null)
{
// First check if the message is "valid" (i.e. not multipart/report)
if ($message) {
$level = explode('.', $part->mime_id);
while (array_pop($level) !== null) {
$parent = $message->mime_parts[join('.', $level) ?: 0];
if ($parent->mimetype == 'multipart/report') {
return false;
}
}
}
return (
in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
// Apple sends files as application/x-any (!?)
($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
);
}
/**
* Single occourrences of recurring events are identified by their RECURRENCE-ID property
* in iCal which is represented as 'recurrence_date' in our internal data structure.
*
* Check if such a property exists and derive the '_instance' identifier and '_savemode'
* attributes which are used in the storage backend to identify the nested exception item.
*/
public static function identify_recurrence_instance(&$object)
{
// for savemode=all, remove recurrence instance identifiers
if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) {
unset($object['_instance'], $object['recurrence_date']);
}
// set instance and 'savemode' according to recurrence-id
else if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
$object['_instance'] = self::recurrence_instance_identifier($object);
$object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current';
}
else if (!empty($object['recurrence_id']) && !empty($object['_instance'])) {
if (strlen($object['_instance']) > 4) {
$object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
}
else {
$object['recurrence_date'] = clone $object['start'];
}
}
}
/**
* Return a date() format string to render identifiers for recurrence instances
*
* @param array Hash array with event properties
* @return string Format string
*/
public static function recurrence_id_format($event)
{
return $event['allday'] ? 'Ymd' : 'Ymd\THis';
}
/**
* Return the identifer for the given instance of a recurring event
*
* @param array Hash array with event properties
* @param bool All-day flag from the main event
*
* @return mixed Format string or null if identifier cannot be generated
*/
public static function recurrence_instance_identifier($event, $allday = null)
{
$instance_date = $event['recurrence_date'] ?: $event['start'];
if ($instance_date && is_a($instance_date, 'DateTime')) {
// According to RFC5545 (3.8.4.4) RECURRENCE-ID format should
// be date/date-time depending on the main event type, not the exception
if ($allday === null) {
$allday = $event['allday'];
}
return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis');
}
}
/********* Attendee handling functions *********/
/**
* Handler for attendee group expansion requests
*/
public function expand_attendee_group()
{
$id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
$result = array('id' => $id, 'members' => array());
$maxnum = 500;
// iterate over all autocomplete address books (we don't know the source of the group)
foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) {
if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) {
foreach ($abook->list_groups($data['name'], 1) as $group) {
// this is the matching group to expand
if (in_array($data['email'], (array)$group['email'])) {
$abook->set_pagesize($maxnum);
$abook->set_group($group['ID']);
// get all members
$res = $abook->list_records($this->rc->config->get('contactlist_fields'));
// handle errors (e.g. sizelimit, timelimit)
if ($abook->get_error()) {
$result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring');
$res = false;
}
// check for maximum number of members (we don't wanna bloat the UI too much)
else if ($res->count > $maxnum) {
$result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring');
$res = false;
}
while ($res && ($member = $res->iterate())) {
$emails = (array)$abook->get_col_values('email', $member, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$result['members'][] = array(
'email' => $email,
'name' => rcube_addressbook::compose_list_name($member),
);
}
}
break 2;
}
}
}
}
$this->rc->output->command('plugin.expand_attendee_callback', $result);
}
/**
* Merge attendees of the old and new event version
* with keeping current user and his delegatees status
*
* @param array &$new New object data
* @param array $old Old object data
* @param bool $status New status of the current user
*/
public function merge_attendees(&$new, $old, $status = null)
{
if (empty($status)) {
$emails = $this->get_user_emails();
$delegates = array();
$attendees = array();
// keep attendee status of the current user
foreach ((array) $new['attendees'] as $i => $attendee) {
if (empty($attendee['email'])) {
continue;
}
$attendees[] = $email = strtolower($attendee['email']);
if (in_array($email, $emails)) {
foreach ($old['attendees'] as $_attendee) {
if ($attendee['email'] == $_attendee['email']) {
$new['attendees'][$i] = $_attendee;
if ($_attendee['status'] == 'DELEGATED' && ($email = $_attendee['delegated-to'])) {
$delegates[] = strtolower($email);
}
break;
}
}
}
}
// make sure delegated attendee is not lost
foreach ($delegates as $delegatee) {
if (!in_array($delegatee, $attendees)) {
foreach ((array) $old['attendees'] as $attendee) {
if ($attendee['email'] && ($email = strtolower($attendee['email'])) && $email == $delegatee) {
$new['attendees'][] = $attendee;
break;
}
}
}
}
}
// We also make sure that status of any attendee
// is not overriden by NEEDS-ACTION if it was already set
// which could happen if you work with shared events
foreach ((array) $new['attendees'] as $i => $attendee) {
if ($attendee['email'] && $attendee['status'] == 'NEEDS-ACTION') {
foreach ($old['attendees'] as $_attendee) {
if ($attendee['email'] == $_attendee['email']) {
$new['attendees'][$i]['status'] = $_attendee['status'];
unset($new['attendees'][$i]['rsvp']);
break;
}
}
}
}
}
/********* Static utility functions *********/
/**
* Convert the internal structured data into a vcalendar rrule 2.0 string
*/
public static function to_rrule($recurrence, $allday = false)
{
if (is_string($recurrence))
return $recurrence;
$rrule = '';
foreach ((array)$recurrence as $k => $val) {
$k = strtoupper($k);
switch ($k) {
case 'UNTIL':
// convert to UTC according to RFC 5545
if (is_a($val, 'DateTime')) {
if (!$allday && !$val->_dateonly) {
$until = clone $val;
$until->setTimezone(new DateTimeZone('UTC'));
$val = $until->format('Ymd\THis\Z');
}
else {
$val = $val->format('Ymd');
}
}
break;
case 'RDATE':
case 'EXDATE':
foreach ((array)$val as $i => $ex) {
if (is_a($ex, 'DateTime'))
$val[$i] = $ex->format('Ymd\THis');
}
$val = join(',', (array)$val);
break;
case 'EXCEPTIONS':
continue 2;
}
if (strlen($val))
$rrule .= $k . '=' . $val . ';';
}
return rtrim($rrule, ';');
}
/**
* Convert from fullcalendar date format to PHP date() format string
*/
public static function to_php_date_format($from)
{
// "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
return strtr(strtr($from, array(
'YYYY' => 'Y',
'YY' => 'y',
'yyyy' => 'Y',
'yy' => 'y',
'MMMM' => 'F',
'MMM' => 'M',
'MM' => 'm',
'M' => 'n',
'dddd' => 'l',
'ddd' => 'D',
'dd' => 'd',
'd' => 'j',
'HH' => '**',
'hh' => '%%',
'H' => 'G',
'h' => 'g',
'mm' => 'i',
'ss' => 's',
'TT' => 'A',
'tt' => 'a',
'T' => 'A',
't' => 'a',
'u' => 'c',
)), array(
'**' => 'H',
'%%' => 'h',
));
}
/**
* Convert from PHP date() format to fullcalendar format string
*/
public static function from_php_date_format($from)
{
// "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
return strtr($from, array(
'y' => 'yy',
'Y' => 'yyyy',
'M' => 'MMM',
'F' => 'MMMM',
'm' => 'MM',
'n' => 'M',
'j' => 'd',
'd' => 'dd',
'D' => 'ddd',
'l' => 'dddd',
'H' => 'HH',
'h' => 'hh',
'G' => 'H',
'g' => 'h',
'i' => 'mm',
's' => 'ss',
'A' => 'TT',
'a' => 'tt',
'c' => 'u',
));
}
}
diff --git a/plugins/libkolab/lib/kolab_attachments_handler.php b/plugins/libkolab/lib/kolab_attachments_handler.php
new file mode 100644
index 00000000..821aaaf9
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_attachments_handler.php
@@ -0,0 +1,313 @@
+
+ * @author Aleksander Machniak
+ *
+ * Copyright (C) 2012-2018, 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_attachments_handler
+{
+ private $rc;
+ private $attachment;
+
+ public function __construct()
+ {
+ $this->rc = rcmail::get_instance();
+ }
+
+ /**
+ * Displays attachment preview page
+ */
+ public function attachment_page($attachment)
+ {
+ $this->attachment = $attachment;
+
+ $this->rc->output->add_handler('plugin.attachmentframe', array($this, 'attachment_frame'));
+ $this->rc->output->add_handler('plugin.attachmentcontrols', array($this, 'attachment_header'));
+ $this->rc->output->set_env('filename', $attachment['name']);
+ $this->rc->output->set_env('mimetype', $attachment['mimetype']);
+ $this->rc->output->send('libkolab.attachment');
+ }
+
+ /**
+ * Handler for attachment uploads
+ */
+ public function attachment_upload($session_key, $id_prefix = '')
+ {
+ // Upload progress update
+ if (!empty($_GET['_progress'])) {
+ $this->rc->upload_progress();
+ }
+
+ $recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ $uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
+
+ if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
+ $_SESSION[$session_key] = array();
+ $_SESSION[$session_key]['id'] = $recid;
+ $_SESSION[$session_key]['attachments'] = array();
+ }
+
+ // clear all stored output properties (like scripts and env vars)
+ $this->rc->output->reset();
+
+ if (is_array($_FILES['_attachments']['tmp_name'])) {
+ foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
+ // Process uploaded attachment if there is no error
+ $err = $_FILES['_attachments']['error'][$i];
+
+ if (!$err) {
+ $filename = $_FILES['_attachments']['name'][$i];
+ $attachment = array(
+ 'path' => $filepath,
+ 'size' => $_FILES['_attachments']['size'][$i],
+ 'name' => $filename,
+ 'mimetype' => rcube_mime::file_content_type($filepath, $filename, $_FILES['_attachments']['type'][$i]),
+ 'group' => $recid,
+ );
+
+ $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
+ }
+
+ if (!$err && $attachment['status'] && !$attachment['abort']) {
+ $id = $attachment['id'];
+
+ // store new attachment in session
+ unset($attachment['status'], $attachment['abort']);
+ $this->rc->session->append($session_key . '.attachments', $id, $attachment);
+
+ if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) {
+ $button = html::img(array(
+ 'src' => $icon,
+ 'alt' => $this->rc->gettext('delete')
+ ));
+ }
+ else if ($_SESSION[$session_key . '_textbuttons']) {
+ $button = rcube::Q($this->rc->gettext('delete'));
+ }
+ else {
+ $button = '';
+ }
+
+ $link_content = sprintf('%s(%s)',
+ rcube::Q($attachment['name']), $this->rc->show_bytes($attachment['size']));
+
+ $delete_link = html::a(array(
+ 'href' => "#delete",
+ 'class' => 'delete',
+ 'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", rcmail_output::JS_OBJECT_NAME, $id),
+ 'title' => $this->rc->gettext('delete'),
+ 'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
+ ), $button);
+
+ $content_link = html::a(array(
+ 'href' => "#load",
+ 'class' => 'filename',
+ 'onclick' => 'return false', // sprintf("return %s.command('load-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
+ ), $link_content);
+
+ $content .= $_SESSION[$session_key . '_icon_pos'] == 'left' ? $delete_link.$content_link : $content_link.$delete_link;
+
+ $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
+ 'html' => $content,
+ 'name' => $attachment['name'],
+ 'mimetype' => $attachment['mimetype'],
+ 'classname' => 'no-menu ' . rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
+ 'complete' => true
+ ), $uploadid);
+ }
+ else { // upload failed
+ if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+ $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
+ 'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
+ }
+ else if ($attachment['error']) {
+ $msg = $attachment['error'];
+ }
+ else {
+ $msg = $this->rc->gettext('fileuploaderror');
+ }
+
+ $this->rc->output->command('display_message', $msg, 'error');
+ $this->rc->output->command('remove_from_attachment_list', $uploadid);
+ }
+ }
+ }
+ else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ // if filesize exceeds post_max_size then $_FILES array is empty,
+ // show filesizeerror instead of fileuploaderror
+ if ($maxsize = ini_get('post_max_size'))
+ $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
+ 'size' => $this->rc->show_bytes(parse_bytes($maxsize)))));
+ else
+ $msg = $this->rc->gettext('fileuploaderror');
+
+ $this->rc->output->command('display_message', $msg, 'error');
+ $this->rc->output->command('remove_from_attachment_list', $uploadid);
+ }
+
+ $this->rc->output->send('iframe');
+ }
+
+
+ /**
+ * Deliver an event/task attachment to the client
+ * (similar as in Roundcube core program/steps/mail/get.inc)
+ */
+ public function attachment_get($attachment)
+ {
+ ob_end_clean();
+
+ if ($attachment && $attachment['body']) {
+ // allow post-processing of the attachment body
+ $part = new rcube_message_part;
+ $part->filename = $attachment['name'];
+ $part->size = $attachment['size'];
+ $part->mimetype = $attachment['mimetype'];
+
+ $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
+ 'body' => $attachment['body'],
+ 'mimetype' => strtolower($attachment['mimetype']),
+ 'download' => !empty($_GET['_download']),
+ 'part' => $part,
+ ));
+
+ if ($plugin['abort'])
+ exit;
+
+ $mimetype = $plugin['mimetype'];
+ list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+ $browser = $this->rc->output->browser;
+
+ // send download headers
+ if ($plugin['download']) {
+ header("Content-Type: application/octet-stream");
+ if ($browser->ie)
+ header("Content-Type: application/force-download");
+ }
+ else if ($ctype_primary == 'text') {
+ header("Content-Type: text/$ctype_secondary");
+ }
+ else {
+ header("Content-Type: $mimetype");
+ header("Content-Transfer-Encoding: binary");
+ }
+
+ // display page, @TODO: support text/plain (and maybe some other text formats)
+ if ($mimetype == 'text/html' && empty($_GET['_download'])) {
+ $OUTPUT = new rcmail_html_page();
+ // @TODO: use washtml on $body
+ $OUTPUT->write($plugin['body']);
+ }
+ else {
+ // don't kill the connection if download takes more than 30 sec.
+ @set_time_limit(0);
+
+ $filename = $attachment['name'];
+ $filename = preg_replace('[\r\n]', '', $filename);
+
+ if ($browser->ie && $browser->ver < 7)
+ $filename = rawurlencode(abbreviate_string($filename, 55));
+ else if ($browser->ie)
+ $filename = rawurlencode($filename);
+ else
+ $filename = addcslashes($filename, '"');
+
+ $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
+ header("Content-Disposition: $disposition; filename=\"$filename\"");
+
+ echo $plugin['body'];
+ }
+
+ exit;
+ }
+
+ // if we arrive here, the requested part was not found
+ header('HTTP/1.1 404 Not Found');
+ exit;
+ }
+
+ /**
+ * Show "loading..." page in attachment iframe
+ */
+ public function attachment_loading_page()
+ {
+ $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
+ $message = $this->rc->gettext('loadingdata');
+
+ header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
+ print "\n\n"
+ . '' . "\n"
+ . '' . "\n"
+ . "\n\n$message\n\n";
+ exit;
+ }
+
+ /**
+ * Template object for attachment display frame
+ */
+ public function attachment_frame($attrib = array())
+ {
+ $mimetype = strtolower($this->attachment['mimetype']);
+ list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+ $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
+
+ $this->rc->output->add_gui_object('attachmentframe', $attrib['id']);
+
+ return html::iframe($attrib);
+ }
+
+ /**
+ *
+ */
+ public function attachment_header($attrib = array())
+ {
+ $rcmail = rcmail::get_instance();
+ $dl_link = strtolower($attrib['downloadlink']) == 'true';
+ $dl_url = $this->rc->url(array('_frame' => null, '_download' => 1) + $_GET);
+ $table = new html_table(array('cols' => $dl_link ? 3 : 2));
+
+ if (!empty($this->attachment['name'])) {
+ $table->add('title', rcube::Q($this->rc->gettext('filename')));
+ $table->add('header', rcube::Q($this->attachment['name']));
+
+ if ($dl_link) {
+ $table->add('download-link', html::a($dl_url, rcube::Q($this->rc->gettext('download'))));
+ }
+ }
+
+ if (!empty($this->attachment['mimetype'])) {
+ $table->add('title', rcube::Q($this->rc->gettext('type')));
+ $table->add('header', rcube::Q($this->attachment['mimetype']));
+ }
+
+ if (!empty($this->attachment['size'])) {
+ $table->add('title', rcube::Q($this->rc->gettext('filesize')));
+ $table->add('header', rcube::Q($this->rc->show_bytes($this->attachment['size'])));
+ }
+
+ $this->rc->output->set_env('attachment_download_url', $dl_url);
+
+ return $table->show($attrib);
+ }
+}
diff --git a/plugins/tasklist/skins/elastic/templates/attachment.html b/plugins/libkolab/skins/elastic/templates/attachment.html
similarity index 83%
rename from plugins/tasklist/skins/elastic/templates/attachment.html
rename to plugins/libkolab/skins/elastic/templates/attachment.html
index e81fabe4..eb4d6950 100644
--- a/plugins/tasklist/skins/elastic/templates/attachment.html
+++ b/plugins/libkolab/skins/elastic/templates/attachment.html
@@ -1,67 +1,86 @@
+
+
diff --git a/plugins/libkolab/skins/larry/libkolab.css b/plugins/libkolab/skins/larry/libkolab.css
index a9791007..3be1c926 100644
--- a/plugins/libkolab/skins/larry/libkolab.css
+++ b/plugins/libkolab/skins/larry/libkolab.css
@@ -1,87 +1,138 @@
ul.toolbarmenu li a.history span.history {
background-image: url('data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAABXUlEQVQoFbXBvUsCYQAH4N/mdm5uN9UStzn1AYG0HC1Jg01tV/9A0NYQ5dRm4BI0VYRYUYOD3CSB2FaLXBJhIXYeXr7veVGd8qus0D6Whp4H+Duq1LnEBDX8jiqz3XpgPzqB3a0zSw3fUetavlux0hkjlc5cnPtut84EBlEJLr1WOoN5RKEiisTylmMHNjX0Pa17rd0TRAFqzOLN8Ma2FN4u+tpVx8Y4Xp1O+y7eaRXroUkFn7xWqYieXFwKfDjek6IxCYAqy6xJ8dBkjatALi4FDfQUN6XIxfEqPJt0bCmkMAuIAXMjx/nnBvPUAGtfip0p9ERmk45tFqDjTQix0TXrzFqg4t3cVhHBh8jECnQqVPAujAS0zqHXOshgQJhKx+ycUUcPx5j33YqFcXwRMo/a94HNMrMsd+58t1gaWsQPMSNlFq7Kvlu7LpaMFGYQwi9U6JjHEgzMQMU/eAHASL3dDo5S4wAAAABJRU5ErkJggg==');
background-position: 0px 2px;
background-repeat: no-repeat;
}
.changelog-table .loading {
color: #666;
margin: 1em 0;
padding: 1px 0 2px 24px;
background: url('data:image/gif;base64,R0lGODlhEAAQAPQAAP///xB1v/j6/ESTzIu83xV4wDOJyNjo9K3P6CSAxH603G+s2OXv957G5Mnf8FOb0GCj1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFUCAgjmRpnqUwFGwhKoRgqq2YFMaRGjWA8AbZiIBbjQQ8AmmFUJEQhQGJhaKOrCksgEla+KIkYvC6SJKQOISoNSYdeIk1ayA8ExTyeR3F749CACH5BAkKAAAALAAAAAAQABAAAAVoICCKR9KMaCoaxeCoqEAkRX3AwMHWxQIIjJSAZWgUEgzBwCBAEQpMwIDwY1FHgwJCtOW2UDWYIDyqNVVkUbYr6CK+o2eUMKgWrqKhj0FrEM8jQQALPFA3MAc8CQSAMA5ZBjgqDQmHIyEAIfkECQoAAAAsAAAAABAAEAAABWAgII4j85Ao2hRIKgrEUBQJLaSHMe8zgQo6Q8sxS7RIhILhBkgumCTZsXkACBC+0cwF2GoLLoFXREDcDlkAojBICRaFLDCOQtQKjmsQSubtDFU/NXcDBHwkaw1cKQ8MiyEAIfkECQoAAAAsAAAAABAAEAAABVIgII5kaZ6AIJQCMRTFQKiDQx4GrBfGa4uCnAEhQuRgPwCBtwK+kCNFgjh6QlFYgGO7baJ2CxIioSDpwqNggWCGDVVGphly3BkOpXDrKfNm/4AhACH5BAkKAAAALAAAAAAQABAAAAVgICCOZGmeqEAMRTEQwskYbV0Yx7kYSIzQhtgoBxCKBDQCIOcoLBimRiFhSABYU5gIgW01pLUBYkRItAYAqrlhYiwKjiWAcDMWY8QjsCf4DewiBzQ2N1AmKlgvgCiMjSQhACH5BAkKAAAALAAAAAAQABAAAAVfICCOZGmeqEgUxUAIpkA0AMKyxkEiSZEIsJqhYAg+boUFSTAkiBiNHks3sg1ILAfBiS10gyqCg0UaFBCkwy3RYKiIYMAC+RAxiQgYsJdAjw5DN2gILzEEZgVcKYuMJiEAOwAAAAAAAAAAAA==') top left no-repeat;
}
.changelog-dialog .compare-button {
margin: 4px 0;
}
.changelog-table tbody td {
padding: 4px 7px;
vertical-align: middle;
}
.changelog-table tbody tr:last-child td {
border-bottom: 0;
}
.changelog-table tbody tr.undisclosed td.date,
.changelog-table tbody tr.undisclosed td.user {
font-style: italic;
}
.changelog-table .diff {
width: 4em;
padding: 2px;
}
.changelog-table .revision {
width: 6em;
}
.changelog-table .date {
width: 11em;
}
.changelog-table .user {
width: auto;
}
.changelog-table .operation {
width: 15%;
}
.changelog-table .actions {
width: 50px;
text-align: right;
padding: 4px;
}
#mailmessagehistory .changelog-table .diff,
#mailmessagehistory .changelog-table .actions {
display: none;
}
#mailmessagehistory .changelog-table .operation {
width: 25%;
}
fieldset.birthdays .proplist li {
padding: 0;
border: 0;
}
fieldset.categories input {
height: auto !important;
line-height: 16px !important;
}
#calendarcategories .minicolors-swatch {
height: 14px;
width: 14px;
}
a.button.input-group-text {
line-height: 19px;
vertical-align: middle;
}
+
+.libkolab.attachmentwin #attachmenttoolbar {
+ position: relative;
+ top: -6px;
+ height: 40px;
+}
+
+.libkolab.attachmentwin #attachmentcontainer {
+ position: absolute;
+ top: 0;
+ left: 232px;
+ right: 0;
+ bottom: 0;
+}
+
+.libkolab.attachmentwin #attachmentframe {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ background-color: #fff;
+ border-radius: 4px;
+}
+
+.libkolab.attachmentwin #partheader {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 220px;
+ bottom: 0;
+}
+
+.libkolab.attachmentwin #partheader table {
+ table-layout: fixed;
+ overflow: hidden;
+}
+
+.libkolab.attachmentwin #partheader table td {
+ color: #666;
+ padding: 4px 6px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.libkolab.attachmentwin #partheader table td.header {
+ font-weight: bold;
+}
+
+.libkolab.attachmentwin #partheader table td.title {
+ width: 60px;
+ padding-right: 0;
+}
diff --git a/plugins/tasklist/skins/larry/templates/attachment.html b/plugins/libkolab/skins/larry/templates/attachment.html
similarity index 69%
rename from plugins/tasklist/skins/larry/templates/attachment.html
rename to plugins/libkolab/skins/larry/templates/attachment.html
index 68d4dd99..664d5f7c 100644
--- a/plugins/tasklist/skins/larry/templates/attachment.html
+++ b/plugins/libkolab/skins/larry/templates/attachment.html
@@ -1,65 +1,82 @@
-
+
diff --git a/plugins/tasklist/skins/elastic/templates/taskedit.html b/plugins/tasklist/skins/elastic/templates/taskedit.html
index f262b003..d8ee9006 100644
--- a/plugins/tasklist/skins/elastic/templates/taskedit.html
+++ b/plugins/tasklist/skins/elastic/templates/taskedit.html
@@ -1,127 +1,127 @@
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 0241dc34..728c6bac 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -1,1438 +1,1386 @@
/**
* Roundcube Taklist plugin styles for skin "Larry"
*
* Copyright (C) 2012, Kolab Systems AG
* Screendesign by FLINT / Bรผro fรผr Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#taskbar a.button-tasklist span.button-inner {
background-image: url(images/buttons.png);
background-position: 0 0;
}
#taskbar a.button-tasklist:hover span.button-inner,
#taskbar a.button-tasklist.button-selected span.button-inner {
background-position: 0 -26px;
}
ul.toolbarmenu li span.icon.taskadd,
#attachmentmenu li a.tasklistlink span.icon.taskadd {
background-image: url(images/buttons.png);
background-position: -4px -90px;
}
#taskstoolbar a.button.export {
background-image: url(images/buttons.png);
background-position: center -179px;
min-width: 50px;
max-width: 70px;
}
#taskstoolbar a.button.import {
background-image: url(images/buttons.png);
background-position: center -139px;
}
#taskedit.uidialog,
.tasklistview div.uidialog {
display: none;
}
.tasklistview #mainscreen {
min-width: 1000px !important;
min-height: 520px !important;
}
.tasklistview #header {
min-width: 1020px !important;
}
#sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 240px;
}
.tasklistview #searchmenulink {
width: 15px;
}
#tasklistsbox {
position: absolute;
top: 42px;
left: 0;
width: 100%;
bottom: 0px;
}
#tasklistsbox .boxtitle a.iconbutton.search {
position: absolute;
top: 8px;
right: 8px;
width: 16px;
cursor: pointer;
background-position: -2px -317px;
}
#tasklistsbox .listsearchbox {
display: none;
}
#tasklistsbox .listsearchbox.expanded {
display: block;
}
#tasklistsbox .scroller {
top: 34px;
}
#tasklistsbox .listsearchbox.expanded + .scroller {
top: 68px;
}
#taskselector {
margin: -1px 40px 0 0;
padding: 0;
}
#taskselector li {
display: inline-block;
position: relative;
font-size: 90%;
padding-right: 0.3em;
}
#taskselector li a {
display: inline-block;
color: #004458;
min-width: 3.5em;
padding: 0.2em 0.6em 0.2em 0.6em;
text-align: center;
text-decoration: none;
border: 1px solid #eee;
border-color: transparent;
}
.webkit #taskselector li a {
padding-bottom: 0.25em;
}
#taskselector li:first-child {
border-top: 0;
border-radius: 4px 4px 0 0;
}
#taskselector li:last-child {
border-bottom: 0;
border-radius: 0 0 4px 4px;
}
#taskselector li.overdue a {
color: #b72a2a;
font-weight: bold;
}
#taskselector li.inactive a {
color: #97b3bf;
}
#taskselector li.selected a {
color: #fff;
background: #005d76;
border-radius: 10px;
text-shadow: none;
}
#taskselector li .count {
display: none;
position: absolute;
top: -18px;
right: 5px;
min-width: 1.8em;
padding: 2px 4px;
background: #004558;
color: #fff;
border-radius: 3px;
text-align: center;
font-weight: bold;
font-size: 80%;
text-shadow: none;
}
#taskselector li .count:after {
content: "";
position: absolute;
bottom: -5px;
left: 50%;
margin-left: -5px;
border-style: solid;
border-width: 5px 5px 0;
border-color: #004558 transparent;
/* reduce the damage in FF3.0 */
display: block;
width: 0;
}
#taskselector li.overdue .count {
background: #ff3800;
}
#taskselector li.overdue .count:after {
border-color: #ff3800 transparent;
}
#tasklistsbox .treelist li {
margin: 0;
display: block;
position: relative;
}
#tasklistsbox .treelist li div.tasklist {
margin: 0;
height: 20px;
padding: 6px 8px 2px 6px;
position: relative;
white-space: nowrap;
}
#tasklistsbox .treelist li.virtual > div.tasklist {
height: 14px;
}
#tasklistsbox .treelist ul li > div.tasklist {
margin-left: 16px;
}
#tasklistsbox .treelist ul ul li > div.tasklist {
margin-left: 32px;
}
#tasklistsbox .treelist ul ul ul li > div.tasklist {
margin-left: 48px;
}
#tasklistsbox .treelist li label {
display: block;
}
#tasklistsbox .treelist li a.listname {
display: block;
position: absolute;
top: 7px;
left: 38px;
right: 40px;
cursor: default;
padding: 0px 30px 2px 2px;
color: #004458;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: url(images/sprites.png) right 20px no-repeat;
}
.quickview-active #tasklistsbox .treelist li input,
.quickview-active #tasklistsbox .treelist li a.listname {
opacity: 0.35;
}
.quickview-active #tasklistsbox .treelist div.focusview a.listname {
opacity: 1.0;
}
#tasklistsbox .treelist div span.actions {
display: inline-block;
position: absolute;
top: 2px;
right: 2px;
padding: 5px 20px 0 6px;
min-width: 40px;
height: 19px;
text-align: right;
}
#tasklistsbox .treelist div:hover span.actions {
top: 1px;
right: 1px;
border: 1px solid #ababab;
border-radius: 4px;
background: #f1f1f1;
}
#tasklistsbox .treelist div a.remove,
#tasklistsbox .treelist div a.quickview,
#tasklistsbox .treelist div a.subscribed {
display: inline-block;
width: 16px;
height: 16px;
padding: 0;
margin-right: 4px;
background: url(images/sprites.png) -200px 0 no-repeat;
overflow: hidden;
text-indent: -5000px;
cursor: pointer;
}
#tasklistsbox .treelist div a.subscribed {
position: absolute;
top: 5px;
right: 4px;
margin: 0;
}
#tasklistsbox .treelist div a.subscribed:focus,
#tasklistsbox .treelist div:hover a.subscribed {
background-position: -2px -215px;
}
#tasklistsbox .treelist div.subscribed a.subscribed {
background-position: -20px -215px;
}
#tasklistsbox .treelist div a.quickview:focus,
#tasklistsbox .treelist div:hover a.quickview {
background-position: -20px -101px;
background-color: transparent !important;
}
#tasklistsbox .treelist div a.remove:focus,
#tasklistsbox .treelist div:hover a.remove {
background-position: -2px -371px;
background-color: transparent !important;
}
#tasklistsbox .treelist div.focusview a.quickview {
background-position: -2px -101px;
}
#tasklistsbox .searchresults .treelist div a.remove,
#tasklistsbox .searchresults .treelist div a.quickview {
display: none;
}
#tasklistsbox .treelist div a.remove:focus,
#tasklistsbox .treelist div a.quickview:focus,
#tasklistsbox .treelist div a.subscribed:focus {
border-radius: 3px;
outline: 2px solid rgba(30,150,192, 0.5);
}
#tasklistsbox .treelist li.selected > div > a.listname {
font-weight: bold;
}
#tasklistsbox .treelist .readonly > a.listname {
background-position: right -142px;
}
#tasklistsbox .treelist .user > a.listname {
background-position: right -160px;
}
#tasklistsbox .treelist .virtual > a.listname {
color: #aaa;
top: 4px;
left: 20px;
right: 5px;
}
#tasklistsbox .treelist.flat li span.calname {
left: 24px;
right: 22px;
}
#tasklistsbox .treelist li input {
position: absolute;
top: 5px;
left: 18px;
}
#tasklistsbox .treelist li .treetoggle {
top: 8px;
}
#tasklistsbox .treelist li.virtual > .treetoggle {
top: 6px;
}
#tasklistsbox .searchresults {
background: #b0ccd7;
margin-top: 8px;
}
#tasklistsbox .searchresults .boxtitle {
background: none;
padding: 2px 8px 2px 8px;
}
#tasklistsbox .searchresults .listing li {
background-color: #c7e3ef;
}
#mainview-right {
position: absolute;
top: 0;
left: 256px;
right: 0;
bottom: 0;
}
#taskstoolbar {
position: absolute;
top: -6px;
left: 0;
width: 100%;
height: 40px;
white-space: nowrap;
}
#taskstoolbar a.button.newtask {
background-image: url(images/buttons.png);
background-position: center -53px;
}
#quickaddbox {
position: absolute;
top: 2px;
left: 0;
width: 60%;
height: 32px;
white-space: nowrap;
}
#quickaddinput {
width: 85%;
margin: 0;
padding: 3px 8px;
height: 18px;
background: #f1f1f1;
background: rgba(255, 255, 255, 0.7);
border-color: #a3a3a3;
font-weight: bold;
}
#quickaddbox .button {
margin-left: 5px;
padding: 4px 10px;
font-weight: bold;
}
#tasksview {
position: absolute;
top: 42px;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.25);
overflow: visible;
}
.quickview-active #tasksview {
background-image: url('images/focusview.png');
background-position: center;
background-repeat: no-repeat;
}
#message.statusbar {
border-top: 1px solid #c3c3c3;
}
#tasksview .scroller {
position: absolute;
left: 0;
top: 35px;
width: 100%;
bottom: 0;
overflow: auto;
}
#tasksview .buttonbar {
color: #777;
background: #eaeaea;
border-bottom: 1px solid #ccc;
position: relative;
line-height: 13px;
height: 20px;
}
#tasksview .buttonbar .buttonbar-right {
position: absolute;
top: 6px;
right: 8px;
}
.buttonbar-right .listmenu {
display: inline-block;
cursor: pointer;
}
.buttonbar-right a.iconbutton {
padding: 0;
background-image: url(images/sprites.png);
background-position: 0 -238px;
}
.buttonbar-right a.iconbutton.sorting {
background-position: -18px -347px;
}
#thelist {
padding: 0;
margin: 1em;
list-style: none;
}
#listmessagebox {
display: none;
font-size: 14px;
color: #666;
margin: 1.5em;
text-align:center;
}
.taskitem {
position: relative;
display: block;
margin-bottom: 3px;
}
.taskitem.dragging {
opacity: 0.5;
}
.taskitem .childtasks {
position: relative;
padding: 0;
margin: 3px 0 0 20px;
list-style: none;
}
.taskitem .childtoggle {
display: none;
position: absolute;
top: 4px;
left: -5px;
padding: 2px;
font-size: 10px;
color: #727272;
cursor: pointer;
width: 14px;
height: 14px;
background: url(images/sprites.png) -2px -80px no-repeat;
text-indent: -1000px;
overflow: hidden;
}
.taskitem .childtoggle.collapsed {
background-position: -18px -81px;
}
.taskhead {
position: relative;
margin-left: 14px;
padding: 4px 5px 3px 5px;
border: 1px solid #fff;
border-radius: 5px;
background: #fff;
-webkit-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.4);
-moz-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.4);
box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.4);
padding-right: 26em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
outline: none;
}
.taskhead:focus,
.taskhead.droptarget {
border-color: #4787b1;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
.taskhead .complete {
margin: -1px 1em 0 0;
}
.taskhead .title {
font-size: 12px;
}
.taskhead .flagged,
.taskshow.status-flagged h2:after {
display: inline-block;
width: 16px;
height: 16px;
background: url(images/sprites.png) 1000px -3px no-repeat;
margin: -3px 1em 0 0;
vertical-align: middle;
cursor: pointer;
}
.taskhead .flagged:focus,
.taskhead:hover .flagged {
background-position: -2px -3px;
}
.taskhead.flagged .flagged,
.taskshow.status-flagged h2:after {
background-position: -2px -23px;
}
.taskhead .tags {
display: block;
position: absolute;
top: 3px;
right: 10em;
max-width: 14em;
height: 16px;
overflow: hidden;
padding-top: 1px;
text-align: right;
}
.tag-draghelper .tag,
.taskhead .tags .tag {
font-size: 85%;
background: #d9ecf4;
border: 1px solid #c2dae5;
border-radius: 4px;
padding: 1px 7px;
margin-right: 3px;
}
.tag-draghelper li.tag {
list-style: none;
font-size: 100%;
}
.taskhead .date {
position: absolute;
top: 4px;
right: 30px;
text-align: right;
cursor: pointer;
}
.taskhead.nodate .date {
color: #ddd;
}
.taskhead.overdue .date {
color: #d00;
}
.taskhead.nodate:hover .date {
color: #999;
}
.taskhead .date input {
padding: 1px 2px;
border: 1px solid #ddd;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
outline: none;
text-align: right;
width: 6em;
font-size: 11px;
}
.taskhead .actions {
display: block;
position: absolute;
top: 3px;
right: 6px;
width: 18px;
height: 18px;
background: url(images/sprites.png) 1000px -80px no-repeat;
text-indent: -5000px;
overflow: hidden;
cursor: pointer;
}
.taskhead .actions:focus,
.taskhead:hover .actions {
background-position: 0 -80px;
}
.taskhead.complete {
opacity: 0.6;
}
.taskhead.complete .title {
text-decoration: line-through;
}
.taskhead .progressbar {
position: absolute;
bottom: 1px;
left: 6px;
right: 6px;
height: 2px;
}
.taskhead.complete .progressbar {
display: none;
}
.taskhead .progressvalue {
height: 1px;
background: rgba(1, 124, 180, 0.2);
border-top: 1px solid #219de6;
}
ul.toolbarmenu li span.add,
ul.toolbarmenu li span.expand,
ul.toolbarmenu li span.collapse,
ul.toolbarmenu li span.history,
ul.toolbarmenu.iconized .selected span.icon {
background-image: url(images/sprites.png);
}
ul.toolbarmenu li span.add {
background-position: 0 -302px;
}
ul.toolbarmenu li span.expand {
background-position: 0 -258px;
}
ul.toolbarmenu li span.collapse {
background-position: 0 -280px;
}
ul.toolbarmenu li span.delete {
background-position: 0 -1508px;
}
ul.toolbarmenu li span.history {
background-position: 0 -408px;
}
ul.toolbarmenu.iconized .selected span.icon {
background-position: 0 -324px;
}
ul.toolbarmenu .sortcol.by-auto a {
font-style: italic;
}
.taskitem-draghelper {
/*
width: 32px;
height: 26px;
*/
background: #444;
border: 1px solid #555;
border-radius: 4px;
box-shadow: 0 2px 6px 0 #333;
-moz-box-shadow: 0 2px 6px 0 #333;
-webkit-box-shadow: 0 2px 6px 0 #333;
-o-box-shadow: 0 2px 6px 0 #333;
z-index: 5000;
padding: 2px 10px;
font-size: 20px;
color: #ccc;
opacity: 0.92;
filter: alpha(opacity=90);
text-shadow: 0px 1px 1px #333;
}
#rootdroppable {
display: none;
position: absolute;
top: 2px;
left: 1em;
right: 1em;
height: 5px;
background: #ddd;
border-radius: 3px;
}
#rootdroppable.droptarget {
background: #4787b1;
box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
}
/*** task edit form ***/
#taskedit,
#taskshow,
#taskdiff {
display: none;
}
#taskedit {
position: relative;
top: -1.5em;
padding: 0.5em 0.1em;
margin: 0 -0.2em;
}
.taskshow h2 {
margin-top: -0.5em;
}
#taskdiff h2 {
font-size: 18px;
margin: -0.3em 0 0.4em 0;
}
.taskshow.status-completed h2 {
text-decoration: line-through;
}
.taskshow.status-flagged h2:after {
content: " ";
position: relative;
margin-left: 0.6em;
top: 1px;
cursor: default;
}
.taskshow label {
color: #999;
}
.taskshow.status-cancelled {
background: url(images/badge_cancelled.png) top right no-repeat;
}
.task-parent-title {
position: relative;
top: -0.6em;
}
a.morelink {
font-size: 90%;
color: #0069a6;
text-decoration: none;
outline: none;
}
a.morelink:hover {
text-decoration: underline;
}
#taskedit .ui-tabs-panel {
min-height: 24em;
}
#taskeditform input.text,
#taskeditform textarea {
width: 97%;
}
#taskeditform .buttons {
margin: 0.5em 0;
}
#taskedit .border-after {
padding-bottom: 0.8em;
margin-bottom: 0.8em;
border-bottom: 2px solid #fafafa;
}
#taskedit .edit-attendees-table {
width: 100%;
margin-top: 0.5em;
}
#taskedit .edit-attendees-table tbody td {
padding: 5px 7px 6px;
}
#taskedit .edit-attendees-table tbody tr:last-child td {
border-bottom: 0;
}
#taskedit .edit-attendees-table th.role,
#taskedit .edit-attendees-table td.role {
width: 9em;
}
#taskedit .edit-attendees-table th.availability,
#taskedit .edit-attendees-table td.availability,
#taskedit .edit-attendees-table th.confirmstate,
#taskedit .edit-attendees-table td.confirmstate {
width: 6em;
}
#taskedit .edit-attendees-table th.options,
#taskedit .edit-attendees-table td.options {
width: 24px;
padding: 2px 4px;
text-align: right;
}
#taskedit .edit-attendees-table th.invite,
#taskedit .edit-attendees-table td.invite {
width: 50px;
padding: 2px;
}
#taskedit .edit-attendees-table th.invite label {
display: inline-block;
position: relative;
top: 4px;
width: 24px;
height: 18px;
min-width: 24px;
padding: 0;
overflow: hidden;
text-indent: -5000px;
white-space: nowrap;
background: url(images/sendinvitation.png) 1px 0 no-repeat;
}
#taskedit .edit-attendees-table th.name,
#taskedit .edit-attendees-table td.name {
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#taskedit .edit-attendees-table td.name .attendee-name {
display: block;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 7px 6px 4px;
margin: -5px -7px -6px;
}
#taskedit .edit-attendees-table td.name select {
width: 100%;
}
#taskedit .edit-attendees-table a.deletelink {
display: inline-block;
width: 17px;
height: 17px;
padding: 0;
overflow: hidden;
text-indent: 1000px;
}
#taskedit .edit-attendees-table a.expandlink {
position: absolute;
top: 4px;
right: 6px;
width: 16px;
height: 16px;
}
#edit-attendees-form {
position: relative;
margin-top: 15px;
}
#edit-attendees-form .attendees-invitebox {
text-align: right;
margin: 0;
}
#edit-attendees-form .attendees-invitebox label {
padding-right: 3px;
}
#taskedit-attachments {
margin: 0.6em 0;
}
#taskedit-attachments ul li {
display: block;
color: #333;
font-weight: bold;
padding: 2px 4px 2px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
line-height: 20px;
}
#taskedit-attachments ul li a.filename {
padding: 0;
}
#taskedit-attachments-form {
margin-top: 1em;
padding-top: 0.8em;
border-top: 2px solid #fafafa;
}
div.form-section {
position: relative;
margin-top: 0.2em;
margin-bottom: 0.5em;
}
.form-section label {
display: inline-block;
min-width: 7em;
padding-right: 0.5em;
margin-bottom: 0.3em;
}
.tasklistview div.form-section span.task-text + label {
margin-left: 2em;
}
label.block {
display: block;
margin-bottom: 0.3em;
}
.task-description {
margin-bottom: 1em;
}
.taskshow .task-text-old,
.taskshow .task-text-new,
.taskshow .task-text-diff {
padding: 2px;
}
.taskshow .task-text-diff del,
.taskshow .task-text-diff ins {
text-decoration: none;
color: inherit;
}
.taskshow .task-text-old,
.taskshow .task-text-diff del {
background-color: #fdd;
/* text-decoration: line-through; */
}
.taskshow .task-text-new,
.taskshow .task-text-diff ins {
background-color: #dfd;
}
.tasklistview .taskshow label span.index {
vertical-align: inherit;
margin-left: 0.6em;
}
#taskedit-completeness-slider {
display: inline-block;
margin-left: 2em;
width: 30em;
height: 0.8em;
border: 1px solid #ccc;
}
#taskedit-tagline {
width: 97%;
}
#taskedit .droptarget {
background-image: url(../../../../skins/larry/images/filedrop.png) !important;
background-position: center bottom !important;
background-repeat: no-repeat !important;
}
#taskedit .droptarget.hover,
#taskedit .droptarget.active {
border-color: #019bc6;
box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-moz-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-webkit-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-o-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
}
#taskedit .droptarget.hover {
background-color: #d9ecf4;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
#task-links {
margin-top: 0;
margin-bottom: 0.2em;
}
#task-links label {
vertical-align: top;
margin-top: 0.3em;
}
#task-links .attachmentslist {
display: inline-block;
}
#task-links .attachmentslist li {
display: inline-block;
margin-right: 1em;
}
#taskedit-links .attachmentslist li.message.eml,
#task-links .attachmentslist li.message.eml {
background-image: url(images/sprites.png);
background-position: -2px -388px;
}
#taskedit-links .attachmentslist li.message a.messagelink,
#task-links .attachmentslist li.message a.messagelink {
padding: 0 0 0 24px;
}
#taskedit-links .attachmentslist li.deleted a.messagelink,
#taskedit-links .attachmentslist li.deleted a.messagelink:hover {
text-decoration: line-through;
}
#taskedit-links label {
float: left;
margin-top: 0.3em;
}
#taskedit-links .task-text {
margin-left: 8em;
min-height: 22px;
}
#taskedit-links .attachmentslist li a.delete {
top: 0;
background-position: -6px -378px;
}
#task-attachments .attachmentslist li {
float: left;
margin-right: 1em;
}
#task-attachments .attachmentslist li a {
outline: none;
}
.task-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
background: url(images/attendee-status.png) right 0 no-repeat;
}
.task-attendees span.attendee a.mailtolink {
text-decoration: none;
white-space: nowrap;
outline: none;
}
.task-attendees span.attendee a.mailtolink:hover {
text-decoration: underline;
}
.task-attendees span.completed {
background-position: right -20px;
}
.task-attendees span.declined {
background-position: right -40px;
}
.task-attendees span.tentative {
background-position: right -60px;
}
.task-attendees span.delegated {
background-position: right -180px;
}
.task-attendees span.in-process {
background-position: right -200px;
}
.task-attendees span.accepted {
background-position: right -220px;
}
.task-attendees span.organizer {
background-position: right 100px;
}
#all-task-attendees span.attendee {
display: block;
margin-bottom: 0.4em;
padding-bottom: 0.3em;
border-bottom: 1px solid #ddd;
}
.tasklistview .uidialog .tabbed {
min-width: 600px;
}
.tasklistview .uidialog .propform fieldset.ui-tabs-panel {
min-height: 290px;
}
.tasklistview .uidialog .propform #taskedit-tasklistname {
width: 20em;
}
.task-dialog-message {
margin-top: 0.5em;
padding: 0.8em;
border: 1px solid #ffdf0e;
background-color: #fef893;
}
.task-dialog-message .message,
.task-update-confirm .message {
margin-bottom: 0.5em;
}
/* Invitation UI in mail */
.messagelist tbody .attachment span.ical {
display: inline-block;
vertical-align: middle;
height: 18px;
width: 20px;
padding: 0;
background: url(images/ical-attachment.png) 2px 1px no-repeat;
}
div.tasklist-invitebox {
min-height: 20px;
margin: 5px 8px;
padding: 3px 6px 6px 34px;
border: 1px solid #ffdf0e;
background: url(images/tasklist.png) 6px 5px no-repeat #fef893;
}
div.tasklist-invitebox td {
padding: 2px;
}
div.tasklist-invitebox td.ititle {
font-weight: bold;
padding-right: 0.5em;
}
div.tasklist-invitebox td.label {
color: #666;
padding-right: 1em;
}
#task-rsvp .rsvp-buttons,
#task-rsvp .itip-reply-controls,
div.tasklist-invitebox .itip-buttons div {
margin-top: 0.5em;
}
#task-rsvp .itip-reply-controls a,
#task-rsvp .itip-reply-controls label {
color: #333;
}
#task-rsvp input.button,
div.tasklist-invitebox input.button {
font-weight: bold;
margin-right: 0.5em;
}
div.tasklist-invitebox .folder-select {
font-weight: 10px;
margin-left: 1em;
}
div.tasklist-invitebox .rsvp-status {
padding-left: 2px;
}
div.tasklist-invitebox .rsvp-status.loading {
color: #666;
padding: 1px 0 2px 24px;
background: url(images/loading_blue.gif) top left no-repeat;
}
div.tasklist-invitebox .rsvp-status.hint {
color: #666;
text-shadow: none;
font-style: italic;
}
#task-partstat .changersvp,
.tasklistview .edit-attendees-table td.confirmstate span,
div.tasklist-invitebox .rsvp-status.declined,
div.tasklist-invitebox .rsvp-status.tentative,
div.tasklist-invitebox .rsvp-status.accepted,
div.tasklist-invitebox .rsvp-status.delegated,
div.tasklist-invitebox .rsvp-status.in-process,
div.tasklist-invitebox .rsvp-status.completed,
div.tasklist-invitebox .rsvp-status.needs-action {
padding: 0 0 1px 22px;
background: url(images/attendee-status.png) 2px -20px no-repeat;
}
#task-partstat .changersvp.declined,
div.tasklist-invitebox .rsvp-status.declined,
.tasklistview .edit-attendees-table td.confirmstate span.declined {
background-position: 2px -40px;
}
#task-partstat .changersvp.tentative,
div.tasklist-invitebox .rsvp-status.tentative,
.tasklistview .edit-attendees-table td.confirmstate span.tentative {
background-position: 2px -60px;
}
#task-partstat .changersvp.delegated,
div.tasklist-invitebox .rsvp-status.delegated,
.tasklistview .edit-attendees-table td.confirmstate span.delegated {
background-position: 2px -180px;
}
#task-partstat .changersvp.needs-action,
div.tasklist-invitebox .rsvp-status.needs-action,
.tasklistview .edit-attendees-table td.confirmstate span.needs-action {
background-position: 2px 0;
}
#task-partstat .changersvp.in-process,
div.tasklist-invitebox .rsvp-status.in-process,
.tasklistview .edit-attendees-table td.confirmstate span.in-process {
background-position: 2px -200px;
}
#task-partstat .changersvp.accepted,
div.tasklist-invitebox .rsvp-status.accepted,
.tasklistview .edit-attendees-table td.confirmstate span.accepted {
background-position: 2px -220px;
}
div.messagetasklinks {
position: relative;
margin: 8px 8px;
padding: 4px 8px 4px 30px;
border: 1px solid #dfdfdf;
background: #fafafa;
border-radius: 4px;
}
div.messagetasklinks::before {
content: " ";
position: absolute;
top: 4px;
left: 8px;
width: 18px;
height: 18px;
background: url(images/buttons.png) -6px -115px no-repeat;
}
div.messagetasklinks ul.tasklist {
margin: 0;
padding: 0;
list-style: none;
}
div.messagetasklinks .messagetaskref {
display: block;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div.messagetasklinks a.messagetasklink {
position: relative;
display: inline-block;
color: #333;
font-weight: bold;
padding: 3px 0 2px 2px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
vertical-align: middle;
}
div.messagetasklinks .messagetaskref.complete a.messagetasklink {
text-decoration: line-through;
text-shadow: none;
color: #666;
}
div.messagetasklinks .messagetaskref input.complete {
vertical-align: middle;
}
-
-.tasklist.attachmentwin #attachmenttoolbar {
- position: relative;
- top: -6px;
- height: 40px;
-}
-
-.tasklist.attachmentwin #attachmentcontainer {
- position: absolute;
- top: 0;
- left: 232px;
- right: 0;
- bottom: 0;
-}
-
-.tasklist.attachmentwin #attachmentframe {
- width: 100%;
- height: 100%;
- border: 0;
- background-color: #fff;
- border-radius: 4px;
-}
-
-.tasklist.attachmentwin #partheader {
- position: absolute;
- top: 0;
- left: 0;
- width: 220px;
- bottom: 0;
-}
-
-.tasklist.attachmentwin #partheader table {
- table-layout: fixed;
- overflow: hidden;
-}
-
-.tasklist.attachmentwin #partheader table td {
- color: #666;
- padding: 4px 6px;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-
-.tasklist.attachmentwin #partheader table td.header {
- font-weight: bold;
-}
-
-.tasklist.attachmentwin #partheader table td.title {
- width: 60px;
- padding-right: 0;
-}
-
.form-section label + div {
line-height: 2;
vertical-align: top;
}
.form-section label + div {
display: inline-block;
}
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index ad0ec875..41d93948 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1,2548 +1,2546 @@
*
* Copyright (C) 2012, Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
class tasklist extends rcube_plugin
{
const FILTER_MASK_TODAY = 1;
const FILTER_MASK_TOMORROW = 2;
const FILTER_MASK_WEEK = 4;
const FILTER_MASK_LATER = 8;
const FILTER_MASK_NODATE = 16;
const FILTER_MASK_OVERDUE = 32;
const FILTER_MASK_FLAGGED = 64;
const FILTER_MASK_COMPLETE = 128;
const FILTER_MASK_ASSIGNED = 256;
const FILTER_MASK_MYTASKS = 512;
const SESSION_KEY = 'tasklist_temp';
public static $filter_masks = array(
'today' => self::FILTER_MASK_TODAY,
'tomorrow' => self::FILTER_MASK_TOMORROW,
'week' => self::FILTER_MASK_WEEK,
'later' => self::FILTER_MASK_LATER,
'nodate' => self::FILTER_MASK_NODATE,
'overdue' => self::FILTER_MASK_OVERDUE,
'flagged' => self::FILTER_MASK_FLAGGED,
'complete' => self::FILTER_MASK_COMPLETE,
'assigned' => self::FILTER_MASK_ASSIGNED,
'mytasks' => self::FILTER_MASK_MYTASKS,
);
public $task = '?(?!login|logout).*';
public $allowed_prefs = array('tasklist_sort_col','tasklist_sort_order');
public $rc;
public $lib;
public $timezone;
public $ui;
public $home; // declare public to be used in other classes
// These are handled by __get()
// public $driver;
// public $itip;
// public $ical;
private $collapsed_tasks = array();
private $message_tasks = array();
/**
* Plugin initialization.
*/
function init()
{
$this->require_plugin('libcalendaring');
$this->require_plugin('libkolab');
$this->require_plugin('jqueryui');
$this->rc = rcube::get_instance();
$this->lib = libcalendaring::get_instance();
$this->register_task('tasks', 'tasklist');
// load plugin configuration
$this->load_config();
$this->timezone = $this->lib->timezone;
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('user_delete', array($this, 'user_delete'));
}
/**
* Startup hook
*/
public function startup($args)
{
// the tasks module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('tasklist_disabled', false) || !$this->rc->config->get('tasklist_enabled', true))
return;
// load localizations
$this->add_texts('localization/', $args['task'] == 'tasks' && (!$args['action'] || $args['action'] == 'print'));
$this->rc->load_language($_SESSION['language'], array('tasks.tasks' => $this->gettext('navtitle'))); // add label for task title
if ($args['task'] == 'tasks' && $args['action'] != 'save-pref') {
$this->load_driver();
// register calendar actions
$this->register_action('index', array($this, 'tasklist_view'));
$this->register_action('task', array($this, 'task_action'));
$this->register_action('tasklist', array($this, 'tasklist_action'));
$this->register_action('counts', array($this, 'fetch_counts'));
$this->register_action('fetch', array($this, 'fetch_tasks'));
$this->register_action('print', array($this, 'print_tasks'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('mail2task', array($this, 'mail_message2task'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->register_action('import', array($this, 'import_tasks'));
$this->register_action('export', array($this, 'export_tasks'));
$this->register_action('mailimportitip', array($this, 'mail_import_itip'));
$this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
$this->register_action('itip-status', array($this, 'task_itip_status'));
$this->register_action('itip-remove', array($this, 'task_itip_remove'));
$this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
$this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
$this->add_hook('refresh', array($this, 'refresh'));
$this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
}
else if ($args['task'] == 'mail') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
if ($this->rc->config->get('tasklist_mail_embed', true)) {
$this->add_hook('message_load', array($this, 'mail_message_load'));
}
$this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'tasklist-create-from-mail',
'label' => 'tasklist.createfrommail',
'type' => 'link',
'classact' => 'icon taskaddlink active',
'class' => 'icon taskaddlink',
'innerclass' => 'icon taskadd',
))),
'messagemenu');
$this->api->output->add_label('tasklist.createfrommail');
}
}
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->load_ui();
$this->ui->init();
}
// add hooks for alarms handling
$this->add_hook('pending_alarms', array($this, 'pending_alarms'));
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
/**
*
*/
private function load_ui()
{
if (!$this->ui) {
require_once($this->home . '/tasklist_ui.php');
$this->ui = new tasklist_ui($this);
}
}
/**
* Helper method to load the backend driver according to local config
*/
private function load_driver()
{
if (is_object($this->driver)) {
return;
}
$driver_name = $this->rc->config->get('tasklist_driver', 'database');
$driver_class = 'tasklist_' . $driver_name . '_driver';
require_once($this->home . '/drivers/tasklist_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
$this->driver = new $driver_class($this);
$this->rc->output->set_env('tasklist_driver', $driver_name);
}
/**
* Dispatcher for task-related actions initiated by the client
*/
public function task_action()
{
$filter = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$rec = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true);
$oldrec = $rec;
$success = $refresh = $got_msg = false;
// force notify if hidden + active
$itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3);
if ($itip_send_option === 1 && empty($rec['_reportpartstat']))
$rec['_notify'] = 1;
switch ($action) {
case 'new':
$oldrec = null;
$rec = $this->prepare_task($rec);
$rec['uid'] = $this->generate_uid();
$temp_id = $rec['tempid'];
if ($success = $this->driver->create_task($rec)) {
$refresh = $this->driver->get_task($rec);
if ($temp_id) $refresh['tempid'] = $temp_id;
$this->cleanup_task($rec);
}
break;
case 'complete':
$complete = intval(rcube_utils::get_input_value('complete', rcube_utils::INPUT_POST));
if (!($rec = $this->driver->get_task($rec))) {
break;
}
$oldrec = $rec;
$rec['status'] = $complete ? 'COMPLETED' : ($rec['complete'] > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION');
// sent itip notifications if enabled (no user interaction here)
if (($itip_send_option & 1)) {
if ($this->is_attendee($rec)) {
$rec['_reportpartstat'] = $rec['status'];
}
else if ($this->is_organizer($rec)) {
$rec['_notify'] = 1;
}
}
case 'edit':
$oldrec = $this->driver->get_task($rec);
$rec = $this->prepare_task($rec);
$clone = $this->handle_recurrence($rec, $this->driver->get_task($rec));
if ($success = $this->driver->edit_task($rec)) {
$new_task = $this->driver->get_task($rec);
$new_task['tempid'] = $rec['id'];
$refresh[] = $new_task;
$this->cleanup_task($rec);
// add clone from recurring task
if ($clone && $this->driver->create_task($clone)) {
$new_clone = $this->driver->get_task($clone);
$new_clone['tempid'] = $clone['id'];
$refresh[] = $new_clone;
$this->driver->clear_alarms($rec['id']);
}
// move all childs if list assignment was changed
if (!empty($rec['_fromlist']) && !empty($rec['list']) && $rec['_fromlist'] != $rec['list']) {
foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist']), true) as $cid) {
$child = array('id' => $cid, 'list' => $rec['list'], '_fromlist' => $rec['_fromlist']);
if ($this->driver->move_task($child)) {
$r = $this->driver->get_task($child);
if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) {
$r['tempid'] = $cid;
$refresh[] = $r;
}
}
}
}
}
break;
case 'move':
foreach ((array)$rec['id'] as $id) {
$r = $rec;
$r['id'] = $id;
if ($this->driver->move_task($r)) {
$new_task = $this->driver->get_task($r);
$new_task['tempid'] = $id;
$refresh[] = $new_task;
$success = true;
// move all childs, too
foreach ($this->driver->get_childs(array('id' => $id, 'list' => $rec['_fromlist']), true) as $cid) {
$child = $rec;
$child['id'] = $cid;
if ($this->driver->move_task($child)) {
$r = $this->driver->get_task($child);
if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) {
$r['tempid'] = $cid;
$refresh[] = $r;
}
}
}
}
}
break;
case 'delete':
$mode = intval(rcube_utils::get_input_value('mode', rcube_utils::INPUT_POST));
$oldrec = $this->driver->get_task($rec);
if ($success = $this->driver->delete_task($rec, false)) {
// delete/modify all childs
foreach ($this->driver->get_childs($rec, $mode) as $cid) {
$child = array('id' => $cid, 'list' => $rec['list']);
if ($mode == 1) { // delete all childs
if ($this->driver->delete_task($child, false)) {
if ($this->driver->undelete)
$_SESSION['tasklist_undelete'][$rec['id']][] = $cid;
}
else
$success = false;
}
else {
$child['parent_id'] = strval($oldrec['parent_id']);
$this->driver->edit_task($child);
}
}
// update parent task to adjust list of children
if (!empty($oldrec['parent_id'])) {
$parent = array('id' => $oldrec['parent_id'], 'list' => $rec['list']);
if ($parent = $this->driver->get_task()) {
$refresh[] = $parent;
}
}
}
if (!$success)
$this->rc->output->command('plugin.reload_data');
break;
case 'undelete':
if ($success = $this->driver->undelete_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
foreach ((array)$_SESSION['tasklist_undelete'][$rec['id']] as $cid) {
if ($this->driver->undelete_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
}
}
}
break;
case 'collapse':
foreach (explode(',', $rec['id']) as $rec_id) {
if (intval(rcube_utils::get_input_value('collapsed', rcube_utils::INPUT_GPC))) {
$this->collapsed_tasks[] = $rec_id;
}
else {
$i = array_search($rec_id, $this->collapsed_tasks);
if ($i !== false)
unset($this->collapsed_tasks[$i]);
}
}
$this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks))));
return; // avoid further actions
case 'rsvp':
$status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC);
$noreply = intval(rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC)) || $status == 'needs-action';
$task = $this->driver->get_task($rec);
$task['attendees'] = $rec['attendees'];
$task['_type'] = 'task';
// send invitation to delegatee + add it as attendee
if ($status == 'delegated' && $rec['to']) {
$itip = $this->load_itip();
if ($itip->delegate_to($task, $rec['to'], (bool)$rec['rsvp'])) {
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
$refresh[] = $task;
$noreply = false;
}
}
$rec = $task;
if ($success = $this->driver->edit_task($rec)) {
if (!$noreply) {
// let the reply clause further down send the iTip message
$rec['_reportpartstat'] = $status;
}
}
break;
case 'changelog':
$data = $this->driver->get_task_changelog($rec);
if (is_array($data) && !empty($data)) {
$lib = $this->lib;
$dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
array_walk($data, function(&$change) use ($lib, $dtformat) {
if ($change['date']) {
$dt = $lib->adjust_timezone($change['date']);
if ($dt instanceof DateTime) {
$change['date'] = $this->rc->format_date($dt, $dtformat, false);
}
}
});
$this->rc->output->command('plugin.task_render_changelog', $data);
}
else {
$this->rc->output->command('plugin.task_render_changelog', false);
}
$got_msg = true;
break;
case 'diff':
$data = $this->driver->get_task_diff($rec, $rec['rev1'], $rec['rev2']);
if (is_array($data)) {
// convert some properties, similar to self::_client_event()
$lib = $this->lib;
$date_format = $this->rc->config->get('date_format', 'Y-m-d');
$time_format = $this->rc->config->get('time_format', 'H:i');
array_walk($data['changes'], function(&$change, $i) use ($lib, $date_format, $time_format) {
// convert date cols
if (in_array($change['property'], array('date','start','created','changed'))) {
if (!empty($change['old'])) {
$dtformat = strlen($change['old']) == 10 ? $date_format : $date_format . ' ' . $time_format;
$change['old_'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format($dtformat);
}
if (!empty($change['new'])) {
$dtformat = strlen($change['new']) == 10 ? $date_format : $date_format . ' ' . $time_format;
$change['new_'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format($dtformat);
}
}
// create textual representation for alarms and recurrence
if ($change['property'] == 'alarms') {
if (is_array($change['old']))
$change['old_'] = libcalendaring::alarm_text($change['old']);
if (is_array($change['new']))
$change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
}
if ($change['property'] == 'recurrence') {
if (is_array($change['old']))
$change['old_'] = $lib->recurrence_text($change['old']);
if (is_array($change['new']))
$change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
}
if ($change['property'] == 'complete') {
$change['old_'] = intval($change['old']) . '%';
$change['new_'] = intval($change['new']) . '%';
}
if ($change['property'] == 'attachments') {
if (is_array($change['old']))
$change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
if (is_array($change['new'])) {
$change['new'] = array_merge((array)$change['old'], $change['new']);
$change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
}
}
// resolve parent_id to the refered task title for display
if ($change['property'] == 'parent_id') {
$change['property'] = 'parent-title';
if (!empty($change['old']) && ($old_parent = $this->driver->get_task(array('id' => $change['old'], 'list' => $rec['list'])))) {
$change['old_'] = $old_parent['title'];
}
if (!empty($change['new']) && ($new_parent = $this->driver->get_task(array('id' => $change['new'], 'list' => $rec['list'])))) {
$change['new_'] = $new_parent['title'];
}
}
// compute a nice diff of description texts
if ($change['property'] == 'description') {
$change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
}
});
$this->rc->output->command('plugin.task_show_diff', $data);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
}
$got_msg = true;
break;
case 'show':
if ($rec = $this->driver->get_task_revison($rec, $rec['rev'])) {
$this->encode_task($rec);
$rec['readonly'] = 1;
$this->rc->output->command('plugin.task_show_revision', $rec);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
}
$got_msg = true;
break;
case 'restore':
if ($success = $this->driver->restore_task_revision($rec, $rec['rev'])) {
$refresh = $this->driver->get_task($rec);
$this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rec['rev']))), 'confirmation');
$this->rc->output->command('plugin.close_history_dialog');
}
else {
$this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
}
$got_msg = true;
break;
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
$this->update_counts($oldrec, $refresh);
}
else if (!$got_msg) {
$this->rc->output->show_message('tasklist.errorsaving', 'error');
}
// send out notifications
if ($success && $rec['_notify'] && ($rec['attendees'] || $oldrec['attendees'])) {
// make sure we have the complete record
$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($task, $oldrec)) {
$sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']);
if ($sent > 0)
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
else if ($sent < 0)
$this->rc->output->show_message('tasklist.errornotifying', 'error');
}
}
if ($success && $rec['_reportpartstat'] && $rec['_reportpartstat'] != 'NEEDS-ACTION') {
// get the full record after update
if (!$task) {
$task = $this->driver->get_task($rec);
}
// send iTip REPLY with the updated partstat
if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) {
$sender = $task['attendees'][$idx];
$status = strtolower($sender['status']);
if (!empty($_POST['comment']))
$task['comment'] = rcube_utils::get_input_value('comment', rcube_utils::INPUT_POST);
$itip = $this->load_itip();
$itip->set_sender_email($sender['email']);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
if ($refresh['id']) {
$this->encode_task($refresh);
}
else if (is_array($refresh)) {
foreach ($refresh as $i => $r)
$this->encode_task($refresh[$i]);
}
$this->rc->output->command('plugin.update_task', $refresh);
}
else if ($success && ($action == 'delete' || $action == 'undelete')) {
$this->rc->output->command('plugin.refresh_tagcloud');
}
}
/**
* Load iTIP functions
*/
private function load_itip()
{
if (!$this->itip) {
require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
$this->itip = new libcalendaring_itip($this, 'tasklist');
$this->itip->set_rsvp_actions(array('accepted','declined','delegated'));
$this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
}
return $this->itip;
}
/**
* repares new/edited task properties before save
*/
private function prepare_task($rec)
{
// try to be smart and extract date from raw input
if ($rec['raw']) {
foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
$locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
$normwords[] = $word;
$datewords[] = $word;
}
foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
$locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
$normwords[] = $month;
$datewords[] = $month;
}
foreach (array('on','this','next','at') as $word) {
$fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
$fillwords[] = $word;
}
$raw = trim($rec['raw']);
$date_str = '';
// translate localized keywords
$raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
$raw = preg_replace($locwords, $normwords, $raw);
// find date pattern
$date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
if (preg_match($date_pattern, $raw, $m)) {
$date_str .= $m[1] . $m[2] . $m[3];
$raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
// add year to date string
if ($m[1] && !$m[3])
$date_str .= date('Y');
}
// find time pattern
$time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
if (preg_match($time_pattern, $raw, $m)) {
$has_time = true;
$date_str .= ($date_str ? ' ' : 'today ') . $m[1];
$raw = preg_replace($time_pattern, '', $raw);
}
// yes, raw input matched a (valid) date
if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
$rec['date'] = $date->format('Y-m-d');
if ($has_time)
$rec['time'] = $date->format('H:i');
$rec['title'] = $raw;
}
else
$rec['title'] = $rec['raw'];
}
// normalize input from client
if (isset($rec['complete'])) {
$rec['complete'] = floatval($rec['complete']);
if ($rec['complete'] > 1)
$rec['complete'] /= 100;
}
if (isset($rec['flagged']))
$rec['flagged'] = intval($rec['flagged']);
// fix for garbage input
if ($rec['description'] == 'null')
$rec['description'] = '';
foreach ($rec as $key => $val) {
if ($val === 'null')
$rec[$key] = null;
}
if (!empty($rec['date'])) {
$this->normalize_dates($rec, 'date', 'time');
}
if (!empty($rec['startdate'])) {
$this->normalize_dates($rec, 'startdate', 'starttime');
}
// convert tags to array, filter out empty entries
if (isset($rec['tags']) && !is_array($rec['tags'])) {
$rec['tags'] = array_filter((array)$rec['tags']);
}
// convert the submitted alarm values
if ($rec['valarms']) {
$valarms = array();
foreach (libcalendaring::from_client_alarms($rec['valarms']) as $alarm) {
// alarms can only work with a date (either task start, due or absolute alarm date)
if (is_a($alarm['trigger'], 'DateTime') || $rec['date'] || $rec['startdate'])
$valarms[] = $alarm;
}
$rec['valarms'] = $valarms;
}
// convert the submitted recurrence settings
if (is_array($rec['recurrence'])) {
$refdate = null;
if (!empty($rec['date'])) {
$refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
}
else if (!empty($rec['startdate'])) {
$refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
}
if ($refdate) {
$rec['recurrence'] = $this->lib->from_client_recurrence($rec['recurrence'], $refdate);
// translate count into an absolute end date.
// why? because when shifting completed tasks to the next recurrence,
// the initial start date to count from gets lost.
if ($rec['recurrence']['COUNT']) {
$engine = libcalendaring::get_recurrence();
$engine->init($rec['recurrence'], $refdate);
if ($until = $engine->end()) {
$rec['recurrence']['UNTIL'] = $until;
unset($rec['recurrence']['COUNT']);
}
}
}
else { // recurrence requires a reference date
$rec['recurrence'] = '';
}
}
$attachments = array();
$taskid = $rec['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
unset($attachments[$id]['abort'], $attachments[$id]['group']);
}
}
}
}
$rec['attachments'] = $attachments;
// convert link references into simple URIs
if (array_key_exists('links', $rec)) {
$rec['links'] = array_map(function($link) { return is_array($link) ? $link['uri'] : strval($link); }, (array)$rec['links']);
}
// convert invalid data
if (isset($rec['attendees']) && !is_array($rec['attendees']))
$rec['attendees'] = array();
foreach ((array)$rec['attendees'] as $i => $attendee) {
if (is_string($attendee['rsvp'])) {
$rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
}
}
// copy the task status to my attendee partstat
if (!empty($rec['_reportpartstat'])) {
if (($idx = $this->is_attendee($rec)) !== false) {
if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED'))
$rec['attendees'][$idx]['status'] = $rec['_reportpartstat'];
else
unset($rec['_reportpartstat']);
}
}
// set organizer from identity selector
if ((isset($rec['_identity']) || (!empty($rec['attendees']) && empty($rec['organizer']))) &&
($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']);
return $rec;
}
/**
* Utility method to convert a tasks date/time values into a normalized format
*/
private function normalize_dates(&$rec, $date_key, $time_key)
{
try {
// parse date from user format (#2801)
$date_format = $this->rc->config->get(empty($rec[$time_key]) ? 'date_format' : 'date_long', 'Y-m-d');
$date = DateTime::createFromFormat($date_format, trim($rec[$date_key] . ' ' . $rec[$time_key]), $this->timezone);
// fall back to default strtotime logic
if (empty($date)) {
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
}
$rec[$date_key] = $date->format('Y-m-d');
if (!empty($rec[$time_key]))
$rec[$time_key] = $date->format('H:i');
return true;
}
catch (Exception $e) {
$rec[$date_key] = $rec[$time_key] = null;
}
return false;
}
/**
* Releases some resources after successful save
*/
private function cleanup_task(&$rec)
{
// remove temp. attachment files
if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid));
$this->rc->session->remove(self::SESSION_KEY);
}
}
/**
* When flagging a recurring task as complete,
* clone it and shift dates to the next occurrence
*/
private function handle_recurrence(&$rec, $old)
{
$clone = null;
if ($this->driver->is_complete($rec) && $old && !$this->driver->is_complete($old) && is_array($rec['recurrence'])) {
$engine = libcalendaring::get_recurrence();
$rrule = $rec['recurrence'];
$updates = array();
// compute the next occurrence of date attributes
foreach (array('date'=>'time', 'startdate'=>'starttime') as $date_key => $time_key) {
if (empty($rec[$date_key]))
continue;
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
$engine->init($rrule, $date);
if ($next = $engine->next()) {
$updates[$date_key] = $next->format('Y-m-d');
if (!empty($rec[$time_key]))
$updates[$time_key] = $next->format('H:i');
}
}
// shift absolute alarm dates
if (!empty($updates) && is_array($rec['valarms'])) {
$updates['valarms'] = array();
unset($rrule['UNTIL'], $rrule['COUNT']); // make recurrence rule unlimited
foreach ($rec['valarms'] as $i => $alarm) {
if ($alarm['trigger'] instanceof DateTime) {
$engine->init($rrule, $alarm['trigger']);
if ($next = $engine->next()) {
$alarm['trigger'] = $next;
}
}
$updates['valarms'][$i] = $alarm;
}
}
if (!empty($updates)) {
// clone task to save a completed copy
$clone = $rec;
$clone['uid'] = $this->generate_uid();
$clone['parent_id'] = $rec['id'];
unset($clone['id'], $clone['recurrence'], $clone['attachments']);
// update the task but unset completed flag
$rec = array_merge($rec, $updates);
$rec['complete'] = $old['complete'];
$rec['status'] = $old['status'];
}
}
return $clone;
}
/**
* Send out an invitation/notification to all task attendees
*/
private function notify_attendees($task, $old, $action = 'edit', $comment = null)
{
if ($action == 'delete' || ($task['status'] == 'CANCELLED' && $old['status'] != $task['status'])) {
$task['cancelled'] = true;
$is_cancelled = true;
}
$itip = $this->load_itip();
$emails = $this->lib->get_user_emails();
$itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3);
// add comment to the iTip attachment
$task['comment'] = $comment;
// needed to generate VTODO instead of VEVENT entry
$task['_type'] = 'task';
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'delete' ? 'CANCEL' : 'REQUEST';
$object = $this->to_libcal($task);
$message = $itip->compose_itip_message($object, $method, $task['sequence'] > $old['sequence']);
// list existing attendees from the $old task
$old_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
// send to every attendee
$sent = 0; $current = array();
foreach ((array)$task['attendees'] as $attendee) {
$current[] = strtolower($attendee['email']);
// skip myself for obvious reasons
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) {
continue;
}
// skip if notification is disabled for this attendee
if ($attendee['noreply'] && $itip_notify & 2) {
continue;
}
// skip if this attendee has delegated and set RSVP=FALSE
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) {
continue;
}
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$is_rsvp = $is_new || $task['sequence'] > $old['sequence'];
$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($object, $method, $attendee, $subject, $bodytext, $message, $is_rsvp))
$sent++;
else
$sent = -100;
}
// send CANCEL message to removed attendees
foreach ((array)$old['attendees'] as $attendee) {
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
continue;
}
$vtodo = $this->to_libcal($old);
$vtodo['cancelled'] = $is_cancelled;
$vtodo['attendees'] = array($attendee);
$vtodo['comment'] = $comment;
if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody'))
$sent++;
else
$sent = -100;
}
return $sent;
}
/**
* Compare two task objects and return differing properties
*
* @param array Event A
* @param array Event B
* @return array List of differing task properties
*/
public static function task_diff($a, $b)
{
$diff = array();
$ignore = array('changed' => 1, 'attachments' => 1);
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
if (!$ignore[$key] && $a[$key] != $b[$key])
$diff[] = $key;
}
// only compare number of attachments
if (count($a['attachments']) != count($b['attachments']))
$diff[] = 'attachments';
return $diff;
}
/**
* Dispatcher for tasklist actions initiated by the client
*/
public function tasklist_action()
{
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$list = rcube_utils::get_input_value('l', rcube_utils::INPUT_GPC, true);
$success = false;
if (isset($list['showalarms']))
$list['showalarms'] = intval($list['showalarms']);
switch ($action) {
case 'form-new':
case 'form-edit':
$this->load_ui();
echo $this->ui->tasklist_editform($action, $list);
exit;
case 'new':
$list += array('showalarms' => true, 'active' => true, 'editable' => true);
if ($insert_id = $this->driver->create_list($list)) {
$list['id'] = $insert_id;
if (!$list['_reload']) {
$this->load_ui();
$list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv);
$list += (array)$jsenv[$insert_id];
}
$this->rc->output->command('plugin.insert_tasklist', $list);
$success = true;
}
break;
case 'edit':
if ($newid = $this->driver->edit_list($list)) {
$list['oldid'] = $list['id'];
$list['id'] = $newid;
$this->rc->output->command('plugin.update_tasklist', $list);
$success = true;
}
break;
case 'subscribe':
$success = $this->driver->subscribe_list($list);
break;
case 'delete':
if (($success = $this->driver->delete_list($list)))
$this->rc->output->command('plugin.destroy_tasklist', $list);
break;
case 'search':
$this->load_ui();
$results = array();
$query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
foreach ((array)$this->driver->search_lists($query, $source) as $id => $prop) {
$editname = $prop['editname'];
unset($prop['editname']); // force full name to be displayed
$prop['active'] = false;
// let the UI generate HTML and CSS representation for this calendar
$html = $this->ui->tasklist_list_item($id, $prop, $jsenv);
$prop += (array)$jsenv[$id];
$prop['editname'] = $editname;
$prop['html'] = $html;
$results[] = $prop;
}
// report more results available
if ($this->driver->search_more_results) {
$this->rc->output->show_message('autocompletemore', 'info');
}
$this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
return;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
$this->rc->output->command('plugin.unlock_saving');
}
/**
* Get counts for active tasks divided into different selectors
*/
public function fetch_counts()
{
if (isset($_REQUEST['lists'])) {
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
}
else {
foreach ($this->driver->get_lists() as $list) {
if ($list['active'])
$lists[] = $list['id'];
}
}
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
/**
* Adjust the cached counts after changing a task
*/
public function update_counts($oldrec, $newrec)
{
// rebuild counts until this function is finally implemented
$this->fetch_counts();
// $this->rc->output->command('plugin.update_counts', $counts);
}
/**
*
*/
public function fetch_tasks()
{
$mask = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
$search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
$filter = array('mask' => $mask, 'search' => $search);
$data = $this->tasks_data($this->driver->list_tasks($filter, $lists));
$this->rc->output->command('plugin.data_ready', array(
'filter' => $mask,
'lists' => $lists,
'search' => $search,
'data' => $data,
'tags' => $this->driver->get_tags(),
));
}
/**
* Handler for printing calendars
*/
public function print_tasks()
{
// Add CSS stylesheets to the page header
$skin_path = $this->local_skin_path();
$this->include_stylesheet($skin_path . '/print.css');
$this->include_script('tasklist.js');
$this->rc->output->add_handlers(array(
'plugin.tasklist_print' => array($this, 'print_tasks_list'),
));
$this->rc->output->set_pagetitle($this->gettext('print'));
$this->rc->output->send('tasklist.print');
}
/**
* Handler for printing calendars
*/
public function print_tasks_list($attrib)
{
$mask = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
$search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
$filter = array('mask' => $mask, 'search' => $search);
$data = $this->tasks_data($this->driver->list_tasks($filter, $lists));
// we'll build the tasks table in javascript on page load
// where we have sorting methods, etc.
$this->rc->output->set_env('tasks', $data);
$this->rc->output->set_env('filtermask', $mask);
return $this->ui->tasks_resultview($attrib);
}
/**
* Prepare and sort the given task records to be sent to the client
*/
private function tasks_data($records)
{
$data = $this->task_tree = $this->task_titles = array();
foreach ($records as $rec) {
if ($rec['parent_id']) {
$this->task_tree[$rec['id']] = $rec['parent_id'];
}
$this->encode_task($rec);
$data[] = $rec;
}
// assign hierarchy level indicators for later sorting
array_walk($data, array($this, 'task_walk_tree'));
return $data;
}
/**
* Prepare the given task record before sending it to the client
*/
private function encode_task(&$rec)
{
$rec['mask'] = $this->filter_mask($rec);
$rec['flagged'] = intval($rec['flagged']);
$rec['complete'] = floatval($rec['complete']);
if (is_object($rec['created'])) {
$rec['created_'] = $this->rc->format_date($rec['created']);
$rec['created'] = $rec['created']->format('U');
}
if (is_object($rec['changed'])) {
$rec['changed_'] = $this->rc->format_date($rec['changed']);
$rec['changed'] = $rec['changed']->format('U');
}
else {
$rec['changed'] = null;
}
if ($rec['date']) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['datetime'] = intval($date->format('U'));
$rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
$rec['_hasdate'] = 1;
}
catch (Exception $e) {
$rec['date'] = $rec['datetime'] = null;
}
}
else {
$rec['date'] = $rec['datetime'] = null;
$rec['_hasdate'] = 0;
}
if ($rec['startdate']) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdatetime'] = intval($date->format('U'));
$rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
}
catch (Exception $e) {
$rec['startdate'] = $rec['startdatetime'] = null;
}
}
if ($rec['valarms']) {
$rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']);
$rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']);
}
if ($rec['recurrence']) {
$rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']);
$rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']);
}
foreach ((array)$rec['attachments'] as $k => $attachment) {
$rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
}
// convert link URIs references into structs
if (array_key_exists('links', $rec)) {
foreach ((array) $rec['links'] as $i => $link) {
if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link, 'task'))) {
$rec['links'][$i] = $msgref;
}
}
}
// Convert HTML description into plain text
if ($this->is_html($rec)) {
$h2t = new rcube_html2text($rec['description'], false, true, 0);
$rec['description'] = $h2t->get_text();
}
if (!is_array($rec['tags']))
$rec['tags'] = (array)$rec['tags'];
sort($rec['tags'], SORT_LOCALE_STRING);
if (in_array($rec['id'], $this->collapsed_tasks))
$rec['collapsed'] = true;
if (empty($rec['parent_id']))
$rec['parent_id'] = null;
$this->task_titles[$rec['id']] = $rec['title'];
}
/**
* Determine whether the given task description is HTML formatted
*/
private function is_html($task)
{
// check for opening and closing or tags
return (preg_match('/<(html|body)(\s+[a-z]|>)/', $task['description'], $m) && strpos($task['description'], ''.$m[1].'>') > 0);
}
/**
* Callback function for array_walk over all tasks.
* Sets tree depth and parent titles
*/
private function task_walk_tree(&$rec)
{
$rec['_depth'] = 0;
$parent_titles = array();
$parent_id = $this->task_tree[$rec['id']];
while ($parent_id) {
$rec['_depth']++;
array_unshift($parent_titles, $this->task_titles[$parent_id]);
$parent_id = $this->task_tree[$parent_id];
}
if (count($parent_titles)) {
$rec['parent_title'] = join(' ยป ', array_filter($parent_titles));
}
}
/**
* Compute the filter mask of the given task
*
* @param array Hash array with Task record properties
* @return int Filter mask
*/
public function filter_mask($rec)
{
static $today, $today_date, $tomorrow, $weeklimit;
if (!$today) {
$today_date = new DateTime('now', $this->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
// In Kolab-mode we hide "Next 7 days" filter, which means
// "Later" should catch tasks with date after tomorrow (#5353)
if ($this->rc->output->get_env('tasklist_driver') == 'kolab') {
$weeklimit = $tomorrow;
}
else {
$week_date = new DateTime('now + 7 days', $this->timezone);
$weeklimit = $week_date->format('Y-m-d');
}
}
$mask = 0;
$start = $rec['startdate'] ?: '1900-00-00';
$duedate = $rec['date'] ?: '3000-00-00';
if ($rec['flagged'])
$mask |= self::FILTER_MASK_FLAGGED;
if ($this->driver->is_complete($rec))
$mask |= self::FILTER_MASK_COMPLETE;
if (empty($rec['date']))
$mask |= self::FILTER_MASK_NODATE;
else if ($rec['date'] < $today)
$mask |= self::FILTER_MASK_OVERDUE;
if (empty($rec['recurrence']) || $duedate < $today || $start > $weeklimit) {
if ($duedate <= $today || ($rec['startdate'] && $start <= $today))
$mask |= self::FILTER_MASK_TODAY;
else if (($start > $today && $start <= $tomorrow) || ($duedate > $today && $duedate <= $tomorrow))
$mask |= self::FILTER_MASK_TOMORROW;
else if (($start > $tomorrow && $start <= $weeklimit) || ($duedate > $tomorrow && $duedate <= $weeklimit))
$mask |= self::FILTER_MASK_WEEK;
else if ($start > $weeklimit || $duedate > $weeklimit)
$mask |= self::FILTER_MASK_LATER;
}
else if ($rec['startdate'] || $rec['date']) {
$date = new DateTime($rec['startdate'] ?: $rec['date'], $this->timezone);
// set safe recurrence start
while ($date->format('Y-m-d') >= $today) {
switch ($rec['recurrence']['FREQ']) {
case 'DAILY':
$date = clone $today_date;
$date->sub(new DateInterval('P1D'));
break;
case 'WEEKLY': $date->sub(new DateInterval('P7D')); break;
case 'MONTHLY': $date->sub(new DateInterval('P1M')); break;
case 'YEARLY': $date->sub(new DateInterval('P1Y')); break;
default; break 2;
}
}
$date->_dateonly = true;
$engine = libcalendaring::get_recurrence();
$engine->init($rec['recurrence'], $date);
// check task occurrences (stop next week)
// FIXME: is there a faster way of doing this?
while ($date = $engine->next()) {
$date = $date->format('Y-m-d');
// break iteration asap
if ($date > $duedate || ($mask & self::FILTER_MASK_LATER)) {
break;
}
if ($date == $today) {
$mask |= self::FILTER_MASK_TODAY;
}
else if ($date == $tomorrow) {
$mask |= self::FILTER_MASK_TOMORROW;
}
else if ($date > $tomorrow && $date <= $weeklimit) {
$mask |= self::FILTER_MASK_WEEK;
}
else if ($date > $weeklimit) {
$mask |= self::FILTER_MASK_LATER;
break;
}
}
}
// add masks for assigned tasks
if ($this->is_organizer($rec) && !empty($rec['attendees']) && $this->is_attendee($rec) === false)
$mask |= self::FILTER_MASK_ASSIGNED;
else if (/*empty($rec['attendees']) ||*/ $this->is_attendee($rec) !== false)
$mask |= self::FILTER_MASK_MYTASKS;
return $mask;
}
/**
* Determine whether the current user is an attendee of the given task
*/
public function is_attendee($task)
{
$emails = $this->lib->get_user_emails();
foreach ((array)$task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
return $i;
}
}
return false;
}
/**
* Determine whether the current user is the organizer of the given task
*/
public function is_organizer($task)
{
$emails = $this->lib->get_user_emails();
return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function tasklist_view()
{
$this->ui->init();
$this->ui->init_templates();
// set autocompletion env
$this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
$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', 'delete', 'close');
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['tasklist.'.$label] = $this->gettext($label);
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
$this->ui->init_templates();
$this->ui->tasklists();
// collect env variables
$env = array(
'tasklists' => $this->rc->output->get_env('tasklists'),
'tasklist_settings' => $this->ui->load_settings(),
);
echo $this->api->output->parse('tasklist.taskedit', false, false);
$script_add = '';
foreach ($this->ui->get_gui_objects() as $obj => $id) {
$script_add .= rcmail_output::JS_OBJECT_NAME . ".gui_object('$obj', '$id');\n";
}
echo html::tag('script', array('type' => 'text/javascript'),
rcmail_output::JS_OBJECT_NAME . ".set_env(" . json_encode($env) . ");\n".
rcmail_output::JS_OBJECT_NAME . ".add_label(" . json_encode($texts) . ");\n".
$script_add
);
exit;
}
/**
* Handler for keep-alive requests
* This will check for updated data in active lists and sync them to the client
*/
public function refresh($attr)
{
// refresh the entire list every 10th time to also sync deleted items
if (rand(0,10) == 10) {
$this->rc->output->command('plugin.reload_data');
return;
}
$filter = array(
'since' => $attr['last'],
'search' => rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),
'mask' => intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)) & self::FILTER_MASK_COMPLETE,
);
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);;
$updates = $this->driver->list_tasks($filter, $lists);
if (!empty($updates)) {
$this->rc->output->command('plugin.refresh_tasks', $this->tasks_data($updates), true);
// update counts
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
}
/**
* Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
* This will check for pending notifications and pass them to the client
*/
public function pending_alarms($p)
{
$this->load_driver();
if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) {
foreach ($alarms as $alarm) {
// encode alarm object to suit the expectations of the calendaring code
if ($alarm['date'])
$alarm['start'] = new DateTime($alarm['date'].' '.$alarm['time'], $this->timezone);
$alarm['id'] = 'task:' . $alarm['id']; // prefix ID with task:
$alarm['allday'] = empty($alarm['time']) ? 1 : 0;
$p['alarms'][] = $alarm;
}
}
return $p;
}
/**
* Handler for alarm dismiss hook triggered by the calendar module
*/
public function dismiss_alarms($p)
{
$this->load_driver();
foreach ((array)$p['ids'] as $id) {
if (strpos($id, 'task:') === 0)
$p['success'] |= $this->driver->dismiss_alarm(substr($id, 5), $p['snooze']);
}
return $p;
}
/**
* Handler for importing .ics files
*/
function import_tasks()
{
// Upload progress update
if (!empty($_GET['_progress'])) {
$this->rc->upload_progress();
}
@set_time_limit(0);
// process uploaded file if there is no error
$err = $_FILES['_data']['error'];
if (!$err && $_FILES['_data']['tmp_name']) {
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
$lists = $this->driver->get_lists();
$list = $lists[$source] ?: $this->get_default_tasklist();
$source = $list['id'];
// extract zip file
if ($_FILES['_data']['type'] == 'application/zip') {
$count = 0;
if (class_exists('ZipArchive', false)) {
$zip = new ZipArchive();
if ($zip->open($_FILES['_data']['tmp_name'])) {
$randname = uniqid('zip-' . session_id(), true);
$tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname;
mkdir($tmpdir, 0700);
// extract each ical file from the archive and import it
for ($i = 0; $i < $zip->numFiles; $i++) {
$filename = $zip->getNameIndex($i);
if (preg_match('/\.ics$/i', $filename)) {
$tmpfile = $tmpdir . '/' . basename($filename);
if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) {
$count += $this->import_from_file($tmpfile, $source, $errors);
unlink($tmpfile);
}
}
}
rmdir($tmpdir);
$zip->close();
}
else {
$errors = 1;
$msg = 'Failed to open zip file.';
}
}
else {
$errors = 1;
$msg = 'Zip files are not supported for import.';
}
}
else {
// attempt to import the uploaded file directly
$count = $this->import_from_file($_FILES['_data']['tmp_name'], $source, $errors);
}
if ($count) {
$this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
$this->rc->output->command('plugin.import_success', array('source' => $source, 'refetch' => true));
}
else if (!$errors) {
$this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
$this->rc->output->command('plugin.import_success', array('source' => $source));
}
else {
$this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
}
}
else {
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else {
$msg = $this->rc->gettext('fileuploaderror');
}
$this->rc->output->command('plugin.import_error', array('message' => $msg));
}
$this->rc->output->send('iframe');
}
/**
* Helper function to parse and import a single .ics file
*/
private function import_from_file($filepath, $source, &$errors)
{
$user_email = $this->rc->user->get_username();
$ical = $this->get_ical();
$errors = !$ical->fopen($filepath);
$count = $i = 0;
foreach ($ical as $task) {
// keep the browser connection alive on long import jobs
if (++$i > 100 && $i % 100 == 0) {
echo "";
ob_flush();
}
if ($task['_type'] == 'task') {
$task['list'] = $source;
if ($this->driver->create_task($task)) {
$count++;
}
else {
$errors++;
}
}
}
return $count;
}
/**
* Construct the ics file for exporting tasks to iCalendar format
*/
function export_tasks()
{
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
$task_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GPC);
$attachments = (bool) rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GPC);
$this->load_driver();
$browser = new rcube_browser;
$lists = $this->driver->get_lists();
$tasks = array();
$filter = array();
// get message UIDs for filter
if ($source && ($list = $lists[$source])) {
$filename = html_entity_decode($list['name']) ?: $sorce;
$filter = array($source => true);
}
else if ($task_id) {
$filename = 'tasks';
foreach (explode(',', $task_id) as $id) {
list($list_id, $task_id) = explode(':', $id, 2);
if ($list_id && $task_id) {
$filter[$list_id][] = $task_id;
}
}
}
// Get tasks
foreach ($filter as $list_id => $uids) {
$_filter = is_array($uids) ? array('uid' => $uids) : null;
$_tasks = $this->driver->list_tasks($_filter, $list_id);
if (!empty($_tasks)) {
$tasks = array_merge($tasks, $_tasks);
}
}
// Set file name
if ($source && count($tasks) == 1) {
$filename = $tasks[0]['title'] ?: 'task';
}
$filename .= '.ics';
$filename = $browser->ie ? rawurlencode($filename) : addcslashes($filename, '"');
$tasks = array_map(array($this, 'to_libcal'), $tasks);
// Give plugins a possibility to implement other output formats or modify the result
$plugin = $this->rc->plugins->exec_hook('tasks_export', array(
'result' => $tasks,
'attachments' => $attachments,
'filename' => $filename,
'plugin' => $this,
));
if ($plugin['abort']) {
exit;
}
$this->rc->output->nocacheing_headers();
// don't kill the connection if download takes more than 30 sec.
@set_time_limit(0);
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=\"". $plugin['filename'] ."\"");
$this->get_ical()->export($plugin['result'], '', true,
$plugins['attachments'] ? array($this->driver, 'get_attachment_body') : null);
exit;
}
/******* Attachment handling *******/
/**
* Handler for attachments upload
*/
public function attachment_upload()
{
- $this->lib->attachment_upload(self::SESSION_KEY);
+ $handler = new kolab_attachments_handler();
+ $handler->attachment_upload(self::SESSION_KEY);
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
+ $handler = new kolab_attachments_handler();
+
// show loading page
if (!empty($_GET['_preload'])) {
- return $this->lib->attachment_loading_page();
+ return $handler->attachment_loading_page();
}
$task = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
$list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
- $task = array('id' => $task, 'list' => $list, 'rev' => $rev);
+ $task = array('id' => $task, 'list' => $list, 'rev' => $rev);
$attachment = $this->driver->get_attachment($id, $task);
// show part page
if (!empty($_GET['_frame'])) {
- $this->lib->attachment = $attachment;
- $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
- $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
- $this->rc->output->set_env('filename', $attachment['name']);
- $this->rc->output->set_env('mimetype', $attachment['mimetype']);
- $this->rc->output->send('tasklist.attachment');
+ $handler->attachment_page($attachment);
}
// deliver attachment content
else if ($attachment) {
$attachment['body'] = $this->driver->get_attachment_body($id, $task);
- $this->lib->attachment_get($attachment);
+ $handler->attachment_get($attachment);
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/******* Email related function *******/
public function mail_message2task()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$task = array();
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_folder($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$task['title'] = trim($message->subject);
$task['description'] = trim($message->first_text_part());
$task['id'] = -$uid;
$this->load_driver();
// add a reference to the email message
if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
$task['links'] = array($msgref);
}
// copy mail attachments to task
else if ($message->attachments && $this->driver->attachments) {
if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) {
$_SESSION[self::SESSION_KEY] = array();
$_SESSION[self::SESSION_KEY]['id'] = $task['id'];
$_SESSION[self::SESSION_KEY]['attachments'] = array();
}
foreach ((array)$message->attachments as $part) {
$attachment = array(
'data' => $imap->get_message_part($uid, $part->mime_id, $part),
'size' => $part->size,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'group' => $task['id'],
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
$attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
// store new attachment in session
unset($attachment['status'], $attachment['abort'], $attachment['data']);
$_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
$attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new'
$task['attachments'][] = $attachment;
}
}
}
$this->rc->output->command('plugin.mail2taskdialog', $task);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/**
* Add UI element to copy task invitations or updates to the tasklist
*/
public function mail_messagebody_html($p)
{
// load iCalendar functions (if necessary)
if (!empty($this->lib->ical_parts)) {
$this->get_ical();
$this->load_itip();
}
$html = '';
$has_tasks = false;
$ical_objects = $this->lib->get_mail_ical_objects();
// show a box for every task in the file
foreach ($ical_objects as $idx => $task) {
if ($task['_type'] != 'task') {
continue;
}
$has_tasks = true;
// get prepared inline UI for this event object
if ($ical_objects->method) {
$html .= html::div('tasklist-invitebox invitebox boxinformation',
$this->itip->mail_itip_inline_ui(
$task,
$ical_objects->method,
$ical_objects->mime_id . ':' . $idx,
'tasks',
rcube_utils::anytodatetime($ical_objects->message_date)
)
);
}
// limit listing
if ($idx >= 3) {
break;
}
}
// list linked tasks
$links = array();
foreach ($this->message_tasks as $task) {
$checkbox = new html_checkbox(array(
'name' => 'completed',
'class' => 'complete',
'title' => $this->gettext('complete'),
'data-list' => $task['list'],
));
$complete = $this->driver->is_complete($task);
$links[] = html::tag('li', 'messagetaskref' . ($complete ? ' complete' : ''),
$checkbox->show($complete ? $task['id'] : null, array('value' => $task['id'])) . ' ' .
html::a(array(
'href' => $this->rc->url(array(
'task' => 'tasks',
'list' => $task['list'],
'id' => $task['id'],
)),
'class' => 'messagetasklink',
'rel' => $task['id'] . '@' . $task['list'],
'target' => '_blank',
), rcube::Q($task['title']))
);
}
if (count($links)) {
$html .= html::div('messagetasklinks', html::tag('ul', 'tasklist', join("\n", $links)));
}
// prepend iTip/relation boxes to message body
if ($html) {
$this->load_ui();
$this->ui->init();
$p['content'] = $html . $p['content'];
$this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm');
}
// add "Save to tasks" button into attachment menu
if ($has_tasks) {
$this->add_button(array(
'id' => 'attachmentsavetask',
'name' => 'attachmentsavetask',
'type' => 'link',
'wrapper' => 'li',
'command' => 'attachment-save-task',
'class' => 'icon tasklistlink',
'classact' => 'icon tasklistlink active',
'innerclass' => 'icon taskadd',
'label' => 'tasklist.savetotasklist',
), 'attachmentmenu');
}
return $p;
}
/**
* Lookup backend storage and find notes associated with the given message
*/
public function mail_message_load($p)
{
if (!$p['object']->headers->others['x-kolab-type']) {
$this->load_driver();
$this->message_tasks = $this->driver->get_message_related_tasks($p['object']->headers, $p['object']->folder);
// sort message tasks by completeness and due date
$driver = $this->driver;
array_walk($this->message_tasks, array($this, 'encode_task'));
usort($this->message_tasks, function($a, $b) use ($driver) {
$a_complete = intval($driver->is_complete($a));
$b_complete = intval($driver->is_complete($b));
$d = $a_complete - $b_complete;
if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
if (!$d) $d = $a['datetime'] - $b['datetime'];
return $d;
});
}
}
/**
* Load iCalendar functions
*/
public function get_ical()
{
if (!$this->ical) {
$this->ical = libcalendaring::get_ical();
}
return $this->ical;
}
/**
* Get properties of the tasklist this user has specified as default
*/
public function get_default_tasklist($sensitivity = null, $lists = null)
{
if ($lists === null) {
$lists = $this->driver->get_lists(tasklist_driver::FILTER_PERSONAL | tasklist_driver::FILTER_WRITEABLE);
}
$list = null;
foreach ($lists as $l) {
if ($sensitivity && $l['subtype'] == $sensitivity) {
$list = $l;
break;
}
if ($l['default']) {
$list = $l;
}
if ($l['editable']) {
$first = $l;
}
}
return $list ?: $first;
}
/**
* Import the full payload from a mail message attachment
*/
public function mail_import_attachment()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$charset = RCUBE_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_folder($mbox);
if ($uid && $mime_id) {
$part = $imap->get_message_part($uid, $mime_id);
// $headers = $imap->get_message_headers($uid);
if ($part->ctype_parameters['charset']) {
$charset = $part->ctype_parameters['charset'];
}
if ($part) {
$tasks = $this->get_ical()->import($part, $charset);
}
}
$success = $existing = 0;
if (!empty($tasks)) {
// find writeable tasklist to store task
$cal_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
$lists = $this->driver->get_lists();
foreach ($tasks as $task) {
// save to tasklist
$list = $lists[$cal_id] ?: $this->get_default_tasklist($task['sensitivity']);
if ($list && $list['editable'] && $task['_type'] == 'task') {
$task = $this->from_ical($task);
$task['list'] = $list['id'];
if (!$this->driver->get_task($task['uid'])) {
$success += (bool) $this->driver->create_task($task);
}
else {
$existing++;
}
}
}
}
if ($success) {
$this->rc->output->command('display_message', $this->gettext(array(
'name' => 'importsuccess',
'vars' => array('nr' => $success),
)), 'confirmation');
}
else if ($existing) {
$this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
}
else {
$this->rc->output->command('display_message', $this->gettext('errorimportingtask'), 'error');
}
}
/**
* Handler for POST request to import an event attached to a mail message
*/
public function mail_import_itip()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
$comment = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
$delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
$noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)) || $status == 'needs-action';
$error_msg = $this->gettext('errorimportingtask');
$success = false;
if ($status == 'delegated') {
$delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
$delegate = reset($delegates);
if (empty($delegate) || empty($delegate['mailto'])) {
$this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error');
return;
}
}
// successfully parsed tasks?
if ($task = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'task')) {
$task = $this->from_ical($task);
// forward iTip request to delegatee
if ($delegate) {
$rsvpme = rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST);
$itip = $this->load_itip();
$task['comment'] = $comment;
if ($itip->delegate_to($task, $delegate, !empty($rsvpme))) {
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
}
else {
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
unset($task['comment']);
}
$mode = tasklist_driver::FILTER_PERSONAL
| tasklist_driver::FILTER_SHARED
| tasklist_driver::FILTER_WRITEABLE;
// find writeable list to store the task
$list_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null;
$lists = $this->driver->get_lists($mode);
$list = $lists[$list_id];
$dontsave = ($_REQUEST['_folder'] === '' && $task['_method'] == 'REQUEST');
// select default list except user explicitly selected 'none'
if (!$list && !$dontsave) {
$list = $this->get_default_tasklist($task['sensitivity'], $lists);
}
$metadata = array(
'uid' => $task['uid'],
'changed' => is_object($task['changed']) ? $task['changed']->format('U') : 0,
'sequence' => intval($task['sequence']),
'fallback' => strtoupper($status),
'method' => $task['_method'],
'task' => 'tasks',
);
// update my attendee status according to submitted method
if (!empty($status)) {
$organizer = $task['organizer'];
$emails = $this->lib->get_user_emails();
foreach ($task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
$reply_sender = $attendee['email'];
$task['attendees'][$i]['status'] = strtoupper($status);
if (!in_array($task['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) {
$task['attendees'][$i]['rsvp'] = false; // unset RSVP attribute
}
}
}
// add attendee with this user's default identity if not listed
if (!$reply_sender) {
$sender_identity = $this->rc->user->list_emails(true);
$task['attendees'][] = array(
'name' => $sender_identity['name'],
'email' => $sender_identity['email'],
'role' => 'OPT-PARTICIPANT',
'status' => strtoupper($status),
);
$metadata['attendee'] = $sender_identity['email'];
}
}
// save to tasklist
if ($list && $list['editable']) {
$task['list'] = $list['id'];
// check for existing task with the same UID
$existing = $this->find_task($task['uid'], $mode);
if ($existing) {
// only update attendee status
if ($task['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address
$existing_attendee = -1;
$existing_attendee_emails = array();
foreach ($existing['attendees'] as $i => $attendee) {
$existing_attendee_emails[] = $attendee['email'];
if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
$existing_attendee = $i;
}
}
$task_attendee = null;
foreach ($task['attendees'] as $attendee) {
if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
$task_attendee = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
if ($attendee['status'] != 'DELEGATED') {
break;
}
}
// also copy delegate attendee
else if (!empty($attendee['delegated-from']) &&
(stripos($attendee['delegated-from'], $task['_sender']) !== false || stripos($attendee['delegated-from'], $task['_sender_utf']) !== false) &&
(!in_array($attendee['email'], $existing_attendee_emails))) {
$existing['attendees'][] = $attendee;
}
}
// if delegatee has declined, set delegator's RSVP=True
if ($task_attendee && $task_attendee['status'] == 'DECLINED' && $task_attendee['delegated-from']) {
foreach ($existing['attendees'] as $i => $attendee) {
if ($attendee['email'] == $task_attendee['delegated-from']) {
$existing['attendees'][$i]['rsvp'] = true;
break;
}
}
}
// found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $task_attendee) {
$existing['attendees'][$existing_attendee] = $task_attendee;
$success = $this->driver->edit_task($existing);
}
// update the entire attendees block
else if (($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) && $task_attendee) {
$existing['attendees'][] = $task_attendee;
$success = $this->driver->edit_task($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
// delete the task when declined
else if ($status == 'declined' && $delete) {
$deleted = $this->driver->delete_task($existing, true);
$success = true;
}
// import the (newer) task
else if ($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) {
$task['id'] = $existing['id'];
$task['list'] = $existing['list'];
// preserve my participant status for regular updates
if (empty($status)) {
$this->lib->merge_attendees($task, $existing);
}
// set status=CANCELLED on CANCEL messages
if ($task['_method'] == 'CANCEL') {
$task['status'] = 'CANCELLED';
}
// update attachments list, allow attachments update only on REQUEST (#5342)
if ($task['_method'] == 'REQUEST') {
$task['deleted_attachments'] = true;
}
else {
unset($task['attachments']);
}
// show me as free when declined (#1670)
if ($status == 'declined' || $task['status'] == 'CANCELLED') {
$task['free_busy'] = 'free';
}
$success = $this->driver->edit_task($task);
}
else if (!empty($status)) {
$existing['attendees'] = $task['attendees'];
if ($status == 'declined') { // show me as free when declined (#1670)
$existing['free_busy'] = 'free';
}
$success = $this->driver->edit_event($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_tasklists'))) {
$success = $this->driver->create_task($task);
}
else if ($status == 'declined') {
$error_msg = null;
}
}
else if ($status == 'declined' || $dontsave) {
$error_msg = null;
}
else {
$error_msg = $this->gettext('nowritetasklistfound');
}
}
if ($success || $dontsave) {
if ($success) {
$message = $task['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
}
$metadata['rsvp'] = intval($metadata['rsvp']);
$metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
$this->rc->output->command('plugin.itip_message_processed', $metadata);
$error_msg = null;
}
else if ($error_msg) {
$this->rc->output->command('display_message', $error_msg, 'error');
}
// send iTip reply
if ($task['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
$task['comment'] = $comment;
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
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['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
$this->rc->output->send();
}
/**** Task invitation plugin hooks ****/
/**
* Handler for task/itip-delegate requests
*/
function mail_itip_delegate()
{
// forward request to mail_import_itip() with the right status
$_POST['_status'] = $_REQUEST['_status'] = 'delegated';
$this->mail_import_itip();
}
/**
* Find a task in user tasklists
*/
protected function find_task($task, &$mode)
{
$this->load_driver();
// We search for writeable folders in personal namespace by default
$mode = tasklist_driver::FILTER_WRITEABLE | tasklist_driver::FILTER_PERSONAL;
$result = $this->driver->get_task($task, $mode);
// ... now check shared folders if not found
if (!$result) {
$result = $this->driver->get_task($task, tasklist_driver::FILTER_WRITEABLE | tasklist_driver::FILTER_SHARED);
if ($result) {
$mode |= tasklist_driver::FILTER_SHARED;
}
}
return $result;
}
/**
* Handler for task/itip-status requests
*/
public function task_itip_status()
{
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
// find local copy of the referenced task
$existing = $this->find_task($data, $mode);
$is_shared = $mode & tasklist_driver::FILTER_SHARED;
$itip = $this->load_itip();
$response = $itip->get_itip_status($data, $existing);
// get a list of writeable lists to save new tasks to
if ((!$existing || $is_shared) && $response['action'] == 'rsvp' || $response['action'] == 'import') {
$lists = $this->driver->get_lists($mode);
$select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true, 'class' => 'form-control'));
$select->add('--', '');
foreach ($lists as $list) {
if ($list['editable']) {
$select->add($list['name'], $list['id']);
}
}
}
if ($select) {
$default_list = $this->get_default_tasklist($data['sensitivity'], $lists);
$response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . ' ' .
$select->show($is_shared ? $existing['list'] : $default_list['id']));
}
$this->rc->output->command('plugin.update_itip_object_status', $response);
}
/**
* Handler for task/itip-remove requests
*/
public function task_itip_remove()
{
$success = false;
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
// search for event if only UID is given
if ($task = $this->driver->get_task($uid)) {
$success = $this->driver->delete_task($task, true);
}
if ($success) {
$this->rc->output->show_message('tasklist.successremoval', 'confirmation');
}
else {
$this->rc->output->show_message('tasklist.errorsaving', 'error');
}
}
/******* Utility functions *******/
/**
* Generate a unique identifier for an event
*/
public function generate_uid()
{
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
}
/**
* Map task properties for ical exprort using libcalendaring
*/
public function to_libcal($task)
{
$object = $task;
$object['_type'] = '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
*/
public function user_delete($args)
{
$this->load_driver();
return $this->driver->user_delete($args);
}
/**
* Magic getter for public access to protected members
*/
public function __get($name)
{
switch ($name) {
case 'ical':
return $this->get_ical();
case 'itip':
return $this->load_itip();
case 'driver':
$this->load_driver();
return $this->driver;
}
return null;
}
}