diff --git a/plugins/calendar/TODO b/plugins/calendar/TODO
index ad1c05c6..905d4eca 100644
--- a/plugins/calendar/TODO
+++ b/plugins/calendar/TODO
@@ -5,7 +5,7 @@
+ Edit: 3.16: Reminder set
+ Edit: 3.17: Priority: High/Low
- Edit: 3.18: Recurrence (in line with Kontact)
-- Edit: 3.19: Attachment Upload
++ Edit: 3.19: Attachment Upload
- Edit: 3.20: Print
- Add/Manage Attendees
- Edit: 3.21: Required / Optional / Resource specification
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 0b4b00ef..28372491 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -106,6 +106,8 @@ class calendar extends rcube_plugin
$this->register_action('load_events', array($this, 'load_events'));
$this->register_action('search_events', array($this, 'search_events'));
$this->register_action('export_events', array($this, 'export_events'));
+ $this->register_action('upload', array($this, 'attachment_upload'));
+ $this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('randomdata', array($this, 'generate_randomdata'));
}
else if ($this->rc->task == 'settings') {
@@ -156,7 +158,7 @@ class calendar extends rcube_plugin
// Add JS files to the page header
$this->ui->addJS();
-
+
$this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
$this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
$this->register_handler('plugin.calendar_select', array($this->ui, 'calendar_select'));
@@ -167,14 +169,16 @@ class calendar extends rcube_plugin
$this->register_handler('plugin.alarm_select', array($this->ui, 'alarm_select'));
$this->register_handler('plugin.snooze_select', array($this->ui, 'snooze_select'));
$this->register_handler('plugin.recurrence_form', array($this->ui, 'recurrence_form'));
+ $this->register_handler('plugin.attachments_form', array($this->ui, 'attachments_form'));
+ $this->register_handler('plugin.attachments_list', array($this->ui, 'attachments_list'));
$this->register_handler('plugin.edit_recurring_warning', array($this->ui, 'recurring_event_warning'));
$this->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
-
- $this->rc->output->add_label('low','normal','high');
+
+ $this->rc->output->add_label('low','normal','high','delete','cancel','uploading');
$this->rc->output->send("calendar.calendar");
}
-
+
/**
* Handler for preferences_sections_list hook.
* Adds Calendar settings sections into preferences sections list.
@@ -426,16 +430,20 @@ class calendar extends rcube_plugin
$action = get_input_value('action', RCUBE_INPUT_POST);
$event = get_input_value('e', RCUBE_INPUT_POST);
$success = $reload = false;
-
+
switch ($action) {
case "new":
// create UID for new event
$event['uid'] = $this->generate_uid();
- $success = $this->driver->new_event($event);
+ $this->prepare_event($event);
+ if ($success = $this->driver->new_event($event))
+ $this->cleanup_event($event);
$reload = true;
break;
case "edit":
- $success = $this->driver->edit_event($event);
+ $this->prepare_event($event);
+ if ($success = $this->driver->edit_event($event))
+ $this->cleanup_event($event);
$reload = true;
break;
case "resize":
@@ -616,7 +624,7 @@ class calendar extends rcube_plugin
$event['alarms_text'] = $this->_alarms_text($event['alarms']);
if ($event['recurrence'])
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
-
+
$json[] = array(
'start' => gmdate('c', $this->fromGMT($event['start'])), // client treats date strings as they were in users's timezone
'end' => gmdate('c', $this->fromGMT($event['end'])), // so shift timestamps to users's timezone and render a date string
@@ -842,4 +850,250 @@ class calendar extends rcube_plugin
$this->rc->output->redirect('');
}
+ /**
+ * Handler for attachments upload
+ */
+ public function attachment_upload()
+ {
+ $event = get_input_value('_id', RCUBE_INPUT_GPC);
+ $calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
+ $uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
+
+ $eventid = $calendar.':'.$event;
+
+ if (!is_array($_SESSION['event_session']) || $_SESSION['event_session']['id'] != $eventid) {
+ $_SESSION['event_session'] = array();
+ $_SESSION['event_session']['id'] = $eventid;
+ $_SESSION['event_session']['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) {
+ $attachment = array(
+ 'path' => $filepath,
+ 'size' => $_FILES['_attachments']['size'][$i],
+ 'name' => $_FILES['_attachments']['name'][$i],
+ 'mimetype' => rc_mime_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
+ 'group' => $eventid,
+ );
+
+ $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']);
+ $_SESSION['event_session']['attachments'][$id] = $attachment;
+
+ if (($icon = $_SESSION['calendar_deleteicon']) && is_file($icon)) {
+ $button = html::img(array(
+ 'src' => $icon,
+ 'alt' => rcube_label('delete')
+ ));
+ }
+ else {
+ $button = Q(rcube_label('delete'));
+ }
+
+ $content = html::a(array(
+ 'href' => "#delete",
+ 'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
+ 'title' => rcube_label('delete'),
+ ), $button);
+
+ $content .= Q($attachment['name']);
+
+ $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
+ 'html' => $content,
+ 'name' => $attachment['name'],
+ 'mimetype' => $attachment['mimetype'],
+ 'complete' => true), $uploadid);
+ }
+ else { // upload failed
+ 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 if ($attachment['error']) {
+ $msg = $attachment['error'];
+ }
+ else {
+ $msg = rcube_label('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 = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
+ 'size' => show_bytes(parse_bytes($maxsize)))));
+ else
+ $msg = rcube_label('fileuploaderror');
+
+ $this->rc->output->command('display_message', $msg, 'error');
+ $this->rc->output->command('remove_from_attachment_list', $uploadid);
+ }
+
+ $this->rc->output->send('iframe');
+ }
+
+ /**
+ * Handler for attachments download/displaying
+ */
+ public function attachment_get()
+ {
+ $event = get_input_value('_event', RCUBE_INPUT_GPC);
+ $calendar = get_input_value('_cal', RCUBE_INPUT_GPC);
+ $id = get_input_value('_id', RCUBE_INPUT_GPC);
+
+ $event = array('id' => $event, 'calendar' => $calendar);
+
+ // show loading page
+ if (!empty($_GET['_preload'])) {
+ $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
+ $message = rcube_label('loadingdata');
+
+ header('Content-Type: text/html; charset=' . RCMAIL_CHARSET);
+ print "\n
\n"
+ . '' . "\n"
+ . '' . "\n"
+ . "\n\n$message\n\n";
+ exit;
+ }
+
+ ob_end_clean();
+ send_nocacheing_headers();
+
+ if (isset($_SESSION['calendar_attachment']))
+ $attachment = $_SESSION['calendar_attachment'];
+ else
+ $attachment = $_SESSION['calendar_attachment'] = $this->driver->get_attachment($id, $event);
+
+ // show part page
+ if (!empty($_GET['_frame'])) {
+ $this->rc->output->add_handlers(array('attachmentframe' => array($this, 'attachment_frame')));
+ $this->rc->output->send('calendar.attachment');
+ exit;
+ }
+
+ unset($_SESSION['calendar_attachment']);
+
+ if ($attachment) {
+ $mimetype = strtolower($attachment['mimetype']);
+ list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+ $browser = $this->rc->output->browser;
+
+ // send download headers
+ if ($_GET['_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 {
+// $mimetype = rcmail_fix_mimetype($mimetype);
+ header("Content-Type: $mimetype");
+ header("Content-Transfer-Encoding: binary");
+ }
+
+ $body = $this->driver->get_attachment_body($id, $event);
+
+ // display page, @TODO: support text/plain (and maybe some other text formats)
+ if ($mimetype == 'text/html' && empty($_GET['_download'])) {
+ $OUTPUT = new rcube_html_page();
+ // @TODO: use washtml on $body
+ $OUTPUT->write($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\"");
+ }
+
+ exit;
+ }
+
+ // if we arrive here, the requested part was not found
+ header('HTTP/1.1 404 Not Found');
+ exit;
+ }
+
+ /**
+ * Template object for attachment display frame
+ */
+ public function attachment_frame($attrib)
+ {
+ $attachment = $_SESSION['calendar_attachment'];
+
+ $mimetype = strtolower($attachment['mimetype']);
+ list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+ $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
+
+ return html::iframe($attrib);
+ }
+
+ /**
+ * Prepares new/edited event properties before save
+ */
+ private function prepare_event(&$event)
+ {
+ $eventid = $event['calendar'].':'.$event['id'];
+
+ $attachments = array();
+ if (is_array($_SESSION['event_session']) && $_SESSION['event_session']['id'] == $eventid) {
+ if (!empty($_SESSION['event_session']['attachments'])) {
+ foreach ($_SESSION['event_session']['attachments'] as $id => $attachment) {
+ if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
+ $attachments[$id] = $attachment;
+ }
+ }
+ }
+ }
+
+ $event['attachments'] = $attachments;
+ }
+
+ /**
+ * Releases some resources after successful event save
+ */
+ private function cleanup_event(&$event)
+ {
+ // remove temp. attachment files
+ if (!empty($_SESSION['event_session']) && ($eventid = $_SESSION['event_session']['id'])) {
+ $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
+ unset($_SESSION['event_session']);
+ }
+ }
+
}
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index e98bbc0f..6a42c7c6 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -114,6 +114,78 @@ function rcube_calendar_ui(settings)
return fromto;
};
+ var load_attachment = function(event, att)
+ {
+ var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.id)+'&_cal='+urlencode(event.calendar);
+
+ // open attachment in frame if it's of a supported mimetype
+ if (id && att.mimetype && $.inArray(att.mimetype, rcmail.mimetypes)>=0) {
+ rcmail.attachment_win = window.open(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', 'rcubeeventattachment');
+ if (rcmail.attachment_win) {
+ window.setTimeout(function() { rcmail.attachment_win.focus(); }, 10);
+ return;
+ }
+ }
+
+ rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
+ };
+
+ // build event attachments list
+ var event_show_attachments = function(list, container, event, edit)
+ {
+ var i, id, len, img, content, li, elem,
+ ul = document.createElement('UL');
+
+ for (i=0, len=list.length; i 0) {
+ $('#event-attachments').show();
+ }
+ }
+ else if (calendar.attachments) {
+ // fetch attachments, some drivers doesn't set 'attachments' popr of the event
+ }
+
var buttons = {};
if (calendar.editable && event.editable !== false) {
buttons[rcmail.gettext('edit', 'calendar')] = function() {
@@ -306,7 +389,24 @@ function rcube_calendar_ui(settings)
}
else
$('#edit-recurring-warning').hide();
-
+
+ // attachments
+ if (calendar.attachments) {
+ rcmail.enable_command('remove-attachment', !calendar.readonly);
+ rcmail.env.deleted_attachments = [];
+ // we're sharing some code for uploads handling with app.js
+ rcmail.env.attachments = [];
+ rcmail.env.compose_id = event.id; // for rcmail.async_upload_form()
+
+ if ($.isArray(event.attachments)) {
+ event_show_attachments(event.attachments, $('#edit-attachments'), event, true);
+ }
+ else {
+ $('#edit-attachments > ul').empty();
+ // fetch attachments, some drivers doesn't set 'attachments' array for event
+ }
+ }
+
// buttons
var buttons = {};
@@ -334,9 +434,10 @@ function rcube_calendar_ui(settings)
priority: priority.val(),
sensitivity: sensitivity.val(),
recurrence: '',
- alarms: ''
+ alarms: '',
+ deleted_attachments: rcmail.env.deleted_attachments
};
-
+
// serialize alarm settings
// TODO: support multiple alarm entries
var alarm = $('select.edit-alarm-type').val();
@@ -347,7 +448,14 @@ function rcube_calendar_ui(settings)
else if ((val = parseInt($('input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
data.alarms = offset[0] + val + offset[1] + ':' + alarm;
}
-
+
+ // uploaded attachments list
+ var attachments = [];
+ for (var i in rcmail.env.attachments)
+ if (i.match(/^rcmfile([0-9a-z]+)/))
+ attachments.push(RegExp.$1);
+ data.attachments = attachments;
+
// gather recurrence settings
var freq;
if ((freq = recurrence.val()) != '') {
@@ -1065,7 +1173,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
// configure toobar buttons
rcmail.register_command('addevent', function(){ cal.add_event(); }, true);
-
+
// configure list operations
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);
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 1eed9bfb..145bf775 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -51,6 +51,13 @@
* 'sensitivity' => 0|1|2, // Event sensitivity (0=public, 1=private, 2=confidential)
* 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event)
* 'savemode' => 'all|future|current|new', // How changes on recurring event should be handled
+ * 'attachments' => array( // List of attachments
+ * 'name' => 'File name',
+ * 'mimetype' => 'Content type',
+ * 'size' => 1..n, // in bytes
+ * 'id' => 'Attachment identifier'
+ * ),
+ * 'deleted_attachments' => array(), // array of attachment identifiers to delete when event is updated
* );
*/
@@ -196,14 +203,49 @@ abstract class calendar_driver
abstract function dismiss_alarm($event_id, $snooze = 0);
/**
- * Save an attachment related to the given event
+ * Get list of event's attachments.
+ * Drivers can return list of attachments as event property.
+ * If they will do not do this list_attachments() method will be used.
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array List of attachments, each as hash array:
+ * id: Attachment identifier
+ * name: Attachment name
+ * mimetype: MIME content type of the attachment
+ * size: Attachment size
*/
- public function add_attachment($attachment, $event_id) { }
+ public function list_attachments($event) { }
/**
- * Remove a specific attachment from the given event
+ * Get attachment properties
+ *
+ * @param string $id Attachment identifier
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array Hash array with attachment properties:
+ * id: Attachment identifier
+ * name: Attachment name
+ * mimetype: MIME content type of the attachment
+ * size: Attachment size
*/
- public function remove_attachment($attachment, $event_id) { }
+ public function get_attachment($id, $event) { }
+
+ /**
+ * Get attachment body
+ *
+ * @param string $id Attachment identifier
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return string Attachment body
+ */
+ public function get_attachment_body($id, $event) { }
/**
* List availabale categories
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 865c8cde..20fd5cf4 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -156,22 +156,15 @@ class database_driver extends calendar_driver
{
if (!$this->calendars[$prop['id']])
return false;
-
- // delete all events of this calendar
- $query = $this->rc->db->query(
- "DELETE FROM " . $this->db_events . "
- WHERE calendar_id=?",
- $prop['id']
- );
-
- // TODO: also delete linked attachments
-
+
+ // events and attachments will be deleted by foreign key cascade
+
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_calendars . "
WHERE calendar_id=?",
$prop['id']
);
-
+
return $this->rc->db->affected_rows($query);
}
@@ -213,11 +206,28 @@ class database_driver extends calendar_driver
$event['alarms'],
$event['notifyat']
);
-
- if ($success = $this->rc->db->insert_id($this->sequence_events))
+
+ $event_id = $this->rc->db->insert_id($this->sequence_events);
+
+ if ($event_id) {
+ // add attachments
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $attachment = $this->rc->plugins->exec_hook('attachment_get', $attachment);
+
+ if (!$attachment['data']) {
+ $attachments['data'] = file_get_contents($attachment['path']);
+ }
+
+ $this->add_attachment($attachment, $event_id);
+ unset($attachment);
+ }
+ }
+
$this->_update_recurring($event);
-
- return $success;
+ }
+
+ return $event_id;
}
return false;
@@ -403,11 +413,33 @@ class database_driver extends calendar_driver
),
$event['id']
);
-
+
$success = $this->rc->db->affected_rows($query);
+
+ // add attachments
+ if ($success && !empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $attachment = $this->rc->plugins->exec_hook('attachment_get', $attachment);
+
+ if (!$attachment['data']) {
+ $attachments['data'] = file_get_contents($attachment['path']);
+ }
+
+ $this->add_attachment($attachment, $event['id']);
+ unset($attachment);
+ }
+ }
+
+ // remove attachments
+ if ($success && !empty($event['deleted_attachments'])) {
+ foreach ($event['deleted_attachments'] as $attachment) {
+ $this->remove_attachment($attachment, $event['id']);
+ }
+ }
+
if ($success && $update_recurring)
$this->_update_recurring($event);
-
+
return $success;
}
@@ -736,19 +768,110 @@ class database_driver extends calendar_driver
/**
* Save an attachment related to the given event
*/
- public function add_attachment($attachment, $event_id)
+ private function add_attachment($attachment, $event_id)
{
- // TBD.
- return false;
+ $query = $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_attachments .
+ " (event_id, filename, mimetype, size, data)" .
+ " VALUES (?, ?, ?, ?, ?)",
+ $event_id,
+ $attachment['name'],
+ $attachment['mimetype'],
+ strlen($attachment['data']),
+ base64_encode($attachment['data']),
+ );
+
+ return $this->rc->db->affected_rows($query);
}
/**
* Remove a specific attachment from the given event
*/
- public function remove_attachment($attachment, $event_id)
+ private function remove_attachment($attachment_id, $event_id)
{
- // TBD.
- return false;
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_attachments .
+ " WHERE attachment_id = ?" .
+ " AND event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id = ?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))",
+ $attachment_id,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * List attachments of specified event
+ */
+ public function list_attachments($event)
+ {
+ $attachments = array();
+
+ if (!empty($this->rc->user->ID)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE user_id=?".
+ " AND event_id=?".
+ "ORDER BY filename",
+ $this->rc->user->ID,
+ $event['id']
+ );
+
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $attachments[] = $arr;
+ }
+ }
+
+ return $attachments;
+ }
+
+ /**
+ * Get attachment properties
+ */
+ public function get_attachment($id, $event)
+ {
+ if (!empty($this->rc->user->ID)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?".
+ " AND event_id=?".
+ $id,
+ $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return $arr;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get attachment body
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!empty($this->rc->user->ID)) {
+ $result = $this->rc->db->query(
+ "SELECT data " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?".
+ " AND event_id=?".
+ $id,
+ $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return base64_decode($arr['data']);
+ }
+ }
+
+ return null;
}
/**
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index d1c92e72..e9cf514b 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -23,6 +23,7 @@ class kolab_calendar
public $id;
public $ready = false;
public $readonly = true;
+ public $attachments = true;
private $cal;
private $storage;
@@ -145,6 +146,15 @@ class kolab_calendar
}
+ /**
+ * Getter for the attachment body
+ */
+ public function get_attachment_body($id)
+ {
+ return $this->storage->getAttachment($id);
+ }
+
+
/**
* Getter for a single event object
*/
@@ -206,7 +216,7 @@ class kolab_calendar
$events = array_merge($events, $this->_get_recurring_events($event, $start, $end));
}
}
-
+
return $events;
}
@@ -222,7 +232,7 @@ class kolab_calendar
{
if (!is_array($event))
return false;
-
+
//generate new event from RC input
$object = $this->_from_rcube_event($event);
$saved = $this->storage->save($object);
@@ -415,10 +425,24 @@ class kolab_calendar
$rrule['EXDATE'][] = strtotime($excl . date(' H:i:s', $rec['start-date'])); // use time of event start
}
}
-
+
$sensitivity_map = array_flip($this->sensitivity_map);
$priority_map = array_flip($this->priority_map);
-
+
+ // @TODO: Horde code assumes that there will be no more than
+ // one file with the same name, while this is not required by MIME format
+ // and not forced by the Calendar UI
+ if (!empty($rec['_attachments'])) {
+ foreach ($rec['_attachments'] as $name => $attachment) {
+ // @TODO: 'type' and 'key' are the only supported (no 'size')
+ $attachments[] = array(
+ 'id' => $attachment['key'],
+ 'mimetype' => $attachment['type'],
+ 'name' => $name,
+ );
+ }
+ }
+
return array(
'id' => $rec['uid'],
'uid' => $rec['uid'],
@@ -431,6 +455,7 @@ class kolab_calendar
'recurrence' => $rrule,
'alarms' => $alarm_value . $alarm_unit,
'categories' => $rec['categories'],
+ 'attachments' => $attachments,
'free_busy' => $rec['show-time-as'],
'priority' => isset($priority_map[$rec['priority']]) ? $priority_map[$rec['priority']] : 1,
'sensitivity' => $sensitivity_map[$rec['sensitivity']],
@@ -555,7 +580,18 @@ class kolab_calendar
$object['start-date'] += $tz_offset - date('Z'); // because Horde_Kolab_Format_Date::encodeDate() uses strftime()
$object['_is_all_day'] = 1;
}
-
+
+ // in Horde attachments are indexed by name
+ $object['_attachments'] = array();
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $idx => $attachment) {
+ // Roundcube ID has nothing to Horde ID, remove it
+ unset($attachment['id']);
+ $object['_attachments'][$attachment['name']] = $attachment;
+ unset($event['attachments'][$idx]);
+ }
+ }
+
return $object;
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 5f448404..779253fd 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -26,7 +26,7 @@ class kolab_driver extends calendar_driver
// features this backend supports
public $alarms = true;
public $attendees = false;
- public $attachments = false;
+ public $attachments = true;
public $categoriesimmutable = true;
private $rc;
@@ -229,8 +229,20 @@ class kolab_driver extends calendar_driver
public function new_event($event)
{
$cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
- if ($storage = $this->calendars[$cid])
+ if ($storage = $this->calendars[$cid]) {
+ // handle attachments to add
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $idx => $attachment) {
+ // we'll read file contacts into memory, Horde/Kolab classes does the same
+ // So we cannot save memory, rcube_imap class can do this better
+ $attachment = $this->cal->rc->plugins->exec_hook('attachment_get', $attachment);
+
+ $event['attachments'][$idx]['content'] = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
+ }
+ }
+
return $storage->insert_event($event);
+ }
return false;
}
@@ -327,21 +339,52 @@ class kolab_driver extends calendar_driver
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
-
+
$success = false;
$savemode = 'all';
+ $attachments = array();
$old = $master = $storage->get_event($event['id']);
-
+
+ // delete existing attachment(s)
+ if (!empty($event['deleted_attachments'])) {
+ foreach ($event['deleted_attachments'] as $attachment) {
+ if (!empty($old['attachments'])) {
+ foreach ($old['attachments'] as $idx => $att) {
+ if ($att['id'] == $attachment) {
+ unset($old['attachments'][$idx]);
+ }
+ }
+ }
+ }
+ }
+
+ // handle attachments to add
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ // we'll read file contacts into memory, Horde/Kolab classes does the same
+ // So we cannot save memory, rcube_imap class can do this better
+ $attachment = $this->cal->rc->plugins->exec_hook('attachment_get', $attachment);
+
+ $attachments[] = array(
+ 'name' => $attachment['name'],
+ 'type' => $attachment['mimetype'],
+ 'content' => $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']),
+ );
+ }
+ }
+
+ $event['attachments'] = array_merge($old['attachments'], $attachments);
+
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id']) {
$master = $old['recurrence_id'] ? $storage->get_event($old['recurrence_id']) : $old;
$savemode = $event['savemode'];
}
-
+
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'])
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
-
+
switch ($savemode) {
case 'new':
// save submitted data as new (non-recurring) event
@@ -525,23 +568,51 @@ class kolab_driver extends calendar_driver
return $this->rc->db->affected_rows($query);
}
-
+
/**
- * Save an attachment related to the given event
+ * List attachments from the given event
*/
- public function add_attachment($attachment, $event_id)
+ public function list_attachments($event)
{
-
+ if (!($storage = $this->calendars[$event['calendar']]))
+ return false;
+
+ $event = $storage->get_event($event['id']);
+
+ return $event['attachments'];
}
/**
- * Remove a specific attachment from the given event
+ * Get attachment properties
*/
- public function remove_attachment($attachment, $event_id)
+ public function get_attachment($id, $event)
{
-
+ if (!($storage = $this->calendars[$event['calendar']]))
+ return false;
+
+ $event = $storage->get_event($event['id']);
+
+ if ($event && !empty($event['attachments'])) {
+ foreach ($event['attachments'] as $att) {
+ if ($att['id'] == $id) {
+ return $att;
+ }
+ }
+ }
+
+ return null;
}
+ /**
+ * Get attachment body
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!($storage = $this->calendars[$event['calendar']]))
+ return false;
+
+ return $storage->get_attachment_body($id);
+ }
/**
* List availabale categories
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index e897e4cb..2c24d62f 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -461,4 +461,60 @@ class calendar_ui
return $select_prefix->show() . ' ' . $select_wday->show();
}
-}
\ No newline at end of file
+
+ /**
+ * Generate the form for event attachments upload
+ */
+ function attachments_form($attrib = array())
+ {
+ // add ID if not given
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmUploadForm';
+
+ // find max filesize value
+ $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
+ $max_postsize = parse_bytes(ini_get('post_max_size'));
+ if ($max_postsize && $max_postsize < $max_filesize)
+ $max_filesize = $max_postsize;
+
+ $this->rc->output->set_env('max_filesize', $max_filesize);
+
+ $max_filesize = show_bytes($max_filesize);
+
+ $button = new html_inputfield(array('type' => 'button'));
+ $input = new html_inputfield(array(
+ 'type' => 'file', 'name' => '_attachments[]',
+ 'multiple' => 'multiple', 'size' => $attrib['attachmentfieldsize']));
+
+ return html::div($attrib,
+ html::div(null, $input->show()) .
+ html::div('buttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction',
+ 'onclick' => JS_OBJECT_NAME . ".upload_file(this.form)"))) .
+ html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
+ );
+ }
+
+ /**
+ * Generate HTML element for attachments list
+ */
+ function attachments_list($attrib = array())
+ {
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmAttachmentList';
+
+ $skin_path = $this->rc->config->get('skin_path');
+ if ($attrib['deleteicon']) {
+ $_SESSION['calendar_deleteicon'] = $skin_path . $attrib['deleteicon'];
+ $this->rc->output->set_env('deleteicon', $skin_path . $attrib['deleteicon']);
+ }
+ if ($attrib['cancelicon'])
+ $this->rc->output->set_env('cancelicon', $skin_path . $attrib['cancelicon']);
+ if ($attrib['loadingicon'])
+ $this->rc->output->set_env('loadingicon', $skin_path . $attrib['loadingicon']);
+
+ $this->rc->output->add_gui_object('attachmentlist', $attrib['id']);
+
+ return html::tag('ul', $attrib, '', html::$common_attrib);
+ }
+
+}
diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css
index 22cc5155..c1663fc6 100644
--- a/plugins/calendar/skins/default/calendar.css
+++ b/plugins/calendar/skins/default/calendar.css
@@ -247,6 +247,71 @@ a.miniColors-trigger {
margin-top: -3px;
}
+#attachmentcontainer
+{
+ position: absolute;
+ top: 80px;
+ left: 20px;
+ right: 20px;
+ bottom: 20px;
+}
+
+#attachmentframe
+{
+ width: 100%;
+ height: 100%;
+ border: 1px solid #999999;
+ background-color: #F9F9F9;
+}
+
+.attachments-list ul
+{
+ margin: 0px;
+ padding: 0px;
+ list-style-image: none;
+ list-style-type: none;
+}
+
+.attachments-list ul li
+{
+ height: 18px;
+ font-size: 12px;
+ padding-left: 2px;
+ padding-top: 2px;
+ padding-right: 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -o-text-overflow: ellipsis;
+}
+
+.attachments-list ul li img
+{
+ padding-right: 2px;
+ vertical-align: middle;
+}
+
+.attachments-list ul li a
+{
+ text-decoration: none;
+}
+
+.attachments-list ul li a:hover
+{
+ text-decoration: underline;
+}
+
+#eventshow .attachments-list ul
+{
+ display: block;
+}
+
+#eventshow .attachments-list ul li
+{
+ float: left;
+}
+
+
/* jQuery UI overrides */
#eventshow h1 {
diff --git a/plugins/calendar/skins/default/templates/attachment.html b/plugins/calendar/skins/default/templates/attachment.html
new file mode 100644
index 00000000..cd39109e
--- /dev/null
+++ b/plugins/calendar/skins/default/templates/attachment.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html
index 592e1062..938e88b8 100644
--- a/plugins/calendar/skins/default/templates/calendar.html
+++ b/plugins/calendar/skins/default/templates/calendar.html
@@ -7,6 +7,7 @@
+
@@ -75,10 +76,14 @@
+
+
+
+
-
+
-
+
-
+
@@ -235,4 +245,4 @@ $(document).ready(function(e){
-