From e6ae3b8702f78accf033df43c0a0654508142719 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 12 Oct 2011 19:44:38 +0200 Subject: [PATCH] Add feature to upload and import .ics files --- plugins/calendar/calendar.php | 80 ++++++++++- plugins/calendar/calendar_ui.js | 130 +++++++++++++----- plugins/calendar/lib/calendar_ical.php | 61 +++++++- plugins/calendar/lib/calendar_ui.php | 50 +++++++ plugins/calendar/localization/de_CH.inc | 7 + plugins/calendar/localization/de_DE.inc | 7 + plugins/calendar/localization/en_US.inc | 7 + .../skins/default/templates/calendar.html | 5 + 8 files changed, 297 insertions(+), 50 deletions(-) diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 5895b307..1addd503 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -130,6 +130,7 @@ class calendar extends rcube_plugin $this->register_action('calendar', array($this, 'calendar_action')); $this->register_action('load_events', array($this, 'load_events')); $this->register_action('export_events', array($this, 'export_events')); + $this->register_action('import_events', array($this, 'import_events')); $this->register_action('upload', array($this, 'attachment_upload')); $this->register_action('get-attachment', array($this, 'attachment_get')); $this->register_action('freebusy-status', array($this, 'freebusy_status')); @@ -808,7 +809,69 @@ class calendar extends rcube_plugin // NOP $this->rc->output->send(); } - + + /** + * + */ + function import_events() + { + // Upload progress update + if (!empty($_GET['_progress'])) { + rcube_upload_progress(); + } + + $calendar = get_input_value('calendar', RCUBE_INPUT_GPC); + $uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC); + + // process uploaded file if there is no error + $err = $_FILES['_data']['error']; + + if (!$err && $_FILES['_data']['tmp_name']) { + $calendar = get_input_value('calendar', RCUBE_INPUT_GPC); + $events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name']); + + $count = $errors = 0; + $rangestart = $_REQUEST['_range'] ? strtotime("now -" . intval($_REQUEST['_range']) . " months") : 0; + foreach ($events as $event) { + // TODO: correctly handle recurring events which start before $rangestart + if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart))) + continue; + + $event['calendar'] = $calendar; + if ($success = $this->driver->new_event($event)) { + $count++; + } + else + $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' => $calendar, 'refetch' => true)); + } + else if (!$errors) { + $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice'); + $this->rc->output->command('plugin.import_success', array('source' => $calendar)); + } + else + $this->rc->output->command('display_message', $this->gettext('importerror'), 'error'); + } + else { + if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { + $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array( + 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); + } + else { + $msg = rcube_label('fileuploaderror'); + } + + $this->rc->output->command('display_message', $msg, 'error'); + $this->rc->output->command('plugin.unlock_saving', false); + } + + $this->rc->output->send('iframe'); + } + /** * Construct the ics file for exporting events to iCalendar format; */ @@ -818,11 +881,16 @@ class calendar extends rcube_plugin $end = get_input_value('end', RCUBE_INPUT_GET); if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1); if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+10); - $calendar_name = get_input_value('source', RCUBE_INPUT_GET); - $events = $this->driver->load_events($start, $end, null, $calendar_name, 0); - + $calid = $calname = get_input_value('source', RCUBE_INPUT_GET); + $calendars = $this->driver->list_calendars(); + + if ($calendars[$calid]) { + $calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid; + $events = $this->driver->load_events($start, $end, null, $calid, 0); + } + header("Content-Type: text/calendar"); - header("Content-Disposition: inline; filename=".$calendar_name.'.ics'); + header("Content-Disposition: inline; filename=".$calname.'.ics'); $this->get_ical()->export($events, '', true); exit; @@ -2015,7 +2083,7 @@ class calendar extends rcube_plugin // find writeable calendar to store event $cal_id = $this->rc->config->get('calendar_default_calendar'); $calendars = $this->driver->list_calendars(); - $calendar = $calendars[$cal_id] ? $calendars[$calname] : null; + $calendar = $calendars[$cal_id] ? $calendars[$cal_id] : null; if (!$calendar || $calendar['readonly']) { foreach ($calendars as $cal) { if (!$cal['readonly']) { diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 107c12ce..7dd90e07 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -1893,6 +1893,95 @@ function rcube_calendar_ui(settings) } }; + // open a dialog to upload an .ics file with events to be imported + this.import_events = function(calendar) + { + // close show dialog first + var $dialog = $("#eventsimport").dialog('close'); + var form = rcmail.gui_objects.importform; + + $('#event-import-calendar').val(calendar.id); + + var buttons = {}; + buttons[rcmail.gettext('import', 'calendar')] = function() { + if (form && form.elements._data.value) { + rcmail.async_upload_form(form, 'import_events', function(e) { + rcmail.set_busy(false, null, me.saving_lock); + }); + + // display upload indicator + me.saving_lock = rcmail.set_busy(true, 'uploading'); + } + }; + + buttons[rcmail.gettext('cancel', 'calendar')] = function() { + $dialog.dialog("close"); + }; + + // open jquery UI dialog + $dialog.dialog({ + modal: true, + resizable: false, + closeOnEscape: false, + title: rcmail.gettext('importevents', 'calendar'), + close: function() { + $dialog.dialog("destroy").hide(); + }, + buttons: buttons, + width: 520 + }).show(); + + }; + + // callback from server if import succeeded + this.import_success = function(p) + { + $("#eventsimport").dialog('close'); + rcmail.set_busy(false, null, me.saving_lock); + rcmail.gui_objects.importform.reset(); + + if (p.refetch) + this.refresh(p); + }; + + // refresh the calendar view after saving event data + this.refresh = function(p) + { + var source = me.calendars[p.source]; + + if (source && (p.refetch || (p.update && !source.active))) { + // activate event source if new event was added to an invisible calendar + if (!source.active) { + source.active = true; + fc.fullCalendar('addEventSource', source); + $('#' + rcmail.get_folder_li(source.id, 'rcmlical').id + ' input').prop('checked', true); + } + else + fc.fullCalendar('refetchEvents', source); + } + // add/update single event object + else if (source && p.update) { + var event = p.update; + event.temp = false; + event.editable = source.editable; + var existing = fc.fullCalendar('clientEvents', event.id); + if (existing.length) { + $.extend(existing[0], event); + fc.fullCalendar('updateEvent', existing[0]); + } + else { + event.source = source; // link with source + fc.fullCalendar('renderEvent', event); + } + // refresh fish-eye view + if (me.fisheye_date) + me.fisheye_view(me.fisheye_date); + } + + // remove temp events + fc.fullCalendar('removeEvents', function(e){ return e.temp; }); + }; + /*** event searching ***/ @@ -2085,7 +2174,7 @@ function rcube_calendar_ui(settings) var id = $(this).data('id'); rcmail.select_folder(id, 'rcmlical'); rcmail.enable_command('calendar-edit', true); - rcmail.enable_command('calendar-remove', !me.calendars[id].readonly); + rcmail.enable_command('calendar-remove', 'events-import', !me.calendars[id].readonly); me.selected_calendar = id; }) .dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); }) @@ -2589,6 +2678,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true); rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false); rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false); + rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, false); // search and export events rcmail.register_command('export', function(){ rcmail.goto_url('export_events', { source:cal.selected_calendar }); }, true); @@ -2599,42 +2689,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); }); rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); }); rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, cal.saving_lock); }); - rcmail.addEventListener('plugin.refresh_calendar', function(p){ - var fc = $('#calendar'); - var source = cal.calendars[p.source]; - - if (source && (p.refetch || (p.update && !source.active))) { - // activate event source if new event was added to an invisible calendar - if (!source.active) { - source.active = true; - fc.fullCalendar('addEventSource', source); - $('#' + rcmail.get_folder_li(source.id, 'rcmlical').id + ' input').prop('checked', true); - } - else - fc.fullCalendar('refetchEvents', source); - } - // add/update single event object - else if (source && p.update) { - var event = p.update; - event.temp = false; - event.editable = source.editable; - var existing = fc.fullCalendar('clientEvents', event.id); - if (existing.length) { - $.extend(existing[0], event); - fc.fullCalendar('updateEvent', existing[0]); - } - else { - event.source = source; // link with source - fc.fullCalendar('renderEvent', event); - } - // refresh fish-eye view - if (cal.fisheye_date) - cal.fisheye_view(cal.fisheye_date); - } - - // remove temp events - fc.fullCalendar('removeEvents', function(e){ return e.temp; }); - }); + rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); }); + rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); }); // let's go var cal = new rcube_calendar_ui(rcmail.env.calendar_settings); diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php index f859b635..284a5e7a 100644 --- a/plugins/calendar/lib/calendar_ical.php +++ b/plugins/calendar/lib/calendar_ical.php @@ -66,13 +66,7 @@ class calendar_ical */ public function import($vcal, $charset = RCMAIL_CHARSET) { - // use Horde:iCalendar to parse vcalendar file format - require_once 'Horde/iCalendar.php'; - - // set target charset for parsed events - $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET; - - $parser = new Horde_iCalendar; + $parser = $this->get_parser(); $parser->parsevCalendar($vcal, 'VCALENDAR', $charset); $this->method = $parser->getAttributeDefault('METHOD', ''); $this->events = array(); @@ -86,6 +80,59 @@ class calendar_ical return $this->events; } + /** + * Read iCalendar events from a file + * + * @param string File path to read from + * @return array List of events extracted from the file + */ + public function import_from_file($filepath) + { + $this->events = array(); + $fp = fopen($filepath, 'r'); + + // check file content first + $begin = fread($fp, 1024); + if (!preg_match('/BEGIN:VCALENDAR/i', $begin)) + return $this->events; + + $parser = $this->get_parser(); + $buffer = ''; + + fseek($fp, 0); + while (($line = fgets($fp, 2048)) !== false) { + $buffer .= $line; + if (preg_match('/END:VEVENT/i', $line)) { + $parser->parsevCalendar($buffer, 'VCALENDAR', RCMAIL_CHARSET, false); + $buffer = ''; + } + } + fclose($fp); + + if ($data = $parser->getComponents()) { + foreach ($data as $comp) { + if ($comp->getType() == 'vEvent') + $this->events[] = $this->_to_rcube_format($comp); + } + } + + return $this->events; + } + + /** + * Load iCal parser from the Horde lib + */ + private function get_parser() + { + // use Horde:iCalendar to parse vcalendar file format + require_once 'Horde/iCalendar.php'; + + // set target charset for parsed events + $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET; + + return new Horde_iCalendar; + } + /** * Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format */ diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 224a93e8..e5baba6a 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -88,6 +88,7 @@ class calendar_ui $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning')); $this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons')); $this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options')); + $this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form')); $this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template } @@ -555,6 +556,55 @@ class calendar_ui return $select_prefix->show() . ' ' . $select_wday->show(); } + /** + * Form for uploading and importing events + */ + function events_import_form($attrib = array()) + { + if (!$attrib['id']) + $attrib['id'] = 'rcmImportForm'; + + // Get max filesize, enable upload progress bar + $max_filesize = rcube_upload_init(); + + $button = new html_inputfield(array('type' => 'button')); + $input = new html_inputfield(array( + 'type' => 'file', 'name' => '_data', 'size' => $attrib['uploadfieldsize'])); + + $select = new html_select(array('name' => '_range', 'id' => 'event-import-range')); + $select->add(array( + $this->cal->gettext('onemonthback'), + $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))), + $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))), + $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))), + $this->cal->gettext('all'), + ), + array('1','2','6','12',0)); + + $html .= html::div('form-section', + html::div(null, $input->show()) . + html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) + ); + + $html .= html::div('form-section', + html::label('event-import-calendar', $this->cal->gettext('calendar')) . + $this->calendar_select(array('name' => 'calendar', 'id' => 'event-import-calendar')) + ); + + $html .= html::div('form-section', + html::label('event-import-range', $this->cal->gettext('importrange')) . + $select->show(1) + ); + + $this->rc->output->add_gui_object('importform', $attrib['id']); + $this->rc->output->add_label('import'); + + return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'import_events')), + 'method' => "post", 'enctype' => 'multipart/form-data', 'id' => $attrib['id']), + $html + ); + } + /** * Generate the form for event attachments upload */ diff --git a/plugins/calendar/localization/de_CH.inc b/plugins/calendar/localization/de_CH.inc index ad97c098..6b888c2a 100644 --- a/plugins/calendar/localization/de_CH.inc +++ b/plugins/calendar/localization/de_CH.inc @@ -68,6 +68,10 @@ $labels['andnmore'] = '$nr weitere...'; $labels['showmore'] = 'Mehr anzeigen...'; $labels['togglerole'] = 'Klick zum Ändern der Rolle'; $labels['createfrommail'] = 'Als Termin speichern'; +$labels['importevents'] = 'Termine importieren'; +$labels['importrange'] = 'Termine ab'; +$labels['onemonthback'] = '1 Monat zurück'; +$labels['nmonthsback'] = '$nr Monate zurück'; // agenda view $labels['listrange'] = 'Angezeigter Bereich:'; @@ -196,6 +200,9 @@ $labels['attendeupdateesuccess'] = 'Teilnehmerstatus erfolgreich aktualisiert'; $labels['itipresponseerror'] = 'Die Antwort auf diese Einladung konnte nicht versendet werden'; $labels['sentresponseto'] = 'Antwort auf diese Einladung erfolgreich an $mailto gesendet'; $labels['localchangeswarning'] = 'Die Änderungen an diesem Termin können nur in Ihrem persönlichen Kalender gespeichert werden.'; +$labels['importsuccess'] = 'Es wurden $nr Termine erfolgreich importiert'; +$labels['importnone'] = 'Keine Termine zum Importieren gefunden'; +$labels['importerror'] = 'Fehler beim Importieren'; // recurrence form $labels['repeat'] = 'Wiederholung'; diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc index e1452d63..8c37f2c1 100644 --- a/plugins/calendar/localization/de_DE.inc +++ b/plugins/calendar/localization/de_DE.inc @@ -68,6 +68,10 @@ $labels['andnmore'] = '$nr weitere...'; $labels['showmore'] = 'Mehr anzeigen...'; $labels['togglerole'] = 'Klick zum Ändern der Rolle'; $labels['createfrommail'] = 'Als Termin speichern'; +$labels['importevents'] = 'Termine importieren'; +$labels['importrange'] = 'Termine ab'; +$labels['onemonthback'] = '1 Monat zurück'; +$labels['nmonthsback'] = '$nr Monate zurück'; // agenda view $labels['listrange'] = 'Angezeigter Bereich:'; @@ -196,6 +200,9 @@ $labels['attendeupdateesuccess'] = 'Teilnehmerstatus erfolgreich aktualisiert'; $labels['itipresponseerror'] = 'Die Antwort auf diese Einladung konnte nicht versendet werden'; $labels['sentresponseto'] = 'Antwort auf diese Einladung erfolgreich an $mailto gesendet'; $labels['localchangeswarning'] = 'Die Änderungen an diesem Termin können nur in Ihrem persönlichen Kalender gespeichert werden.'; +$labels['importsuccess'] = 'Es wurden $nr Termine erfolgreich importiert'; +$labels['importnone'] = 'Keine Termine zum Importieren gefunden'; +$labels['importerror'] = 'Fehler beim Importieren'; // recurrence form $labels['repeat'] = 'Wiederholung'; diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index ce0bfe0d..4656110b 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -68,6 +68,10 @@ $labels['andnmore'] = '$nr more...'; $labels['showmore'] = 'Show more...'; $labels['togglerole'] = 'Click to toggle role'; $labels['createfrommail'] = 'Save as event'; +$labels['importevents'] = 'Import events'; +$labels['importrange'] = 'Events from'; +$labels['onemonthback'] = '1 month back'; +$labels['nmonthsback'] = '$nr months back'; // agenda view $labels['listrange'] = 'Range to display:'; @@ -197,6 +201,9 @@ $labels['itipresponseerror'] = 'Failed to send the response to this event invita $labels['itipinvalidrequest'] = 'This invitation is no longer valid'; $labels['sentresponseto'] = 'Successfully sent invitation response to $mailto'; $labels['localchangeswarning'] = 'You are about to make changes that will only be reflected on your personal calendar'; +$labels['importsuccess'] = 'Successfully imported $nr events'; +$labels['importnone'] = 'No events found to be imported'; +$labels['importerror'] = 'An error occured while importing'; // recurrence form $labels['repeat'] = 'Repeat'; diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html index 4098e5dd..6253ab7c 100644 --- a/plugins/calendar/skins/default/templates/calendar.html +++ b/plugins/calendar/skins/default/templates/calendar.html @@ -35,6 +35,7 @@