Page MenuHomePhorge

No OneTemporary

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index a742efa2..98c7b584 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1,2605 +1,2681 @@
<?php
/**
* Calendar plugin for Roundcube webmail
*
* @version @package_version@
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class calendar extends rcube_plugin
{
const FREEBUSY_UNKNOWN = 0;
const FREEBUSY_FREE = 1;
const FREEBUSY_BUSY = 2;
const FREEBUSY_TENTATIVE = 3;
const FREEBUSY_OOF = 4;
const SESSION_KEY = 'calendar_temp';
public $task = '?(?!logout).*';
public $rc;
public $lib;
public $resources_dir;
public $home; // declare public to be used in other classes
public $urlbase;
public $timezone;
public $timezone_offset;
public $gmt_offset;
public $ui;
public $defaults = array(
'calendar_default_view' => "agendaWeek",
'calendar_timeslots' => 2,
'calendar_work_start' => 6,
'calendar_work_end' => 18,
'calendar_agenda_range' => 60,
'calendar_agenda_sections' => 'smart',
'calendar_event_coloring' => 0,
'calendar_time_indicator' => true,
'calendar_allow_invite_shared' => false,
);
private $ical;
private $itip;
private $driver;
private $ics_parts = array();
/**
* Plugin initialization.
*/
function init()
{
$this->require_plugin('libcalendaring');
$this->rc = rcube::get_instance();
$this->lib = libcalendaring::get_instance();
$this->register_task('calendar', 'calendar');
// load calendar configuration
$this->load_config();
// load localizations
$this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));
$this->timezone = $this->lib->timezone;
$this->gmt_offset = $this->lib->gmt_offset;
$this->dst_active = $this->lib->dst_active;
$this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
require($this->home . '/lib/calendar_ui.php');
$this->ui = new calendar_ui($this);
// catch iTIP confirmation requests that don're require a valid session
if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {
$this->add_hook('startup', array($this, 'itip_attend_response'));
}
else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) {
$this->add_hook('startup', array($this, 'ical_feed_export'));
}
else {
// default startup routine
$this->add_hook('startup', array($this, 'startup'));
}
}
/**
* Startup hook
*/
public function startup($args)
{
// the calendar module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true))
return;
// load Calendar user interface
- if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
+ if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || $args['action'] == 'preview')) {
$this->ui->init();
// settings are required in (almost) every GUI step
if ($args['action'] != 'attend')
$this->rc->output->set_env('calendar_settings', $this->load_settings());
}
if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') {
if ($args['action'] != 'upload') {
$this->load_driver();
}
// register calendar actions
$this->register_action('index', array($this, 'calendar_view'));
$this->register_action('event', array($this, 'event_action'));
$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'));
$this->register_action('freebusy-times', array($this, 'freebusy_times'));
$this->register_action('randomdata', array($this, 'generate_randomdata'));
$this->register_action('print', array($this,'print_view'));
- $this->register_action('mailimportitip', array($this, 'mail_import_event'));
+ $this->register_action('mailimportitip', array($this, 'mail_import_itip'));
+ $this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
$this->register_action('mailtoevent', array($this, 'mail_message2event'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('check-recent', array($this, 'check_recent'));
$this->register_action('itip-status', array($this, 'event_itip_status'));
$this->register_action('itip-remove', array($this, 'event_itip_remove'));
$this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
$this->register_action('resources-list', array($this, 'resources_list'));
$this->register_action('resources-owner', array($this, 'resources_owner'));
$this->register_action('resources-calendar', array($this, 'resources_calendar'));
$this->register_action('resources-autocomplete', array($this, 'resources_autocomplete'));
$this->add_hook('refresh', array($this, 'refresh'));
// remove undo information...
if ($undo = $_SESSION['calendar_event_undo']) {
// ...after timeout
$undo_time = $this->rc->config->get('undo_timeout', 0);
if ($undo['ts'] < time() - $undo_time) {
$this->rc->session->remove('calendar_event_undo');
// @TODO: do EXPUNGE on kolab objects?
}
}
}
else if ($args['task'] == 'settings') {
// add hooks for Calendar settings
$this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
$this->add_hook('preferences_list', array($this, 'preferences_list'));
$this->add_hook('preferences_save', array($this, 'preferences_save'));
}
else if ($args['task'] == 'mail') {
// hooks to catch event invitations on incoming mails
if ($args['action'] == 'show' || $args['action'] == 'preview') {
$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' => 'calendar-create-from-mail',
'label' => 'calendar.createfrommail',
'type' => 'link',
'classact' => 'icon calendarlink active',
'class' => 'icon calendarlink',
'innerclass' => 'icon calendar',
))),
'messagemenu');
$this->api->output->add_label('calendar.createfrommail');
}
}
else if ($args['task'] == 'addressbook') {
if ($this->rc->config->get('calendar_contact_birthdays')) {
$this->add_hook('contact_update', array($this, 'contact_update'));
$this->add_hook('contact_create', array($this, 'contact_update'));
}
}
// add hooks to display alarms
$this->add_hook('pending_alarms', array($this, 'pending_alarms'));
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
/**
* 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('calendar_driver', 'database');
$driver_class = $driver_name . '_driver';
require_once($this->home . '/drivers/calendar_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
$this->driver = new $driver_class($this);
if ($this->driver->undelete)
$this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0;
}
/**
* Load iTIP functions
*/
private function load_itip()
{
if (!$this->itip) {
require_once($this->home . '/lib/calendar_itip.php');
$this->itip = new calendar_itip($this);
}
return $this->itip;
}
/**
* Load iCalendar functions
*/
public function get_ical()
{
if (!$this->ical) {
$this->ical = libcalendaring::get_ical();
}
return $this->ical;
}
/**
* Get properties of the calendar this user has specified as default
*/
public function get_default_calendar($writeable = false)
{
$default_id = $this->rc->config->get('calendar_default_calendar');
$calendars = $this->driver->list_calendars(false, true);
$calendar = $calendars[$default_id] ?: null;
if (!$calendar || ($writeable && $calendar['readonly'])) {
foreach ($calendars as $cal) {
if ($cal['default']) {
$calendar = $cal;
break;
}
if (!$writeable || !$cal['readonly']) {
$first = $cal;
}
}
}
return $calendar ?: $first;
}
/**
* Render the main calendar view from skin template
*/
function calendar_view()
{
$this->rc->output->set_pagetitle($this->gettext('calendar'));
// Add CSS stylesheets to the page header
$this->ui->addCSS();
// Add JS files to the page header
$this->ui->addJS();
$this->ui->init_templates();
$this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close');
// initialize attendees autocompletion
rcube_autocomplete_init();
$this->rc->output->set_env('timezone', $this->timezone->getName());
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver'));
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list')));
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$this->rc->output->set_env('view', $view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
$this->rc->output->send("calendar.calendar");
}
/**
* Handler for preferences_sections_list hook.
* Adds Calendar settings sections into preferences sections list.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_sections_list($p)
{
$p['list']['calendar'] = array(
'id' => 'calendar', 'section' => $this->gettext('calendar'),
);
return $p;
}
/**
* Handler for preferences_list hook.
* Adds options blocks into Calendar settings sections in Preferences.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_list($p)
{
if ($p['section'] != 'calendar') {
return $p;
}
$no_override = array_flip((array)$this->rc->config->get('dont_override'));
$p['blocks']['view']['name'] = $this->gettext('mainoptions');
if (!isset($no_override['calendar_default_view'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_default_view';
$select = new html_select(array('name' => '_default_view', 'id' => $field_id));
$select->add($this->gettext('day'), "agendaDay");
$select->add($this->gettext('week'), "agendaWeek");
$select->add($this->gettext('month'), "month");
$select->add($this->gettext('agenda'), "table");
$p['blocks']['view']['options']['default_view'] = array(
'title' => html::label($field_id, Q($this->gettext('default_view'))),
'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])),
);
}
if (!isset($no_override['calendar_timeslots'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_timeslot';
$choices = array('1', '2', '3', '4', '6');
$select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
$select->add($choices);
$p['blocks']['view']['options']['timeslots'] = array(
'title' => html::label($field_id, Q($this->gettext('timeslots'))),
'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))),
);
}
if (!isset($no_override['calendar_first_day'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_firstday';
$select = new html_select(array('name' => '_first_day', 'id' => $field_id));
$select->add(rcube_label('sunday'), '0');
$select->add(rcube_label('monday'), '1');
$select->add(rcube_label('tuesday'), '2');
$select->add(rcube_label('wednesday'), '3');
$select->add(rcube_label('thursday'), '4');
$select->add(rcube_label('friday'), '5');
$select->add(rcube_label('saturday'), '6');
$p['blocks']['view']['options']['first_day'] = array(
'title' => html::label($field_id, Q($this->gettext('first_day'))),
'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))),
);
}
if (!isset($no_override['calendar_first_hour'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
$select_hours = new html_select();
for ($h = 0; $h < 24; $h++)
$select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
$field_id = 'rcmfd_firsthour';
$p['blocks']['view']['options']['first_hour'] = array(
'title' => html::label($field_id, Q($this->gettext('first_hour'))),
'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
);
}
if (!isset($no_override['calendar_work_start'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_workstart';
$p['blocks']['view']['options']['workinghours'] = array(
'title' => html::label($field_id, Q($this->gettext('workinghours'))),
'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
' &mdash; ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
);
}
if (!isset($no_override['calendar_event_coloring'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_coloring';
$select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
$select_colors->add($this->gettext('coloringmode0'), 0);
$select_colors->add($this->gettext('coloringmode1'), 1);
$select_colors->add($this->gettext('coloringmode2'), 2);
$select_colors->add($this->gettext('coloringmode3'), 3);
$p['blocks']['view']['options']['eventcolors'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))),
'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])),
);
}
// loading driver is expensive, don't do it if not needed
$this->load_driver();
if (!isset($no_override['calendar_default_alarm_type'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_alarm';
$select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
$select_type->add($this->gettext('none'), '');
foreach ($this->driver->alarm_types as $type)
$select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
$p['blocks']['view']['options']['alarmtype'] = array(
'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))),
'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')),
);
}
if (!isset($no_override['calendar_default_alarm_offset'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_alarm';
$input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
$select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
$preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$p['blocks']['view']['options']['alarmoffset'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
);
}
if (!isset($no_override['calendar_default_calendar'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
// default calendar selection
$field_id = 'rcmfd_default_calendar';
$select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true));
foreach ((array)$this->driver->list_calendars(false, true) as $id => $prop) {
$select_cal->add($prop['name'], strval($id));
if ($prop['default'])
$default_calendar = $id;
}
$p['blocks']['view']['options']['defaultcalendar'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))),
'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)),
);
}
// category definitions
if (!$this->driver->nocategories && !isset($no_override['calendar_categories'])) {
$p['blocks']['categories']['name'] = $this->gettext('categories');
if (!$p['current']) {
$p['blocks']['categories']['content'] = true;
return $p;
}
$categories = (array) $this->driver->list_categories();
$categories_list = '';
foreach ($categories as $name => $color) {
$key = md5($name);
$field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
$category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
$category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable));
$category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6));
$hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : '';
$categories_list .= html::div(null, $hidden . $category_name->show($name) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $category_remove->show());
}
$p['blocks']['categories']['options']['category_' . $name] = array(
'content' => html::div(array('id' => 'calendarcategories'), $categories_list),
);
$field_id = 'rcmfd_new_category';
$new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
$add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
$p['blocks']['categories']['options']['categories'] = array(
'content' => $new_category->show('') . '&nbsp;' . $add_category->show(),
);
$this->rc->output->add_script('function rcube_calendar_add_category(){
var name = $("#rcmfd_new_category").val();
if (name.length) {
var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
$("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").append(button).appendTo("#calendarcategories");
color.miniColors({ colorValues:(rcmail.env.mscolors || []) });
$("#rcmfd_new_category").val("");
}
}');
$this->rc->output->add_script('$("#rcmfd_new_category").keypress(function(event){
if (event.which == 13) {
rcube_calendar_add_category();
event.preventDefault();
}
});
', 'docready');
// include color picker
$this->include_script('lib/js/jquery.miniColors.min.js');
$this->include_stylesheet($this->local_skin_path() . '/jquery.miniColors.css');
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$this->rc->output->add_script('$("input.colors").miniColors({ colorValues:rcmail.env.mscolors })', 'docready');
}
// virtual birthdays calendar
if (!isset($no_override['calendar_contact_birthdays'])) {
$p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar');
if (!$p['current']) {
$p['blocks']['birthdays']['content'] = true;
return $p;
}
$field_id = 'rcmfd_contact_birthdays';
$input = new html_checkbox(array('name' => '_contact_birthdays', 'id' => $field_id, 'value' => 1, 'onclick' => '$(".calendar_birthday_props").prop("disabled",!this.checked)'));
$p['blocks']['birthdays']['options']['contact_birthdays'] = array(
'title' => html::label($field_id, $this->gettext('displaybirthdayscalendar')),
'content' => $input->show($this->rc->config->get('calendar_contact_birthdays')?1:0),
);
$input_attrib = array(
'class' => 'calendar_birthday_props',
'disabled' => !$this->rc->config->get('calendar_contact_birthdays'),
);
$sources = array();
$checkbox = new html_checkbox(array('name' => '_birthday_adressbooks[]') + $input_attrib);
foreach ($this->rc->get_address_sources(false, true) as $source) {
$active = in_array($source['id'], (array)$this->rc->config->get('calendar_birthday_adressbooks', array())) ? $source['id'] : '';
$sources[] = html::label(null, $checkbox->show($active, array('value' => $source['id'])) . '&nbsp;' . rcube::Q($source['realname'] ?: $source['name']));
}
$p['blocks']['birthdays']['options']['birthday_adressbooks'] = array(
'title' => rcube::Q($this->gettext('birthdayscalendarsources')),
'content' => join(html::br(), $sources),
);
$field_id = 'rcmfd_birthdays_alarm';
$select_type = new html_select(array('name' => '_birthdays_alarm_type', 'id' => $field_id) + $input_attrib);
$select_type->add($this->gettext('none'), '');
foreach ($this->driver->alarm_types as $type) {
$select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
}
$input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib);
$select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib);
foreach (array('-M','-H','-D') as $trigger)
$select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
$preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D'));
$p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = array(
'title' => html::label($field_id . 'value', rcube::Q($this->gettext('showalarms'))),
'content' => $select_type->show($this->rc->config->get('calendar_birthdays_alarm_type', '')) . ' ' . $input_value->show($preset[0]) . '&nbsp;' . $select_offset->show($preset[1]),
);
}
return $p;
}
/**
* Handler for preferences_save hook.
* Executed on Calendar settings form submit.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_save($p)
{
if ($p['section'] == 'calendar') {
$this->load_driver();
// compose default alarm preset value
$alarm_offset = get_input_value('_alarm_offset', RCUBE_INPUT_POST);
$default_alarm = $alarm_offset[0] . intval(get_input_value('_alarm_value', RCUBE_INPUT_POST)) . $alarm_offset[1];
$birthdays_alarm_offset = get_input_value('_birthdays_alarm_offset', RCUBE_INPUT_POST);
$birthdays_alarm_value = $birthdays_alarm_offset[0] . intval(get_input_value('_birthdays_alarm_value', RCUBE_INPUT_POST)) . $birthdays_alarm_offset[1];
$p['prefs'] = array(
'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST),
'calendar_timeslots' => intval(get_input_value('_timeslots', RCUBE_INPUT_POST)),
'calendar_first_day' => intval(get_input_value('_first_day', RCUBE_INPUT_POST)),
'calendar_first_hour' => intval(get_input_value('_first_hour', RCUBE_INPUT_POST)),
'calendar_work_start' => intval(get_input_value('_work_start', RCUBE_INPUT_POST)),
'calendar_work_end' => intval(get_input_value('_work_end', RCUBE_INPUT_POST)),
'calendar_event_coloring' => intval(get_input_value('_event_coloring', RCUBE_INPUT_POST)),
'calendar_default_alarm_type' => get_input_value('_alarm_type', RCUBE_INPUT_POST),
'calendar_default_alarm_offset' => $default_alarm,
'calendar_default_calendar' => get_input_value('_default_calendar', RCUBE_INPUT_POST),
'calendar_date_format' => null, // clear previously saved values
'calendar_time_format' => null,
'calendar_contact_birthdays' => get_input_value('_contact_birthdays', RCUBE_INPUT_POST) ? true : false,
'calendar_birthday_adressbooks' => (array)get_input_value('_birthday_adressbooks', RCUBE_INPUT_POST),
'calendar_birthdays_alarm_type' => get_input_value('_birthdays_alarm_type', RCUBE_INPUT_POST),
'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null,
);
// categories
if (!$this->driver->nocategories) {
$old_categories = $new_categories = array();
foreach ($this->driver->list_categories() as $name => $color) {
$old_categories[md5($name)] = $name;
}
$categories = (array) get_input_value('_categories', RCUBE_INPUT_POST);
$colors = (array) get_input_value('_colors', RCUBE_INPUT_POST);
foreach ($categories as $key => $name) {
$color = preg_replace('/^#/', '', strval($colors[$key]));
// rename categories in existing events -> driver's job
if ($oldname = $old_categories[$key]) {
$this->driver->replace_category($oldname, $name, $color);
unset($old_categories[$key]);
}
else
$this->driver->add_category($name, $color);
$new_categories[$name] = $color;
}
// these old categories have been removed, alter events accordingly -> driver's job
foreach ((array)$old_categories[$key] as $key => $name) {
$this->driver->remove_category($name);
}
$p['prefs']['calendar_categories'] = $new_categories;
}
}
return $p;
}
/**
* Dispatcher for calendar actions initiated by the client
*/
function calendar_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$cal = get_input_value('c', RCUBE_INPUT_GPC);
$success = $reload = false;
if (isset($cal['showalarms']))
$cal['showalarms'] = intval($cal['showalarms']);
switch ($action) {
case "form-new":
case "form-edit":
echo $this->ui->calendar_editform($action, $cal);
exit;
case "new":
$success = $this->driver->create_calendar($cal);
$reload = true;
break;
case "edit":
$success = $this->driver->edit_calendar($cal);
$reload = true;
break;
case "remove":
if ($success = $this->driver->remove_calendar($cal))
$this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
break;
case "subscribe":
if (!$this->driver->subscribe_calendar($cal))
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
return;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else {
$error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :'');
$this->rc->output->show_message($error_msg, 'error');
}
$this->rc->output->command('plugin.unlock_saving');
if ($success && $reload)
$this->rc->output->command('plugin.reload_view');
}
/**
* Dispatcher for event actions initiated by the client
*/
function event_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$event = get_input_value('e', RCUBE_INPUT_POST, true);
$success = $reload = $got_msg = false;
// don't notify if modifying a recurring instance (really?)
if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify'])
unset($event['_notify']);
// read old event data in order to find changes
if (($event['_notify'] || $event['decline']) && $action != 'new')
$old = $this->driver->get_event($event);
switch ($action) {
case "new":
// create UID for new event
$event['uid'] = $this->generate_uid();
$this->prepare_event($event, $action);
if ($success = $this->driver->new_event($event)) {
$event['id'] = $event['uid'];
$this->cleanup_event($event);
}
$reload = $success && $event['recurrence'] ? 2 : 1;
break;
case "edit":
$this->prepare_event($event, $action);
if ($success = $this->driver->edit_event($event))
$this->cleanup_event($event);
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
break;
case "resize":
$this->prepare_event($event, $action);
$success = $this->driver->resize_event($event);
$reload = $event['_savemode'] ? 2 : 1;
break;
case "move":
$this->prepare_event($event, $action);
$success = $this->driver->move_event($event);
$reload = $success && $event['_savemode'] ? 2 : 1;
break;
case "remove":
// remove previous deletes
$undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
$this->rc->session->remove('calendar_event_undo');
// search for event if only UID is given
if (!isset($event['calendar']) && $event['uid']) {
if (!($event = $this->driver->get_event($event, true))) {
break;
}
$undo_time = 0;
}
$success = $this->driver->remove_event($event, $undo_time < 1);
$reload = (!$success || $event['_savemode']) ? 2 : 1;
if ($undo_time > 0 && $success) {
$_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
// display message with Undo link.
$msg = html::span(null, $this->gettext('successremoval'))
. ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo'));
$this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
$got_msg = true;
}
else if ($success) {
$this->rc->output->show_message('calendar.successremoval', 'confirmation');
$got_msg = true;
}
// send iTIP reply that participant has declined the event
if ($success && $event['decline']) {
$emails = $this->get_user_emails();
foreach ($old['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = $attendee;
else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$old['attendees'][$i]['status'] = 'DECLINED';
$reply_sender = $attendee['email'];
}
}
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "undo":
// Restore deleted event
$event = $_SESSION['calendar_event_undo']['data'];
if ($event)
$success = $this->driver->restore_event($event);
if ($success) {
$this->rc->session->remove('calendar_event_undo');
$this->rc->output->show_message('calendar.successrestore', 'confirmation');
$got_msg = true;
$reload = 2;
}
break;
case "rsvp":
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$event = $ev;
if ($success = $this->driver->edit_event($event)) {
$status = get_input_value('status', RCUBE_INPUT_GPC);
$organizer = null;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
break;
}
}
$itip = $this->load_itip();
if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "dismiss":
$event['ids'] = explode(',', $event['id']);
$plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event);
$success = $plugin['success'];
foreach ($event['ids'] as $id) {
if (strpos($id, 'cal:') === 0)
$success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);
}
break;
}
// show confirmation/error message
if (!$got_msg) {
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
// send out notifications
if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) {
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
// only notify if data really changed (TODO: do diff check on client already)
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
$sent = $this->notify_attendees($event, $old, $action);
if ($sent > 0)
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
else if ($sent < 0)
$this->rc->output->show_message('calendar.errornotifying', 'error');
}
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
// update event object on the client or trigger a complete refretch if too complicated
if ($reload) {
$args = array('source' => $event['calendar']);
if ($reload > 1)
$args['refetch'] = true;
else if ($success && $action != 'remove')
$args['update'] = $this->_client_event($this->driver->get_event($event));
$this->rc->output->command('plugin.refresh_calendar', $args);
}
}
/**
* Handler for load-requests from fullcalendar
* This will return pure JSON formatted output
*/
function load_events()
{
$events = $this->driver->load_events(
get_input_value('start', RCUBE_INPUT_GET),
get_input_value('end', RCUBE_INPUT_GET),
($query = get_input_value('q', RCUBE_INPUT_GET)),
get_input_value('source', RCUBE_INPUT_GET)
);
echo $this->encode($events, !empty($query));
exit;
}
/**
* Handler for keep-alive requests
* This will check for updated data in active calendars and sync them to the client
*/
public function refresh($attr)
{
// refresh the entire calendar every 10th time to also sync deleted events
if (rand(0,10) == 10) {
$this->rc->output->command('plugin.refresh_calendar', array('refetch' => true));
return;
}
foreach ($this->driver->list_calendars(true) as $cal) {
$events = $this->driver->load_events(
get_input_value('start', RCUBE_INPUT_GET),
get_input_value('end', RCUBE_INPUT_GET),
get_input_value('q', RCUBE_INPUT_GET),
$cal['id'],
1,
$attr['last']
);
foreach ($events as $event) {
$this->rc->output->command('plugin.refresh_calendar',
array('source' => $cal['id'], 'update' => $this->_client_event($event)));
}
}
}
/**
* 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();
$time = $p['time'] ?: time();
if ($alarms = $this->driver->pending_alarms($time)) {
foreach ($alarms as $alarm) {
$alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal:
$p['alarms'][] = $alarm;
}
}
// get alarms for birthdays calendar
if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') {
$cache = $this->rc->get_cache('calendar.birthdayalarms', 'db');
foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) {
$alarm = libcalendaring::get_next_alarm($e);
// overwrite alarm time with snooze value (or null if dismissed)
if ($dismissed = $cache->get($e['id']))
$alarm['time'] = $dismissed['notifyat'];
// add to list if alarm is set
if ($alarm && $alarm['time'] && $alarm['time'] <= $time) {
$e['id'] = 'cal:bday:' . $e['id'];
$e['notifyat'] = $alarm['time'];
$p['alarms'][] = $e;
}
}
}
return $p;
}
/**
* Handler for alarm dismiss hook triggered by libcalendaring
*/
public function dismiss_alarms($p)
{
$this->load_driver();
foreach ((array)$p['ids'] as $id) {
if (strpos($id, 'cal:bday:') === 0) {
$p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']);
}
else if (strpos($id, 'cal:') === 0) {
$p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']);
}
}
return $p;
}
/**
* Handler for check-recent requests which are accidentally sent to calendar taks
*/
function check_recent()
{
// NOP
$this->rc->output->send();
}
/**
* Hook triggered when a contact is saved
*/
function contact_update($p)
{
// clear birthdays calendar cache
if (!empty($p['record']['birthday'])) {
$cache = $this->rc->get_cache('calendar.birthdays', 'db');
$cache->remove();
}
}
/**
*
*/
function import_events()
{
// Upload progress update
if (!empty($_GET['_progress'])) {
rcube_upload_progress();
}
@set_time_limit(0);
// 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);
$rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
// 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, $calendar, $rangestart, $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 teh uploaded file directly
$count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $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('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
}
}
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('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, $calendar, $rangestart, &$errors)
{
$user_email = $this->rc->user->get_username();
$ical = $this->get_ical();
$errors = !$ical->fopen($filepath);
$count = $i = 0;
foreach ($ical as $event) {
// keep the browser connection alive on long import jobs
if (++$i > 100 && $i % 100 == 0) {
echo "<!-- -->";
ob_flush();
}
// TODO: correctly handle recurring events which start before $rangestart
if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
continue;
$event['_owner'] = $user_email;
$event['calendar'] = $calendar;
if ($this->driver->new_event($event)) {
$count++;
}
else {
$errors++;
}
}
return $count;
}
/**
* Construct the ics file for exporting events to iCalendar format;
*/
function export_events($terminate = true)
{
$start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET);
if (!isset($start))
$start = 'today -1 year';
if (!is_numeric($start))
$start = strtotime($start . ' 00:00:00');
if (!$end)
$end = 'today +10 years';
if (!is_numeric($end))
$end = strtotime($end . ' 23:59:59');
$attachments = get_input_value('attachments', RCUBE_INPUT_GET);
$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;
$calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii
if (empty($calname)) $calname = $calid;
$events = $this->driver->load_events($start, $end, null, $calid, 0);
}
else
$events = array();
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=".$calname.'.ics');
$this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null);
if ($terminate)
exit;
}
/**
* Handler for iCal feed requests
*/
function ical_feed_export()
{
// process HTTP auth info
if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
$_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host()
$auth = $this->rc->plugins->exec_hook('authenticate', array(
'host' => $this->rc->autoselect_host(),
'user' => trim($_SERVER['PHP_AUTH_USER']),
'pass' => $_SERVER['PHP_AUTH_PW'],
'cookiecheck' => true,
'valid' => true,
));
if ($auth['valid'] && !$auth['abort'])
$this->rc->login($auth['user'], $auth['pass'], $auth['host']);
}
// require HTTP auth
if (empty($_SESSION['user_id'])) {
header('WWW-Authenticate: Basic realm="Roundcube Calendar"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
// decode calendar feed hash
$format = 'ics';
$calhash = get_input_value('_cal', RCUBE_INPUT_GET);
if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) {
$format = strtolower($m[1]);
$calhash = preg_replace($suff_regex, '', $calhash);
}
if (!strpos($calhash, ':'))
$calhash = base64_decode($calhash);
list($user, $_GET['source']) = explode(':', $calhash, 2);
// sanity check user
if ($this->rc->user->get_username() == $user) {
$this->load_driver();
$this->export_events(false);
}
else {
header('HTTP/1.0 404 Not Found');
}
// don't save session data
session_destroy();
exit;
}
/**
*
*/
function load_settings()
{
$this->lib->load_settings();
$this->defaults += $this->lib->defaults;
$settings = array();
// configuration
$settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
$settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
$settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']);
$settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
$settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
$settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
$settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);
$settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']);
$settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
$settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
// get user identity to create default attendee
if ($this->ui->screen == 'calendar') {
foreach ($this->rc->user->list_identities() as $rec) {
if (!$identity)
$identity = $rec;
$identity['emails'][] = $rec['email'];
$settings['identities'][$rec['identity_id']] = $rec['email'];
}
$identity['emails'][] = $this->rc->user->get_username();
$settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails'])));
}
return $settings;
}
/**
* Encode events as JSON
*
* @param array Events as array
* @param boolean Add CSS class names according to calendar and categories
* @return string JSON encoded events
*/
function encode($events, $addcss = false)
{
$json = array();
foreach ($events as $event) {
$json[] = $this->_client_event($event, $addcss);
}
return json_encode($json);
}
/**
* Convert an event object to be used on the client
*/
private function _client_event($event, $addcss = false)
{
// compose a human readable strings for alarms_text and recurrence_text
if ($event['alarms'])
$event['alarms_text'] = libcalendaring::alarms_text($event['alarms']);
if ($event['recurrence']) {
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
if ($event['recurrence']['UNTIL'])
$event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'], $event['allday'])->format('c');
unset($event['recurrence']['EXCEPTIONS']);
// format RDATE values
if (is_array($event['recurrence']['RDATE'])) {
$libcal = $this->lib;
$event['recurrence']['RDATE'] = array_map(function($rdate) use ($libcal) {
return $libcal->adjust_timezone($rdate, true)->format('c');
}, $event['recurrence']['RDATE']);
}
}
foreach ((array)$event['attachments'] as $k => $attachment) {
$event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
}
// check for organizer in attendees list
$organizer = null;
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
break;
}
}
if ($organizer === null && !empty($event['organizer'])) {
$organizer = $event['organizer'];
$organizer['role'] = 'ORGANIZER';
if (!is_array($event['attendees']))
$event['attendees'] = array();
array_unshift($event['attendees'], $organizer);
}
// mapping url => vurl because of the fullcalendar client script
$event['vurl'] = $event['url'];
unset($event['url']);
return array(
'_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar
'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'),
'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'),
// 'changed' might be empty for event recurrences (Bug #2185)
'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,
'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->format('c') : null,
'title' => strval($event['title']),
'description' => strval($event['description']),
'location' => strval($event['location']),
'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true),
'allDay' => ($event['allday'] == 1),
) + $event;
}
/**
* Render localized text describing the recurrence rule of an event
*/
private function _recurrence_text($rrule)
{
// derive missing FREQ and INTERVAL from RDATE list
if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
$first = $rrule['RDATE'][0];
$second = $rrule['RDATE'][1];
$third = $rrule['RDATE'][2];
if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) {
$diff = $first->diff($second);
foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) {
if ($diff->$k != 0) {
$rrule['FREQ'] = $freq;
$rrule['INTERVAL'] = $diff->$k;
// verify interval with next item
if (is_a($third, 'DateTime')) {
$diff2 = $second->diff($third);
if ($diff2->$k != $diff->$k) {
unset($rrule['INTERVAL']);
}
}
break;
}
}
}
if (!$rrule['INTERVAL'])
$rrule['FREQ'] = 'RDATE';
$rrule['UNTIL'] = end($rrule['RDATE']);
}
// TODO: finish this
$freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
$details = '';
switch ($rrule['FREQ']) {
case 'DAILY':
$freq .= $this->gettext('days');
break;
case 'WEEKLY':
$freq .= $this->gettext('weeks');
break;
case 'MONTHLY':
$freq .= $this->gettext('months');
break;
case 'YEARLY':
$freq .= $this->gettext('years');
break;
}
if ($rrule['INTERVAL'] <= 1)
$freq = $this->gettext(strtolower($rrule['FREQ']));
if ($rrule['COUNT'])
$until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
else if ($rrule['UNTIL'])
$until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], libcalendaring::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
else
$until = $this->gettext('forever');
return rtrim($freq . $details . ', ' . $until);
}
/**
* 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));
}
/**
* TEMPORARY: generate random event data for testing
* Create events by opening http://<roundcubeurl>/?_task=calendar&_action=randomdata&_num=500
*/
public function generate_randomdata()
{
$num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100;
$cats = array_keys($this->driver->list_categories());
$cals = $this->driver->list_calendars(true);
$count = 0;
while ($count++ < $num) {
$start = round((time() + rand(-2600, 2600) * 1000) / 300) * 300;
$duration = round(rand(30, 360) / 30) * 30 * 60;
$allday = rand(0,20) > 18;
$alarm = rand(-30,12) * 5;
$fb = rand(0,2);
if (date('G', $start) > 23)
$start -= 3600;
if ($allday) {
$start = strtotime(date('Y-m-d 00:00:00', $start));
$duration = 86399;
}
$title = '';
$len = rand(2, 12);
$words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
// $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890";
for ($i = 0; $i < $len; $i++)
$title .= $words[rand(0,count($words)-1)] . " ";
$this->driver->new_event(array(
'uid' => $this->generate_uid(),
'start' => new DateTime('@'.$start),
'end' => new DateTime('@'.($start + $duration)),
'allday' => $allday,
'title' => rtrim($title),
'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),
'categories' => $cats[array_rand($cats)],
'calendar' => array_rand($cals),
'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '',
'priority' => rand(0,9),
));
}
$this->rc->output->redirect('');
}
/**
* Handler for attachments upload
*/
public function attachment_upload()
{
$this->lib->attachment_upload(self::SESSION_KEY, 'cal:');
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
// show loading page
if (!empty($_GET['_preload'])) {
return $this->lib->attachment_loading_page();
}
$event_id = 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_id, 'calendar' => $calendar);
$attachment = $this->driver->get_attachment($id, $event);
// 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->send('calendar.attachment');
}
// deliver attachment content
else if ($attachment) {
$attachment['body'] = $this->driver->get_attachment_body($id, $event);
$this->lib->attachment_get($attachment);
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/**
* Prepares new/edited event properties before save
*/
private function prepare_event(&$event, $action)
{
// convert dates into DateTime objects in user's current timezone
$event['start'] = new DateTime($event['start'], $this->timezone);
$event['end'] = new DateTime($event['end'], $this->timezone);
// start/end is all we need for 'move' action (#1480)
if ($action == 'move') {
return;
}
if (is_array($event['recurrence']) && !empty($event['recurrence']['UNTIL']))
$event['recurrence']['UNTIL'] = new DateTime($event['recurrence']['UNTIL'], $this->timezone);
if (is_array($event['recurrence']) && is_array($event['recurrence']['RDATE'])) {
$tz = $this->timezone;
$start = $event['start'];
$event['recurrence']['RDATE'] = array_map(function($rdate) use ($tz, $start) {
try {
$dt = new DateTime($rdate, $tz);
$dt->setTime($start->format('G'), $start->format('i'));
return $dt;
}
catch (Exception $e) {
return null;
}
}, $event['recurrence']['RDATE']);
}
$attachments = array();
$eventid = 'cal:'.$event['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
}
}
}
}
$event['attachments'] = $attachments;
// check for organizer in attendees
if ($action == 'new' || $action == 'edit') {
if (!$event['attendees'])
$event['attendees'] = array();
$emails = $this->get_user_emails();
$organizer = $owner = false;
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = $i;
if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails))
$owner = $i;
else if (!isset($attendee['rsvp']))
$event['attendees'][$i]['rsvp'] = true;
}
// set new organizer identity
if ($organizer !== false && !empty($event['_identity']) && ($identity = $this->rc->user->get_identity($event['_identity']))) {
$event['attendees'][$organizer]['name'] = $identity['name'];
$event['attendees'][$organizer]['email'] = $identity['email'];
}
// set owner as organizer if yet missing
if ($organizer === false && $owner !== false) {
$event['attendees'][$owner]['role'] = 'ORGANIZER';
unset($event['attendees'][$owner]['rsvp']);
}
else if ($organizer === false && $action == 'new' && ($identity = $this->rc->user->get_identity($event['_identity'])) && $identity['email']) {
array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED'));
}
}
// mapping url => vurl because of the fullcalendar client script
if (array_key_exists('vurl', $event)) {
$event['url'] = $event['vurl'];
unset($event['vurl']);
}
}
/**
* Releases some resources after successful event save
*/
private function cleanup_event(&$event)
{
// remove temp. attachment files
if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
$this->rc->session->remove(self::SESSION_KEY);
}
}
/**
* Send out an invitation/notification to all event attendees
*/
private function notify_attendees($event, $old, $action = 'edit')
{
if ($action == 'remove') {
$event['cancelled'] = true;
$is_cancelled = true;
}
$itip = $this->load_itip();
$emails = $this->get_user_emails();
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
$message = $itip->compose_itip_message($event, $method);
// list existing attendees from $old event
$old_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
// send to every attendee
$sent = 0; $current = array();
foreach ((array)$event['attendees'] as $attendee) {
$current[] = strtolower($attendee['email']);
// skip myself for obvious reasons
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails))
continue;
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
$subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
// finally send the message
if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message))
$sent++;
else
$sent = -100;
}
// send CANCEL message to removed attendees
foreach ((array)$old['attendees'] as $attendee) {
if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current))
continue;
$vevent = $old;
$vevent['cancelled'] = $is_cancelled;
$vevent['attendees'] = array($attendee);
if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody'))
$sent++;
else
$sent = -100;
}
return $sent;
}
/**
* Echo simple free/busy status text for the given user and time range
*/
public function freebusy_status()
{
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GPC);
$end = get_input_value('end', RCUBE_INPUT_GPC);
// convert dates into unix timestamps
if (!empty($start) && !is_numeric($start)) {
$dts = new DateTime($start, $this->timezone);
$start = $dts->format('U');
}
if (!empty($end) && !is_numeric($end)) {
$dte = new DateTime($end, $this->timezone);
$end = $dte->format('U');
}
if (!$start) $start = time();
if (!$end) $end = $start + 3600;
$fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE');
$status = 'UNKNOWN';
// if the backend has free-busy information
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
if (is_array($fblist)) {
$status = 'FREE';
foreach ($fblist as $slot) {
list($from, $to, $type) = $slot;
if ($from < $end && $to > $start) {
$status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY';
break;
}
}
}
// let this information be cached for 5min
send_future_expire_header(300);
echo $status;
exit;
}
/**
* Return a list of free/busy time slots within the given period
* Echo data in JSON encoding
*/
public function freebusy_times()
{
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GPC);
$end = get_input_value('end', RCUBE_INPUT_GPC);
$interval = intval(get_input_value('interval', RCUBE_INPUT_GPC));
$strformat = $interval > 60 ? 'Ymd' : 'YmdHis';
// convert dates into unix timestamps
if (!empty($start) && !is_numeric($start)) {
$dts = new DateTime($start, $this->timezone);
$start = $dts->format('U');
}
if (!empty($end) && !is_numeric($end)) {
$dte = new DateTime($end, $this->timezone);
$end = $dte->format('U');
}
if (!$start) $start = time();
if (!$end) $end = $start + 86400 * 30;
if (!$interval) $interval = 60; // 1 hour
if (!$dte) {
$dts = new DateTime('@'.$start);
$dts->setTimezone($this->timezone);
}
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
$slots = array();
// build a list from $start till $end with blocks representing the fb-status
for ($s = 0, $t = $start; $t <= $end; $s++) {
$status = self::FREEBUSY_UNKNOWN;
$t_end = $t + $interval * 60;
$dt = new DateTime('@'.$t);
$dt->setTimezone($this->timezone);
// determine attendee's status
if (is_array($fblist)) {
$status = self::FREEBUSY_FREE;
foreach ($fblist as $slot) {
list($from, $to, $type) = $slot;
if ($from < $t_end && $to > $t) {
$status = isset($type) ? $type : self::FREEBUSY_BUSY;
if ($status == self::FREEBUSY_BUSY) // can't get any worse :-)
break;
}
}
}
$slots[$s] = $status;
$times[$s] = intval($dt->format($strformat));
$t = $t_end;
}
$dte = new DateTime('@'.$t_end);
$dte->setTimezone($this->timezone);
// let this information be cached for 5min
send_future_expire_header(300);
echo json_encode(array(
'email' => $email,
'start' => $dts->format('c'),
'end' => $dte->format('c'),
'interval' => $interval,
'slots' => $slots,
'times' => $times,
));
exit;
}
/**
* Handler for printing calendars
*/
public function print_view()
{
$title = $this->gettext('print');
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$view = 'agendaDay';
$this->rc->output->set_env('view',$view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
if ($range = get_input_value('range', RCUBE_INPUT_GPC))
$this->rc->output->set_env('listRange', intval($range));
if (isset($_REQUEST['sections']))
$this->rc->output->set_env('listSections', get_input_value('sections', RCUBE_INPUT_GPC));
if ($search = get_input_value('search', RCUBE_INPUT_GPC)) {
$this->rc->output->set_env('search', $search);
$title .= ' "' . $search . '"';
}
// Add CSS stylesheets to the page header
$skin_path = $this->local_skin_path();
$this->include_stylesheet($skin_path . '/fullcalendar.css');
$this->include_stylesheet($skin_path . '/print.css');
// Add JS files to the page header
$this->include_script('print.js');
$this->include_script('lib/js/fullcalendar.js');
$this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
$this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
$this->rc->output->set_pagetitle($title);
$this->rc->output->send("calendar.print");
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['calendar.'.$label] = $this->gettext($label);
$texts['calendar.new_event'] = $this->gettext('createfrommail');
$this->ui->init_templates();
$this->ui->calendar_list(); # set env['calendars']
echo $this->api->output->parse('calendar.eventedit', false, false);
echo html::tag('script', array('type' => 'text/javascript'),
"rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n".
"rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
"rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
"rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
"rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
}
/**
* Compare two event objects and return differing properties
*
* @param array Event A
* @param array Event B
* @return array List of differing event properties
*/
public static function event_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;
}
/**** Resource management functions ****/
/**
* Getter for the configured implementation of the resource directory interface
*/
private function resources_directory()
{
if (is_object($this->resources_dir)) {
return $this->resources_dir;
}
if ($driver_name = $this->rc->config->get('calendar_resources_driver')) {
$driver_class = 'resources_driver_' . $driver_name;
require_once($this->home . '/drivers/resources_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
$this->resources_dir = new $driver_class($this);
}
return $this->resources_dir;
}
/**
* Handler for resoruce autocompletion requests
*/
public function resources_autocomplete()
{
$search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
$sid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$maxnum = (int)$this->rc->config->get('autocomplete_max', 15);
$results = array();
if ($directory = $this->resources_directory()) {
foreach ($directory->load_resources($search, $maxnum) as $rec) {
$results[] = array(
'name' => $rec['name'],
'email' => $rec['email'],
'type' => $rec['_type'],
);
}
}
$this->rc->output->command('ksearch_query_results', $results, $search, $sid);
$this->rc->output->send();
}
/**
* Handler for load-requests for resource data
*/
function resources_list()
{
$data = array();
if ($directory = $this->resources_directory()) {
foreach ($directory->load_resources() as $rec) {
$data[] = $rec;
}
}
$this->rc->output->command('plugin.resource_data', $data);
$this->rc->output->send();
}
/**
* Handler for requests loading resource owner information
*/
function resources_owner()
{
if ($directory = $this->resources_directory()) {
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$data = $directory->get_resource_owner($id);
}
$this->rc->output->command('plugin.resource_owner', $data);
$this->rc->output->send();
}
/**
* Deliver event data for a resource's calendar
*/
function resources_calendar()
{
$events = array();
if ($directory = $this->resources_directory()) {
$events = $directory->get_resource_calendar(
rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
rcube_utils::get_input_value('start', RCUBE_INPUT_GET),
rcube_utils::get_input_value('end', RCUBE_INPUT_GET));
}
echo $this->encode($events);
exit;
}
/**** Event invitation plugin hooks ****/
/**
* Handler for calendar/itip-status requests
*/
function event_itip_status()
{
$data = get_input_value('data', RCUBE_INPUT_POST, true);
// find local copy of the referenced event
$this->load_driver();
$existing = $this->driver->get_event($data, true, false, true);
$itip = $this->load_itip();
$response = $itip->get_itip_status($data, $existing);
// get a list of writeable calendars to save new events to
if (!$existing && $response['action'] == 'rsvp' || $response['action'] == 'import') {
$calendars = $this->driver->list_calendars(false, true);
$calendar_select = new html_select(array('name' => 'calendar', 'id' => 'itip-saveto', 'is_escaped' => true));
$numcals = 0;
foreach ($calendars as $calendar) {
if (!$calendar['readonly']) {
$calendar_select->add($calendar['name'], $calendar['id']);
$numcals++;
}
}
if ($numcals <= 1)
$calendar_select = null;
}
if ($calendar_select) {
$default_calendar = $this->get_default_calendar(true);
$response['select'] = html::span('folder-select', $this->gettext('saveincalendar') . '&nbsp;' .
$calendar_select->show($this->rc->config->get('calendar_default_calendar', $default_calendar['id'])));
}
$this->rc->output->command('plugin.update_itip_object_status', $response);
}
/**
* Handler for calendar/itip-remove requests
*/
function event_itip_remove()
{
$success = false;
// search for event if only UID is given
if ($event = $this->driver->get_event(array('uid' => get_input_value('uid', RCUBE_INPUT_POST)), true)) {
$success = $this->driver->remove_event($event, true);
}
if ($success) {
$this->rc->output->show_message('calendar.successremoval', 'confirmation');
}
else {
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
}
/**
* Handler for URLs that allow an invitee to respond on his invitation mail
*/
public function itip_attend_response($p)
{
if ($p['action'] == 'attend') {
$this->ui->init();
$this->rc->output->set_env('task', 'calendar'); // override some env vars
$this->rc->output->set_env('refresh_interval', 0);
$this->rc->output->set_pagetitle($this->gettext('calendar'));
$itip = $this->load_itip();
$token = get_input_value('_t', RCUBE_INPUT_GPC);
// read event info stored under the given token
if ($invitation = $itip->get_invitation($token)) {
$this->token = $token;
$this->event = $invitation['event'];
// show message about cancellation
if ($invitation['cancelled']) {
$this->invitestatus = html::div('rsvp-status declined', $itip->gettext('eventcancelled'));
}
// save submitted RSVP status
else if (!empty($_POST['rsvp'])) {
$status = null;
foreach (array('accepted','tentative','declined') as $method) {
if ($_POST['rsvp'] == $itip->gettext('itip' . $method)) {
$status = $method;
break;
}
}
// send itip reply to organizer
if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
$this->invitestatus = html::div('rsvp-status ' . strtolower($status), $itip->gettext('youhave'.strtolower($status)));
}
else
$this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);
// if user is logged in...
if ($this->rc->user->ID) {
$this->load_driver();
$invitation = $itip->get_invitation($token);
// save the event to his/her default calendar if not yet present
if (!$this->driver->get_event($this->event) && ($calendar = $this->get_default_calendar(true))) {
$invitation['event']['calendar'] = $calendar['id'];
if ($this->driver->new_event($invitation['event']))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
}
}
}
$this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
$this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox'));
if (!$this->invitestatus)
$this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
$this->rc->output->set_pagetitle($itip->gettext('itipinvitation') . ' ' . $this->event['title']);
}
else
$this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
$this->rc->output->send('calendar.itipattend');
}
}
/**
*
*/
public function itip_event_inviteform($attrib)
{
$hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token));
return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show();
}
/**
* Check mail message structure of there are .ics files attached
*/
public function mail_message_load($p)
{
$this->message = $p['object'];
$itip_part = null;
// check all message parts for .ics files
foreach ((array)$this->message->mime_parts as $part) {
if ($this->is_vcalendar($part)) {
if ($part->ctype_parameters['method'])
$itip_part = $part->mime_id;
else
$this->ics_parts[] = $part->mime_id;
}
}
// priorize part with method parameter
if ($itip_part)
$this->ics_parts = array($itip_part);
}
/**
* Add UI element to copy event invitations or updates to the calendar
*/
public function mail_messagebody_html($p)
{
// load iCalendar functions (if necessary)
if (!empty($this->ics_parts)) {
$this->get_ical();
+ $this->load_itip();
}
- $this->load_itip();
$html = '';
foreach ($this->ics_parts as $mime_id) {
$part = $this->message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
$events = $this->ical->import($this->message->get_part_content($mime_id), $charset);
$title = $this->gettext('title');
// successfully parsed events?
if (empty($events))
continue;
// show a box for every event in the file
foreach ($events as $idx => $event) {
if ($event['_type'] != 'event') // skip non-event objects (#2928)
continue;
// get prepared inline UI for this event object
$html .= html::div('calendar-invitebox',
$this->itip->mail_itip_inline_ui(
$event,
$this->ical->method,
$mime_id.':'.$idx,
'calendar',
rcube_utils::anytodatetime($this->message->headers->date)
)
);
// limit listing
if ($idx >= 3)
break;
}
}
// prepend event boxes to message body
if ($html) {
$this->ui->init();
$p['content'] = $html . $p['content'];
$this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm');
}
+ // add "Save to calendar" button into attachment menu
+ if (!empty($this->ics_parts)) {
+ $this->add_button(array(
+ 'id' => 'attachmentsavecal',
+ 'name' => 'attachmentsavecal',
+ 'type' => 'link',
+ 'wrapper' => 'li',
+ 'command' => 'attachment-save-calendar',
+ 'class' => 'icon calendarlink',
+ 'classact' => 'icon calendarlink active',
+ 'innerclass' => 'icon calendar',
+ 'label' => 'calendar.savetocalendar',
+ ), 'attachmentmenu');
+ }
+
return $p;
}
/**
* Read the given mime message from IMAP and parse ical data
*/
private function mail_get_itip_event($mbox, $uid, $mime_id)
{
$charset = RCMAIL_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
if ($uid && $mime_id) {
list($mime_id, $index) = explode(':', $mime_id);
$part = $imap->get_message_part($uid, $mime_id);
if ($part->ctype_parameters['charset'])
$charset = $part->ctype_parameters['charset'];
$headers = $imap->get_message_headers($uid);
if ($part) {
$events = $this->get_ical()->import($part, $charset);
}
}
// successfully parsed events?
if (!empty($events) && ($event = $events[$index])) {
// store the message's sender address for comparisons
$event['_sender'] = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : '';
$event['_sender_utf'] = rcube_idn_to_utf8($event['_sender']);
return $event;
}
return null;
}
/**
* Handler for POST request to import an event attached to a mail message
*/
- public function mail_import_event()
+ public function mail_import_itip()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$mime_id = get_input_value('_part', RCUBE_INPUT_POST);
$status = get_input_value('_status', RCUBE_INPUT_POST);
$delete = intval(get_input_value('_del', RCUBE_INPUT_POST));
$error_msg = $this->gettext('errorimportingevent');
$success = false;
// successfully parsed events?
if ($event = $this->mail_get_itip_event($mbox, $uid, $mime_id)) {
// find writeable calendar to store event
$cal_id = !empty($_REQUEST['_folder']) ? get_input_value('_folder', RCUBE_INPUT_POST) : null;
$calendars = $this->driver->list_calendars(false, true);
$calendar = $calendars[$cal_id] ?: $this->get_default_calendar(true);
$metadata = array(
'uid' => $event['uid'],
'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0,
'sequence' => intval($event['sequence']),
'fallback' => strtoupper($status),
'method' => $this->ical->method,
'task' => 'calendar',
);
// update my attendee status according to submitted method
if (!empty($status)) {
$organizer = null;
$emails = $this->get_user_emails();
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
$reply_sender = $attendee['email'];
}
}
// add attendee with this user's default identity if not listed
if (!$reply_sender) {
$sender_identity = $this->rc->user->get_identity();
$event['attendees'][] = array(
'name' => $sender_identity['name'],
'email' => $sender_identity['email'],
'role' => 'OPT-PARTICIPANT',
'status' => strtoupper($status),
'rsvp' => true,
);
$metadata['attendee'] = $sender_identity['email'];
}
}
// save to calendar
if ($calendar && !$calendar['readonly']) {
$event['calendar'] = $calendar['id'];
// check for existing event with the same UID
$existing = $this->driver->get_event($event['uid'], true, false, true);
if ($existing) {
// only update attendee status
if ($this->ical->method == 'REPLY') {
// try to identify the attendee using the email sender address
$existing_attendee = -1;
foreach ($existing['attendees'] as $i => $attendee) {
if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
$existing_attendee = $i;
break;
}
}
$event_attendee = null;
foreach ($event['attendees'] as $attendee) {
if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
$event_attendee = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
break;
}
}
// found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $event_attendee) {
$existing['attendees'][$existing_attendee] = $event_attendee;
$success = $this->driver->edit_event($existing);
}
// update the entire attendees block
else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
$existing['attendees'][] = $event_attendee;
$success = $this->driver->edit_event($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
// delete the event when declined (#1670)
else if ($status == 'declined' && $delete) {
$deleted = $this->driver->remove_event($existing, true);
$success = true;
}
// import the (newer) event
else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) {
$event['id'] = $existing['id'];
$event['calendar'] = $existing['calendar'];
if ($status == 'declined' || $event['status'] == 'CANCELLED') // show me as free when declined (#1670)
$event['free_busy'] = 'free';
$success = $this->driver->edit_event($event);
}
else if (!empty($status)) {
$existing['attendees'] = $event['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') {
$success = $this->driver->new_event($event);
}
else if ($status == 'declined')
$error_msg = null;
}
else if ($status == 'declined')
$error_msg = null;
else
$error_msg = $this->gettext('nowritecalendarfound');
}
if ($success) {
$message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : 'importedsuccessfully');
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
$metadata['rsvp'] = intval($metadata['rsvp']);
$this->rc->output->command('plugin.fetch_itip_object_status', $metadata);
$error_msg = null;
}
else if ($error_msg)
$this->rc->output->command('display_message', $error_msg, 'error');
// send iTip reply
if ($this->ical->method == 'REQUEST' && $organizer && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
$event['comment'] = get_input_value('_comment', RCUBE_INPUT_POST);
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
$this->rc->output->send();
}
/**
* Handler for calendar/itip-remove requests
*/
function mail_itip_decline_reply()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$mime_id = get_input_value('_part', RCUBE_INPUT_POST);
if (($event = $this->mail_get_itip_event($mbox, $uid, $mime_id)) && $this->ical->method == 'REPLY') {
$event['comment'] = get_input_value('_comment', RCUBE_INPUT_POST);
foreach ($event['attendees'] as $_attendee) {
if ($_attendee['role'] != 'ORGANIZER') {
$attendee = $_attendee;
break;
}
}
$itip = $this->load_itip();
if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel'))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
else {
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
}
+ /**
+ * Import the full payload from a mail message attachment
+ */
+ public function mail_import_attachment()
+ {
+ $uid = get_input_value('_uid', RCUBE_INPUT_POST);
+ $mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
+ $mime_id = get_input_value('_part', RCUBE_INPUT_POST);
+ $charset = RCMAIL_CHARSET;
+
+ // establish imap connection
+ $imap = $this->rc->get_storage();
+ $imap->set_mailbox($mbox);
+
+ if ($uid && $mime_id) {
+ $part = $imap->get_message_part($uid, $mime_id);
+ if ($part->ctype_parameters['charset'])
+ $charset = $part->ctype_parameters['charset'];
+ $headers = $imap->get_message_headers($uid);
+
+ if ($part) {
+ $events = $this->get_ical()->import($part, $charset);
+ }
+ }
+
+ $success = $existing = 0;
+ if (!empty($events)) {
+ // find writeable calendar to store event
+ $cal_id = !empty($_REQUEST['_calendar']) ? get_input_value('_calendar', RCUBE_INPUT_POST) : null;
+ $calendars = $this->driver->list_calendars(false, true);
+ $calendar = $calendars[$cal_id] ?: $this->get_default_calendar(true);
+
+ foreach ($events as $event) {
+ // save to calendar
+ if ($calendar && !$calendar['readonly'] && $event['_type'] == 'event') {
+ $event['calendar'] = $calendar['id'];
+
+ if (!$this->driver->get_event($event['uid'], true, false)) {
+ $success += (bool)$this->driver->new_event($event);
+ }
+ 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('errorimportingevent'), 'error');
+ }
+ }
/**
* Read email message and return contents for a new event based on that message
*/
public function mail_message2event()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$event = array();
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$event['title'] = trim($message->subject);
$event['description'] = trim($message->first_text_part());
// copy mail attachments to event
if ($message->attachments) {
$eventid = 'cal:';
if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
$_SESSION[self::SESSION_KEY] = array();
$_SESSION[self::SESSION_KEY]['id'] = $eventid;
$_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' => $eventid,
);
$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'
$event['attachments'][] = $attachment;
}
}
}
$this->rc->output->command('plugin.mail2event_dialog', $event);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/**
* Checks if specified message part is a vcalendar data
*
* @param rcube_message_part Part object
* @return boolean True if part is of type vcard
*/
private function is_vcalendar($part)
{
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))
);
}
/**
* Get a list of email addresses of the current user (from login and identities)
*/
private function get_user_emails()
{
return $this->lib->get_user_emails();
}
/**
* Build an absolute URL with the given parameters
*/
public function get_url($param = array())
{
$param += array('task' => 'calendar');
$schema = 'http';
$default_port = 80;
if (rcube_https_check()) {
$schema = 'https';
$default_port = 443;
}
$url = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
if ($_SERVER['SERVER_PORT'] != $default_port)
$url .= ':' . $_SERVER['SERVER_PORT'];
if (dirname($_SERVER['SCRIPT_NAME']) != '/')
$url .= dirname($_SERVER['SCRIPT_NAME']);
$url .= preg_replace('!^\./!', '/', $this->rc->url($param));
return $url;
}
public function ical_feed_hash($source)
{
return base64_encode($this->rc->user->get_username() . ':' . $source);
}
/**
* 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;
}
}
diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js
index 37552ce6..cc7c65af 100644
--- a/plugins/calendar/calendar_base.js
+++ b/plugins/calendar/calendar_base.js
@@ -1,117 +1,142 @@
/**
* Base Javascript class for the Calendar plugin
*
* @version @package_version@
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
// Basic setup for Roundcube calendar client class
function rcube_calendar(settings)
{
// extend base class
rcube_libcalendaring.call(this, settings);
// member vars
this.ui;
this.ui_loaded = false;
+ this.selected_attachment = null;
// private vars
var me = this;
// create new event from current mail message
this.create_from_mail = function(uid)
{
if (uid || (uid = rcmail.get_single_uid())) {
// load calendar UI (scripts and edit dialog template)
if (!this.ui_loaded) {
$.when(
$.getScript('./plugins/calendar/calendar_ui.js'),
$.getScript('./plugins/calendar/lib/js/fullcalendar.js'),
$.get(rcmail.url('calendar/inlineui'), function(html){ $(document.body).append(html); }, 'html')
).then(function() {
// disable attendees feature (autocompletion and stuff is not initialized)
for (var c in rcmail.env.calendars)
rcmail.env.calendars[c].attendees = rcmail.env.calendars[c].resources = false;
me.ui_loaded = true;
me.ui = new rcube_calendar_ui(me.settings);
me.create_from_mail(uid); // start over
});
return;
}
else {
// get message contents for event dialog
var lock = rcmail.set_busy(true, 'loading');
rcmail.http_post('calendar/mailtoevent', {
'_mbox': rcmail.env.mailbox,
'_uid': uid
}, lock);
}
}
};
// callback function triggered from server with contents for the new event
this.mail2event_dialog = function(event)
{
if (event.title) {
this.ui.add_event(event);
rcmail.message_list.blur();
}
};
+
+ // handler for attachment-save-calendar commands
+ this.save_to_calendar = function(p)
+ {
+ // TODO: show dialog to select the calendar for importing
+ if (this.selected_attachment && window.rcube_libcalendaring) {
+ rcmail.http_post('calendar/mailimportattach', {
+ _uid: rcmail.env.uid,
+ _mbox: rcmail.env.mailbox,
+ _part: this.selected_attachment,
+ // _calendar: $('#calendar-attachment-saveto').val(),
+ }, rcmail.set_busy(true, 'itip.savingdata'));
+ }
+ }
}
/* calendar plugin initialization (for non-calendar tasks) */
window.rcmail && rcmail.addEventListener('init', function(evt) {
if (rcmail.task != 'calendar') {
var cal = new rcube_calendar($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));
// register create-from-mail command to message_commands array
if (rcmail.env.task == 'mail') {
rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail() });
+ rcmail.register_command('attachment-save-calendar', function() { cal.save_to_calendar() });
rcmail.addEventListener('plugin.mail2event_dialog', function(p){ cal.mail2event_dialog(p) });
rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.ui && cal.ui.unlock_saving(); });
if (rcmail.env.action != 'show') {
rcmail.env.message_commands.push('calendar-create-from-mail');
rcmail.add_element($('<a>'));
}
- else
+ else {
rcmail.enable_command('calendar-create-from-mail', true);
+ }
+
+ rcmail.addEventListener('beforemenu-open', function(p) {
+ if (p.menu == 'attachmentmenu') {
+ cal.selected_attachment = p.id;
+ var mimetype = rcmail.env.attachments[p.id];
+ rcmail.enable_command('attachment-save-calendar', mimetype == 'text/calendar' || mimetype == 'text/x-vcalendar' || mimetype == 'application/ics');
+ }
+ });
// add contextmenu item
if (window.rcm_contextmenu_register_command) {
rcm_contextmenu_register_command(
'calendar-create-from-mail',
function(cmd,el){ cal.create_from_mail() },
'calendar.createfrommail',
'moveto');
}
}
}
rcmail.register_command('plugin.calendar', function() { rcmail.switch_task('calendar'); }, true);
rcmail.addEventListener('plugin.ping_url', function(p){
var action = p.action;
p.action = p.event = null;
new Image().src = rcmail.url(action, p);
});
});
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index cc8b3974..b243c24b 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -1,255 +1,257 @@
<?php
$labels = array();
// preferences
$labels['default_view'] = 'Default view';
$labels['time_format'] = 'Time format';
$labels['timeslots'] = 'Time slots per hour';
$labels['first_day'] = 'First weekday';
$labels['first_hour'] = 'First hour to show';
$labels['workinghours'] = 'Working hours';
$labels['add_category'] = 'Add category';
$labels['remove_category'] = 'Remove category';
$labels['defaultcalendar'] = 'Create new events in';
$labels['eventcoloring'] = 'Event coloring';
$labels['coloringmode0'] = 'According to calendar';
$labels['coloringmode1'] = 'According to category';
$labels['coloringmode2'] = 'Calendar for outline, category for content';
$labels['coloringmode3'] = 'Category for outline, calendar for content';
// calendar
$labels['calendar'] = 'Calendar';
$labels['calendars'] = 'Calendars';
$labels['category'] = 'Category';
$labels['categories'] = 'Categories';
$labels['createcalendar'] = 'Create new calendar';
$labels['editcalendar'] = 'Edit calendar properties';
$labels['name'] = 'Name';
$labels['color'] = 'Color';
$labels['day'] = 'Day';
$labels['week'] = 'Week';
$labels['month'] = 'Month';
$labels['agenda'] = 'Agenda';
$labels['new'] = 'New';
$labels['new_event'] = 'New event';
$labels['edit_event'] = 'Edit event';
$labels['edit'] = 'Edit';
$labels['save'] = 'Save';
$labels['remove'] = 'Remove';
$labels['cancel'] = 'Cancel';
$labels['select'] = 'Select';
$labels['print'] = 'Print';
$labels['printtitle'] = 'Print calendars';
$labels['title'] = 'Summary';
$labels['description'] = 'Description';
$labels['all-day'] = 'all-day';
$labels['export'] = 'Export';
$labels['exporttitle'] = 'Export to iCalendar';
$labels['exportrange'] = 'Events from';
$labels['exportattachments'] = 'With attachments';
$labels['customdate'] = 'Custom date';
$labels['location'] = 'Location';
$labels['url'] = 'URL';
$labels['date'] = 'Date';
$labels['start'] = 'Start';
$labels['end'] = 'End';
$labels['selectdate'] = 'Choose date';
$labels['freebusy'] = 'Show me as';
$labels['free'] = 'Free';
$labels['busy'] = 'Busy';
$labels['outofoffice'] = 'Out of Office';
$labels['tentative'] = 'Tentative';
$labels['priority'] = 'Priority';
$labels['sensitivity'] = 'Privacy';
$labels['public'] = 'public';
$labels['private'] = 'private';
$labels['confidential'] = 'confidential';
$labels['alarms'] = 'Reminder';
$labels['comment'] = 'Comment';
$labels['generated'] = 'generated at';
$labels['printdescriptions'] = 'Print descriptions';
$labels['parentcalendar'] = 'Insert inside';
$labels['searchearlierdates'] = '« Search for earlier events';
$labels['searchlaterdates'] = 'Search for later events »';
$labels['andnmore'] = '$nr 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';
$labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
// agenda view
$labels['listrange'] = 'Range to display:';
$labels['listsections'] = 'Divide into:';
$labels['smartsections'] = 'Smart sections';
$labels['until'] = 'until';
$labels['today'] = 'Today';
$labels['tomorrow'] = 'Tomorrow';
$labels['thisweek'] = 'This week';
$labels['nextweek'] = 'Next week';
$labels['thismonth'] = 'This month';
$labels['nextmonth'] = 'Next month';
$labels['weekofyear'] = 'Week';
$labels['pastevents'] = 'Past';
$labels['futureevents'] = 'Future';
// alarm/reminder settings
$labels['showalarms'] = 'Show reminders';
$labels['defaultalarmtype'] = 'Default reminder setting';
$labels['defaultalarmoffset'] = 'Default reminder time';
// attendees
$labels['attendee'] = 'Participant';
$labels['role'] = 'Role';
$labels['availability'] = 'Avail.';
$labels['confirmstate'] = 'Status';
$labels['addattendee'] = 'Add participant';
$labels['roleorganizer'] = 'Organizer';
$labels['rolerequired'] = 'Required';
$labels['roleoptional'] = 'Optional';
$labels['rolechair'] = 'Chair';
$labels['rolenonparticipant'] = 'Absent';
$labels['cutypeindividual'] = 'Individual';
$labels['cutypegroup'] = 'Group';
$labels['cutyperesource'] = 'Resource';
$labels['cutyperoom'] = 'Room';
$labels['availfree'] = 'Free';
$labels['availbusy'] = 'Busy';
$labels['availunknown'] = 'Unknown';
$labels['availtentative'] = 'Tentative';
$labels['availoutofoffice'] = 'Out of Office';
$labels['delegatedto'] = 'Delegated to: ';
$labels['delegatedfrom'] = 'Delegated from: ';
$labels['scheduletime'] = 'Find availability';
$labels['sendinvitations'] = 'Send invitations';
$labels['sendnotifications'] = 'Notify participants about modifications';
$labels['sendcancellation'] = 'Notify participants about event cancellation';
$labels['onlyworkinghours'] = 'Find availability within my working hours';
$labels['reqallattendees'] = 'Required/all participants';
$labels['prevslot'] = 'Previous Slot';
$labels['nextslot'] = 'Next Slot';
$labels['noslotfound'] = 'Unable to find a free time slot';
$labels['invitationsubject'] = 'You\'ve been invited to "$title"';
$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the event details which you can import to your calendar application.";
$labels['invitationattendlinks'] = "In case your email client doesn't support iTip requests you can use the following link to either accept or decline this invitation:\n\$url";
$labels['eventupdatesubject'] = '"$title" has been updated';
$labels['eventupdatesubjectempty'] = 'An event that concerns you has been updated';
$labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated event details which you can import to your calendar application.";
$labels['eventcancelsubject'] = '"$title" has been canceled';
$labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe event has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated event details.";
// invitation handling (overrides labels from libcalendaring)
$labels['itipobjectnotfound'] = 'The event referred by this message was not found in your calendar.';
$labels['itipmailbodyaccepted'] = "\$sender has accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
$labels['itipmailbodydeclined'] = "\$sender has declined the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
$labels['itipmailbodycancel'] = "\$sender has rejected your participation in the following event:\n\n*\$title*\n\nWhen: \$date";
$labels['itipdeclineevent'] = 'Do you want to decline your invitation to this event?';
$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
$labels['notanattendee'] = 'You\'re not listed as an attendee of this event';
$labels['eventcancelled'] = 'The event has been cancelled';
$labels['saveincalendar'] = 'save in';
$labels['updatemycopy'] = 'Update in my calendar';
+$labels['savetocalendar'] = 'Save to calendar';
// resources
$labels['resource'] = 'Resource';
$labels['addresource'] = 'Book resource';
$labels['findresources'] = 'Find resources';
$labels['resourcedetails'] = 'Details';
$labels['resourceavailability'] = 'Availability';
$labels['resourceowner'] = 'Owner';
$labels['resourceadded'] = 'The resource was added to your event';
// event dialog tabs
$labels['tabsummary'] = 'Summary';
$labels['tabrecurrence'] = 'Recurrence';
$labels['tabattendees'] = 'Participants';
$labels['tabresources'] = 'Resources';
$labels['tabattachments'] = 'Attachments';
$labels['tabsharing'] = 'Sharing';
// messages
$labels['deleteobjectconfirm'] = 'Do you really want to delete this event?';
$labels['deleteventconfirm'] = 'Do you really want to delete this event?';
$labels['deletecalendarconfirm'] = 'Do you really want to delete this calendar with all its events?';
$labels['deletecalendarconfirmrecursive'] = 'Do you really want to delete this calendar with all its events and sub-calendars?';
$labels['savingdata'] = 'Saving data...';
$labels['errorsaving'] = 'Failed to save changes.';
$labels['operationfailed'] = 'The requested operation failed.';
$labels['invalideventdates'] = 'Invalid dates entered! Please check your input.';
$labels['invalidcalendarproperties'] = 'Invalid calendar properties! Please set a valid name.';
$labels['searchnoresults'] = 'No events found in the selected calendars.';
$labels['successremoval'] = 'The event has been deleted successfully.';
$labels['successrestore'] = 'The event has been restored successfully.';
$labels['errornotifying'] = 'Failed to send notifications to event participants';
$labels['errorimportingevent'] = 'Failed to import the event';
+$labels['importwarningexists'] = 'A copy of this event already exists in your calendar.';
$labels['newerversionexists'] = 'A newer version of this event already exists! Aborted.';
$labels['nowritecalendarfound'] = 'No calendar found to save the event';
$labels['importedsuccessfully'] = 'The event was successfully added to \'$calendar\'';
$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
$labels['itipsendsuccess'] = 'Invitation sent to participants.';
$labels['itipresponseerror'] = 'Failed to send the response to this event invitation';
$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 calendar and not be sent to the organizer of the event.';
$labels['importsuccess'] = 'Successfully imported $nr events';
$labels['importnone'] = 'No events found to be imported';
$labels['importerror'] = 'An error occured while importing';
$labels['aclnorights'] = 'You do not have administrator rights on this calendar.';
// recurrence form
$labels['repeat'] = 'Repeat';
$labels['frequency'] = 'Repeat';
$labels['never'] = 'never';
$labels['daily'] = 'daily';
$labels['weekly'] = 'weekly';
$labels['monthly'] = 'monthly';
$labels['yearly'] = 'annually';
$labels['rdate'] = 'on dates';
$labels['every'] = 'Every';
$labels['days'] = 'day(s)';
$labels['weeks'] = 'week(s)';
$labels['months'] = 'month(s)';
$labels['years'] = 'year(s) in:';
$labels['bydays'] = 'On';
$labels['untildate'] = 'the';
$labels['each'] = 'Each';
$labels['onevery'] = 'On every';
$labels['onsamedate'] = 'On the same date';
$labels['forever'] = 'forever';
$labels['recurrencend'] = 'until';
$labels['forntimes'] = 'for $nr time(s)';
$labels['first'] = 'first';
$labels['second'] = 'second';
$labels['third'] = 'third';
$labels['fourth'] = 'fourth';
$labels['last'] = 'last';
$labels['dayofmonth'] = 'Day of month';
$labels['addrdate'] = 'Add repeat date';
$labels['changeeventconfirm'] = 'Change event';
$labels['removeeventconfirm'] = 'Remove event';
$labels['changerecurringeventwarning'] = 'This is a recurring event. Would you like to edit the current event only, this and all future occurences, all occurences or save it as a new event?';
$labels['removerecurringeventwarning'] = 'This is a recurring event. Would you like to remove the current event only, this and all future occurences or all occurences of this event?';
$labels['currentevent'] = 'Current';
$labels['futurevents'] = 'Future';
$labels['allevents'] = 'All';
$labels['saveasnew'] = 'Save as new';
// birthdays calendar
$labels['birthdays'] = 'Birthdays';
$labels['birthdayscalendar'] = 'Birthdays Calendar';
$labels['displaybirthdayscalendar'] = 'Display birthdays calendar';
$labels['birthdayscalendarsources'] = 'From these address books';
$labels['birthdayeventtitle'] = '$name\'s Birthday';
$labels['birthdayage'] = 'Age $age';
?>
diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 346d61f4..87ef2da0 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -1,1479 +1,1480 @@
/*** Style for Calendar plugin ***/
body.calendarmain {
overflow: hidden;
}
#taskbar a.button-calendar {
background: url(images/calendar.png) 0px 1px no-repeat;
}
/* hack for IE 6/7 */
* html #taskbar a.button-calendar {
background-image: url(images/calendar.gif);
}
#main {
position: absolute;
clear: both;
top: 72px;
left: 0;
right: 0;
bottom: 10px;
}
#calendarsidebar {
position: absolute;
top: 0px;
left: 10px;
bottom: 0;
width: 230px;
}
#datepicker {
position: relative;
top: 42px;
width: 100%;
}
#datepicker .ui-datepicker {
width: 97% !important;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
}
#datepicker .ui-datepicker-activerange a {
border-color: #c33;
color: #a22;
}
#datepicker .ui-datepicker-activerange a.ui-state-active {
color: #fff;
}
#datepicker .ui-priority-secondary {
opacity: 0.4;
}
#datepicker td.ui-datepicker-week-col {
cursor: pointer;
}
#calendarsidebartoggle {
position: absolute;
left: 244px;
width: 8px;
top: 4px;
bottom: 0;
background: url(images/toggle.gif) 0 48% no-repeat transparent;
cursor: pointer;
}
div.sidebarclosed {
background-position: -8px 48% !important;
}
#calendarsidebartoggle:hover {
background-color: #ddd;
}
#calendar {
position: absolute;
top: 4px;
left: 256px;
right: 10px;
bottom: 0;
}
#print {
width: 680px;
}
pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
}
#calendars {
position: absolute;
top: 228px;
left: 0;
bottom: 0;
right: 0;
background-color: #F9F9F9;
border: 1px solid #999999;
overflow: hidden;
}
#calendarslist {
list-style: none;
margin: 0;
padding: 0;
}
#attachmentlist li,
#calendarslist li {
margin: 0;
padding: 1px;
display: block;
background: #fff;
border-bottom: 1px solid #EBEBEB;
white-space: nowrap;
cursor: default;
}
#calendarslist li label {
display: block;
}
#calendarslist li span.handle {
cursor: default;
background: url(images/calendars.png) 0 -2px no-repeat;
display: inline-block;
width: 20px;
}
#calendarslist li input {
margin-right: 5px;
}
#calendarslist li.selected {
background-color: #ccc;
border-bottom: 1px solid #bbb;
}
#calendarslist li.selected span {
font-weight: bold;
}
#calendarslist li.readonly span.handle {
background-position: 0 -20px;
}
#calendarslist li.other span.handle {
background-position: 0 -38px;
}
#calendarslist li.other.readonly span.handle {
background-position: 0 -56px;
}
#calendarslist li.shared span.handle {
background-position: 0 -74px;
}
#calendarslist li.shared.readonly span.handle {
background-position: 0 -92px;
}
#calendarslist li.virtual span.calname {
color: #666;
}
#calfeedurl,
#caldavurl {
width: 98%;
background: #fbfbfb;
padding: 4px;
margin-bottom: 1em;
resize: none;
}
#agendalist {
width: 100%;
margin: 0 auto;
margin-top: 60px;
border: 1px solid #C1DAD7;
display: none;
}
#agendalist table {
width: 100%;
}
#agendalist td,
#agendalist th {
border-right: 1px solid #C1DAD7;
border-bottom: 1px solid #C1DAD7;
background: #fff;
padding: 6px 6px 6px 12px;
}
#agendalist tr {
vertical-align: top;
}
#agendalist th {
font-weight: bold;
}
#calendartoolbar {
position: absolute;
top: 0px;
left: 0px;
height: 35px;
}
#calendartoolbar a {
padding-right: 10px;
}
#calendartoolbar a.button,
#calendartoolbar a.buttonPas {
display: block;
float: left;
width: 32px;
height: 32px;
padding: 0;
margin-right: 10px;
overflow: hidden;
background: url(images/toolbar.png) 0 0 no-repeat transparent;
opacity: 0.99; /* this is needed to make buttons appear correctly in Chrome */
}
#calendartoolbar a.buttonPas {
opacity: 0.35;
}
#calendartoolbar a.addeventSel {
background-position: 0 -32px;
}
#calendartoolbar a.delete {
background-position: -32px 0;
}
#calendartoolbar a.deleteSel {
background-position: -32px -32px;
}
#calendartoolbar a.print {
background-position: -64px 0;
}
#calendartoolbar a.printSel {
background-position: -64px -32px;
}
#calendartoolbar a.import {
background-position: -168px 0;
}
#calendartoolbar a.importSel {
background-position: -168px -32px;
}
#calendartoolbar a.export {
background-position: -128px 0;
}
#calendartoolbar a.exportSel {
background-position: -128px -32px;
}
.calendarmain #quicksearchbar {
top: 82px;
right: 4px;
}
div.uidialog {
display: none;
}
#user {
position: absolute;
top: 10px;
right: 100px;
left: 100px;
text-align: center;
}
a.morelink {
font-size: 90%;
color: #C33;
text-decoration: none;
}
a.morelink:hover {
text-decoration: underline;
}
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;
}
#partheader {
position: absolute;
top: 20px;
left: 220px;
right: 20px;
height: 40px;
}
#partheader table td {
padding-left: 2px;
padding-right: 4px;
vertical-align: middle;
font-size: 11px;
}
#partheader table td.title {
color: #666;
font-weight: bold;
}
.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-top: 2px;
padding-right: 8px;
white-space: nowrap;
}
.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;
}
#attachmentlist {
margin: 0 -0.8em;
}
#attachmentlist li {
padding: 2px 2px 3px 0.8em;
}
#eventshow .attachments-list ul li {
float: left;
}
#edit-attachments-form {
padding-top: 1.2em;
}
#edit-attachments-form .formbuttons {
margin: 0.5em 0;
}
.event-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
background: url(images/attendee-status.gif) right 0 no-repeat;
}
.event-attendees span.attendee a.mailtolink {
text-decoration: none;
white-space: nowrap;
}
.event-attendees span.attendee a.mailtolink:hover {
text-decoration: underline;
}
.event-attendees span.accepted {
background-position: right -20px;
}
.event-attendees span.declined {
background-position: right -40px;
}
.event-attendees span.tentative {
background-position: right -60px;
}
.event-attendees span.delegated {
background-position: right -180px;
}
.event-attendees span.organizer {
background-position: right -80px;
}
#all-event-attendees span.attendee {
display: block;
margin-bottom: 4px;
padding-bottom: 3px;
border-bottom: 1px solid #ddd;
}
/* jQuery UI overrides */
#eventshow h1 {
font-size: 20px;
margin: 0.1em 0 0.4em 0;
}
#eventshow label,
#eventshow h5.label {
font-weight: normal;
font-size: 0.9em;
color: #999;
margin: 0 0 0.2em 0;
}
#eventshow {
margin: 0 -0.2em;
}
#eventshow.sensitivity-private {
background: url(images/badge_private.png) top right no-repeat;
}
#eventshow.sensitivity-confidential {
background: url(images/badge_confidential.png) top right no-repeat;
}
.sensitivity-private #event-title {
margin-right: 50px;
}
.sensitivity-confidential #event-title {
margin-right: 60px;
}
#eventshow div.event-line {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
#eventedit {
position: relative;
padding: 0.5em 0.1em;
}
#eventedit input.text,
#eventedit textarea {
width: 97%;
}
#eventtabs {
position: relative;
padding: 0;
border: 0;
border-radius: 0;
}
div.form-section,
#eventshow div.event-section,
#eventtabs div.event-section {
margin-top: 0.2em;
margin-bottom: 0.8em;
}
#eventtabs .tabsbar {
position: absolute;
top: 0;
}
#eventtabs .ui-tabs-panel {
padding: 1em 0.8em;
border: 1px solid #aaa;
border-width: 0 1px 1px 1px;
}
#eventtabs .ui-tabs-nav {
background: none;
padding: 0;
border-width: 0 0 1px 0;
border-radius: 0;
}
#eventtabs .border-after {
padding-bottom: 0.6em;
margin-bottom: 0.6em;
border-bottom: 1px solid #999;
}
#eventshow label,
#eventedit label,
.form-section label {
display: inline-block;
min-width: 7em;
padding-right: 0.5em;
}
#eventedit .formtable td.label {
min-width: 6em;
}
td.topalign {
vertical-align: top;
}
#eventedit label.weekday,
#eventedit label.monthday {
min-width: 3em;
}
#eventedit label.month {
min-width: 5em;
}
#edit-recurrence-yearly-bymonthblock {
margin-left: 7.5em;
}
#edit-recurrence-rdates {
display: block;
list-style: none;
margin: 0 0 0.8em 0;
padding: 0;
max-height: 300px;
overflow: auto;
}
#edit-recurrence-rdates li {
display: block;
position: relative;
width: 14em;
padding: 1px;
}
#edit-recurrence-rdates li a.delete {
position: absolute;
top: 1px;
right: 0;
}
#eventedit .recurrence-form {
display: none;
}
#eventedit .formtable td {
padding: 0.2em 0;
}
.ui-dialog .event-update-confirm {
padding: 0 0.5em 0.5em 0.5em;
}
.event-dialog-message,
.event-update-confirm .message {
margin-top: 0.5em;
padding: 0.8em;
background-color: #F7FDCB;
border: 1px solid #C2D071;
}
.event-dialog-message .message,
.event-update-confirm .message {
margin-bottom: 0.5em;
}
.edit-recurring-warning .savemode {
padding-left: 20px;
}
.event-update-confirm .savemode {
padding-left: 30px;
}
.event-dialog-message span.ui-icon,
.event-update-confirm span.ui-icon {
float: left;
margin: 0 7px 20px 0;
}
.event-dialog-message label,
.event-update-confirm label {
min-width: 3em;
padding-right: 1em;
}
.event-update-confirm a.button {
margin: 0 0.5em 0 0.2em;
min-width: 5em;
}
#event-rsvp,
#edit-attendees-notify {
margin: 0.3em 0;
padding: 0.5em;
background-color: #F7FDCB;
border: 1px solid #C2D071;
}
.edit-attendees-table {
width: 100%;
display: table;
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #ccc;
}
.edit-attendees-table td {
padding: 3px;
border-bottom: 1px solid #ccc;
}
.edit-attendees-table td.role {
width: 8em;
}
.edit-attendees-table td.availability,
.edit-attendees-table td.confirmstate {
width: 4em;
}
.edit-attendees-table td.options {
width: 3em;
text-align: right;
padding-right: 4px;
}
.edit-attendees-table td.name {
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.edit-attendees-table thead td {
background: url(images/listheader.gif) top left repeat-x #CCC;
}
#edit-attendees-form,
#edit-resources-form {
position: relative;
margin-top: 1em;
}
#edit-attendees-form #edit-attendee-schedule,
#edit-resources-form #edit-resource-find {
position: absolute;
top: 0;
right: 0;
}
.edit-attendees-table select.edit-attendee-role {
border: 0;
padding: 2px;
background: white;
}
.availability img.availabilityicon {
margin: 1px;
width: 14px;
height: 14px;
border-radius: 4px;
-moz-border-radius: 4px;
}
.availability img.availabilityicon.loading {
background: url(images/loading_blue.gif) center no-repeat;
}
#schedule-freebusy-times td.unknown,
.availability img.availabilityicon.unknown {
background: #ddd;
}
#schedule-freebusy-times td.free,
.availability img.availabilityicon.free {
background: #0c0;
}
#schedule-freebusy-times td.busy,
.availability img.availabilityicon.busy {
background: #c00;
}
#schedule-freebusy-times td.tentative,
.availability img.availabilityicon.tentative {
background: #66d;
}
#schedule-freebusy-times td.out-of-office,
.availability img.availabilityicon.out-of-office {
background: #f0b400;
}
#schedule-freebusy-times td.all-busy,
#schedule-freebusy-times td.all-tentative,
#schedule-freebusy-times td.all-out-of-office {
background-image: url(images/freebusy-colors.png);
background-position: top right;
background-repeat: no-repeat;
}
#schedule-freebusy-times td.all-tentative {
background-position: right -40px;
}
#schedule-freebusy-times td.all-out-of-office {
background-position: right -80px;
}
#edit-attendees-legend {
margin-top: 3em;
margin-bottom: 0.5em;
}
#edit-attendees-legend .legend {
margin-right: 2em;
white-space: nowrap;
}
#edit-attendees-legend img.availabilityicon {
vertical-align: middle;
}
.edit-attendees-table tbody td.confirmstate {
overflow: hidden;
white-space: nowrap;
text-indent: -2000%;
}
.edit-attendees-table td.confirmstate span {
display: block;
width: 20px;
background: url(images/attendee-status.gif) 5px 0 no-repeat;
}
.edit-attendees-table td.confirmstate span.needs-action {
}
.edit-attendees-table td.confirmstate span.accepted {
background-position: 5px -20px;
}
.edit-attendees-table td.confirmstate span.declined {
background-position: 5px -40px;
}
.edit-attendees-table td.confirmstate span.tentative {
background-position: 5px -60px;
}
.edit-attendees-table td.confirmstate span.delegated {
background-position: 5px -180px;
}
#attendees-freebusy-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin: 0.5em 0;
}
#attendees-freebusy-table td.attendees {
width: 18em;
border: 1px solid #ccc;
vertical-align: top;
overflow: hidden;
}
#attendees-freebusy-table td.times {
width: auto;
vertical-align: top;
border: 1px solid #ccc;
}
#attendees-freebusy-table div.scroll {
position: relative;
overflow: auto;
}
#attendees-freebusy-table h3.boxtitle {
margin: 0;
height: auto !important;
border-color: #ccc;
}
.attendees-list .attendee {
padding: 3px 4px 3px 1px;
background: url(images/attendee-status.gif) 2px -97px no-repeat;
white-space: nowrap;
}
.attendees-list a.attendee-role-toggle {
display: inline-block;
width: 16px;
margin-right: 3px;
cursor: pointer;
}
.attendees-list div.attendee {
border-top: 1px solid #ccc;
}
.attendees-list span.attendee {
padding-left: 20px;
margin-right: 2em;
}
.attendees-list .organizer {
background-position: 3px -77px;
}
.attendees-list .opt-participant {
background-position: 2px -117px;
}
.attendees-list .non-participant {
background-position: 2px -137px;
}
.attendees-list .chair {
background-position: 2px -157px;
}
.attendees-list .loading {
background: url(images/loading_blue.gif) 1px 50% no-repeat;
}
.attendees-list .total {
background: none;
padding-left: 4px;
font-weight: bold;
}
.attendees-list .spacer,
#schedule-freebusy-times tr.spacer td {
background: 0;
font-size: 50%;
}
#schedule-freebusy-times {
border-collapse: collapse;
width: 100%;
}
#schedule-freebusy-times td {
padding: 3px;
border: 1px solid #ccc;
}
#schedule-freebusy-times tr.dates th {
border-color: #aaa;
border-style: solid;
border-width: 0 1px 0 1px;
white-space: nowrap;
}
#attendees-freebusy-table div.timesheader,
#schedule-freebusy-times tr.times td {
min-width: 30px;
font-size: 9px;
padding: 5px 2px 6px 2px;
text-align: center;
}
#schedule-freebusy-times tr.times td.allday {
min-width: 60px;
}
#schedule-freebusy-times tr.times td {
cursor: pointer;
}
#schedule-event-time {
position: absolute;
border: 2px solid #333;
background: #777;
background: rgba(60, 60, 60, 0.6);
opacity: 0.5;
border-radius: 4px;
cursor: move;
filter: alpha(opacity=40); /* IE8 */
}
#eventfreebusy .schedule-options {
position: relative;
margin-bottom: 1.5em;
}
#eventfreebusy .schedule-buttons {
position: absolute;
top: 0;
right: 0;
}
#eventfreebusy .schedule-find-buttons {
padding-bottom:0.5em;
}
#eventfreebusy .schedule-find-buttons button {
min-width: 9em;
text-align: center;
}
span.edit-alarm-set {
white-space: nowrap;
}
a.dropdown-link {
color: #CC0000;
font-size: 12px;
text-decoration: none;
}
a.dropdown-link:after {
content: ' ▼';
font-size: 11px;
color: #666;
}
#eventedit .ui-tabs-panel {
min-height: 20em;
}
.alarm-item {
margin: 0.4em 0 1em 0;
}
.alarm-item .event-title {
font-size: 14px;
margin: 0.1em 0 0.3em 0;
}
.alarm-item div.event-section {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
.alarm-item .alarm-actions {
margin-top: 0.4em;
}
.alarm-item div.alarm-actions a {
color: #CC0000;
margin-right: 0.8em;
text-decoration: none;
}
a.alarm-action-snooze:after {
content: ' ▼';
font-size: 10px;
color: #666;
}
#alarm-snooze-dropdown {
z-index: 5000;
}
.ui-dialog-buttonset a.dropdown-link {
margin-right: 1em;
}
.ui-datepicker-calendar .ui-datepicker-today .ui-state-default {
border-color: #cccccc;
background: #ffffcc;
color: #000;
}
.ui-datepicker-calendar .ui-datepicker-week-col {
text-align: right;
padding-right: 0.5em;
}
.ui-datepicker th {
padding: 0.3em 0;
font-size: 10px;
}
.ui-datepicker td span,
.ui-datepicker td a {
padding-left: 0.1em;
}
.ui-autocomplete {
max-height: 160px;
overflow-y: auto;
overflow-x: hidden;
}
.ui-autocomplete .ui-menu-item {
white-space: nowrap;
}
* html .ui-autocomplete {
height: 160px;
}
span.spacer {
padding-left: 3em;
}
#agendaoptions {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: auto;
z-index: 200;
border: 1px solid #ccc;
padding: 2px 5px 1px;
font-size: 90%;
}
#agendaoptions label {
color: #444;
text-shadow: 1px 1px #eee;
padding-right: 0.5em;
}
#calendar-kolabform {
position: relative;
padding-top: 24px;
margin: 0 -8px;
min-width: 660px;
min-height: 400px;
}
#calendar-kolabform div.tabsbar {
top: 0;
right: 2px;
left: 2px;
height: 24px;
}
#calendar-kolabform fieldset.tabbed {
background-color: #fff;
margin-top: 0;
}
#calendar-kolabform span.tablink {
background-color: #e8e8e9;
background-image: -moz-linear-gradient(center top, #f4f4f4, #e6e6e6);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0.0, #f4f4f4), color-stop(1.0, #e6e6e6));
filter: progid:DXImageTransform.Microsoft.gradient(enabled='true', startColorstr=#f4f4f4, endColorstr=#e6e6e6, GradientType=0);
height: 24px !important;
}
#calendar-kolabform span.tablink-selected {
background: #fff;
height: 25px !important;
}
#calendar-kolabform span.tablink a,
#calendar-kolabform span.tablink-selected a {
background: none;
border: 1px solid #AAAAAA;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
padding: 4px 10px 0 10px;
margin-left: 0;
}
#calendar-kolabform table td.title {
font-weight: bold;
white-space: nowrap;
color: #666;
padding-right: 10px;
}
#resource-dialog-right {
position: absolute;
top: 10px;
left: 300px;
right: 8px;
bottom: 10px;
}
#resource-info,
#resource-availability {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 48%;
border: 1px solid #999;
background-color: #F9F9F9;
overflow: auto;
}
#resource-availability {
top: auto;
bottom: 0;
height: 49%;
overflow: hidden;
}
#resource-info .boxtitle,
#resource-availability .boxtitle {
margin-top: 0;
}
#resource-freebusy-calendar {
position: absolute;
top: 20px;
left: -1px;
right: -1px;
bottom: -1px;
}
#resource-freebusy-calendar .fc-content {
top: 0;
}
#resource-freebusy-calendar .fc-content .fc-event-bg {
background: 0;
}
#resource-freebusy-calendar .fc-event.status-busy,
#resource-freebusy-calendar .status-busy .fc-event-skin {
border-color: #e26569;
background-color: #e26569;
}
#resource-freebusy-calendar .fc-event.status-tentative,
#resource-freebusy-calendar .status-tentative .fc-event-skin {
border-color: #8383fc;
background: #8383fc;
}
#resource-freebusy-calendar .fc-event.status-outofoffice,
#resource-freebusy-calendar .status-outofoffice .fc-event-skin {
border-color: #fbaa68;
background: #fbaa68;
}
#resources-list div.treetoggle {
left: 3px !important;
top: -2px;
}
#resources-list li ul div.treetoggle {
left: 23px !important;
}
#resource-selection {
position: absolute;
top: 10px;
bottom: 10px;
left: 8px;
width: 280px;
border: 1px solid #999999;
background-color: #F9F9F9;
overflow: hidden;
}
#resource-selection .boxlistcontent {
top: 25px;
border-top: 1px solid #eee;
}
#resourcequicksearch {
position: absolute;
top: 3px;
left: 7px;
right: 4px;
height: 17px;
background: #fff;
border: 1px solid #888;
border-radius: 10px;
-webkit-box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3);
-moz-box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3);
box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3);
}
#resourcesearchbox {
position: absolute;
top: 1px;
left: 24px;
width: 140px;
height: 15px;
font-size: 11px;
padding: 0px;
border: none;
outline: none;
background: #fff;
}
#resourcesearchreset {
position: absolute;
top: 2px;
right: 2px;
text-decoration: none;
}
#resource-details,
#resource-details-owner {
margin: 8px;
}
#resource-details td.title,
#resource-details-owner td.title {
color: #666;
padding-right: 10px;
min-width: 10em;
}
#resource-details-owner thead td {
color: #333;
font-size: 13px;
font-weight: bold;
}
/* fullcalendar style overrides */
#calendar .fc-header-right {
padding-right: 200px;
padding-top: 4px;
}
.rcube-fc-content {
position: absolute !important;
top: 38px;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.fc-event-title {
font-weight: bold;
}
.fc-event-hori .fc-event-title {
font-weight: normal;
white-space: nowrap;
}
.fc-event-hori .fc-event-time {
white-space: nowrap;
font-weight: normal !important;
font-size: 10px;
padding-right: 0.6em;
}
.fc-grid .fc-event-time {
font-weight: normal !important;
padding-right: 0.3em;
}
.fc-event-cateories {
font-style:italic;
}
div.fc-event-location {
font-size: 90%;
}
.fc-more-link {
color: #999;
padding-top: 1px;
cursor: pointer;
}
.fc-agenda-slots td div {
height: 22px;
}
.fc-mon, .fc-tue, .fc-wed, .fc-thu, .fc-fri {
background-color: #fdfdfd;
}
.fc-widget-header {
background-color: #fff;
}
.fc-icon-alarms,
.fc-icon-sensitive,
.fc-icon-recurring {
display: inline-block;
width: 11px;
height: 11px;
background: url(images/eventicons.gif) 0 0 no-repeat;
margin-left: 3px;
line-height: 10px;
}
.fc-icon-alarms {
background-position: 0 -13px;
}
.fc-icon-sensitive {
background-position: 0 -25px;
}
.fc-list-section .fc-event {
cursor: pointer;
}
.fc-view-list div.fc-list-header,
.fc-view-table td.fc-list-header,
.edit-attendees-table thead td {
padding: 3px;
background: #dddddd;
background-image: -moz-linear-gradient(center top, #f4f4f4, #d2d2d2);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0.00, #f4f4f4), color-stop(1.00, #d2d2d2));
filter: progid:DXImageTransform.Microsoft.gradient(enabled='true', startColorstr=#f4f4f4, endColorstr=#d2d2d2, GradientType=0);
font-weight: bold;
color: #333;
}
.fc-view-list .fc-event-skin .fc-event-content {
background: #F6F6F6;
padding: 2px;
}
.fc-view-list .fc-event-skin .fc-event-title,
.fc-view-list .fc-event-skin .fc-event-location {
color: #333;
}
.fc-view-table table.fc-list-smart {
table-layout: auto;
}
.fc-listappend {
text-align: center;
margin: 1em 0;
}
.fc-listappend .message {
padding: 0.5em;
margin-bottom: 0.5em;
font-size: 150%;
color: #999;
}
.fc-listappend .formlinks a {
font-size: 12px;
padding: 0 0.3em;
}
.fc-event-temp {
opacity: 0.4;
filter: alpha(opacity=40); /* IE8 */
}
/* Settings section */
fieldset #calendarcategories div {
margin-bottom: 0.3em;
}
/* Invitation UI in mail */
-#messagemenu li a.calendarlink {
+#messagemenu li a.calendarlink,
+#attachmentmenu li a.calendarlink {
background-image: url(images/calendars.png);
background-position: 7px -109px;
background-repeat: no-repeat;
}
div.calendar-invitebox {
min-height: 20px;
margin: 5px 8px;
padding: 3px 6px 6px 34px;
border: 1px solid #C2D071;
background: url(images/calendar.png) 6px 5px no-repeat #F7FDCB;
}
div.calendar-invitebox td.ititle {
font-weight: bold;
padding-right: 0.5em;
}
div.calendar-invitebox td.label {
color: #666;
padding-right: 1em;
}
#event-rsvp .rsvp-buttons,
div.calendar-invitebox .itip-buttons div {
margin-top: 0.5em;
}
#event-rsvp input.button,
div.calendar-invitebox input.button,
div.calendar-invitebox select {
font-size: 11px;
margin-right: 0.5em;
}
div.calendar-invitebox .folder-select {
font-size: 11px;
margin-left: 1em;
}
div.calendar-invitebox .rsvp-status.loading {
color: #666;
padding: 1px 0 2px 24px;
background: url(images/loading_blue.gif) top left no-repeat;
}
div.calendar-invitebox .rsvp-status.declined,
div.calendar-invitebox .rsvp-status.tentative,
div.calendar-invitebox .rsvp-status.delegated,
div.calendar-invitebox .rsvp-status.accepted {
padding: 0 0 1px 22px;
background: url(images/attendee-status.gif) 2px -20px no-repeat;
}
div.calendar-invitebox .rsvp-status.declined {
background-position: 2px -40px;
}
div.calendar-invitebox .rsvp-status.tentative {
background-position: 2px -60px;
}
div.calendar-invitebox .rsvp-status.delegated {
background-position: 2px -180px;
}
/* iTIP attend reply page */
.calendaritipattend .centerbox {
width: 40em;
margin: 80px auto;
padding: 10px 10px 10px 90px;
border: 1px solid #ccc;
box-shadow: 1px 1px 24px #ccc;
-moz-box-shadow: 1px 1px 18px #ccc;
-webkit-box-shadow: #ccc 1px 1px 18px;
background: url(images/invitation.png) 10px 10px no-repeat #fbfbfb;
}
.calendaritipattend .calendar-invitebox {
background: none;
padding-left: 0;
border: 0;
margin: 0 0 2em 0;
}
.calendaritipattend .calendar-invitebox .rsvp-status {
margin-top: 2.5em;
font-size: 110%;
font-weight: bold;
}
.calendaritipattend .calendar-invitebox td.title,
.calendaritipattend .calendar-invitebox td.ititle {
font-size: 120%;
}
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index d2ade1a1..803ceb6f 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1,1645 +1,1646 @@
/**
* Roundcube Calendar plugin styles for skin "Larry"
*
* Copyright (c) 2012, Kolab Systems AG <contact@kolabsys.com>
* 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.
*/
body.calendarmain {
overflow: hidden;
}
body.calendarmain #mainscreen {
left: 0;
}
body.attachmentwin #mainscreen {
top: 60px;
}
body.attachmentwin #topnav .topright {
margin-top: 20px;
}
#calendarsidebar {
position: absolute;
top: 0;
left: 10px;
bottom: 0;
width: 250px;
}
#datepicker {
position: absolute;
top: 40px;
left: 0;
width: 100%;
min-height: 190px;
}
#datepicker .ui-datepicker {
width: 100% !important;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
}
#datepicker .ui-datepicker td a {
padding: 5px 4px;
font-size: 12px;
}
#datepicker td.ui-datepicker-activerange {
border-color: #69a2b6;
}
#datepicker .ui-datepicker-activerange a {
color: #185d7a;
background: #d9f1fb;
background: -moz-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d9f1fb), color-stop(100%,#c5e3ee));
background: -o-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: -ms-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d9f1fb', endColorstr='#c5e3ee', GradientType=0);
}
#datepicker .ui-datepicker-activerange a.ui-state-active {
color: #fff;
background: #00acd4;
background: -moz-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00acd4), color-stop(100%,#008fc7));
background: -o-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: -ms-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: linear-gradient(top, #00acd4 0%, #008fc7 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00acd4', endColorstr='#008fc7', GradientType=0);
}
#datepicker td.ui-datepicker-week-col {
cursor: pointer;
}
#datepicker .ui-datepicker-title {
margin: 2px 2.3em 3px 2.3em;
}
#datepicker .ui-datepicker .ui-datepicker-prev,
#datepicker .ui-datepicker .ui-datepicker-next {
top: 4px;
}
#calendarsidebartoggle {
position: absolute;
left: 264px;
width: 8px;
top: 40px;
bottom: 0;
background: url(images/toggle.gif) 0 48% no-repeat transparent;
cursor: pointer;
}
div.sidebarclosed {
background-position: -8px 48% !important;
}
#calendarsidebartoggle:hover {
background-color: #ddd;
}
#calendar {
position: absolute;
top: 0;
left: 276px;
right: 0;
bottom: 0;
}
.calendarmain #message.statusbar {
border: 1px solid #c3c3c3;
border-bottom-color: #ababab;
}
#timezonedisplay {
position: absolute;
bottom: 5px;
right: 12px;
font-size: 0.85em;
color: #666;
}
#print {
width: 680px;
}
pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
}
#calendars {
position: absolute;
top: 276px;
left: 0;
bottom: 0;
right: 0;
}
#calendarslist li {
margin: 0;
height: 20px;
padding: 6px 8px 2px;
display: block;
position: relative;
}
#calendarslist li.virtual {
height: 12px;
}
#calendarslist li label {
display: block;
}
#calendarslist li span.calname {
display: block;
position: absolute;
top: 6px;
left: 26px;
right: 24px;
cursor: default;
background: url(images/calendars.png) right 20px no-repeat;
padding-bottom: 2px;
padding-right: 30px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #004458;
}
#calendarslist li span.handle {
display: inline-block;
padding: 0;
border-radius: 7px;
margin-right: 6px;
width: 10px;
height: 10px;
font-size: 0.8em;
border: 1px solid rgba(0, 0, 0, 0.5);
-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
-moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
#calendarslist li input {
position: absolute;
top: 4px;
right: 5px;
}
#calendarslist li.selected {
background-color: #c7e3ef;
}
#calendarslist li.selected span.calname {
font-weight: bold;
}
#calendarslist li.readonly span.calname {
background-position: right -20px;
}
#calendarslist li.other span.calname {
background-position: right -38px;
}
#calendarslist li.other.readonly span.calname {
background-position: right -56px;
}
#calendarslist li.shared span.calname {
background-position: right -74px;
}
#calendarslist li.shared.readonly span.calname {
background-position: right -92px;
}
#calendarslist li.virtual span.calname {
color: #aaa;
top: 2px;
}
#calfeedurl,
#caldavurl {
width: 98%;
background: #fbfbfb;
padding: 4px;
margin-bottom: 1em;
resize: none;
}
#agendalist {
width: 100%;
margin: 0 auto;
margin-top: 60px;
border: 1px solid #C1DAD7;
display: none;
}
#agendalist table {
width: 100%;
}
#agendalist td,
#agendalist th {
border-right: 1px solid #C1DAD7;
border-bottom: 1px solid #C1DAD7;
background: #fff;
padding: 6px 6px 6px 12px;
}
#agendalist tr {
vertical-align: top;
}
#agendalist th {
font-weight: bold;
}
#calendartoolbar {
position: absolute;
top: -6px;
left: 0;
height: 40px;
white-space: nowrap;
}
#calendartoolbar a.button {
background-image: url(images/toolbar.png);
padding-left: 0;
padding-right: 0;
min-width: 50px;
max-width: 60px;
}
#calendartoolbar a.button.addevent {
background-position: center 1px;
max-width: 70px;
}
#calendartoolbar a.button.export {
background-position: center -40px;
}
#calendartoolbar a.button.import {
background-position: center -440px;
}
#calendartoolbar a.button.print {
background-position: center -80px;
}
body.calendarmain #quicksearchbar {
z-index: 200;
}
body.calendarmain #searchmenulink {
width: 15px;
}
div.uidialog {
display: none;
}
#user {
position: absolute;
top: 10px;
right: 100px;
left: 100px;
text-align: center;
}
a.morelink {
font-size: 90%;
color: #0069a6;
text-decoration: none;
}
a.morelink:hover {
text-decoration: underline;
}
a.miniColors-trigger {
margin-top: -3px;
}
#attachmentcontainer {
position: absolute;
top: 60px;
left: 0px;
right: 0px;
bottom: 0px;
}
#attachmentframe {
width: 100%;
height: 100%;
border: 0;
background-color: #fff;
border-radius: 4px;
}
#partheader {
position: relative;
padding: 3px 0;
background: #f9f9f9;
background: -moz-linear-gradient(top, #fff 0%, #e9e9e9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fff), color-stop(100%,#e9e9e9));
background: -o-linear-gradient(top, #fff 0%, #e9e9e9 100%);
background: -ms-linear-gradient(top, #fff 0%, #e9e9e9 100%);
background: linear-gradient(top, #fff 0%, #e9e9e9 100%);
}
#partheader table td {
color: #666;
padding: 2px 8px;
}
#partheader table td.header {
font-weight: bold;
}
#partheader table td.title a {
color: #666;
text-decoration: none;
}
#edit-attachments {
margin: 0.6em 0;
}
#edit-attachments ul li {
display: block;
color: #333;
font-weight: bold;
padding: 4px 4px 3px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
}
#edit-attachments ul li a.file {
padding: 0;
}
#edit-attachments-form {
margin-top: 1em;
padding-top: 0.8em;
border-top: 2px solid #fafafa;
}
#edit-attachments-form .formbuttons {
margin: 0.5em 0;
}
#eventedit .droptarget {
background-image: url(../../../../skins/larry/images/filedrop.png) !important;
background-position: center bottom !important;
background-repeat: no-repeat !important;
}
#eventedit .droptarget.hover,
#eventedit .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);
}
#eventedit .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);
}
#event-attachments .attachmentslist li {
float: left;
margin-right: 1em;
}
#event-attachments .attachmentslist li a {
outline: none;
}
.event-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
background: url(images/attendee-status.png) right 0 no-repeat;
}
.event-attendees span.attendee a.mailtolink {
text-decoration: none;
white-space: nowrap;
outline: none;
}
.event-attendees span.attendee a.mailtolink:hover {
text-decoration: underline;
}
.event-attendees span.accepted {
background-position: right -20px;
}
.event-attendees span.declined {
background-position: right -40px;
}
.event-attendees span.tentative {
background-position: right -60px;
}
.event-attendees span.delegated {
background-position: right -180px;
}
.event-attendees span.organizer {
background-position: right -80px;
}
#all-event-attendees span.attendee {
display: block;
margin-bottom: 0.4em;
padding-bottom: 0.3em;
border-bottom: 1px solid #ddd;
}
.calendarmain .fc-view-table td.fc-list-header,
#attendees-freebusy-table h3.boxtitle,
#schedule-freebusy-times thead th,
.edit-attendees-table thead td
{
color: #69939e;
font-size: 11px;
font-weight: bold;
background: #d6eaf3;
background: -moz-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -webkit-gradient(linear, left top, right top, color-stop(0,#e3f2f6), color-stop(8%,#d6eaf3), color-stop(100%,#d6eaf3));
background: -o-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%);
background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
border: 0;
border-bottom: 1px solid #ccc;
height: 18px;
padding: 8px 7px 3px 7px;
}
/* jQuery UI overrides */
#eventshow h1 {
font-size: 18px;
margin: -0.3em 0 0.4em 0;
}
#eventshow label,
#eventshow h5.label {
font-weight: normal;
font-size: 1em;
color: #999;
margin: 0 0 0.2em 0;
}
#eventshow {
margin: 0 -0.2em;
}
#eventshow.sensitivity-private {
background: url(images/badge_private.png) top right no-repeat;
}
#eventshow.sensitivity-confidential {
background: url(images/badge_confidential.png) top right no-repeat;
}
.sensitivity-private #event-title {
margin-right: 50px;
}
.sensitivity-confidential #event-title {
margin-right: 60px;
}
#eventshow div.event-line {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
#eventedit {
position: relative;
top: -1.5em;
padding: 0.5em 0.1em;
margin: 0 -0.2em;
}
#eventedit input.text,
#eventedit textarea {
width: 97%;
}
#eventtabs {
position: relative;
padding: 0;
border: 0;
border-radius: 0;
}
div.form-section,
#eventshow div.event-section,
#eventtabs div.event-section {
margin-top: 0.2em;
margin-bottom: 0.6em;
}
#eventtabs .border-after {
padding-bottom: 0.8em;
margin-bottom: 0.8em;
border-bottom: 2px solid #fafafa;
}
#eventshow label,
#eventedit label,
.form-section label {
display: inline-block;
min-width: 7em;
padding-right: 0.5em;
}
#eventedit .formtable td.label {
min-width: 6em;
}
td.topalign {
vertical-align: top;
}
#eventedit label.weekday,
#eventedit label.monthday {
min-width: 3em;
}
#eventedit label.month {
min-width: 5em;
}
#edit-recurrence-yearly-bymonthblock {
margin-left: 7.5em;
}
#eventedit .recurrence-form {
display: none;
}
#edit-recurrence-rdates {
display: block;
list-style: none;
margin: 0 0 0.8em 0;
padding: 0;
max-height: 300px;
overflow: auto;
}
#edit-recurrence-rdates li {
display: block;
position: relative;
width: 12em;
padding: 4px 0 4px 0;
}
#edit-recurrence-rdates li a.delete {
position: absolute;
top: 2px;
right: 0;
width: 20px;
height: 18px;
background-position: -7px -337px;
}
#eventedit .formtable td {
padding: 0.2em 0;
}
.ui-dialog .event-update-confirm {
padding: 0 0.5em 0.5em 0.5em;
}
.event-dialog-message,
.event-update-confirm .message {
margin-top: 0.5em;
padding: 0.8em;
border: 1px solid #ffdf0e;
background-color: #fef893;
}
.event-dialog-message .message,
.event-update-confirm .message {
margin-bottom: 0.5em;
}
.edit-recurring-warning .savemode {
padding-left: 20px;
}
.event-update-confirm .savemode {
padding-left: 30px;
}
.event-dialog-message span.ui-icon,
.event-update-confirm span.ui-icon {
float: left;
margin: 0 7px 20px 0;
}
.event-dialog-message label,
.event-update-confirm label {
min-width: 3em;
padding-right: 1em;
}
.event-update-confirm a.button {
margin: 0 0.5em 0 0.2em;
min-width: 5em;
}
#event-rsvp,
#edit-attendees-notify {
margin: 0.3em 0;
padding: 0.5em;
}
.edit-attendees-table {
width: 100%;
margin-top: 0.5em;
}
.edit-attendees-table td.role {
width: 9em;
}
.edit-attendees-table td.availability,
.edit-attendees-table td.confirmstate {
width: 4em;
}
.edit-attendees-table td.options {
width: 3em;
text-align: right;
padding-right: 4px;
}
.edit-attendees-table td.name {
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.edit-attendees-table a.deletelink {
display: block;
width: 17px;
height: 17px;
padding: 0;
overflow: hidden;
text-indent: 1000px;
}
#edit-attendees-form,
#edit-resources-form {
position: relative;
margin-top: 1em;
}
#edit-attendees-form #edit-attendee-schedule,
#edit-resources-form #edit-resource-find {
position: absolute;
top: 0;
right: 0;
}
.edit-attendees-table select.edit-attendee-role {
border: 0;
padding: 2px;
background: white;
}
.availability img.availabilityicon {
margin: 1px;
width: 14px;
height: 14px;
border-radius: 4px;
-moz-border-radius: 4px;
}
.availability img.availabilityicon.loading {
background: url(images/loading_blue.gif) center no-repeat;
}
#schedule-freebusy-times td.unknown,
.availability img.availabilityicon.unknown {
background: #ddd;
}
#schedule-freebusy-times td.free,
.availability img.availabilityicon.free {
background: #abd640;
}
#schedule-freebusy-times td.busy,
.availability img.availabilityicon.busy {
background: #e26569;
}
#schedule-freebusy-times td.tentative,
.availability img.availabilityicon.tentative {
background: #8383fc;
}
#schedule-freebusy-times td.out-of-office,
.availability img.availabilityicon.out-of-office {
background: #fbaa68;
}
#schedule-freebusy-times td.all-busy,
#schedule-freebusy-times td.all-tentative,
#schedule-freebusy-times td.all-out-of-office {
background-image: url(images/freebusy-colors.png);
background-position: top right;
background-repeat: no-repeat;
}
#schedule-freebusy-times td.all-tentative {
background-position: right -40px;
}
#schedule-freebusy-times td.all-out-of-office {
background-position: right -80px;
}
#edit-attendees-legend {
margin-top: 3em;
margin-bottom: 0.5em;
}
#edit-attendees-legend .legend {
margin-right: 2em;
white-space: nowrap;
}
#edit-attendees-legend img.availabilityicon {
vertical-align: middle;
}
.edit-attendees-table tbody td.confirmstate {
overflow: hidden;
white-space: nowrap;
text-indent: -2000%;
}
.edit-attendees-table td.confirmstate span {
display: block;
width: 20px;
background: url(images/attendee-status.png) 5px 0 no-repeat;
}
.edit-attendees-table td.confirmstate span.needs-action {
}
.edit-attendees-table td.confirmstate span.accepted {
background-position: 5px -20px;
}
.edit-attendees-table td.confirmstate span.declined {
background-position: 5px -40px;
}
.edit-attendees-table td.confirmstate span.tentative {
background-position: 5px -60px;
}
.edit-attendees-table td.confirmstate span.delegated {
background-position: 5px -180px;
}
#attendees-freebusy-table {
width: 100%;
table-layout: fixed;
border: 1px solid #bbd3da;
}
#attendees-freebusy-table td.attendees {
width: 18em;
vertical-align: top;
overflow: hidden;
}
#attendees-freebusy-table td.times {
width: auto;
vertical-align: top;
}
#attendees-freebusy-table div.scroll {
position: relative;
overflow: auto;
}
#attendees-freebusy-table h3.boxtitle {
margin: 0;
border-color: #ccc;
}
.attendees-list .attendee {
padding: 4px 4px 4px 1px;
background: url(images/attendee-status.png) 2px -97px no-repeat;
white-space: nowrap;
}
.attendees-list a.attendee-role-toggle {
display: inline-block;
width: 16px;
margin-right: 3px;
cursor: pointer;
}
.attendees-list div.attendee {
border-top: 1px solid #ccc;
}
.attendees-list span.attendee {
padding-left: 20px;
margin-right: 2em;
}
.attendees-list .organizer {
background-position: 3px -77px;
}
.attendees-list .opt-participant {
background-position: 2px -117px;
}
.attendees-list .non-participant {
background-position: 2px -137px;
}
.attendees-list .chair {
background-position: 2px -157px;
}
.attendees-list .loading {
background: url(images/loading_blue.gif) 1px 50% no-repeat;
}
.attendees-list .total {
background: none;
padding-left: 4px;
font-weight: bold;
}
.attendees-list .spacer,
#schedule-freebusy-times tr.spacer td {
background: 0;
font-size: 50%;
}
#schedule-freebusy-times {
border-collapse: collapse;
width: 100%;
}
#schedule-freebusy-times td {
padding: 4px;
border: 1px solid #ccc;
}
#attendees-freebusy-table div.timesheader,
#schedule-freebusy-times tr.times td {
min-width: 30px;
font-size: 9px;
padding: 5px 2px 6px 2px;
text-align: center;
color: #004658;
}
#schedule-freebusy-times tr.times td.allday {
min-width: 60px;
}
#schedule-freebusy-times tr.times td {
cursor: pointer;
}
#schedule-event-time {
position: absolute;
border: 2px solid #333;
background: #777;
background: rgba(60, 60, 60, 0.6);
opacity: 0.5;
border-radius: 4px;
cursor: move;
filter: alpha(opacity=40); /* IE8 */
}
#eventfreebusy .schedule-options {
position: relative;
margin-bottom: 1.5em;
}
#eventfreebusy .schedule-buttons {
position: absolute;
top: 0.5em;
right: 0;
margin-right: 0;
}
#eventfreebusy .schedule-find-buttons {
padding-bottom:0.5em;
}
#eventfreebusy .schedule-find-buttons button {
min-width: 9em;
text-align: center;
}
a.dropdown-link {
font-size: 12px;
text-decoration: none;
}
a.dropdown-link:after {
content: ' ▼';
font-size: 11px;
color: #666;
}
#eventedit .ui-tabs-panel {
min-height: 24em;
}
.ui-dialog-buttonset a.dropdown-link {
margin-right: 1em;
}
#calendarsidebar .ui-datepicker-calendar {
table-layout: fixed;
}
.ui-datepicker-calendar .ui-datepicker-week-col {
border: 0;
color: #999;
font-size: 90%;
text-align: right;
padding-right: 6px;
width: 20px;
overflow: hidden;
}
.ui-autocomplete {
max-height: 160px;
overflow-y: auto;
overflow-x: hidden;
}
.ui-autocomplete .ui-menu-item {
white-space: nowrap;
}
* html .ui-autocomplete {
height: 160px;
}
.calendarmain span.spacer {
padding-left: 3em;
}
#agendaoptions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: auto;
z-index: 200;
padding: 4px 5px;
border: 1px solid #c3c3c3;
border-top-color: #ddd;
border-bottom-color: #bbb;
border-radius: 0 0 4px 4px;
background: #ebebeb;
background: -moz-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ebebeb), color-stop(100%,#c6c6c6));
background: -o-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: -ms-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
}
#agendaoptions label {
text-shadow: 1px 1px #fff;
padding-right: 0.5em;
}
#calendar-kolabform {
position: relative;
margin: 0 -8px;
min-width: 660px;
min-height: 400px;
}
#calendar-kolabform table td.title {
font-weight: bold;
white-space: nowrap;
color: #666;
padding-right: 10px;
}
#calendar-kolabform fieldset.tab {
background: #efefef;
display: block;
margin-top: 0.5em;
padding: 0.5em 1em;
}
#resource-selection {
position: absolute;
top: 0;
left: 8px;
right: 0;
bottom: 0;
}
#resource-selection .scroller {
top: 34px;
}
#resource-dialog-left {
position: absolute;
top: 10px;
left: 0;
width: 380px;
bottom: 10px;
}
#resource-dialog-right {
position: absolute;
top: 10px;
left: 392px;
right: 8px;
bottom: 10px;
}
#resource-info {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 48%;
}
#resource-info table {
margin: 8px;
width: 97%;
}
#resource-info thead td {
background: none;
font-weight: bold;
font-size: 14px;
}
#resource-availability {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 49%;
}
#resource-freebusy-calendar {
position: absolute;
top: 33px;
left: -1px;
right: -1px;
bottom: -1px;
}
#resource-freebusy-calendar .fc-content {
top: 0;
}
#resource-freebusy-calendar .fc-content .fc-event-bg {
background: 0;
}
#resource-freebusy-calendar .fc-event.status-busy,
#resource-freebusy-calendar .status-busy .fc-event-skin {
border-color: #e26569;
background-color: #e26569;
}
#resource-freebusy-calendar .fc-event.status-tentative,
#resource-freebusy-calendar .status-tentative .fc-event-skin {
border-color: #8383fc;
background: #8383fc;
}
#resource-freebusy-calendar .fc-event.status-outofoffice,
#resource-freebusy-calendar .status-outofoffice .fc-event-skin {
border-color: #fbaa68;
background: #fbaa68;
}
#resourcequicksearch {
padding: 4px;
background: #c7e3ef;
}
#resourcesearchbox {
width: 100%;
height: 26px;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#resourcequicksearch .iconbutton.searchoptions {
position: absolute;
top: 5px;
left: 6px;
width: 16px;
}
.searchbox .iconbutton.reset {
position: absolute;
top: 4px;
right: 1px;
}
/* fullcalendar style overrides */
.rcube-fc-content {
overflow: hidden;
border: 0;
border-radius: 4px;
box-shadow: 0 0 2px #999;
-o-box-shadow: 0 0 2px #999;
-webkit-box-shadow: 0 0 2px #999;
-moz-box-shadow: 0 0 2px #999;
}
.calendarmain .fc-content {
position: absolute !important;
top: 40px;
left: 0;
right: 0;
bottom: 0;
background: #fff;
}
#fish-eye-view .fc-content {
top: 2px;
bottom: 2px;
}
.calendarmain .fc-button,
.calendarmain .fc-button.fc-state-hover,
.calendarmain .fc-button.fc-state-down {
border: 0;
background: none;
}
.calendarmain .fc-state-default .fc-button-inner,
.calendarmain .fc-state-hover .fc-button-inner {
margin: 0 0 0 0;
color: #505050;
text-shadow: 0px 1px 1px #fff;
border: 1px solid #e6e6e6;
background: #d8d8d8;
background: -moz-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d8d8d8), color-stop(100%,#bababa));
background: -o-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: -ms-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: linear-gradient(top, #d8d8d8 0%, #bababa 100%);
box-shadow: 0 1px 1px 0 #999;
-o-box-shadow: 0 1px 1px 0 #999;
-webkit-box-shadow: 0 1px 1px 0 #999;
-moz-box-shadow: 0 1px 1px 0 #999;
text-decoration: none;
}
.calendarmain .fc-state-disabled .fc-button-inner {
color: #999;
background: #d8d8d8;
}
.calendarmain .fc-header .fc-button {
margin-left: -1px;
margin-right: 0;
}
.calendarmain .fc-state-down .fc-button-inner {
margin: 0;
background: #bababa;
background: -moz-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bababa), color-stop(100%,#d8d8d8));
background: -o-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -ms-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: linear-gradient(top, #bababa 0%, #d8d8d8 100%);
}
.calendarmain .fc-state-active .fc-button-inner {
color: #333;
background: #bababa;
background: -moz-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bababa), color-stop(100%,#d8d8d8));
background: -o-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -ms-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: linear-gradient(top, #bababa 0%, #d8d8d8 100%);
}
.calendarmain .fc-corner-left .fc-button-inner,
.calendarmain .fc-corner-left .fc-button-content {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.calendarmain .fc-corner-right .fc-button-inner,
.calendarmain .fc-corner-right .fc-button-content {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.calendarmain .fc-button-effect {
display: none;
}
.calendarmain .fc-button-content {
height: 20px;
line-height: 20px;
}
.calendarmain .fc-header-left .fc-button {
margin: -7px 0 0 0;
padding: 0;
}
.calendarmain .fc-header-left .fc-state-default .fc-button-inner,
.calendarmain .fc-header-left .fc-state-hover .fc-button-inner {
margin: 0;
text-align: center;
font-size: 10px;
color: #555;
min-width: 50px;
max-width: 75px;
height: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 28px 2px 0 2px;
text-shadow: 0px 1px 1px #EEE;
border: 0;
background: url(images/toolbar.png) center 100px no-repeat;
box-shadow: none;
-o-box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
}
.calendarmain .fc-header-left .fc-button-content {
float: none;
line-height: 1em;
padding: 0;
}
.calendarmain .fc-header-left .fc-state-active .fc-button-content {
font-weight: bold;
color: #222;
}
.calendarmain .fc-header-left .fc-button-agendaDay .fc-button-inner {
background-position: center -120px;
}
.calendarmain .fc-header-left .fc-button-agendaDay.fc-state-active .fc-button-inner {
background-position: center -160px;
}
.calendarmain .fc-header-left .fc-button-agendaWeek .fc-button-inner {
background-position: center -200px;
}
.calendarmain .fc-header-left .fc-button-agendaWeek.fc-state-active .fc-button-inner {
background-position: center -240px;
}
.calendarmain .fc-header-left .fc-button-month .fc-button-inner {
background-position: center -280px;
}
.calendarmain .fc-header-left .fc-button-month.fc-state-active .fc-button-inner {
background-position: center -320px;
}
.calendarmain .fc-header-left .fc-button-table .fc-button-inner {
background-position: center -360px;
}
.calendarmain .fc-header-left .fc-button-table.fc-state-active .fc-button-inner {
background-position: center -400px;
}
.calendarmain .fc-header-right {
padding-right: 252px;
padding-top: 4px;
}
.fc-header-title {
padding-top: 5px;
}
.fc-event {
font-size: 1em !important;
}
.fc-event-title {
font-weight: bold;
}
.fc-event-hori .fc-event-title {
font-weight: normal;
white-space: nowrap;
}
.fc-event-hori .fc-event-time {
white-space: nowrap;
font-weight: normal !important;
font-size: 10px;
padding-right: 0.6em;
}
.fc-grid .fc-event-time {
font-weight: normal !important;
padding-right: 0.3em;
}
.fc-event-cateories {
font-style:italic;
}
div.fc-event-location {
font-size: 90%;
}
.fc-more-link {
color: #999;
padding-top: 1px;
cursor: pointer;
}
.fc-agenda-slots td div {
height: 22px;
}
.fc-sat, .fc-sun {
background-color: #fdfdfd;
}
.fc-widget-header {
background-color: #d6eaf3;
color: #004458;
text-shadow: 0px 1px 1px #fff;
}
.fc-view thead th.fc-widget-header {
padding: 8px 0;
color: #69939e;
}
.fc-day-number {
color: #578da5;
}
.fc-icon-alarms,
.fc-icon-sensitive,
.fc-icon-recurring {
display: inline-block;
width: 11px;
height: 11px;
background: url(images/eventicons.png) 0 0 no-repeat;
margin-left: 3px;
line-height: 10px;
}
.fc-icon-alarms {
background-position: 0 -13px;
}
.fc-icon-sensitive {
background-position: 0 -25px;
}
.fc-list-section .fc-event {
cursor: pointer;
}
.calendarmain .fc-view-table td.fc-list-header {
color: #004458;
font-size: 12px;
}
.calendarmain .fc-view-table tr.fc-event td {
border-color: #ddd;
padding: 4px 7px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.calendarmain .fc-view-table tr.fc-event td.fc-event-handle {
padding: 5px 0 2px 7px;
width: 12px;
}
.calendarmain .fc-view-table .fc-event-handle .fc-event-skin {
margin: 0;
padding: 0;
display: inline-block;
width: 10px;
height: 10px;
font-size: 6px;
border-radius: 8px;
}
.calendarmain .fc-view-table .fc-event-handle .fc-event-inner {
display: inline-block;
width: 10px;
height: 10px;
padding: 0;
font-size: 10px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.4);
-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
-moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
.calendarmain .fc-view-table col.fc-event-location {
width: 25%;
}
.fc-view-table table.fc-list-smart {
/* table-layout: auto; */
}
.fc-listappend {
text-align: center;
margin: 1em 0;
}
.fc-listappend .message {
padding: 0.5em;
margin-bottom: 0.5em;
font-size: 150%;
color: #999;
}
.fc-listappend .formlinks a {
font-size: 12px;
padding: 0 0.3em;
}
.fc-event-temp {
opacity: 0.4;
filter: alpha(opacity=40); /* IE8 */
}
/* Settings section */
fieldset #calendarcategories div {
margin-bottom: 0.3em;
}
/* Invitation UI in mail */
-#messagemenu li a.calendarlink span.calendar {
+#messagemenu li a.calendarlink span.calendar,
+#attachmentmenu li a.calendarlink span.calendar {
background-position: 0px -2197px;
}
div.calendar-invitebox {
min-height: 20px;
margin: 5px 8px;
padding: 3px 6px 6px 34px;
border: 1px solid #ffdf0e;
background: url(images/calendar.png) 6px 5px no-repeat #fef893;
}
div.calendar-invitebox td.ititle {
font-weight: bold;
padding-right: 0.5em;
}
div.calendar-invitebox td.label {
color: #666;
padding-right: 1em;
}
#event-rsvp .rsvp-buttons,
div.calendar-invitebox .itip-buttons div {
margin-top: 0.5em;
}
#event-rsvp input.button,
div.calendar-invitebox input.button {
font-weight: bold;
margin-right: 0.5em;
}
div.calendar-invitebox .folder-select {
font-weight: 10px;
margin-left: 1em;
}
div.calendar-invitebox .rsvp-status {
padding-left: 2px;
}
div.calendar-invitebox .rsvp-status.loading {
color: #666;
padding: 1px 0 2px 24px;
background: url(images/loading_blue.gif) top left no-repeat;
}
div.calendar-invitebox .rsvp-status.hint {
color: #666;
text-shadow: none;
font-style: italic;
}
div.calendar-invitebox .rsvp-status.declined,
div.calendar-invitebox .rsvp-status.tentative,
div.calendar-invitebox .rsvp-status.accepted,
div.calendar-invitebox .rsvp-status.delegated {
padding: 0 0 1px 22px;
background: url(images/attendee-status.png) 2px -20px no-repeat;
}
div.calendar-invitebox .rsvp-status.declined {
background-position: 2px -40px;
}
div.calendar-invitebox .rsvp-status.tentative {
background-position: 2px -60px;
}
div.calendar-invitebox .rsvp-status.delegated {
background-position: 2px -180px;
}
/* iTIP attend reply page */
.calendaritipattend .centerbox {
width: 40em;
min-height: 7em;
margin: 80px auto 0 auto;
padding: 10px 10px 10px 90px;
background: url(images/invitation.png) 10px 10px no-repeat #fff;
}
.calendaritipattend #message {
width: 46em;
margin: 0 auto;
padding: 10px;
}
.calendaritipattend .calendar-invitebox {
background: none;
padding-left: 0;
border: 0;
margin: 0 0 2em 0;
}
.calendaritipattend .calendar-invitebox .rsvp-status {
margin-top: 2.5em;
font-size: 110%;
font-weight: bold;
}
.calendaritipattend .calendar-invitebox td.title,
.calendaritipattend .calendar-invitebox td.ititle {
font-size: 120%;
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Aug 25, 2:14 PM (1 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
257653
Default Alt Text
(178 KB)

Event Timeline