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 @@ + + + +<roundcube:object name="pagetitle" /> + + + + + + +
+ +
+ [] +
+
+ + +
+ +
+ + + + + + +<roundcube:object name="pagetitle" /> + + + + + + +
+ +
+ [] +
+
+ + +
+ +
+ + + 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 @@ +
+ + +
-
+
  • @@ -161,14 +166,19 @@
- +
- +
- +
+ +
+ + +
- +